ฐานของโค้ด Bazel

รายงานปัญหา ดูแหล่งที่มา รุ่น Nightly · 7.4 7.3 · 7.2 · 7.1 · 7.0 · 6.5

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  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() จะใช้ FragmentOptions นั้นไม่มีปัญหา)

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 เรียกว่า "ที่เก็บหลัก" ส่วนที่เหลือเรียกว่า "ที่เก็บภายนอก"

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

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

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

แพ็กเกจ

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

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

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

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

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

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

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

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

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

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

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

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

Skyframe

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

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

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

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

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

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

Starlark

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

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

Starlark ใช้ในบริบทต่างๆ ซึ่งรวมถึง

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

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

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

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

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

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

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

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

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

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

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

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

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

การกำหนดค่า

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

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

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

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

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

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

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

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

การเปลี่ยนการกำหนดค่ามีการใช้งาน เช่น

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

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

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

ผู้ให้บริการข้อมูลแบบเปลี่ยนผ่าน

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

โดยทั่วไปแล้ว ผู้ให้บริการข้อมูลแบบเปลี่ยนผ่านของ 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

ไฟล์รันไทม์

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

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

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

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

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

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

ลักษณะ

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

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

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

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

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

ระบบจะบันทึกความซับซ้อนขององค์ประกอบในองค์ประกอบไว้ในคลาส AspectCollection

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

ซึ่งติดตั้งใช้งานในตำแหน่งต่อไปนี้

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

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

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

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

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

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

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

รายการต่างๆ และการดำเนินการ

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

วิธีดำเนินการเหล่านี้ทำได้โดยใช้วิธีดำเนินการ

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

การทดสอบ

Bazel รองรับการทดสอบที่ทำงานอยู่อย่างหลากหลาย โดยรองรับการดำเนินการต่อไปนี้

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

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

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

การกำหนดการทดสอบที่จะเรียกใช้

การกำหนดการทดสอบที่จะทํางานเป็นกระบวนการที่ซับซ้อน

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

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

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

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

การทดสอบที่ทำงานอยู่

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

ขณะนี้ความครอบคลุมของข้อมูลพื้นฐานใช้งานไม่ได้

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

การทดสอบ Bazel

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

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

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

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

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