ฐานของ Bazel Code

รายงานปัญหา ดูแหล่งที่มา /3} /4} {3/4} {3/4} {3/4} {3/4} /4.

เอกสารฉบับนี้เป็นคำอธิบายฐานของโค้ดและโครงสร้างของ Bazel เอกสารนี้มีไว้สำหรับผู้ที่ต้องการมีส่วนร่วมกับ Bazel ไม่ใช่สำหรับผู้ใช้ปลายทาง

เกริ่นนำ

ฐานของโค้ด Bazel มีขนาดใหญ่ (รหัสการผลิตประมาณ 350KLOC และรหัสทดสอบ KLOC ประมาณ 260 ตัว) และไม่มีใครคุ้นเคยกับภูมิทัศน์ทั้งหมด ทุกคนรู้จักหุบเขาของตัวเองดี แต่มีเพียงไม่กี่คนเท่านั้นที่รู้ว่ามีอะไรเหนือเนินเขาในทุกทิศทาง

เอกสารนี้จึงพยายามแสดงภาพรวมของฐานของโค้ดเพื่อให้ง่ายต่อการเริ่มต้นใช้งาน

ซอร์สโค้ดเวอร์ชันสาธารณะของ Bazel มีอยู่ใน GitHub ที่ github.com/bazelbuild/bazel นี่ไม่ใช่ "แหล่งข้อมูลที่ถูกต้อง" แต่ได้มาจากแผนผังแหล่งที่มาภายใน Google ซึ่งมีฟังก์ชันการทำงานเพิ่มเติมที่ไม่เป็นประโยชน์ภายนอก Google เป้าหมายระยะยาวคือการทำให้ GitHub เป็นแหล่งข้อมูลที่เชื่อถือได้

การสนับสนุนได้รับการยอมรับผ่านกลไกการดึง GitHub ปกติและนำเข้าด้วยตนเองโดย Googler ไปยังโครงสร้างแหล่งที่มาภายใน จากนั้นจึงส่งออกกลับไปยัง GitHub อีกครั้ง

สถาปัตยกรรมไคลเอ็นต์/เซิร์ฟเวอร์

กลุ่ม Bazel จำนวนมากอยู่ในกระบวนการของเซิร์ฟเวอร์ที่ยังอยู่ใน RAM ระหว่างบิลด์ต่างๆ วิธีนี้ช่วยให้ Bazel รักษาสถานะระหว่างบิลด์ได้

และนี่คือเหตุผลที่บรรทัดคำสั่ง Bazel มีตัวเลือก 2 ประเภท ได้แก่ การเริ่มต้นและคำสั่ง ในบรรทัดคำสั่งแบบนี้

    bazel --host_jvm_args=-Xmx8G build -c opt //foo:bar

ตัวเลือกบางอย่าง (--host_jvm_args=) อยู่ก่อนชื่อคำสั่งที่จะเรียกใช้ และบางตัวเลือกอยู่หลัง (-c opt) ตัวเลือกก่อนหน้านี้เรียกว่า "startup options" และส่งผลต่อกระบวนการของเซิร์ฟเวอร์โดยรวม ส่วน "ตัวเลือกคำสั่ง" จะมีผลเฉพาะกับคำสั่งเดี่ยวๆ เท่านั้น

อินสแตนซ์เซิร์ฟเวอร์แต่ละรายการมีโครงสร้างต้นทางที่เชื่อมโยงเดียว ("พื้นที่ทำงาน") และพื้นที่ทำงานแต่ละแห่งมักจะมีอินสแตนซ์เซิร์ฟเวอร์ที่ใช้งานอยู่เพียงรายการเดียว สามารถหลีกเลี่ยงได้โดยการระบุฐานเอาต์พุตที่กำหนดเอง (ดูข้อมูลเพิ่มเติมที่ส่วน "การจัดวางไดเรกทอรี")

Bazel ได้รับการแจกจ่ายเป็นไฟล์ปฏิบัติการ ELF ไฟล์เดียว ซึ่งเป็นไฟล์ .zip ที่ถูกต้องด้วย เมื่อคุณพิมพ์ bazel ไฟล์ปฏิบัติการ ELF ข้างต้นที่นำไปใช้ใน C++ ("ไคลเอ็นต์") จะควบคุมได้ โดยจะตั้งค่ากระบวนการของเซิร์ฟเวอร์ที่เหมาะสมโดยใช้ขั้นตอนต่อไปนี้

  1. ตรวจสอบว่ามีการแยกตัวเองแล้วหรือไม่ แต่หากไม่เป็นเช่นนั้น ซึ่งเป็นที่มาของการติดตั้งใช้งานเซิร์ฟเวอร์
  2. ตรวจสอบว่ามีอินสแตนซ์เซิร์ฟเวอร์ที่ใช้งานอยู่ที่ทำงานได้ไหม อินสแตนซ์นี้ทำงานอยู่ มีตัวเลือกการเริ่มต้นที่ถูกต้อง และใช้ไดเรกทอรีพื้นที่ทำงานที่ถูกต้อง โดยจะหาเซิร์ฟเวอร์ที่กำลังทำงานอยู่โดยดูที่ไดเรกทอรี $OUTPUT_BASE/server ซึ่งมีไฟล์ล็อกซึ่งมีพอร์ตที่เซิร์ฟเวอร์กำลังฟังอยู่
  3. ปิดกระบวนการของเซิร์ฟเวอร์เก่าหากจำเป็น
  4. หากจำเป็น ให้เริ่มกระบวนการของเซิร์ฟเวอร์ใหม่

หลังจากกระบวนการของเซิร์ฟเวอร์ที่เหมาะสมพร้อมแล้ว ระบบจะสื่อสารคำสั่งที่ต้องเรียกใช้ผ่านอินเทอร์เฟซ gRPC จากนั้นเอาต์พุตของ Bazel จะถูกเชื่อมต่อกลับไปยังเทอร์มินัล เรียกใช้คำสั่งได้ครั้งละ 1 รายการเท่านั้น ซึ่งทำได้โดยใช้กลไกล็อกที่ซับซ้อนซึ่งมีส่วนต่างๆ ใน C++ และส่วนต่างๆ ใน Java มีโครงสร้างพื้นฐานสำหรับการเรียกใช้คำสั่งหลายรายการพร้อมกัน เนื่องจากการเรียกใช้ bazel version พร้อมกันกับคำสั่งอื่นไม่ได้เป็นสิ่งที่น่าอาย ตัวบล็อกหลักคือวงจรชีวิตของ BlazeModule และบางสถานะใน BlazeRuntime

ในตอนท้ายของคำสั่ง เซิร์ฟเวอร์ Bazel จะส่งโค้ดสำหรับออกที่ไคลเอ็นต์ควรส่งคืน ส่วนเล็กๆ ที่น่าสนใจคือการนำ bazel run มาใช้ หน้าที่ของคำสั่งนี้คือการเรียกใช้สิ่งที่ Bazel เพิ่งสร้างขึ้น แต่ไม่สามารถทำได้จากกระบวนการของเซิร์ฟเวอร์เนื่องจากไม่มีเทอร์มินัล ดังนั้นจึงบอกไคลเอ็นต์ว่าควรใช้ ujexec() ไบนารีใดและอาร์กิวเมนต์ใด

เมื่อกด Ctrl-C แล้ว ไคลเอ็นต์จะแปลผลเป็นการเรียกใช้ "Cancel" ในการเชื่อมต่อ gRPC ซึ่งจะพยายามสิ้นสุดคําสั่งโดยเร็วที่สุด หลังจาก Ctrl-C ครั้งที่ 3 ไคลเอ็นต์จะส่ง SIGKILL ไปยังเซิร์ฟเวอร์แทน

ซอร์สโค้ดของไคลเอ็นต์อยู่ภายใต้ src/main/cpp และโปรโตคอลที่ใช้สื่อสารกับเซิร์ฟเวอร์อยู่ใน src/main/protobuf/command_server.proto

จุดแรกเข้าหลักของเซิร์ฟเวอร์คือ BlazeRuntime.main() และ GrpcServerImpl.run() จะเป็นผู้จัดการการเรียก gRPC จากไคลเอ็นต์

เลย์เอาต์ไดเรกทอรี

Bazel สร้างชุดไดเรกทอรีที่ค่อนข้างซับซ้อนในระหว่างการสร้าง มีคำอธิบายแบบเต็มอยู่ในเลย์เอาต์ไดเรกทอรีเอาต์พุต

"พื้นที่ทํางาน" คือโครงสร้างต้น Bazel ที่มีการเรียกใช้ โดยปกติจะสอดคล้องกับ สิ่งที่คุณได้ตรวจสอบจากการควบคุมแหล่งที่มา

Bazel วางข้อมูลทั้งหมดไว้ภายใต้ "รูทของผู้ใช้เอาต์พุต" ซึ่งโดยปกติจะเป็น $HOME/.cache/bazel/_bazel_${USER} แต่ลบล้างได้โดยใช้ตัวเลือกการเริ่มต้น --output_user_root

"ฐานผู้ใช้งาน" คือที่ดึงข้อมูล Bazel ไป การดำเนินการนี้จะเกิดขึ้นโดยอัตโนมัติและ Bazel แต่ละเวอร์ชันจะได้รับไดเรกทอรีย่อยตามผลรวมตรวจสอบภายใต้ฐานผู้ใช้งาน ซึ่งอยู่ที่ $OUTPUT_USER_ROOT/install โดยค่าเริ่มต้นและเปลี่ยนได้โดยใช้ตัวเลือกบรรทัดคำสั่ง --install_base

"ฐานเอาต์พุต" คือตำแหน่งที่อินสแตนซ์ Bazel แนบกับพื้นที่ทำงานที่เฉพาะเจาะจงเขียนถึง ฐานเอาต์พุตแต่ละฐานจะมีอินสแตนซ์ของเซิร์ฟเวอร์ Bazel ที่ทำงานได้ทุกเมื่อสูงสุด 1 รายการ โดยทั่วไปอุณหภูมิจะอยู่ที่ $OUTPUT_USER_ROOT/<checksum of the path to the workspace> เปลี่ยนแปลงได้โดยใช้ตัวเลือกการเริ่มต้น --output_base ซึ่งมีประโยชน์ในการหลบเลี่ยงข้อจำกัดที่ว่าอินสแตนซ์ Bazel เพียง 1 รายการจะทำงานในพื้นที่ทำงานใดก็ตามในช่วงเวลาหนึ่งๆ ได้

ไดเรกทอรีเอาต์พุตจะมีสิ่งต่อไปนี้ด้วย

  • ที่เก็บภายนอกที่ดึงข้อมูลที่ $OUTPUT_BASE/external
  • รูทของ exec ซึ่งเป็นไดเรกทอรีที่มีลิงก์สัญลักษณ์ไปยังซอร์สโค้ดทั้งหมดของบิลด์ปัจจุบัน ซึ่งตั้งอยู่ที่ $OUTPUT_BASE/execroot ในระหว่างบิลด์ ไดเรกทอรีที่ใช้งานอยู่คือ $EXECROOT/<name of main repository> เราวางแผนที่จะเปลี่ยนเป็น $EXECROOT แม้ว่าจะเป็นแพ็กเกจระยะยาวเนื่องจากเป็นการเปลี่ยนแปลงที่ใช้ร่วมกันไม่ได้
  • ไฟล์ที่สร้างขึ้นระหว่างการสร้าง

กระบวนการเรียกใช้คำสั่ง

เมื่อเซิร์ฟเวอร์ Bazel ได้ควบคุมและได้รับทราบถึงคำสั่งที่ต้องใช้ในการปฏิบัติการแล้ว ลำดับเหตุการณ์ต่อไปนี้จะเกิดขึ้น

  1. BlazeCommandDispatcher ได้รับแจ้งเกี่ยวกับคำขอใหม่แล้ว โดยจะตัดสินว่าคำสั่งต้องใช้พื้นที่ทำงานหรือไม่ (แทบจะทุกคำสั่ง ยกเว้นคำสั่งที่ไม่มีส่วนเกี่ยวข้องกับซอร์สโค้ด เช่น เวอร์ชันหรือความช่วยเหลือ) และมีคำสั่งอื่นทำงานอยู่หรือไม่

  2. พบคำสั่งที่ถูกต้อง แต่ละคำสั่งต้องใช้อินเทอร์เฟซ BlazeCommand และต้องมีคำอธิบายประกอบ @Command (นี่เป็นเพียงรูปแบบหนึ่งเล็กน้อย คงจะดีหากได้อธิบายข้อมูลเมตาทั้งหมดที่คำสั่งต้องใช้ด้วยเมธอดใน BlazeCommand)

  3. ตัวเลือกบรรทัดคำสั่งจะได้รับการแยกวิเคราะห์ แต่ละคำสั่งจะมีตัวเลือกบรรทัดคำสั่งแตกต่างกัน ซึ่งอธิบายไว้ในคำอธิบายประกอบ @Command

  4. มีการสร้างรถโดยสารสำหรับกิจกรรม รถบัสสำหรับงานกิจกรรมคือกิจกรรมที่เกิดขึ้น ระหว่างการสร้าง ตัวอย่างบางรายการส่งออกไปนอก Bazel ภายใต้ Aegis ของ Build Event Protocol เพื่อบอกคนทั่วโลกว่าบิลด์นี้ทำงานอย่างไร

  5. โดยคำสั่งจะได้รับการควบคุม คำสั่งที่น่าสนใจที่สุดคือคำสั่งที่เรียกใช้บิลด์ ได้แก่ การสร้าง ทดสอบ เรียกใช้ การครอบคลุม และอื่นๆ โดย BuildTool จะเป็นผู้ใช้งานฟังก์ชันนี้

  6. ชุดรูปแบบเป้าหมายในบรรทัดคำสั่งได้รับการแยกวิเคราะห์และแก้ไขไวลด์การ์ด เช่น //pkg:all และ //pkg/... ซึ่งจะนำไปใช้ใน AnalysisPhaseRunner.evaluateTargetPatterns() และได้รับการปรับแต่งใหม่ใน Skyframe เป็น TargetPatternPhaseValue

  7. ขั้นตอนการโหลด/การวิเคราะห์จะทำงานเพื่อสร้างกราฟการดำเนินการ (กราฟแบบวนซ้ำของคำสั่งที่ต้องเรียกใช้สำหรับบิลด์)

  8. ขั้นตอนการดำเนินการจะทำงาน ซึ่งหมายความว่าการเรียกใช้การกระทำทุกอย่างที่จำเป็นเพื่อสร้างเป้าหมายระดับบนสุดที่ขอนั้นจะทำงาน

ตัวเลือกบรรทัดคำสั่ง

ตัวเลือกบรรทัดคำสั่งสำหรับการเรียกใช้ Bazel จะอธิบายไว้ในออบเจ็กต์ OptionsParsingResult ซึ่งจะมีแผนที่จาก "option classes" ไปจนถึงค่าของตัวเลือก "option class" คือคลาสย่อยของ OptionsBase และจัดกลุ่มตัวเลือกบรรทัดคำสั่งเข้าด้วยกันซึ่งเกี่ยวข้องกัน เช่น

  1. ตัวเลือกที่เกี่ยวข้องกับภาษาโปรแกรม (CppOptions หรือ JavaOptions) ตัวเลือกเหล่านี้ควรเป็นคลาสย่อยของ FragmentOptions และท้ายที่สุดจะรวมไว้ในออบเจ็กต์ BuildOptions
  2. ตัวเลือกที่เกี่ยวข้องกับวิธีที่ Bazel ดำเนินการ (ExecutionOptions)

ตัวเลือกเหล่านี้ออกแบบมาเพื่อใช้งานในช่วงการวิเคราะห์และ (ผ่าน RuleContext.getFragment() ใน Java หรือ ctx.fragments ใน Starlark) ระบบอ่านคำสั่งบางส่วน (เช่น จะใช้ C++ รวมการสแกนหรือไม่) ในขั้นตอนการดำเนินการ แต่ต้องใช้ท่อประปาทันทีเนื่องจาก BuildConfiguration ไม่พร้อมใช้งาน สำหรับข้อมูลเพิ่มเติม โปรดดูที่ ส่วน "การกำหนดค่า"

คำเตือน: เราชอบเหมือนว่าอินสแตนซ์ OptionsBase จะเปลี่ยนแปลงไม่ได้และใช้รูปแบบนั้น (เช่น เป็นส่วนหนึ่งของ SkyKeys) นี่ไม่ใช่กรณีที่เกิดขึ้นและการปรับเปลี่ยนเป็นวิธีที่ดีจริงๆ ในการทำลาย Bazel ด้วยวิธีใหม่ๆ ที่แก้ไขข้อบกพร่องได้ยาก น่าเสียดายที่การทำให้กลยุทธ์เหล่านี้เปลี่ยนแปลงไม่ได้นั้นต้องอาศัยความพยายามอย่างมาก (การแก้ไข FragmentOptions ทันทีหลังจากการก่อสร้างก่อนที่ผู้อื่นจะได้มีการอ้างอิงไว้ และก่อนที่จะมีการเรียก equals() หรือ hashCode() นั้น สามารถทำได้)

Bazel ได้เรียนรู้เกี่ยวกับคลาสตัวเลือกด้วยวิธีต่อไปนี้

  1. บางรุ่นต่อสายเข้ากับ Bazel (CommonCommandOptions)
  2. จากคำอธิบายประกอบ @Command ในคำสั่ง Bazel แต่ละรายการ
  3. ตั้งแต่ ConfiguredRuleClassProvider (ตัวเลือกเหล่านี้คือตัวเลือกบรรทัดคำสั่งที่เกี่ยวข้องกับภาษาโปรแกรมแต่ละภาษา)
  4. กฎของ Starlark ยังกำหนดตัวเลือกของตัวเองได้อีกด้วย (ดูที่นี่)

แต่ละตัวเลือก (ยกเว้นตัวเลือกที่กำหนดโดย Starlark) เป็นตัวแปรสมาชิกของคลาสย่อย FragmentOptions ที่มีคำอธิบายประกอบ @Option ซึ่งระบุชื่อและประเภทของตัวเลือกบรรทัดคำสั่งพร้อมกับข้อความช่วยเหลือบางส่วน

ประเภท Java ของค่าของตัวเลือกบรรทัดคำสั่งมักเป็นค่าที่เรียบง่าย (เช่น สตริง จำนวนเต็ม บูลีน ป้ายกำกับ ฯลฯ) อย่างไรก็ตาม เรายังรองรับตัวเลือกประเภทที่ซับซ้อนขึ้นด้วย ในกรณีนี้ การแปลงจากสตริงบรรทัดคำสั่งเป็นประเภทข้อมูลจะเป็นไปตามการใช้งาน com.google.devtools.common.options.Converter

