ฐานของโค้ด Bazel

รายงานปัญหา ดูแหล่งที่มา

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

เกริ่นนำ

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

เอกสารฉบับนี้จะพยายามแสดงภาพรวมของฐานของโค้ดเพื่อให้เริ่มต้นใช้งานได้ง่ายขึ้น เพื่อให้ผู้คนที่อยู่ระหว่างการเดินทางได้ไม่ต้องค้นพบตัวเองในป่าอันมืดมิดและเส้นทางที่ตรงไปตรงมา

ซอร์สโค้ดสาธารณะของ 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) ตัวเลือกแบบแรกเรียกว่า "ตัวเลือกการเริ่มต้น" ซึ่งมีผลกับกระบวนการของเซิร์ฟเวอร์ทั้งหมด ในขณะที่ประเภทหลังคือ "ตัวเลือกคำสั่ง" จะมีผลกับคำสั่งเดียวเท่านั้น

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

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

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

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

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

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

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

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

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

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

"ที่เก็บหลัก" คือต้นไม้ต้นทางที่ Bazel ทำงานอยู่ ซึ่งมักจะตรงกับรายการ ที่คุณชำระเงินจากการควบคุมแหล่งที่มา โดยรูทของไดเรกทอรีนี้ เรียกว่า "workspaceRoot"

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

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

ขั้นตอนการเรียกใช้คำสั่ง

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

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

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

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

  4. มีการสร้างบัสกิจกรรม รถบัสของเหตุการณ์เป็นสตรีมของเหตุการณ์ที่เกิดขึ้นระหว่างการสร้าง โดยบางส่วนมีการส่งออกไปภายนอก Bazel ภายใต้ aegis ของโปรโตคอลเหตุการณ์การสร้างเพื่อบอกให้โลกรู้ว่างานสร้างมีอย่างไร

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

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

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

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

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

ตัวเลือกบรรทัดคำสั่งสำหรับการเรียกใช้ Bazel จะอธิบายในออบเจ็กต์ OptionsParsingResult ซึ่งในทางกลับกันจะมีแมปจาก "คลาสตัวเลือก" ไปจนถึงค่าของตัวเลือก "คลาสตัวเลือก" เป็นคลาสย่อยของ 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 มักจะเป็นประเภท Java แบบง่าย (สตริง จำนวนเต็ม บูลีน ป้ายกำกับ ฯลฯ) อย่างไรก็ตาม เรารองรับตัวเลือกประเภทที่ซับซ้อนมากกว่าด้วย ในกรณีนี้ งานการแปลงจากสตริงบรรทัดคำสั่งเป็นประเภทข้อมูลจะเป็นการใช้งาน com.google.devtools.common.options.Converter

แผนผังแหล่งที่มาตามที่เห็นโดย Bazel

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

ที่เก็บ

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

ที่เก็บจะมีเครื่องหมายกำกับไว้โดยไฟล์ขอบเขตที่เก็บ (MODULE.bazel, REPO.bazel หรือในบริบทเดิม, WORKSPACE หรือ WORKSPACE.bazel) ในไดเรกทอรีรูท ที่เก็บหลักคือแผนผังแหล่งที่มาที่คุณเรียกใช้ Bazel ที่เก็บภายนอกมีการกำหนดไว้หลายวิธี ดูภาพรวมของทรัพยากร Dependency ภายนอกสำหรับข้อมูลเพิ่มเติม

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

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

กล่องพัสดุ

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

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

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

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

Globbing มีการนำไปใช้กับคลาสต่อไปนี้

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

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

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

ตามหลักแล้ว ควรแยกการแยกวิเคราะห์แพ็กเกจ "ภายนอก" ออกจากการแยกวิเคราะห์แพ็กเกจปกติมากกว่า เพื่อให้ 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 วัตถุทั้งสองแบบนี้จะเปลี่ยนแปลงไม่ได้อย่างมาก ควรเข้าถึงได้เฉพาะออบเจ็กต์ที่เปลี่ยนแปลงไม่ได้เท่านั้น ค่าความไม่สม่ำเสมอนี้จะคงไว้ชั่วคราวเสมอและในกรณีที่ไม่มี (เช่น สำหรับคลาสตัวเลือกเดี่ยว BuildOptions ซึ่งเป็นสมาชิกของ BuildConfigurationValue และ SkyKey) เราพยายามอย่างยิ่งที่จะไม่เปลี่ยนแปลงหรือเปลี่ยนแปลงการดำเนินการในรูปแบบที่ไม่สามารถสังเกตได้จากภายนอกเท่านั้น ด้วยเหตุนี้ ทุกสิ่งที่คํานวณภายใน Skyframe (เช่น เป้าหมายที่กําหนดค่าไว้) จะต้องเปลี่ยนแปลงไม่ได้เช่นกัน

