เอกสารนี้เป็นคำอธิบายของโค้ดเบสและโครงสร้างของ Bazel โดยมีไว้สำหรับผู้ที่ต้องการมีส่วนร่วมใน Bazel ไม่ใช่สำหรับผู้ใช้ปลายทาง
บทนำ
โค้ดเบสของ Bazel มีขนาดใหญ่ (โค้ดที่ใช้จริงประมาณ 350,000 บรรทัดและโค้ดทดสอบประมาณ 260,000 บรรทัด) และไม่มีใครคุ้นเคยกับภาพรวมทั้งหมด ทุกคนรู้จักส่วนที่ตนเองรับผิดชอบเป็นอย่างดี แต่มีเพียงไม่กี่คนที่รู้ว่ามีอะไรอยู่บนเนินเขาในทุกทิศทาง
เอกสารนี้พยายามให้ภาพรวมของโค้ดเบสเพื่อให้เริ่มต้นใช้งานได้ง่ายขึ้น เพื่อไม่ให้ผู้ที่อยู่กลางเส้นทางพบว่าตนเองอยู่ในป่ามืดและสูญเสียเส้นทางตรงไปตรงมา
ซอร์สโค้ดเวอร์ชันสาธารณะของ Bazel อยู่ใน GitHub ที่ github.com/bazelbuild/bazel นี่ไม่ใช่ "แหล่งข้อมูลที่เชื่อถือได้" แต่เป็นข้อมูลที่ได้จากโครงสร้างแหล่งข้อมูลภายในของ Google ซึ่งมีฟังก์ชันการทำงานเพิ่มเติมที่ไม่เป็นประโยชน์ภายนอก Google เป้าหมายระยะยาวคือการทำให้ GitHub เป็นแหล่งข้อมูลที่ถูกต้อง
เรายอมรับการมีส่วนร่วมผ่านกลไกคำขอ Pull Request ของ 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++ ("ไคลเอ็นต์") จะได้รับการควบคุม โดยจะตั้งค่ากระบวนการเซิร์ฟเวอร์ที่เหมาะสมโดยใช้
ขั้นตอนต่อไปนี้
- ตรวจสอบว่ามีการแยกตัวเองแล้วหรือไม่ หากไม่เป็นเช่นนั้น ระบบจะดำเนินการดังกล่าว ซึ่งเป็นที่มาของการติดตั้งใช้งานเซิร์ฟเวอร์
- ตรวจสอบว่ามีอินสแตนซ์เซิร์ฟเวอร์ที่ใช้งานอยู่ซึ่งทำงานได้หรือไม่ โดยอินสแตนซ์นั้นต้องทำงาน
มีตัวเลือกการเริ่มต้นที่ถูกต้อง และใช้ไดเรกทอรีพื้นที่ทำงานที่ถูกต้อง โดยจะ
ค้นหาเซิร์ฟเวอร์ที่ทำงานอยู่โดยดูที่ไดเรกทอรี
$OUTPUT_BASE/serverซึ่งมีไฟล์ล็อกที่มีพอร์ตที่เซิร์ฟเวอร์กำลังรับฟังอยู่ - หากจำเป็น ให้หยุดกระบวนการเซิร์ฟเวอร์เก่า
- เริ่มกระบวนการเซิร์ฟเวอร์ใหม่หากจำเป็น
หลังจากที่กระบวนการของเซิร์ฟเวอร์ที่เหมาะสมพร้อมแล้ว ระบบจะสื่อสารคำสั่งที่ต้องเรียกใช้กับเซิร์ฟเวอร์ผ่านอินเทอร์เฟซ gRPC จากนั้นจะส่งเอาต์พุตของ Bazel กลับไปยังเทอร์มินัล คุณจะเรียกใช้คำสั่งได้ครั้งละ 1 รายการเท่านั้น ซึ่ง
ใช้กลไกการล็อกที่ซับซ้อนโดยมีส่วนที่เขียนด้วย C++ และส่วนที่เขียนด้วย
Java มีโครงสร้างพื้นฐานบางอย่างสำหรับการเรียกใช้คำสั่งหลายรายการแบบคู่ขนาน
เนื่องจากความไม่สามารถเรียกใช้ bazel version แบบคู่ขนานกับคำสั่งอื่น
เป็นเรื่องที่น่าอายอยู่บ้าง อุปสรรคหลักคือวงจรการใช้งานของ BlazeModules
และสถานะบางอย่างใน BlazeRuntime
เมื่อสิ้นสุดคำสั่ง เซิร์ฟเวอร์ Bazel จะส่งรหัสออกที่ไคลเอ็นต์ควรส่งคืน
ข้อควรทราบที่น่าสนใจคือการใช้งาน bazel run: คำสั่งนี้มีหน้าที่เรียกใช้สิ่งที่ Bazel เพิ่งสร้าง แต่ทำไม่ได้จากกระบวนการเซิร์ฟเวอร์เนื่องจากไม่มีเทอร์มินัล ดังนั้นจึงจะบอก
ไคลเอ็นต์ว่าควรเรียกใช้ไบนารีใดexec()และมีอาร์กิวเมนต์ใดบ้าง
เมื่อกด Ctrl-C ไคลเอ็นต์จะแปลเป็นคำสั่งยกเลิกใน gRPC connection ซึ่งพยายามสิ้นสุดคำสั่งโดยเร็วที่สุด หลังจากกด Ctrl-C ครั้งที่ 3 ไคลเอ็นต์จะส่ง SIGKILL ไปยังเซิร์ฟเวอร์แทน
ซอร์สโค้ดของไคลเอ็นต์อยู่ภายใต้ src/main/cpp และโปรโตคอลที่ใช้ในการ
สื่อสารกับเซิร์ฟเวอร์อยู่ใน src/main/protobuf/command_server.proto
จุดแรกเข้าหลักของเซิร์ฟเวอร์คือ BlazeRuntime.main() และการเรียก gRPC จากไคลเอ็นต์จะได้รับการจัดการโดย GrpcServerImpl.run()
เลย์เอาต์ไดเรกทอรี
Bazel จะสร้างชุดไดเรกทอรีที่ค่อนข้างซับซ้อนในระหว่างการบิลด์ ดูคำอธิบายแบบเต็มได้ในเลย์เอาต์ไดเรกทอรีเอาต์พุต
"ที่เก็บหลัก" คือโครงสร้างแหล่งที่มาที่ Bazel ทำงาน โดยปกติแล้วจะสอดคล้องกับ สิ่งที่คุณเช็คเอาต์จากการควบคุมแหล่งที่มา รูทของไดเรกทอรีนี้เรียกว่า "รูทของพื้นที่ทำงาน"
Bazel จะวางข้อมูลทั้งหมดไว้ใต้ "รูทของผู้ใช้เอาต์พุต" โดยปกติจะเป็น
$HOME/.cache/bazel/_bazel_${USER} แต่สามารถลบล้างได้โดยใช้
--output_user_root ตัวเลือกการเริ่มต้น
"ฐานการติดตั้ง" คือตำแหน่งที่แยก Bazel ซึ่งจะดำเนินการโดยอัตโนมัติ
และ Bazel แต่ละเวอร์ชันจะมีไดเรกทอรีย่อยตามผลรวมตรวจสอบภายใต้
ฐานการติดตั้ง โดยค่าเริ่มต้นจะอยู่ที่ $OUTPUT_USER_ROOT/install และเปลี่ยนได้
โดยใช้ตัวเลือกบรรทัดคำสั่ง --install_base
"เอาต์พุตเบส" คือที่ที่อินสแตนซ์ Bazel ที่แนบมากับพื้นที่ทํางานที่เฉพาะเจาะจง
จะเขียน ฐานเอาต์พุตแต่ละฐานจะมีอินสแตนซ์เซิร์ฟเวอร์ Bazel อย่างน้อย 1 รายการ
ที่ทำงานได้ตลอดเวลา โดยปกติจะอยู่ที่ $OUTPUT_USER_ROOT/<checksum of the path
to the workspace> คุณสามารถเปลี่ยนได้โดยใช้--output_baseตัวเลือกการเริ่มต้น
ซึ่งมีประโยชน์ในการหลีกเลี่ยงข้อจำกัดที่ว่ามีอินสแตนซ์ Bazel ได้เพียง
อินสแตนซ์เดียวที่ทำงานในเวิร์กสเปซใดก็ได้ในเวลาใดก็ตาม
ไดเรกทอรีเอาต์พุตมีข้อมูลต่อไปนี้
- ที่เก็บภายนอกที่ดึงข้อมูลมาที่
$OUTPUT_BASE/external - รูทของไฟล์ที่เรียกใช้ ซึ่งเป็นไดเรกทอรีที่มีลิงก์สัญลักษณ์ไปยังซอร์สโค้ดทั้งหมด
สำหรับการสร้างปัจจุบัน ตั้งอยู่ที่
$OUTPUT_BASE/execrootในระหว่าง การบิลด์ ไดเรกทอรีการทำงานคือ$EXECROOT/<name of main repository>เราวางแผนที่จะเปลี่ยนเป็น$EXECROOTแต่เป็นแผนระยะยาวเนื่องจากเป็นการเปลี่ยนแปลงที่ไม่เข้ากัน - ไฟล์ที่สร้างขึ้นระหว่างการสร้าง
กระบวนการดำเนินการคำสั่ง
เมื่อเซิร์ฟเวอร์ Bazel ได้รับการควบคุมและได้รับแจ้งเกี่ยวกับคำสั่งที่ต้อง เรียกใช้ ลำดับเหตุการณ์ต่อไปนี้จะเกิดขึ้น
BlazeCommandDispatcherจะได้รับแจ้งเกี่ยวกับคำขอใหม่ โดยจะตัดสิน ว่าคำสั่งต้องใช้พื้นที่ทำงานในการเรียกใช้หรือไม่ (เกือบทุกคำสั่งยกเว้น คำสั่งที่ไม่มีส่วนเกี่ยวข้องกับซอร์สโค้ด เช่น version หรือ help) และมีคำสั่งอื่นกำลังทำงานอยู่หรือไม่พบคำสั่งที่ถูกต้อง แต่ละคำสั่งต้องใช้ส่วนติดต่อ
BlazeCommandและต้องมีคำอธิบายประกอบ@Command(นี่เป็นรูปแบบที่ไม่พึงประสงค์เล็กน้อย หากข้อมูลเมตาทั้งหมดที่คำสั่งต้องการอธิบายด้วยเมธอดในBlazeCommandจะดีมาก)ระบบจะแยกวิเคราะห์ตัวเลือกบรรทัดคำสั่ง แต่ละคำสั่งมีตัวเลือกบรรทัดคำสั่งที่แตกต่างกัน ซึ่งอธิบายไว้ในคำอธิบายประกอบ
@Commandระบบจะสร้าง Event Bus Event Bus เป็นสตรีมสำหรับเหตุการณ์ที่เกิดขึ้น ระหว่างการสร้าง ระบบจะส่งออกบางส่วนเหล่านี้ไปยังภายนอก Bazel ภายใต้ การดูแลของ 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()
ถือว่าใช้ได้)
Bazel จะเรียนรู้เกี่ยวกับคลาสตัวเลือกด้วยวิธีต่อไปนี้
- บางอย่างจะฝังอยู่ใน Bazel (
CommonCommandOptions) - จากคำอธิบายประกอบ
@Commandในแต่ละคำสั่ง Bazel - จาก
ConfiguredRuleClassProvider(ตัวเลือกบรรทัดคำสั่งที่เกี่ยวข้อง กับภาษาโปรแกรมแต่ละภาษา) - กฎ Starlark ยังกำหนดตัวเลือกของตัวเองได้ด้วย (ดูที่นี่)
ตัวเลือกแต่ละรายการ (ยกเว้นตัวเลือกที่กำหนดโดย Starlark) เป็นตัวแปรสมาชิกของคลาสย่อย FragmentOptions ที่มีคำอธิบายประกอบ @Option ซึ่งระบุชื่อและประเภทของตัวเลือกบรรทัดคำสั่งพร้อมกับข้อความช่วยเหลือบางส่วน
โดยปกติแล้ว ประเภท Java ของค่าของตัวเลือกบรรทัดคำสั่งมักจะเป็นอะไรที่เรียบง่าย
(สตริง จำนวนเต็ม บูลีน ป้ายกำกับ ฯลฯ) อย่างไรก็ตาม เรายังรองรับตัวเลือกประเภทที่ซับซ้อนกว่าด้วย ในกรณีนี้ งานของการแปลงจากสตริงบรรทัดคำสั่งเป็นประเภทข้อมูลจะขึ้นอยู่กับการใช้งานของ com.google.devtools.common.options.Converter
โครงสร้างแบบต้นไม้ต้นฉบับตามที่ Bazel เห็น
Bazel อยู่ในธุรกิจการสร้างซอฟต์แวร์ ซึ่งเกิดขึ้นจากการอ่านและ ตีความซอร์สโค้ด ซอร์สโค้ดทั้งหมดที่ Bazel ดำเนินการเรียกว่า "พื้นที่ทำงาน" และมีโครงสร้างเป็นที่เก็บ แพ็กเกจ และกฎ
ที่เก็บ
"ที่เก็บข้อมูล" คือโครงสร้างแหล่งที่มาที่นักพัฒนาซอฟต์แวร์ใช้ทำงาน ซึ่งมักจะแสดงถึงโปรเจ็กต์เดียว Blaze ซึ่งเป็นรุ่นก่อนหน้าของ Bazel ทำงานใน Monorepo ซึ่งเป็นแผนผังแหล่งที่มาเดียวที่มีซอร์สโค้ดทั้งหมดที่ใช้ในการเรียกใช้บิลด์ ในทางตรงกันข้าม Bazel รองรับโปรเจ็กต์ที่มีซอร์สโค้ดกระจายอยู่ในที่เก็บหลายแห่ง ที่เก็บที่เรียกใช้ Bazel เรียกว่า "ที่เก็บหลัก" ส่วนที่เก็บอื่นๆ เรียกว่า "ที่เก็บภายนอก"
ที่เก็บจะทำเครื่องหมายด้วยไฟล์ขอบเขตของ repo (MODULE.bazel, REPO.bazel หรือในบริบทเดิม WORKSPACE หรือ WORKSPACE.bazel) ในไดเรกทอรีราก
ที่เก็บหลักคือโครงสร้างแหล่งที่มาที่คุณเรียกใช้ Bazel ที่เก็บข้อมูลภายนอก
มีการกำหนดไว้หลายวิธี ดูข้อมูลเพิ่มเติมได้ที่ภาพรวมของทรัพยากร Dependency ภายนอก
โค้ดของที่เก็บภายนอกจะได้รับการลิงก์สัญลักษณ์หรือดาวน์โหลดภายใต้
$OUTPUT_BASE/external
เมื่อเรียกใช้บิลด์ คุณจะต้องต่อโครงสร้างแหล่งที่มาทั้งหมดเข้าด้วยกัน ซึ่งSymlinkForestจะทำหน้าที่นี้โดยการสร้างลิงก์สัญลักษณ์ของทุกแพ็กเกจในที่เก็บหลักไปยัง $EXECROOT และทุกที่เก็บภายนอกไปยัง $EXECROOT/external หรือ $EXECROOT/..
แพ็กเกจ
ที่เก็บทุกแห่งประกอบด้วยแพ็กเกจ ซึ่งเป็นคอลเล็กชันของไฟล์ที่เกี่ยวข้องและ
ข้อกำหนดของทรัพยากร Dependency ซึ่งระบุโดยไฟล์ที่ชื่อ BUILD หรือ BUILD.bazel หากมีทั้ง 2 ไฟล์ Bazel จะเลือกใช้ BUILD.bazel เหตุผล
ที่ยังรับไฟล์ BUILD อยู่ก็คือ Blaze ซึ่งเป็นรุ่นก่อนหน้าของ Bazel ใช้ชื่อไฟล์นี้
อย่างไรก็ตาม ปรากฏว่าส่วนเส้นทางนี้เป็นส่วนที่ใช้กันทั่วไป โดยเฉพาะ
ใน Windows ซึ่งชื่อไฟล์จะไม่คำนึงถึงตัวพิมพ์เล็กและตัวพิมพ์ใหญ่
แพ็กเกจแต่ละรายการจะแยกจากกัน การเปลี่ยนแปลงไฟล์ BUILD ของแพ็กเกจ
จะไม่ทำให้แพ็กเกจอื่นๆ เปลี่ยนแปลง การเพิ่มหรือนำไฟล์ BUILD ออก
_อาจ_เปลี่ยนแพ็กเกจอื่นๆ เนื่องจาก Glob แบบเรียกซ้ำจะหยุดที่ขอบเขตของแพ็กเกจ
และดังนั้นการมีไฟล์ BUILD จะหยุดการเรียกซ้ำ
การประเมินไฟล์ BUILD เรียกว่า "การโหลดแพ็กเกจ" โดยจะมีการติดตั้งใช้งาน
ในคลาส PackageFactory ซึ่งทำงานโดยการเรียกใช้ตัวแปล Starlark และ
ต้องมีความรู้เกี่ยวกับชุดคลาสของกฎที่มีอยู่ ผลลัพธ์ของการโหลดแพ็กเกจ
คือออบเจ็กต์ Package ส่วนใหญ่จะเป็นการแมปจากสตริง (ชื่อของเป้าหมาย) ไปยังเป้าหมายเอง
ความซับซ้อนส่วนใหญ่ในระหว่างการโหลดแพ็กเกจคือ globbing: Bazel ไม่ได้กำหนดให้ต้องแสดงไฟล์แหล่งที่มาทุกไฟล์อย่างชัดเจน แต่สามารถเรียกใช้ glob (เช่น glob(["**/*.java"])) ได้ ซึ่งต่างจากเชลล์ที่รองรับ glob แบบเรียกซ้ำที่ลงไปในไดเรกทอรีย่อย (แต่ไม่ใช่ในแพ็กเกจย่อย) ซึ่งต้องมีสิทธิ์เข้าถึงระบบไฟล์ และเนื่องจากอาจทำงานช้า เราจึงใช้เทคนิคต่างๆ เพื่อให้ทำงานแบบคู่ขนานและมีประสิทธิภาพมากที่สุด
การใช้ Globbing จะดำเนินการในคลาสต่อไปนี้
LegacyGlobberซึ่งเป็นโปรแกรมรวบรวมข้อมูลที่รวดเร็วและไม่รู้จัก SkyframeSkyframeHybridGlobberซึ่งเป็นเวอร์ชันที่ใช้ Skyframe และกลับไปใช้ Globber เวอร์ชันเดิมเพื่อหลีกเลี่ยง "การรีสตาร์ท Skyframe" (อธิบายไว้ด้านล่าง)
Package คลาสนี้มีสมาชิกบางรายที่ใช้เพื่อแยกวิเคราะห์แพ็กเกจ "ภายนอก" (ที่เกี่ยวข้องกับทรัพยากรภายนอก) โดยเฉพาะ และสมาชิกเหล่านั้นไม่เหมาะกับแพ็กเกจจริง นี่เป็นข้อบกพร่องในการออกแบบเนื่องจากออบเจ็กต์ที่อธิบายแพ็กเกจปกติไม่ควรมีฟิลด์ที่อธิบายสิ่งอื่น ซึ่งได้แก่
- การแมปที่เก็บ
- เครื่องมือที่ลงทะเบียน
- แพลตฟอร์มการดำเนินการที่ลงทะเบียน
ในอุดมคติแล้ว ควรมีการแยกการแยกวิเคราะห์แพ็กเกจ "ภายนอก" ออกจากการแยกวิเคราะห์แพ็กเกจปกติ เพื่อให้ Package ไม่ต้องรองรับความต้องการของทั้ง 2 อย่าง แต่การดำเนินการนี้ทำได้ยากเนื่องจากทั้ง 2 อย่างมีความเชื่อมโยงกันอย่างลึกซึ้ง
ป้ายกำกับ เป้าหมาย และกฎ
แพ็กเกจประกอบด้วยเป้าหมายซึ่งมีประเภทต่อไปนี้
- ไฟล์: สิ่งที่เป็นอินพุตหรือเอาต์พุตของการสร้าง ใน ภาษา Bazel เราเรียกสิ่งเหล่านี้ว่าอาร์ติแฟกต์ (กล่าวถึงในที่อื่น) ไฟล์ที่สร้างขึ้นระหว่างการบิลด์ไม่ได้เป็นเป้าหมายเสมอไป โดยปกติแล้วเอาต์พุตของ 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() ในขณะที่ Bazel "รู้จัก" คลาสกฎ Java โดยอัตโนมัติ
เนื่องจากมีการลงทะเบียนกับ ConfiguredRuleClassProvider
คลาสกฎประกอบด้วยข้อมูลต่อไปนี้
- แอตทริบิวต์ (เช่น
srcs,deps): ประเภท ค่าเริ่มต้น ข้อจำกัด ฯลฯ - การเปลี่ยนการกำหนดค่าและลักษณะที่แนบกับแต่ละแอตทริบิวต์ (หากมี)
- การใช้กฎ
- ผู้ให้บริการข้อมูลแบบทรานซิทีฟที่กฎ "มักจะ" สร้าง
หมายเหตุเกี่ยวกับคำศัพท์: ในโค้ดเบส เรามักใช้คำว่า "กฎ" เพื่อหมายถึงเป้าหมาย
ที่สร้างโดยคลาสกฎ แต่ใน Starlark และในเอกสารประกอบที่ผู้ใช้มองเห็น
ควรใช้ "Rule" เพื่ออ้างอิงถึงคลาสของกฎเท่านั้น ส่วนเป้าหมาย
ก็เป็นเพียง "เป้าหมาย" นอกจากนี้ โปรดทราบว่าแม้ว่า RuleClass จะมี "class" อยู่ในชื่อ แต่ไม่มีความสัมพันธ์แบบการสืบทอดของ Java ระหว่างคลาสของกฎกับเป้าหมายประเภทนั้น
Skyframe
กรอบการประเมินที่อยู่เบื้องหลัง Bazel เรียกว่า Skyframe โมเดลของมันคือ ทุกอย่างที่ต้องสร้างในระหว่างการสร้างจะจัดระเบียบเป็นกราฟแบบมีทิศทางแบบไม่มีวงจร โดยมีขอบที่ชี้จากข้อมูลใดๆ ไปยังการขึ้นต่อกัน นั่นคือ ข้อมูลอื่นๆ ที่ต้องทราบเพื่อสร้าง
โหนดในกราฟเรียกว่า SkyValue และชื่อของโหนดเรียกว่า SkyKey ทั้ง 2 อย่างนี้เปลี่ยนแปลงไม่ได้โดยสิ้นเชิง และควรเข้าถึงได้จากออบเจ็กต์ที่เปลี่ยนแปลงไม่ได้เท่านั้น โดยปกติแล้วค่าคงที่นี้จะใช้ได้เสมอ และในกรณีที่ใช้ไม่ได้ (เช่น สำหรับคลาสตัวเลือกแต่ละรายการ BuildOptions ซึ่งเป็นสมาชิกของ BuildConfigurationValue และ SkyKey) เราจะพยายามอย่างเต็มที่ที่จะไม่เปลี่ยนแปลงหรือเปลี่ยนแปลงในลักษณะที่สังเกตจากภายนอกไม่ได้เท่านั้น
ดังนั้น ทุกอย่างที่คำนวณภายใน Skyframe (เช่น เป้าหมายที่กำหนดค่า) จะต้องเปลี่ยนแปลงไม่ได้ด้วย
วิธีที่สะดวกที่สุดในการสังเกตกราฟ Skyframe คือการเรียกใช้ bazel dump
--skyframe=deps ซึ่งจะทิ้งกราฟ 1 SkyValue ต่อบรรทัด เราขอแนะนำ
ให้ทำสำหรับบิลด์ขนาดเล็ก เนื่องจากอาจมีขนาดใหญ่มาก
Skyframe อยู่ในแพ็กเกจ com.google.devtools.build.skyframe แพ็กเกจที่มีชื่อคล้ายกัน com.google.devtools.build.lib.skyframe มีการ
ติดตั้งใช้งาน Bazel บน Skyframe ดูข้อมูลเพิ่มเติมเกี่ยวกับ Skyframe ได้ที่นี่
หากต้องการประเมิน SkyKey เป็น SkyValue Skyframe จะเรียกใช้
SkyFunction ที่สอดคล้องกับประเภทของคีย์ ในระหว่างการประเมินฟังก์ชัน
ฟังก์ชันอาจขอทรัพยากร Dependency อื่นๆ จาก Skyframe โดยการเรียกใช้
การโอเวอร์โหลดต่างๆ ของ SkyFunction.Environment.getValue() ซึ่งมีผลข้างเคียงคือการลงทะเบียนการอ้างอิงเหล่านั้นลงในกราฟภายในของ Skyframe เพื่อให้ Skyframe ทราบว่าต้องประเมินฟังก์ชันอีกครั้งเมื่อมีการเปลี่ยนแปลงการอ้างอิง กล่าวอีกนัยหนึ่งคือ การแคชและการคำนวณที่เพิ่มขึ้นของ Skyframe ทำงานที่
ระดับความละเอียดของ SkyFunction และ SkyValue
เมื่อใดก็ตามที่ SkyFunction ขอทรัพยากร Dependency ที่ไม่พร้อมใช้งาน getValue()
จะแสดงผลเป็น Null จากนั้นฟังก์ชันควรส่งคืนการควบคุมไปยัง Skyframe ด้วยการแสดงผลค่า Null ในภายหลัง Skyframe จะประเมินการอ้างอิงที่ไม่พร้อมใช้งาน แล้วรีสตาร์ทฟังก์ชันตั้งแต่ต้น แต่คราวนี้การเรียกใช้ getValue() จะสำเร็จโดยมีผลลัพธ์ที่ไม่ใช่ค่าว่าง
ผลที่ตามมาคือการคำนวณใดๆ ที่ดำเนินการภายใน SkyFunction
ก่อนการรีสตาร์ทจะต้องทำซ้ำ แต่จะไม่รวมงานที่ทำเพื่อ
ประเมินการขึ้นต่อกันSkyValuesซึ่งแคชไว้ ดังนั้น เราจึงมักจะ
แก้ปัญหานี้ด้วยการทำดังนี้
- ประกาศการขึ้นต่อกันเป็นชุด (โดยใช้
getValuesAndExceptions()) เพื่อ จำกัดจำนวนการรีสตาร์ท - การแบ่ง
SkyValueออกเป็นชิ้นส่วนแยกกันซึ่งคำนวณโดยSkyFunctionต่างๆ เพื่อให้คำนวณและแคชได้อย่างอิสระ คุณควรทำขั้นตอนนี้อย่างมีกลยุทธ์ เนื่องจากอาจเพิ่มการใช้หน่วยความจำได้ - การจัดเก็บสถานะระหว่างการรีสตาร์ท ไม่ว่าจะใช้
SkyFunction.Environment.getState()หรือการแคชแบบคงที่เฉพาะกิจ "เบื้องหลัง Skyframe" เมื่อใช้ SkyFunctions ที่ซับซ้อน การจัดการสถานะ ระหว่างการรีสตาร์ทอาจทำได้ยาก เราจึงได้เปิดตัวStateMachines เพื่อให้มี แนวทางที่มีโครงสร้างสำหรับการทำงานพร้อมกันเชิงตรรกะ รวมถึงฮุกเพื่อระงับและ ดำเนินการต่อในการคำนวณแบบลำดับชั้นภายในSkyFunctionตัวอย่างDependencyResolver#computeDependenciesใช้StateMachineที่มีgetState()เพื่อคำนวณชุดการอ้างอิงโดยตรงที่อาจมีขนาดใหญ่มาก ของเป้าหมายที่กำหนดค่าไว้ ซึ่งอาจส่งผลให้ ต้องรีสตาร์ทที่มีค่าใช้จ่ายสูง
โดยพื้นฐานแล้ว Bazel จำเป็นต้องมีวิธีแก้ปัญหาประเภทนี้เนื่องจากมีโหนด Skyframe ที่กำลังทำงานอยู่หลายแสนโหนดเป็นเรื่องปกติ และการรองรับเธรดน้ำหนักเบาของ Java ไม่ได้มีประสิทธิภาพสูงกว่าการใช้งาน StateMachine ณ ปี 2023
Starlark
Starlark เป็นภาษาเฉพาะของโดเมนที่ผู้คนใช้เพื่อกำหนดค่าและขยาย Bazel โดยมีแนวคิดเป็นส่วนย่อยของ Python ที่ถูกจำกัดซึ่งมีประเภทน้อยกว่ามาก ข้อจำกัดเพิ่มเติมเกี่ยวกับการควบคุมโฟลว์ และที่สำคัญที่สุดคือการรับประกันความไม่เปลี่ยนแปลงที่เข้มงวด เพื่อเปิดใช้การอ่านพร้อมกัน ภาษาดังกล่าวไม่สมบูรณ์แบบตามทฤษฎีของทัวริง ซึ่ง ทำให้ผู้ใช้บางราย (แต่ไม่ใช่ทั้งหมด) ไม่พยายามทำงานด้านการเขียนโปรแกรมทั่วไป ภายในภาษา
Starlark ได้รับการติดตั้งใช้งานในแพ็กเกจ net.starlark.java
นอกจากนี้ยังมีฟีเจอร์การใช้งาน Go แยกต่างหากที่นี่ การใช้งาน Java
ที่ใช้ใน Bazel ปัจจุบันเป็นตัวแปล
Starlark ใช้ในหลายบริบท ได้แก่
- ไฟล์
BUILDส่วนนี้ใช้กำหนดเป้าหมายการบิลด์ใหม่ โค้ด Starlark ที่ทำงานในบริบทนี้จะมีสิทธิ์เข้าถึงเนื้อหาของไฟล์BUILDเองและไฟล์.bzlที่โหลดโดยไฟล์ดังกล่าวเท่านั้น - ไฟล์
MODULE.bazelส่วนนี้ใช้สำหรับกำหนดทรัพยากร Dependency ภายนอก โค้ด Starlark ที่ทำงานในบริบทนี้มีสิทธิ์เข้าถึงแบบจำกัดมาก สำหรับคำสั่งที่กำหนดไว้ล่วงหน้าเพียงไม่กี่รายการ - ไฟล์
.bzlส่วนนี้ใช้กำหนดกฎการบิลด์ใหม่ กฎของ repo และส่วนขยายของโมดูล โค้ด Starlark ที่นี่สามารถกำหนดฟังก์ชันใหม่และโหลดจากไฟล์.bzlอื่นๆ ได้
สำเนียงที่ใช้ได้สำหรับไฟล์ BUILD และ .bzl จะแตกต่างกันเล็กน้อย
เนื่องจากแสดงถึงสิ่งต่างๆ ดูรายการความแตกต่างได้ที่นี่
ดูข้อมูลเพิ่มเติมเกี่ยวกับ Starlark ได้ที่นี่
ระยะการโหลด/วิเคราะห์
ระยะการโหลด/การวิเคราะห์คือระยะที่ Bazel จะพิจารณาว่าต้องดำเนินการใดบ้างเพื่อ สร้างกฎที่เฉพาะเจาะจง หน่วยพื้นฐานคือ "เป้าหมายที่กำหนดค่า" ซึ่งเป็นคู่ (เป้าหมาย, การกำหนดค่า)
เราเรียกขั้นตอนนี้ว่า "ระยะการโหลด/การวิเคราะห์" เนื่องจากแบ่งออกเป็น 2 ส่วนที่แตกต่างกันได้ ซึ่งก่อนหน้านี้จะดำเนินการตามลำดับ แต่ตอนนี้สามารถทับซ้อนกันได้
- การโหลดแพ็กเกจ ซึ่งก็คือการเปลี่ยนไฟล์
BUILDเป็นออบเจ็กต์Packageที่แสดงถึงแพ็กเกจเหล่านั้น - การวิเคราะห์เป้าหมายที่กำหนดค่าไว้ ซึ่งก็คือการเรียกใช้การติดตั้งใช้งานของ กฎเพื่อสร้างกราฟการดำเนินการ
เป้าหมายที่กำหนดค่าแต่ละรายการใน Closure แบบทรานซิทีฟของเป้าหมายที่กำหนดค่า ซึ่งขอในบรรทัดคำสั่งต้องได้รับการวิเคราะห์จากล่างขึ้นบน กล่าวคือ โหนดใบ ก่อน จากนั้นจึงขึ้นไปยังโหนดในบรรทัดคำสั่ง ข้อมูลที่ใช้ในการวิเคราะห์ เป้าหมายเดียวที่กำหนดค่าไว้มีดังนี้
- การกำหนดค่า ("วิธี" สร้างกฎนั้น เช่น แพลตฟอร์มเป้าหมาย แต่รวมถึงสิ่งต่างๆ เช่น ตัวเลือกบรรทัดคำสั่งที่ผู้ใช้ต้องการ ส่งไปยังคอมไพเลอร์ C++)
- การขึ้นต่อกันโดยตรง ผู้ให้บริการข้อมูลแบบทรานซิทีฟของกฎที่กำลังวิเคราะห์ ที่เรียกเช่นนี้เนื่องจากจะให้ "สรุป" ข้อมูลใน Closure แบบทรานซิทีฟของเป้าหมายที่กำหนดค่าไว้ เช่น ไฟล์ .jar ทั้งหมดใน Classpath หรือไฟล์ .o ทั้งหมดที่ต้องลิงก์กับไบนารี C++)
- เป้าหมายเอง นี่คือผลลัพธ์ของการโหลดแพ็กเกจที่เป้าหมาย อยู่ สำหรับกฎ จะรวมถึงแอตทริบิวต์ของกฎ ซึ่งมักจะเป็นสิ่งที่สำคัญ
- การติดตั้งใช้งานเป้าหมายที่กำหนดค่าไว้ สำหรับกฎ จะอยู่ใน Starlark หรือ Java ก็ได้ เป้าหมายทั้งหมดที่ไม่ได้กำหนดค่ากฎจะได้รับการติดตั้งใช้งาน ใน Java
เอาต์พุตของการวิเคราะห์เป้าหมายที่กำหนดค่าไว้มีดังนี้
- ผู้ให้บริการข้อมูลแบบทรานซิทีฟที่กำหนดค่าเป้าหมายซึ่งขึ้นอยู่กับผู้ให้บริการดังกล่าวจะ เข้าถึง
- อาร์ติแฟกต์ที่สร้างได้และการดำเนินการที่สร้างอาร์ติแฟกต์
API ที่มีให้สำหรับกฎ Java คือ RuleContext ซึ่งเทียบเท่ากับอาร์กิวเมนต์ ctx ของกฎ Starlark API ของมันมีประสิทธิภาพมากขึ้น แต่ในขณะเดียวกันก็ทำสิ่งที่ไม่ดี™ ได้ง่ายขึ้นด้วย เช่น การเขียนโค้ดที่มีความซับซ้อนด้านเวลาหรือพื้นที่เป็นกำลังสอง (หรือแย่กว่านั้น) การทำให้เซิร์ฟเวอร์ Bazel ขัดข้องด้วยข้อยกเว้นของ Java หรือการละเมิดค่าคงที่ (เช่น โดยการแก้ไขอินสแตนซ์ Options โดยไม่ตั้งใจ หรือโดยการทำให้เป้าหมายที่กำหนดค่าแล้วเปลี่ยนแปลงได้)
อัลกอริทึมที่กำหนดทรัพยากร Dependency โดยตรงของเป้าหมายที่กำหนดค่า
อยู่ใน DependencyResolver.dependentNodeMap()
การกำหนดค่า
การกำหนดค่าคือ "วิธี" สร้างเป้าหมาย: สำหรับแพลตฟอร์มใด มีตัวเลือกบรรทัดคำสั่งใด ฯลฯ
คุณสร้างเป้าหมายเดียวกันสำหรับการกำหนดค่าหลายรายการในการสร้างเดียวกันได้ ซึ่งจะมีประโยชน์ เช่น เมื่อใช้โค้ดเดียวกันสำหรับเครื่องมือที่ทำงานระหว่างบิลด์และสำหรับโค้ดเป้าหมาย และเรากำลังคอมไพล์ข้าม หรือเมื่อเรากำลังสร้างแอป Android แบบ Fat (แอปที่มีโค้ดแบบเนทีฟสำหรับสถาปัตยกรรม CPU หลายรายการ)
ในเชิงแนวคิด การกำหนดค่าคือBuildOptionsอินสแตนซ์ อย่างไรก็ตาม ในทางปฏิบัติ BuildOptions จะห่อหุ้มด้วย BuildConfiguration ซึ่งมี
ฟังก์ชันการทำงานอื่นๆ เพิ่มเติม โดยจะแพร่กระจายจากด้านบนของกราฟการขึ้นต่อกันไปยังด้านล่าง หากมีการเปลี่ยนแปลง คุณจะต้องวิเคราะห์บิลด์อีกครั้ง
ซึ่งส่งผลให้เกิดความผิดปกติ เช่น ต้องวิเคราะห์ทั้งบิลด์อีกครั้งหากจำนวนการทดสอบที่ขอเปลี่ยนแปลง แม้ว่าการเปลี่ยนแปลงนั้นจะส่งผลต่อเป้าหมายการทดสอบเท่านั้น (เรามีแผนที่จะ "ตัด" การกำหนดค่าเพื่อไม่ให้เกิดกรณีนี้ แต่ยังไม่พร้อมใช้งาน)
เมื่อการใช้งานกฎต้องใช้ส่วนหนึ่งของการกำหนดค่า ก็จะต้องประกาศ
ในคำจำกัดความโดยใช้ RuleClass.Builder.requiresConfigurationFragments()
ทั้งนี้เพื่อหลีกเลี่ยงข้อผิดพลาด (เช่น กฎ Python ที่ใช้ส่วน Java) และเพื่ออำนวยความสะดวกในการตัดแต่งการกำหนดค่า เพื่อให้หากตัวเลือก Python เปลี่ยนไป เป้าหมาย C++ ไม่จำเป็นต้องได้รับการวิเคราะห์ซ้ำ
การกำหนดค่าของกฎไม่จำเป็นต้องเหมือนกับกฎ "หลัก" กระบวนการเปลี่ยนการกำหนดค่าในขอบเขตการพึ่งพาเรียกว่า "การเปลี่ยนการกำหนดค่า" ซึ่งอาจเกิดขึ้นได้ 2 แห่ง ดังนี้
- ที่ขอบทรัพยากร Dependency การเปลี่ยนสถานะเหล่านี้ระบุไว้ใน
Attribute.Builder.cfg()และเป็นฟังก์ชันจากRule(ที่เกิดการเปลี่ยนสถานะ) และBuildOptions(การกำหนดค่าเดิม) ไปยังBuildOptionsอย่างน้อย 1 รายการ (การกำหนดค่าเอาต์พุต) - ที่ขอบขาเข้าใดๆ ไปยังเป้าหมายที่กำหนดค่าไว้ ซึ่งระบุไว้ใน
RuleClass.Builder.cfg()
ชั้นเรียนที่เกี่ยวข้องคือ TransitionFactory และ ConfigurationTransition
ตัวอย่างการใช้การเปลี่ยนการกำหนดค่า
- เพื่อประกาศว่ามีการใช้การขึ้นต่อกันที่เฉพาะเจาะจงในระหว่างการบิลด์ และควรบิลด์การขึ้นต่อกันดังกล่าวในสถาปัตยกรรมการดำเนินการ
- หากต้องการประกาศว่าต้องสร้างการขึ้นต่อกันที่เฉพาะเจาะจงสำหรับสถาปัตยกรรมหลายรายการ (เช่น สำหรับโค้ดเนทีฟใน APK แบบ Fat ของ Android)
หากการเปลี่ยนการกำหนดค่าส่งผลให้มีการกำหนดค่าหลายรายการ เราจะเรียกว่าการเปลี่ยนแบบแยก
นอกจากนี้ คุณยังใช้การเปลี่ยนการกำหนดค่าใน Starlark ได้ด้วย (เอกสารประกอบที่นี่)
ผู้ให้บริการข้อมูลแบบทรานซิทีฟ
ผู้ให้บริการข้อมูลแบบทรานซิทีฟเป็นวิธี (และเป็นวิธีเดียว) ที่เป้าหมายที่กำหนดค่า จะเรียนรู้สิ่งต่างๆ เกี่ยวกับเป้าหมายอื่นๆ ที่กำหนดค่าซึ่งเป้าหมายนั้นๆ ขึ้นอยู่กับ และเป็นวิธีเดียว ในการบอกสิ่งต่างๆ เกี่ยวกับตัวเองแก่เป้าหมายอื่นๆ ที่กำหนดค่าซึ่งขึ้นอยู่กับ เป้าหมายนั้นๆ เหตุผลที่ชื่อมีคำว่า "transitive" ก็คือโดยปกติแล้วจะเป็นการสรุปการปิดแบบทรานซิทีฟของเป้าหมายที่กำหนดค่าไว้
โดยทั่วไปแล้ว ผู้ให้บริการข้อมูลแบบทรานซิทีฟของ Java จะสอดคล้องกับผู้ให้บริการข้อมูลแบบทรานซิทีฟของ Starlark แบบ 1:1 (ข้อยกเว้นคือ DefaultInfo ซึ่งเป็นการรวมกันของ FileProvider, FilesToRunProvider และ RunfilesProvider เนื่องจาก API นั้นถือว่ามีความเป็น Starlark มากกว่าการทับศัพท์โดยตรงจาก 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 และใช้
OutputGroupInfoprovider ใน Java
Runfiles
ไบนารีบางรายการต้องใช้ไฟล์ข้อมูลจึงจะทำงานได้ ตัวอย่างที่ชัดเจนคือการทดสอบที่ต้องใช้ ไฟล์อินพุต ซึ่งแสดงใน Bazel ด้วยแนวคิดของ "runfiles" "ทรีไฟล์ที่เรียกใช้" คือโครงสร้างไดเรกทอรีของไฟล์ข้อมูลสำหรับไบนารีหนึ่งๆ โดยจะสร้างในระบบไฟล์เป็นโครงสร้าง symlink ที่มี symlink แต่ละรายการ ซึ่งชี้ไปยังไฟล์ในโครงสร้างแหล่งที่มาหรือเอาต์พุต
ชุดไฟล์ที่เรียกใช้จะแสดงเป็นอินสแตนซ์ Runfiles ในเชิงแนวคิดแล้ว
แมปจากเส้นทางของไฟล์ในโครงสร้างไฟล์ที่สร้างขึ้นระหว่างการทดสอบไปยังอินสแตนซ์ Artifact ที่
แสดงไฟล์นั้น ซึ่งมีความซับซ้อนกว่าการใช้ Map เพียงรายการเดียวด้วย 2 เหตุผลต่อไปนี้
- โดยส่วนใหญ่แล้ว เส้นทางไฟล์ที่รันของไฟล์จะเหมือนกับเส้นทางที่เรียกใช้ เราใช้ฟีเจอร์นี้เพื่อประหยัด RAM
- ในโครงสร้างไฟล์ที่เรียกใช้มีรายการหลายประเภทที่เลิกใช้งานแล้ว ซึ่งต้องแสดงด้วย
ระบบจะรวบรวม Runfiles โดยใช้ RunfilesProvider: อินสแตนซ์ของคลาสนี้
แสดงถึง Runfiles ที่เป้าหมายที่กำหนดค่า (เช่น ไลบรารี) และการปิดทรานซิทีฟ
ต้องใช้ และระบบจะรวบรวม Runfiles เหมือนกับชุดที่ซ้อนกัน (ในความเป็นจริงแล้ว ระบบจะ
ใช้ชุดที่ซ้อนกันในการติดตั้งใช้งานภายใต้การครอบคลุม): แต่ละเป้าหมายจะรวม Runfiles
ของทรัพยากร Dependency เพิ่ม Runfiles ของตัวเองบางส่วน แล้วส่งชุดผลลัพธ์ขึ้นไป
ในกราฟทรัพยากร Dependency RunfilesProviderอินสแตนซ์Runfiles
มี 2 อินสแตนซ์ ได้แก่ อินสแตนซ์หนึ่งสำหรับเมื่อกฎขึ้นอยู่กับแอตทริบิวต์ "data" และ
อีกอินสแตนซ์หนึ่งสำหรับ Dependency ขาเข้าประเภทอื่นๆ ทั้งหมด เนื่องจากเป้าหมาย
บางครั้งจะแสดงไฟล์ที่รันแตกต่างกันเมื่อขึ้นอยู่กับแอตทริบิวต์ข้อมูล
มากกว่าในกรณีอื่นๆ นี่เป็นลักษณะการทำงานเดิมที่ไม่พึงประสงค์ซึ่งเรายังไม่ได้
นำออก
ไฟล์ที่เรียกใช้ของไบนารีจะแสดงเป็นอินสแตนซ์ของ RunfilesSupport ซึ่งแตกต่างจาก Runfiles เนื่องจาก RunfilesSupport มีความสามารถในการสร้างจริง (ต่างจาก Runfiles ซึ่งเป็นเพียงการแมป) ซึ่งต้องมีคอมโพเนนต์เพิ่มเติมต่อไปนี้
- ไฟล์ Manifest ของไฟล์ที่รันอินพุต นี่คือคำอธิบายแบบอนุกรมของ โครงสร้างไฟล์ที่สร้างขึ้นระหว่างการทดสอบ โดยจะใช้เป็นพร็อกซีสำหรับเนื้อหาของทรีไฟล์ที่เรียกใช้ และ Bazel จะถือว่าทรีไฟล์ที่เรียกใช้มีการเปลี่ยนแปลงก็ต่อเมื่อเนื้อหา ของไฟล์ Manifest มีการเปลี่ยนแปลง
- ไฟล์ Manifest ของไฟล์ที่รันได้ของเอาต์พุต ไลบรารีรันไทม์ใช้ตัวแปรนี้เพื่อ จัดการโครงสร้างไฟล์ที่สร้างขึ้นระหว่างการคอมไพล์ โดยเฉพาะใน Windows ซึ่งบางครั้งไม่รองรับ ลิงก์สัญลักษณ์
- ตัวกลางของไฟล์รันไทม์ หากต้องการให้มีโครงสร้าง runfiles คุณต้อง สร้างโครงสร้างลิงก์สัญลักษณ์และอาร์ติแฟกต์ที่ลิงก์สัญลักษณ์ชี้ไป หากต้องการลดจำนวนขอบการขึ้นต่อกัน คุณสามารถใช้ตัวกลางของไฟล์ที่เรียกใช้เพื่อแสดงรายการทั้งหมดนี้ได้
- อาร์กิวเมนต์บรรทัดคำสั่งสำหรับการเรียกใช้ไบนารีที่มีไฟล์ที่เรียกใช้ซึ่งออบเจ็กต์
RunfilesSupportแสดง
ลักษณะ
Aspect เป็นวิธี "เผยแพร่การคำนวณลงในกราฟการขึ้นต่อกัน" โดยจะอธิบายสำหรับผู้ใช้ Bazel
ที่นี่ ตัวอย่างที่กระตุ้นให้เกิดความเข้าใจที่ดีคือ Protocol Buffer ซึ่งproto_library ไม่ควรรู้proto_libraryเกี่ยวกับภาษาใดภาษาหนึ่ง แต่การสร้างการใช้งานข้อความ Protocol Buffer ("หน่วยพื้นฐาน" ของ Protocol Buffer) ในภาษาโปรแกรมใดๆ ควรเชื่อมโยงกับproto_library เพื่อให้หากเป้าหมาย 2 รายการในภาษาเดียวกันขึ้นอยู่กับ Protocol Buffer เดียวกัน ระบบจะสร้างเพียงครั้งเดียว
เช่นเดียวกับเป้าหมายที่กำหนดค่าไว้ เป้าหมายเหล่านี้จะแสดงใน Skyframe เป็น SkyValue
และวิธีสร้างเป้าหมายเหล่านี้จะคล้ายกับวิธีสร้างเป้าหมายที่กำหนดค่าไว้มาก โดยจะมีคลาส Factory ที่เรียกว่า ConfiguredAspectFactory ซึ่งมีสิทธิ์เข้าถึง RuleContext แต่ต่างจาก Factory ของเป้าหมายที่กำหนดค่าไว้ตรงที่ Factory นี้ยังทราบเกี่ยวกับเป้าหมายที่กำหนดค่าไว้ซึ่งเชื่อมต่ออยู่และผู้ให้บริการของเป้าหมายนั้นด้วย
ระบบจะระบุชุดแง่มุมที่ส่งต่อลงในกราฟทรัพยากร Dependency สำหรับแต่ละแอตทริบิวต์โดยใช้ฟังก์ชัน Attribute.Builder.aspects() มีคลาสบางคลาสที่ชื่ออาจทำให้สับสนซึ่งเข้าร่วมในกระบวนการนี้
AspectClassคือการใช้งานแง่มุม โดยอาจอยู่ใน Java (ในกรณีนี้จะเป็นคลาสย่อย) หรือใน Starlark (ในกรณีนี้จะเป็นอินสแตนซ์ของStarlarkAspectClass) ซึ่งคล้ายกับRuleConfiguredTargetFactoryAspectDefinitionคือคำจำกัดความของแง่มุม ซึ่งรวมถึง ผู้ให้บริการที่ต้องใช้ ผู้ให้บริการที่ให้บริการ และมีการอ้างอิงถึง การติดตั้งใช้งาน เช่น อินสแตนซ์AspectClassที่เหมาะสม ซึ่งคล้ายกับRuleClassAspectParametersเป็นวิธีกำหนดพารามิเตอร์ให้กับแง่มุมที่ส่งต่อลงมา ในกราฟทรัพยากร Dependency ปัจจุบันเป็นแผนที่สตริงต่อสตริง ตัวอย่างที่ดี ที่แสดงให้เห็นว่าเหตุใดจึงมีประโยชน์คือ Protocol Buffer หากภาษาหนึ่งมี API หลายรายการ ข้อมูลที่ระบุว่าควรสร้าง Protocol Buffer สำหรับ API ใดควร ส่งต่อลงในกราฟการขึ้นต่อกันAspectแสดงข้อมูลทั้งหมดที่จำเป็นต่อการคำนวณแง่มุมที่ แพร่กระจายลงในกราฟการอ้างอิง โดยประกอบด้วยคลาสลักษณะ คำจำกัดความ และพารามิเตอร์RuleAspectคือฟังก์ชันที่กำหนดว่ากฎหนึ่งๆ ควรกระจายลักษณะใด ซึ่งเป็นฟังก์ชันRule->Aspect
ความซับซ้อนที่ค่อนข้างคาดไม่ถึงคือแง่มุมต่างๆ สามารถเชื่อมโยงกับแง่มุมอื่นๆ ได้
เช่น แง่มุมที่รวบรวม classpath สำหรับ Java IDE อาจ
ต้องการทราบเกี่ยวกับไฟล์ .jar ทั้งหมดใน classpath แต่บางไฟล์เป็น
บัฟเฟอร์โปรโตคอล ในกรณีดังกล่าว ด้าน IDE จะต้องการแนบกับคู่ (proto_library กฎ + ด้าน Java proto)
ความซับซ้อนของแง่มุมต่างๆ จะบันทึกไว้ในคลาส
AspectCollection
แพลตฟอร์มและเชนเครื่องมือ
Bazel รองรับการบิลด์แบบหลายแพลตฟอร์ม ซึ่งเป็นการบิลด์ที่อาจมี สถาปัตยกรรมหลายแบบที่การดำเนินการบิลด์ทำงานอยู่ และสถาปัตยกรรมหลายแบบที่ มีการบิลด์โค้ด สถาปัตยกรรมเหล่านี้เรียกว่าแพลตฟอร์มในภาษา Bazel (เอกสารฉบับเต็มที่นี่)
แพลตฟอร์มจะอธิบายโดยการแมปคีย์-ค่าจากการตั้งค่าข้อจำกัด (เช่น แนวคิดของ "สถาปัตยกรรม CPU") ไปยังค่าข้อจำกัด (เช่น CPU ที่เฉพาะเจาะจง เช่น x86_64) เรามี "พจนานุกรม" ของการตั้งค่าและค่าข้อจำกัดที่ใช้บ่อยที่สุดในที่เก็บ @platforms
แนวคิดของทูลเชนมาจากข้อเท็จจริงที่ว่าคุณอาจต้องใช้คอมไพเลอร์ที่แตกต่างกัน ทั้งนี้ขึ้นอยู่กับแพลตฟอร์มที่ใช้ในการสร้างและแพลตฟอร์มเป้าหมาย ตัวอย่างเช่น ทูลเชน C++ ที่เฉพาะเจาะจงอาจทํางานบนระบบปฏิบัติการที่เฉพาะเจาะจงและสามารถกําหนดเป้าหมายไปยังระบบปฏิบัติการอื่นๆ ได้ Bazel ต้องกำหนดคอมไพเลอร์ C++ ที่ใช้โดยอิงตามแพลตฟอร์มการดำเนินการและเป้าหมายที่ตั้งไว้ (เอกสารประกอบสำหรับ Toolchain ที่นี่)
ในการดำเนินการนี้ เราจะใส่คำอธิบายประกอบ Toolchain ด้วยชุดข้อจำกัดของแพลตฟอร์มการดำเนินการและเป้าหมายที่รองรับ โดยคำจำกัดความของ เครื่องมือจะแบ่งออกเป็น 2 ส่วน ดังนี้
- กฎ
toolchain()ที่อธิบายชุดข้อจำกัดในการดำเนินการและเป้าหมายที่ Toolchain รองรับ และบอกว่า Toolchain เป็นประเภทใด (เช่น C++ หรือ Java) (อย่างหลังแสดงโดยกฎtoolchain_type()) - กฎเฉพาะภาษาที่อธิบายเครื่องมือจริง (เช่น
cc_toolchain())
เราทำเช่นนี้เนื่องจากต้องทราบข้อจำกัดของทุก
เครื่องมือเพื่อทำการแก้ไขเครื่องมือและกฎเฉพาะภาษา
*_toolchain()มีข้อมูลมากกว่านั้นมาก จึงใช้เวลาในการโหลดนานกว่า
แพลตฟอร์มการดำเนินการจะระบุด้วยวิธีใดวิธีหนึ่งต่อไปนี้
- ในไฟล์ MODULE.bazel โดยใช้ฟังก์ชัน
register_execution_platforms() - ในบรรทัดคำสั่งโดยใช้ตัวเลือกบรรทัดคำสั่ง --extra_execution_platforms
ระบบจะคำนวณชุดแพลตฟอร์มการดำเนินการที่พร้อมใช้งานใน
RegisteredExecutionPlatformsFunction
แพลตฟอร์มเป้าหมายสำหรับเป้าหมายที่กำหนดค่าจะกำหนดโดย
PlatformOptions.computeTargetPlatform() รายการนี้เป็นรายการแพลตฟอร์มเนื่องจากเราต้องการรองรับแพลตฟอร์มเป้าหมายหลายรายการในท้ายที่สุด แต่ยังไม่ได้ใช้งาน
ชุดเครื่องมือที่จะใช้สำหรับเป้าหมายที่กำหนดค่าจะกำหนดโดย
ToolchainResolutionFunction โดยขึ้นอยู่กับ
- ชุดเครื่องมือที่ลงทะเบียน (ในไฟล์ MODULE.bazel และ การกำหนดค่า)
- แพลตฟอร์มการดำเนินการและแพลตฟอร์มเป้าหมายที่ต้องการ (ในการกำหนดค่า)
- ชุดประเภทเชนเครื่องมือที่เป้าหมายที่กำหนดค่าไว้ต้องการ (ใน
UnloadedToolchainContextKey) - ชุดข้อจํากัดของแพลตฟอร์มการเรียกใช้ของเป้าหมายที่กําหนดค่า (แอตทริบิวต์
exec_compatible_with) และการกําหนดค่า (--experimental_add_exec_constraints_to_targets) ในUnloadedToolchainContextKey
ผลลัพธ์คือ UnloadedToolchainContext ซึ่งโดยพื้นฐานแล้วคือการแมปจาก
ประเภท Toolchain (แสดงเป็นอินสแตนซ์ ToolchainTypeInfo) ไปยังป้ายกำกับของ
Toolchain ที่เลือก เราเรียกการกำหนดค่านี้ว่า "ไม่ได้โหลด" เนื่องจากไม่มี
ทูลเชนเอง แต่มีเพียงป้ายกำกับของทูลเชน
จากนั้นจะโหลด Toolchain จริงๆ โดยใช้ ResolvedToolchainContext.load()
และใช้โดยการติดตั้งใช้งานเป้าหมายที่กำหนดค่าซึ่งขอ Toolchain
นอกจากนี้ เรายังมีระบบเดิมที่ต้องอาศัยการกำหนดค่า "โฮสต์" เดียว
และการกำหนดค่าเป้าหมายที่แสดงด้วยค่าสถานะการกำหนดค่าต่างๆ เช่น --cpu เรากำลังค่อยๆ เปลี่ยนไปใช้ระบบข้างต้น
เพื่อรองรับกรณีที่ผู้ใช้ต้องพึ่งพาค่าการกำหนดค่าเดิม
เราจึงได้ใช้การแมปแพลตฟอร์ม
เพื่อแปลระหว่าง Flag เดิมกับข้อจำกัดของแพลตฟอร์มรูปแบบใหม่
โค้ดของเครื่องมือนี้อยู่ใน PlatformMappingFunction และใช้ "ภาษาเล็กๆ" ที่ไม่ใช่ Starlark
ข้อจำกัด
บางครั้งคุณอาจต้องการกำหนดเป้าหมายให้เข้ากันได้กับแพลตฟอร์มเพียงไม่กี่แพลตฟอร์ม Bazel มีกลไกหลายอย่าง (น่าเสียดาย) เพื่อให้บรรลุเป้าหมายนี้
- ข้อจำกัดเฉพาะกฎ
environment_group()/environment()- ข้อจำกัดของแพลตฟอร์ม
ข้อจํากัดเฉพาะกฎส่วนใหญ่จะใช้ภายใน Google สําหรับกฎ Java ซึ่งกําลังจะเลิกใช้และไม่มีใน Bazel แต่ซอร์สโค้ดอาจมีการอ้างอิงถึงข้อจํากัดดังกล่าว แอตทริบิวต์ที่ควบคุมการดำเนินการนี้เรียกว่า
constraints=
environment_group() และ environment()
กฎเหล่านี้เป็นกลไกเดิมและไม่ได้ใช้กันอย่างแพร่หลาย
กฎการบิลด์ทั้งหมดสามารถประกาศ "สภาพแวดล้อม" ที่สามารถบิลด์ได้ โดยที่ "สภาพแวดล้อม" คืออินสแตนซ์ของกฎ 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
ข้อจำกัดของแพลตฟอร์ม
ปัจจุบันวิธี "อย่างเป็นทางการ" ในการอธิบายว่าแพลตฟอร์มใดบ้างที่เป้าหมายเข้ากันได้ คือการใช้ข้อจำกัดเดียวกันกับที่ใช้ในการอธิบายทูลเชนและแพลตฟอร์ม โดยมีการนำไปใช้ใน Pull Request #10945
ระดับการแชร์
หากคุณทำงานในโค้ดเบสขนาดใหญ่ที่มีนักพัฒนาซอฟต์แวร์จำนวนมาก (เช่น ที่ Google) คุณ ควรระมัดระวังเพื่อป้องกันไม่ให้คนอื่นๆ ขึ้นอยู่กับโค้ดของคุณ โดยพลการ ไม่เช่นนั้น ตามกฎของไฮรัม ผู้คนจะพึ่งพาพฤติกรรมที่คุณถือว่าเป็นรายละเอียดการใช้งาน
Bazel รองรับสิ่งนี้ด้วยกลไกที่เรียกว่าการมองเห็น ซึ่งคุณสามารถจำกัดเป้าหมายที่ขึ้นอยู่กับเป้าหมายหนึ่งๆ ได้โดยใช้แอตทริบิวต์การมองเห็น แอตทริบิวต์นี้ ค่อนข้างพิเศษเนื่องจากแม้ว่าจะมีรายการป้ายกำกับ แต่ป้ายกำกับเหล่านี้ อาจเข้ารหัสรูปแบบชื่อแพ็กเกจแทนที่จะเป็นตัวชี้ไปยังเป้าหมายใดเป้าหมายหนึ่ง (ใช่ นี่คือข้อบกพร่องในการออกแบบ)
โดยมีการใช้งานในตำแหน่งต่อไปนี้
RuleVisibilityอินเทอร์เฟซแสดงการประกาศระดับการมองเห็น โดยอาจเป็นค่าคงที่ (สาธารณะทั้งหมดหรือส่วนตัวทั้งหมด) หรือรายการป้ายกำกับ- ป้ายกำกับอาจอ้างอิงถึงกลุ่มแพ็กเกจ (รายการแพ็กเกจที่กำหนดไว้ล่วงหน้า) หรืออ้างอิงถึงแพ็กเกจโดยตรง (
//pkg:__pkg__) หรืออ้างอิงถึงแพ็กเกจย่อย (//pkg:__subpackages__) ซึ่งแตกต่างจากไวยากรณ์ของบรรทัดคำสั่งที่ใช้//pkg:*หรือ//pkg/... - กลุ่มแพ็กเกจจะใช้เป็นเป้าหมายของตัวเอง (
PackageGroup) และ เป้าหมายที่กำหนดค่า (PackageGroupConfiguredTarget) เราอาจ แทนที่เป้าหมายเหล่านี้ด้วยกฎง่ายๆ ได้หากต้องการ โดยตรรกะของแอตทริบิวต์เหล่านี้จะได้รับการติดตั้งใช้งาน ด้วยความช่วยเหลือจากPackageSpecificationซึ่งสอดคล้องกับรูปแบบเดียว เช่น//pkg/...PackageGroupContentsซึ่งสอดคล้องกับแอตทริบิวต์packagesของpackage_groupรายการเดียว และPackageSpecificationProviderซึ่งรวบรวมข้อมูลผ่านpackage_groupและincludesแบบทรานซิทีฟ - การแปลงจากรายการป้ายกำกับการมองเห็นเป็น Dependency จะดำเนินการใน
DependencyResolver.visitTargetVisibilityและที่อื่นๆ อีก 2-3 แห่ง - การตรวจสอบจริงจะดำเนินการใน
CommonPrerequisiteValidator.validateDirectPrerequisiteVisibility()
ชุดที่ซ้อนกัน
บ่อยครั้งที่เป้าหมายที่กำหนดค่าจะรวบรวมชุดไฟล์จากทรัพยากร Dependency เพิ่มไฟล์ของตัวเอง และรวมชุดที่รวบรวมไว้ในตัวให้บริการข้อมูลแบบทรานซิทีฟเพื่อให้ เป้าหมายที่กำหนดค่าซึ่งขึ้นอยู่กับเป้าหมายนั้นทำแบบเดียวกันได้ ตัวอย่าง
- ไฟล์ส่วนหัว C++ ที่ใช้สำหรับการสร้าง
- ไฟล์ออบเจ็กต์ที่แสดงการปิดทรานซิทีฟของ
cc_library - ชุดไฟล์ .jar ที่ต้องอยู่ใน classpath เพื่อให้กฎ Java คอมไพล์หรือเรียกใช้ได้
- ชุดไฟล์ Python ในการปิดทรานซิทีฟของกฎ Python
หากเราทำแบบง่ายๆ โดยใช้ List หรือ Set เป็นต้น เราจะลงเอยด้วยการใช้หน่วยความจำแบบกำลังสอง กล่าวคือ หากมีเชนของกฎ N รายการและแต่ละกฎเพิ่มไฟล์ เราจะมีสมาชิกในคอลเล็กชัน 1+2+...+N
เราจึงคิดค้นแนวคิดของ
NestedSetเพื่อหลีกเลี่ยงปัญหานี้ ซึ่งเป็นโครงสร้างข้อมูลที่ประกอบด้วยNestedSet
อินสแตนซ์อื่นๆ และสมาชิกบางส่วนของอินสแตนซ์เอง จึงทำให้เกิดกราฟแบบมีทิศทางและไม่มีวัฏจักร
ของชุด โดยจะเปลี่ยนแปลงไม่ได้และสามารถวนซ้ำสมาชิกได้ เรากำหนด
ลำดับการวนซ้ำหลายรายการ (NestedSet.Order): ลำดับก่อนการประมวลผล ลำดับหลังการประมวลผล ลำดับโทโพโลยี
(โหนดจะอยู่หลังบรรพบุรุษเสมอ) และ "ไม่สนใจ แต่ควรเป็น
ลำดับเดียวกันทุกครั้ง"
โครงสร้างข้อมูลเดียวกันนี้เรียกว่า depset ใน Starlark
อาร์ติแฟกต์และการดำเนินการ
การสร้างจริงประกอบด้วยชุดคำสั่งที่ต้องเรียกใช้เพื่อสร้างเอาต์พุตที่ผู้ใช้ต้องการ
คำสั่งจะแสดงเป็นอินสแตนซ์ของคลาส Action และไฟล์จะแสดงเป็นอินสแตนซ์ของคลาส Artifact โดยจะจัดเรียงในกราฟแบบสองส่วน กราฟแบบมีทิศทาง และกราฟแบบไม่มีวงจรที่เรียกว่า "กราฟการดำเนินการ"
อาร์ติแฟกต์มี 2 ประเภท ได้แก่ อาร์ติแฟกต์ต้นทาง (อาร์ติแฟกต์ที่พร้อมใช้งาน ก่อนที่ Bazel จะเริ่มดำเนินการ) และอาร์ติแฟกต์ที่ได้มา (อาร์ติแฟกต์ที่ต้อง สร้าง) อาร์ติแฟกต์ที่ได้มาอาจมีหลายประเภท ดังนี้
- **อาร์ติแฟกต์ปกติ **ระบบจะตรวจสอบความใหม่ของไฟล์เหล่านี้โดยการคำนวณ ผลรวมตรวจสอบของไฟล์ โดยใช้ mtime เป็นทางลัด เราจะไม่คำนวณผลรวมตรวจสอบของไฟล์หาก ctime ของไฟล์ ไม่มีการเปลี่ยนแปลง
- อาร์ติแฟกต์ของ Symlink ที่ยังไม่ได้รับการแก้ไข โดยจะมีการตรวจสอบความใหม่ของไฟล์เหล่านี้ด้วยการเรียกใช้ readlink() ซึ่งต่างจากอาร์ติแฟกต์ปกติที่ไฟล์เหล่านี้อาจเป็นซิมลิงก์ที่ไม่มีอยู่จริง โดยปกติจะใช้ในกรณีที่ผู้ใช้แพ็กไฟล์บางไฟล์ลงใน ที่เก็บถาวร
- อาร์ติแฟกต์แผนผัง ซึ่งไม่ใช่ไฟล์เดียว แต่เป็นโครงสร้างไดเรกทอรี ระบบจะตรวจสอบความใหม่ของไฟล์โดยการตรวจสอบชุดไฟล์ในนั้นและเนื้อหาของไฟล์ โดยจะแสดงเป็น
TreeArtifact - อาร์ติแฟกต์ข้อมูลเมตาคงที่ การเปลี่ยนแปลงอาร์ติแฟกต์เหล่านี้จะไม่ทริกเกอร์การสร้างใหม่ โดยจะใช้เพื่อข้อมูลการประทับเวลาของบิลด์เท่านั้น เราไม่ต้องการ สร้างบิลด์ใหม่เพียงเพราะเวลาปัจจุบันเปลี่ยนไป
ไม่มีเหตุผลพื้นฐานที่อาร์ติแฟกต์แหล่งที่มาจะเป็นอาร์ติแฟกต์แบบทรีหรืออาร์ติแฟกต์ Symlink ที่ยังไม่ได้รับการแก้ไขไม่ได้ เพียงแต่เรายังไม่ได้ติดตั้งใช้งาน (เราควรทำเช่นนั้น การอ้างอิงไดเรกทอรีแหล่งที่มาในไฟล์ BUILD เป็นหนึ่งในปัญหาความไม่ถูกต้องที่ทราบกันมานานไม่กี่อย่างของ Bazel เรามีการติดตั้งใช้งานที่ใช้งานได้ซึ่งเปิดใช้โดยพร็อพเพอร์ตี้ BAZEL_TRACK_SOURCE_DIRECTORIES=1 JVM)
Artifact ที่สำคัญคือคนกลาง โดยจะระบุด้วย Artifact
อินสแตนซ์ที่เป็นเอาต์พุตของ MiddlemanAction โดยจะใช้ในกรณีพิเศษต่อไปนี้
- ตัวกลางของไฟล์ที่เรียกใช้จะใช้เพื่อให้มั่นใจว่ามีแผนผังไฟล์ที่เรียกใช้ เพื่อไม่ให้ต้องขึ้นอยู่กับไฟล์ Manifest เอาต์พุตและอาร์ติแฟกต์ทุกรายการที่อ้างอิงโดยแผนผังไฟล์ที่เรียกใช้แยกกัน
การดำเนินการควรเข้าใจได้ดีที่สุดในฐานะคำสั่งที่ต้องเรียกใช้ สภาพแวดล้อมที่ต้องการ และชุดเอาต์พุตที่สร้างขึ้น องค์ประกอบหลักของคำอธิบายการดำเนินการมีดังนี้
- บรรทัดคำสั่งที่ต้องเรียกใช้
- อาร์ติแฟกต์อินพุตที่จำเป็น
- ตัวแปรสภาพแวดล้อมที่ต้องตั้งค่า
- คำอธิบายประกอบที่อธิบายสภาพแวดล้อม (เช่น แพลตฟอร์ม) ที่ต้องใช้ในการเรียกใช้ \
นอกจากนี้ ยังมีกรณีพิเศษอื่นๆ อีก 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 สามารถใส่อาร์ติแฟกต์ที่ได้มาไว้ที่ใดก็ได้
เราถือว่านี่เป็นฟีเจอร์ที่ไม่ดี แต่การกำจัดฟีเจอร์นี้ออกไปเป็นเรื่องยากมาก เนื่องจากช่วยประหยัดเวลาในการดำเนินการได้อย่างมาก เช่น เมื่อต้องประมวลผลไฟล์ต้นฉบับ และกฎหลายข้ออ้างอิงไฟล์นั้น (โบกมือไปมา) ซึ่งต้องใช้ 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
เนื่องจากการเรียกใช้การดำเนินการมีค่าใช้จ่ายสูง เราจึงมีแคชหลายเลเยอร์ที่สามารถ เข้าถึงได้เบื้องหลัง Skyframe
ActionExecutionFunction.stateMapมีข้อมูลที่ทำให้การรีสตาร์ท Skyframe ของActionExecutionFunctionมีราคาถูก- แคชการดำเนินการในเครื่องมีข้อมูลเกี่ยวกับสถานะของระบบไฟล์
- โดยปกติแล้วระบบการดำเนินการจากระยะไกลจะมีแคชของตัวเองด้วย
แคชการกระทำเกี่ยวกับสถานที่
แคชนี้เป็นอีกเลเยอร์ที่อยู่เบื้องหลัง Skyframe แม้ว่าจะมีการดำเนินการ อีกครั้งใน Skyframe แต่ก็ยังคงอยู่ในแคชการดำเนินการในเครื่องได้ ซึ่งแสดงถึงสถานะของระบบไฟล์ในเครื่องและจะได้รับการซีเรียลไลซ์ไปยังดิสก์ ซึ่งหมายความว่าเมื่อเริ่มต้นเซิร์ฟเวอร์ Bazel ใหม่ คุณจะได้รับการเข้าชมแคชการดำเนินการในเครื่องแม้ว่ากราฟ Skyframe จะว่างเปล่าก็ตาม
ระบบจะตรวจสอบแคชนี้เพื่อหาการเข้าชมโดยใช้วิธีการ
ActionCacheChecker.getTokenIfNeedToExecute()
แม้จะมีชื่อว่า "ArtifactPath" แต่จริงๆ แล้วคือแผนที่จากเส้นทางของอาร์ติแฟกต์ที่ได้มาไปยัง การดำเนินการที่ปล่อยอาร์ติแฟกต์นั้น การดำเนินการนี้มีคำอธิบายดังนี้
- ชุดไฟล์อินพุตและเอาต์พุตของงาน รวมถึงผลรวมตรวจสอบของไฟล์เหล่านั้น
- "คีย์การดำเนินการ" ซึ่งโดยปกติคือบรรทัดคำสั่งที่ดำเนินการ แต่โดยทั่วไปจะแสดงทุกอย่างที่ไม่ได้บันทึกโดยผลรวมตรวจสอบของไฟล์อินพุต (เช่น สำหรับ
FileWriteActionคือผลรวมตรวจสอบของข้อมูลที่เขียน)
นอกจากนี้ ยังมี "แคชการดำเนินการจากบนลงล่าง" ซึ่งเป็นฟีเจอร์ทดลองขั้นสูงที่ยังอยู่ระหว่างการพัฒนา โดยใช้แฮชแบบทรานซิทีฟเพื่อหลีกเลี่ยงการเข้าถึงแคชหลายครั้ง
การค้นหาอินพุตและการตัดอินพุต
การดำเนินการบางอย่างมีความซับซ้อนมากกว่าการมีชุดอินพุต การเปลี่ยนแปลงชุดอินพุตของการดำเนินการมี 2 รูปแบบ ดังนี้
- การดำเนินการอาจค้นพบอินพุตใหม่ก่อนการดำเนินการ หรืออาจตัดสินใจว่าอินพุตบางอย่างไม่จำเป็นจริงๆ ตัวอย่างที่ชัดเจนคือ C++
ซึ่งควรคาดเดาอย่างรอบคอบว่าไฟล์ส่วนหัวใดที่ไฟล์ C++
ใช้จาก Closure แบบทรานซิทีฟ เพื่อที่เราจะได้ไม่ต้องส่งทุกไฟล์ไปยังเครื่องมือดำเนินการระยะไกล ดังนั้นเราจึงมีตัวเลือกที่จะไม่ลงทะเบียนไฟล์ส่วนหัวทุกไฟล์เป็น "อินพุต" แต่จะสแกนไฟล์ต้นฉบับเพื่อหาไฟล์ส่วนหัวที่รวมแบบทรานซิทีฟ และทำเครื่องหมายเฉพาะไฟล์ส่วนหัวที่ระบุไว้ในคำสั่ง
#includeเป็นอินพุต (เราประเมินค่าสูงเกินไปเพื่อที่จะไม่ต้องใช้ตัวประมวลผลล่วงหน้า C แบบเต็ม) ปัจจุบันตัวเลือกนี้ได้รับการฮาร์ดโค้ดเป็น "false" ใน Bazel และใช้ที่ Google เท่านั้น - การดำเนินการอาจทราบว่าไม่ได้ใช้ไฟล์บางไฟล์ในระหว่างการดำเนินการ ใน C++ เราเรียกไฟล์นี้ว่า "ไฟล์ .d" ซึ่งคอมไพเลอร์จะบอกว่าใช้ไฟล์ส่วนหัวใดหลังจากนั้น และเพื่อหลีกเลี่ยงความอับอายที่การเพิ่มขึ้นแย่กว่า Make Bazel จึงใช้ข้อเท็จจริงนี้ ซึ่งจะให้ค่าประมาณที่ดีกว่าเครื่องมือสแกนรวมเนื่องจากอาศัยคอมไพเลอร์
โดยจะติดตั้งใช้งานโดยใช้วิธีการในการดำเนินการ
Action.discoverInputs()จะถูกเรียกใช้ โดยควรแสดงผลชุดอาร์ติแฟกต์ที่ซ้อนกัน ซึ่งกำหนดให้เป็นอาร์ติแฟกต์ที่จำเป็น ซึ่งต้องเป็นอาร์ติแฟกต์ต้นทาง เพื่อไม่ให้มีขอบการอ้างอิงในกราฟการดำเนินการที่ไม่มี เทียบเท่าในกราฟเป้าหมายที่กำหนดค่าไว้- ระบบจะดำเนินการโดยการเรียกใช้
Action.execute() - เมื่อสิ้นสุด
Action.execute()การดำเนินการจะเรียกAction.updateInputs()เพื่อบอก Bazel ว่าไม่จำเป็นต้องใช้ข้อมูลทั้งหมด ซึ่งอาจส่งผลให้การสร้างแบบเพิ่มไม่ถูกต้องหากมีการรายงานอินพุตที่ใช้ว่าไม่ได้ใช้
เมื่อแคชการดำเนินการแสดงผลการเข้าชมในอินสแตนซ์การดำเนินการใหม่ (เช่น สร้างขึ้น
หลังจากรีสตาร์ทเซิร์ฟเวอร์) Bazel จะเรียกใช้ updateInputs() เองเพื่อให้ชุด
อินพุตแสดงผลลัพธ์ของการค้นหาและการตัดอินพุตที่ทำก่อนหน้านี้
การดำเนินการ Starlark สามารถใช้ฟีเจอร์นี้เพื่อประกาศอินพุตบางอย่างว่าไม่ได้ใช้
โดยใช้unused_inputs_list=อาร์กิวเมนต์ของ
ctx.actions.run()
วิธีต่างๆ ในการเรียกใช้การดำเนินการ: กลยุทธ์/ActionContexts
คุณเรียกใช้การดำเนินการบางอย่างได้หลายวิธี เช่น บรรทัดคำสั่งอาจ
ดำเนินการในเครื่อง ในเครื่องแต่ในแซนด์บ็อกซ์ประเภทต่างๆ หรือจากระยะไกล
แนวคิดที่แสดงให้เห็นถึงเรื่องนี้เรียกว่า ActionContext (หรือ Strategy เนื่องจากเรา
เปลี่ยนชื่อได้แค่ครึ่งทาง...)
วงจรของบริบทการดำเนินการมีดังนี้
- เมื่อเริ่มระยะการดำเนินการ ระบบจะถามอินสแตนซ์
BlazeModuleว่ามีบริบทการดำเนินการใดบ้าง ซึ่งจะเกิดขึ้นในตัวสร้างของExecutionToolประเภทบริบทการดำเนินการจะระบุโดยอินสแตนซ์ JavaClassที่อ้างอิงถึงอินเทอร์เฟซย่อยของActionContextและอินเทอร์เฟซที่บริบทการดำเนินการต้องใช้ - ระบบจะเลือกบริบทการดำเนินการที่เหมาะสมจากบริบทที่มีอยู่และส่งต่อไปยัง
ActionExecutionContextและBlazeExecutor - Actions จะขอบริบทโดยใช้
ActionExecutionContext.getContext()และBlazeExecutor.getStrategy()(จริงๆ แล้วควรมีวิธีเดียวในการทำเช่นนี้…)
กลยุทธ์สามารถเรียกกลยุทธ์อื่นๆ เพื่อทำงานได้โดยไม่มีค่าใช้จ่าย ซึ่งใช้ในกลยุทธ์แบบไดนามิกที่เริ่มการดำเนินการทั้งในเครื่องและจากระยะไกล จากนั้นจะใช้กลยุทธ์ที่เสร็จสิ้นก่อน
กลยุทธ์ที่น่าสังเกตอย่างหนึ่งคือกลยุทธ์ที่ใช้กระบวนการทำงานแบบต่อเนื่อง (WorkerSpawnStrategy) แนวคิดคือเครื่องมือบางอย่างมีเวลาเริ่มต้นนาน ดังนั้นจึงควรนำกลับมาใช้ซ้ำระหว่างการดำเนินการแทนที่จะเริ่มใหม่สำหรับการดำเนินการทุกครั้ง (ซึ่งอาจทำให้เกิดปัญหาความถูกต้อง เนื่องจาก Bazel อาศัยสัญญาของกระบวนการทำงานที่ว่ากระบวนการทำงานดังกล่าวไม่มีสถานะที่สังเกตได้ระหว่างคำขอแต่ละรายการ)
หากมีการเปลี่ยนแปลงเครื่องมือ คุณจะต้องรีสตาร์ทกระบวนการทำงาน ระบบจะพิจารณาว่าสามารถนำ Worker
กลับมาใช้ซ้ำได้หรือไม่โดยการคำนวณผลรวมตรวจสอบสำหรับเครื่องมือที่ใช้โดยใช้
WorkerFilesHash โดยอาศัยการทราบว่าอินพุตใดของการดำเนินการแสดงถึง
ส่วนหนึ่งของเครื่องมือ และอินพุตใดแสดงถึงอินพุต ซึ่งกำหนดโดยผู้สร้าง
ของการดำเนินการ: Spawn.getToolFiles() และไฟล์ที่เรียกใช้ของ Spawn จะ
นับเป็นส่วนหนึ่งของเครื่องมือ
ข้อมูลเพิ่มเติมเกี่ยวกับกลยุทธ์ (หรือบริบทการดำเนินการ)
- ดูข้อมูลเกี่ยวกับกลยุทธ์ต่างๆ ในการเรียกใช้การดำเนินการได้ที่นี่
- ดูข้อมูลเกี่ยวกับกลยุทธ์แบบไดนามิก ซึ่งเราจะดำเนินการทั้งในเครื่องและจากระยะไกลเพื่อดูว่าการดำเนินการใดเสร็จก่อนได้ที่นี่
- ดูข้อมูลเกี่ยวกับรายละเอียดของการดำเนินการในเครื่องได้ที่นี่
เครื่องมือจัดการทรัพยากรในเครื่อง
Bazel สามารถเรียกใช้การดำเนินการหลายอย่างแบบขนานได้ จำนวนการดำเนินการในเครื่องที่ควรเรียกใช้แบบขนานจะแตกต่างกันไปตามการดำเนินการแต่ละอย่าง ยิ่งการดำเนินการต้องใช้ทรัพยากรมากเท่าใด ก็ควรเรียกใช้พร้อมกันน้อยลงเท่านั้นเพื่อหลีกเลี่ยงไม่ให้เครื่องในเครื่องทำงานหนักเกินไป
ซึ่งจะใช้งานในคลาส ResourceManager: การดำเนินการแต่ละอย่างต้อง
มีคำอธิบายประกอบพร้อมค่าประมาณของทรัพยากรในเครื่องที่ต้องใช้ในรูปแบบของอินสแตนซ์ ResourceSet (CPU และ RAM)
จากนั้นเมื่อบริบทการดำเนินการทำสิ่งใดก็ตาม
ที่ต้องใช้ทรัพยากรในเครื่อง บริบทการดำเนินการจะเรียกใช้ ResourceManager.acquireResources()
และจะถูกบล็อกจนกว่าจะมีทรัพยากรที่จำเป็น
ดูคำอธิบายโดยละเอียดเกี่ยวกับการจัดการทรัพยากรในพื้นที่ได้ ที่นี่
โครงสร้างของไดเรกทอรีเอาต์พุต
การดำเนินการแต่ละอย่างต้องมีที่แยกกันในไดเรกทอรีเอาต์พุตเพื่อวางเอาต์พุต โดยปกติแล้ว อาร์ติแฟกต์ที่ได้จะอยู่ในตำแหน่งต่อไปนี้
$EXECROOT/bazel-out/<configuration>/bin/<package>/<artifact name>
ระบบกำหนดชื่อของไดเรกทอรีที่เชื่อมโยงกับการกำหนดค่าหนึ่งๆ อย่างไร คุณสมบัติที่ต้องการซึ่งขัดแย้งกันมี 2 อย่าง ได้แก่
- หากการกำหนดค่า 2 รายการเกิดขึ้นในการสร้างเดียวกัน การกำหนดค่าทั้ง 2 รายการควรมี ไดเรกทอรีที่แตกต่างกันเพื่อให้ทั้ง 2 รายการมีเวอร์ชันของตัวเองสำหรับการดำเนินการเดียวกัน มิฉะนั้น หากการกำหนดค่า 2 รายการไม่เห็นด้วย เช่น บรรทัดคำสั่งของการดำเนินการที่สร้างไฟล์เอาต์พุตเดียวกัน Bazel จะไม่ทราบว่าควรเลือกการดำเนินการใด ("การดำเนินการขัดแย้ง")
- หากการกำหนดค่า 2 รายการแสดงถึงสิ่งเดียวกัน "โดยประมาณ" การกำหนดค่าทั้ง 2 รายการควรมีชื่อเดียวกันเพื่อให้สามารถนำการดำเนินการที่ดำเนินการในการกำหนดค่าหนึ่งไปใช้ซ้ำกับการกำหนดค่าอื่นได้หากบรรทัดคำสั่งตรงกัน เช่น การเปลี่ยนแปลงตัวเลือกบรรทัดคำสั่งไปยังคอมไพเลอร์ Java ไม่ควรส่งผลให้มีการเรียกใช้การดำเนินการคอมไพล์ C++ อีกครั้ง
จนถึงตอนนี้ เรายังไม่พบวิธีแก้ปัญหานี้ตามหลักการ ซึ่ง มีลักษณะคล้ายกับปัญหาการตัดแต่งการกำหนดค่า ดูรายละเอียดเพิ่มเติม เกี่ยวกับตัวเลือกได้ที่นี่ ส่วนที่มีปัญหาหลักๆ คือกฎ Starlark (ซึ่งโดยปกติแล้วผู้เขียนจะไม่คุ้นเคยกับ Bazel อย่างใกล้ชิด) และ Aspect ซึ่งเพิ่มมิติใหม่ให้กับพื้นที่ของสิ่งต่างๆ ที่สามารถสร้างไฟล์เอาต์พุต "เดียวกัน" ได้
แนวทางปัจจุบันคือส่วนเส้นทางสำหรับการกำหนดค่าคือ
<CPU>-<compilation mode> โดยมีการเพิ่มคำต่อท้ายต่างๆ เพื่อให้การกำหนดค่า
การเปลี่ยนสถานะที่ใช้ใน Java ไม่ส่งผลให้เกิดความขัดแย้งในการดำเนินการ นอกจากนี้ ยังมีการเพิ่ม
ผลรวมตรวจสอบของชุดการเปลี่ยนการกำหนดค่า Starlark เพื่อให้ผู้ใช้
ไม่ทำให้เกิดความขัดแย้งในการดำเนินการ ซึ่งยังไม่สมบูรณ์แบบ ซึ่งจะใช้งานใน
OutputDirectories.buildMnemonic() และอาศัยการกำหนดค่าแต่ละส่วน
เพื่อเพิ่มส่วนของตัวเองลงในชื่อของไดเรกทอรีเอาต์พุต
การทดสอบ
Bazel รองรับการเรียกใช้การทดสอบอย่างเต็มที่ โดยรองรับการดำเนินการต่อไปนี้
- การทดสอบจากระยะไกล (หากมีแบ็กเอนด์การดำเนินการระยะไกล)
- การเรียกใช้การทดสอบหลายครั้งพร้อมกัน (เพื่อแก้ไขข้อบกพร่องหรือรวบรวมข้อมูลเวลา ข้อมูล)
- การทดสอบ Sharding (การแยกกรณีทดสอบในการทดสอบเดียวกันผ่านหลายกระบวนการ เพื่อเพิ่มความเร็ว)
- การเรียกใช้การทดสอบที่ไม่น่าเชื่อถืออีกครั้ง
- การจัดกลุ่มการทดสอบเป็นชุดทดสอบ
การทดสอบคือเป้าหมายที่กำหนดค่าตามปกติซึ่งมี TestProvider ที่อธิบาย วิธีเรียกใช้การทดสอบ
- อาร์ติแฟกต์ที่การสร้างทำให้มีการเรียกใช้การทดสอบ นี่คือไฟล์ "cache
status" ที่มีข้อความ
TestResultDataที่แปลงเป็นอนุกรมแล้ว - จำนวนครั้งที่ควรทำการทดสอบ
- จำนวนชาร์ดที่ควรแบ่งการทดสอบออกเป็น
- พารามิเตอร์บางอย่างเกี่ยวกับวิธีเรียกใช้การทดสอบ (เช่น การหมดเวลาทดสอบ)
การพิจารณาว่าจะเรียกใช้การทดสอบใด
การพิจารณาว่าจะเรียกใช้การทดสอบใดเป็นกระบวนการที่ซับซ้อน
ก่อนอื่น ในระหว่างการแยกวิเคราะห์รูปแบบเป้าหมาย ระบบจะขยายชุดการทดสอบแบบเรียกซ้ำ
การขยายนี้จะดำเนินการใน TestsForTargetPatternFunction ข้อควรทราบที่อาจทำให้ประหลาดใจเล็กน้อยคือหากชุดการทดสอบประกาศว่าไม่มีการทดสอบใดๆ ชุดการทดสอบนั้นจะหมายถึงการทดสอบทุกรายการในแพ็กเกจ ซึ่งจะใช้งานใน Package.beforeBuild() โดย
การเพิ่มแอตทริบิวต์โดยนัยที่เรียกว่า $implicit_tests ลงในกฎชุดเครื่องมือทดสอบ
จากนั้นระบบจะกรองการทดสอบตามขนาด แท็ก การหมดเวลา และภาษาตาม
ตัวเลือกบรรทัดคำสั่ง ซึ่งจะใช้งานใน TestFilter และเรียกใช้จาก
TargetPatternPhaseFunction.determineTests() ระหว่างการแยกวิเคราะห์เป้าหมาย และ
ผลลัพธ์จะอยู่ใน TargetPatternPhaseValue.getTestsToRunLabels() สาเหตุที่กำหนดค่าแอตทริบิวต์ของกฎที่กรองได้ไม่ได้เป็นเพราะการดำเนินการนี้เกิดขึ้นก่อนระยะการวิเคราะห์ จึงทำให้ไม่มีการกำหนดค่า
จากนั้นจะมีการประมวลผลเพิ่มเติมใน BuildView.createResult(): ระบบจะกรองเป้าหมายที่วิเคราะห์ไม่สำเร็จออก และแยกการทดสอบเป็นการทดสอบแบบเฉพาะและการทดสอบแบบไม่เฉพาะ จากนั้นจะนำไปใส่ใน AnalysisResult ซึ่งเป็นวิธีที่ ExecutionTool รู้ว่าควรเรียกใช้การทดสอบใด
เพื่อให้กระบวนการที่ซับซ้อนนี้มีความโปร่งใสมากขึ้น เราจึงมีtests()
ตัวดำเนินการคำค้นหา (ใช้งานใน TestsFunction) เพื่อบอกว่าระบบจะเรียกใช้การทดสอบใด
เมื่อมีการระบุเป้าหมายที่เฉพาะเจาะจงในบรรทัดคำสั่ง แต่เราต้องขออภัยที่ต้องแจ้งให้ทราบว่าเราได้
นำมาใช้งานใหม่แล้ว ดังนั้นจึงอาจแตกต่างจากที่กล่าวไว้ข้างต้นใน
หลายๆ จุด
การทดสอบที่ทำงานอยู่
การทดสอบจะทำงานโดยการขออาร์ติแฟกต์สถานะแคช จากนั้น
จะส่งผลให้มีการเรียกใช้ TestRunnerAction ซึ่งจะเรียกใช้
TestActionContext ที่เลือกโดยตัวเลือกบรรทัดคำสั่ง --test_strategy ซึ่ง
เรียกใช้การทดสอบในลักษณะที่ขอ
การทดสอบจะทำงานตามโปรโตคอลที่ซับซ้อนซึ่งใช้ตัวแปรสภาพแวดล้อม เพื่อบอกการทดสอบว่าคาดหวังอะไรจากการทดสอบ ดูคำอธิบายโดยละเอียดเกี่ยวกับสิ่งที่ Bazel คาดหวังจากการทดสอบและสิ่งที่การทดสอบคาดหวังจาก Bazel ได้ที่นี่ โดยพื้นฐานแล้ว รหัสออก 0 หมายถึงสำเร็จ ส่วนรหัสอื่นๆ หมายถึงล้มเหลว
นอกเหนือจากไฟล์สถานะแคชแล้ว กระบวนการทดสอบแต่ละกระบวนการยังสร้างไฟล์อื่นๆ อีกจำนวนหนึ่ง
โดยจะอยู่ใน "ไดเรกทอรีบันทึกการทดสอบ" ซึ่งเป็นไดเรกทอรีย่อยที่ชื่อ
testlogs ของไดเรกทอรีเอาต์พุตของการกำหนดค่าเป้าหมาย
test.xmlไฟล์ XML รูปแบบ JUnit ที่แสดงรายละเอียดของกรณีทดสอบแต่ละรายการใน การทดสอบแบบ Shardtest.logเอาต์พุตคอนโซลของการทดสอบ stdout และ stderr ไม่ได้ แยกกันtest.outputsซึ่งเป็น "ไดเรกทอรีเอาต์พุตที่ไม่ได้ประกาศ" ซึ่งใช้โดยการทดสอบ ที่ต้องการเอาต์พุตไฟล์นอกเหนือจากที่พิมพ์ไปยังเทอร์มินัล
ในระหว่างการทดสอบอาจเกิดสิ่งต่างๆ 2 อย่างที่เกิดขึ้นไม่ได้ในระหว่างการสร้างเป้าหมายปกติ ได้แก่ การทดสอบแบบพิเศษและการสตรีมเอาต์พุต
การทดสอบบางอย่างต้องดำเนินการในโหมดเฉพาะ เช่น ไม่ดำเนินการควบคู่ไปกับการทดสอบอื่นๆ ซึ่งทำได้โดยการเพิ่ม tags=["exclusive"] ลงในกฎการทดสอบหรือเรียกใช้การทดสอบด้วย --test_strategy=exclusive การทดสอบพิเศษแต่ละรายการจะดำเนินการโดยการเรียกใช้ Skyframe แยกต่างหาก ซึ่งจะขอให้ดำเนินการทดสอบหลังจากบิลด์ "หลัก" ซึ่งจะใช้งานได้ใน
SkyframeExecutor.runExclusiveTest()
ซึ่งต่างจากการดำเนินการปกติที่ระบบจะทิ้งเอาต์พุตสุดท้ายเมื่อการดำเนินการเสร็จสิ้น ผู้ใช้สามารถขอให้สตรีมเอาต์พุตของการทดสอบเพื่อให้ทราบความคืบหน้าของการทดสอบที่ใช้เวลานาน โดยระบุด้วย
--test_output=streamed ตัวเลือกบรรทัดคำสั่งและหมายถึงการทดสอบเฉพาะ
การดำเนินการเพื่อให้เอาต์พุตของการทดสอบต่างๆ ไม่ปะปนกัน
ซึ่งจะใช้งานในคลาส StreamedTestOutput ที่มีชื่อเหมาะสมและทำงานโดย
การสำรวจการเปลี่ยนแปลงในไฟล์ test.log ของการทดสอบที่เป็นปัญหาและส่งไบต์ใหม่
ไปยังเทอร์มินัลที่กฎของ Bazel อยู่
ผลการทดสอบที่ดำเนินการจะพร้อมใช้งานใน Event Bus โดยการสังเกตเหตุการณ์ต่างๆ (เช่น TestAttempt, TestResult หรือ TestingCompleteEvent)
ระบบจะส่งผลการทดสอบไปยังโปรโตคอลเหตุการณ์การสร้างและส่งไปยังคอนโซล
โดย AggregatingTestListener
การรวบรวมข้อมูลความครอบคลุม
การครอบคลุมจะรายงานโดยการทดสอบในรูปแบบ LCOV ในไฟล์
bazel-testlogs/$PACKAGE/$TARGET/coverage.dat
หากต้องการรวบรวมความครอบคลุม การดำเนินการทดสอบแต่ละครั้งจะอยู่ในสคริปต์ที่ชื่อ
collect_coverage.sh
สคริปต์นี้จะตั้งค่าสภาพแวดล้อมของการทดสอบเพื่อเปิดใช้การรวบรวมความครอบคลุม และกำหนดว่ารันไทม์ความครอบคลุมจะเขียนไฟล์ความครอบคลุมไว้ที่ใด จากนั้นจะทำการทดสอบ การทดสอบอาจเรียกใช้กระบวนการย่อยหลายรายการและประกอบด้วยส่วนที่เขียนในภาษาโปรแกรมที่แตกต่างกันหลายภาษา (โดยมีรันไทม์การรวบรวมความครอบคลุมแยกกัน) สคริปต์ Wrapper มีหน้าที่แปลง ไฟล์ผลลัพธ์เป็นรูปแบบ LCOV หากจำเป็น และผสานรวมไฟล์เหล่านั้นเป็นไฟล์เดียว
การแทรกของ collect_coverage.sh จะดำเนินการโดยกลยุทธ์การทดสอบ และต้องมี collect_coverage.sh ในอินพุตของการทดสอบ ซึ่งทำได้โดยใช้แอตทริบิวต์โดยนัย :coverage_support ซึ่งจะเปลี่ยนเป็นค่าของแฟล็กการกำหนดค่า --coverage_support (ดูTestConfiguration.TestOptions.coverageSupport)
บางภาษาจะใช้การวัดผลแบบออฟไลน์ ซึ่งหมายความว่าจะมีการเพิ่มการวัดผลความครอบคลุมในเวลาคอมไพล์ (เช่น C++) และบางภาษาจะใช้การวัดผลแบบออนไลน์ ซึ่งหมายความว่าจะมีการเพิ่มการวัดผลความครอบคลุมในเวลาดำเนินการ
แนวคิดหลักอีกอย่างคือความครอบคลุมพื้นฐาน นี่คือความครอบคลุมของไลบรารี
ไบนารี หรือการทดสอบหากไม่มีการเรียกใช้โค้ดในนั้น ปัญหาที่เครื่องมือนี้ช่วยแก้คือหากคุณต้องการคำนวณความครอบคลุมของการทดสอบสำหรับไบนารี การผสานความครอบคลุมของการทดสอบทั้งหมดไม่เพียงพอ เนื่องจากอาจมีโค้ดในไบนารีที่ไม่ได้ลิงก์กับการทดสอบใดๆ ดังนั้น เราจึงปล่อยไฟล์ความครอบคลุมสำหรับไบนารีทุกรายการ
ซึ่งมีเฉพาะไฟล์ที่เราเก็บรวบรวมความครอบคลุมโดยไม่มีบรรทัดที่ครอบคลุม
ไฟล์ความครอบคลุมพื้นฐานสำหรับเป้าหมายอยู่ที่
bazel-testlogs/$PACKAGE/$TARGET/baseline_coverage.dat นอกจากนี้ยังสร้างขึ้น
สำหรับไบนารีและไลบรารีด้วยนอกเหนือจากการทดสอบ หากคุณส่งแฟล็ก --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 ได้โดยการเพิ่มโมดูลลงใน Bazel แต่ละโมดูลต้องเป็นคลาสย่อยของ
BlazeModule (ชื่อนี้เป็นสิ่งที่หลงเหลือจากประวัติของ Bazel เมื่อก่อนที่เคย
เรียกว่า Blaze) และรับข้อมูลเกี่ยวกับเหตุการณ์ต่างๆ ระหว่างการเรียกใช้
คำสั่ง
ส่วนใหญ่จะใช้เพื่อติดตั้งฟังก์ชันการทำงาน "ที่ไม่ใช่หลัก" ต่างๆ ที่ Bazel บางเวอร์ชันเท่านั้นที่ต้องการ (เช่น เวอร์ชันที่เราใช้ที่ Google)
- อินเทอร์เฟซกับระบบการดำเนินการระยะไกล
- คำสั่งใหม่
ชุดจุดขยาย BlazeModule ที่เสนอค่อนข้างไม่เป็นระเบียบ อย่า
ใช้เป็นตัวอย่างหลักการออกแบบที่ดี
Event Bus
วิธีหลักที่ BlazeModules สื่อสารกับส่วนอื่นๆ ของ Bazel คือผ่าน Event Bus
(EventBus) ซึ่งจะสร้างอินสแตนซ์ใหม่สําหรับการสร้างทุกครั้ง โดยส่วนต่างๆ ของ Bazel
สามารถโพสต์เหตุการณ์ไปยัง Event Bus และโมดูลสามารถลงทะเบียน Listener สําหรับเหตุการณ์ที่สนใจได้ ตัวอย่างเช่น ระบบจะแสดงสิ่งต่อไปนี้เป็นเหตุการณ์
- ระบบได้กำหนดรายการเป้าหมายการสร้างที่จะสร้างแล้ว
(
TargetParsingCompleteEvent) - ระบบได้กำหนดการกำหนดค่าระดับบนสุดแล้ว
(
BuildConfigurationEvent) - สร้างเป้าหมายแล้ว ไม่ว่าจะสำเร็จหรือไม่ก็ตาม (
TargetCompleteEvent) - มีการทดสอบ (
TestAttempt,TestSummary)
เหตุการณ์บางอย่างเหล่านี้แสดงอยู่นอก Bazel ใน
โปรโตคอลเหตุการณ์การสร้าง
(ซึ่งเป็นBuildEvent) ซึ่งไม่เพียงช่วยให้ BlazeModules แต่ยังช่วยให้สิ่งต่างๆ
ภายนอกกระบวนการ Bazel สังเกตการสร้างได้ด้วย โดยจะเข้าถึงได้ทั้งในรูปแบบ
ไฟล์ที่มีข้อความโปรโตคอล หรือ Bazel สามารถเชื่อมต่อกับเซิร์ฟเวอร์ (เรียกว่า
บริการเหตุการณ์บิลด์) เพื่อสตรีมเหตุการณ์ได้
ซึ่งจะใช้งานในแพ็กเกจ build.lib.buildeventservice และ
build.lib.buildeventstream ของ Java
ที่เก็บภายนอก
ในขณะที่ Bazel ได้รับการออกแบบมาเพื่อใช้ใน Monorepo (แหล่งที่มาเดียว ของโครงสร้างไดเรกทอรีที่มีทุกอย่างที่จำเป็นต่อการสร้าง) แต่ Bazel ก็อยู่ในโลกที่ สิ่งนี้ไม่จำเป็นต้องเป็นจริงเสมอไป "ที่เก็บภายนอก" เป็นการแยกส่วนที่ใช้เพื่อเชื่อมโยงโลกทั้ง 2 นี้ โดยแสดงถึงโค้ดที่จำเป็นสำหรับการสร้าง แต่ไม่ได้อยู่ในโครงสร้างแหล่งที่มาหลัก
ไฟล์ WORKSPACE
ระบบจะกำหนดชุดที่เก็บข้อมูลภายนอกโดยการแยกวิเคราะห์ไฟล์ WORKSPACE เช่น การประกาศแบบนี้
local_repository(name="foo", path="/foo/bar")
ซึ่งจะทำให้ที่เก็บที่ชื่อ @foo พร้อมใช้งาน ความซับซ้อนของเรื่องนี้คือเราสามารถกำหนดกฎใหม่ของที่เก็บในไฟล์ Starlark ซึ่งจะใช้เพื่อโหลดโค้ด Starlark ใหม่ได้ จากนั้นโค้ด Starlark ใหม่จะใช้เพื่อกำหนดกฎใหม่ของที่เก็บได้ และอื่นๆ
ในการจัดการกรณีนี้ การแยกวิเคราะห์ไฟล์ WORKSPACE (ใน WorkspaceFileFunction) จะแบ่งออกเป็นกลุ่มๆ ที่คั่นด้วยคำสั่ง load()
ดัชนีของกลุ่มจะระบุด้วย WorkspaceFileKey.getIndex() และ
การคำนวณ WorkspaceFileFunction จนถึงดัชนี X หมายถึงการประเมินจนถึง
คำสั่งที่ X load()
การดึงข้อมูลที่เก็บ
ก่อนที่ Bazel จะใช้โค้ดของที่เก็บได้ คุณต้องดึงข้อมูลก่อน ซึ่งจะส่งผลให้ Bazel สร้างไดเรกทอรีภายใต้
$OUTPUT_BASE/external/<repository name>
การดึงข้อมูลที่เก็บจะเกิดขึ้นในขั้นตอนต่อไปนี้
PackageLookupFunctionตระหนักว่าต้องมีที่เก็บและสร้างRepositoryNameเป็นSkyKeyซึ่งเรียกใช้RepositoryLoaderFunctionRepositoryLoaderFunctionจะส่งต่อคำขอไปยังRepositoryDelegatorFunctionโดยไม่ทราบสาเหตุ (โค้ดระบุว่าเพื่อ หลีกเลี่ยงการดาวน์โหลดซ้ำในกรณีที่ Skyframe รีสตาร์ท แต่ก็ไม่ใช่ เหตุผลที่หนักแน่นนัก)RepositoryDelegatorFunctionจะค้นหากฎของที่เก็บที่ได้รับคำขอให้ ดึงข้อมูลโดยการวนซ้ำในก้อนของไฟล์ WORKSPACE จนกว่าจะพบที่เก็บที่ขอ- ระบบจะค้นหา
RepositoryFunctionที่เหมาะสมซึ่งใช้ที่เก็บข้อมูล การดึงข้อมูล โดยอาจเป็นการใช้งานที่เก็บข้อมูล Starlark หรือ แมปที่ฮาร์ดโค้ดสำหรับที่เก็บข้อมูลที่ใช้งานใน Java
การแคชมีหลายเลเยอร์เนื่องจากการดึงข้อมูลที่เก็บอาจมีค่าใช้จ่ายสูงมาก
- มีแคชสำหรับไฟล์ที่ดาวน์โหลดซึ่งมีคีย์ตามการตรวจสอบ (
RepositoryCache) ซึ่งต้องมีการตรวจสอบในไฟล์ WORKSPACE แต่ก็ดีต่อการแยกตัวอยู่แล้ว ซึ่งอินสแตนซ์เซิร์ฟเวอร์ Bazel ทุกรายการในเวิร์กสเตชันเดียวกันจะแชร์ข้อมูลนี้ ไม่ว่าอินสแตนซ์นั้นจะทำงานในพื้นที่ทำงานหรือเอาต์พุตเบสใดก็ตาม - ระบบจะเขียน "ไฟล์เครื่องหมาย" สำหรับแต่ละที่เก็บภายใต้
$OUTPUT_BASE/externalซึ่งมีผลรวมตรวจสอบของกฎที่ใช้ในการดึงข้อมูล หากเซิร์ฟเวอร์ Bazel รีสตาร์ท แต่ผลรวมเช็คซัมไม่เปลี่ยนแปลง ระบบจะไม่ดึงข้อมูลอีกครั้ง ฟีเจอร์นี้ ใช้งานได้ในRepositoryDelegatorFunction.DigestWriter - ตัวเลือกบรรทัดคำสั่ง
--distdirจะกำหนดแคชอื่นที่ใช้ในการ ค้นหาสิ่งประดิษฐ์ที่จะดาวน์โหลด ซึ่งจะมีประโยชน์ในการตั้งค่าระดับองค์กร ที่ Bazel ไม่ควรดึงข้อมูลแบบสุ่มจากอินเทอร์เน็ต ซึ่งDownloadManagerเป็นผู้ดำเนินการ
เมื่อดาวน์โหลดที่เก็บแล้ว ระบบจะถือว่าอาร์ติแฟกต์ในที่เก็บเป็นอาร์ติแฟกต์ต้นทาง
ซึ่งก่อให้เกิดปัญหาเนื่องจากโดยปกติแล้ว Bazel จะตรวจสอบความใหม่ล่าสุด
ของอาร์ติแฟกต์ต้นทางโดยการเรียก stat() ในอาร์ติแฟกต์เหล่านั้น และอาร์ติแฟกต์เหล่านี้จะ
ใช้ไม่ได้ด้วยเมื่อคำจำกัดความของที่เก็บที่อาร์ติแฟกต์อยู่มีการเปลี่ยนแปลง ดังนั้น
FileStateValueสำหรับอาร์ติแฟกต์ในที่เก็บภายนอกจึงต้องขึ้นอยู่กับ
ที่เก็บภายนอกของอาร์ติแฟกต์ ExternalFilesHelper เป็นผู้จัดการเรื่องนี้
การแมปที่เก็บ
อาจมีที่เก็บหลายแห่งที่ต้องการใช้ที่เก็บเดียวกัน
แต่ใช้ในเวอร์ชันที่ต่างกัน (นี่คือตัวอย่างของ "ปัญหาการขึ้นต่อกันแบบไดมอนด์") ตัวอย่างเช่น หากไบนารี 2 รายการในที่เก็บที่แยกกันในการสร้างต้องการขึ้นอยู่กับ Guava ทั้ง 2 รายการจะอ้างอิงถึง Guava ด้วยป้ายกำกับที่ขึ้นต้นด้วย @guava// และคาดหวังว่าป้ายกำกับนั้นจะหมายถึง Guava เวอร์ชันต่างๆ
ดังนั้น Bazel จึงอนุญาตให้แมปป้ายกำกับของที่เก็บภายนอกใหม่เพื่อให้สตริง @guava// อ้างอิงที่เก็บ Guava ที่หนึ่ง (เช่น @guava1//) ในที่เก็บของไบนารีหนึ่ง และที่เก็บ Guava อีกที่เก็บหนึ่ง (เช่น @guava2//) ในที่เก็บของไบนารีอื่น
หรือจะใช้เพื่อเข้าร่วมเพชรก็ได้ หากที่เก็บ
ขึ้นอยู่กับ @guava1// และอีกที่เก็บขึ้นอยู่กับ @guava2// การแมปที่เก็บ
จะช่วยให้คุณแมปที่เก็บทั้ง 2 อีกครั้งเพื่อใช้ที่เก็บ @guava// Canonical ได้
การแมปจะระบุไว้ในไฟล์ WORKSPACE เป็นrepo_mappingแอตทริบิวต์
ของคำจำกัดความของที่เก็บแต่ละรายการ จากนั้นจะปรากฏใน Skyframe ในฐานะสมาชิกของ
WorkspaceFileValue ซึ่งเชื่อมต่อกับ
Package.Builder.repositoryMappingซึ่งใช้เพื่อเปลี่ยนรูปแบบแอตทริบิวต์ที่มีค่าป้ายกำกับ ของกฎในแพ็กเกจโดยRuleClass.populateRuleAttributeValues()Package.repositoryMappingซึ่งใช้ในระยะการวิเคราะห์ (สําหรับการแก้ไขสิ่งต่างๆ เช่น$(location)ซึ่งไม่ได้แยกวิเคราะห์ในระยะการโหลด)BzlLoadFunctionสำหรับการแก้ปัญหาป้ายกำกับในคำสั่ง load()
บิต JNI
เซิร์ฟเวอร์ของ Bazel เขียนด้วยภาษา Java เป็นส่วนใหญ่ ข้อยกเว้นคือส่วนที่ Java ทำเองไม่ได้หรือทำเองไม่ได้เมื่อเรานำไปใช้ ซึ่งส่วนใหญ่จะจำกัดอยู่แค่การโต้ตอบกับระบบไฟล์ การควบคุมกระบวนการ และ สิ่งอื่นๆ ระดับล่างต่างๆ
โค้ด C++ อยู่ภายใต้ src/main/native และคลาส Java ที่มีเมธอดเนทีฟ คือ
NativePosixFilesและNativePosixFileSystemProcessUtilsWindowsFileOperationsและWindowsFileProcessescom.google.devtools.build.lib.platform
เอาต์พุตคอนโซล
การส่งเอาต์พุตของคอนโซลดูเหมือนจะเป็นเรื่องง่าย แต่การทำงานของ หลายกระบวนการ (บางครั้งจากระยะไกล) การแคชแบบละเอียด ความต้องการที่จะ มีเอาต์พุตของเทอร์มินัลที่สวยงามและมีสีสัน และการมีเซิร์ฟเวอร์ที่ทำงานเป็นเวลานานทำให้ เรื่องนี้ไม่ใช่เรื่องง่าย
หลังจากที่การเรียก RPC เข้ามาจากไคลเอ็นต์ ระบบจะสร้างอินสแตนซ์ 2 รายการRpcOutputStream (สําหรับ stdout และ stderr) ซึ่งจะส่งต่อข้อมูลที่พิมพ์ลงใน
อินสแตนซ์เหล่านั้นไปยังไคลเอ็นต์ จากนั้นจะห่อหุ้มด้วย OutErr (คู่ (stdout, stderr)
pair) ทุกอย่างที่ต้องพิมพ์ในคอนโซลจะผ่านสตรีมเหล่านี้ จากนั้นระบบจะส่งสตรีมเหล่านี้ไปยัง
BlazeCommandDispatcher.execExclusively()
โดยค่าเริ่มต้น ระบบจะพิมพ์เอาต์พุตด้วยลำดับการหลีก ANSI เมื่อไม่ต้องการใช้ (--color=no) AnsiStrippingOutputStream จะนำออก นอกจากนี้ ระบบจะเปลี่ยนเส้นทาง System.out และ System.err ไปยังสตรีมเอาต์พุตเหล่านี้
เพื่อให้สามารถพิมพ์ข้อมูลการแก้ไขข้อบกพร่องโดยใช้
System.err.println() และยังคงแสดงในเอาต์พุตเทอร์มินัลของไคลเอ็นต์
(ซึ่งแตกต่างจากของเซิร์ฟเวอร์) เราจะดูแลให้มั่นใจว่าหากกระบวนการ
สร้างเอาต์พุตไบนารี (เช่น bazel query --output=proto) จะไม่มีการดัดแปลง stdout
ข้อความสั้นๆ (ข้อผิดพลาด คำเตือน และอื่นๆ) จะแสดงผ่านEventHandlerอินเทอร์เฟซ โปรดทราบว่าสิ่งเหล่านี้แตกต่างจากสิ่งที่ผู้ใช้โพสต์ใน EventBus (ซึ่งอาจทำให้สับสน) Eventแต่ละรายการมีEventKind (ข้อผิดพลาด คำเตือน ข้อมูล และอื่นๆ อีก 2-3 รายการ) และอาจมีLocation (ตำแหน่งในซอร์สโค้ดที่ทำให้เกิดเหตุการณ์)
การติดตั้งใช้งาน EventHandler บางอย่างจะจัดเก็บเหตุการณ์ที่ได้รับ ซึ่งใช้
เพื่อเล่นข้อมูลซ้ำใน UI ที่เกิดจากการประมวลผลที่แคชไว้หลายประเภท
เช่น คำเตือนที่ปล่อยออกมาจากเป้าหมายที่กำหนดค่าไว้ซึ่งแคชไว้
EventHandler บางรายการยังอนุญาตให้โพสต์กิจกรรมซึ่งจะปรากฏใน
Event Bus (Event ปกติจะ_ไม่_ปรากฏในนั้น) ซึ่งเป็นการติดตั้งใช้งาน ExtendedEventHandler และมีไว้เพื่อเล่นเหตุการณ์ EventBus ที่แคชไว้ซ้ำ เหตุการณ์ EventBus เหล่านี้ทั้งหมดใช้ Postable แต่ไม่ใช่ทุกอย่างที่โพสต์ไปยัง EventBus จะใช้อินเทอร์เฟซนี้เสมอไป
เฉพาะรายการที่แคชโดย ExtendedEventHandler เท่านั้น (จะดีมากและ
ส่วนใหญ่ก็เป็นเช่นนั้น แต่ก็ไม่ได้บังคับ)
เอาต์พุตของเทอร์มินัลส่วนใหญ่จะปล่อยผ่าน UiEventHandler ซึ่งมีหน้าที่
รับผิดชอบการจัดรูปแบบเอาต์พุตที่สวยงามทั้งหมดและการรายงานความคืบหน้าที่ Bazel
ทำ โดยมีอินพุต 2 รายการ ได้แก่
- Event Bus
- สตรีมเหตุการณ์ที่ส่งผ่าน Reporter
การเชื่อมต่อโดยตรงเพียงอย่างเดียวที่กลไกการดำเนินการคำสั่ง (เช่น ส่วนที่เหลือของ
Bazel) มีกับสตรีม RPC ไปยังไคลเอ็นต์คือผ่าน Reporter.getOutErr()
ซึ่งอนุญาตให้เข้าถึงสตรีมเหล่านี้ได้โดยตรง โดยจะใช้เฉพาะเมื่อคำสั่งต้องการ
ทิ้งข้อมูลไบนารีที่เป็นไปได้จำนวนมาก (เช่น bazel query)
การทำโปรไฟล์ Bazel
Bazel ทำงานเร็ว นอกจากนี้ Bazel ยังช้าด้วย เนื่องจากบิลด์มักจะขยายขนาดจนถึงขีดจำกัดที่รับได้ ด้วยเหตุนี้ Bazel จึงมีโปรไฟล์เลอร์ที่ใช้เพื่อสร้างโปรไฟล์บิลด์และตัว Bazel เองได้ โดยมีการติดตั้งใช้งานในคลาสที่ชื่อ Profiler โดยค่าเริ่มต้นจะเปิดอยู่ แต่จะบันทึกเฉพาะข้อมูลที่ย่อแล้วเพื่อให้ค่าใช้จ่ายที่เพิ่มขึ้นอยู่ในระดับที่ยอมรับได้ ส่วนบรรทัดคำสั่ง--record_full_profiler_dataจะทำให้บันทึกทุกอย่างที่ทำได้
โดยจะสร้างโปรไฟล์ในรูปแบบโปรไฟล์ของ Chrome ซึ่งควรดูใน Chrome รูปแบบข้อมูลของฟีเจอร์นี้คือสแต็กงาน ซึ่งผู้ใช้สามารถเริ่มและสิ้นสุดงานได้ และงานต่างๆ จะซ้อนกันอย่างเป็นระเบียบ แต่ละเธรด Java จะมี สแต็กงานของตัวเอง TODO: How does this work with actions and continuation-passing style?
ระบบจะเริ่มและหยุดโปรไฟล์เลอร์ใน BlazeRuntime.initProfiler() และ
BlazeRuntime.afterCommand() ตามลำดับ และพยายามให้โปรไฟล์เลอร์ทำงานได้นานที่สุด
เท่าที่จะเป็นไปได้เพื่อให้เราสามารถสร้างโปรไฟล์ทุกอย่างได้ หากต้องการเพิ่มข้อมูลในโปรไฟล์
โปรดโทรหา Profiler.instance().profile() โดยจะแสดงผล Closeable ซึ่งการปิด
แสดงถึงจุดสิ้นสุดของงาน โดยควรใช้กับคำสั่ง try-with-resources
นอกจากนี้ เรายังทำการสร้างโปรไฟล์หน่วยความจำเบื้องต้นใน MemoryProfiler ด้วย นอกจากนี้ยังเปิดอยู่เสมอ
และส่วนใหญ่จะบันทึกขนาดฮีปสูงสุดและลักษณะการทำงานของ GC
การทดสอบ Bazel
Bazel มีการทดสอบ 2 ประเภทหลักๆ ได้แก่ การทดสอบที่สังเกต Bazel เป็น "กล่องดำ" และ การทดสอบที่เรียกใช้เฉพาะเฟสการวิเคราะห์ เราเรียกการทดสอบแบบแรกว่า "การทดสอบการผสานรวม" และแบบหลังว่า "การทดสอบหน่วย" แม้ว่าการทดสอบแบบหลังจะคล้ายกับการทดสอบการผสานรวมที่ มีการผสานรวมน้อยกว่าก็ตาม นอกจากนี้ เรายังมีการทดสอบหน่วยจริงบางอย่างในกรณีที่จำเป็น
การทดสอบการผสานรวมมี 2 ประเภท ได้แก่
- ซึ่งใช้เฟรมเวิร์กการทดสอบ Bash ที่ซับซ้อนมากภายใต้
src/test/shell - ที่ใช้ใน Java ซึ่งจะใช้เป็นคลาสย่อยของ
BuildIntegrationTestCase
BuildIntegrationTestCase เป็นเฟรมเวิร์กการทดสอบการผสานรวมที่แนะนำ เนื่องจากมี
เครื่องมือที่พร้อมใช้งานสำหรับสถานการณ์การทดสอบส่วนใหญ่ เนื่องจากเป็นเฟรมเวิร์ก Java จึง
ให้ความสามารถในการแก้ไขข้อบกพร่องและการผสานรวมกับเครื่องมือพัฒนาทั่วไป
หลายอย่างได้อย่างราบรื่น มีตัวอย่างBuildIntegrationTestCaseคลาสมากมายในที่เก็บ Bazel
การทดสอบการวิเคราะห์จะใช้เป็นคลาสย่อยของ BuildViewTestCase มีระบบไฟล์ชั่วคราวที่คุณใช้เขียนไฟล์ BUILD ได้ จากนั้นเมธอดตัวช่วยต่างๆ จะขอเป้าหมายที่กำหนดค่า เปลี่ยนการกำหนดค่า และยืนยันสิ่งต่างๆ เกี่ยวกับผลลัพธ์ของการวิเคราะห์ได้