ต้นกำเนิดอย่างที่ Bazel เห็น

Bazel ทำธุรกิจการสร้างซอฟต์แวร์ ซึ่งเกิดจากการอ่านและตีความซอร์สโค้ด จำนวนรวมของซอร์สโค้ดที่ Bazel ดำเนินการอยู่นี้เรียกว่า "พื้นที่ทำงาน" และจัดโครงสร้างให้กับที่เก็บ แพ็กเกจ และกฎต่างๆ

ที่เก็บ

"ที่เก็บ" คือแผนผังแหล่งที่มาที่นักพัฒนาซอฟต์แวร์ทำงาน โดยมักจะเป็นตัวแทนของโปรเจ็กต์เดี่ยว บรรพบุรุษของ Bazel ชื่อ Blaze ดำเนินการบนระบบเดียว นั่นคือแผนผังต้นทางเดี่ยวที่มีซอร์สโค้ดทั้งหมดที่ใช้ในการเรียกใช้บิลด์ ในทางตรงกันข้าม Bazel สนับสนุนโปรเจ็กต์ที่มีซอร์สโค้ดครอบคลุมที่เก็บหลายแห่ง ที่เก็บที่มีการเรียกใช้ Bazel เรียกว่า "ที่เก็บหลัก" ส่วนที่เก็บอื่นๆ เรียกว่า "ที่เก็บภายนอก"

ที่เก็บมีการทำเครื่องหมายโดยไฟล์ชื่อ WORKSPACE (หรือ WORKSPACE.bazel) ในไดเรกทอรีราก ไฟล์นี้มีข้อมูลที่เป็น "ส่วนกลาง" ของทั้งบิลด์ เช่น ชุดที่เก็บภายนอกที่พร้อมใช้งาน ซึ่งทำงานเหมือนไฟล์ Starlark ทั่วไป ซึ่งหมายความว่าสามารถload()ไฟล์ Starlark อื่นๆ ได้ วิธีนี้มักใช้เพื่อดึงข้อมูลในที่เก็บซึ่งที่เก็บซึ่งจำเป็นต้องมีการอ้างอิงอย่างชัดแจ้ง (เราเรียกว่า "รูปแบบ deps.bzl")

โค้ดของที่เก็บภายนอกมีการลิงก์หรือดาวน์โหลดภายใต้ $OUTPUT_BASE/external

เมื่อเรียกใช้บิลด์ โครงสร้างต้นทางทั้งหมดจะต้องรวมเข้าด้วยกัน ซึ่ง ทำโดย SymlinkForest ซึ่งจะลิงก์ทุกแพ็กเกจในที่เก็บหลักกับ $EXECROOT และที่เก็บภายนอกทั้งหมดไปยัง $EXECROOT/external หรือ $EXECROOT/.. (แน่นอนว่าการเดิมทำให้ไม่มีแพ็กเกจชื่อ external ในที่เก็บหลัก จึงเป็นเหตุผลที่เราจะย้ายข้อมูลออกจากที่เก็บดังกล่าว)

กล่องพัสดุ

ที่เก็บทั้งหมดประกอบด้วยแพ็กเกจ คอลเล็กชันของไฟล์ที่เกี่ยวข้อง และข้อกำหนดของทรัพยากร Dependency ซึ่งระบุโดยไฟล์ชื่อ BUILD หรือ BUILD.bazel หากมีทั้งคู่ Bazel ต้องการ BUILD.bazel แต่เหตุผลที่ไฟล์ BUILD ยังคงได้รับการยอมรับก็คือ Blaze ซึ่งเป็นบรรพบุรุษของ Bazel ใช้ชื่อไฟล์นี้ แต่กลายเป็นกลุ่มเส้นทางที่ใช้กันโดยทั่วไป โดยเฉพาะใน Windows ที่ชื่อไฟล์ไม่คำนึงถึงตัวพิมพ์เล็กหรือใหญ่

แพ็กเกจเป็นอิสระต่อกัน ดังนั้นการเปลี่ยนแปลงไฟล์ BUILD ของแพ็กเกจจะไม่ทำให้แพ็กเกจอื่นๆ เปลี่ยนแปลง การเพิ่มหรือนำไฟล์ BUILD ออก _can _change แพ็กเกจอื่นๆ ได้ เนื่องจาก glob ที่เกิดซ้ำจะหยุดที่ขอบเขตแพ็กเกจ และด้วยเหตุนี้การมีไฟล์ BUILD จึงจะหยุดการเกิดซ้ำ

การประเมินไฟล์ BUILD มีชื่อว่า "การโหลดแพ็กเกจ" ซึ่งมีการนำไปใช้ในชั้นเรียน PackageFactory ซึ่งทำงานโดยการเรียกใช้ล่าม Starlark และต้องการความรู้เกี่ยวกับชุดคลาสกฎที่ใช้ได้ ผลลัพธ์ของการโหลดแพ็กเกจคือออบเจ็กต์ Package โดยส่วนใหญ่จะเป็นแผนที่จากสตริง (ชื่อเป้าหมาย) ไปยังตัวเป้าหมายเอง

ความซับซ้อนอย่างมากระหว่างการโหลดแพ็กเกจคือ globbing: Bazel ไม่ได้กำหนดให้ไฟล์ต้นทางทุกไฟล์ต้องมีการระบุไว้อย่างชัดเจน และสามารถเรียกใช้ glob แทนได้ (เช่น glob(["**/*.java"])) ซึ่งต่างจาก Shell ตรงที่รองรับ globing ที่เกิดซ้ำซึ่งลงไปยังไดเรกทอรีย่อย (แต่ไม่ใช่ในแพ็กเกจย่อย) การดำเนินการนี้ต้องเข้าถึงระบบไฟล์ และเนื่องจากอาจทำได้ช้า เราจึงใช้กลอุบายทั้งหมดเพื่อทำให้ระบบทำงานพร้อมกันและมีประสิทธิภาพมากที่สุด

มีการใช้ Globbing ในชั้นเรียนต่อไปนี้

  • LegacyGlobber ผู้เล่นดาวเคราะห์ Skyframe ที่มีความสุขและไม่รู้จักเร็ว
  • SkyframeHybridGlobber ซึ่งเป็นเวอร์ชันที่ใช้ Skyframe และเปลี่ยนกลับไปใช้ globber แบบเดิมเพื่อหลีกเลี่ยง “การรีสตาร์ท Skyframe” (ตามที่อธิบายไว้ด้านล่าง)

คลาส Package เองก็มีสมาชิกบางรายที่ใช้เฉพาะในการแยกวิเคราะห์ไฟล์ WORKSPACE ซึ่งไม่เหมาะสำหรับแพ็กเกจจริง ซึ่งเป็นข้อบกพร่องในการออกแบบเนื่องจากออบเจ็กต์ที่อธิบายแพ็กเกจปกติไม่ควรมีฟิลด์ที่อธิบายอย่างอื่น ซึ่งรวมถึงการใช้งานดังต่อไปนี้

  • การแมปที่เก็บ
  • Toolchains ที่จดทะเบียน
  • แพลตฟอร์มการดำเนินการที่ลงทะเบียนไว้

ตามหลักการแล้ว ควรแยกไฟล์ WORKSPACE ออกจากการแยกวิเคราะห์แพ็กเกจปกติเพื่อให้ Packageไม่ต้องตอบสนองความต้องการของทั้ง 2 ไฟล์ แต่น่าเสียดายที่ทั้ง 2 อย่างนี้สอดประสานกันอย่างลึกซึ้ง

ป้ายกำกับ เป้าหมาย และกฎ

แพ็กเกจประกอบด้วยเป้าหมายในประเภทต่อไปนี้

  1. ไฟล์: สิ่งต่างๆ ที่เป็นอินพุตหรือเอาต์พุตของบิลด์ ในคำอธิบายบาเซล เราเรียกสิ่งนี้ว่าสิ่งประดิษฐ์ (มีอภิปรายกันที่อื่น) ไฟล์ทั้งหมดที่สร้างขึ้นระหว่างบิลด์จะไม่ได้เป็นเป้าหมาย เป็นเรื่องปกติที่เอาต์พุตของ Bazel จะไม่มีป้ายกำกับที่เชื่อมโยง
  2. กฎ: ส่วนนี้จะอธิบายขั้นตอนการดึงเอาต์พุตจากอินพุต โดยทั่วไปมักเชื่อมโยงกับภาษาโปรแกรม (เช่น cc_library, java_library หรือ py_library) แต่ก็มีบางภาษาที่เข้าใจได้โดยไม่จำเป็นต้องเข้าใจภาษาที่พูด (เช่น genrule หรือ filegroup)
  3. กลุ่มแพ็กเกจ: อธิบายในหัวข้อระดับการเข้าถึง

โดยชื่อของเป้าหมายจะเรียกว่าป้ายกำกับ ไวยากรณ์ของป้ายกำกับคือ @repo//pac/kage:name โดยที่ repo คือชื่อของที่เก็บป้ายกำกับนั้น pac/kage คือไดเรกทอรีที่ไฟล์ BUILD อยู่ และ name คือเส้นทางของไฟล์ (หากป้ายกำกับอ้างอิงไฟล์ต้นฉบับ) ซึ่งสัมพันธ์กับไดเรกทอรีของแพ็กเกจ เมื่อพูดถึงเป้าหมายในบรรทัดคำสั่ง บางส่วนของป้ายกำกับจะละเว้นได้ ดังนี้

  1. หากไม่ระบุที่เก็บ ระบบจะนำป้ายกำกับไปอยู่ในที่เก็บหลัก
  2. หากละเว้นส่วนแพ็กเกจ (เช่น name หรือ :name) ระบบจะนำป้ายกำกับไปอยู่ในแพ็กเกจของไดเรกทอรีที่ใช้งานอยู่ปัจจุบัน (ไม่อนุญาตให้ใช้เส้นทางแบบสัมพัทธ์ที่มีการอ้างอิงระดับบน (..))

ชนิดของกฎ (เช่น "ไลบรารี C++") จะเรียกว่า "คลาสกฎ" คุณสามารถใช้คลาสของกฎได้ใน Starlark (ฟังก์ชัน rule()) หรือใน Java (ซึ่งจะเรียกว่า “กฎเนทีฟ” ประเภท RuleClass) ในระยะยาว กฎเฉพาะภาษาทุกกฎจะใช้ใน Starlark แต่กลุ่มกฎเดิมบางตระกูล (เช่น Java หรือ C++) จะยังคงอยู่ใน Java ในขณะนี้

ต้องนำเข้าคลาสกฎ Starlark ที่จุดเริ่มต้นของไฟล์ BUILD โดยใช้คำสั่ง load() ในขณะที่คลาสของกฎ Java จะรู้จักโดย Bazel จากการลงทะเบียนด้วย ConfiguredRuleClassProvider

คลาสของกฎจะมีข้อมูลต่างๆ เช่น

  1. แอตทริบิวต์ (เช่น srcs, deps): ประเภท ค่าเริ่มต้น ข้อจำกัด ฯลฯ
  2. การเปลี่ยนการกำหนดค่าและลักษณะที่แนบมากับแอตทริบิวต์แต่ละรายการ หากมี
  3. การใช้กฎ
  4. ผู้ให้บริการข้อมูลทรานซิทีฟที่กฎ "โดยปกติ" สร้างขึ้น

หมายเหตุคำศัพท์: ในฐานของโค้ด เรามักจะใช้ "กฎ" หมายถึงเป้าหมายที่สร้างโดยคลาสกฎ แต่ใน Starlark และในเอกสารที่แสดงต่อผู้ใช้ ควรใช้ "กฎ" เพื่ออ้างถึงคลาสของกฎโดยเฉพาะ เป้าหมายจะเป็นเพียง "เป้าหมาย" และโปรดทราบว่าแม้ RuleClass จะมี "class" ในชื่อ แต่ไม่มีความสัมพันธ์การสืบทอดค่า Java ระหว่างคลาสกฎและเป้าหมายประเภทดังกล่าว

สกายเฟรม

กรอบการประเมินที่อยู่เบื้องหลัง Bazel เรียกว่า Skyframe โมเดลของโมเดลก็คือ ทุกสิ่งที่จำเป็นต้องสร้างระหว่างบิลด์จะได้รับการจัดระเบียบเป็นกราฟแบบวนซ้ำที่มีเส้นตรงโดยมีขอบชี้จากข้อมูลชิ้นใดก็ตามไปยังทรัพยากร Dependency ก็คือข้อมูลอื่นๆ ที่ต้องทราบในการสร้างข้อมูล

โหนดในกราฟจะเรียกว่า SkyValue และชื่อของโหนดจะเรียกว่า SkyKey ทั้ง 2 อย่างนี้จะเปลี่ยนแปลงไม่ได้ มีเพียงวัตถุที่เปลี่ยนแปลงไม่ได้เท่านั้นที่เข้าถึงได้ ค่าคงที่นี้จะระงับไว้เกือบทุกครั้ง และในกรณีที่ไม่เป็นเช่นนั้น (เช่น สำหรับคลาสตัวเลือกแต่ละรายการ BuildOptions ซึ่งเป็นสมาชิกของ BuildConfigurationValue และ SkyKey) เราพยายามอย่างมากที่จะไม่เปลี่ยนหรือเปลี่ยนแปลงตัวเลือกเฉพาะในลักษณะที่ไม่สามารถสังเกตได้จากภายนอก หลังจากนั้นทุกอย่างที่คํานวณภายใน Skyframe (เช่น เป้าหมายที่กำหนดค่า) ก็จะต้องเปลี่ยนแปลงไม่ได้เช่นกัน

วิธีสังเกตกราฟ Skyframe ที่สะดวกที่สุดคือการเรียกใช้ bazel dump --skyframe=detailed ซึ่งจะทิ้งกราฟ 1 SkyValue ต่อบรรทัด วิธีที่ดีที่สุดคือสร้าง กับงานสร้างขนาดเล็กๆ เพราะอาจมีขนาดใหญ่พอสมควร

Skyframe อยู่ในแพ็กเกจ com.google.devtools.build.skyframe แพ็กเกจ com.google.devtools.build.lib.skyframe ที่มีชื่อคล้ายกันมีการติดตั้งใช้งาน Bazel ที่ด้านบนของ Skyframe ดูข้อมูลเพิ่มเติมเกี่ยวกับ Skyframe ได้ที่นี่

ในการประเมิน SkyKey ที่ระบุลงใน SkyValue Skyframe จะเรียกใช้ SkyFunction ที่สอดคล้องกับประเภทของคีย์ ในระหว่างการประเมินฟังก์ชัน แอปอาจขอทรัพยากร Dependency อื่นๆ จาก Skyframe โดยเรียกใช้การโอเวอร์โหลดต่างๆ ของ SkyFunction.Environment.getValue() การดำเนินการนี้จะส่งผลข้างเคียงของการลงทะเบียนทรัพยากร Dependency เหล่านั้นลงในกราฟภายในของ Skyframe ดังนั้น Skyframe จะทราบว่าต้องประเมินฟังก์ชันอีกครั้งเมื่อทรัพยากร Dependency มีการเปลี่ยนแปลง กล่าวคือ การแคชและการคำนวณที่เพิ่มขึ้นของ Skyframe จะทำงานที่ความละเอียด SkyFunction และ SkyValue

เมื่อใดก็ตามที่ SkyFunction ขอทรัพยากร Dependency ที่ไม่พร้อมใช้งาน getValue() จะแสดงผลเป็น Null ฟังก์ชันดังกล่าวควรแสดงผลการควบคุมกลับไปยัง Skyframe โดยการส่งคืนค่า Null ในภายหลัง Skyframe จะประเมินทรัพยากร Dependency ที่ไม่พร้อมใช้งาน จากนั้นรีสตาร์ทฟังก์ชันตั้งแต่ต้น และมีเพียงการเรียกใช้ getValue() เท่านั้นที่จะสำเร็จโดยมีผลลัพธ์ที่ไม่เป็นค่าว่าง

ผลที่ตามมาก็คือการคำนวณใดๆ ที่ดำเนินการภายใน SkyFunction ก่อนที่จะรีสตาร์ทจะต้องทำซ้ำ แต่ไม่รวมงานที่ทำเพื่อประเมินทรัพยากร Dependency SkyValues ซึ่งแคชไว้ ดังนั้น โดยทั่วไปเราจึงแก้ไขปัญหานี้ ได้ดังนี้

  1. การประกาศทรัพยากร Dependency เป็นกลุ่ม (โดยใช้ getValuesAndExceptions()) เพื่อจำกัดจำนวนการรีสตาร์ท
  2. การแบ่ง SkyValue ออกเป็นชิ้นส่วนต่างๆ ที่คํานวณโดย SkyFunction ที่แตกต่างกันเพื่อให้คํานวณและแคชได้อย่างอิสระ ซึ่งเป็นสิ่งที่ควรทำอย่างมีกลยุทธ์ เนื่องจากมีโอกาสเพิ่มการใช้หน่วยความจำ
  3. จัดเก็บสถานะระหว่างการรีสตาร์ท โดยใช้ SkyFunction.Environment.getState() หรือเก็บแคชเฉพาะกิจแบบ "ข้างหลัง Skyframe"

โดยพื้นฐานแล้ว เราต้องใช้วิธีแก้ปัญหาประเภทนี้ เนื่องจากเรามีโหนด Skyframe บนเครื่องบินหลายแสนโหนดอยู่เป็นแสนๆ โหนด และ Java ไม่รองรับชุดข้อความขนาดเล็ก

สตาร์ลาร์ก

Starlark เป็นภาษาเฉพาะโดเมนที่ผู้คนใช้เพื่อกำหนดค่าและขยาย Bazel โดยถือว่าเป็นชุดย่อยที่ถูกจำกัดของ Python ซึ่งมีประเภทน้อยกว่ามาก มีข้อจำกัดในการควบคุมมากกว่า และที่สำคัญที่สุดคือการรับประกันว่า Python จะไม่สามารถอ่านได้พร้อมกันอย่างแข็งแกร่ง โซลูชันนี้ไม่ใช่ Turing-complete ซึ่งไม่สนับสนุนให้ผู้ใช้บางส่วน (แต่ไม่ใช่ทั้งหมด) พยายามทำงานการเขียนโปรแกรมทั่วไปในภาษานั้นๆ

มีการใช้งาน Starlark ในแพ็กเกจ net.starlark.java นอกจากนี้ยังมีการใช้งาน Go แบบอิสระที่นี่ การใช้งาน Java ที่ใช้ใน Bazel เป็นอินเทอร์พรีเตอร์ในปัจจุบัน

