เอกสารฉบับนี้เป็นคำอธิบายฐานของโค้ดและโครงสร้างของ 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
) ตัวเลือกก่อนหน้านี้เรียกว่า "startup options" และส่งผลต่อกระบวนการของเซิร์ฟเวอร์โดยรวม ส่วน "ตัวเลือกคำสั่ง" จะมีผลเฉพาะกับคำสั่งเดี่ยวๆ เท่านั้น
อินสแตนซ์เซิร์ฟเวอร์แต่ละรายการจะมีทรีต้นทาง ("เวิร์กสเปซ") ที่เชื่อมโยงอยู่รายการเดียว และโดยปกติแล้วเวิร์กสเปซแต่ละรายการจะมีอินสแตนซ์เซิร์ฟเวอร์ที่ใช้งานอยู่รายการเดียว สามารถหลีกเลี่ยงได้โดยการระบุฐานเอาต์พุตที่กำหนดเอง (ดูข้อมูลเพิ่มเติมที่ส่วน "การออกแบบไดเรกทอรี")
Bazel มีการเผยแพร่เป็นไฟล์ ELF ปฏิบัติการไฟล์เดียวที่ยังเป็นไฟล์ .zip ที่ถูกต้องด้วย
เมื่อคุณพิมพ์ bazel
ไฟล์ ELF ที่เรียกใช้งานได้ข้างต้นซึ่งเขียนด้วย C++ ("ไคลเอ็นต์") จะควบคุม โดยจะตั้งค่ากระบวนการเซิร์ฟเวอร์ที่เหมาะสมโดยใช้ขั้นตอนต่อไปนี้
- ตรวจสอบว่ามีการแยกตัวเองแล้วหรือไม่ แต่หากไม่เป็นเช่นนั้น การติดตั้งใช้งานเซิร์ฟเวอร์จึงเกิดขึ้นจากจุดนี้
- ตรวจสอบว่ามีอินสแตนซ์เซิร์ฟเวอร์ที่ใช้งานอยู่ที่ทำงานได้ไหม อินสแตนซ์นี้ทำงานอยู่ มีตัวเลือกการเริ่มต้นที่ถูกต้อง และใช้ไดเรกทอรีพื้นที่ทำงานที่ถูกต้อง โดยจะหาเซิร์ฟเวอร์ที่กำลังทำงานอยู่โดยดูที่ไดเรกทอรี
$OUTPUT_BASE/server
ซึ่งมีไฟล์ล็อกซึ่งมีพอร์ตที่เซิร์ฟเวอร์นั้นฟังอยู่ - หยุดกระบวนการเซิร์ฟเวอร์เดิมหากจำเป็น
- หากจำเป็น ให้เริ่มกระบวนการของเซิร์ฟเวอร์ใหม่
หลังจากกระบวนการของเซิร์ฟเวอร์ที่เหมาะสมพร้อมแล้ว ระบบจะสื่อสารคำสั่งที่ต้องเรียกใช้ผ่านอินเทอร์เฟซ gRPC จากนั้นเอาต์พุตของ Bazel จะถูกเชื่อมต่อกลับไปยังเทอร์มินัล เรียกใช้คำสั่งได้ครั้งละ 1 รายการเท่านั้น ซึ่งทำได้โดยใช้กลไกล็อกที่ซับซ้อนซึ่งมีส่วนต่างๆ ใน C++ และส่วนต่างๆ ใน Java มีโครงสร้างพื้นฐานบางอย่างสําหรับการเรียกใช้คําสั่งหลายรายการพร้อมกัน เนื่องจากการที่เรียกใช้ bazel version
ควบคู่ไปกับคําสั่งอื่นไม่ได้นั้นเป็นเรื่องที่น่าอาย ตัวบล็อกหลักคือวงจรชีวิตของ BlazeModule
และบางสถานะใน BlazeRuntime
ในตอนท้ายของคำสั่ง เซิร์ฟเวอร์ Bazel จะส่งโค้ดสำหรับออกที่ไคลเอ็นต์ควรส่งคืน ส่วนเล็กๆ ที่น่าสนใจคือการนำ bazel run
มาใช้ หน้าที่ของคำสั่งนี้คือการเรียกใช้สิ่งที่ Bazel เพิ่งสร้างขึ้น แต่ไม่สามารถทำได้จากกระบวนการของเซิร์ฟเวอร์เนื่องจากไม่มีเทอร์มินัล ดังนั้นจึงบอกไคลเอ็นต์ว่าควรใช้ ujexec() ไบนารีใด และอาร์กิวเมนต์ใด
เมื่อกด Ctrl-C แล้ว ไคลเอ็นต์จะแปลผลเป็นการเรียกใช้ "Cancel" ในการเชื่อมต่อ 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 ได้ควบคุมและได้รับทราบถึงคำสั่งที่ต้องใช้ในการปฏิบัติการแล้ว ลำดับเหตุการณ์ต่อไปนี้จะเกิดขึ้น
BlazeCommandDispatcher
ได้รับแจ้งเกี่ยวกับคำขอใหม่แล้ว โดยจะตัดสินใจว่าคำสั่งต้องใช้เวิร์กスペースเพื่อทำงานหรือไม่ (เกือบทุกคำสั่งยกเว้นคำสั่งที่ไม่มีส่วนเกี่ยวข้องกับซอร์สโค้ด เช่น เวอร์ชันหรือความช่วยเหลือ) และดูว่าคำสั่งอื่นกำลังทำงานอยู่หรือไม่พบคำสั่งที่ถูกต้อง แต่ละคำสั่งต้องใช้อินเทอร์เฟซ
BlazeCommand
และต้องมีคำอธิบายประกอบ@Command
(นี่เป็นเพียงรูปแบบหนึ่งเล็กน้อย คงจะดีหากได้อธิบายข้อมูลเมตาทั้งหมดที่คำสั่งต้องใช้ด้วยเมธอดในBlazeCommand
)ระบบจะแยกวิเคราะห์ตัวเลือกบรรทัดคำสั่ง แต่ละคำสั่งจะมีตัวเลือกบรรทัดคำสั่งแตกต่างกัน ซึ่งอธิบายไว้ในคำอธิบายประกอบ
@Command
มีการสร้างรถโดยสารสำหรับกิจกรรม บัสเหตุการณ์คือสตรีมสําหรับเหตุการณ์ที่เกิดขึ้นระหว่างการสร้าง ตัวอย่างบางรายการส่งออกไปนอก Bazel ภายใต้ Aegis ของ Build Event Protocol เพื่อบอกคนทั่วโลกว่าบิลด์นี้ทำงานอย่างไร
คำสั่งจะควบคุม คำสั่งที่น่าสนใจที่สุดคือคำสั่งที่ใช้เรียกใช้การสร้าง เช่น การสร้าง การทดสอบ การทำงาน ความครอบคลุม และอื่นๆ ฟังก์ชันการทำงานนี้ใช้
BuildTool
ชุดรูปแบบเป้าหมายในบรรทัดคำสั่งได้รับการแยกวิเคราะห์และแก้ไขไวลด์การ์ด เช่น
//pkg:all
และ//pkg/...
ซึ่งจะนำไปใช้ในAnalysisPhaseRunner.evaluateTargetPatterns()
และได้รับการปรับแต่งใหม่ใน Skyframe เป็นTargetPatternPhaseValue
ขั้นตอนการโหลด/การวิเคราะห์จะทำงานเพื่อสร้างกราฟการดำเนินการ (กราฟแบบวนซ้ำของคำสั่งที่ต้องเรียกใช้สำหรับบิลด์)
ขั้นตอนการดำเนินการจะทำงาน ซึ่งหมายความว่าการเรียกใช้การกระทำทุกอย่างที่จำเป็นเพื่อสร้างเป้าหมายระดับบนสุดที่ขอนั้นจะทำงาน
ตัวเลือกบรรทัดคำสั่ง
ตัวเลือกบรรทัดคำสั่งสำหรับการเรียกใช้ Bazel จะอธิบายไว้ในออบเจ็กต์ OptionsParsingResult
ซึ่งจะมีแผนที่จาก "option classes" ไปจนถึงค่าของตัวเลือก "คลาสตัวเลือก" เป็นคลาสย่อยของ OptionsBase
และจัดกลุ่มตัวเลือกบรรทัดคำสั่งที่เกี่ยวข้องไว้ด้วยกัน เช่น
- ตัวเลือกที่เกี่ยวข้องกับภาษาโปรแกรม (
CppOptions
หรือJavaOptions
) ตัวเลือกเหล่านี้ควรเป็นคลาสย่อยของFragmentOptions
และได้รับการรวมไว้ในออบเจ็กต์BuildOptions
ในท้ายที่สุด - ตัวเลือกที่เกี่ยวข้องกับวิธีที่ Bazel ดำเนินการ (
ExecutionOptions
)
ตัวเลือกเหล่านี้ออกแบบมาเพื่อใช้ในเฟสการวิเคราะห์และ (ผ่าน RuleContext.getFragment()
ใน Java หรือ ctx.fragments
ใน Starlark)
ระบบจะอ่านข้อมูลบางอย่าง (เช่น จะทำการสแกนรวม C++ หรือไม่) ในเฟสการดำเนินการ แต่จะต้องทำการเดินท่ออย่างชัดเจนเสมอเนื่องจากBuildConfiguration
ไม่พร้อมใช้งานในตอนนั้น สำหรับข้อมูลเพิ่มเติม โปรดดูที่
ส่วน "การกำหนดค่า"
คำเตือน: เราชอบเหมือนว่าอินสแตนซ์ OptionsBase
จะเปลี่ยนแปลงไม่ได้และใช้รูปแบบนั้น (เช่น เป็นส่วนหนึ่งของ SkyKeys
) นี่ไม่ใช่กรณีที่เกิดขึ้นและการปรับเปลี่ยนเป็นวิธีที่ดีจริงๆ ในการทำลาย Bazel ด้วยวิธีการอย่างละเอียดที่แก้ไขข้อบกพร่องได้ยาก แต่การทำให้ข้อมูลเหล่านี้เป็นแบบคงที่จริงๆ นั้นเป็นเรื่องยาก
(การแก้ไข FragmentOptions
ทันทีหลังจากสร้างก่อนที่จะมีคนอื่นอ้างอิงถึง และก่อนที่ equals()
หรือ hashCode()
จะใช้ FragmentOptions
นั้นไม่มีปัญหา)
Bazel ได้เรียนรู้เกี่ยวกับคลาสตัวเลือกด้วยวิธีต่อไปนี้
- บางอันแบบมีสายต่อเข้ากับ Bazel (
CommonCommandOptions
) - จากคำอธิบายประกอบ @Command ในคำสั่ง Bazel แต่ละรายการ
- ตั้งแต่
ConfiguredRuleClassProvider
(ตัวเลือกเหล่านี้คือตัวเลือกบรรทัดคำสั่งที่เกี่ยวข้องกับภาษาโปรแกรมแต่ละภาษา) - กฎของ Starlark ยังกำหนดตัวเลือกของตัวเองได้อีกด้วย (ดูที่นี่)
ตัวเลือกแต่ละรายการ (ยกเว้นตัวเลือกที่ Starlark กำหนด) คือตัวแปรสมาชิกของFragmentOptions
ซับคลาสที่มีคำอธิบายประกอบ @Option
ซึ่งระบุชื่อและประเภทของตัวเลือกบรรทัดคำสั่งพร้อมกับข้อความความช่วยเหลือบางส่วน
ประเภท Java ของค่าของตัวเลือกบรรทัดคำสั่งมักเป็นค่าที่เรียบง่าย (เช่น สตริง จำนวนเต็ม บูลีน ป้ายกำกับ ฯลฯ) อย่างไรก็ตาม เรายังรองรับตัวเลือกประเภทที่ซับซ้อนขึ้นด้วย ในกรณีนี้ การแปลงจากสตริงบรรทัดคำสั่งเป็นประเภทข้อมูลจะเป็นไปตามการใช้งาน com.google.devtools.common.options.Converter
ต้นไม้ต้นกำเนิดอย่างที่เห็นโดย Bazel
Bazel อยู่ในธุรกิจการสร้างซอฟต์แวร์ ซึ่งเกิดขึ้นจากการอ่านและตีความซอร์สโค้ด จำนวนรวมของซอร์สโค้ดที่ Bazel ดำเนินการอยู่นี้เรียกว่า "พื้นที่ทำงาน" และจัดโครงสร้างให้กับที่เก็บ แพ็กเกจ และกฎต่างๆ
ที่เก็บ
"ที่เก็บ" คือแผนผังแหล่งที่มาที่นักพัฒนาซอฟต์แวร์ทำงาน โดยมักจะเป็นตัวแทนของโปรเจ็กต์เดี่ยว บรรพบุรุษของ Bazel ชื่อ Blaze ทำงานบนระบบเดียว นั่นคือแผนผังต้นทางเดี่ยวที่มีซอร์สโค้ดทั้งหมดที่ใช้ในการเรียกใช้บิลด์ ในทางตรงกันข้าม Bazel สนับสนุนโปรเจ็กต์ที่มีซอร์สโค้ดครอบคลุมที่เก็บหลายแห่ง ที่เก็บที่มีการเรียกใช้ Bazel เรียกว่า "ที่เก็บหลัก" ส่วนที่เก็บอื่นๆ เรียกว่า "ที่เก็บภายนอก"
ที่เก็บมีการทำเครื่องหมายโดยไฟล์ชื่อ WORKSPACE
(หรือ WORKSPACE.bazel
) ในไดเรกทอรีราก ไฟล์นี้มีข้อมูลที่ "ส่วนกลาง" สำหรับทั้งบิลด์ เช่น ชุดที่เก็บข้อมูลภายนอกที่ใช้ได้ ซึ่งทำงานเหมือนไฟล์ Starlark ทั่วไป ซึ่งหมายความว่าสามารถload()
ไฟล์ Starlark อื่นๆ ได้
วิธีนี้มักใช้เพื่อดึงข้อมูลในที่เก็บซึ่งที่เก็บซึ่งจำเป็นต้องมีการอ้างอิงอย่างชัดแจ้ง (เราเรียกว่า "รูปแบบ deps.bzl
")
โค้ดของที่เก็บภายนอกมีการลิงก์หรือดาวน์โหลดภายใต้ $OUTPUT_BASE/external
เมื่อเรียกใช้บิลด์ โครงสร้างต้นทางทั้งหมดจะต้องรวมเข้าด้วยกัน ซึ่ง
ทำโดย SymlinkForest ซึ่งจะลิงก์ทุกแพ็กเกจในที่เก็บหลักกับ
$EXECROOT
และที่เก็บภายนอกทั้งหมดไปยัง $EXECROOT/external
หรือ
$EXECROOT/..
(ซึ่งเดิมจะทำให้ไม่มีแพ็กเกจชื่อ external
ในที่เก็บหลัก จึงเป็นเหตุผลที่เราจะย้ายข้อมูลออกจากที่เก็บดังกล่าว)
แพ็กเกจ
ที่เก็บข้อมูลทุกแห่งประกอบด้วยแพ็กเกจ คอลเล็กชันไฟล์ที่เกี่ยวข้อง และข้อกำหนดของข้อกำหนดเบื้องต้น ซึ่งระบุโดยไฟล์ชื่อ BUILD
หรือ BUILD.bazel
หากมีทั้ง 2 ไฟล์ Bazel จะเลือก BUILD.bazel
เหตุผลที่ยังคงยอมรับไฟล์ BUILD
อยู่เนื่องจาก Blaze ซึ่งเป็นบรรพบุรุษของ Bazel ใช้ชื่อไฟล์นี้ แต่กลายเป็นกลุ่มเส้นทางที่ใช้กันโดยทั่วไป โดยเฉพาะใน Windows ที่ชื่อไฟล์ไม่คำนึงถึงตัวพิมพ์เล็กหรือใหญ่
แพ็กเกจเป็นอิสระต่อกัน ดังนั้นการเปลี่ยนแปลงไฟล์ BUILD
ของแพ็กเกจจะไม่ทำให้แพ็กเกจอื่นๆ เปลี่ยนแปลง การเพิ่มหรือนำไฟล์ BUILD
ออก
_can _change แพ็กเกจอื่นๆ ได้ เนื่องจาก glob ที่เกิดซ้ำจะหยุดที่ขอบเขตแพ็กเกจ
และด้วยเหตุนี้การมีไฟล์ BUILD
จึงจะหยุดการเกิดซ้ำ
การประเมินไฟล์ BUILD
เรียกว่า "การโหลดแพ็กเกจ" ซึ่งมีการนำไปใช้ในชั้นเรียน PackageFactory
ซึ่งทำงานโดยการเรียกใช้ล่าม Starlark และต้องการความรู้เกี่ยวกับชุดคลาสกฎที่ใช้ได้ ผลลัพธ์ของการโหลดแพ็กเกจคือออบเจ็กต์ Package
โดยส่วนใหญ่จะเป็นแผนที่จากสตริง (ชื่อเป้าหมาย) ไปยังเป้าหมายนั้นๆ
ความซับซ้อนส่วนใหญ่ระหว่างการโหลดแพ็กเกจคือรูปแบบทั่วไป: Bazel ไม่ได้กำหนดให้ต้องระบุไฟล์ต้นฉบับทุกไฟล์อย่างชัดเจน แต่สามารถเรียกใช้รูปแบบทั่วไป (เช่น glob(["**/*.java"])
) แทน ต่างจากเชลล์ตรงที่รองรับรูปแบบทั่วไปแบบซ้ำซ้อนที่ไปยังไดเรกทอรีย่อย (แต่ไม่ใช่ไปยังแพ็กเกจย่อย) การดำเนินการนี้ต้องเข้าถึงระบบไฟล์ และเนื่องจากอาจทำได้ช้า เราจึงใช้กลอุบายทั้งหมดเพื่อทำให้ระบบทำงานพร้อมกันและมีประสิทธิภาพมากที่สุด
มีการใช้ Globbing ในชั้นเรียนต่อไปนี้
LegacyGlobber
ผู้ใช้ Globber ที่รวดเร็วและไม่รู้เรื่อง SkyframeSkyframeHybridGlobber
ซึ่งเป็นเวอร์ชันที่ใช้ Skyframe และเปลี่ยนกลับไปใช้ globber แบบเดิมเพื่อหลีกเลี่ยง “การรีสตาร์ท Skyframe” (ตามที่อธิบายไว้ด้านล่าง)
คลาส Package
เองก็มีสมาชิกบางรายที่ใช้เฉพาะในการแยกวิเคราะห์ไฟล์ WORKSPACE ซึ่งไม่เหมาะสำหรับแพ็กเกจจริง ซึ่งเป็นข้อบกพร่องในการออกแบบเนื่องจากออบเจ็กต์ที่อธิบายแพ็กเกจปกติไม่ควรมีฟิลด์ที่อธิบายอย่างอื่น ซึ่งได้แก่
- การแมปที่เก็บ
- เครื่องมือทางเทคนิคที่ลงทะเบียน
- แพลตฟอร์มการดำเนินการที่ลงทะเบียนไว้
ตามหลักการแล้ว การแยกการแยกวิเคราะห์ไฟล์ WORKSPACE ออกจากการแยกวิเคราะห์แพ็กเกจปกติจะดีกว่า เพื่อให้ Package
ไม่ต้องตอบสนองต่อความต้องการทั้ง 2 อย่าง แต่การดำเนินการนี้ทำได้ยากเนื่องจาก 2 รายการนี้มีความเชื่อมโยงกันอย่างมาก
ป้ายกำกับ เป้าหมาย และกฎ
แพ็กเกจประกอบด้วยเป้าหมายในประเภทต่อไปนี้
- ไฟล์: สิ่งต่างๆ ที่ใช้เป็นอินพุตหรือเอาต์พุตของบิลด์ ในคำอธิบายบาเซล เราเรียกสิ่งนี้ว่าสิ่งประดิษฐ์ (มีอภิปรายกันที่อื่น) ไฟล์ที่สร้างขึ้นในระหว่างการบิลด์ไม่ใช่เป้าหมายทั้งหมด เป็นเรื่องปกติที่เอาต์พุตของ Bazel จะไม่มีป้ายกำกับที่เชื่อมโยง
- กฎ: อธิบายขั้นตอนในการดึงเอาเอาต์พุตจากอินพุต โดยทั่วไปมักเชื่อมโยงกับภาษาโปรแกรม (เช่น
cc_library
,java_library
หรือpy_library
) แต่ก็มีบางภาษาที่เข้าใจได้โดยไม่จำเป็นต้องเข้าใจภาษาที่พูด (เช่นgenrule
หรือfilegroup
) - กลุ่มแพ็กเกจ: อธิบายในหัวข้อระดับการเข้าถึง
ชื่อของเป้าหมายเรียกว่าป้ายกํากับ รูปแบบคำสั่งของป้ายกำกับคือ @repo//pac/kage:name
โดยที่ repo
คือชื่อที่เก็บของป้ายกำกับ pac/kage
คือไดเรกทอรีที่มีไฟล์ BUILD
อยู่ และ name
คือเส้นทางของไฟล์ (หากป้ายกำกับหมายถึงไฟล์ต้นทาง) สัมพันธ์กับไดเรกทอรีของแพ็กเกจ เมื่ออ้างอิงเป้าหมายในบรรทัดคำสั่ง คุณสามารถละเว้นส่วนต่างๆ ของป้ายกำกับได้ ดังนี้
- หากไม่ระบุที่เก็บ ระบบจะนำป้ายกำกับไปอยู่ในที่เก็บหลัก
- หากละเว้นส่วนแพ็กเกจ (เช่น
name
หรือ:name
) ระบบจะนำป้ายกำกับไปอยู่ในแพ็กเกจของไดเรกทอรีที่ใช้งานอยู่ปัจจุบัน (ไม่อนุญาตให้ใช้เส้นทางแบบสัมพัทธ์ที่มีการอ้างอิงระดับบน (..))
ชนิดของกฎ (เช่น "ไลบรารี C++") จะเรียกว่า "คลาสกฎ" คุณสามารถใช้คลาสของกฎได้ใน Starlark (ฟังก์ชัน rule()
) หรือใน Java (ซึ่งจะเรียกว่า “กฎเนทีฟ” ประเภท RuleClass
) ในระยะยาว กฎเฉพาะภาษาทุกกฎจะใช้ใน Starlark แต่กลุ่มกฎเดิมบางตระกูล (เช่น Java หรือ C++) จะยังคงอยู่ใน Java ในขณะนี้
คุณต้องนำเข้าคลาสกฎ Starlark ที่ส่วนต้นของไฟล์ BUILD
โดยใช้คำสั่ง load()
ส่วนคลาสกฎ Java นั้น Bazel จะรู้จัก "โดยกำเนิด" เนื่องจากมีการลงทะเบียนกับ ConfiguredRuleClassProvider
คลาสกฎมีข้อมูลต่อไปนี้
- แอตทริบิวต์ (เช่น
srcs
,deps
) ได้แก่ ประเภท ค่าเริ่มต้น ข้อจำกัด ฯลฯ - การเปลี่ยนการกำหนดค่าและลักษณะที่แนบมากับแอตทริบิวต์แต่ละรายการ หากมี
- การใช้กฎ
- ผู้ให้บริการข้อมูลแบบทรานซิทีฟที่กฎ "มักจะ" สร้าง
หมายเหตุคำศัพท์: ในฐานของโค้ด เรามักจะใช้ "กฎ" หมายถึงเป้าหมายที่สร้างโดยคลาสกฎ แต่ควรใช้ "กฎ" เพื่ออ้างอิงคลาสกฎเท่านั้นใน Starlark และเอกสารที่แสดงต่อผู้ใช้ ส่วนเป้าหมายเป็นเพียง "เป้าหมาย" เท่านั้น นอกจากนี้ โปรดทราบว่าแม้ว่า RuleClass
จะมี "class" ในชื่อ แต่คลาสกฎและเป้าหมายประเภทนั้นๆ จะไม่มีความสัมพันธ์แบบรับช่วงของ Java
Skyframe
กรอบการประเมินที่อยู่เบื้องหลัง Bazel เรียกว่า Skyframe โมเดลของโมเดลก็คือ ทุกสิ่งที่จำเป็นต้องสร้างในระหว่างการสร้างจะได้รับการจัดระเบียบเป็นกราฟแบบวนซ้ำที่มีทิศทางโดยมีขอบชี้จากข้อมูลชิ้นใดก็ตามไปยังทรัพยากร Dependency ซึ่งก็คือข้อมูลชิ้นอื่นๆ ที่ต้องทราบในการสร้างข้อมูลขึ้นมา
โหนดในกราฟเรียกว่า SkyValue
และชื่อของโหนดเรียกว่า SkyKey
ทั้ง 2 อย่างนี้จะเปลี่ยนแปลงไม่ได้ มีเพียงวัตถุที่เปลี่ยนแปลงไม่ได้เท่านั้นที่เข้าถึงได้ เงื่อนไขคงที่นี้มักจะเป็นจริงเสมอ และในกรณีที่ไม่เป็นเช่นนั้น (เช่น สำหรับคลาสตัวเลือกแต่ละคลาส BuildOptions
ซึ่งเป็นสมาชิกของ BuildConfigurationValue
และ SkyKey
ของ BuildConfigurationValue
) เราจะพยายามอย่างเต็มที่ที่จะไม่เปลี่ยนแปลงคลาสเหล่านั้น หรือจะเปลี่ยนแปลงก็ให้เปลี่ยนแปลงในลักษณะที่มองไม่เห็นจากภายนอกเท่านั้น
หลังจากนั้นทุกอย่างที่คํานวณภายใน Skyframe (เช่น เป้าหมายที่กำหนดค่า) ก็จะต้องเปลี่ยนแปลงไม่ได้เช่นกัน
วิธีสังเกตกราฟ Skyframe ที่สะดวกที่สุดคือการเรียกใช้ bazel dump
--skyframe=detailed
ซึ่งจะทิ้งกราฟ 1 SkyValue
ต่อบรรทัด วิธีนี้เหมาะสําหรับบิลด์ขนาดเล็ก เนื่องจากไฟล์อาจมีขนาดค่อนข้างใหญ่
Skyframe อยู่ในแพ็กเกจ com.google.devtools.build.skyframe
แพ็กเกจ com.google.devtools.build.lib.skyframe
ที่มีชื่อคล้ายกันมีการติดตั้งใช้งาน Bazel ที่ด้านบนของ Skyframe ดูข้อมูลเพิ่มเติมเกี่ยวกับ Skyframe
ได้ที่นี่
หากต้องการประเมิน SkyKey
หนึ่งๆ เป็น SkyValue
Skyframe จะเรียกใช้ SkyFunction
ที่สอดคล้องกับประเภทของคีย์ ในระหว่างการประเมินฟังก์ชัน แอปอาจขอทรัพยากร Dependency อื่นๆ จาก Skyframe โดยเรียกใช้การโอเวอร์โหลดต่างๆ ของ SkyFunction.Environment.getValue()
ซึ่งจะส่งผลข้างเคียงในการลงทะเบียนการพึ่งพาเหล่านั้นในกราฟภายในของ Skyframe เพื่อให้ Skyframe ทราบว่าต้องประเมินฟังก์ชันอีกครั้งเมื่อการพึ่งพาใดๆ เปลี่ยนแปลง กล่าวคือ การแคชและการคำนวณที่เพิ่มขึ้นของ Skyframe จะทำงานที่ความละเอียด SkyFunction
และ SkyValue
เมื่อใดก็ตามที่ SkyFunction
ขอทรัพยากร Dependency ที่ไม่พร้อมใช้งาน getValue()
จะแสดงผลเป็น Null จากนั้นฟังก์ชันควรส่งการควบคุมกลับไปที่ Skyframe โดยแสดงผลเป็น Null ในภายหลัง Skyframe จะประเมินทรัพยากร Dependency ที่ไม่พร้อมใช้งาน จากนั้นรีสตาร์ทฟังก์ชันตั้งแต่ต้น และมีเพียงการเรียกใช้ getValue()
เท่านั้นที่จะสำเร็จโดยมีผลลัพธ์ที่ไม่เป็นค่าว่าง
ผลที่ตามมาก็คือการคำนวณใดๆ ที่ดำเนินการภายใน SkyFunction
ก่อนที่จะรีสตาร์ทจะต้องทำซ้ำ แต่ไม่รวมงานที่ทำเพื่อประเมินทรัพยากร Dependency SkyValues
ซึ่งแคชไว้ ดังนั้น โดยทั่วไปเราจึงแก้ไขปัญหานี้
ได้ดังนี้
- การประกาศทรัพยากร Dependency เป็นกลุ่ม (โดยใช้
getValuesAndExceptions()
) เพื่อจำกัดจำนวนการรีสตาร์ท - การแบ่ง
SkyValue
ออกเป็นชิ้นส่วนต่างๆ ที่คํานวณโดยSkyFunction
ที่แตกต่างกันเพื่อให้คํานวณและแคชได้อย่างอิสระ ซึ่งเป็นสิ่งที่ควรทำอย่างมีกลยุทธ์ เนื่องจากมีโอกาสเพิ่มการใช้หน่วยความจำ - จัดเก็บสถานะระหว่างการรีสตาร์ท โดยใช้
SkyFunction.Environment.getState()
หรือเก็บแคชเฉพาะกิจแบบ "เบื้องหลัง Skyframe"
โดยพื้นฐานแล้ว เราต้องใช้วิธีแก้ปัญหาประเภทนี้ เนื่องจากเรามีโหนด Skyframe บนเครื่องบินหลายแสนโหนดอยู่เป็นแสนๆ โหนด และ Java ไม่รองรับชุดข้อความขนาดเล็ก
Starlark
Starlark เป็นภาษาเฉพาะโดเมนที่ผู้คนใช้เพื่อกำหนดค่าและขยาย Bazel โดยถือว่าเป็นชุดย่อยที่ถูกจำกัดของ Python ซึ่งมีประเภทน้อยกว่ามาก มีข้อจำกัดในการควบคุมมากกว่า และที่สำคัญที่สุดคือรับประกันความเปลี่ยนแปลงไม่ได้อย่างชัดเจนเพื่อให้อ่านพร้อมกันได้ ไม่ใช่ Turing-complete ซึ่งไม่สนับสนุนให้ผู้ใช้บางส่วน (แต่ไม่ใช่ทั้งหมด) พยายามทำงานการเขียนโปรแกรมทั่วไปในภาษานั้นๆ
Starlark ติดตั้งใช้งานในแพ็กเกจ net.starlark.java
นอกจากนี้ยังมีการใช้งาน Go แบบอิสระที่นี่ ปัจจุบันการใช้งาน Java ที่ใช้ใน Bazel ยังเป็นโปรแกรมล่าม
Starlark ใช้ในบริบทต่างๆ ซึ่งรวมถึง
- ภาษา
BUILD
ในส่วนนี้จะมีการกำหนดกฎใหม่ โค้ด Starlark ที่ทำงานในบริบทนี้มีสิทธิ์เข้าถึงเฉพาะเนื้อหาของไฟล์BUILD
และไฟล์.bzl
ที่ตัวไฟล์โหลดไว้เท่านั้น - คำจำกัดความของกฎ นี่คือวิธีกำหนดกฎใหม่ (เช่น การรองรับภาษาใหม่) โค้ด Starlark ที่ทำงานในบริบทนี้จะมีสิทธิ์เข้าถึงการกำหนดค่าและข้อมูลที่ได้จากข้อกำหนดโดยตรง (ดูข้อมูลเพิ่มเติมในภายหลัง)
- ไฟล์ WORKSPACE ซึ่งเป็นการกำหนดที่เก็บภายนอก (โค้ดที่ไม่ได้อยู่ในแผนผังแหล่งที่มาหลัก)
- คำจำกัดความของกฎที่เก็บ ในส่วนนี้จะมีการกําหนดประเภทที่เก็บข้อมูลภายนอกใหม่ โค้ด Starlark ที่ทำงานในบริบทนี้สามารถเรียกใช้โค้ดที่กำหนดเองบนเครื่องที่ Bazel ทำงานอยู่ และเข้าถึงนอกพื้นที่ทำงานได้
ภาษาถิ่นที่ใช้กับไฟล์ BUILD
และ .bzl
จะแตกต่างกันเล็กน้อยเนื่องจากแสดงข้อมูลที่แตกต่างกัน ดูรายการความแตกต่างได้ที่นี่
ดูข้อมูลเพิ่มเติมเกี่ยวกับ Starlark ได้ที่นี่
ระยะการโหลด/การวิเคราะห์
ขั้นตอนการโหลด/การวิเคราะห์คือจุดที่ Bazel จะพิจารณาว่าต้องดำเนินการใดบ้างเพื่อสร้างกฎหนึ่งๆ หน่วยพื้นฐานของ คือ "เป้าหมายที่กำหนดค่า" ซึ่งเป็นคู่ (เป้าหมาย การกำหนดค่า) ที่ค่อนข้างสมเหตุสมผล
ขั้นตอนนี้เรียกว่า "ระยะการโหลด/การวิเคราะห์" เนื่องจากสามารถแบ่งออกเป็น 2 ส่วนที่แตกต่างกัน ซึ่งก่อนหน้านี้จะทำงานตามลําดับ แต่ตอนนี้สามารถซ้อนทับกันได้
- การโหลดแพ็กเกจ กล่าวคือ การเปลี่ยนไฟล์
BUILD
ไฟล์เป็นออบเจ็กต์Package
ที่แสดงแพ็กเกจดังกล่าว - การวิเคราะห์เป้าหมายที่กำหนดค่าไว้ กล่าวคือ เรียกใช้การใช้งานกฎเพื่อสร้างกราฟการดำเนินการ
เป้าหมายที่กําหนดค่าแต่ละรายการใน Closure แบบโอนของเป้าหมายที่กําหนดค่าซึ่งขอในบรรทัดคําสั่งต้องได้รับการวิเคราะห์จากล่างขึ้นบน กล่าวคือ เริ่มจากโหนดใบก่อน แล้วจึงไปยังโหนดในบรรทัดคําสั่ง อินพุตของการวิเคราะห์เป้าหมาย ที่กำหนดค่าเดียวคือ
- การกําหนดค่า ("วิธีการ" สร้างกฎ เช่น แพลตฟอร์มเป้าหมาย รวมถึงตัวเลือกบรรทัดคำสั่งที่ผู้ใช้ต้องการส่งผ่านไปยังคอมไพเลอร์ C++)
- ทรัพยากร Dependency โดยตรง ผู้ให้บริการข้อมูลแบบทรานซิทีฟของบุคคลที่สามพร้อมให้บริการแก่กฎที่วิเคราะห์ ไฟล์เหล่านี้เรียกว่า "รวม" เนื่องจากให้ "การรวม" ข้อมูลใน Closure แบบทรานซิทีฟของเป้าหมายที่กําหนดค่า เช่น ไฟล์ .jar ทั้งหมดใน Classpath หรือไฟล์ .o ทั้งหมดที่ต้องลิงก์กับไบนารี C++)
- เป้าหมายเอง นี่เป็นผลการโหลดแพ็กเกจที่มีเป้าหมายอยู่ สําหรับกฎ ข้อมูลนี้รวมถึงแอตทริบิวต์ของกฎ ซึ่งมักจะเป็นสิ่งที่สําคัญ
- การใช้งานเป้าหมายที่กำหนดค่า สำหรับกฎ ข้อมูลนี้อาจเป็นสตาร์แลร์กหรือ Java ระบบจะใช้เป้าหมายที่กําหนดค่าไว้ซึ่งไม่ใช่กฎทั้งหมดใน Java
เอาต์พุตของการวิเคราะห์เป้าหมายที่กำหนดค่าไว้คือ
- ผู้ให้บริการข้อมูลแบบทรานซิทีฟที่กําหนดค่าเป้าหมายซึ่งใช้ข้อมูลดังกล่าวจะเข้าถึงได้
- อาร์ติแฟกต์ที่สามารถสร้างและการดำเนินการที่ทำให้เกิดอาร์ติแฟกต์
API ที่เสนอให้กับกฎ Java คือ RuleContext
ซึ่งเทียบเท่ากับอาร์กิวเมนต์ ctx
ของกฎ Starlark API ของ API มีประสิทธิภาพมากขึ้น แต่ขณะเดียวกัน การทำ Bad ThingsTM จะทำได้ง่ายขึ้น เช่น การเขียนโค้ดที่เวลาหรือพื้นที่มีความซับซ้อนเป็นกำลังสอง (หรือแย่กว่า) เพื่อทำให้เซิร์ฟเวอร์ Bazel ขัดข้องด้วยข้อยกเว้น Java หรือละเมิดค่าคงที่ (เช่น โดยการแก้ไขอินสแตนซ์ Options
โดยไม่ได้ตั้งใจ หรือโดยการเปลี่ยนแปลงเป้าหมายที่กำหนดค่าไว้)
อัลกอริทึมที่กําหนดความเกี่ยวข้องโดยตรงของเป้าหมายที่กําหนดค่าไว้จะอยู่ในส่วนDependencyResolver.dependentNodeMap()
การกำหนดค่า
การกำหนดค่าคือ "วิธีการ" ของการสร้างเป้าหมาย สำหรับแพลตฟอร์มใด พร้อมตัวเลือกบรรทัดคำสั่งใด ฯลฯ
เป้าหมายเดียวกันนั้นสร้างขึ้นสำหรับการกำหนดค่าหลายรายการในบิลด์เดียวกันได้ การดำเนินการนี้มีประโยชน์ เช่น เมื่อใช้โค้ดเดียวกันกับเครื่องมือที่ทำงานระหว่างการสร้างและสำหรับโค้ดเป้าหมาย และเรากำลังคอมไพล์ข้าม หรือเมื่อเราสร้างแอป Android แบบรวม (แอปที่มีโค้ดเนทีฟสำหรับสถาปัตยกรรม CPU หลายแบบ)
โดยหลักการแล้ว การกำหนดค่าคืออินสแตนซ์ BuildOptions
อย่างไรก็ตาม ในทางปฏิบัติ BuildOptions
จะรวมอยู่ใน BuildConfiguration
ที่มีฟังก์ชันการทำงานอื่นๆ เพิ่มเติม โดยจะถ่ายทอดจากด้านบนของกราฟการอ้างอิงไปยังด้านล่าง หากมีการเปลี่ยนแปลง จะต้องมีการวิเคราะห์บิลด์อีกครั้ง
ซึ่งส่งผลให้เกิดความผิดปกติ เช่น ต้องมีการวิเคราะห์บิลด์ทั้งหมดอีกครั้งหากมีการเปลี่ยนแปลงจำนวนการเรียกใช้การทดสอบที่ขอ แม้ว่าจะส่งผลต่อเป้าหมายการทดสอบเท่านั้น (เรามีแผนที่จะ "ตัด" การกําหนดค่าเพื่อไม่ให้เกิดกรณีเช่นนี้ แต่ยังไม่พร้อมใช้งาน)
เมื่อการติดตั้งใช้งานกฎต้องใช้การกําหนดค่าบางส่วน จะต้องประกาศการกําหนดค่านั้นในคําจํากัดความโดยใช้ RuleClass.Builder.requiresConfigurationFragments()
การดำเนินการนี้ทั้งเพื่อหลีกเลี่ยงข้อผิดพลาด (เช่น กฎ Python ที่ใช้ข้อมูลโค้ด Java) และเพื่ออำนวยความสะดวกในการตัดแต่งการกำหนดค่า เช่น หากตัวเลือก Python เปลี่ยนแปลง ก็ไม่จำเป็นต้องวิเคราะห์เป้าหมาย C++ อีกครั้ง
การกำหนดค่าของกฎไม่จำเป็นต้องเหมือนกับกฎ "หลัก" ของกฎดังกล่าว กระบวนการเปลี่ยนการกําหนดค่าในขอบความเกี่ยวข้องเรียกว่า "การเปลี่ยนการกําหนดค่า" ซึ่งอาจเกิดขึ้นได้ 2 แห่ง ดังนี้
- อยู่ใน Dependency Edge การเปลี่ยนเหล่านี้จะระบุไว้ใน
Attribute.Builder.cfg()
และจะเป็นฟังก์ชันจากRule
(ตำแหน่งที่เกิดการเปลี่ยน) และBuildOptions
(การกำหนดค่าเดิม) ไปยังBuildOptions
อย่างน้อย 1 รายการ (การกำหนดค่าเอาต์พุต) - ใน Edge ที่เข้ามายังเป้าหมายที่กําหนดค่าไว้ ซึ่งระบุไว้ใน
RuleClass.Builder.cfg()
ชั้นเรียนที่เกี่ยวข้องคือ TransitionFactory
และ ConfigurationTransition
การเปลี่ยนการกำหนดค่ามีการใช้งาน เช่น
- เพื่อประกาศว่าใช้ทรัพยากรบางอย่างในระหว่างการสร้าง และควรสร้างในสถาปัตยกรรมการดำเนินการ
- ในการประกาศว่าต้องสร้างทรัพยากร Dependency ที่เฉพาะเจาะจงสำหรับสถาปัตยกรรมหลายรายการ (เช่น สำหรับโค้ดแบบเนทีฟใน APK ของ Android ที่เป็นไฟล์จำนวนมาก)
หากการเปลี่ยนการกำหนดค่าส่งผลให้เกิดการกำหนดค่าหลายรายการ ก็เรียกว่าการเปลี่ยนแบบแยกกัน
คุณยังใช้การเปลี่ยนการกำหนดค่าใน Starlark ได้ด้วย (เอกสารประกอบที่นี่)
ผู้ให้บริการข้อมูลแบบเปลี่ยนผ่าน
ผู้ให้บริการข้อมูลแบบทรานซิชันคือวิธี (และ _only _way) สำหรับเป้าหมายที่กําหนดค่าไว้ ในการบอกสิ่งต่างๆ เกี่ยวกับเป้าหมายอื่นๆ ที่กําหนดค่าไว้ซึ่งจะขึ้นอยู่กับเป้าหมายนั้น เหตุผลที่ "สกรรมกริยา" อยู่ในชื่อก็เพราะว่า โดยปกติจะเป็นการรวมการปิดแบบทรานซิทีฟของเป้าหมายที่กำหนดค่าไว้
โดยทั่วไปจะมีการโต้ตอบแบบ 1:1 ระหว่างผู้ให้บริการข้อมูลแบบทรานซิทีฟของ Java กับผู้ให้บริการข้อมูล Starlark (ข้อยกเว้นคือ DefaultInfo
ซึ่งเป็นการรวม FileProvider
, FilesToRunProvider
และ RunfilesProvider
เนื่องจากคาดว่า API นั้นเป็น Starlark-ish มากกว่าการทับศัพท์ของ Java โดยตรง)
รหัสของผู้ใช้คือสิ่งใดสิ่งหนึ่งต่อไปนี้
- ออบเจ็กต์คลาส Java ตัวเลือกนี้ใช้ได้กับผู้ให้บริการที่เข้าถึงจาก Starlark ไม่ได้เท่านั้น ผู้ให้บริการเหล่านี้เป็นคลาสย่อยของ
TransitiveInfoProvider
- สตริง รูปแบบนี้เป็นรูปแบบเดิมที่เราไม่แนะนำให้ใช้เนื่องจากมีแนวโน้มที่จะเกิดการทับซ้อนของชื่อ ผู้ให้บริการข้อมูลแบบเปลี่ยนผ่านดังกล่าวเป็นคลาสย่อยโดยตรงของ
build.lib.packages.Info
- สัญลักษณ์ผู้ให้บริการ ซึ่งสร้างได้จาก Starlark โดยใช้ฟังก์ชัน
provider()
และเป็นวิธีที่แนะนำในการสร้างผู้ให้บริการใหม่ สัญลักษณ์จะแสดงโดยอินสแตนซ์Provider.Key
ใน Java
ผู้ให้บริการรายใหม่ที่ใช้งานใน Java ควรใช้งานโดยใช้ BuiltinProvider
NativeProvider
เลิกใช้งานแล้ว (เรายังไม่มีเวลาในการนำออก) และจะเข้าถึงคลาสย่อย TransitiveInfoProvider
จาก Starlark ไม่ได้
เป้าหมายที่กําหนดค่า
เป้าหมายที่กำหนดค่าไว้จะนำไปใช้เป็น RuleConfiguredTargetFactory
แต่ละคลาสกฎที่ติดตั้งใช้งานใน Java จะมีคลาสย่อย เป้าหมายที่กำหนดค่า Starlark
สร้างขึ้นผ่าน StarlarkRuleConfiguredTargetUtil.buildRule()
โรงงานเป้าหมายที่กำหนดค่าไว้ควรใช้ RuleConfiguredTargetBuilder
เพื่อสร้างมูลค่าผลลัพธ์ ซึ่งประกอบด้วยสิ่งต่อไปนี้
filesToBuild
ซึ่งเป็นแนวคิดที่ไม่ชัดเจนของ "ชุดของไฟล์ที่กฎนี้" ไฟล์เหล่านี้คือไฟล์ที่ถูกสร้างขึ้นเมื่อเป้าหมายที่กำหนดค่าไว้อยู่ในบรรทัดคำสั่งหรือใน srcs ของ Genrule- ไฟล์รันไฟล์ ปกติ และข้อมูล
- กลุ่มเอาต์พุต รายการเหล่านี้คือ "ชุดไฟล์อื่นๆ" ต่างๆ ที่กฎสามารถสร้างได้ โดยเข้าถึงได้โดยใช้แอตทริบิวต์ output_group ของกฎ filegroup ใน BUILD และใช้ผู้ให้บริการ
OutputGroupInfo
ใน Java
ไฟล์รันไทม์
ไฟล์ไบนารีบางไฟล์ต้องใช้ไฟล์ข้อมูลจึงจะทำงานได้ ตัวอย่างที่เห็นได้ชัดคือทดสอบที่ต้องใช้ไฟล์อินพุต ลักษณะนี้แสดงอยู่ใน Bazel ตามแนวคิดของ "runfiles" "ต้นไม้ runfiles" คือต้นไม้ไดเรกทอรีของไฟล์ข้อมูลสําหรับไบนารีหนึ่งๆ ระบบจะสร้างไฟล์นี้ในระบบไฟล์เป็นต้นไม้ลิงก์ที่มีลิงก์แต่ละรายการซึ่งชี้ไปยังไฟล์ในแหล่งที่มาของต้นไม้เอาต์พุต
ชุดไฟล์รันจะแสดงเป็นอินสแตนซ์ Runfiles
โดยแนวคิดแล้ว ไฟล์นี้เป็นการแมปจากเส้นทางของไฟล์ในโครงสร้าง runfiles ไปยังอินสแตนซ์ Artifact
ที่แสดงไฟล์นั้น ซึ่งจะซับซ้อนกว่า Map
รายการเดียวด้วยเหตุผล 2 ประการดังนี้
- ส่วนใหญ่แล้ว เส้นทางรันไฟล์ของไฟล์จะเหมือนกับเส้นทางผู้ดำเนินการ เราใช้วิธีนี้เพื่อประหยัด RAM
- มีรายการเดิมหลายประเภทในแผนผัง Runfile ซึ่งต้องแสดงด้วย
ระบบรวบรวมไฟล์รันไฟล์โดยใช้ RunfilesProvider
: อินสแตนซ์ของคลาสนี้แสดงแทนไฟล์การเรียกใช้ของเป้าหมายที่กำหนดค่าไว้ (เช่น ไลบรารี) และความจำเป็นการปิดแบบทรานซิทีฟ โดยจะรวบรวมไฟล์เหล่านั้นเหมือนกับชุดที่ซ้อนกัน (อันที่จริงแล้ว ไฟล์ดังกล่าวติดตั้งโดยใช้ชุดที่ซ้อนกันใต้หน้าปก): แต่ละการรวมเป้าหมายกับรันไฟล์ของทรัพยากร Dependency จะเพิ่มไฟล์ของตนเองบางส่วน จากนั้นส่งการตั้งค่าที่ได้ไปยังกราฟทรัพยากร Dependency อินสแตนซ์ RunfilesProvider
มีอินสแตนซ์ Runfiles
2 รายการ โดยอินสแตนซ์หนึ่งเป็นอินสแตนซ์ที่ขึ้นอยู่กับกฎผ่านแอตทริบิวต์ "data" และอีกอินสแตนซ์หนึ่งสำหรับทรัพยากร Dependency ที่เข้ามาใหม่ทุกประเภท เนื่องจากบางครั้งเป้าหมายอาจนำเสนอไฟล์การเรียกใช้ที่แตกต่างกันเมื่อพึ่งพาผ่านแอตทริบิวต์ข้อมูลมากกว่าแบบอื่น ซึ่งเป็นลักษณะการทำงานเดิมที่ไม่พึงประสงค์ซึ่งเรายังไม่ได้นำออก
ไฟล์รันไทม์ของไบนารีจะแสดงเป็นอินสแตนซ์ของ RunfilesSupport
สิ่งนี้แตกต่างจาก Runfiles
เนื่องจาก RunfilesSupport
มีความสามารถของสิ่งที่สร้างขึ้นมาได้จริง (ต่างจาก Runfiles
ซึ่งเป็นเพียงการแมปเท่านั้น) ซึ่งต้องใช้คอมโพเนนต์เพิ่มเติมต่อไปนี้
- ไฟล์ Manifest ของการเรียกใช้ไฟล์อินพุต นี่เป็นคำอธิบายแบบอนุกรมของโครงสร้าง Runfiles ซึ่งใช้เป็นพร็อกซีสำหรับเนื้อหาของโครงสร้างไฟล์ Manifest และ Bazel จะถือว่าโครงสร้างของการเรียกใช้ไฟล์มีการเปลี่ยนแปลงในกรณีที่เนื้อหาของไฟล์ Manifest มีการเปลี่ยนแปลงเท่านั้น
- ไฟล์ Manifest ของการเรียกใช้ไฟล์เอาต์พุต มีการใช้งานโดยไลบรารีรันไทม์ที่จัดการโครงสร้างของรันไฟล์ โดยเฉพาะใน Windows ซึ่งบางครั้งก็ไม่รองรับลิงก์สัญลักษณ์
- คนกลางของรันไฟล์ หากต้องการให้มีโครงสร้าง runfiles คุณต้องสร้างโครงสร้างลิงก์สัญลักษณ์ (Symlink) และอาร์ติแฟกต์ที่ลิงก์สัญลักษณ์ชี้ไป ในการลดจำนวนเอดจ์ของทรัพยากร Dependency จะใช้สื่อกลางของ Runfile เพื่อแทนค่าเหล่านี้ได้
- อาร์กิวเมนต์บรรทัดคำสั่งสำหรับเรียกใช้ไบนารีที่มีการเรียกใช้ไฟล์ที่มีออบเจ็กต์
RunfilesSupport
แทน
ลักษณะ
Aspects เป็นวิธีหนึ่งในการ "เผยแพร่การคํานวณลงในกราฟทรัพยากร Dependency" ซึ่งได้อธิบายไว้สำหรับผู้ใช้ Bazel ไว้ที่นี่ ตัวอย่างที่สร้างแรงจูงใจที่ดีคือบัฟเฟอร์โปรโตคอล กฎ proto_library
ไม่ควรทราบเกี่ยวกับภาษาใดภาษาหนึ่ง แต่การสร้างการใช้ข้อความบัฟเฟอร์โปรโตคอล ("หน่วยพื้นฐาน" ของบัฟเฟอร์โปรโตคอล) ในภาษาโปรแกรมใดก็ตามควรเชื่อมโยงกับกฎ proto_library
เพื่อที่ว่าหากเป้าหมาย 2 รายการในภาษาเดียวกันขึ้นอยู่กับบัฟเฟอร์โปรโตคอลเดียวกัน เป้าหมาย 2 รายการจะได้รับการสร้างขึ้นเพียงครั้งเดียวเท่านั้น
เช่นเดียวกับเป้าหมายที่กําหนดค่าไว้ เป้าหมายเหล่านี้จะแสดงใน Skyframe เป็น SkyValue
และวิธีการสร้างจะคล้ายกับการสร้างเป้าหมายที่กําหนดค่าไว้มาก กล่าวคือ มีคลาสโรงงานชื่อ ConfiguredAspectFactory
ที่มีสิทธิ์เข้าถึง RuleContext
แต่ต่างจากโรงงานเป้าหมายที่กําหนดค่าไว้ตรงที่เป้าหมายเหล่านี้จะทราบเกี่ยวกับเป้าหมายที่กําหนดค่าไว้ซึ่งเชื่อมโยงอยู่และผู้ให้บริการของเป้าหมายนั้นด้วย
ชุดของลักษณะที่เผยแพร่ลงในกราฟทรัพยากร Dependency จะระบุสำหรับแต่ละแอตทริบิวต์โดยใช้ฟังก์ชัน Attribute.Builder.aspects()
มีชั้นเรียนที่มีชื่อและสร้างความสับสน
ที่เข้าร่วมในกระบวนการนี้ 2-3 ชั้นเรียน ได้แก่
AspectClass
คือการใช้งานในด้านต่างๆ ซึ่งอาจอยู่ใน Java (ในกรณีนี้เป็นคลาสย่อย) หรือใน Starlark (ในกรณีนี้เป็นอินสแตนซ์ของStarlarkAspectClass
) โดยเทียบได้กับRuleConfiguredTargetFactory
AspectDefinition
คือคําจํากัดความของลักษณะ ซึ่งรวมถึงผู้ให้บริการที่จําเป็น ผู้ให้บริการที่มีให้ และมีการอ้างอิงถึงการใช้งาน เช่น อินสแตนซ์AspectClass
ที่เหมาะสม ซึ่งคล้ายกับRuleClass
AspectParameters
เป็นวิธีพารามิเตอร์ที่จะทำให้มุมมองต่างๆ เผยแพร่ลงในกราฟทรัพยากร Dependency ได้ ค่าปัจจุบันคือสตริงที่เชื่อมกับสตริง ตัวอย่างที่ดีของประโยชน์คือบัฟเฟอร์โปรโตคอล ถ้าภาษาหนึ่งมี API หลาย API ควรเผยแพร่ข้อมูลของ API ที่บัฟเฟอร์โปรโตคอลให้ลงไปในกราฟทรัพยากร DependencyAspect
แสดงข้อมูลทั้งหมดที่จําเป็นสําหรับคํานวณแง่มุมที่ส่งต่อไปยังกราฟความเกี่ยวข้อง ซึ่งประกอบด้วยคลาสการแสดงผล คำจำกัดความ และพารามิเตอร์RuleAspect
คือฟังก์ชันที่กำหนดว่ากฎใดควรเผยแพร่ในลักษณะใด นั่นคือฟังก์ชันRule
->Aspect
ความซับซ้อนที่คาดไม่ถึงคือแง่มุมหนึ่งสามารถแนบไปกับแง่มุมอื่นได้ เช่น แง่มุมที่รวบรวม classpath สําหรับ Java IDE อาจต้องการทราบเกี่ยวกับไฟล์ .jar ทั้งหมดใน classpath แต่ไฟล์บางไฟล์เป็นบัฟเฟอร์โปรโตคอล ในกรณีนี้ แง่มุม IDE จะต้องการแนบไปกับคู่ (proto_library
rule + Java proto aspect)
ระบบจะบันทึกความซับซ้อนของแง่มุมต่างๆ ไว้ในชั้นเรียน
AspectCollection
แพลตฟอร์มและชุดเครื่องมือ
Bazel รองรับบิลด์แบบหลายแพลตฟอร์ม กล่าวคือ บิลด์อาจมีสถาปัตยกรรมหลายอย่างที่การดำเนินการของบิลด์ทำงาน และบิลด์หลายรายการสำหรับโค้ดที่สร้างขึ้น สถาปัตยกรรมเหล่านี้เรียกว่าแพลตฟอร์มในภาษาของ Bazel (ดูเอกสารประกอบฉบับเต็มที่นี่)
แพลตฟอร์มจะอธิบายโดยการแมปคีย์-ค่าจากการตั้งค่าข้อจำกัด (เช่น แนวคิดของ "สถาปัตยกรรม CPU") ไปจนถึงค่าจำกัด (เช่น CPU ตัวใดตัวหนึ่ง เช่น x86_64) เรามี "พจนานุกรม" ของการตั้งค่าข้อจำกัดและค่าจำกัดที่ใช้บ่อยที่สุดในที่เก็บ @platforms
แนวคิดของ toolchain มาจากข้อเท็จจริงที่ว่าคุณอาจต้องใช้คอมไพเลอร์ที่แตกต่างกัน ทั้งนี้ขึ้นอยู่กับแพลตฟอร์มที่ใช้บิลด์และแพลตฟอร์มเป้าหมาย เช่น toolchain C++ บางรายการอาจทำงานบนระบบปฏิบัติการที่เฉพาะเจาะจงและสามารถกำหนดเป้าหมายไปยังระบบปฏิบัติการอื่นๆ ได้ Bazel ต้องกำหนดคอมไพเลอร์ C++ ที่ใช้ตามการเรียกใช้ที่กำหนดและแพลตฟอร์มเป้าหมาย (ดูเอกสารประกอบสำหรับชุดเครื่องมือที่นี่)
ในการทำเช่นนี้ เครื่องมือทางเทคนิคจะมีคำอธิบายประกอบชุดข้อจำกัดด้านการดำเนินการและแพลตฟอร์มเป้าหมายที่รองรับ ด้วยเหตุนี้ คําจํากัดความของเครื่องมือทางเทคนิคจึงแบ่งออกเป็น 2 ส่วน ดังนี้
- กฎ
toolchain()
ที่อธิบายชุดการดำเนินการและข้อจํากัดเป้าหมายจะบังคับให้ Toolchain รองรับและระบุว่า Toolchain เป็นชนิดใด (เช่น C++ หรือ Java) (กฎหลังจะแสดงด้วยกฎtoolchain_type()
) - กฎเฉพาะภาษาที่อธิบาย Toolchain จริง (เช่น
cc_toolchain()
)
การดำเนินการนี้เกิดขึ้นเนื่องจากเราจำเป็นต้องทราบข้อจำกัดของเครื่องมือทุกชุดเพื่อทำการแก้ไขเครื่องมือและกฎ *_toolchain()
สำหรับภาษาใดภาษาหนึ่งจะมีข้อมูลมากกว่านั้นมาก จึงใช้เวลาโหลดนานกว่า
แพลตฟอร์มการดำเนินการจะระบุโดยใช้วิธีใดวิธีหนึ่งต่อไปนี้
- ในไฟล์ WORKSPACE โดยใช้ฟังก์ชัน
register_execution_platforms()
- ในบรรทัดคำสั่งโดยใช้ตัวเลือกบรรทัดคำสั่ง --extra_execution_platforms
ระบบจะคำนวณชุดแพลตฟอร์มการดำเนินการที่ใช้ได้ใน
RegisteredExecutionPlatformsFunction
แพลตฟอร์มเป้าหมายสําหรับเป้าหมายที่กําหนดค่าไว้จะกําหนดโดย
PlatformOptions.computeTargetPlatform()
เป็นรายการแพลตฟอร์มเพราะในที่สุดแล้วเราต้องการรองรับแพลตฟอร์มเป้าหมายหลายแพลตฟอร์ม แต่ก็ยังไม่ได้นำมาใช้
ชุดของ Toolchain ที่จะใช้สำหรับเป้าหมายที่กำหนดค่าจะกำหนดโดย ToolchainResolutionFunction
โดยเป็นฟังก์ชันของสิ่งต่อไปนี้
- ชุดเครื่องมือทางเทคนิคที่ลงทะเบียน (ในไฟล์ WORKSPACE และการกำหนดค่า)
- แพลตฟอร์มการดำเนินการและแพลตฟอร์มเป้าหมายที่ต้องการ (ในการกําหนดค่า)
- ชุดประเภท Toolchain ที่เป้าหมายที่กำหนดค่าไว้ต้องใช้ (ใน
UnloadedToolchainContextKey)
- ชุดข้อจำกัดแพลตฟอร์มการดำเนินการของเป้าหมายที่กำหนดค่า (แอตทริบิวต์
exec_compatible_with
) และการกำหนดค่า (--experimental_add_exec_constraints_to_targets
) ในUnloadedToolchainContextKey
ผลลัพธ์ที่ได้คือ UnloadedToolchainContext
ซึ่งโดยพื้นฐานแล้วเป็นแผนที่จากประเภท Toolchain (แสดงเป็นอินสแตนซ์ ToolchainTypeInfo
) ไปยังป้ายกำกับของ Toolchain ที่เลือก ไฟล์นี้เรียกว่า "ไม่ได้โหลด" เนื่องจากไม่มีเครื่องมือทางเทคนิคเอง แต่มีเฉพาะป้ายกำกับของเครื่องมือทางเทคนิคเท่านั้น
จากนั้นระบบจะโหลดเครื่องมือทางเทคนิคโดยใช้ ResolvedToolchainContext.load()
และนำไปใช้งานโดยการติดตั้งใช้งานเป้าหมายที่กำหนดค่าไว้ซึ่งขอเครื่องมือทางเทคนิคเหล่านั้น
นอกจากนี้เรายังมีระบบเดิมที่ต้องมีการกำหนดค่า "โฮสต์" และการกำหนดค่าเป้าหมายเพียงรายการเดียวที่แสดงด้วยแฟล็กการกำหนดค่าต่างๆ เช่น --cpu
เราจะค่อยๆ เปลี่ยนไปใช้ระบบข้างต้น เราได้ใช้การแมปแพลตฟอร์มเพื่อแปลค่าระหว่างแฟล็กเดิมกับข้อจำกัดแพลตฟอร์มรูปแบบใหม่ ในการจัดการกรณีที่ผู้คนใช้ค่าการกําหนดค่าเดิม
โค้ดของพวกเขาอยู่ใน PlatformMappingFunction
และใช้ "ภาษาง่ายๆ" ที่ไม่ใช่ Starlark
ข้อจำกัด
บางครั้งมีลูกค้าต้องการตั้งเป้าหมายว่าเข้ากันได้กับแพลตฟอร์มเพียง 2-3 แพลตฟอร์ม Bazel มีกลไกหลายอย่าง (ซึ่งน่าเสียดาย) ในการบรรลุเป้าหมายนี้ ดังนี้
- ข้อจำกัดเฉพาะกฎ
environment_group()
/environment()
- ข้อจำกัดของแพลตฟอร์ม
ข้อจำกัดเฉพาะกฎส่วนใหญ่ใช้ใน Google สำหรับกฎ Java ซึ่งกำลังจะเลิกใช้งานและไม่มีให้ใช้งานใน Bazel แต่ซอร์สโค้ดอาจมีการอ้างอิงถึงข้อจำกัดดังกล่าว แอตทริบิวต์ที่ควบคุมกระบวนการนี้เรียกว่า
constraints=
สภาพแวดล้อม_group() และสภาพแวดล้อม()
กฎเหล่านี้เป็นกลไกเดิมและไม่ได้ใช้กันอย่างแพร่หลาย
กฎการสร้างทั้งหมดสามารถประกาศ "สภาพแวดล้อม" ที่สามารถสร้างได้ โดยที่ "สภาพแวดล้อม" คืออินสแตนซ์ของกฎ environment()
คุณสามารถระบุสภาพแวดล้อมที่รองรับสำหรับกฎได้หลายวิธี ดังนี้
- ผ่านแอตทริบิวต์
restricted_to=
รูปแบบนี้เป็นรูปแบบที่ตรงที่สุดของข้อกําหนด ซึ่งจะประกาศชุดสภาพแวดล้อมที่แน่นอนที่กฎรองรับสําหรับกลุ่มนี้ - ผ่านแอตทริบิวต์
compatible_with=
การดำเนินการนี้เป็นการประกาศสภาพแวดล้อมที่กฎรองรับนอกเหนือจากสภาพแวดล้อม "มาตรฐาน" ที่รองรับโดยค่าเริ่มต้น - ผ่านแอตทริบิวต์ระดับแพ็กเกจ
default_restricted_to=
และdefault_compatible_with=
- ผ่านข้อกําหนดเริ่มต้นในกฎ
environment_group()
สภาพแวดล้อมทุกสภาพแวดล้อมจะอยู่ในกลุ่มของคู่แข่งที่เกี่ยวข้องตามธีม (เช่น "สถาปัตยกรรม CPU" "เวอร์ชัน JDK" หรือ "ระบบปฏิบัติการบนอุปกรณ์เคลื่อนที่") การกําหนดกลุ่มสภาพแวดล้อมจะระบุสภาพแวดล้อมที่ควรรองรับโดย "ค่าเริ่มต้น" หากไม่ได้ระบุไว้เป็นอย่างอื่นโดยแอตทริบิวต์restricted_to=
/environment()
กฎที่ไม่มีแอตทริบิวต์ดังกล่าว จะรับช่วงค่าเริ่มต้นทั้งหมด - ผ่านค่าเริ่มต้นของคลาสกฎ ซึ่งจะลบล้างค่าเริ่มต้นส่วนกลางสำหรับอินสแตนซ์ทั้งหมดของคลาสกฎที่ระบุ ตัวอย่างเช่น วิธีนี้จะช่วยให้กฎ
*_test
ทั้งหมดทดสอบได้โดยที่แต่ละอินสแตนซ์ไม่ต้องประกาศความสามารถนี้อย่างชัดแจ้ง
มีการใช้งาน environment()
เป็นกฎปกติ ส่วน environment_group()
เป็นทั้งคลาสย่อยของ Target
แต่ไม่ใช่ Rule
(EnvironmentGroup
) และฟังก์ชันที่พร้อมใช้งานโดยค่าเริ่มต้นจาก Starlark (StarlarkLibrary.environmentGroup()
) ซึ่งสุดท้ายแล้วจะสร้างเป้าหมายที่เป็นนามแฝง ทั้งนี้เพื่อหลีกเลี่ยงการขึ้นต่อกันแบบวนซ้ำที่จะเกิดขึ้นเนื่องจากแต่ละสภาพแวดล้อมต้องประกาศกลุ่มสภาพแวดล้อมที่กลุ่มสภาพแวดล้อมนั้นเป็นสมาชิกอยู่ และแต่ละกลุ่มสภาพแวดล้อมต้องประกาศสภาพแวดล้อมเริ่มต้น
คุณจำกัดบิลด์ให้ใช้ได้กับบางสภาพแวดล้อมได้โดยใช้ตัวเลือกบรรทัดคำสั่ง --target_environment
การใช้งานการตรวจสอบข้อจำกัดมีอยู่ใน RuleContextConstraintSemantics
และ TopLevelConstraintSemantics
ข้อจำกัดของแพลตฟอร์ม
ปัจจุบันวิธี "อย่างเป็นทางการ" ในการอธิบายแพลตฟอร์มที่เป้าหมายเข้ากันได้คือการใช้ข้อจำกัดเดียวกับที่ใช้อธิบายเครื่องมือและแพลตฟอร์ม อยู่ระหว่างตรวจสอบการดึงคำขอ #10945
ระดับการแชร์
ถ้าคุณทำงานกับโค้ดเบสขนาดใหญ่กับนักพัฒนาซอฟต์แวร์จำนวนมาก (เช่นที่ Google) คุณต้องดูแลไม่ให้คนอื่นๆ พึ่งพาโค้ดของคุณโดยพลการ มิเช่นนั้น ตามกฎของ Hyrum ผู้คนจะพึ่งพฤติกรรมที่คุณถือว่าเป็นรายละเอียดการนำไปใช้
Bazel รองรับการดำเนินการนี้ด้วยกลไกที่เรียกว่าระดับการเข้าถึง ซึ่งคุณสามารถประกาศว่าเป้าหมายหนึ่งๆ ใช้ได้เฉพาะกับแอตทริบิวต์visibility เท่านั้น แอตทริบิวต์นี้มีความพิเศษเล็กน้อยเนื่องจากแม้ว่าจะมีรายการป้ายกำกับ แต่ป้ายกำกับเหล่านี้อาจเข้ารหัสรูปแบบเหนือชื่อแพ็กเกจแทนที่จะเป็นเคอร์เซอร์ไปยังเป้าหมายที่เฉพาะเจาะจง (ใช่ เป็นข้อบกพร่องของการออกแบบ)
ซึ่งทำได้ในที่ต่อไปนี้
- อินเทอร์เฟซ
RuleVisibility
แสดงการประกาศระดับการเข้าถึง โดยอาจเป็นค่าคงที่ (สาธารณะทั้งหมดหรือส่วนตัวทั้งหมด) หรือรายการป้ายกำกับก็ได้ - ป้ายกำกับอาจหมายถึงกลุ่มแพ็กเกจ (รายการแพ็กเกจที่กำหนดไว้ล่วงหน้า) แพ็กเกจโดยตรง (
//pkg:__pkg__
) หรือโครงสร้างย่อยของแพ็กเกจ (//pkg:__subpackages__
) ซึ่งแตกต่างจากไวยากรณ์บรรทัดคำสั่งซึ่งใช้//pkg:*
หรือ//pkg/...
- ใช้งานกลุ่มแพ็กเกจเป็นเป้าหมายของตนเอง (
PackageGroup
) และกำหนดเป้าหมายที่กำหนดค่า (PackageGroupConfiguredTarget
) แล้ว เราอาจแทนที่กลุ่มแพ็กเกจด้วยกฎง่ายๆ หากต้องการ ระบบนำตรรกะนี้ไปใช้ด้วยความช่วยเหลือของPackageSpecification
ซึ่งสอดคล้องกับรูปแบบเดียว เช่น//pkg/...
,PackageGroupContents
ซึ่งตรงกับแอตทริบิวต์packages
ของpackage_group
เดียว และPackageSpecificationProvider
ซึ่งรวมกันเป็นpackage_group
และincludes
ที่เป็นทางอ้อม - การแปลงจากรายการป้ายกำกับระดับการเข้าถึงเป็นทรัพยากร Dependency จะดำเนินการใน
DependencyResolver.visitTargetVisibility
และที่อื่นๆ อีก 2-3 แห่ง - การตรวจสอบจริงจะเสร็จสิ้นใน
CommonPrerequisiteValidator.validateDirectPrerequisiteVisibility()
ชุดที่ซ้อนกัน
บ่อยครั้ง เป้าหมายที่กําหนดค่าไว้จะรวบรวมชุดไฟล์จากข้อกําหนด เพิ่มไฟล์ของตัวเอง และรวมชุดข้อมูลรวมไว้ในผู้ให้บริการข้อมูลแบบเปลี่ยนผ่านเพื่อให้เป้าหมายที่กําหนดค่าไว้ซึ่งใช้ข้อมูลดังกล่าวทําสิ่งเดียวกันได้ ตัวอย่าง
- ไฟล์ส่วนหัว C++ ที่ใช้สำหรับบิลด์
- ไฟล์ออบเจ็กต์ที่แทนการปิดแบบทรานซิทีฟของ
cc_library
- ชุดไฟล์ .jar ที่ต้องอยู่ใน Classpath เพื่อให้กฎ Java คอมไพล์หรือทํางานได้
- ชุดของไฟล์ Python เมื่อปิดทรานซิทีฟของกฎ Python
หากเราทำเช่นนี้โดยใช้วิธีที่ไร้เดียงสา เช่น List
หรือ Set
เราจะได้การใช้หน่วยความจำกำลังสอง กล่าวคือ หากมีเชนกฎ N กฎและกฎแต่ละข้อเพิ่มไฟล์ เราจะมีสมาชิกคอลเล็กชัน 1+2...+N คน
ในการแก้ปัญหานี้ เราจึงคิดวิธี NestedSet
โครงสร้างข้อมูลประกอบด้วยอินสแตนซ์ NestedSet
อื่นๆ และสมาชิกบางส่วนของตัวเอง จึงสร้างกราฟแบบอะไซคัลที่มีทิศทางของชุดต่างๆ ซึ่งจะเปลี่ยนแปลงไม่ได้และสมาชิกสามารถทำซ้ำได้ เรากําหนดลําดับการวนซ้ำหลายรายการ (NestedSet.Order
) ได้แก่ ลําดับก่อน ลําดับหลัง ลําดับเชิง topologic (โหนดจะตามหลังบรรพบุรุษเสมอ) และ "ไม่สนใจ แต่ควรเหมือนกันทุกครั้ง"
โครงสร้างข้อมูลเดียวกันนี้เรียกว่า depset
ใน Starlark
รายการต่างๆ และการดำเนินการ
บิลด์จริงประกอบด้วยชุดคำสั่งที่ต้องเรียกใช้เพื่อสร้างเอาต์พุตที่ผู้ใช้ต้องการ คำสั่งจะแสดงเป็นอินสแตนซ์ของคลาส Action
และไฟล์จะแสดงเป็นอินสแตนซ์ของคลาส Artifact
กราฟเหล่านี้ถูกจัดเรียงเป็นกราฟแบบ 2 ภาคแบบมีทิศทาง ที่เรียกว่า "กราฟการกระทำ"
ออบเจ็กต์โค้ดมี 2 ประเภท ได้แก่ ออบเจ็กต์โค้ดต้นทาง (ออบเจ็กต์โค้ดที่มีให้ใช้งานก่อน Bazel เริ่มดำเนินการ) และออบเจ็กต์โค้ดที่ดึงข้อมูล (ออบเจ็กต์โค้ดที่ต้องสร้าง) อาร์ติแฟกต์ที่ดึงข้อมูลมาอาจมีได้หลายประเภท ดังนี้
- **รายการทั่วไป **ข้อมูลเหล่านี้จะตรวจสอบความเป็นปัจจุบันด้วยการคำนวณตรวจสอบข้อผิดพลาดโดยใช้เวลา mtime เป็นทางลัด เราจะไม่ตรวจสอบความถูกต้องของไฟล์หากเวลาไม่มีการเปลี่ยนแปลง
- อาร์ติแฟกต์ลิงก์สัญลักษณ์ที่ยังไม่ได้รับการแก้ไข ซึ่งมีการตรวจสอบความทันสมัยโดยเรียกใช้ readlink() ซึ่งต่างจากอาร์ติแฟกต์ทั่วไปตรงที่อาจทำให้เกิดลิงก์สัญลักษณ์ที่ทำให้อันตราย มักใช้ในกรณีที่จะแพ็กไฟล์บางไฟล์ลงในไฟล์เก็บถาวร
- อาร์ติแฟกต์ต้นไม้ ไฟล์เหล่านี้ไม่ใช่ไฟล์เดี่ยว แต่เป็นแผนผังไดเรกทอรี ระบบจะตรวจสอบความทันสมัยของไฟล์โดยตรวจสอบชุดไฟล์และเนื้อหาของไฟล์ โดยจะแสดงเป็น
TreeArtifact
- อาร์ติแฟกต์ข้อมูลเมตาแบบคงที่ การเปลี่ยนแปลงอาร์ติแฟกต์เหล่านี้จะไม่ทริกเกอร์การสร้างใหม่ ข้อมูลนี้ใช้สำหรับข้อมูลการประทับเวลาของบิลด์เท่านั้น เราไม่ต้องการสร้างใหม่เพียงเพราะเวลาปัจจุบันมีการเปลี่ยนแปลง
ไม่มีเหตุผลพื้นฐานใดที่ทำให้อาร์ติแฟกต์ต้นฉบับไม่สามารถเป็นอาร์ติแฟกต์ต้นไม้หรืออาร์ติแฟกต์ลิงก์สัญลักษณ์ที่ยังไม่ได้รับการแก้ไข เพียงแต่เรายังไม่ได้ใช้งาน (แต่ควรใช้งาน การอ้างอิงไดเรกทอรีต้นฉบับในไฟล์ 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 ของกราฟการดำเนินการไปยังขอบของ Skyframe มีการอธิบายไว้ใน ActionExecutionFunction.getInputDeps()
และ Artifact.key()
และมีการเพิ่มประสิทธิภาพเล็กน้อยเพื่อทำให้จำนวนขอบของ Skyframe อยู่ในระดับต่ำ
- อาร์ติแฟกต์ที่ได้มาไม่มี
SkyValue
เป็นของตัวเอง แต่จะใช้Artifact.getGeneratingActionKey()
เพื่อค้นหาคีย์สําหรับการดําเนินการที่สร้างรายการนั้นแทน - ชุดที่ฝังจะมีคีย์ Skyframe ของตนเอง
การดำเนินการที่แชร์
การดำเนินการบางอย่างสร้างขึ้นโดยเป้าหมายที่กำหนดค่าไว้หลายรายการ กฎ Starlark จะจํากัดมากกว่าเนื่องจากได้รับอนุญาตให้วางการดำเนินการที่ดึงข้อมูลไว้ในไดเรกทอรีที่กําหนดโดยการกำหนดค่าและแพ็กเกจเท่านั้น (แต่ถึงอย่างนั้น กฎในแพ็กเกจเดียวกันก็อาจขัดแย้งกันได้) แต่กฎที่ติดตั้งใช้งานใน Java จะวางอาร์ติแฟกต์ที่ดึงข้อมูลไว้ที่ใดก็ได้
วิธีนี้ถือเป็นฟีเจอร์ที่ไม่ถูกต้อง แต่การกำจัดฟีเจอร์ดังกล่าวนั้นเป็นเรื่องยากมากเพราะจะช่วยลดเวลาในการดำเนินการได้อย่างมาก เช่น เมื่อต้องประมวลผลไฟล์ต้นฉบับและต้องอ้างอิงไฟล์นั้นด้วยกฎหลายข้อ (handwave-handwave) เรื่องนี้ขึ้นอยู่กับค่าใช้จ่ายของ RAM บางส่วน: อินสแตนซ์แต่ละรายการของการดำเนินการที่แชร์ต้องจัดเก็บไว้ในหน่วยความจำแยกกัน
หากการดำเนินการ 2 รายการสร้างไฟล์เอาต์พุตเดียวกัน การดำเนินการดังกล่าวต้องเหมือนกันทุกประการ กล่าวคือ มีอินพุตเดียวกัน เอาต์พุตเดียวกัน และเรียกใช้บรรทัดคำสั่งเดียวกัน ความสัมพันธ์ที่เทียบเท่านี้ใช้ใน Actions.canBeShared()
และได้รับการยืนยันระหว่างระยะการวิเคราะห์และระยะดำเนินการโดยดูที่การดำเนินการแต่ละรายการ
การดำเนินการนี้มีการติดตั้งใช้งานใน SkyframeActionExecutor.findAndStoreArtifactConflicts()
และเป็นหนึ่งในไม่กี่แห่งใน Bazel ที่จำเป็นต้องมีมุมมอง "ทั่วโลก" ของบิลด์
ระยะการดำเนินการ
ขั้นตอนนี้เป็นเวลาที่ Bazel เริ่มเรียกใช้การดำเนินการสร้างจริง เช่น คำสั่งที่ผลิตเอาต์พุต
สิ่งแรกที่ Bazel ทำหลังจากระยะการวิเคราะห์คือกำหนดว่าต้องสร้างอาร์ติแฟกต์ใด ตรรกะของสิ่งนี้จะเข้ารหัสในรูปแบบ TopLevelArtifactHelper
กล่าวโดยคร่าวๆ ก็คือ filesToBuild
ของเป้าหมายที่กำหนดค่าในบรรทัดคำสั่งและเนื้อหาของกลุ่มเอาต์พุตพิเศษเพื่อวัตถุประสงค์ที่ชัดเจนในการแสดง "หากเป้าหมายนี้อยู่ในบรรทัดคำสั่ง ให้สร้างอาร์ติแฟกต์เหล่านี้"
ขั้นตอนถัดไปคือการสร้างรูทการดําเนินการ เนื่องจาก Bazel มีตัวเลือกในการอ่านแพ็กเกจแหล่งที่มาจากตำแหน่งต่างๆ ในระบบไฟล์ (--package_path
) จึงต้องมอบการดำเนินการที่ดำเนินการในเครื่องพร้อมทรีแหล่งที่มาแบบเต็ม การดำเนินการนี้จัดการโดยคลาส SymlinkForest
และทำงานโดยการบันทึกเป้าหมายทั้งหมดที่ใช้ในระยะการวิเคราะห์ และสร้างต้นไม้ไดเรกทอรีเดียวซึ่งลิงก์สัญลักษณ์แพ็กเกจทั้งหมดที่มีเป้าหมายที่ใช้จากตำแหน่งจริง อีกทางเลือกหนึ่งคือการส่งผ่านเส้นทางที่ถูกต้องไปยังคำสั่ง (โดยนำ --package_path
มาพิจารณา)
ซึ่งเป็นสิ่งที่ไม่พึงประสงค์เนื่องจากเหตุผลต่อไปนี้
- โดยเปลี่ยนบรรทัดคำสั่งการดำเนินการเมื่อมีการย้ายแพ็กเกจจากรายการเส้นทางแพ็กเกจไปยังรายการอื่น (เคยเกิดขึ้นบ่อย)
- หากเรียกใช้การดำเนินการจากระยะไกลจะให้ผลลัพธ์ในบรรทัดคำสั่งต่างกัน
- จำเป็นต้องเปลี่ยนรูปแบบบรรทัดคำสั่งเฉพาะในเครื่องมือที่ใช้งานอยู่ (พิจารณาความแตกต่างระหว่างเส้นทาง เช่น Java classpaths และ C++ รวม)
- การเปลี่ยนบรรทัดคำสั่งของการดำเนินการจะทำให้รายการแคชการดำเนินการของการดำเนินการนั้นไม่ถูกต้อง
--package_path
กำลังเลิกใช้งานอย่างช้าๆ
จากนั้น Bazel เริ่มข้ามผ่านกราฟการกระทำ (กราฟสองพาร์ท กราฟทิศทางที่ประกอบด้วยการกระทำ และอาร์ติแฟกต์อินพุตและเอาต์พุต) และการเรียกใช้การกระทำ
การดำเนินการแต่ละรายการจะแสดงโดยอินสแตนซ์ของSkyValue
คลาส ActionExecutionValue
เนื่องจากการดำเนินการมีค่าใช้จ่ายสูง เราจึงมีการแคช 2-3 ชั้นที่เข้าถึงได้เบื้องหลัง Skyframe ดังนี้
ActionExecutionFunction.stateMap
มีข้อมูลที่ทำให้การรีสตาร์ท Skyframe ของActionExecutionFunction
ราคาถูก- แคชการดำเนินการในเครื่องมีข้อมูลเกี่ยวกับสถานะของระบบไฟล์
- ระบบการดําเนินการจากระยะไกลมักจะมีแคชของตนเองด้วย
แคชการดำเนินการภายใน
แคชนี้เป็นอีกเลเยอร์ที่อยู่เบื้องหลัง Skyframe แม้ว่าระบบจะเรียกใช้การดำเนินการใน Skyframe อีกครั้ง แต่การดำเนินการดังกล่าวก็ยังคงแสดงผลในแคชการดำเนินการในเครื่องได้ โดยจะแสดงสถานะระบบไฟล์ในเครื่องและถูกทำให้เป็นอนุกรมลงในดิสก์ ซึ่งหมายความว่าเมื่อเริ่มต้นเซิร์ฟเวอร์ Bazel ใหม่ เซิร์ฟเวอร์จะได้รับแคชการดำเนินการในเครื่องได้ แม้ว่ากราฟ Skyframe จะว่างเปล่าก็ตาม
แคชนี้จะตรวจสอบ Hit โดยใช้เมธอด
ActionCacheChecker.getTokenIfNeedToExecute()
ซึ่งตรงข้ามกับชื่อที่เรียก เพราะเป็นแผนที่จากเส้นทางของอาร์ติแฟกต์ที่ดึงข้อมูลมายังการดำเนินการที่ทำให้เกิดอาร์ติแฟกต์ การดําเนินการมีคำอธิบายดังนี้
- ชุดของไฟล์อินพุตและเอาต์พุตและการตรวจสอบข้อผิดพลาด
- "คีย์การดำเนินการ" ซึ่งมักจะเป็นบรรทัดคำสั่งที่เรียกใช้ แต่โดยทั่วไปจะแสดงทุกอย่างที่ไม่ได้บันทึกไว้โดย checksum ของไฟล์อินพุต (เช่น สำหรับ
FileWriteAction
คือ checksum ของข้อมูลที่เขียนไว้)
นอกจากนี้ ยังมี “แคชการดำเนินการจากด้านบน” ที่อยู่ระหว่างการพัฒนา ซึ่งยังใช้แฮชแบบทรานซิทีฟเพื่อหลีกเลี่ยงการไปยังแคชหลายครั้ง
การค้นพบอินพุตและการกรองอินพุต
การกระทำบางอย่างซับซ้อนกว่าแค่การมีชุดอินพุต การเปลี่ยนแปลงชุดอินพุตของการดําเนินการมีอยู่ 2 รูปแบบ ดังนี้
- การดำเนินการอาจค้นพบอินพุตใหม่ก่อนดำเนินการ หรือตัดสินใจว่าอินพุตบางอย่างไม่จำเป็น ตัวอย่างมาตรฐานคือ C++ ซึ่งควรที่จะคาดเดาอย่างมีข้อมูลเกี่ยวกับไฟล์ส่วนหัวที่ไฟล์ C++ ใช้จาก Closure แบบทรานซิทีฟ เพื่อที่เราจะได้ไม่ต้องส่งไฟล์ทุกไฟล์ไปยังผู้ดำเนินการระยะไกล ดังนั้นเราจึงมีตัวเลือกที่จะไม่ลงทะเบียนไฟล์ส่วนหัวทุกไฟล์เป็น "อินพุต" แต่สแกนไฟล์ต้นฉบับเพื่อหาส่วนหัวที่รวมอยู่แบบทรานซิทีฟ และทําเครื่องหมายไฟล์ส่วนหัวเหล่านั้นเป็นอินพุตที่กล่าวถึงในคำสั่ง
#include
เท่านั้น (เราประเมินค่าสูงเกินจริงเพื่อที่จะไม่ต้องใช้โปรแกรมเตรียม C แบบสมบูรณ์) ปัจจุบันตัวเลือกนี้ได้รับการตั้งค่าเป็น "เท็จ" ใน Bazel และใช้ใน Google เท่านั้น - การดำเนินการอาจพบว่าไฟล์บางไฟล์ไม่มีการใช้งานระหว่างการดำเนินการ ใน C++ สิ่งนี้เรียกว่า "ไฟล์ .d": คอมไพเลอร์จะบอกไฟล์ส่วนหัวที่ใช้หลังจากสร้างไปแล้ว และ Bazel ใช้ประโยชน์จากข้อเท็จจริงนี้เพื่อหลีกเลี่ยงความอับอายที่การปรับปรุงแย่กว่า Make ซึ่งจะประมาณได้ดีกว่าเครื่องมือสแกนรวม เนื่องจากใช้คอมไพเลอร์
วิธีดำเนินการเหล่านี้ทำได้โดยใช้วิธีดำเนินการ
- โทรหา
Action.discoverInputs()
ซึ่งควรแสดงผลชุดอาร์ติแฟกต์ที่ซ้อนกันซึ่งพิจารณาแล้วว่าจำเป็น ตัวแปรเหล่านี้ต้องเป็นอาร์ติแฟกต์ต้นทางเพื่อไม่ให้มีขอบของทรัพยากร Dependency ในกราฟการดำเนินการที่ไม่มีสิ่งที่เทียบเท่ากันในกราฟเป้าหมายที่กําหนดค่าไว้ - การดำเนินการนี้จะเรียกใช้
Action.execute()
- เมื่อสิ้นสุด
Action.execute()
การดำเนินการจะเรียกAction.updateInputs()
เพื่อบอก Bazel ว่าไม่จำเป็นต้องใช้อินพุตทั้งหมด ซึ่งอาจส่งผลให้มีการบิลด์ที่เพิ่มขึ้นไม่ถูกต้องหากมีการรายงานว่าอินพุตที่ใช้แล้วไม่ได้ใช้งาน
เมื่อแคชการดำเนินการแสดงผล Hit ในอินสแตนซ์การดำเนินการใหม่ (เช่น สร้างหลังจากรีสตาร์ทเซิร์ฟเวอร์) Bazel จะเรียก updateInputs()
เองเพื่อให้ชุดอินพุตแสดงผลของการค้นพบอินพุตและการตัดข้อมูลที่เคยทำก่อนหน้านี้
การดำเนินการของ Starlark จะใช้ประโยชน์จากเครื่องมือเพื่อประกาศอินพุตบางรายการว่าไม่ได้ใช้งานโดยใช้อาร์กิวเมนต์ unused_inputs_list=
ของ ctx.actions.run()
วิธีต่างๆ ในการดำเนินการ: Strategies/ActionContexts
การดำเนินการบางอย่างสามารถเรียกใช้ได้หลายวิธี เช่น บรรทัดคำสั่งสามารถดำเนินการได้แบบในเครื่อง ดำเนินการในเครื่องแต่ในแซนด์บ็อกซ์ประเภทต่างๆ หรือดำเนินการจากระยะไกล แนวคิดที่รวมสิ่งนี้ไว้เรียกว่า ActionContext
(หรือ Strategy
เนื่องจากเราเปลี่ยนชื่อได้เพียงครึ่งทางเท่านั้น...)
วงจรชีวิตของบริบทการดำเนินการมีดังนี้
- เมื่อเริ่มระยะการดำเนินการ ระบบจะถามอินสแตนซ์
BlazeModule
ว่ามีบริบทการดำเนินการใดบ้าง สิ่งนี้จะเกิดขึ้นในเครื่องมือสร้างของExecutionTool
ประเภทบริบทการดำเนินการจะระบุโดยอินสแตนซ์ JavaClass
ที่อ้างถึงอินเทอร์เฟซย่อยของActionContext
และอินเทอร์เฟซที่บริบทการดำเนินการต้องใช้ - ระบบจะเลือกบริบทการดำเนินการที่เหมาะสมจากรายการที่พร้อมใช้งาน และส่งต่อไปยัง
ActionExecutionContext
และBlazeExecutor
- การดำเนินการจะขอบริบทโดยใช้
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 รายการ ได้แก่
- หากการกําหนดค่า 2 รายการเกิดขึ้นในบิลด์เดียวกัน ก็ควรมีไดเรกทอรีต่างกันเพื่อให้ทั้ง 2 รายการมีการดำเนินการเดียวกันในเวอร์ชันของตัวเอง มิเช่นนั้นหากการกําหนดค่า 2 รายการขัดแย้งกัน เช่น บรรทัดคําสั่งของการดำเนินการที่สร้างไฟล์เอาต์พุตเดียวกัน Bazel จะไม่ทราบว่าควรเลือกการดำเนินการใด ("การดำเนินการขัดแย้งกัน")
- หากการกำหนดค่า 2 รายการกล่าวถึงสิ่งเดียวกันอย่าง "คล้ายคลึง" การกำหนดค่าดังกล่าวควรมีชื่อเดียวกันเพื่อให้การดำเนินการที่ดำเนินการในรายการหนึ่งสามารถใช้ซ้ำสำหรับอีกรายการหนึ่งได้ในกรณีที่บรรทัดคำสั่งตรงกัน เช่น การเปลี่ยนแปลงตัวเลือกบรรทัดคำสั่งในคอมไพเลอร์ Java ไม่ควรทำให้การดำเนินการคอมไพล์ C++ ถูกเรียกใช้ซ้ำ
ตอนนี้เรายังคิดไม่ถึงวิธีหลักในการแก้ปัญหานี้ ซึ่งคล้ายกับโจทย์การตัดการกำหนดค่า ดูการสนทนาเพิ่มเติม เกี่ยวกับตัวเลือกได้ที่นี่ ส่วนที่เป็นปัญหาหลักๆ คือกฎของ Starlark (ซึ่งผู้เขียนมักไม่ค่อยคุ้นเคยกับ Bazel) และแง่มุมต่างๆ ซึ่งช่วยเพิ่มมิติของสิ่งที่สามารถสร้างไฟล์เอาต์พุต "เดียวกัน" ได้
แนวทางปัจจุบันคือส่วนของเส้นทางสำหรับการกําหนดค่าคือ <CPU>-<compilation mode>
ที่มีการเพิ่มส่วนต่อท้ายต่างๆ เพื่อให้การเปลี่ยนการกำหนดค่าที่ติดตั้งใช้งานใน Java จะไม่ทําให้การดำเนินการขัดแย้งกัน นอกจากนี้ยังมีการเพิ่ม Checksum ของชุดการเปลี่ยนการกำหนดค่า Starlark เพื่อไม่ให้ผู้ใช้สร้างความขัดแย้งของการดำเนินการ แต่ก็ยังห่างไกลจากความสมบูรณ์แบบ การดำเนินการนี้ใช้ใน OutputDirectories.buildMnemonic()
และอาศัยข้อมูลโค้ดการกำหนดค่าแต่ละรายการในการเพิ่มส่วนของตัวเองลงในชื่อไดเรกทอรีเอาต์พุต
การทดสอบ
Bazel รองรับการทดสอบที่ทำงานอยู่อย่างหลากหลาย โดยรองรับการดำเนินการต่อไปนี้
- เรียกใช้การทดสอบจากระยะไกล (หากมีแบ็กเอนด์การดำเนินการระยะไกล)
- การทดสอบหลายครั้งพร้อมกัน (สำหรับการลดจำนวนการทดสอบหรือรวบรวมข้อมูลเกี่ยวกับเวลา)
- การแยกการทดสอบ (การแยกเฟรมทดสอบในการทดสอบเดียวกันไปยังหลายกระบวนการเพื่อเพิ่มความเร็ว)
- ทำการทดสอบที่ไม่น่าเชื่อถืออีกครั้ง
- จัดกลุ่มการทดสอบเป็นชุดทดสอบ
การทดสอบเป็นเป้าหมายที่กำหนดค่าไว้ตามปกติซึ่งมี TestProvider ซึ่งจะอธิบายวิธีดำเนินการทดสอบ ดังนี้
- อาร์ติแฟกต์ที่มีอาคารซึ่งอยู่ระหว่างการทดสอบ นี่คือไฟล์ "สถานะแคช" ที่มีข้อความ
TestResultData
แบบอนุกรม - จำนวนครั้งที่ทำการทดสอบ
- จำนวนชาร์ดที่ควรแยกการทดสอบเป็น
- พารามิเตอร์บางอย่างเกี่ยวกับวิธีเรียกใช้การทดสอบ (เช่น การหมดเวลาทดสอบ)
การกำหนดการทดสอบที่จะดำเนินการ
การกำหนดการทดสอบที่จะทํางานเป็นกระบวนการที่ซับซ้อน
ขั้นแรก ในระหว่างการแยกวิเคราะห์รูปแบบเป้าหมาย ระบบจะขยายชุดทดสอบแบบซ้ำ มีการขยายการให้บริการใน TestsForTargetPatternFunction
สิ่งที่น่าประหลาดใจคือหากชุดทดสอบไม่ได้ประกาศการทดสอบใดๆ เลย จะเป็นการอ้างอิงการทดสอบทั้งหมดในแพ็กเกจ ซึ่งนำมาใช้ใน Package.beforeBuild()
ด้วยการเพิ่มแอตทริบิวต์โดยนัยชื่อว่า $implicit_tests
ในการทดสอบกฎชุดโปรแกรม
จากนั้นระบบจะกรองการทดสอบตามขนาด แท็ก การหมดเวลา และภาษาตามตัวเลือกบรรทัดคำสั่ง การดำเนินการนี้ใช้ใน TestFilter
และเรียกใช้จาก TargetPatternPhaseFunction.determineTests()
ระหว่างการแยกวิเคราะห์เป้าหมาย และใส่ผลลัพธ์ไว้ใน TargetPatternPhaseValue.getTestsToRunLabels()
สาเหตุที่ไม่สามารถกําหนดค่าแอตทริบิวต์กฎที่กรองได้นั้นเนื่องด้วยแอตทริบิวต์นี้เกิดขึ้นก่อนระยะการวิเคราะห์ จึงไม่สามารถกําหนดค่าได้
จากนั้นระบบจะประมวลผลเพิ่มเติมใน BuildView.createResult()
กล่าวคือ เป้าหมายที่การวิเคราะห์ล้มเหลวจะถูกกรองออก และการทดสอบจะแยกเป็นการทดสอบแบบเฉพาะตัวและไม่จำกัด จากนั้นระบบจะใส่ข้อมูลลงใน AnalysisResult
ซึ่งเป็นวิธีที่ ExecutionTool
รู้ว่าจะเรียกใช้การทดสอบใด
tests()
เป็นตัวดำเนินการการค้นหา (ติดตั้งใช้งานใน TestsFunction
) เพื่อระบุการทดสอบที่จะเรียกใช้เมื่อระบุเป้าหมายที่เฉพาะเจาะจงในบรรทัดคำสั่ง ซึ่งจะช่วยให้กระบวนการที่ซับซ้อนนี้มีความโปร่งใสมากขึ้น น่าเสียดายที่การนำไปใช้งานใหม่อาจต่างจากเดิมในหลายด้านมาก
กำลังทดสอบ
วิธีทำการทดสอบคือขออาร์ติแฟกต์สถานะแคช จากนั้นจะส่งผลให้เกิดการดำเนินการของ TestRunnerAction
ซึ่งจะเรียกใช้ TestActionContext
ที่เลือกไว้โดยตัวเลือกบรรทัดคำสั่ง --test_strategy
ซึ่งจะทำการทดสอบตามวิธีที่ขอ
การทดสอบจะทํางานตามโปรโตคอลที่ซับซ้อนซึ่งใช้ตัวแปรสภาพแวดล้อมเพื่อบอกการทดสอบว่าคาดหวังอะไรจากพวกเขา ดูคำอธิบายโดยละเอียดเกี่ยวกับสิ่งที่ Bazel คาดหวังจากการทดสอบและสิ่งที่การทดสอบคาดหวังจาก Bazel ได้ที่นี่ หากง่ายที่สุด รหัสการออกเป็น 0 หมายถึงสำเร็จ หรืออะไรก็ได้หมายถึงความล้มเหลว
นอกจากไฟล์สถานะแคชแล้ว กระบวนการทดสอบแต่ละครั้งยังปล่อยไฟล์อื่นๆ อีกจำนวนหนึ่งด้วย ไฟล์เหล่านี้จะอยู่ใน "ไดเรกทอรีบันทึกการทดสอบ" ซึ่งเป็นไดเรกทอรีย่อยที่ชื่อ testlogs
ของไดเรกทอรีเอาต์พุตของการกําหนดค่าเป้าหมาย
test.xml
ไฟล์ XML สไตล์ JUnit ที่แสดงรายละเอียดกรอบการทดสอบแต่ละรายการในชาร์ดทดสอบtest.log
เอาต์พุตคอนโซลของการทดสอบ โดยไม่มีการแยก stdout และ stderrtest.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
ซึ่งจะได้รับการแก้ไขเป็นค่าของ Flag การกําหนดค่า --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
ข้อกำหนดเล็กๆ น้อยๆ ของรูปแบบเอาต์พุตการค้นหาบางรูปแบบ (โปรโตนะอย่างแน่นอน) คือBazel ต้องปล่อย _all _ข้อมูลที่การโหลดแพ็กเกจให้มา เพื่อให้สามารถ แยกความแตกต่างของเอาต์พุตและระบุว่าเป้าหมายหนึ่งๆ มีการเปลี่ยนแปลงหรือไม่ ด้วยเหตุนี้ ค่าแอตทริบิวต์จึงต้องจัดเก็บเป็นอนุกรมได้ จึงมีแอตทริบิวต์ประเภทต่างๆ เพียงไม่กี่ประเภทที่ไม่มีแอตทริบิวต์ที่มีค่า 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 ได้รับการออกแบบมาเพื่อใช้ในโมโนเรโป (ต้นไม้แหล่งเดียวที่มีทุกอย่างที่จำเป็นต่อการสร้าง) แต่ 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>
การดึงข้อมูลที่เก็บจะเกิดขึ้นตามขั้นตอนต่อไปนี้
PackageLookupFunction
พบว่าต้องการที่เก็บและสร้างRepositoryName
เป็นSkyKey
ซึ่งจะเรียกใช้RepositoryLoaderFunction
RepositoryLoaderFunction
ส่งต่อคำขอไปยังRepositoryDelegatorFunction
ด้วยเหตุผลที่ไม่ชัดเจน (โค้ดระบุว่าต้องหลีกเลี่ยงการดาวน์โหลดสิ่งต่างๆ ซ้ำในกรณีที่ Skyframe รีสตาร์ท แต่ไม่ใช่การให้เหตุผลที่ชัดเจน)RepositoryDelegatorFunction
จะค้นหากฎที่เก็บซึ่งได้รับคําขอให้ดึงข้อมูลโดยวนผ่านข้อมูลส่วนต่างๆ ของไฟล์ WORKSPACE จนกว่าจะพบที่เก็บที่ขอ- พบ
RepositoryFunction
ที่เหมาะสมซึ่งใช้การดึงข้อมูลที่เก็บ โดยอาจเป็นการใช้งานที่เก็บของ Starlark หรือแผนที่แบบฮาร์ดโค้ดสำหรับที่เก็บที่มีการใช้งานใน Java
การแคชมีหลายเลเยอร์เนื่องจากการดึงข้อมูลที่เก็บมีราคาแพงมาก
- มีแคชสำหรับไฟล์ที่ดาวน์โหลดซึ่งคีย์โดยการตรวจสอบข้อผิดพลาด (
RepositoryCache
) ซึ่งกำหนดให้ไฟล์ตรวจสอบข้อผิดพลาดพร้อมใช้งานในไฟล์ WORKSPACE แต่ก็ยังคงดีต่อความต่อเนื่อง ซึ่งจะแชร์โดยอินสแตนซ์ของเซิร์ฟเวอร์ Bazel ทุกรายการในเวิร์กสเตชันเดียวกัน ไม่ว่าอินสแตนซ์เหล่านั้นจะอยู่ในพื้นที่ทำงานหรือฐานเอาต์พุตใด - มีการเขียน "ไฟล์เครื่องหมาย" สำหรับที่เก็บแต่ละแห่งใน
$OUTPUT_BASE/external
ซึ่งมี checksum ของกฎที่ใช้ในการดึงข้อมูล หากเซิร์ฟเวอร์ Bazel รีสตาร์ท แต่ checksum ไม่เปลี่ยนแปลง ระบบจะไม่ดึงข้อมูลอีกครั้ง การดำเนินการนี้ใช้ในRepositoryDelegatorFunction.DigestWriter
- ตัวเลือกบรรทัดคำสั่ง
--distdir
จะกำหนดแคชอื่นที่ใช้ค้นหาอาร์ติแฟกต์ที่จะดาวน์โหลด ซึ่งมีประโยชน์ในการตั้งค่าองค์กรที่ Bazel ไม่ควรดึงข้อมูลแบบสุ่มจากอินเทอร์เน็ตDownloadManager
เป็นผู้ติดตั้งใช้งาน
เมื่อดาวน์โหลดที่เก็บแล้ว อาร์ติแฟกต์ในที่เก็บจะถือว่าเป็นอาร์ติแฟกต์ต้นทาง ปัญหานี้ทำให้เกิดปัญหาเนื่องจากโดยปกติแล้ว Bazel จะตรวจสอบเวอร์ชันล่าสุดของอาร์ติแฟกต์ต้นทางด้วยการเรียกใช้ stat() และอาร์ติแฟกต์เหล่านี้จะถูกทำให้ไม่ถูกต้องเช่นกันเมื่อคำจำกัดความของที่เก็บมีการเปลี่ยนแปลง ดังนั้น FileStateValue
สำหรับอาร์ติแฟกต์ในที่เก็บภายนอกจึงจำเป็นต้องใช้ที่เก็บภายนอก ExternalFilesHelper
จะจัดการเรื่องนี้
ไดเรกทอรีที่มีการจัดการ
บางครั้งที่เก็บภายนอกจำเป็นต้องแก้ไขไฟล์ภายใต้รูทของพื้นที่ทำงาน (เช่น ตัวจัดการแพ็กเกจที่เก็บแพ็กเกจที่ดาวน์โหลดไว้ในไดเรกทอรีย่อยของโครงสร้างต้นทาง) ซึ่งขัดแย้งกับสมมติฐานของ Bazel ที่ว่าไฟล์ต้นฉบับมีเพียงผู้ใช้เท่านั้นที่แก้ไขได้ และอนุญาตให้แพ็กเกจอ้างอิงไดเรกทอรีทุกรายการภายใต้รูทเวิร์กช็อป Bazel ทำ 2 สิ่งต่อไปนี้เพื่อให้ที่เก็บภายนอกแบบนี้ใช้งานได้
- อนุญาตให้ผู้ใช้ระบุไดเรกทอรีย่อยของพื้นที่ทำงาน Bazel สามารถเข้าถึงได้ โดยจะแสดงอยู่ในไฟล์ชื่อ
.bazelignore
และใช้งานฟังก์ชันการทำงานในBlacklistedPackagePrefixesFunction
- เราจะเข้ารหัสการแมปจากไดเรกทอรีย่อยของเวิร์กスペースไปยังที่เก็บข้อมูลภายนอกที่จัดการ
ManagedDirectoriesKnowledge
และจัดการFileStateValue
ที่อ้างอิงถึงไดเรกทอรีย่อยดังกล่าวในลักษณะเดียวกับที่เก็บข้อมูลภายนอกทั่วไป
การแมปที่เก็บ
กรณีที่รีโพซิทอรีหลายแห่งต้องการใช้รีโพซิทอรีเดียวกัน แต่ใช้เวอร์ชันต่างกัน (นี่คืออินสแตนซ์ของ "ปัญหาการพึ่งพาแบบเพชร") ตัวอย่างเช่น หากไบนารี 2 รายการในที่เก็บข้อมูลแยกกันในบิลด์ต้องการใช้ Guava ก็อาจมีการอ้างอิง Guava ด้วยป้ายกำกับที่ขึ้นต้นด้วย @guava//
ทั้ง 2 รายการ และคาดว่าจะเป็นเวอร์ชันที่แตกต่างกัน
ดังนั้น Bazel จึงอนุญาตให้รายการหนึ่งแมปป้ายกำกับที่เก็บภายนอกอีกครั้งเพื่อให้สตริง @guava//
อ้างถึงที่เก็บ Guava หนึ่ง (เช่น @guava1//
) ในที่เก็บของไบนารีหนึ่งและที่เก็บ Guava อีกรายการหนึ่ง (เช่น @guava2//
) กับที่เก็บของอีกแห่งได้
หรือจะใช้เพื่อต่อเพชรก็ได้ หากที่เก็บขึ้นอยู่กับ @guava1//
และอีกรายการขึ้นอยู่กับ @guava2//
การแมปที่เก็บจะช่วยให้ 1 แมปที่เก็บทั้ง 2 รายการได้อีกครั้งเพื่อใช้ที่เก็บ Canonical @guava//
มีการระบุการแมปไว้ในไฟล์ WORKSPACE เป็นแอตทริบิวต์ repo_mapping
ของการกำหนดที่เก็บแต่ละรายการ จากนั้นจะปรากฏใน Skyframe ในฐานะสมาชิกของ WorkspaceFileValue
ซึ่งเชื่อมต่อกับสิ่งต่อไปนี้
Package.Builder.repositoryMapping
ที่ใช้สำหรับเปลี่ยนรูปแบบแอตทริบิวต์ที่มีป้ายกำกับ ของกฎในแพ็กเกจตามRuleClass.populateRuleAttributeValues()
Package.repositoryMapping
ซึ่งใช้ในเฟสการวิเคราะห์ (สำหรับการแก้ปัญหาต่างๆ เช่น$(location)
ที่ไม่ได้แยกวิเคราะห์ในเฟสการโหลด)BzlLoadFunction
สำหรับการแปลป้ายกำกับในคำสั่งLoad()
บิต JNI
เซิร์ฟเวอร์ของ Bazel นั้น_ ส่วนใหญ่เป็น _write ใน Java แต่มีข้อยกเว้นคือส่วนที่ Java ไม่สามารถทำได้เองหรือทำไม่ได้ในตอนที่ใช้ ซึ่งส่วนใหญ่จะจำกัดอยู่ที่การโต้ตอบกับระบบไฟล์ การควบคุมกระบวนการ และการดำเนินการระดับล่างอื่นๆ
โค้ด C++ จะอยู่ภายใต้ src/main/native และคลาส Java ที่มีเมธอดแบบเนทีฟดังนี้
NativePosixFiles
และNativePosixFileSystem
ProcessUtils
WindowsFileOperations
และWindowsFileProcesses
com.google.devtools.build.lib.platform
เอาต์พุตของคอนโซล
การปล่อยเอาต์พุตของคอนโซลดูเหมือนจะเป็นเรื่องง่าย แต่ผลจากการทำงานที่หลายกระบวนการ (บางครั้งทำงานจากระยะไกล) การแคชแบบละเอียด ความต้องการให้มีเอาต์พุตเทอร์มินัลที่มีสีสันและมีสีสัน และการมีเซิร์ฟเวอร์ที่ทำงานเป็นเวลานานทำให้เป็นงานที่ไม่สำคัญ
หลังจากเรียกใช้ RPC จากไคลเอ็นต์ ระบบจะสร้างอินสแตนซ์ RpcOutputStream
2 รายการ (สำหรับ Stdout และ Stderr) ที่จะส่งต่อข้อมูลที่พิมพ์ลงในไคลเอ็นต์ จากนั้นระบบจะรวมข้อมูลเหล่านี้ไว้ใน OutErr
(คู่ (stdout, stderr)) ทุกสิ่งที่ต้องพิมพ์บนคอนโซลจะผ่านสตรีมเหล่านี้ จากนั้นระบบจะส่งสตรีมเหล่านี้ให้
BlazeCommandDispatcher.execExclusively()
โดยค่าเริ่มต้น เอาต์พุตจะพิมพ์โดยมีลำดับการยกเว้น ANSI เมื่อไม่เป็นที่ต้องการ (--color=no
) จะถูก AnsiStrippingOutputStream
ตัดออก นอกจากนี้ ระบบจะเปลี่ยนเส้นทาง System.out
และ System.err
ไปยังสตรีมเอาต์พุตเหล่านี้
ทั้งนี้เพื่อให้พิมพ์ข้อมูลการแก้ไขข้อบกพร่องโดยใช้ System.err.println()
ได้ และสุดท้ายยังได้ไปยังเอาต์พุตเทอร์มินัลของไคลเอ็นต์ (ซึ่งต่างจากเซิร์ฟเวอร์) ระบบจะตรวจสอบว่าหากกระบวนการสร้างเอาต์พุตไบนารี (เช่น bazel query --output=proto
) ระบบจะไม่ทำการแปลง stdout
ข้อความสั้นๆ (ข้อผิดพลาด คำเตือน และสิ่งอื่นๆ ที่คล้ายกัน) จะแสดงผ่านอินเทอร์เฟซ EventHandler
ที่สำคัญคือข้อความเหล่านี้แตกต่างจากสิ่งที่โพสต์ลงใน EventBus
(ทำให้เกิดความสับสน) Event
แต่ละรายการมี EventKind
(ข้อผิดพลาด คำเตือน ข้อมูล และอื่นๆ อีก 2-3 รายการ) และอาจมี Location
(ตำแหน่งในซอร์สโค้ดที่ทําให้เหตุการณ์เกิดขึ้น)
การติดตั้งใช้งาน EventHandler
บางรายการจะจัดเก็บเหตุการณ์ที่ได้รับ ซึ่งใช้ในการเล่นข้อมูลซ้ำใน UI ที่เกิดจากการประมวลผลที่แคชไว้ประเภทต่างๆ เช่น คำเตือนที่มาจากเป้าหมายที่กำหนดค่าไว้ในแคช
นอกจากนี้ EventHandler
บางแห่งยังอนุญาตให้โพสต์กิจกรรมที่พบกับเส้นทางไปรถบัสของกิจกรรมด้วย (Event
ทั่วไปจะ _ไม่ _ปรากฏที่นี่) เหล่านี้คือการใช้งาน ExtendedEventHandler
และการใช้งานหลักคือเล่นเหตุการณ์ EventBus
ที่แคชไว้ซ้ำ เหตุการณ์ EventBus
ทั้งหมดเหล่านี้ใช้ Postable
แต่ไม่ใช่ทุกรายการที่โพสต์ใน EventBus
จะต้องติดตั้งใช้งานอินเทอร์เฟซนี้ เฉพาะเหตุการณ์ที่แคชไว้โดย ExtendedEventHandler
เท่านั้น (ทั้งนี้ก็น่าจะดีและส่วนใหญ่ก็ทำไม่ได้ แต่ก็ไม่มีการบังคับใช้)
ผลลัพธ์ของเทอร์มินัลจะส่วนใหญ่แสดงผ่าน UiEventHandler
ซึ่งมีหน้าที่รับผิดชอบการจัดรูปแบบผลลัพธ์และรายงานความคืบหน้าที่ดูดีทั้งหมดที่ Bazel ดำเนินการ โดยจะมีอินพุต 2 รายการ ดังนี้
- รถบัสในงาน
- สตรีมเหตุการณ์ที่ส่งผ่านเข้ามาผ่าน Reporter
การเชื่อมต่อโดยตรงเพียงอย่างเดียวที่กลไกการดําเนินการตามคําสั่ง (เช่น Bazel ที่เหลือ) มีกับสตรีม RPC ไปยังไคลเอ็นต์คือผ่าน Reporter.getOutErr()
ซึ่งช่วยให้เข้าถึงสตรีมเหล่านี้ได้โดยตรง โดยจะใช้ได้ก็ต่อเมื่อคำสั่งต้องถ่ายโอนข้อมูลไบนารีจำนวนมาก (เช่น bazel query
)
การทำโปรไฟล์ Bazel
Bazel ทำงานเร็ว Bazel ยังทำงานช้าด้วย เนื่องจากบิลด์มีแนวโน้มที่จะเติบโตจนถึงขอบของสิ่งที่รองรับได้ ด้วยเหตุนี้ Bazel จึงมีเครื่องมือสร้างโปรไฟล์ซึ่งใช้ในการสร้างโปรไฟล์และ Bazel ได้ด้วย และมีการใช้งานในคลาสที่ชื่อ Profiler
อย่างเหมาะสม ฟีเจอร์นี้เปิดอยู่โดยค่าเริ่มต้น แต่จะบันทึกเฉพาะข้อมูลฉบับย่อเพื่อให้โอเวอร์เฮดใช้ได้ บรรทัดคำสั่ง --record_full_profiler_data
จะช่วยบันทึกทุกอย่างที่ทำได้
ซึ่งจะแสดงโปรไฟล์ในรูปแบบเครื่องมือวิเคราะห์โปรไฟล์ของ Chrome ซึ่งดูได้ดีที่สุดใน Chrome รูปแบบข้อมูลของเครื่องมือนี้คือกองงาน ซึ่งผู้ใช้สามารถเริ่มและสิ้นสุดงานได้ และงานต่างๆ ควรซ้อนกันอยู่อย่างเป็นระเบียบ แต่ละเธรด Java จะมีกองงานของตัวเอง TODO: วิธีนี้ทำงานกับรูปแบบการกระทำ และรูปแบบการส่งอย่างต่อเนื่องอย่างไร
ระบบจะเริ่มและหยุดเครื่องมือวิเคราะห์ใน BlazeRuntime.initProfiler()
และ BlazeRuntime.afterCommand()
ตามลำดับ และจะพยายามทำงานเป็นเวลานานที่สุดเพื่อให้เราวิเคราะห์ทุกอย่างได้ หากต้องการเพิ่มบางสิ่งลงในโปรไฟล์
ให้โทรหา Profiler.instance().profile()
โดยจะแสดงผล Closeable
ซึ่งการปิดท้ายนั้นแสดงถึงจุดสิ้นสุดของงาน วิธีใช้ที่ดีที่สุดคือใช้กับคำสั่ง try-with-resources
นอกจากนี้เรายังทำโปรไฟล์หน่วยความจำเบื้องต้นใน MemoryProfiler
ด้วย และยังเปิดอยู่เสมอ
และจะบันทึกขนาดฮีปสูงสุดและลักษณะการทำงานของ GC เป็นส่วนใหญ่
การทดสอบ Bazel
บาเซลมีการทดสอบหลักๆ 2 ประเภท ได้แก่ การทดสอบที่สังเกต Bazel เป็น "กล่องดำ" และการทดสอบที่ดำเนินการเฉพาะช่วงการวิเคราะห์ เราเรียกการทดสอบแบบบูรณาการว่า "การทดสอบแบบบูรณาการ" แบบเดิมและ "การทดสอบหน่วย" แบบหลัง แม้ว่าจะเป็นเหมือนการทดสอบการผสานรวมมากกว่า ซึ่งมีการผสานรวมน้อยกว่า นอกจากนี้ เรายังมีการทดสอบหน่วยจริงบางส่วนในกรณีที่จําเป็น
ของการทดสอบการผสานรวมมี 2 ประเภท ได้แก่
- โซลูชันที่นำไปใช้โดยใช้เฟรมเวิร์กการทดสอบแบบ Bash ที่ละเอียดประณีตภายใต้
src/test/shell
- เบราว์เซอร์ที่ติดตั้งใช้งานใน Java ซึ่งติดตั้งใช้งานเป็นคลาสย่อยของ
BuildIntegrationTestCase
BuildIntegrationTestCase
เป็นเฟรมเวิร์กการทดสอบการผสานรวมที่แนะนำให้ใช้ เนื่องจากเตรียมพร้อมสำหรับสถานการณ์การทดสอบส่วนใหญ่ เนื่องจากเป็นเฟรมเวิร์ก Java จึงสามารถแก้ไขข้อบกพร่องและผสานรวมกับเครื่องมือการพัฒนาทั่วไปได้หลายอย่างอย่างราบรื่น มีตัวอย่างคลาส BuildIntegrationTestCase
มากมายในที่เก็บ Bazel
ใช้การทดสอบการวิเคราะห์เป็นคลาสย่อยของ BuildViewTestCase
มีระบบไฟล์ Scratch ที่ใช้เขียนไฟล์ BUILD
จากนั้นใช้ตัวช่วยต่างๆ เพื่อขอเป้าหมายที่กำหนดค่าไว้ เปลี่ยนการกำหนดค่า และยืนยันหลายๆ อย่างเกี่ยวกับผลการวิเคราะห์