ฐานของโค้ด Bazel

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

บทนำ

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

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

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

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

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

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

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

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

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

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

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

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

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 เพียง อินสแตนซ์เดียวเท่านั้นที่สามารถทำงานในเวิร์กสเปซใดก็ได้ในเวลาใดก็ตาม

ไดเรกทอรีเอาต์พุตมีข้อมูลต่อไปนี้

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

กระบวนการดำเนินการคำสั่ง

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

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

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

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

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

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

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

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

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

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

ตัวเลือกบรรทัดคำสั่งสำหรับการเรียกใช้ Bazel อธิบายไว้ในออบเจ็กต์ OptionsParsingResult ซึ่งมีแผนที่จาก "option classes" ไปยังค่าของตัวเลือก "คลาสตัวเลือก" คือคลาสย่อยของ 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 ดำเนินการเรียกว่า "พื้นที่ทำงาน" และมีโครงสร้างเป็นที่เก็บ แพ็กเกจ และกฎ

ที่เก็บ

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

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

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

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

แพ็กเกจ

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

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

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

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

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

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

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

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

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

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

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

  1. ไฟล์: สิ่งที่เป็นอินพุตหรือเอาต์พุตของการสร้าง ใน ภาษาของ Bazel เราเรียกสิ่งเหล่านี้ว่าอาร์ติแฟกต์ (กล่าวถึงในที่อื่น) ไฟล์ที่สร้างขึ้นระหว่างการบิลด์ไม่ได้เป็นเป้าหมายเสมอไป โดยปกติแล้วเอาต์พุตของ 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() ในขณะที่ Bazel "รู้จัก" คลาสกฎ Java โดยกำเนิด เนื่องจากมีการลงทะเบียนกับ ConfiguredRuleClassProvider

คลาสของกฎจะมีข้อมูลต่อไปนี้

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

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

Skyframe

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

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

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

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

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

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

Starlark

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

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

Starlark ใช้ในหลายบริบท ได้แก่

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

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

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

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

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

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

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

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

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

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

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

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

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

การกำหนดค่า

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

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

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

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

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

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

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

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

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

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

หากการเปลี่ยนการกำหนดค่าส่งผลให้มีการกำหนดค่าหลายรายการ จะเรียกว่าการเปลี่ยนแบบแยก

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

ผู้ให้บริการข้อมูลแบบทรานซิทีฟ

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

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

Runfiles

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

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

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

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

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

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

ลักษณะ

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

ชุดเครื่องมือที่จะใช้สำหรับเป้าหมายที่กำหนดค่าจะกำหนดโดย ToolchainResolutionFunction โดยขึ้นอยู่กับ

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

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

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

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

ข้อจำกัด

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

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

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

environment_group() และ environment()

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

กฎการบิลด์ทั้งหมดสามารถประกาศ "สภาพแวดล้อม" ที่สามารถบิลด์ได้ โดยที่ "สภาพแวดล้อม" คืออินสแตนซ์ของกฎ 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) คุณ ควรระมัดระวังเพื่อป้องกันไม่ให้คนอื่นๆ ขึ้นอยู่กับโค้ดของคุณ โดยพลการ ไม่เช่นนั้น ตามกฎของไฮรัม ผู้ใช้จะพึ่งพาพฤติกรรมที่คุณถือว่าเป็นรายละเอียดการใช้งาน

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 ที่ต้องอยู่ใน classpath เพื่อให้กฎ Java คอมไพล์หรือเรียกใช้ได้
  • ชุดไฟล์ Python ในการปิดทรานซิทีฟของกฎ Python

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

แคชการกระทำเกี่ยวกับสถานที่

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

ระบบจะตรวจสอบแคชนี้เพื่อหารายการที่ตรงกันโดยใช้วิธีการ ActionCacheChecker.getTokenIfNeedToExecute()

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

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

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

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

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

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

โดยจะใช้เมธอดในการดำเนินการ

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

เมื่อแคชการดำเนินการแสดงผลการค้นหาในอินสแตนซ์การดำเนินการใหม่ (เช่น สร้างขึ้น หลังจากรีสตาร์ทเซิร์ฟเวอร์) 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 อาศัยสัญญาของกระบวนการทำงานที่ว่ากระบวนการดังกล่าวไม่มีสถานะที่สังเกตได้ระหว่างคำขอแต่ละรายการ)

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

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

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

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

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

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

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

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

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

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