Starlark มีการใช้ในบริบทต่างๆ ได้แก่

  1. ภาษา BUILD นี่คือที่ที่มีการกำหนดกฎใหม่ โค้ด Starlark ที่ทำงานในบริบทนี้มีสิทธิ์เข้าถึงเฉพาะเนื้อหาของไฟล์ BUILD และไฟล์ .bzl ที่ตัวไฟล์โหลดไว้เท่านั้น
  2. คำจำกัดความของกฎ นี่คือวิธีกำหนดกฎใหม่ (เช่น การรองรับภาษาใหม่) โค้ด Starlark ที่เรียกใช้ในบริบทนี้มีสิทธิ์เข้าถึงการกำหนดค่าและข้อมูลที่ระบุโดยทรัพยากร Dependency โดยตรง (เราจะอธิบายเพิ่มเติมในภายหลัง)
  3. ไฟล์ WORKSPACE ซึ่งเป็นการกำหนดที่เก็บภายนอก (โค้ดที่ไม่ได้อยู่ในแผนผังแหล่งที่มาหลัก)
  4. คำจำกัดความของกฎที่เก็บ ในที่นี้จะกำหนดประเภทที่เก็บภายนอกใหม่ โค้ด Starlark ที่ทำงานในบริบทนี้สามารถเรียกใช้โค้ดที่กำหนดเองบนเครื่องที่ Bazel ทำงานอยู่ และเข้าถึงนอกพื้นที่ทำงานได้

ภาษาถิ่นที่ใช้กับไฟล์ BUILD และ .bzl จะแตกต่างกันเล็กน้อยเนื่องจากแสดงข้อมูลที่แตกต่างกัน ดูรายการความแตกต่างได้ที่นี่

ดูข้อมูลเพิ่มเติมเกี่ยวกับ Starlark ได้ที่นี่

ระยะการโหลด/การวิเคราะห์

ขั้นตอนการโหลด/การวิเคราะห์คือจุดที่ Bazel จะพิจารณาว่าต้องดำเนินการใดบ้างเพื่อสร้างกฎหนึ่งๆ หน่วยพื้นฐานของ คือ "เป้าหมายที่กำหนดค่า" ซึ่งเป็นคู่ (เป้าหมาย การกำหนดค่า) ที่ค่อนข้างสมเหตุสมผล

เราเรียกขั้นตอนนี้ว่า "ระยะการโหลด/การวิเคราะห์" เนื่องจากสามารถแบ่งออกเป็น 2 ส่วนที่แตกต่างกัน ซึ่งก่อนหน้านี้เคยมีการเรียงลำดับ แต่ปัจจุบันสามารถซ้อนทับกันได้ในปัจจุบัน ดังนี้

  1. การโหลดแพ็กเกจ กล่าวคือ การเปลี่ยนไฟล์ BUILD ไฟล์เป็นออบเจ็กต์ Package ที่แสดงแพ็กเกจดังกล่าว
  2. การวิเคราะห์เป้าหมายที่กำหนดค่าไว้ กล่าวคือ เรียกใช้การใช้งานกฎเพื่อสร้างกราฟการดำเนินการ

เป้าหมายที่กำหนดค่าไว้แต่ละรายการในการปิดแบบทรานซิทีฟของเป้าหมายที่กำหนดค่าไว้ที่ขอในบรรทัดคำสั่งจะได้รับการวิเคราะห์จากด้านล่างขึ้นบน กล่าวคือ ต้องวิเคราะห์โหนด Leaf ก่อน จากนั้นจึงจะวิเคราะห์จนถึงโหนดในบรรทัดคำสั่ง อินพุตของการวิเคราะห์เป้าหมาย ที่กำหนดค่าเดียวคือ

  1. การกำหนดค่า ("วิธี" สร้างกฎ เช่น แพลตฟอร์มเป้าหมาย รวมถึงตัวเลือกบรรทัดคำสั่งที่ผู้ใช้ต้องการส่งผ่านไปยังคอมไพเลอร์ C++)
  2. ทรัพยากร Dependency โดยตรง ผู้ให้บริการข้อมูลทางอ้อม จะพร้อมใช้งานกับกฎที่วิเคราะห์ ซึ่งจะเรียกแบบนั้น เนื่องจากมี "ภาพรวม" ของข้อมูลการปิดแบบทรานซิทีฟของเป้าหมายที่กำหนดค่าไว้ เช่น ไฟล์ .jar ทั้งหมดในคลาสพาธหรือไฟล์ .o ทั้งหมดที่ต้องลิงก์กับไบนารี C++)
  3. ตัวเป้าหมาย นี่เกิดจากการโหลดแพ็กเกจที่มีเป้าหมายอยู่ สำหรับกฎ เกณฑ์นี้รวมถึงแอตทริบิวต์ของกฎต่างๆ ซึ่งมักเป็นสิ่งที่สำคัญ
  4. การใช้งานเป้าหมายที่กำหนดค่า สำหรับกฎ การกำหนดสถานะนี้จะอยู่ใน Starlark หรือ Java เป้าหมายที่กำหนดค่าที่ไม่ใช่กฎทั้งหมดจะนำไปใช้ใน Java

เอาต์พุตของการวิเคราะห์เป้าหมายที่กำหนดค่าไว้คือ

  1. ผู้ให้บริการข้อมูลทรานซิทีฟที่กำหนดค่าเป้าหมายซึ่งอาศัยข้อมูลดังกล่าวสามารถเข้าถึงได้
  2. อาร์ติแฟกต์ที่ AI สร้างขึ้นได้และการดำเนินการที่สร้างอาร์ติแฟกต์เหล่านี้

API ที่เสนอให้กับกฎ Java คือ RuleContext ซึ่งเทียบเท่ากับอาร์กิวเมนต์ ctx ของกฎ Starlark API ของ API มีประสิทธิภาพมากขึ้น แต่ขณะเดียวกัน การทำ Bad ThingsTM จะทำได้ง่ายขึ้น เช่น เขียนโค้ดที่มีเวลาหรือพื้นที่ซับซ้อนเป็นกำลังสอง (หรือแย่กว่า) เพื่อทำให้เซิร์ฟเวอร์ Bazel ขัดข้องด้วยข้อยกเว้น Java หรือละเมิดค่าคงที่ (เช่น ด้วยการแก้ไขอินสแตนซ์ Options โดยไม่ได้ตั้งใจ หรือโดยการเปลี่ยนแปลงเป้าหมายที่กำหนดค่าไว้)

อัลกอริทึมที่กำหนดทรัพยากร Dependency โดยตรงของเป้าหมายที่กำหนดค่าแล้วจะทำงานใน DependencyResolver.dependentNodeMap()

การกำหนดค่า

การกำหนดค่าคือ "วิธีการ" ของการสร้างเป้าหมาย สำหรับแพลตฟอร์มใด พร้อมตัวเลือกบรรทัดคำสั่งใด ฯลฯ

เป้าหมายเดียวกันนั้นสร้างขึ้นสำหรับการกำหนดค่าหลายรายการในบิลด์เดียวกันได้ การทำเช่นนี้มีประโยชน์ เช่น เมื่อมีการใช้รหัสเดียวกันสำหรับเครื่องมือที่ทำงานระหว่างบิลด์และโค้ดเป้าหมาย และเรากำลังทำการคอมไพล์แบบข้ามแพลตฟอร์ม หรือเมื่อเราสร้างแอป Android แบบอ้วน (แอปที่มีโค้ดแบบเนทีฟสำหรับสถาปัตยกรรม CPU หลายระบบ)

โดยหลักการแล้ว การกำหนดค่าคืออินสแตนซ์ BuildOptions อย่างไรก็ตาม ในทางปฏิบัติ BuildOptions จะรวมอยู่ใน BuildConfiguration ที่มีฟังก์ชันการทำงานอื่นๆ เพิ่มเติม โดยจะถ่ายทอดจากด้านบนของกราฟการอ้างอิงไปยังด้านล่าง หากมีการเปลี่ยนแปลง จะต้องมีการวิเคราะห์บิลด์อีกครั้ง

ซึ่งทำให้เกิดความผิดปกติ เช่น ต้องวิเคราะห์บิลด์ทั้งหมดใหม่อีกครั้ง ตัวอย่างเช่น จำนวนการทดสอบที่ขอมีการเปลี่ยนแปลง แม้ว่าจะส่งผลต่อเป้าหมายการทดสอบเท่านั้น (เรามีแผนที่จะ "ตัด" การกำหนดค่าเพื่อไม่ให้เกิดกรณีเช่นนี้ แต่ยังไม่พร้อม)

เมื่อการใช้กฎต้องมีส่วนหนึ่งของการกำหนดค่า ก็จะต้องประกาศในคำจำกัดความโดยใช้ RuleClass.Builder.requiresConfigurationFragments() เพื่อหลีกเลี่ยงข้อผิดพลาด (เช่น กฎ Python ที่ใช้ส่วน Java) และเพื่ออำนวยความสะดวกในการตัดการกำหนดค่า เช่น ไม่จำเป็นต้องวิเคราะห์เป้าหมาย C++ ใหม่ เช่น หากตัวเลือก Python มีการเปลี่ยนแปลง

การกำหนดค่าของกฎไม่จำเป็นต้องเหมือนกับกฎ "หลัก" ของกฎดังกล่าว กระบวนการเปลี่ยนแปลงการกำหนดค่าใน Dependency Edge เรียกว่า "การเปลี่ยนการกำหนดค่า" ปัญหานี้อาจเกิดขึ้นจาก 2 ที่ ดังนี้

  1. อยู่ใน Dependency Edge การเปลี่ยนเหล่านี้ระบุอยู่ใน Attribute.Builder.cfg() และเป็นฟังก์ชันจาก Rule (ที่ที่เกิดการเปลี่ยน) และ BuildOptions (การกำหนดค่าเดิม) เป็น BuildOptions อย่างน้อย 1 รายการ (การกำหนดค่าเอาต์พุต)
  2. ใน Edge ขาเข้าไปยังเป้าหมายที่กำหนดค่าไว้ ซึ่งระบุไว้ใน RuleClass.Builder.cfg()

ชั้นเรียนที่เกี่ยวข้องคือ TransitionFactory และ ConfigurationTransition

ระบบจะใช้การเปลี่ยนการกำหนดค่า เช่น

  1. เพื่อประกาศว่ามีการใช้ทรัพยากร Dependency ที่เฉพาะเจาะจงระหว่างบิลด์ และควรสร้างไว้ในสถาปัตยกรรมการดำเนินการ
  2. ในการประกาศว่าต้องสร้างทรัพยากร Dependency ที่เฉพาะเจาะจงสำหรับสถาปัตยกรรมหลายรายการ (เช่น สำหรับโค้ดแบบเนทีฟใน APK ของ Android ที่เป็นไฟล์จำนวนมาก)

หากการเปลี่ยนการกำหนดค่าส่งผลให้เกิดการกำหนดค่าหลายรายการ ก็เรียกว่าการเปลี่ยนแบบแยกกัน

คุณยังใช้การเปลี่ยนการกำหนดค่าใน Starlark ได้ด้วย (เอกสารประกอบที่นี่)

ผู้ให้บริการข้อมูลทางอ้อม

ผู้ให้บริการข้อมูลแบบทรานซิชันคือวิธี (และ _only _way) สำหรับเป้าหมายที่กําหนดค่าไว้ ในการบอกสิ่งต่างๆ เกี่ยวกับเป้าหมายอื่นๆ ที่กําหนดค่าไว้ ซึ่งอาศัยข้อมูลดังกล่าว เหตุผลที่ "สกรรมกริยา" อยู่ในชื่อก็เพราะว่า โดยปกติจะเป็นการรวมการปิดแบบทรานซิทีฟของเป้าหมายที่กำหนดค่าไว้

โดยทั่วไปจะมีการโต้ตอบแบบ 1:1 ระหว่างผู้ให้บริการข้อมูลแบบทรานซิทีฟของ Java กับผู้ให้บริการข้อมูล Starlark (ข้อยกเว้นคือ DefaultInfo ซึ่งเป็นการรวม FileProvider, FilesToRunProvider และ RunfilesProvider เนื่องจากคาดว่า API นั้นเป็น Starlark-ish มากกว่าการทับศัพท์ของ Java โดยตรง) คีย์ของพวกเขาเป็นหนึ่งในสิ่งต่อไปนี้:

  1. ออบเจ็กต์คลาส Java วิธีนี้ใช้ได้กับผู้ให้บริการที่ เข้าถึงจาก Starlark ไม่ได้เท่านั้น ผู้ให้บริการเหล่านี้เป็นคลาสย่อยของ TransitiveInfoProvider
  2. สตริง นี่เป็นเรื่องเก่าและไม่สนับสนุนอย่างยิ่งเพราะอาจเสี่ยงที่จะเกิดการชนกัน ผู้ให้บริการข้อมูลทรานซิชันดังกล่าวคือคลาสย่อยโดยตรงของ build.lib.packages.Info
  3. สัญลักษณ์ผู้ให้บริการ ซึ่งสร้างจาก Starlark โดยใช้ฟังก์ชัน provider() และเป็นวิธีที่แนะนำในการสร้างผู้ให้บริการใหม่ สัญลักษณ์นี้จะแทนโดยอินสแตนซ์ Provider.Key ใน Java

ผู้ให้บริการรายใหม่ที่ใช้งานใน Java ควรใช้งานโดยใช้ BuiltinProvider NativeProvider เลิกใช้งานแล้ว (เรายังไม่มีเวลาในการนำออก) และจะเข้าถึงคลาสย่อย TransitiveInfoProvider จาก Starlark ไม่ได้

เป้าหมายที่กำหนดค่าแล้ว

เป้าหมายที่กำหนดค่าไว้จะนำไปใช้เป็น RuleConfiguredTargetFactory มีคลาสย่อยสำหรับคลาสกฎแต่ละคลาสที่นำไปใช้ใน Java เป้าหมายที่กำหนดค่าไว้ของ Starlark สร้างขึ้นผ่าน StarlarkRuleConfiguredTargetUtil.buildRule()

โรงงานเป้าหมายที่กำหนดค่าไว้ควรใช้ RuleConfiguredTargetBuilder เพื่อสร้างมูลค่าผลลัพธ์ ซึ่งประกอบด้วยสิ่งต่อไปนี้

  1. filesToBuild ซึ่งเป็นแนวคิดที่ไม่ชัดเจนของ "ชุดของไฟล์ที่กฎนี้" ไฟล์เหล่านี้คือไฟล์ที่ถูกสร้างขึ้นเมื่อเป้าหมายที่กำหนดค่าไว้อยู่ในบรรทัดคำสั่งหรือใน srcs ของ Genrule
  2. ไฟล์รันไฟล์ ปกติ และข้อมูล
  3. กลุ่มเอาต์พุต นี่คือ "ไฟล์ชุดอื่นๆ" ที่กฎสร้างได้ คุณจะเข้าถึงไฟล์ได้โดยใช้แอตทริบิวต์ Output_group ของกฎ Filegroup ใน BUILD และใช้ผู้ให้บริการ OutputGroupInfo ใน Java

ไฟล์เรียกใช้

ไบนารีบางรายการต้องใช้ไฟล์ข้อมูลเพื่อเรียกใช้ ตัวอย่างที่ชัดเจนคือการทดสอบที่ต้องใช้ไฟล์อินพุต ลักษณะนี้แสดงอยู่ใน Bazel ตามแนวคิดของ "runfiles" "โครงสร้าง Runfiles" เป็นโครงสร้างไดเรกทอรีของไฟล์ข้อมูลสำหรับไบนารีเฉพาะ โดยจะสร้างขึ้นในระบบไฟล์เป็นแผนผังลิงก์สัญลักษณ์ซึ่งมีลิงก์สัญลักษณ์แต่ละลิงก์ชี้ไปยังไฟล์ในแหล่งที่มาของต้นไม้เอาต์พุต

ชุดไฟล์รันจะแสดงเป็นอินสแตนซ์ Runfiles แนวคิดคือแผนที่จากเส้นทางของไฟล์ในโครงสร้าง Runfile ไปจนถึงอินสแตนซ์ Artifact ที่เป็นตัวแทน มีความซับซ้อนกว่า Map รายการเดียวเล็กน้อยด้วยเหตุผล 2 ประการ ดังนี้

  • ส่วนใหญ่แล้ว เส้นทางรันไฟล์ของไฟล์จะเหมือนกับเส้นทางผู้ดำเนินการ เราใช้ข้อมูลนี้เพื่อประหยัด RAM บางส่วน
  • มีรายการเดิมหลายประเภทในโครงสร้าง Runfile ที่ต้องแสดงด้วยเช่นกัน

ระบบรวบรวมไฟล์รันไฟล์โดยใช้ RunfilesProvider: อินสแตนซ์ของคลาสนี้แสดงแทนไฟล์รันไฟล์ของเป้าหมายที่กำหนดค่าไว้ (เช่น ไลบรารี) และความจำเป็นการปิดแบบทรานซิทีฟ โดยจะรวบรวมไฟล์เหล่านั้นเหมือนชุดที่ซ้อนกัน (อันที่จริงแล้ว ไฟล์ดังกล่าวใช้งานโดยใช้ชุดที่ซ้อนกันใต้หน้าปก): แต่ละการรวมเป้าหมายกับรันไฟล์ของทรัพยากร Dependency จะเพิ่มไฟล์ของตนเองบางส่วน จากนั้นส่งการตั้งค่าที่ได้ไปยังกราฟทรัพยากร Dependency อินสแตนซ์ RunfilesProvider มีอินสแตนซ์ Runfiles 2 รายการ โดยอินสแตนซ์หนึ่งเป็นอินสแตนซ์ที่ขึ้นอยู่กับกฎผ่านแอตทริบิวต์ "data" และอีกอินสแตนซ์หนึ่งสำหรับทรัพยากร Dependency ที่เข้ามาใหม่ทุกประเภท เนื่องจากบางครั้งเป้าหมายอาจนำเสนอไฟล์การเรียกใช้ที่แตกต่างกันเมื่อพึ่งพาผ่านแอตทริบิวต์ข้อมูลมากกว่ารูปแบบอื่น ซึ่งเป็นลักษณะการทำงานเดิมที่ไม่พึงประสงค์ซึ่งเรายังไม่ได้นำออก