วิธีที่สะดวกที่สุดในการดูกราฟ Skyframe คือการเรียกใช้ bazel dump --skyframe=deps ซึ่งจะถ่ายโอนกราฟ 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 ทราบว่าต้องประเมินฟังก์ชันอีกครั้งเมื่อความพึ่งพิงใดๆ ของ Skyframe มีการเปลี่ยนแปลง กล่าวคือ การแคชและการประมวลผลที่เพิ่มขึ้นของ Skyframe จะทำงานที่ระดับความละเอียดของ SkyFunction และ SkyValue

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

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

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

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

สตาร์ลาร์ก

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

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

มีการนำ Starlark มาใช้ในบริบทต่างๆ เช่น

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

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

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

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

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

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

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

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

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

ผลลัพธ์ที่ได้จากการวิเคราะห์เป้าหมายที่กำหนดค่าไว้คือ

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

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) และเพื่อช่วยในการตัดการกำหนดค่า เช่นในกรณีที่ตัวเลือก Python เปลี่ยนไป ก็ไม่จำเป็นต้องวิเคราะห์เป้าหมาย C++ ใหม่

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

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

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

การเปลี่ยนการกำหนดค่าจะใช้ดังตัวอย่างต่อไปนี้

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

หากการเปลี่ยนการกำหนดค่าทำให้เกิดการกำหนดค่าหลายรายการ กรณีนี้จะเรียกว่าการเปลี่ยนรุ่นแยกกัน

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

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

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

โดยทั่วไปจะมีการติดต่อแบบ 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ซึ่งเป็นแนวคิดที่คลุมเครือเกี่ยวกับ "ชุดของไฟล์ที่กฎนี้นำเสนอ" ซึ่งเป็นไฟล์ที่สร้างขึ้นเมื่อเป้าหมายที่กำหนดค่าอยู่ในบรรทัดคำสั่งหรือใน src ของ Genrule
  2. ไฟล์รัน ข้อมูลทั่วไป และข้อมูล
  3. กลุ่มเอาต์พุต ซึ่งก็คือ "ไฟล์ชุดอื่นๆ" ที่กฎสร้างขึ้นได้ ซึ่งเข้าถึงได้โดยใช้แอตทริบิวต์ export_group ของกฎกลุ่มไฟล์ใน BUILD และใช้ผู้ให้บริการ OutputGroupInfo ใน Java

Runfiles

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

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

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

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

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

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

ลักษณะ

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

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

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

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

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

ระบบจะบันทึกความซับซ้อนของด้านต่างๆ ในแง่มุมต่างๆ ไว้ในคลาส AspectCollection

แพลตฟอร์มและเครือเครื่องมือ

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

ข้อจำกัด

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

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

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

Environment_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 รองรับการดำเนินการนี้โดยกลไกที่เรียกว่า visibility: คุณประกาศได้ว่าเป้าหมายหนึ่งๆ จะใช้ได้โดยใช้แอตทริบิวต์ visibility เท่านั้น แอตทริบิวต์นี้มีความพิเศษเล็กน้อยเนื่องจากแม้ว่าจะมีรายการป้ายกำกับ แต่ป้ายกำกับเหล่านี้อาจเข้ารหัสรูปแบบเหนือชื่อแพ็กเกจแทนที่จะเป็นตัวชี้ไปยังเป้าหมายที่เจาะจง (ใช่ นี่เป็นข้อบกพร่องในการออกแบบ)