ระบบกำหนดชื่อของไดเรกทอรีที่เชื่อมโยงกับการกำหนดค่าหนึ่งๆ อย่างไร คุณสมบัติที่ต้องการซึ่งขัดแย้งกันมี 2 อย่าง ได้แก่

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

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

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

การทดสอบ

Bazel มีการรองรับการเรียกใช้การทดสอบอย่างเต็มรูปแบบ โดยรองรับ

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

การทดสอบคือเป้าหมายที่กำหนดค่าตามปกติซึ่งมี TestProvider ที่อธิบาย วิธีเรียกใช้การทดสอบ

  • อาร์ติแฟกต์ที่การสร้างทำให้มีการเรียกใช้การทดสอบ นี่คือไฟล์ "cache status" ที่มีข้อความ 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 ที่แสดงรายละเอียดของกรณีทดสอบแต่ละรายการใน การทดสอบแบบ Shard
  • test.log เอาต์พุตคอนโซลของการทดสอบ stdout และ stderr ไม่ได้ แยกกัน
  • test.outputs ซึ่งเป็น "ไดเรกทอรีเอาต์พุตที่ไม่ได้ประกาศ" ซึ่งใช้โดยการทดสอบ ที่ต้องการเอาต์พุตไฟล์นอกเหนือจากที่พิมพ์ไปยังเทอร์มินัล

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

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

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

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

ผลการทดสอบที่ดำเนินการจะพร้อมใช้งานใน Event Bus โดยการสังเกตเหตุการณ์ต่างๆ (เช่น 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 แต่เราขอแนะนำให้กฎ สร้างไฟล์ความครอบคลุมพื้นฐานของตนเองที่มีเนื้อหา ที่มีความหมายมากกว่าเพียงชื่อไฟล์ต้นฉบับ

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

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

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

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

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

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

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

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

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

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

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

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

ระบบโมดูล

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

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

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

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

Event Bus

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

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

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

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

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

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

ไฟล์ WORKSPACE

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

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

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

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

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

ก่อนที่ 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 ซึ่งมีผลรวมตรวจสอบของกฎที่ใช้ในการดึงข้อมูล หากเซิร์ฟเวอร์ Bazel รีสตาร์ท แต่ผลรวมตรวจสอบไม่เปลี่ยนแปลง ระบบจะไม่ดึงข้อมูลอีก ฟีเจอร์นี้ ใช้งานได้ใน RepositoryDelegatorFunction.DigestWriter
  3. ตัวเลือกบรรทัดคำสั่ง --distdir จะกำหนดแคชอื่นที่ใช้เพื่อ ค้นหาสิ่งประดิษฐ์ที่จะดาวน์โหลด ซึ่งจะมีประโยชน์ในการตั้งค่าระดับองค์กร ที่ Bazel ไม่ควรดึงข้อมูลแบบสุ่มจากอินเทอร์เน็ต ซึ่งDownloadManager เป็นผู้ดำเนินการ

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

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

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

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

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

การแมปจะระบุไว้ในไฟล์ 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 เข้ามาจากไคลเอ็นต์ ระบบจะสร้างอินสแตนซ์ 2 รายการRpcOutputStream (สำหรับ stdout และ stderr) ซึ่งจะส่งต่อข้อมูลที่พิมพ์ลงใน อินสแตนซ์เหล่านั้นไปยังไคลเอ็นต์ จากนั้นจะห่อหุ้มด้วย OutErr (คู่ (stdout, stderr) pair) ทุกอย่างที่ต้องพิมพ์ในคอนโซลจะผ่านสตรีมเหล่านี้ จากนั้นจะส่งต่อสตรีมเหล่านี้ไปยัง BlazeCommandDispatcher.execExclusively()

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

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

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

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

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

  • Event Bus
  • สตรีมเหตุการณ์ที่ส่งผ่านท่อไปยังเครื่องมือนี้ผ่าน Reporter

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

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

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

โดยจะสร้างโปรไฟล์ในรูปแบบโปรไฟล์ของ Chrome ซึ่งควรดูใน Chrome โมเดลข้อมูลของฟีเจอร์นี้คือสแต็กงาน ซึ่งผู้ใช้สามารถเริ่มและสิ้นสุดงานได้ และงานต่างๆ ควรซ้อนกันอย่างเป็นระเบียบ แต่ละเธรด Java จะมี สแต็กงานของตัวเอง TODO: How does this work with actions and continuation-passing style?

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

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

การทดสอบ Bazel

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

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

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

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

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