การเรียกใช้ไฟล์ไบนารีจะแสดงเป็นอินสแตนซ์ของ RunfilesSupport สิ่งนี้แตกต่างจาก Runfiles เนื่องจาก RunfilesSupport มีความสามารถของสิ่งที่สร้างขึ้นมาได้จริง (ต่างจาก Runfiles ซึ่งเป็นเพียงการแมปเท่านั้น) ซึ่งจำเป็นต้องใช้คอมโพเนนต์เพิ่มเติมต่อไปนี้

  • ไฟล์ Manifest ของการเรียกใช้ไฟล์อินพุต นี่คือคำอธิบายแบบต่อเนื่องของโครงสร้าง Runfiles ซึ่งใช้เป็นพร็อกซีสำหรับเนื้อหาของโครงสร้างไฟล์ Manifest และ Bazel จะถือว่าโครงสร้างของการเรียกใช้ไฟล์มีการเปลี่ยนแปลงในกรณีที่เนื้อหาของไฟล์ Manifest มีการเปลี่ยนแปลงเท่านั้น
  • ไฟล์ Manifest ของการเรียกใช้ไฟล์เอาต์พุต มีการใช้งานโดยไลบรารีรันไทม์ที่จัดการโครงสร้างของรันไฟล์ โดยเฉพาะใน Windows ซึ่งบางครั้งก็ไม่รองรับลิงก์สัญลักษณ์
  • คนกลางของรันไฟล์ ในการสร้างต้นไม้รันไฟล์อยู่ จะต้องมีการสร้างต้นไม้ Symlink และอาร์ติแฟกต์ที่ลิงก์สัญลักษณ์ชี้ไป ในการลดจำนวนเอดจ์ของ Dependency จะใช้คนกลางของ Runfile เพื่อแทนค่าเหล่านี้ได้
  • อาร์กิวเมนต์บรรทัดคำสั่งสำหรับเรียกใช้ไบนารีที่มีการเรียกใช้ไฟล์ที่มีออบเจ็กต์ RunfilesSupport แทน

ลักษณะ

Aspects เป็นวิธีหนึ่งในการ "เผยแพร่การคํานวณลงในกราฟทรัพยากร Dependency" ซึ่งได้อธิบายไว้สำหรับผู้ใช้ Bazel ไว้ที่นี่ ตัวอย่างที่สร้างแรงจูงใจที่ดีคือบัฟเฟอร์โปรโตคอล กฎ proto_library ไม่ควรทราบเกี่ยวกับภาษาใดภาษาหนึ่ง แต่การสร้างการใช้ข้อความบัฟเฟอร์โปรโตคอล ("หน่วยพื้นฐาน" ของบัฟเฟอร์โปรโตคอล) ในภาษาโปรแกรมใดก็ตามควรเชื่อมโยงกับกฎ proto_library เพื่อที่ว่าหากเป้าหมาย 2 รายการในภาษาเดียวกันขึ้นอยู่กับบัฟเฟอร์โปรโตคอลเดียวกัน เป้าหมาย 2 รายการจะได้รับการสร้างขึ้นเพียงครั้งเดียวเท่านั้น

เช่นเดียวกับเป้าหมายที่กำหนดค่าไว้ เป้าหมายจะแสดงใน Skyframe เป็น SkyValue และวิธีการสร้างเป้าหมายก็คล้ายกับการสร้างเป้าหมายที่กำหนดค่าไว้อย่างมาก โดยมีคลาสจากโรงงานที่เรียกว่า ConfiguredAspectFactory ซึ่งมีสิทธิ์เข้าถึง RuleContext แต่ต่างจากโรงงานเป้าหมายที่กำหนดค่าไว้ตรงยังรู้เกี่ยวกับเป้าหมายที่กำหนดค่าไว้ซึ่งเชื่อมโยงอยู่และผู้ให้บริการด้วย

ชุดของลักษณะที่เผยแพร่ลงกราฟทรัพยากร Dependency จะระบุแอตทริบิวต์แต่ละรายการโดยใช้ฟังก์ชัน Attribute.Builder.aspects() มีชั้นเรียนที่มีชื่อและสร้างความสับสน ที่เข้าร่วมในกระบวนการนี้ 2-3 ชั้นเรียน ได้แก่

  1. AspectClass คือการใช้งานในด้านต่างๆ ซึ่งอาจอยู่ใน Java (ในกรณีนี้เป็นคลาสย่อย) หรือใน Starlark (ในกรณีนี้เป็นอินสแตนซ์ของ StarlarkAspectClass) โดยจะคล้ายกับ RuleConfiguredTargetFactory
  2. AspectDefinition เป็นคำจำกัดความของคุณสมบัติ โดยจะรวมถึงผู้ให้บริการที่จำเป็น ผู้ให้บริการที่มี และอ้างอิงถึงการใช้งาน เช่น อินสแตนซ์ AspectClass ที่เหมาะสม ซึ่งคล้ายกับ RuleClass
  3. AspectParameters เป็นวิธีจัดทำพารามิเตอร์เกี่ยวกับลักษณะที่เผยแพร่ลงในกราฟทรัพยากร Dependency ค่าปัจจุบันคือสตริงที่เชื่อมกับสตริง ตัวอย่างที่ดีของประโยชน์คือบัฟเฟอร์โปรโตคอล ถ้าภาษาหนึ่งมี API หลาย API ควรเผยแพร่ข้อมูลของ API ที่บัฟเฟอร์โปรโตคอลให้ลงไปในกราฟทรัพยากร Dependency
  4. Aspect แสดงข้อมูลทั้งหมดที่จำเป็นสำหรับการคำนวณด้านต่างๆ ที่ทำให้เกิดกราฟทรัพยากร Dependency ซึ่งประกอบด้วยคลาสการแสดงผล คำจำกัดความ และพารามิเตอร์
  5. RuleAspect คือฟังก์ชันที่กำหนดว่ากฎใดควรเผยแพร่ในลักษณะใด ซึ่งเป็นฟังก์ชัน Rule -> Aspect

ข้อมูลแทรกที่คาดไม่ถึงก็คือลักษณะต่างๆ อาจแนบอยู่กับด้านอื่นๆ ตัวอย่างเช่น ลักษณะที่รวบรวมคลาสพาธสำหรับ Java IDE อาจต้องการทราบเกี่ยวกับไฟล์ .jar ทั้งหมดในคลาสพาธ แต่บางไฟล์อาจเป็นบัฟเฟอร์โปรโตคอล ในกรณีนี้ ด้าน IDE จะต้องแนบกับคู่ (กฎ proto_library + มุมมอง Proto ของ Java)

ระบบจะบันทึกความซับซ้อนของแง่มุมต่างๆ ไว้ในชั้นเรียน AspectCollection

แพลตฟอร์มและ Toolchain

Bazel รองรับบิลด์แบบหลายแพลตฟอร์ม กล่าวคือ บิลด์อาจมีสถาปัตยกรรมหลายอย่างที่การดำเนินการของบิลด์ทำงาน และบิลด์หลายรายการสำหรับโค้ดที่สร้างขึ้น สถาปัตยกรรมเหล่านี้เรียกว่าแพลตฟอร์มในบริการของ Bazel (ดูเอกสารประกอบฉบับเต็มที่นี่)

แพลตฟอร์มจะอธิบายโดยการแมปคีย์-ค่าจากการตั้งค่าข้อจำกัด (เช่น แนวคิดของ "สถาปัตยกรรม CPU") เพื่อจำกัดค่า (เช่น CPU ตัวใดตัวหนึ่ง เช่น x86_64) เรามี "พจนานุกรม" ของการตั้งค่าข้อจำกัดและค่าจำกัดที่ใช้บ่อยที่สุดในที่เก็บ @platforms

แนวคิดของ toolchain มาจากข้อเท็จจริงที่ว่าผู้สร้างอาจต้องใช้คอมไพเลอร์ที่ต่างกันไปตามแพลตฟอร์มที่บิลด์ทำงานอยู่และกำหนดเป้าหมายแพลตฟอร์ม ตัวอย่างเช่น เครื่องมือเชน C++ รายการหนึ่งอาจทำงานบนระบบปฏิบัติการที่เฉพาะเจาะจงและสามารถกำหนดเป้าหมายระบบปฏิบัติการอื่นๆ ได้ Bazel ต้องระบุคอมไพเลอร์ C++ ที่ใช้ตามการดำเนินการที่กำหนดไว้และแพลตฟอร์มเป้าหมาย (เอกสารประกอบสำหรับเครื่องมือเชนที่นี่)

โดย Toolchain จะมีคำอธิบายประกอบด้วยชุดข้อจำกัดแพลตฟอร์มการดำเนินการและเป้าหมายที่เครื่องมือดังกล่าวรองรับ โดยคำจำกัดความของ Toolchain แบ่งออกเป็น 2 ส่วนดังนี้

  1. กฎ toolchain() ที่อธิบายชุดการดำเนินการและข้อจํากัดเป้าหมายจะบังคับให้ Toolchain รองรับและระบุว่า Toolchain เป็นชนิดใด (เช่น C++ หรือ Java) (กฎหลังจะแสดงด้วยกฎ toolchain_type())
  2. กฎเฉพาะภาษาที่อธิบาย Toolchain จริง (เช่น cc_toolchain())

เนื่องจากเราจําเป็นต้องรู้ข้อจํากัดของ Toolchain ทุกตัวเพื่อทำการแปลง Toolchain และกฎ *_toolchain() ที่เจาะจงภาษามีข้อมูลมากกว่านั้นมาก ดังนั้นให้ใช้เวลาโหลดนานขึ้น

แพลตฟอร์มการดำเนินการจะระบุโดยใช้วิธีใดวิธีหนึ่งต่อไปนี้

  1. ในไฟล์ WORKSPACE โดยใช้ฟังก์ชัน register_execution_platforms()
  2. ในบรรทัดคำสั่งโดยใช้ตัวเลือก บรรทัดคำสั่ง --extra_execution_platforms

ระบบจะคำนวณชุดแพลตฟอร์มการดำเนินการที่ใช้ได้ใน RegisteredExecutionPlatformsFunction

แพลตฟอร์มเป้าหมายสำหรับเป้าหมายที่กำหนดค่าจะกำหนดตามPlatformOptions.computeTargetPlatform() เป็นรายการของแพลตฟอร์มเนื่องจากในที่สุดแล้วเราต้องการรองรับแพลตฟอร์มเป้าหมายหลายแพลตฟอร์ม แต่ก็ยังไม่ได้นำมาใช้

ชุดของ Toolchain ที่จะใช้สำหรับเป้าหมายที่กำหนดค่าจะกำหนดโดย ToolchainResolutionFunction โดยเป็นฟังก์ชันของสิ่งต่อไปนี้

  • ชุด Toolchain ที่ลงทะเบียนแล้ว (ในไฟล์ WORKSPACE และการกำหนดค่า)
  • แพลตฟอร์มการดำเนินการและแพลตฟอร์มที่ต้องการ (ในการกำหนดค่า)
  • ชุดประเภท Toolchain ที่เป้าหมายที่กำหนดค่าไว้ต้องใช้ (ใน UnloadedToolchainContextKey)
  • ชุดข้อจำกัดแพลตฟอร์มการดำเนินการของเป้าหมายที่กำหนดค่า (แอตทริบิวต์ exec_compatible_with) และการกำหนดค่า (--experimental_add_exec_constraints_to_targets) ใน UnloadedToolchainContextKey

ผลลัพธ์ที่ได้คือ UnloadedToolchainContext ซึ่งโดยพื้นฐานแล้วเป็นแผนที่จากประเภท Toolchain (แสดงเป็นอินสแตนซ์ ToolchainTypeInfo) ไปยังป้ายกำกับของ Toolchain ที่เลือก วิธีนี้เรียกว่า "ยกเลิกการโหลด" เนื่องจากไม่มีเครื่องมือโซ่ในตัว แต่มีเพียงป้ายกำกับเท่านั้น

จากนั้นระบบจะโหลด Toolchain จริงๆ โดยใช้ ResolvedToolchainContext.load() และใช้โดยการใช้งานเป้าหมายที่กำหนดค่าไว้ซึ่งส่งคำขอ

นอกจากนี้เรายังมีระบบเดิมที่ต้องมีการกำหนดค่า "โฮสต์" และการกำหนดค่าเป้าหมายเพียงรายการเดียวที่แสดงด้วยแฟล็กการกำหนดค่าต่างๆ เช่น --cpu เราจะค่อยๆ เปลี่ยนไปใช้ระบบข้างต้น เราได้ใช้การแมปแพลตฟอร์มเพื่อแปลค่าระหว่างแฟล็กเดิมกับข้อจำกัดแพลตฟอร์มรูปแบบใหม่ ในการจัดการกรณีที่ผู้คนใช้ค่าการกําหนดค่าเดิม โค้ดของพวกเขาอยู่ใน PlatformMappingFunction และใช้ "ภาษาง่ายๆ" ที่ไม่ใช่ Starlark

ข้อจำกัด

บางครั้งมีลูกค้าต้องการตั้งเป้าหมายว่าเข้ากันได้กับแพลตฟอร์มเพียง 2-3 แพลตฟอร์ม Bazel มีกลไกมากมายในการบรรลุเป้าหมายนี้

  • ข้อจำกัดเฉพาะกฎ
  • environment_group() / environment()
  • ข้อจำกัดของแพลตฟอร์ม

ข้อจำกัดเฉพาะกฎจะใช้ใน Google สำหรับกฎของ Java เป็นส่วนใหญ่ ข้อจำกัดเหล่านี้กำลังจะหมดลงแล้วและไม่มีใน Bazel แต่ซอร์สโค้ดอาจมีการอ้างอิงไปยังข้อจำกัดนี้ แอตทริบิวต์ที่ควบคุมสิ่งนี้เรียกว่า constraints=

สภาพแวดล้อม_group() และสภาพแวดล้อม()

กฎเหล่านี้เป็นกลไกเดิมและไม่มีการใช้งานอย่างกว้างขวาง

กฎบิลด์ทั้งหมดจะประกาศได้ว่าสามารถสร้าง "สภาพแวดล้อม" ใดได้ โดย "สภาพแวดล้อม" เป็นอินสแตนซ์ของกฎ environment()

คุณระบุสภาพแวดล้อมที่รองรับสำหรับกฎได้หลายวิธี ดังนี้

  1. ผ่านแอตทริบิวต์ restricted_to= นี่คือรูปแบบข้อกำหนดที่ตรงที่สุด โดยจะประกาศชุดสภาพแวดล้อมที่กฎรองรับสำหรับกลุ่มนี้
  2. ผ่านแอตทริบิวต์ compatible_with= การดำเนินการนี้เป็นการประกาศสภาพแวดล้อมที่กฎรองรับนอกเหนือจากสภาพแวดล้อม "มาตรฐาน" ที่รองรับโดยค่าเริ่มต้น
  3. ผ่านแอตทริบิวต์ระดับแพ็กเกจ default_restricted_to= และ default_compatible_with=
  4. ผ่านข้อกำหนดเริ่มต้นในกฎ environment_group() รายการ ทุกสภาพแวดล้อมอยู่ในกลุ่มแอปเทียบเท่าที่มีธีมเกี่ยวข้องกัน (เช่น "สถาปัตยกรรมของ CPU" "เวอร์ชัน JDK" หรือ "ระบบปฏิบัติการบนอุปกรณ์เคลื่อนที่") คำจำกัดความของกลุ่มสภาพแวดล้อมประกอบด้วยสภาพแวดล้อมใดต่อไปนี้ที่ "ค่าเริ่มต้น" ควรรองรับ หากไม่ได้ระบุไว้เป็นอย่างอื่นโดยแอตทริบิวต์ restricted_to= / environment() กฎที่ไม่มีแอตทริบิวต์ดังกล่าว จะรับช่วงค่าเริ่มต้นทั้งหมด
  5. ผ่านค่าเริ่มต้นของคลาสกฎ ซึ่งจะลบล้างค่าเริ่มต้นส่วนกลางสำหรับอินสแตนซ์ทั้งหมดของคลาสกฎที่ระบุ ตัวอย่างเช่น วิธีนี้จะช่วยให้กฎ *_test ทั้งหมดทดสอบได้โดยที่แต่ละอินสแตนซ์ไม่ต้องประกาศความสามารถนี้อย่างชัดแจ้ง

มีการใช้งาน environment() เป็นกฎปกติ ส่วน environment_group() เป็นทั้งคลาสย่อยของ Target แต่ไม่ใช่ Rule (EnvironmentGroup) และฟังก์ชันที่พร้อมใช้งานโดยค่าเริ่มต้นจาก Starlark (StarlarkLibrary.environmentGroup()) ซึ่งสุดท้ายแล้วจะสร้างเป้าหมายที่เป็นนามแฝง ทั้งนี้เพื่อหลีกเลี่ยงการขึ้นต่อกันแบบวนซ้ำที่จะเกิดขึ้นเนื่องจากแต่ละสภาพแวดล้อมต้องประกาศกลุ่มสภาพแวดล้อมที่กลุ่มสภาพแวดล้อมนั้นเป็นสมาชิกอยู่ และแต่ละกลุ่มสภาพแวดล้อมต้องประกาศสภาพแวดล้อมเริ่มต้น

สามารถจำกัดบิลด์ให้กับสภาพแวดล้อมบางอย่างได้ด้วยตัวเลือกบรรทัดคำสั่ง --target_environment

การใช้งานการตรวจสอบข้อจำกัดอยู่ใน RuleContextConstraintSemantics และ TopLevelConstraintSemantics

ข้อจำกัดของแพลตฟอร์ม

วิธี "อย่างเป็นทางการ" ในปัจจุบันในการอธิบายแพลตฟอร์มที่เป้าหมายรองรับได้คือการใช้ข้อจำกัดเดียวกับที่ใช้ในการอธิบายเครื่องมือเชนและแพลตฟอร์ม อยู่ระหว่างตรวจสอบการดึงคำขอ #10945

ระดับการแชร์

ถ้าคุณทำงานกับโค้ดเบสขนาดใหญ่กับนักพัฒนาซอฟต์แวร์จำนวนมาก (เช่นที่ Google) คุณต้องดูแลไม่ให้คนอื่นๆ ยึดถือโค้ดของคุณโดยไม่มีกฎเกณฑ์ มิเช่นนั้น ตามกฎของ Hyrum ผู้คนจะพึ่งพฤติกรรมที่คุณถือว่าเป็นรายละเอียดการนำไปใช้

Bazel รองรับการทำงานนี้โดยกลไกที่เรียกว่าระดับการเข้าถึง ซึ่งประกาศได้ว่าเป้าหมายที่เฉพาะเจาะจงจะอ้างอิงได้โดยใช้แอตทริบิวต์ระดับการเข้าถึงเท่านั้น แอตทริบิวต์นี้มีความพิเศษเล็กน้อย เนื่องจากแม้จะมีรายการป้ายกำกับ แต่ป้ายกำกับเหล่านี้อาจเข้ารหัสรูปแบบเหนือชื่อแพ็กเกจ แทนที่จะเป็นตัวชี้ไปยังเป้าหมายที่เฉพาะเจาะจงใดๆ (ใช่ เป็นข้อบกพร่องของการออกแบบ)