โดยจะนำไปใช้ในตำแหน่งต่อไปนี้

  • อินเทอร์เฟซ 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. **อาร์ติแฟกต์ปกติ **ระบบจะตรวจสอบความเป็นปัจจุบันด้วยการคำนวณ Checksum ที่มี mtime เป็นทางลัด เราจะไม่ตรวจสอบไฟล์หากเวลา ไม่มีการเปลี่ยนแปลง
  2. อาร์ติแฟกต์ Sym Link ที่ยังไม่ได้แก้ไข ระบบจะตรวจสอบการอัปเดตเหล่านี้เป็นข้อมูลล่าสุดโดยเรียก readlink() อาร์ติแฟกต์เหล่านี้อาจจะเชื่อมโยงเป็น Symlink ที่ต่างจากอาร์ติแฟกต์ทั่วไป มักใช้ในกรณีที่ไฟล์บางไฟล์มีไฟล์ที่ต้องการเก็บถาวร
  3. อาร์ติแฟกต์ต้นไม้ ไฟล์เหล่านี้ไม่ใช่ไฟล์เดี่ยว แต่เป็นต้นไม้ไดเรกทอรี ระบบจะตรวจหาข้อมูลล่าสุดโดยตรวจสอบชุดไฟล์ในนั้นและเนื้อหาของไฟล์ โดยจะแสดงเป็น TreeArtifact
  4. อาร์ติแฟกต์ข้อมูลเมตาอย่างต่อเนื่อง การเปลี่ยนแปลงอาร์ติแฟกต์เหล่านี้จะไม่ทริกเกอร์การสร้างใหม่ โค้ดนี้ใช้สำหรับข้อมูลสแตมป์บิลด์โดยเฉพาะ เราไม่ต้องการ สร้างใหม่เพียงเพราะเวลาปัจจุบันเปลี่ยนไป

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

แคชการดำเนินการในเครื่อง

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

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

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

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

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

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

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

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

โดยนำการเปลี่ยนแปลงเหล่านี้ไปใช้โดยใช้เมธอดในการดำเนินการ ดังนี้

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

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

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

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

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

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

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

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

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

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

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

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

ผู้จัดการทรัพยากรภายใน

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

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

ดูคำอธิบายโดยละเอียดเกี่ยวกับการจัดการทรัพยากรในพื้นที่ได้ที่นี่

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

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

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

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

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

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

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

การทดสอบ

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) โดยการทดสอบเหล่านี้จะถูกถ่ายโอนไปยังโปรโตคอล Build Event และถูกส่งไปยังคอนโซลภายใน 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 นอกจากนี้ ยังสร้างขึ้นสำหรับไบนารีและไลบรารีนอกเหนือจากการทดสอบ หากคุณส่งแฟล็ก --nobuild_tests_only ไปยัง Bazel

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

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

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

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

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

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

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

เครื่องมือ Query

Bazel มีภาษาเล็กน้อยในการถามถึงสิ่งต่างๆ เกี่ยวกับกราฟ ประเภทการสืบค้นที่มีให้มีดังนี้

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

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

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

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

ระบบโมดูล

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

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

  • อินเทอร์เฟซสำหรับระบบการดำเนินการจากระยะไกล
  • คำสั่งใหม่

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

รถบัสสำหรับกิจกรรม

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

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

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

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

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

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

ไฟล์ WORKSPACE

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

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

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

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

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

ก่อนที่ Bazel จะเข้าถึงโค้ดของที่เก็บได้ คุณต้องfetched ซึ่งทำให้ 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

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

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

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

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

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

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

บิตของ JNI

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

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

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

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

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

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

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

ข้อความสั้นๆ (ข้อผิดพลาด คำเตือน และสิ่งอื่นๆ ที่คล้ายกัน) จะแสดงผ่านอินเทอร์เฟซของ 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 ประเภทหลักๆ ได้แก่ การทดสอบที่สังเกตบาเซลเป็น "กล่องดำ" และการทดสอบที่ทำเฉพาะช่วงการวิเคราะห์เท่านั้น เราเรียกการทดสอบแบบผสานรวมว่า "การทดสอบการผสานรวม" และ "การทดสอบ 1 หน่วย" แบบหลัง แม้ว่าการทดสอบเหล่านี้จะดูเหมือนการทดสอบการผสานรวมซึ่งผสานรวมน้อยกว่า นอกจากนี้เรายังมีการทดสอบหน่วยจริง ที่จำเป็นอีกด้วย

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

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

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

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