ซึ่งทำได้ในที่ต่อไปนี้

  • อินเทอร์เฟซ RuleVisibility แสดงการประกาศระดับการเข้าถึง ซึ่งอาจเป็นค่าคงที่ (สาธารณะทั้งหมดหรือส่วนตัวทั้งหมด) หรือเป็นรายการป้ายกำกับก็ได้
  • ป้ายกำกับอาจหมายถึงกลุ่มแพ็กเกจ (รายการแพ็กเกจที่กำหนดไว้ล่วงหน้า) แพ็กเกจโดยตรง (//pkg:__pkg__) หรือโครงสร้างย่อยของแพ็กเกจ (//pkg:__subpackages__) ซึ่งแตกต่างจากไวยากรณ์บรรทัดคำสั่งซึ่งใช้ //pkg:* หรือ //pkg/...
  • ใช้งานกลุ่มแพ็กเกจเป็นเป้าหมายของตนเอง (PackageGroup) และกำหนดเป้าหมายที่กำหนดค่า (PackageGroupConfiguredTarget) แล้ว เราอาจแทนที่กลุ่มแพ็กเกจด้วยกฎง่ายๆ หากต้องการ ระบบนำตรรกะนี้ไปใช้ด้วยความช่วยเหลือของ PackageSpecification ซึ่งสอดคล้องกับรูปแบบเดียว เช่น //pkg/..., PackageGroupContents ซึ่งตรงกับแอตทริบิวต์ packages ของ package_group เดียว และ PackageSpecificationProvider ซึ่งรวมกันเป็น package_group และ includes ที่เป็นทางอ้อม
  • การแปลงจากรายการป้ายกำกับระดับการเข้าถึงเป็นทรัพยากร Dependency จะดำเนินการใน DependencyResolver.visitTargetVisibility และที่อื่นๆ อีก 2-3 แห่ง
  • การตรวจสอบจริงจะเสร็จสิ้นใน CommonPrerequisiteValidator.validateDirectPrerequisiteVisibility()

ชุดที่ซ้อนกัน

บ่อยครั้งที่เป้าหมายที่กำหนดค่าไว้จะรวมชุดของไฟล์จากทรัพยากร Dependency แล้วเพิ่มไฟล์ขึ้นมาเอง และรวมชุดดังกล่าวไว้ในผู้ให้บริการข้อมูลแบบทรานซิทีฟ เพื่อให้เป้าหมายที่กำหนดค่าที่อาศัยการอ้างอิงดังกล่าวสามารถทำเช่นเดียวกันได้ ตัวอย่าง

  • ไฟล์ส่วนหัว C++ ที่ใช้สำหรับบิลด์
  • ไฟล์ออบเจ็กต์ที่แทนการปิดแบบทรานซิทีฟของ cc_library
  • ชุดของไฟล์ .jar ที่จำเป็นต้องอยู่ในคลาสพาธเพื่อให้กฎ Java คอมไพล์หรือเรียกใช้
  • ชุดของไฟล์ Python เมื่อปิดทรานซิทีฟของกฎ Python

หากเราทำเช่นนี้โดยใช้วิธีที่ไร้เดียงสา เช่น List หรือ Set เราจะได้การใช้หน่วยความจำกำลังสอง กล่าวคือ หากมีเชนกฎ N กฎและกฎแต่ละข้อเพิ่มไฟล์ เราจะมีสมาชิกคอลเล็กชัน 1+2...+N คน

ในการแก้ปัญหานี้ เราจึงคิดวิธี NestedSet โครงสร้างข้อมูลที่ประกอบด้วยอินสแตนซ์ NestedSet อื่นๆ และสมาชิกบางส่วนของตัวเอง จึงสร้างกราฟแบบอะไซคัลที่มีทิศทางของชุดต่างๆ ซึ่งจะเปลี่ยนแปลงไม่ได้และสมาชิกสามารถทำซ้ำได้ เรากำหนดลำดับการทำซ้ำหลายรายการ (NestedSet.Order) ได้แก่ สั่งล่วงหน้า สั่งภายหลัง โทโพโลยี (โหนดจะอยู่หลังบรรพบุรุษเสมอ) และ "ไม่สนใจ แต่ควรจะเหมือนกันทุกครั้ง"

โครงสร้างข้อมูลเดียวกันนี้เรียกว่า depset ใน Starlark

อาร์ติแฟกต์และการดำเนินการ

บิลด์จริงประกอบด้วยชุดคำสั่งที่ต้องเรียกใช้เพื่อสร้างเอาต์พุตที่ผู้ใช้ต้องการ คำสั่งจะแสดงเป็นอินสแตนซ์ของคลาส Action และไฟล์จะแสดงเป็นอินสแตนซ์ของคลาส Artifact โดยจะจัดเรียงเป็นกราฟแบบ 2 ภาคแบบมีทิศทาง ที่เรียกว่า "กราฟการกระทำ"

อาร์ติแฟกต์มี 2 ประเภท ได้แก่ อาร์ติแฟกต์ต้นทาง (สิ่งที่พร้อมใช้งานก่อนที่ Bazel จะเริ่มดำเนินการ) และอาร์ติแฟกต์ที่ได้มา (ประเภทที่ต้องสร้าง) อาร์ติแฟกต์ที่ได้มาอาจมีได้หลายประเภท ดังนี้

  1. **อาร์ติแฟกต์ปกติ **ข้อมูลเหล่านี้จะตรวจสอบความเป็นปัจจุบันด้วยการคำนวณตรวจสอบข้อผิดพลาดโดยใช้เวลา mtime เป็นทางลัด เราจะไม่ตรวจสอบความถูกต้องของไฟล์หากเวลาไม่มีการเปลี่ยนแปลง
  2. อาร์ติแฟกต์symlink ที่ยังไม่ได้รับการแก้ไข ซึ่งมีการตรวจสอบความทันสมัยโดยเรียกใช้ readlink() ซึ่งต่างจากอาร์ติแฟกต์ทั่วไปตรงที่อาจทำให้เกิดลิงก์สัญลักษณ์ที่ทำให้อันตราย มักใช้ในกรณีที่ไฟล์หนึ่งๆ จะแพ็กไฟล์ลงในที่เก็บถาวรบางประเภท
  3. อาร์ติแฟกต์ของต้นไม้ ไฟล์เหล่านี้ไม่ใช่ไฟล์เดี่ยว แต่เป็นแผนผังไดเรกทอรี ระบบจะตรวจสอบความเป็นปัจจุบันโดยตรวจสอบชุดของไฟล์และเนื้อหาในไฟล์ โดยจะแสดงเป็น TreeArtifact
  4. อาร์ติแฟกต์ข้อมูลเมตาแบบคงที่ การเปลี่ยนแปลงอาร์ติแฟกต์เหล่านี้จะไม่ทริกเกอร์การสร้างใหม่ ซึ่งจะใช้สำหรับข้อมูลแสตมป์ของบิลด์เท่านั้น เราไม่ต้องการสร้างใหม่เพียงเพราะเวลาปัจจุบันมีการเปลี่ยนแปลง

ไม่มีเหตุผลพื้นฐานว่าทำไมอาร์ติแฟกต์ต้นฉบับไม่สามารถเป็นอาร์ติแฟกต์แบบต้นไม้หรืออาร์ติแฟกต์ลิงก์สัญลักษณ์ที่ยังไม่ได้รับการแก้ไข เพียงแต่เรายังไม่ได้นำอาร์ติแฟกต์ดังกล่าวมาใช้ (แต่เราควร การอ้างอิงไดเรกทอรีแหล่งที่มาในไฟล์ BUILD เป็นหนึ่งในปัญหาความไม่ถูกต้องที่พบมาอย่างยาวนาน 2-3 ปัญหาของ Bazel เรามีการติดตั้งใช้งานประเภทดังกล่าวซึ่งเปิดใช้โดยพร็อพเพอร์ตี้ JVM ของ BAZEL_TRACK_SOURCE_DIRECTORIES=1)

Artifact ลักษณะเด่นคือคนกลาง อินสแตนซ์เหล่านี้ระบุด้วยอินสแตนซ์ Artifact ซึ่งเป็นเอาต์พุตของ MiddlemanAction คำสั่งเหล่านี้ใช้ในสถานการณ์ พิเศษ เช่น

  • ระบบใช้การรวมตัวกลางในการจัดกลุ่มอาร์ติแฟกต์เข้าด้วยกัน กล่าวคือ หากการทำงานจำนวนมากใช้ชุดอินพุตขนาดใหญ่ชุดเดียวกัน เราจะไม่มีขอบเขตการขึ้นต่อกัน N*M และจะมีเฉพาะ N+M (แทนที่ด้วยชุดที่ซ้อนกัน)
  • การกำหนดเวลาให้กับคนกลางของทรัพยากร Dependency จะช่วยให้มั่นใจได้ว่าการดำเนินการจะทำงานก่อนอีกรายการ โดยส่วนใหญ่มักใช้สำหรับการวิเคราะห์โค้ด แต่ใช้สำหรับการรวบรวม C++ ด้วย (ดูคำอธิบายใน CcCompilationContext.createMiddleman())
  • คนกลางของ Runfile ใช้เพื่อให้แน่ใจว่ามีโครงสร้าง Runfile เพื่อที่จะได้ไม่ต้องพึ่งไฟล์ Manifest ของเอาต์พุตและอาร์ติแฟกต์ทั้งหมดที่โครงสร้าง Runfiles อ้างอิง

สิ่งที่ควรทราบคือคำสั่งที่ต้องเรียกใช้ สภาพแวดล้อมที่จำเป็น และชุดเอาต์พุตที่สร้างขึ้น รายการต่อไปนี้คือองค์ประกอบหลัก ของคำอธิบายการกระทำ

  • บรรทัดคำสั่งที่จำเป็นต้องเรียกใช้
  • อาร์ติแฟกต์อินพุตที่จำเป็นต้องใช้
  • ตัวแปรสภาพแวดล้อมที่ต้องตั้งค่า
  • คำอธิบายประกอบที่อธิบายสภาพแวดล้อม (เช่น แพลตฟอร์ม) ที่สภาพแวดล้อมจำเป็นต้องใช้

นอกจากนี้ยังมีกรณีพิเศษอื่นๆ อีก 2-3 กรณี เช่น การเขียนไฟล์ที่ Bazel รู้จักเนื้อหา เป็นคลาสย่อยของ AbstractAction การดำเนินการส่วนใหญ่จะเป็น SpawnAction หรือ StarlarkAction (เหมือนกัน กล่าวคือไม่ควรเป็นคลาสแยกกัน) แม้ว่า Java และ C++ จะมีประเภทการดำเนินการเป็นของตัวเอง (JavaCompileAction, CppCompileAction และ CppLinkAction)

ท้ายที่สุดแล้ว เราต้องการย้ายทุกอย่างไปที่ SpawnAction เนื่องจาก JavaCompileAction นั้นใกล้เคียงกันมาก แต่ C++ เป็นกรณีพิเศษเล็กน้อยเนื่องจากมีการแยกวิเคราะห์ไฟล์ .d และรวมการสแกน

กราฟการดำเนินการส่วนใหญ่จะ "ฝัง" ลงในกราฟ Skyframe โดยในแนวคิด การเรียกใช้การดำเนินการจะแสดงเป็นการเรียกใช้ ActionExecutionFunction การแมปจากขอบของทรัพยากร Dependency ของกราฟการดำเนินการไปยัง Edge ของ Skyframe มีการอธิบายไว้ใน ActionExecutionFunction.getInputDeps() และ Artifact.key() และมีการเพิ่มประสิทธิภาพเล็กน้อยเพื่อทำให้จำนวนขอบของ Skyframe อยู่ในระดับต่ำ

  • อาร์ติแฟกต์ที่ได้มาไม่มี SkyValue เป็นของตัวเอง ระบบจะใช้ Artifact.getGeneratingActionKey() แทนเพื่อหาคีย์สำหรับการดำเนินการที่สร้างคีย์ดังกล่าว
  • ชุดที่ซ้อนกันจะมีคีย์ Skyframe ของตัวเอง

การดำเนินการที่แชร์

การดำเนินการบางอย่างสร้างขึ้นโดยเป้าหมายที่กำหนดค่าไว้หลายรายการ กฎ Starlark มีข้อจำกัดมากกว่าเนื่องจากได้รับอนุญาตให้ใส่การดำเนินการที่ได้รับลงในไดเรกทอรีซึ่งกำหนดโดยการกำหนดค่าและแพ็กเกจเท่านั้น (แต่ถึงอย่างนั้น กฎในแพ็กเกจเดียวกันก็อาจขัดแย้งกัน) แต่กฎที่ใช้งานใน Java อาจวางอาร์ติแฟกต์ที่ดึงข้อมูลมาไว้ที่ใดก็ได้

วิธีนี้ถือเป็นฟีเจอร์ที่ไม่ถูกต้อง แต่การกำจัดฟีเจอร์ดังกล่าวนั้นเป็นเรื่องยากมากเพราะจะช่วยลดเวลาในการดำเนินการได้อย่างมาก เช่น เมื่อต้องประมวลผลไฟล์ต้นฉบับและต้องอ้างอิงไฟล์นั้นด้วยกฎหลายข้อ (handwave-handwave) เรื่องนี้ขึ้นอยู่กับค่าใช้จ่ายของ RAM บางส่วน: อินสแตนซ์แต่ละรายการของการดำเนินการที่แชร์ต้องจัดเก็บไว้ในหน่วยความจำแยกกัน

หากการดำเนินการ 2 รายการสร้างไฟล์เอาต์พุตเดียวกัน การดำเนินการ 2 รายการจะต้องมีแบบเดียวกัน กล่าวคือ มีอินพุตเดียวกัน เอาต์พุตเดียวกัน และเรียกใช้บรรทัดคำสั่งเดียวกัน ความสัมพันธ์ที่เทียบเท่านี้มีการใช้งานใน Actions.canBeShared() และได้รับการยืนยันระหว่างระยะการวิเคราะห์กับการดำเนินการโดยดูที่การดำเนินการทั้งหมด การดำเนินการนี้มีการติดตั้งใช้งานใน SkyframeActionExecutor.findAndStoreArtifactConflicts() และเป็นหนึ่งในไม่กี่แห่งใน Bazel ที่จำเป็นต้องมีมุมมอง "ทั่วโลก" ของบิลด์

ระยะการดำเนินการ

นี่คือเวลาที่ Bazel เริ่มเรียกใช้การดำเนินการบิลด์ เช่น คำสั่งที่สร้างเอาต์พุต

สิ่งแรกที่ Bazel ทำหลังจากช่วงการวิเคราะห์คือการกำหนดสิ่งที่จำเป็นต้องสร้างอาร์ติแฟกต์ ตรรกะของสิ่งนี้จะเข้ารหัสในรูปแบบ TopLevelArtifactHelper กล่าวโดยคร่าวๆ ก็คือ filesToBuild ของเป้าหมายที่กำหนดค่าในบรรทัดคำสั่งและเนื้อหาของกลุ่มเอาต์พุตพิเศษเพื่อวัตถุประสงค์ที่ชัดเจนในการแสดง "หากเป้าหมายนี้อยู่ในบรรทัดคำสั่ง ให้สร้างอาร์ติแฟกต์เหล่านี้"

ขั้นตอนถัดไปคือการสร้างรูทการดำเนินการ เนื่องจาก Bazel มีตัวเลือกในการอ่านแพ็กเกจแหล่งที่มาจากตำแหน่งต่างๆ ในระบบไฟล์ (--package_path) จึงต้องมอบการดำเนินการที่ดำเนินการในเครื่องพร้อมทรีแหล่งที่มาแบบเต็ม ซึ่งได้รับการจัดการโดยคลาส SymlinkForest และทำงานโดยบันทึกทุกเป้าหมายที่ใช้ในขั้นตอนการวิเคราะห์และสร้างไดเรกทอรีต้นไม้เดียวที่เชื่อมโยงแพ็กเกจทุกรายการที่มีเป้าหมายที่ใช้จากตำแหน่งจริง อีกทางเลือกหนึ่งคือการส่งผ่านเส้นทางที่ถูกต้องไปยังคำสั่ง (โดยนำ --package_path มาพิจารณา) ไม่เป็นที่ต้องการเนื่องจาก:

  • โดยเปลี่ยนบรรทัดคำสั่งการดำเนินการเมื่อมีการย้ายแพ็กเกจจากรายการเส้นทางแพ็กเกจไปยังรายการอื่น (เคยเกิดขึ้นบ่อย)
  • หากเรียกใช้การดำเนินการจากระยะไกลจะให้ผลลัพธ์ในบรรทัดคำสั่งต่างกัน
  • ต้องใช้การเปลี่ยนรูปแบบบรรทัดคำสั่งเฉพาะสําหรับเครื่องมือที่ใช้งานอยู่ (พิจารณาความแตกต่างระหว่างเส้นทาง เช่น Java classpaths และ C++ รวม)
  • การเปลี่ยนบรรทัดคำสั่งของการดำเนินการจะทำให้รายการแคชการดำเนินการของการดำเนินการนั้นไม่ถูกต้อง
  • --package_path เริ่มเลิกใช้งานอย่างช้าๆ และคงที่

จากนั้น Bazel เริ่มข้ามผ่านกราฟการกระทำ (กราฟสองพาร์ท กราฟทิศทางที่ประกอบด้วยการกระทำ และอาร์ติแฟกต์อินพุตและเอาต์พุต) และการเรียกใช้การกระทำ การดำเนินการของแต่ละการดำเนินการจะแสดงด้วยอินสแตนซ์ของคลาส SkyValue ActionExecutionValue

เนื่องจากการเรียกใช้การดำเนินการมีค่าใช้จ่ายสูง เรามีการแคช 2-3 เลเยอร์ที่อาจถูกโจมตีหลัง Skyframe ดังนี้

  • ActionExecutionFunction.stateMap มีข้อมูลที่ทำให้การรีสตาร์ท Skyframe ของ ActionExecutionFunction ราคาถูก
  • แคชการดำเนินการภายในมีข้อมูลเกี่ยวกับสถานะของระบบไฟล์
  • ระบบดำเนินการระยะไกลมักจะมีแคชของตนเอง

แคชการดำเนินการภายใน

แคชนี้เป็นอีกเลเยอร์หนึ่งที่อยู่หลัง Skyframe แม้ว่าจะมีการดำเนินการซ้ำใน Skyframe แล้ว แคชก็ยังคงสามารถถูกโจมตีในแคชการกระทำในเครื่องได้ โดยจะแสดงสถานะระบบไฟล์ในเครื่องและถูกทำให้เป็นอนุกรมลงในดิสก์ ซึ่งหมายความว่าเมื่อเริ่มต้นเซิร์ฟเวอร์ Bazel ใหม่ เซิร์ฟเวอร์จะได้รับแคชการดำเนินการในเครื่องได้ แม้ว่ากราฟ Skyframe จะว่างเปล่าก็ตาม

แคชนี้จะตรวจสอบ Hit โดยใช้เมธอด ActionCacheChecker.getTokenIfNeedToExecute()

ตรงกันข้ามกับชื่อแผนที่ เพราะเป็นแผนที่จากเส้นทางของอาร์ติแฟกต์ที่ได้รับไปยังการกระทำที่ทำให้เกิดขึ้น การดำเนินการมีคำอธิบายดังนี้

  1. ชุดของไฟล์อินพุตและเอาต์พุตและการตรวจสอบข้อผิดพลาด
  2. "คีย์การดำเนินการ" ซึ่งมักจะเป็นบรรทัดคำสั่งที่เรียกใช้ แต่โดยทั่วไปจะแสดงทุกอย่างที่ไม่ได้บันทึกไว้โดย checksum ของไฟล์อินพุต (เช่น สำหรับ FileWriteAction คือ checksum ของข้อมูลที่เขียน)

นอกจากนี้ ยังมี “แคชการดำเนินการจากด้านบน” ที่อยู่ระหว่างการพัฒนา ซึ่งใช้แฮชแบบทรานซิทีฟเพื่อหลีกเลี่ยงการไปยังแคชหลายครั้ง

การค้นพบอินพุตและการตัดอินพุต

การกระทำบางอย่างซับซ้อนกว่าแค่การมีชุดอินพุต การเปลี่ยนแปลงชุดอินพุตของการดำเนินการมี 2 รูปแบบ ได้แก่

  • การกระทำอาจค้นพบอินพุตใหม่ก่อนดำเนินการหรือตัดสินว่าอินพุตบางรายการไม่จำเป็นจริงๆ ตัวอย่าง Canonical คือ C++ ซึ่งเป็นการดีกว่าที่จะคาดเดาอย่างมีหลักการว่าไฟล์ส่วนหัวใดที่ไฟล์ C++ นำไปใช้จากการปิดแบบทรานซิชัน เพื่อที่จะได้ไม่ตอบสนองต่อการส่งทุกไฟล์ไปยังผู้ดำเนินการระยะไกล เราจึงมีตัวเลือกที่จะไม่ลงทะเบียนไฟล์ส่วนหัวทุกไฟล์เป็น "input" แต่ในขณะนี้เราสแกนไฟล์ส่วนหัวสำหรับอินพุตที่มีการส่งก่อนจะ และมีเพียงคำสั่งที่ประมวลผลไว้ล่วงหน้าเท่านั้น #include
  • อาจมีการดำเนินการเกิดขึ้นว่าไฟล์บางไฟล์ไม่ได้ใช้ในระหว่างการดำเนินการ ใน C++ ไฟล์นี้เรียกว่า "ไฟล์ .d" โดยคอมไพเลอร์จะบอกว่าไฟล์ส่วนหัวใดที่มีการใช้งานหลังจากเหตุการณ์ดังกล่าว และเบเซลใช้ประโยชน์จากข้อเท็จจริงนี้เพื่อหลีกเลี่ยงความอับอายจากการมีส่วนเพิ่มที่แย่ลงกว่าของ Make วิธีนี้ให้ค่าประมาณที่ดีกว่าเครื่องมือสแกนแบบ include เนื่องจากระบบต้องใช้คอมไพเลอร์

วิธีดำเนินการต่างๆ มีดังนี้

  1. โทรหา Action.discoverInputs() ซึ่งควรแสดงผลชุดอาร์ติแฟกต์ที่ซ้อนกันซึ่งพิจารณาแล้วว่าจำเป็น ตัวแปรเหล่านี้ต้องเป็นอาร์ติแฟกต์ต้นทางเพื่อไม่ให้มีขอบของทรัพยากร Dependency ในกราฟการดำเนินการที่ไม่มีสิ่งที่เทียบเท่ากันในกราฟเป้าหมายที่กําหนดค่าไว้
  2. การดำเนินการนี้จะเรียกใช้ Action.execute()
  3. เมื่อจบ Action.execute() การดำเนินการจะเรียกใช้ Action.updateInputs() เพื่อบอก Bazel ว่าไม่จำเป็นต้องป้อนข้อมูลทั้งหมด ซึ่งอาจส่งผลให้บิลด์เพิ่มขึ้นไม่ถูกต้องหากมีการรายงานอินพุตที่ใช้เป็นไม่ได้ใช้งาน

เมื่อแคชการดำเนินการแสดงผล Hit ในอินสแตนซ์การดำเนินการใหม่ (เช่น สร้างหลังจากรีสตาร์ทเซิร์ฟเวอร์) Bazel จะเรียก updateInputs() เองเพื่อให้ชุดอินพุตแสดงผลของการค้นพบอินพุตและการตัดข้อมูลที่เคยทำก่อนหน้านี้

การดำเนินการของ Starlark จะใช้ประโยชน์จากเครื่องมือเพื่อประกาศอินพุตบางรายการว่าไม่ได้ใช้งานโดยใช้อาร์กิวเมนต์ unused_inputs_list= ของ ctx.actions.run()

วิธีเรียกใช้การกระทำต่างๆ: กลยุทธ์/ActionContexts

การดำเนินการบางอย่างสามารถเรียกใช้ได้หลายวิธี เช่น บรรทัดคำสั่งสามารถเรียกใช้ในเครื่อง ภายในเครื่อง แต่ในแซนด์บ็อกซ์ประเภทต่างๆ หรือจากระยะไกลได้ แนวคิดที่รวมสิ่งนี้เรียกว่า ActionContext (หรือ Strategy เนื่องจากเรา ประสบความสำเร็จได้เพียงครึ่งทางด้วยการเปลี่ยนชื่อ...)

วงจรชีวิตของบริบทการดำเนินการมีดังนี้

  1. เมื่อเริ่มระยะการดำเนินการ ระบบจะถามอินสแตนซ์ BlazeModule ว่ามีบริบทการดำเนินการใดบ้าง สิ่งนี้จะเกิดขึ้นในเครื่องมือสร้างของ ExecutionTool ประเภทบริบทการดำเนินการจะระบุโดยอินสแตนซ์ Java Class ที่อ้างถึงอินเทอร์เฟซย่อยของ ActionContext และอินเทอร์เฟซที่บริบทการดำเนินการต้องใช้
  2. ระบบจะเลือกบริบทการดำเนินการที่เหมาะสมจากรายการที่พร้อมใช้งาน และส่งต่อไปยัง ActionExecutionContext และ BlazeExecutor
  3. การดำเนินการจะขอบริบทโดยใช้ ActionExecutionContext.getContext() และ BlazeExecutor.getStrategy() (จริงๆ แล้วควรมีวิธีเดียวในการดำเนินการ...)

กลยุทธ์มีอิสระในการเรียกกลยุทธ์อื่นมาทำงานแทน ตัวอย่างเช่น ในกลยุทธ์แบบไดนามิกที่เริ่มต้นการดำเนินการทั้งภายในและจากระยะไกล จากนั้นจึงจะใช้ขึ้นอยู่กับว่าเหตุการณ์ใดเสร็จสิ้นก่อน

กลยุทธ์ที่เด่นอย่างหนึ่งคือกลยุทธ์ที่ใช้กระบวนการของผู้ปฏิบัติงานอย่างต่อเนื่อง (WorkerSpawnStrategy) แนวคิดคือเครื่องมือบางอย่างมีระยะเวลาเริ่มต้นนาน จึงควรนำกลับมาใช้ใหม่ระหว่างการดำเนินการต่างๆ แทนการเริ่มต้นใหม่สำหรับการดำเนินการทุกอย่าง (ซึ่งแสดงถึงปัญหาความถูกต้องที่อาจเกิดขึ้น เนื่องจาก Bazel เชื่อในว่ากระบวนการทำงานของผู้ปฏิบัติงานไม่มีสถานะที่สังเกตได้ระหว่างคำขอแต่ละรายการ)

หากเครื่องมือมีการเปลี่ยนแปลง จะต้องเริ่มต้นกระบวนการทำงานของผู้ปฏิบัติงานใหม่ การกำหนดผู้ปฏิบัติงานซ้ำว่าจะใช้ซ้ำได้หรือไม่นั้นโดยการคำนวณผลรวมตรวจสอบของเครื่องมือที่ใช้โดยใช้ WorkerFilesHash ค่านี้ขึ้นอยู่กับการทราบว่าอินพุตใดของการดำเนินการเป็นส่วนหนึ่งของเครื่องมือและอินพุตใดแสดงถึงอินพุต ซึ่งจะกำหนดโดยผู้สร้างการดำเนินการ: Spawn.getToolFiles() และการเรียกใช้ไฟล์ของ Spawn จะนับเป็นส่วนหนึ่งของเครื่องมือ

ข้อมูลเพิ่มเติมเกี่ยวกับกลยุทธ์ (หรือบริบทการดำเนินการ)

  • ดูข้อมูลเกี่ยวกับกลยุทธ์ต่างๆ ในการเรียกใช้การดำเนินการได้ที่นี่
  • ข้อมูลเกี่ยวกับกลยุทธ์แบบไดนามิก ซึ่งเราเรียกใช้การดำเนินการทั้งในเครื่องและจากระยะไกลเพื่อดูว่าระบบใดดำเนินการเสร็จสิ้นก่อน ให้ดูที่นี่
  • ดูข้อมูลเกี่ยวกับความซับซ้อนของการดำเนินการในเครื่องได้ที่นี่

ผู้จัดการทรัพยากรในเครื่อง

Bazel สามารถดำเนินการหลายอย่างควบคู่กันไป จำนวนการดำเนินการเฉพาะที่ที่ควรเรียกใช้พร้อมกันจะต่างจากการดำเนินการที่ทำได้ กล่าวคือ ยิ่งการดำเนินการต้องการทรัพยากรมาก อินสแตนซ์ควรทำงานพร้อมกันน้อยลงเพื่อไม่ให้เครื่องภายในทำงานหนักเกินไป

โดยจะนำมาใช้ในคลาส ResourceManager โดยการดำเนินการแต่ละรายการจะต้องใส่คำอธิบายประกอบพร้อมกับทรัพยากรในเครื่องโดยประมาณที่ต้องใช้ในรูปแบบของอินสแตนซ์ ResourceSet (CPU และ RAM) จากนั้นเมื่อบริบทการดำเนินการกระทำบางอย่างที่ต้องใช้ทรัพยากรในเครื่อง บริบทจะเรียกใช้ ResourceManager.acquireResources() และถูกบล็อกจนกว่าทรัพยากรที่จำเป็นจะพร้อมใช้งาน

ดูรายละเอียดเพิ่มเติมของการจัดการทรัพยากรท้องถิ่นได้ที่นี่

โครงสร้างของไดเรกทอรีเอาต์พุต

การดำเนินการแต่ละรายการต้องมีตำแหน่งแยกกันในไดเรกทอรีเอาต์พุตซึ่งจะวางเอาต์พุตไว้ ตำแหน่งของอาร์ติแฟกต์ที่ดึงมามักจะเป็นดังนี้

$EXECROOT/bazel-out/<configuration>/bin/<package>/<artifact name>

มีการกำหนดชื่อของไดเรกทอรีที่เชื่อมโยงกับการกำหนดค่าหนึ่งๆ อย่างไร มีพร็อพเพอร์ตี้ที่ต้องการที่ขัดแย้งกัน 2 รายการ ดังนี้

  1. หากการกำหนดค่า 2 รายการเกิดขึ้นในบิลด์เดียวกันได้ ทั้ง 2 ควรมีไดเรกทอรีต่างกันเพื่อให้ทั้งสองมีเวอร์ชันการดำเนินการเดียวกัน มิฉะนั้น หากการกำหนดค่าทั้ง 2 รายการขัดแย้งกัน เช่น บรรทัดคำสั่งของการดำเนินการที่สร้างไฟล์เอาต์พุตเดียวกัน Bazel จะไม่รู้ว่าจะเลือกการดำเนินการใด ("ข้อขัดแย้งของการดำเนินการ")
  2. หากการกำหนดค่า 2 รายการกล่าวถึงสิ่งเดียวกันอย่าง "คล้ายคลึง" การกำหนดค่าดังกล่าวควรมีชื่อเดียวกันเพื่อให้การดำเนินการที่ดำเนินการในรายการหนึ่งสามารถใช้ซ้ำสำหรับอีกรายการหนึ่งได้ในกรณีที่บรรทัดคำสั่งตรงกัน เช่น การเปลี่ยนแปลงตัวเลือกบรรทัดคำสั่งในคอมไพเลอร์ Java ไม่ควรทำให้การดำเนินการคอมไพล์ C++ ถูกเรียกใช้ซ้ำ

ตอนนี้เรายังคิดไม่ถึงวิธีหลักในการแก้ปัญหานี้ ซึ่งคล้ายกับโจทย์การตัดการกำหนดค่า ดูการสนทนาเพิ่มเติม เกี่ยวกับตัวเลือกได้ที่นี่ ส่วนที่เป็นปัญหาหลักๆ คือกฎของ Starlark (ซึ่งผู้เขียนมักไม่ค่อยคุ้นเคยกับ Bazel) และแง่มุมต่างๆ ซึ่งช่วยเพิ่มมิติของสิ่งที่สามารถสร้างไฟล์เอาต์พุต "เดียวกัน" ได้

แนวทางในปัจจุบันคือให้กลุ่มเส้นทางสำหรับการกำหนดค่าคือ <CPU>-<compilation mode> โดยเพิ่มคำต่อท้ายที่หลากหลายเพื่อให้การเปลี่ยนการกำหนดค่าที่ใช้งานใน Java ไม่ส่งผลให้การดำเนินการขัดแย้งกัน นอกจากนี้ยังมีการเพิ่ม Checksum ของชุดการเปลี่ยนการกำหนดค่า Starlark เพื่อไม่ให้ผู้ใช้สร้างความขัดแย้งของการดำเนินการ มันยังไม่ใช่คำตอบสมบูรณ์แบบ ซึ่งจะนำไปใช้ใน OutputDirectories.buildMnemonic() และใช้ส่วนย่อยการกำหนดค่าแต่ละรายการที่เพิ่มส่วนของตัวเองไปยังชื่อของไดเรกทอรีเอาต์พุต

การทดสอบ

Bazel รองรับการทดสอบมากมาย โดยรองรับข้อมูลต่อไปนี้

  • เรียกใช้การทดสอบจากระยะไกล (หากมีแบ็กเอนด์การดำเนินการระยะไกล)
  • ทำการทดสอบหลายครั้งพร้อมกัน (สำหรับลบหรือรวบรวมข้อมูลเวลา)
  • การทดสอบชาร์ดดิ้ง (แยกกรอบการทดสอบในการทดสอบเดียวกันผ่านหลายๆ กระบวนการเพื่อความรวดเร็ว)
  • ทำการทดสอบที่ไม่น่าเชื่อถืออีกครั้ง
  • จัดกลุ่มการทดสอบเป็นชุดทดสอบ

การทดสอบเป็นเป้าหมายที่กำหนดค่าไว้ตามปกติซึ่งมี TestProvider ซึ่งจะอธิบายวิธีดำเนินการทดสอบ ดังนี้

  • อาร์ติแฟกต์ที่มีอาคารซึ่งอยู่ระหว่างการทดสอบ นี่คือไฟล์ "สถานะแคช" ที่มีข้อความ TestResultData แบบต่อเนื่อง
  • จำนวนครั้งที่ทำการทดสอบ
  • จำนวนชาร์ดที่ควรแยกการทดสอบเป็น
  • พารามิเตอร์บางอย่างเกี่ยวกับวิธีทำการทดสอบ (เช่น ระยะหมดเวลาของการทดสอบ)

การกำหนดการทดสอบที่จะดำเนินการ

การระบุการทดสอบที่จะใช้เป็นกระบวนการที่ซับซ้อน

ประการแรก ระหว่างการแยกวิเคราะห์รูปแบบเป้าหมาย ชุดทดสอบจะขยายซ้ำแบบซ้ำๆ ใช้การขยายใน TestsForTargetPatternFunction แล้ว มีรอยย่นที่ค่อนข้างน่าประหลาดใจคือ หากชุดทดสอบประกาศว่าไม่มีการทดสอบ จะหมายถึงการทดสอบทั้งหมดที่อยู่ในกล่องพัสดุ ซึ่งนำมาใช้ใน Package.beforeBuild() ด้วยการเพิ่มแอตทริบิวต์โดยนัยชื่อว่า $implicit_tests ในการทดสอบกฎชุดโปรแกรม

จากนั้นจะมีการกรองขนาด แท็ก ระยะหมดเวลา และภาษาตามตัวเลือกบรรทัดคำสั่ง ซึ่งจะนำไปใช้ใน TestFilter และจะเรียกใช้จาก TargetPatternPhaseFunction.determineTests() ในระหว่างการแยกวิเคราะห์เป้าหมายและนำผลลัพธ์ไปไว้ใน TargetPatternPhaseValue.getTestsToRunLabels() สาเหตุที่แอตทริบิวต์ของกฎกรองกำหนดค่าไม่ได้ก็คือเกิดขึ้นก่อนช่วงการวิเคราะห์ การกำหนดค่าจึงไม่พร้อมใช้งาน

จากนั้นระบบจะประมวลผลเพิ่มเติมใน BuildView.createResult() กล่าวคือ เป้าหมายที่การวิเคราะห์ล้มเหลวจะถูกกรองออก และการทดสอบจะแยกเป็นการทดสอบแบบเฉพาะตัวและไม่จำกัด จากนั้นระบบจะป้อน AnalysisResult ซึ่งช่วยให้ ExecutionTool รู้ว่าต้องทำการทดสอบใด

เพื่อเพิ่มความโปร่งใสในกระบวนการที่ซับซ้อนนี้ โอเปอเรเตอร์การค้นหา tests() (ที่ใช้ใน TestsFunction) พร้อมระบุว่าการทดสอบใดจะทํางานเมื่อมีการระบุเป้าหมายหนึ่งๆ ในบรรทัดคำสั่ง น่าเสียดายที่การนำไปใช้งานใหม่อาจต่างจากเดิมในหลายด้านมาก

กำลังทดสอบ

วิธีทำการทดสอบคือขออาร์ติแฟกต์สถานะแคช จากนั้นจะส่งผลให้เกิดการดำเนินการของ TestRunnerAction ซึ่งจะเรียกใช้ TestActionContext ที่เลือกไว้โดยตัวเลือกบรรทัดคำสั่ง --test_strategy ซึ่งจะทำการทดสอบตามวิธีที่ขอ

การทดสอบจะดำเนินการตามโปรโตคอลที่ละเอียดซึ่งใช้ตัวแปรสภาพแวดล้อมเพื่อบอกให้การทดสอบทราบถึงสิ่งที่คาดหวังจากตัวแปรนั้น คำอธิบายโดยละเอียดเกี่ยวกับสิ่งที่ Bazel คาดหวังจากการทดสอบ และการทดสอบที่จะคาดหวังจาก Bazel ได้ที่นี่ หากง่ายที่สุด รหัสการออกเป็น 0 หมายถึงสำเร็จ หรืออะไรก็ได้หมายถึงความล้มเหลว

นอกจากไฟล์สถานะแคชแล้ว กระบวนการทดสอบแต่ละครั้งยังปล่อยไฟล์อื่นๆ อีกจำนวนหนึ่งด้วย โดยจะใส่ไว้ใน "ไดเรกทอรีบันทึกการทดสอบ" ซึ่งเป็นไดเรกทอรีย่อยที่ชื่อ testlogs ของไดเรกทอรีเอาต์พุตของการกำหนดค่าเป้าหมาย ดังนี้

  • test.xml ไฟล์ XML สไตล์ JUnit ที่แสดงรายละเอียดกรอบการทดสอบแต่ละรายการในชาร์ดทดสอบ
  • test.log เอาต์พุตคอนโซลของการทดสอบ ไม่ได้แยก Stdout และ stderr ออก
  • test.outputs หรือ "ไดเรกทอรีเอาต์พุตที่ไม่ได้ประกาศ" ซึ่งใช้โดยการทดสอบที่ต้องการส่งออกไฟล์นอกเหนือจากข้อมูลที่พิมพ์ไปยังเทอร์มินัล

มี 2 สิ่งที่อาจเกิดขึ้นระหว่างการดำเนินการทดสอบซึ่งทำไม่ได้ระหว่างการสร้างเป้าหมายปกติ ได้แก่ การดำเนินการทดสอบพิเศษและการสตรีมเอาต์พุต

การทดสอบบางอย่างต้องดำเนินการในโหมดพิเศษ เช่น ดำเนินการควบคู่กับการทดสอบอื่นๆ โดยป้อน tags=["exclusive"] ลงในกฎการทดสอบหรือเรียกใช้การทดสอบด้วย --test_strategy=exclusive การทดสอบพิเศษแต่ละรายการจะเรียกใช้โดยการเรียกใช้ Skyframe แยกต่างหากที่ขอการดำเนินการทดสอบหลังจากบิลด์ "หลัก" วิธีนี้ใช้ใน SkyframeExecutor.runExclusiveTest()

ผู้ใช้จะขอเอาต์พุตของการทดสอบที่จะสตรีมเพื่อรับข้อมูลเกี่ยวกับความคืบหน้าของการทดสอบที่ใช้เวลานานได้ ซึ่งต่างจากการดำเนินการปกติที่ระบบจะส่งออกเอาต์พุตเทอร์มินัลเมื่อการดำเนินการเสร็จสิ้น ซึ่งระบุโดยตัวเลือกบรรทัดคำสั่ง --test_output=streamed และบอกเป็นนัยถึงการดำเนินการทดสอบพิเศษเพื่อไม่ให้เอาต์พุตของการทดสอบต่างๆ กระจัดกระจาย

ระบบจะนําวิธีนี้ไปใช้ในคลาส StreamedTestOutput ที่มีชื่อที่เหมาะสมและทําการสำรวจการเปลี่ยนแปลงไฟล์ test.log ของการทดสอบดังกล่าวและทิ้งไบต์ใหม่ไปยังเทอร์มินัลที่ Bazel ใช้กฎ

ผลการทดสอบที่ดำเนินการแล้วจะอยู่ในรถบัสกิจกรรมโดยสังเกตเหตุการณ์ต่างๆ (เช่น TestAttempt, TestResult หรือ TestingCompleteEvent) ซึ่งจะถูกถ่ายโอนไปยังโปรโตคอลกิจกรรมบิลด์ และจะส่งออกไปที่คอนโซลโดย AggregatingTestListener

คอลเล็กชันการครอบคลุม

การครอบคลุมจะรายงานโดยการทดสอบในรูปแบบ LCOV ในไฟล์ bazel-testlogs/$PACKAGE/$TARGET/coverage.dat

หากต้องการรวบรวมการครอบคลุม การดำเนินการทดสอบแต่ละครั้งจะรวมอยู่ในสคริปต์ที่ชื่อ collect_coverage.sh

สคริปต์นี้ตั้งค่าสภาพแวดล้อมของการทดสอบเพื่อเปิดใช้การรวบรวมการครอบคลุมและกำหนดตำแหน่งที่รันไทม์การครอบคลุมเขียนไฟล์การครอบคลุม จากนั้นจึงทำการทดสอบ การทดสอบอาจเรียกใช้กระบวนการย่อยหลายรายการและประกอบด้วยส่วนต่างๆ ที่เขียนขึ้นในภาษาโปรแกรมหลายภาษา (โดยมีรันไทม์การรวบรวมหน้าปกแยกต่างหาก) สคริปต์ Wrapper มีหน้าที่แปลงไฟล์ผลลัพธ์เป็นรูปแบบ LCOV หากจำเป็น และรวมไฟล์เหล่านั้นเป็นไฟล์เดียว

การแทรกแซงของ collect_coverage.sh ดำเนินการโดยกลยุทธ์การทดสอบและกำหนดให้ collect_coverage.sh อยู่ในข้อมูลที่ป้อนของการทดสอบ ซึ่งเกิดขึ้นได้พร้อมกับแอตทริบิวต์แบบไม่เจาะจงปลายทาง :coverage_support ซึ่งได้รับการแก้ไขให้เป็นค่าของแฟล็กการกำหนดค่า --coverage_support (ดู TestConfiguration.TestOptions.coverageSupport)

บางภาษามีการวัดคุมแบบออฟไลน์ ซึ่งหมายความว่าจะมีการเพิ่มการวัดคุมการครอบคลุมในเวลาคอมไพล์ (เช่น C++) ส่วนบางภาษาจะทำการวัดคุมทางออนไลน์ ซึ่งหมายความว่าจะมีการเพิ่มการวัดคุมการครอบคลุมขณะดำเนินการ

แนวคิดหลักอีกประการคือการครอบคลุมพื้นฐาน นี่คือการครอบคลุมของไลบรารี ไบนารี หรือการทดสอบ หากไม่มีการเรียกใช้โค้ด ปัญหาที่เครื่องมือนี้แก้ไขได้คือ หากต้องการคำนวณการครอบคลุมของการทดสอบสำหรับไบนารี การผสานรวมการครอบคลุมของการทดสอบทั้งหมดนั้นไม่เพียงพอ เนื่องจากอาจมีโค้ดในไบนารีที่ไม่ได้ลิงก์กับการทดสอบใดๆ ดังนั้น เราจึงทำเพื่อปล่อยไฟล์การครอบคลุมสำหรับทุกไบนารีที่มีเฉพาะไฟล์ที่เรารวบรวมความครอบคลุมโดยไม่มีบรรทัดที่ครอบคลุม ไฟล์พื้นฐานที่ครอบคลุมของเป้าหมายคือ bazel-testlogs/$PACKAGE/$TARGET/baseline_coverage.dat นอกจากนี้ยังสร้างขึ้นสำหรับไบนารีและไลบรารีนอกเหนือจากการทดสอบหากคุณส่ง Flag --nobuild_tests_only ไปยัง Bazel

การครอบคลุมของเกณฑ์พื้นฐานไม่สามารถใช้งานได้ในขณะนี้

เราติดตามไฟล์ 2 กลุ่มสำหรับการรวบรวมการครอบคลุมสำหรับกฎแต่ละข้อ ได้แก่ ชุดไฟล์การวัดคุมและชุดไฟล์ข้อมูลเมตาของการวัดคุม

ชุดของไฟล์อุปกรณ์ก็คือชุดของไฟล์ที่ใช้วัด สำหรับรันไทม์การครอบคลุมของออนไลน์ สามารถใช้การตั้งค่านี้ขณะรันไทม์เพื่อเลือกไฟล์ที่จะใช้วัด นอกจากนี้ยังใช้เพื่อให้ครอบคลุมพื้นฐานด้วย

ชุดไฟล์ข้อมูลเมตาของการวัดคุมคือชุดไฟล์พิเศษที่การทดสอบต้องใช้ในการสร้างไฟล์ LCOV ที่ Bazel ต้องการ ในทางปฏิบัติ ไฟล์นี้ประกอบด้วยไฟล์เฉพาะรันไทม์ เช่น gcc จะส่งไฟล์ .gcno ระหว่างการคอมไพล์ ซึ่งจะเพิ่มลงในชุดอินพุตของการดำเนินการทดสอบหากเปิดใช้โหมดการครอบคลุม

ไม่ว่าจะมีการรวบรวมการครอบคลุมหรือไม่ใน BuildConfiguration วิธีนี้เป็นประโยชน์เพราะเป็นวิธีง่ายๆ ในการเปลี่ยนแปลงการดำเนินการทดสอบและกราฟการทำงานที่ขึ้นอยู่กับบิตนี้ และยังหมายความว่าหากมีการกลับบิตนี้ จะต้องมีการวิเคราะห์เป้าหมายทั้งหมดใหม่ (บางภาษา เช่น C++ ต้องใช้ตัวเลือกคอมไพเลอร์ที่แตกต่างกันไปในการปล่อยโค้ดที่สามารถรวบรวมการครอบคลุม ซึ่งช่วยลดปัญหานี้ได้พอสมควร เนื่องจากต้องมีการวิเคราะห์อีกครั้ง)

ไฟล์สนับสนุนการครอบคลุมจะขึ้นอยู่กับป้ายกำกับแบบขึ้นต่อกันโดยนัยเพื่อให้นโยบายการเรียกใช้ลบล้างได้ ซึ่งทำให้ไฟล์ Bazel แต่ละเวอร์ชันต่างกัน ตามหลักแล้ว เราจะนำความแตกต่างเหล่านี้ออกไป และเราได้กำหนดหนึ่งในความแตกต่างเหล่านี้ให้เป็นมาตรฐานเดียวกัน

นอกจากนี้ เรายังสร้าง "รายงานการครอบคลุม" ซึ่งรวมการครอบคลุมที่เก็บรวบรวมไว้สำหรับการทดสอบทุกรายการในการเรียกใช้ Bazel ด้วย ซึ่งจัดการโดย CoverageReportActionFactory และเรียกใช้จาก BuildView.createResult() และเข้าถึงเครื่องมือที่จำเป็นโดยดูที่แอตทริบิวต์ :coverage_report_generator ของการทดสอบแรกที่ดำเนินการ

เครื่องมือการค้นหา

บาเซลใช้ภาษาง่ายๆ ในการถามเรื่องต่างๆ เกี่ยวกับกราฟต่างๆ ประเภทคำค้นหา มีดังต่อไปนี้

  • bazel query จะใช้ในการตรวจสอบกราฟเป้าหมาย
  • bazel cquery จะใช้ในการตรวจสอบกราฟเป้าหมายที่กำหนดค่าไว้
  • bazel aquery จะใช้เพื่อตรวจสอบกราฟการดำเนินการ

แต่ละรายการเหล่านี้ติดตั้งใช้งานโดยการแยกประเภทย่อย AbstractBlazeQueryEnvironment ฟังก์ชันการค้นหาเพิ่มเติมอื่นๆ ทำได้โดยการแยกประเภทย่อย QueryFunction หากต้องการอนุญาตผลการค้นหาสตรีมมิงแทนการรวบรวมข้อมูลไปยังโครงสร้างข้อมูลบางอย่าง ระบบจะส่ง query2.engine.Callback ไปยัง QueryFunction ซึ่งจะเรียกใช้ผลการค้นหาที่ต้องการแสดง

ผลลัพธ์ของคำค้นหาส่งได้หลายวิธี เช่น ป้ายกำกับ, ป้ายกำกับและคลาสกฎ, XML, protobuf เป็นต้น พารามิเตอร์เหล่านี้มีการใช้งานเป็นคลาสย่อยของ OutputFormatter

ข้อกำหนดที่ละเอียดอ่อนของรูปแบบเอาต์พุตการค้นหาบางรูปแบบ (โปรโตคอลแน่นอน) คือ Bazel จะต้องแสดง _all _ข้อมูลที่การโหลดแพ็กเกจให้มาเพื่อให้สามารถแยกความแตกต่างของเอาต์พุตและระบุได้ว่าเป้าหมายหนึ่งๆ มีการเปลี่ยนแปลงหรือไม่ ดังนั้นค่าแอตทริบิวต์จึงต้องทำให้เป็นอนุกรมได้ จึงเป็นเหตุผลที่แอตทริบิวต์มีเพียงไม่กี่ประเภทที่ไม่มีแอตทริบิวต์ที่มีค่า Starlark ซับซ้อน วิธีหลีกเลี่ยงปัญหาทั่วไปคือการใช้ป้ายกำกับและแนบข้อมูลที่ซับซ้อนกับกฎที่มีป้ายกำกับนั้น วิธีนี้ไม่ใช่วิธีแก้ปัญหาที่น่าพึงพอใจอย่างยิ่ง และจะยกเลิกข้อกำหนดนี้ได้ดี

ระบบโมดูล

คุณสามารถขยาย Bazel ได้โดยเพิ่มโมดูลลงไป แต่ละโมดูลต้องมีคลาสย่อย BlazeModule (ชื่อนี้เป็นประวัติของ Bazel ที่เคยเรียกว่า Blaze) และรับข้อมูลเกี่ยวกับเหตุการณ์ต่างๆ ระหว่างการใช้คำสั่ง

โดยส่วนใหญ่จะใช้สำหรับใช้ฟังก์ชันต่างๆ ที่ "ไม่ใช่หลัก" ซึ่งมีเพียง Bazel บางเวอร์ชัน (เช่น เวอร์ชันที่เราใช้ที่ Google) ที่ต้องการ

  • การเชื่อมต่อกับระบบปฏิบัติการระยะไกล
  • คำสั่งใหม่

ชุดจุดต่อที่ BlazeModule ข้อเสนอนั้นไม่เป็นธรรมชาติ อย่าใช้เป็นตัวอย่างของหลักการออกแบบที่ดี

รถบัสในงาน

วิธีหลักที่ BlazeModules สื่อสารกับส่วนที่เหลือของ Bazel ได้คือการใช้รถบัสกิจกรรม (EventBus): ระบบจะสร้างอินสแตนซ์ใหม่สำหรับทุกบิลด์ โดยส่วนต่างๆ ของ Bazel จะโพสต์กิจกรรมลงในบิลด์ได้และโมดูลสามารถลงทะเบียน Listener สำหรับกิจกรรมที่สนใจได้ ตัวอย่างเช่น รายการต่อไปนี้จะแสดงเป็นเหตุการณ์

  • กำหนดรายการเป้าหมายของบิลด์ที่จะสร้างแล้ว (TargetParsingCompleteEvent)
  • กำหนดการกำหนดค่าระดับบนสุดแล้ว (BuildConfigurationEvent)
  • สร้างเป้าหมายสำเร็จหรือไม่ (TargetCompleteEvent)
  • มีการทดสอบ (TestAttempt, TestSummary)

เหตุการณ์เหล่านี้บางเหตุการณ์จะแสดงนอก Bazel ในโปรโตคอลเหตุการณ์ของบิลด์ (ซึ่งก็คือ BuildEvent) วิธีนี้ไม่เพียงช่วยให้ BlazeModule เท่านั้น แต่ยังรวมถึงสิ่งที่อยู่นอกกระบวนการ Bazel ในการสังเกตบิลด์ด้วย ไฟล์เหล่านี้เข้าถึงได้ในรูปแบบไฟล์ที่มีข้อความโปรโตคอล หรือ Bazel เชื่อมต่อกับเซิร์ฟเวอร์ (เรียกว่าบริการกิจกรรมบิลด์) เพื่อสตรีมเหตุการณ์ได้

ซึ่งจะมีการใช้งานในแพ็กเกจ Java build.lib.buildeventservice และ build.lib.buildeventstream

ที่เก็บภายนอก

ในขณะที่ Bazel ได้รับการออกแบบมาเพื่อใช้ในโมโนเรโป (ต้นไม้ต้นเดียวที่มีทุกอย่างที่ต้องการในการสร้าง) แต่ Bazel อาศัยอยู่ในโลกซึ่งไม่เป็นความจริงเสมอไป "ที่เก็บภายนอก" เป็นกระบวนการนามธรรมที่ใช้ในการเชื่อมโยง 2 โลกเข้าด้วยกัน โดยเป็นโค้ดที่จำเป็นสำหรับบิลด์ แต่ไม่ได้อยู่ในแผนผังต้นทางหลัก

ไฟล์ WORKSPACE

ชุดที่เก็บภายนอกจะกำหนดโดยการแยกวิเคราะห์ไฟล์ WORKSPACE เช่น การประกาศที่มีลักษณะดังนี้

    local_repository(name="foo", path="/foo/bar")

ผลลัพธ์ในที่เก็บที่ชื่อ @foo กำลังพร้อมใช้งาน ส่วนที่ทำให้ซับซ้อนขึ้นก็คือคุณสามารถกำหนดกฎที่เก็บใหม่ในไฟล์ Starlark เพื่อโหลดโค้ด Starlark ใหม่ ซึ่งสามารถใช้เพื่อกำหนดกฎที่เก็บใหม่และอื่นๆ อีกมากมาย...

ในการจัดการกรณีนี้ การแยกวิเคราะห์ไฟล์ WORKSPACE (ใน WorkspaceFileFunction) จะแบ่งออกเป็นส่วนๆ ตามคำสั่ง load() ดัชนีกลุ่มจะระบุด้วย WorkspaceFileKey.getIndex() และการคำนวณ WorkspaceFileFunction จนถึงดัชนี X หมายถึงการประเมินจนถึงคำสั่ง load() ที่ X

กำลังดึงข้อมูลที่เก็บ

ต้องfetchedโค้ดของที่เก็บก่อนที่จะพร้อมใช้งาน Bazel การดำเนินการนี้จะทำให้ Bazel สร้างไดเรกทอรีภายใต้ $OUTPUT_BASE/external/<repository name>

การดึงข้อมูลที่เก็บจะเกิดขึ้นตามขั้นตอนต่อไปนี้

  1. PackageLookupFunction พบว่าต้องการที่เก็บและสร้าง RepositoryName เป็น SkyKey ซึ่งจะเรียกใช้ RepositoryLoaderFunction
  2. RepositoryLoaderFunction ส่งต่อคำขอไปยัง RepositoryDelegatorFunction ด้วยเหตุผลที่ไม่ชัดเจน (โค้ดระบุว่าต้องหลีกเลี่ยงการดาวน์โหลดสิ่งต่างๆ ซ้ำในกรณีที่ Skyframe รีสตาร์ท แต่ไม่ใช่การให้เหตุผลที่ชัดเจน)
  3. RepositoryDelegatorFunction ค้นหากฎที่เก็บที่มีการขอให้ดึงข้อมูลโดยทำซ้ำส่วนต่างๆ ของไฟล์ WORKSPACE จนกว่าจะพบที่เก็บที่ขอ
  4. พบ RepositoryFunction ที่เหมาะสมซึ่งใช้การดึงข้อมูลที่เก็บ โดยอาจเป็นการใช้งานที่เก็บของ Starlark หรือแผนที่แบบฮาร์ดโค้ดสำหรับที่เก็บที่มีการใช้งานใน Java

การแคชมีหลายเลเยอร์เนื่องจากการดึงข้อมูลที่เก็บมีราคาแพงมาก

  1. มีแคชสำหรับไฟล์ที่ดาวน์โหลดซึ่งกำหนดคีย์โดยการตรวจสอบข้อผิดพลาด (RepositoryCache) ซึ่งกำหนดให้ไฟล์ตรวจสอบข้อผิดพลาดพร้อมใช้งานในไฟล์ WORKSPACE แต่ก็ยังคงเป็นไปอย่างเรียบร้อยดี ซึ่งจะแชร์โดยอินสแตนซ์ของเซิร์ฟเวอร์ Bazel ทุกรายการในเวิร์กสเตชันเดียวกัน ไม่ว่าอินสแตนซ์เหล่านั้นจะอยู่ในพื้นที่ทำงานหรือฐานเอาต์พุตใดก็ตาม
  2. มีการเขียน "ไฟล์เครื่องหมาย" สำหรับที่เก็บแต่ละแห่งใน $OUTPUT_BASE/external ซึ่งมี checksum ของกฎที่ใช้ในการดึงข้อมูล หากเซิร์ฟเวอร์ Bazel รีสตาร์ท แต่ checksum ไม่เปลี่ยนแปลง ระบบจะไม่ดึงข้อมูลอีกครั้ง เรานำนโยบายนี้มาใช้ใน RepositoryDelegatorFunction.DigestWriter
  3. ตัวเลือกบรรทัดคำสั่ง --distdir จะกำหนดแคชอื่นที่ใช้ค้นหาอาร์ติแฟกต์ที่จะดาวน์โหลด วิธีนี้เป็นประโยชน์ในการตั้งค่าระดับองค์กร ซึ่ง Bazel ไม่ควรดึงข้อมูลแบบสุ่มจากอินเทอร์เน็ต ซึ่งดำเนินการโดย DownloadManager

เมื่อดาวน์โหลดที่เก็บแล้ว อาร์ติแฟกต์ในที่เก็บจะถือว่าเป็นอาร์ติแฟกต์ต้นทาง สิ่งนี้ทำให้เกิดปัญหาเนื่องจากโดยปกติแล้ว Bazel จะตรวจสอบเวอร์ชันล่าสุดของอาร์ติแฟกต์ต้นทางด้วยการเรียกใช้ stat() และอาร์ติแฟกต์เหล่านี้จะถูกทำให้ไม่ถูกต้องอีกด้วยเมื่อคำจำกัดความของที่เก็บมีการเปลี่ยนแปลง ดังนั้น FileStateValue สำหรับอาร์ติแฟกต์ในที่เก็บภายนอกจึงจำเป็นต้องใช้ที่เก็บภายนอก ซึ่งจัดการโดย ExternalFilesHelper

ไดเรกทอรีที่มีการจัดการ

บางครั้งที่เก็บภายนอกจำเป็นต้องแก้ไขไฟล์ภายใต้รูทของพื้นที่ทำงาน (เช่น ตัวจัดการแพ็กเกจที่เก็บแพ็กเกจที่ดาวน์โหลดไว้ในไดเรกทอรีย่อยของโครงสร้างต้นทาง) วิธีนี้ไม่เหมาะกับการสันนิษฐานว่า Bazel ทำให้ไฟล์ต้นฉบับได้รับการแก้ไขโดยผู้ใช้เท่านั้น ไม่ใช่โดยตัวมันเอง และช่วยให้แพ็กเกจอ้างอิงทุกไดเรกทอรีภายใต้รูทของพื้นที่ทำงานได้ Bazel ทำ 2 สิ่งต่อไปนี้เพื่อให้ที่เก็บภายนอกแบบนี้ใช้งานได้

  1. อนุญาตให้ผู้ใช้ระบุไดเรกทอรีย่อยของพื้นที่ทำงาน Bazel สามารถเข้าถึงได้ โดยจะแสดงอยู่ในไฟล์ชื่อ .bazelignore และมีการใช้งานฟังก์ชันนี้ใน BlacklistedPackagePrefixesFunction
  2. เราเข้ารหัสการแมปจากไดเรกทอรีย่อยของพื้นที่ทำงานไปยังที่เก็บภายนอกซึ่งจัดการโดย ManagedDirectoriesKnowledge และจัดการ FileStateValue ที่อ้างอิงถึงรายการดังกล่าวในวิธีเดียวกับที่เก็บสำหรับที่เก็บภายนอกปกติ

การแมปที่เก็บ

ที่เก็บหลายรายการอาจต้องการใช้ที่เก็บเดียวกัน แต่เป็นเวอร์ชันที่ต่างกัน (นี่เป็นอินสแตนซ์ของ "ปัญหาทรัพยากร Dependency แบบไดมอนด์") เช่น หากไบนารี 2 รายการในที่เก็บที่แยกกันในบิลด์ต้องการพึ่งพา Guava น่าจะอ้างอิง Guava ทั้งสองพร้อมป้ายกำกับที่เริ่มต้น @guava// และคาดหวังว่ามันจะหมายถึงมันคนละเวอร์ชัน

ดังนั้น Bazel จึงอนุญาตให้รายการหนึ่งแมปป้ายกำกับที่เก็บภายนอกอีกครั้งเพื่อให้สตริง @guava// อ้างถึงที่เก็บ Guava หนึ่ง (เช่น @guava1//) ในที่เก็บของไบนารีหนึ่งและที่เก็บ Guava อีกอัน (เช่น @guava2//) กับที่เก็บของอีกแห่งได้

หรือจะใช้เพื่อjoinเพชรก็ได้ หากที่เก็บขึ้นอยู่กับ @guava1// และอีกรายการขึ้นอยู่กับ @guava2// การแมปที่เก็บจะช่วยให้ 1 แมปที่เก็บทั้ง 2 รายการได้อีกครั้งเพื่อใช้ที่เก็บ Canonical @guava//

มีการระบุการแมปไว้ในไฟล์ WORKSPACE เป็นแอตทริบิวต์ repo_mapping ของการกำหนดที่เก็บแต่ละรายการ จากนั้น Skyframe จะปรากฏโดยเป็นสมาชิกของ WorkspaceFileValue ซึ่งจะเชื่อมโยงกับสิ่งต่อไปนี้

  • Package.Builder.repositoryMapping ซึ่งใช้ในการแปลงแอตทริบิวต์ที่มีค่าป้ายกำกับของกฎในแพ็กเกจตาม RuleClass.populateRuleAttributeValues()
  • Package.repositoryMapping ที่ใช้ในขั้นตอนการวิเคราะห์ (สำหรับการแก้ไขสิ่งต่างๆ เช่น $(location) ซึ่งไม่ได้แยกวิเคราะห์ในขั้นตอนการโหลด)
  • BzlLoadFunction สำหรับการแปลป้ายกำกับในคำสั่งload()

บิต JNI

เซิร์ฟเวอร์ของ Bazel นั้น_ ส่วนใหญ่เป็น _write ใน Java แต่มีข้อยกเว้นคือส่วนที่ Java ไม่สามารถทำได้เองหรือทำไม่ได้ในตอนที่ใช้ ซึ่งส่วนใหญ่แล้วจะจำกัดไว้เพียงแค่การโต้ตอบกับระบบไฟล์ การควบคุมกระบวนการ และสิ่งอื่นๆ ในระดับต่ำ

โค้ด C++ จะอยู่ภายใต้ src/main/native และคลาส Java ที่มีเมธอดแบบเนทีฟ ได้แก่

  • NativePosixFilesและNativePosixFileSystem
  • ProcessUtils
  • WindowsFileOperationsและWindowsFileProcesses
  • com.google.devtools.build.lib.platform

เอาต์พุตของคอนโซล

การปล่อยเอาต์พุตของคอนโซลดูเหมือนจะเป็นเรื่องง่าย แต่ผลที่ตามมาจากการเรียกใช้กระบวนการหลายๆ อย่าง (บางครั้งจากระยะไกล) การแคชแบบละเอียด ความต้องการเอาต์พุตเทอร์มินัลที่มีสีสันและสวยงาม และการมีเซิร์ฟเวอร์ที่ทำงานเป็นเวลานานทำให้เป็นงานที่ไม่สำคัญ

หลังจากเรียกใช้ RPC จากไคลเอ็นต์ ระบบจะสร้างอินสแตนซ์ RpcOutputStream 2 รายการ (สำหรับ Stdout และ Stderr) เพื่อส่งต่อข้อมูลที่พิมพ์ลงในไคลเอ็นต์ จากนั้นจะรวมไว้ใน OutErr (คู่ (stdout, stderr)) ทุกสิ่งที่ต้องพิมพ์บนคอนโซลจะผ่านสตรีมเหล่านี้ จากนั้นระบบจะส่งสตรีมเหล่านี้ให้กับ BlazeCommandDispatcher.execExclusively()

โดยค่าเริ่มต้น เอาต์พุตจะพิมพ์โดยมีลำดับการยกเว้น ANSI เมื่อคำขอเหล่านี้ไม่เป็นที่ต้องการ (--color=no) จะถูก AnsiStrippingOutputStream ตัดออก นอกจากนี้ ระบบจะเปลี่ยนเส้นทาง System.out และ System.err ไปยังสตรีมเอาต์พุตเหล่านี้ ทั้งนี้เพื่อให้พิมพ์ข้อมูลการแก้ไขข้อบกพร่องโดยใช้ System.err.println() ได้ และสุดท้ายยังได้ไปยังเอาต์พุตเทอร์มินัลของไคลเอ็นต์ (ซึ่งต่างจากเซิร์ฟเวอร์) โปรดใช้ความระมัดระวังหากกระบวนการสร้างเอาต์พุตไบนารี (เช่น bazel query --output=proto) จะไม่มีการบิดเบือน Stout เกิดขึ้น

ข้อความสั้นๆ (ข้อผิดพลาด คำเตือน และสิ่งอื่นๆ ที่คล้ายกัน) จะแสดงผ่านอินเทอร์เฟซ EventHandler ที่สำคัญคือข้อความเหล่านี้แตกต่างจากสิ่งที่โพสต์ลงใน EventBus (ทำให้เกิดความสับสน) Event แต่ละรายการมี EventKind (ข้อผิดพลาด คำเตือน ข้อมูล และอื่นๆ อีก 2-3 รายการ) และอาจมี Location (ตำแหน่งในซอร์สโค้ดที่ทำให้เหตุการณ์เกิดขึ้น)

การติดตั้งใช้งาน EventHandler บางรายการจะจัดเก็บเหตุการณ์ที่ได้รับ ซึ่งใช้ในการเล่นข้อมูลซ้ำใน UI ที่เกิดจากการประมวลผลที่แคชไว้ประเภทต่างๆ เช่น คำเตือนที่มาจากเป้าหมายที่กำหนดค่าไว้ในแคช

นอกจากนี้ EventHandler บางแห่งยังอนุญาตให้โพสต์กิจกรรมที่พบกับเส้นทางไปรถบัสของกิจกรรมด้วย (Event ทั่วไปจะ _ไม่ _ปรากฏที่นี่) เวอร์ชันเหล่านี้คือการใช้งาน ExtendedEventHandler และการใช้งานหลักคือการเล่นเหตุการณ์ EventBus ที่แคชไว้ซ้ำ เหตุการณ์ EventBus ทั้งหมดเหล่านี้ใช้ Postable แต่ไม่ใช่ทุกรายการที่โพสต์ใน EventBus จะต้องติดตั้งใช้งานอินเทอร์เฟซนี้ เฉพาะรายการที่แคชโดย ExtendedEventHandler เท่านั้น (น่าจะมีประโยชน์และส่วนใหญ่ก็จะไม่บังคับใช้)

เอาต์พุตเทอร์มินัลจะส่งออกส่วนใหญ่ผ่าน UiEventHandler ซึ่งมีหน้าที่ในการจัดรูปแบบเอาต์พุตแฟนซีทั้งหมดและการรายงานความคืบหน้าที่ Bazel ทำ ซึ่งมี 2 อินพุต ได้แก่

  • รถบัสในงาน
  • สตรีมเหตุการณ์ได้เชื่อมไปยังสตรีมดังกล่าวผ่านตัวรายงาน

การเชื่อมต่อโดยตรงเพียงอย่างเดียวที่เครื่องจักรดำเนินการตามคำสั่ง (เช่น ส่วนที่เหลือของ Bazel) กับสตรีม RPC ไปยังไคลเอ็นต์คือผ่าน Reporter.getOutErr() ซึ่งทำให้เข้าถึงสตรีมเหล่านี้ได้โดยตรง โดยจะใช้เมื่อคำสั่งต้องดักข้อมูลไบนารีที่อาจเป็นไปได้จำนวนมากเท่านั้น (เช่น bazel query)

การทำโปรไฟล์ Bazel

Bazel เร็ว Bazel ยังทำงานช้าด้วย เนื่องจากบิลด์มีแนวโน้มที่จะเติบโตจนถึงขอบของสิ่งที่รองรับได้ ด้วยเหตุนี้ Bazel จึงมีเครื่องมือสร้างโปรไฟล์ซึ่งใช้ในการสร้างโปรไฟล์และ Bazel ได้ด้วย และมีการใช้งานในคลาสที่ชื่อ Profiler อย่างเหมาะสม ฟีเจอร์นี้จะเปิดอยู่โดยค่าเริ่มต้น แต่จะบันทึกเฉพาะข้อมูลฉบับย่อเพื่อให้โอเวอร์เฮดพอทนได้ บรรทัดคำสั่ง --record_full_profiler_data จะช่วยบันทึกทุกอย่างที่ทำได้

โดยจะปล่อยโปรไฟล์ในรูปแบบเครื่องมือสร้างโปรไฟล์ของ Chrome ซึ่งเหมาะกับการดูใน Chrome โมเดลข้อมูลคือสแต็กงาน โดยที่กลุ่มหนึ่งจะเริ่มงานและสิ้นสุดงานได้ และควรซ้อนกันอย่างเรียบร้อย เทรด Java แต่ละรายการจะมีสแต็กงานของตัวเอง TODO: วิธีนี้ทำงานกับรูปแบบการกระทำ และรูปแบบการส่งอย่างต่อเนื่องอย่างไร

เครื่องมือสร้างโปรไฟล์เริ่มต้นและหยุดทำงานใน BlazeRuntime.initProfiler() และ BlazeRuntime.afterCommand() ตามลำดับ และพยายามมีอายุการใช้งานให้นานที่สุดเท่าที่จะทำได้เพื่อให้เราทำโปรไฟล์ทุกอย่างได้ หากต้องการเพิ่มบางสิ่งลงในโปรไฟล์ ให้โทรหา Profiler.instance().profile() ผลการค้นหา Closeable จะแสดงเป็นการปิดทำการ หมายถึงการสิ้นสุดงาน โดยควรใช้กับคำสั่ง "ลองด้วยทรัพยากร"

นอกจากนี้เรายังทำโปรไฟล์หน่วยความจำเบื้องต้นใน MemoryProfiler ด้วย และยังเปิดอยู่เสมอ และจะบันทึกขนาดฮีปสูงสุดและลักษณะการทำงานของ GC เป็นส่วนใหญ่

กำลังทดสอบ Bazel

บาเซลมีการทดสอบหลักๆ 2 ประเภท ได้แก่ การทดสอบที่สังเกต Bazel เป็น "กล่องดำ" และการทดสอบที่ดำเนินการเฉพาะช่วงการวิเคราะห์ เราเรียกการทดสอบแบบบูรณาการว่า "การทดสอบแบบบูรณาการ" แบบเดิมและ "การทดสอบหน่วย" แบบหลัง แม้ว่าจะเป็นเหมือนการทดสอบการผสานรวมมากกว่า ซึ่งมีการผสานรวมน้อยกว่า และเรายังมีการทดสอบ 1 หน่วยที่ต้องทำจริงๆ ด้วย

ในการทดสอบการผสานรวม เรามี 2 ประเภท ได้แก่

  1. โซลูชันที่นำไปใช้โดยใช้เฟรมเวิร์กการทดสอบแบบ Bash ที่ละเอียดประณีตภายใต้ src/test/shell
  2. เบราว์เซอร์ที่ติดตั้งใช้งานใน Java ซึ่งจะใช้งานเป็นคลาสย่อยของ BuildIntegrationTestCase

BuildIntegrationTestCase เป็นเฟรมเวิร์กการทดสอบการผสานรวมที่แนะนำให้ใช้ เนื่องจากเตรียมพร้อมสำหรับสถานการณ์การทดสอบส่วนใหญ่ เนื่องจากเป็นเฟรมเวิร์ก Java จึงมีความสามารถในการแก้ไขข้อบกพร่องและการผสานรวมที่ราบรื่นกับเครื่องมือการพัฒนาทั่วไปจำนวนมาก มีตัวอย่างของคลาส BuildIntegrationTestCase มากมายในที่เก็บ Basl

ใช้การทดสอบการวิเคราะห์เป็นคลาสย่อยของ BuildViewTestCase มีระบบไฟล์ Scratch ที่ใช้เขียนไฟล์ BUILD จากนั้นใช้ตัวช่วยต่างๆ เพื่อขอเป้าหมายที่กำหนดค่าไว้ เปลี่ยนการกำหนดค่า และยืนยันหลายๆ อย่างเกี่ยวกับผลการวิเคราะห์