เอกสารนี้เป็นคำอธิบายของโค้ดเบสและโครงสร้างของ Bazel โดยมีไว้สำหรับผู้ที่ต้องการมีส่วนร่วมใน Bazel ไม่ใช่สำหรับผู้ใช้ปลายทาง
บทนำ
โค้ดเบสของ Bazel มีขนาดใหญ่ (โค้ดที่ใช้จริงประมาณ 350,000 บรรทัดและโค้ดทดสอบประมาณ 260,000 บรรทัด) และไม่มีใครคุ้นเคยกับภาพรวมทั้งหมด ทุกคนรู้จักส่วนที่ตนเองรับผิดชอบเป็นอย่างดี แต่มีเพียงไม่กี่คนที่รู้ว่ามีอะไรอยู่บนเนินเขาในทุกทิศทาง
เอกสารนี้พยายามให้ภาพรวมของโค้ดเบสเพื่อให้เริ่มต้นใช้งานได้ง่ายขึ้น เพื่อไม่ให้ผู้ที่อยู่กลางเส้นทางพบว่าตนเองอยู่ในป่ามืดและสูญเสียเส้นทางตรงไปตรงมา
ซอร์สโค้ดเวอร์ชันสาธารณะของ Bazel อยู่ใน GitHub ที่ github.com/bazelbuild/bazel นี่ไม่ใช่ "แหล่งข้อมูลที่เชื่อถือได้" แต่เป็นข้อมูลที่ได้จากโครงสร้างแหล่งข้อมูลภายในของ Google ซึ่งมีฟังก์ชันการทำงานเพิ่มเติมที่ไม่เป็นประโยชน์ภายนอก Google เป้าหมายระยะยาวคือการทำให้ GitHub เป็นแหล่งข้อมูลที่ถูกต้อง
เรายอมรับการมีส่วนร่วมผ่านกลไกคำขอดึงข้อมูลของ GitHub ตามปกติ และ Googler จะนำเข้าด้วยตนเองไปยังโครงสร้างแหล่งข้อมูลภายใน จากนั้น จะส่งออกกลับไปยัง GitHub
สถาปัตยกรรมไคลเอ็นต์/เซิร์ฟเวอร์
Bazel ส่วนใหญ่จะอยู่ในกระบวนการของเซิร์ฟเวอร์ที่อยู่ใน RAM ระหว่างการสร้าง ซึ่งช่วยให้ Bazel รักษาสถานะระหว่างการสร้างได้
ด้วยเหตุนี้ บรรทัดคำสั่ง Bazel จึงมีตัวเลือก 2 ประเภท ได้แก่ ตัวเลือกการเริ่มต้นและตัวเลือกคำสั่ง ในบรรทัดคำสั่งลักษณะนี้
bazel --host_jvm_args=-Xmx8G build -c opt //foo:bar
ตัวเลือกบางอย่าง (--host_jvm_args=
) จะอยู่ก่อนชื่อคำสั่งที่จะเรียกใช้
และบางอย่างจะอยู่หลัง (-c opt
) ตัวเลือกประเภทแรกเรียกว่า "ตัวเลือกการเริ่มต้น" และ
ส่งผลต่อกระบวนการของเซิร์ฟเวอร์โดยรวม ในขณะที่ตัวเลือกประเภทหลัง ซึ่งก็คือ "ตัวเลือกคำสั่ง"
จะส่งผลต่อคำสั่งเดียวเท่านั้น
อินสแตนซ์เซิร์ฟเวอร์แต่ละรายการจะมีพื้นที่ทำงานที่เชื่อมโยงอยู่เพียงรายการเดียว (คอลเล็กชันของโครงสร้างแหล่งข้อมูลที่เรียกว่า "ที่เก็บข้อมูล") และโดยปกติแล้วพื้นที่ทำงานแต่ละรายการจะมีอินสแตนซ์เซิร์ฟเวอร์ที่ใช้งานอยู่เพียงรายการเดียว คุณหลีกเลี่ยงปัญหานี้ได้โดยการระบุฐานเอาต์พุตที่กำหนดเอง (ดูข้อมูลเพิ่มเติมได้ที่ส่วน "โครงสร้างไดเรกทอรี")
Bazel จัดจำหน่ายเป็นไฟล์ปฏิบัติการ ELF เดียวซึ่งเป็นไฟล์ .zip ที่ถูกต้องด้วย
เมื่อคุณพิมพ์ bazel
ไฟล์ที่เรียกใช้งาน ELF ด้านบนซึ่งใช้ใน C++ ("ไคลเอ็นต์") จะได้รับการควบคุม โดยจะตั้งค่ากระบวนการเซิร์ฟเวอร์ที่เหมาะสมโดยใช้
ขั้นตอนต่อไปนี้
- ตรวจสอบว่ามีการแยกไฟล์แล้วหรือไม่ หากไม่ ระบบจะดำเนินการดังกล่าว ซึ่งเป็นที่มาของการติดตั้งใช้งานเซิร์ฟเวอร์
- ตรวจสอบว่ามีอินสแตนซ์เซิร์ฟเวอร์ที่ใช้งานอยู่ซึ่งทำงานได้หรือไม่ โดยอินสแตนซ์นั้นต้องทำงานอยู่
มีตัวเลือกการเริ่มต้นที่ถูกต้อง และใช้ไดเรกทอรีพื้นที่ทำงานที่ถูกต้อง โดยจะ
ค้นหาเซิร์ฟเวอร์ที่กำลังทำงานโดยดูที่ไดเรกทอรี
$OUTPUT_BASE/server
ซึ่งมีไฟล์ล็อกที่มีพอร์ตที่เซิร์ฟเวอร์กำลังรับฟังอยู่ - หากจำเป็น ให้หยุดกระบวนการเซิร์ฟเวอร์เก่า
- เริ่มกระบวนการเซิร์ฟเวอร์ใหม่หากจำเป็น
หลังจากที่กระบวนการของเซิร์ฟเวอร์ที่เหมาะสมพร้อมแล้ว ระบบจะสื่อสารคำสั่งที่ต้องเรียกใช้กับเซิร์ฟเวอร์ผ่านอินเทอร์เฟซ gRPC จากนั้นจะส่งเอาต์พุตของ Bazel กลับไปยังเทอร์มินัล คุณจะเรียกใช้คำสั่งได้ครั้งละ 1 รายการเท่านั้น ซึ่ง
ใช้กลไกการล็อกที่ซับซ้อนโดยมีส่วนต่างๆ ใน C++ และส่วนต่างๆ ใน
Java เรามีโครงสร้างพื้นฐานบางอย่างสำหรับการเรียกใช้คำสั่งหลายรายการแบบคู่ขนาน
เนื่องจากเราไม่สามารถเรียกใช้ bazel version
แบบคู่ขนานกับคำสั่งอื่น
ซึ่งเป็นเรื่องที่น่าอาย อุปสรรคหลักคือวงจรการใช้งานของ BlazeModule
s
และสถานะบางอย่างใน BlazeRuntime
เมื่อสิ้นสุดคำสั่ง เซิร์ฟเวอร์ Bazel จะส่งรหัสออกที่ไคลเอ็นต์ควรส่งคืน
ข้อควรทราบที่น่าสนใจคือการใช้งาน bazel run
ซึ่งคำสั่งนี้มีหน้าที่เรียกใช้สิ่งที่ Bazel เพิ่งสร้าง แต่ทำเช่นนั้นจากกระบวนการของเซิร์ฟเวอร์ไม่ได้เนื่องจากไม่มีเทอร์มินัล ดังนั้นจึงจะบอก
ไคลเอ็นต์ว่าควรexec()
ไบนารีใดและมีอาร์กิวเมนต์ใด
เมื่อกด Ctrl-C ไคลเอ็นต์จะแปลเป็นคำสั่งยกเลิกในการเชื่อมต่อ gRPC ซึ่งพยายามสิ้นสุดคำสั่งโดยเร็วที่สุด หลังจากกด Ctrl-C ครั้งที่ 3 ไคลเอ็นต์จะส่ง SIGKILL ไปยังเซิร์ฟเวอร์แทน
ซอร์สโค้ดของไคลเอ็นต์อยู่ภายใต้ src/main/cpp
และโปรโตคอลที่ใช้ในการ
สื่อสารกับเซิร์ฟเวอร์อยู่ใน src/main/protobuf/command_server.proto
จุดแรกเข้าหลักของเซิร์ฟเวอร์คือ BlazeRuntime.main()
และการเรียก gRPC จากไคลเอ็นต์จะได้รับการจัดการโดย GrpcServerImpl.run()
เลย์เอาต์ไดเรกทอรี
Bazel จะสร้างชุดไดเรกทอรีที่ค่อนข้างซับซ้อนในระหว่างการบิลด์ ดูคำอธิบายแบบเต็มได้ในเลย์เอาต์ไดเรกทอรีเอาต์พุต
"ที่เก็บหลัก" คือโครงสร้างแหล่งที่มาที่ Bazel ทำงาน โดยปกติแล้วจะสอดคล้องกับ สิ่งที่คุณเช็คเอาต์จากการควบคุมแหล่งที่มา รูทของไดเรกทอรีนี้เรียกว่า "รูทของพื้นที่ทำงาน"
Bazel จะวางข้อมูลทั้งหมดไว้ใต้ "รูทของผู้ใช้เอาต์พุต" โดยปกติจะเป็น
$HOME/.cache/bazel/_bazel_${USER}
แต่สามารถลบล้างได้โดยใช้
--output_user_root
ตัวเลือกการเริ่มต้น
"ฐานการติดตั้ง" คือตำแหน่งที่แยก Bazel ระบบจะดำเนินการนี้โดยอัตโนมัติ
และ Bazel แต่ละเวอร์ชันจะมีไดเรกทอรีย่อยตามผลรวมตรวจสอบภายใต้
ฐานการติดตั้ง โดยค่าเริ่มต้นจะอยู่ที่ $OUTPUT_USER_ROOT/install
และเปลี่ยนได้
โดยใช้ตัวเลือกบรรทัดคำสั่ง --install_base
"เอาต์พุตเบส" คือที่ที่อินสแตนซ์ Bazel ที่แนบมากับพื้นที่ทํางานที่เฉพาะเจาะจง
เขียนถึง ฐานเอาต์พุตแต่ละฐานจะมีอินสแตนซ์เซิร์ฟเวอร์ Bazel อย่างมาก 1 รายการ
ที่ทำงานในเวลาใดก็ตาม โดยปกติจะอยู่ที่ $OUTPUT_USER_ROOT/<checksum of the path
to the workspace>
คุณสามารถเปลี่ยนได้โดยใช้--output_base
ตัวเลือกการเริ่มต้น
ซึ่งมีประโยชน์ในหลายๆ ด้าน รวมถึงการหลีกเลี่ยงข้อจำกัดที่ว่ามีอินสแตนซ์ Bazel ได้เพียง
อินสแตนซ์เดียวที่ทำงานในเวิร์กสเปซใดก็ตามในเวลาใดก็ตาม
ไดเรกทอรีเอาต์พุตมีข้อมูลต่อไปนี้
- ที่เก็บภายนอกที่ดึงข้อมูลมาที่
$OUTPUT_BASE/external
- รูทของ Exec ซึ่งเป็นไดเรกทอรีที่มีลิงก์สัญลักษณ์ไปยังซอร์สโค้ดทั้งหมดสำหรับการสร้างปัจจุบัน ตั้งอยู่ที่
$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 เพื่อบอกให้คนทั่วโลกทราบว่าการบิลด์ เป็นอย่างไร
คำสั่งจะได้รับการควบคุม คำสั่งที่น่าสนใจที่สุดคือคำสั่งที่เรียกใช้บิลด์ เช่น build, test, run, coverage และอื่นๆ ซึ่งฟังก์ชันนี้จะได้รับการติดตั้งใช้งานโดย
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
ทันทีหลังจากสร้างก่อนที่คนอื่น
จะมีโอกาสเก็บการอ้างอิงถึง 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 ที่เก็บข้อมูลภายนอก
มีการกำหนดไว้หลายวิธี ดูข้อมูลเพิ่มเติมได้ที่ภาพรวมของทรัพยากรภายนอก
โค้ดของที่เก็บภายนอกจะได้รับการลิงก์สัญลักษณ์หรือดาวน์โหลดภายใต้
$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
คลาสนี้มีสมาชิกบางรายที่ใช้เพื่อแยกวิเคราะห์แพ็กเกจ "ภายนอก" (ที่เกี่ยวข้องกับทรัพยากรภายนอก) โดยเฉพาะ และสมาชิกดังกล่าวไม่เหมาะสำหรับแพ็กเกจจริง นี่เป็นข้อบกพร่องในการออกแบบเนื่องจากออบเจ็กต์ที่อธิบายแพ็กเกจปกติไม่ควรมีฟิลด์ที่อธิบายสิ่งอื่น ซึ่งได้แก่
- การแมปที่เก็บ
- Toolchain ที่ลงทะเบียน
- แพลตฟอร์มการดำเนินการที่ลงทะเบียน
ในอุดมคติแล้ว ควรแยกการแยกวิเคราะห์แพ็กเกจ "ภายนอก" ออกจากการแยกวิเคราะห์แพ็กเกจปกติ เพื่อให้ 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
ทั้งคู่มีการเปลี่ยนแปลงไม่ได้อย่างยิ่ง มีเพียงออบเจ็กต์ที่เปลี่ยนแปลงไม่ได้เท่านั้นที่ควรเข้าถึงได้จากออบเจ็กต์ทั้งสอง
โดยปกติแล้วค่าคงที่นี้จะใช้ได้เสมอ และในกรณีที่ใช้ไม่ได้ (เช่น สำหรับคลาสตัวเลือกแต่ละรายการ 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 ที่ซับซ้อน การจัดการสถานะ ระหว่างการรีสตาร์ทอาจเป็นเรื่องยาก เราจึงได้เปิดตัวStateMachine
s เพื่อให้มีแนวทางที่มีโครงสร้างสำหรับความพร้อมกันเชิงตรรกะ รวมถึงฮุกเพื่อระงับและ ดำเนินการต่อในการคำนวณแบบลำดับชั้นภายใน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++)
- การขึ้นต่อกันโดยตรง ผู้ให้บริการข้อมูลแบบทรานซิทีฟพร้อมใช้งาน สำหรับกฎที่กำลังวิเคราะห์ ที่เรียกว่าเช่นนั้นเนื่องจากจะให้ "สรุป" ข้อมูลใน การปิดทรานซิทีฟของเป้าหมายที่กำหนดค่าไว้ เช่น ไฟล์ .jar ทั้งหมดในเส้นทางคลาสหรือไฟล์ .o ทั้งหมดที่ต้องลิงก์กับไบนารี C++
- เป้าหมายเอง นี่คือผลลัพธ์ของการโหลดแพ็กเกจที่เป้าหมายอยู่ สำหรับกฎ จะรวมถึงแอตทริบิวต์ของกฎ ซึ่งมักจะเป็นสิ่งที่สำคัญ
- การติดตั้งใช้งานเป้าหมายที่กำหนดค่าไว้ สำหรับกฎ จะอยู่ใน Starlark หรือ Java ก็ได้ ระบบจะใช้เป้าหมายทั้งหมดที่ไม่ได้กำหนดค่ากฎ ใน Java
เอาต์พุตของการวิเคราะห์เป้าหมายที่กำหนดค่าคือ
- ผู้ให้บริการข้อมูลแบบทรานซิทีฟที่กำหนดค่าเป้าหมายซึ่งขึ้นอยู่กับข้อมูลดังกล่าวจะ เข้าถึง
- อาร์ติแฟกต์ที่สร้างได้และการดำเนินการที่สร้างอาร์ติแฟกต์เหล่านั้น
API ที่มีให้สำหรับกฎ Java คือ RuleContext
ซึ่งเทียบเท่ากับอาร์กิวเมนต์ ctx
ของกฎ Starlark API ของมันมีประสิทธิภาพมากกว่า แต่ในขณะเดียวกันก็ทำสิ่งที่ไม่ดี™ ได้ง่ายกว่าด้วย เช่น การเขียนโค้ดที่มีความซับซ้อนด้านเวลาหรือพื้นที่เป็นกำลังสอง (หรือแย่กว่านั้น) การทำให้เซิร์ฟเวอร์ Bazel ขัดข้องด้วยข้อยกเว้นของ Java หรือการละเมิดค่าคงที่ (เช่น โดยการแก้ไขอินสแตนซ์ Options
โดยไม่ตั้งใจ หรือโดยการทำให้เป้าหมายที่กำหนดค่าแล้วเปลี่ยนแปลงได้)
อัลกอริทึมที่กำหนดการขึ้นต่อกันโดยตรงของเป้าหมายที่กำหนดค่า
อยู่ใน DependencyResolver.dependentNodeMap()
การกำหนดค่า
การกำหนดค่าคือ "วิธี" สร้างเป้าหมาย: สำหรับแพลตฟอร์มใด มีตัวเลือกบรรทัดคำสั่งใด ฯลฯ
คุณสร้างเป้าหมายเดียวกันสำหรับการกำหนดค่าหลายรายการในการสร้างเดียวกันได้ ซึ่งจะมีประโยชน์ เช่น เมื่อใช้โค้ดเดียวกันสำหรับเครื่องมือที่ทำงานระหว่างการสร้างและสำหรับโค้ดเป้าหมาย และเรากำลังคอมไพล์ข้าม หรือเมื่อเรากำลังสร้างแอป Android แบบ Fat (แอปที่มีโค้ดแบบเนทีฟสำหรับสถาปัตยกรรม CPU หลายรายการ)
ในเชิงแนวคิด การกำหนดค่าคือBuildOptions
อินสแตนซ์ อย่างไรก็ตาม ในทางปฏิบัติ BuildOptions
จะห่อหุ้มด้วย BuildConfiguration
ซึ่งมีฟังก์ชันการทำงานอื่นๆ เพิ่มเติม โดยจะแพร่กระจายจากด้านบนของ
กราฟการอ้างอิงไปยังด้านล่าง หากมีการเปลี่ยนแปลง คุณจะต้องวิเคราะห์บิลด์อีกครั้ง
ซึ่งส่งผลให้เกิดความผิดปกติ เช่น ต้องวิเคราะห์บิลด์ทั้งหมดอีกครั้งหากมีการเปลี่ยนแปลงจำนวนการทดสอบที่ขอ แม้ว่าการเปลี่ยนแปลงนั้นจะส่งผลต่อเป้าหมายการทดสอบเท่านั้น (เรามีแผนที่จะ "ตัด" การกำหนดค่าเพื่อไม่ให้เกิดกรณีนี้ แต่ยังไม่พร้อมใช้งาน)
เมื่อการใช้งานกฎต้องใช้ส่วนหนึ่งของการกำหนดค่า ก็ต้องประกาศในคำจำกัดความโดยใช้ RuleClass.Builder.requiresConfigurationFragments()
ทั้งนี้เพื่อหลีกเลี่ยงข้อผิดพลาด (เช่น กฎ Python ที่ใช้ส่วน Java) และเพื่ออำนวยความสะดวกในการตัดแต่งการกำหนดค่า เช่น หากตัวเลือก Python เปลี่ยนไป เป้าหมาย C++ จะไม่จำเป็นต้องได้รับการวิเคราะห์ซ้ำ
การกำหนดค่าของกฎไม่จำเป็นต้องเหมือนกับการกำหนดค่าของกฎ "ระดับบน" กระบวนการเปลี่ยนการกำหนดค่าใน Edge ที่ขึ้นต่อกันเรียกว่า "การเปลี่ยนการกำหนดค่า" ซึ่งอาจเกิดขึ้นได้ 2 แห่ง ดังนี้
- ที่ขอบทรัพยากร Dependency การเปลี่ยนเหล่านี้ระบุไว้ใน
Attribute.Builder.cfg()
และเป็นฟังก์ชันจากRule
(ที่เกิดการเปลี่ยน) และBuildOptions
(การกำหนดค่าเดิม) ไปยังBuildOptions
อย่างน้อย 1 รายการ (การกำหนดค่าเอาต์พุต) - ที่ขอบใดๆ ที่เข้ามายังเป้าหมายที่กำหนดค่าไว้ ซึ่งระบุไว้ใน
RuleClass.Builder.cfg()
คลาสที่เกี่ยวข้องคือ TransitionFactory
และ ConfigurationTransition
ตัวอย่างการใช้การเปลี่ยนการกำหนดค่า
- เพื่อประกาศว่ามีการใช้การขึ้นต่อกันหนึ่งๆ ในระหว่างการสร้าง และ ควรสร้างในการออกแบบการดำเนินการ
- หากต้องการประกาศว่าต้องสร้างการขึ้นต่อกันที่เฉพาะเจาะจงสำหรับหลายสถาปัตยกรรม (เช่น สำหรับโค้ดเนทีฟใน APK ของ Android แบบ Fat)
หากการเปลี่ยนการกำหนดค่าส่งผลให้มีการกำหนดค่าหลายรายการ จะเรียกว่าการเปลี่ยนแบบแยก
นอกจากนี้ คุณยังใช้ทรานซิชันการกำหนดค่าใน 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 และใช้
OutputGroupInfo
ใน Java
Runfiles
ไบนารีบางรายการต้องใช้ไฟล์ข้อมูลจึงจะทำงานได้ ตัวอย่างที่ชัดเจนคือการทดสอบที่ต้องใช้ไฟล์อินพุต ซึ่งแสดงใน Bazel ด้วยแนวคิดของ "runfiles" "ทรีไฟล์ที่เรียกใช้" คือโครงสร้างไดเรกทอรีของไฟล์ข้อมูลสำหรับไบนารีหนึ่งๆ โดยจะสร้างในระบบไฟล์เป็นแผนผังลิงก์สัญลักษณ์ที่มีลิงก์สัญลักษณ์แต่ละรายการ ซึ่งชี้ไปยังไฟล์ในแผนผังแหล่งที่มาหรือเอาต์พุต
ชุดไฟล์ที่เรียกใช้จะแสดงเป็นอินสแตนซ์ Runfiles
ในเชิงแนวคิดแล้ว
แมปจากเส้นทางของไฟล์ในโครงสร้างไฟล์ที่สร้างขึ้นระหว่างการทดสอบไปยังอินสแตนซ์ Artifact
ที่
แสดงไฟล์นั้น ซึ่งมีความซับซ้อนมากกว่าการใช้ Map
เพียงอย่างเดียวด้วย 2 เหตุผลต่อไปนี้
- โดยส่วนใหญ่แล้ว เส้นทางไฟล์ที่รันของไฟล์จะเหมือนกับเส้นทางที่เรียกใช้ เราใช้ฟีเจอร์นี้เพื่อประหยัด RAM
- มีรายการหลายประเภทที่เลิกใช้งานแล้วในโครงสร้างไฟล์ที่เรียกใช้ ซึ่งต้องแสดงด้วย
ระบบจะรวบรวมไฟล์ที่เรียกใช้โดยใช้ RunfilesProvider
: อินสแตนซ์ของคลาสนี้
แสดงถึงไฟล์ที่เรียกใช้ที่เป้าหมายที่กำหนดค่า (เช่น ไลบรารี) และการปิดทรานซิทีฟ
ต้องใช้ และระบบจะรวบรวมไฟล์ที่เรียกใช้เหมือนชุดที่ซ้อนกัน (ในความเป็นจริงแล้ว ระบบจะ
ใช้ชุดที่ซ้อนกันในการติดตั้งใช้งานภายใต้การครอบคลุม): แต่ละเป้าหมายจะรวมไฟล์ที่เรียกใช้
ของทรัพยากร Dependency เพิ่มไฟล์ที่เรียกใช้ของตัวเองบางส่วน จากนั้นจะส่งชุดผลลัพธ์ขึ้นไป
ในกราฟทรัพยากร Dependency RunfilesProvider
อินสแตนซ์Runfiles
มี 2 อินสแตนซ์ ได้แก่ อินสแตนซ์หนึ่งสำหรับเมื่อกฎขึ้นอยู่กับแอตทริบิวต์ "data" และ
อีกอินสแตนซ์หนึ่งสำหรับ Dependency ขาเข้าประเภทอื่นๆ ทั้งหมด เนื่องจากเป้าหมาย
บางครั้งจะแสดงไฟล์ที่รันแตกต่างกันเมื่อขึ้นอยู่กับแอตทริบิวต์ข้อมูล
มากกว่าในกรณีอื่นๆ นี่เป็นลักษณะการทำงานเดิมที่ไม่พึงประสงค์ซึ่งเรายังไม่ได้
นำออก
ไฟล์ที่เรียกใช้ของไบนารีจะแสดงเป็นอินสแตนซ์ของ RunfilesSupport
ซึ่งแตกต่างจาก Runfiles
เนื่องจาก RunfilesSupport
มีความสามารถในการสร้างจริง (ต่างจาก Runfiles
ซึ่งเป็นเพียงการแมป) ซึ่งต้องมีคอมโพเนนต์เพิ่มเติมต่อไปนี้
- ไฟล์ Manifest ของไฟล์ที่รันอินพุต นี่คือคำอธิบายแบบอนุกรมของ โครงสร้างไฟล์ที่สร้างขึ้นระหว่างการทดสอบ โดยจะใช้เป็นพร็อกซีสำหรับเนื้อหาของทรีไฟล์ที่เรียกใช้ และ Bazel จะถือว่าทรีไฟล์ที่เรียกใช้มีการเปลี่ยนแปลงก็ต่อเมื่อเนื้อหา ของไฟล์ Manifest มีการเปลี่ยนแปลง
- ไฟล์ Manifest ของไฟล์ที่รันเอาต์พุต ใช้โดยไลบรารีรันไทม์ที่ จัดการโครงสร้างไฟล์รัน โดยเฉพาะใน Windows ซึ่งบางครั้งไม่รองรับ ลิงก์สัญลักษณ์
- อาร์กิวเมนต์บรรทัดคำสั่งสำหรับการเรียกใช้ไบนารีที่มีไฟล์รันที่ออบเจ็กต์
RunfilesSupport
แสดง
Aspects
Aspect เป็นวิธี "เผยแพร่การคำนวณลงในกราฟการขึ้นต่อกัน" ซึ่งอธิบายไว้สำหรับผู้ใช้ Bazel
ที่นี่ ตัวอย่างที่กระตุ้นให้เกิดการใช้งานที่ดีคือ Protocol Buffer ซึ่งproto_library
ไม่ควรรู้จักภาษาใดภาษาหนึ่ง แต่การสร้างการใช้งานข้อความ Protocol Buffer ("หน่วยพื้นฐาน" ของ Protocol Buffer) ในภาษาโปรแกรมใดๆ ควรเชื่อมโยงกับกฎ proto_library
เพื่อให้หากเป้าหมาย 2 รายการในภาษาเดียวกันขึ้นอยู่กับ Protocol Buffer เดียวกัน ระบบจะสร้างเพียงครั้งเดียว
เช่นเดียวกับเป้าหมายที่กำหนดค่าไว้ เป้าหมายเหล่านี้จะแสดงใน Skyframe เป็น SkyValue
และวิธีสร้างเป้าหมายเหล่านี้จะคล้ายกับวิธีสร้างเป้าหมายที่กำหนดค่าไว้มาก โดยจะมีคลาส Factory ที่เรียกว่า ConfiguredAspectFactory
ซึ่งมีสิทธิ์เข้าถึง RuleContext
แต่ต่างจาก Factory ของเป้าหมายที่กำหนดค่าไว้ตรงที่ Factory นี้ยังทราบเกี่ยวกับเป้าหมายที่กำหนดค่าไว้ที่แนบอยู่และผู้ให้บริการของเป้าหมายนั้นด้วย
ชุดแง่มุมที่ส่งต่อลงในกราฟการอ้างอิงจะระบุไว้สำหรับแต่ละแอตทริบิวต์โดยใช้ฟังก์ชัน Attribute.Builder.aspects()
มีคลาสบางคลาสที่
มีชื่อที่อาจทำให้สับสนซึ่งเข้าร่วมในกระบวนการนี้
AspectClass
คือการใช้งานแง่มุม โดยอาจอยู่ใน Java (ในกรณีนี้จะเป็นคลาสย่อย) หรือใน Starlark (ในกรณีนี้จะเป็นอินสแตนซ์ของStarlarkAspectClass
) ซึ่งคล้ายกับRuleConfiguredTargetFactory
AspectDefinition
คือคำจำกัดความของแง่มุม ซึ่งรวมถึง ผู้ให้บริการที่ต้องใช้ ผู้ให้บริการที่ให้บริการ และมีการอ้างอิงถึง การใช้งาน เช่น อินสแตนซ์AspectClass
ที่เหมาะสม ซึ่งคล้ายกับRuleClass
AspectParameters
เป็นวิธีกำหนดพารามิเตอร์ให้กับแง่มุมที่ส่งต่อลงมา ในกราฟทรัพยากร 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
) ใน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
ข้อจำกัดของแพลตฟอร์ม
ปัจจุบันวิธี "อย่างเป็นทางการ" ในการอธิบายว่าแพลตฟอร์มใดบ้างที่เป้าหมายเข้ากันได้ คือการใช้ข้อจำกัดเดียวกันกับที่ใช้ในการอธิบายทูลเชนและแพลตฟอร์ม ฟีเจอร์นี้ได้รับการติดตั้งใช้งานในคำขอพุล #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
- อาร์ติแฟกต์ข้อมูลเมตาคงที่ การเปลี่ยนแปลงอาร์ติแฟกต์เหล่านี้จะไม่ทริกเกอร์การสร้างใหม่ โดยจะใช้เพื่อข้อมูลการประทับเวลาของบิลด์เท่านั้น เราไม่ต้องการ สร้างบิลด์ใหม่เพียงเพราะเวลาปัจจุบันเปลี่ยนไป
ไม่มีเหตุผลพื้นฐานที่อาร์ติแฟกต์แหล่งที่มาจะเป็นอาร์ติแฟกต์แบบทรีหรืออาร์ติแฟกต์ซิมลิงก์ที่ยังไม่ได้รับการแก้ไขไม่ได้ เพียงแต่เรายังไม่ได้ติดตั้งใช้งาน (แต่เราควรทำ เนื่องจากอ้างอิงไดเรกทอรีแหล่งที่มาในไฟล์ BUILD
เป็นหนึ่งในปัญหาความไม่ถูกต้องที่ทราบกันมานานไม่กี่อย่างของ Bazel เรามีการติดตั้งใช้งานที่ใช้งานได้ซึ่งเปิดใช้โดยพร็อพเพอร์ตี้ BAZEL_TRACK_SOURCE_DIRECTORIES=1
JVM)
การดำเนินการควรเข้าใจได้ดีที่สุดในฐานะคำสั่งที่ต้องเรียกใช้ สภาพแวดล้อมที่ต้องการ และชุดเอาต์พุตที่สร้างขึ้น องค์ประกอบหลักของคำอธิบายการดำเนินการมีดังนี้
- บรรทัดคำสั่งที่ต้องเรียกใช้
- อาร์ติแฟกต์อินพุตที่จำเป็น
- ตัวแปรสภาพแวดล้อมที่ต้องตั้งค่า
- คำอธิบายประกอบที่อธิบายสภาพแวดล้อม (เช่น แพลตฟอร์ม) ที่ต้องใช้ในการเรียกใช้ \
นอกจากนี้ ยังมีกรณีพิเศษอื่นๆ อีก 2-3 กรณี เช่น การเขียนไฟล์ที่มีเนื้อหาที่ Bazel รู้จัก โดยเป็นคลาสย่อยของ AbstractAction
การดำเนินการส่วนใหญ่คือ SpawnAction
หรือ StarlarkAction
(เหมือนกัน ซึ่งไม่ควรเป็นคลาสแยกกัน) แม้ว่า Java และ C++ จะมีประเภทการดำเนินการของตนเอง (JavaCompileAction
, CppCompileAction
และ CppLinkAction
)
ในที่สุดเราก็ต้องการย้ายทุกอย่างไปที่ SpawnAction
; JavaCompileAction
นั้น
ค่อนข้างใกล้เคียง แต่ C++ เป็นกรณีพิเศษเล็กน้อยเนื่องจากการแยกวิเคราะห์ไฟล์ .d และ
การสแกนการรวม
กราฟการดำเนินการส่วนใหญ่จะ "ฝัง" อยู่ในกราฟ Skyframe โดยในเชิงแนวคิด การดำเนินการจะแสดงเป็นการเรียกใช้ ActionExecutionFunction
การแมประหว่างขอบทรัพยากร Dependency ของกราฟการดำเนินการกับขอบทรัพยากร Dependency ของ Skyframe อธิบายไว้ใน
ActionExecutionFunction.getInputDeps()
และ Artifact.key()
และมีการเพิ่มประสิทธิภาพเล็กน้อยเพื่อรักษาจำนวนขอบของ Skyframe ให้อยู่ในระดับต่ำ
- อาร์ติแฟกต์ที่ได้มาจะไม่มี
SkyValue
ของตัวเอง แต่จะใช้Artifact.getGeneratingActionKey()
เพื่อค้นหาคีย์สำหรับ การดำเนินการที่สร้างคีย์ดังกล่าว - ชุดที่ซ้อนกันจะมีคีย์ Skyframe ของตัวเอง
การดำเนินการที่แชร์
การดำเนินการบางอย่างสร้างขึ้นโดยเป้าหมายที่กำหนดค่าไว้หลายรายการ กฎ Starlark มีข้อจำกัดมากกว่าเนื่องจากอนุญาตให้ใส่การดำเนินการที่ได้มาไว้ในไดเรกทอรีที่กำหนดโดยการกำหนดค่าและแพ็กเกจของกฎเท่านั้น (แต่ถึงอย่างนั้น กฎในแพ็กเกจเดียวกันก็อาจขัดแย้งกันได้) แต่กฎที่ใช้ใน Java สามารถใส่อาร์ติแฟกต์ที่ได้มาไว้ที่ใดก็ได้
เราถือว่านี่เป็นฟีเจอร์ที่ไม่ดี แต่การกำจัดฟีเจอร์นี้ออกไปเป็นเรื่องยากมาก เนื่องจากช่วยประหยัดเวลาในการดำเนินการได้อย่างมาก เช่น เมื่อ ต้องประมวลผลไฟล์ต้นฉบับด้วยวิธีใดวิธีหนึ่ง และกฎหลายรายการอ้างอิงไฟล์นั้น (handwave-handwave) ซึ่งจะทำให้ใช้ RAM มากขึ้น เนื่องจากต้องจัดเก็บอินสแตนซ์ของการดำเนินการที่แชร์แต่ละรายการไว้ในหน่วยความจำแยกกัน
หากการดำเนินการ 2 อย่างสร้างไฟล์เอาต์พุตเดียวกัน การดำเนินการทั้ง 2 อย่างจะต้องเหมือนกันทุกประการ
มีอินพุตเดียวกัน เอาต์พุตเดียวกัน และเรียกใช้บรรทัดคำสั่งเดียวกัน ความสัมพันธ์สมมูลนี้ได้รับการติดตั้งใช้งานใน Actions.canBeShared()
และได้รับการยืนยันระหว่างระยะการวิเคราะห์และการดำเนินการโดยดูที่ทุกการดำเนินการ
ซึ่งจะใช้งานใน SkyframeActionExecutor.findAndStoreArtifactConflicts()
และเป็นหนึ่งในไม่กี่ที่ใน Bazel ที่ต้องมีมุมมอง "ส่วนกลาง" ของ
การสร้าง
ระยะการดำเนินการ
ซึ่งเป็นช่วงที่ Bazel เริ่มเรียกใช้การดำเนินการบิลด์จริง เช่น คำสั่งที่ สร้างเอาต์พุต
สิ่งแรกที่ Bazel ทำหลังจากระยะการวิเคราะห์คือการพิจารณาว่าต้องสร้างอาร์ติแฟกต์ใด ตรรกะสำหรับเรื่องนี้ได้รับการเข้ารหัสใน
TopLevelArtifactHelper
กล่าวโดยคร่าวคือเป็นfilesToBuild
ของ
เป้าหมายที่กำหนดค่าในบรรทัดคำสั่งและเนื้อหาของเอาต์พุตพิเศษ
กลุ่มเพื่อวัตถุประสงค์ที่ชัดเจนในการแสดง "หากเป้าหมายนี้อยู่ในบรรทัดคำสั่ง ให้สร้างอาร์ติแฟกต์เหล่านี้"
ขั้นตอนถัดไปคือการสร้างรูทการดำเนินการ เนื่องจาก Bazel มีตัวเลือกในการอ่านแพ็กเกจแหล่งที่มาจากตำแหน่งต่างๆ ในระบบไฟล์ (--package_path
) จึงต้องมีการดำเนินการที่เรียกใช้ในเครื่องพร้อมกับโครงสร้างแหล่งที่มาแบบเต็ม ซึ่งจัดการโดยคลาส SymlinkForest
และทำงานโดยจดบันทึกทุกเป้าหมาย
ที่ใช้ในระยะการวิเคราะห์ และสร้างโครงสร้างไดเรกทอรีเดียวที่ลิงก์สัญลักษณ์
ทุกแพ็กเกจกับเป้าหมายที่ใช้จากตำแหน่งจริง อีกทางเลือกหนึ่งคือการส่งเส้นทางที่ถูกต้องไปยังคำสั่ง (โดยคำนึงถึง --package_path
)
ซึ่งไม่พึงประสงค์เนื่องจากเหตุผลต่อไปนี้
- ซึ่งจะเปลี่ยนบรรทัดคำสั่งการดำเนินการเมื่อย้ายแพ็กเกจจากรายการเส้นทางแพ็กเกจ ไปยังอีกรายการหนึ่ง (ซึ่งเคยเกิดขึ้นบ่อย)
- ซึ่งจะทำให้เกิดบรรทัดคำสั่งที่แตกต่างกันหากมีการเรียกใช้การดำเนินการจากระยะไกล มากกว่าการเรียกใช้ในเครื่อง
- ต้องมีการแปลงบรรทัดคำสั่งที่เฉพาะเจาะจงกับเครื่องมือที่ใช้ (พิจารณาความแตกต่างระหว่างเส้นทางของคลาส Java และเส้นทางรวมของ C++ เป็นต้น)
- การเปลี่ยนบรรทัดคำสั่งของการดำเนินการจะทำให้รายการแคชการดำเนินการไม่ถูกต้อง
--package_path
กำลังจะถูกเลิกใช้งานอย่างช้าๆ และต่อเนื่อง
จากนั้น Bazel จะเริ่มเรียกใช้กราฟการดำเนินการ (กราฟแบบ 2 ส่วนที่มีทิศทาง
ซึ่งประกอบด้วยการดำเนินการและอาร์ติแฟกต์อินพุตและเอาต์พุตของการดำเนินการ) และเรียกใช้การดำเนินการ
การดำเนินการแต่ละอย่างจะแสดงด้วยอินสแตนซ์ของSkyValue
คลาสActionExecutionValue
เนื่องจากการเรียกใช้การดำเนินการมีค่าใช้จ่ายสูง เราจึงมีแคชหลายเลเยอร์ที่สามารถ เข้าถึงได้เบื้องหลัง Skyframe
ActionExecutionFunction.stateMap
มีข้อมูลที่ทำให้การรีสตาร์ท Skyframe ของActionExecutionFunction
มีราคาถูก- แคชการดำเนินการในเครื่องมีข้อมูลเกี่ยวกับสถานะของระบบไฟล์
- โดยปกติแล้วระบบการดำเนินการจากระยะไกลจะมีแคชของตัวเองด้วย
แคชการกระทำเกี่ยวกับสถานที่
แคชนี้เป็นอีกเลเยอร์ที่อยู่เบื้องหลัง Skyframe แม้ว่าจะมีการ ดำเนินการซ้ำใน Skyframe แต่ก็ยังอาจเป็นรายการที่ตรงกันในแคชการดำเนินการในเครื่อง ซึ่งแสดงถึงสถานะของระบบไฟล์ในเครื่องและจะได้รับการซีเรียลไลซ์ไปยังดิสก์ ซึ่งหมายความว่าเมื่อเริ่มต้นเซิร์ฟเวอร์ Bazel ใหม่ คุณจะได้รับการเข้าชมแคชการดำเนินการในเครื่องแม้ว่ากราฟ Skyframe จะว่างเปล่าก็ตาม
ระบบจะตรวจสอบแคชนี้เพื่อหาการเข้าชมโดยใช้วิธีการ
ActionCacheChecker.getTokenIfNeedToExecute()
ซึ่งต่างจากชื่อตรงที่มันคือแผนที่จากเส้นทางของอาร์ติแฟกต์ที่ได้มาไปยังการดำเนินการที่ปล่อยอาร์ติแฟกต์นั้น การดำเนินการมีคำอธิบายดังนี้
- ชุดไฟล์อินพุตและเอาต์พุตของงาน รวมถึงผลรวมตรวจสอบของไฟล์
- "คีย์การดำเนินการ" ซึ่งมักจะเป็นบรรทัดคำสั่งที่ดำเนินการ แต่โดยทั่วไปจะแสดงทุกอย่างที่ไม่ได้บันทึกโดยผลรวมตรวจสอบของไฟล์อินพุต (เช่น สำหรับ
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
ประเภทบริบทการดำเนินการจะระบุโดยClass
อินสแตนซ์ Java ที่อ้างอิงถึงอินเทอร์เฟซย่อยของActionContext
และอินเทอร์เฟซที่บริบทการดำเนินการต้องใช้ - ระบบจะเลือกบริบทการดำเนินการที่เหมาะสมจากบริบทที่มีอยู่และส่งต่อไปยัง
ActionExecutionContext
และBlazeExecutor
- Actions ขอบริบทโดยใช้
ActionExecutionContext.getContext()
และBlazeExecutor.getStrategy()
(จริงๆ แล้วควรมีวิธีเดียวในการทำเช่นนี้)
กลยุทธ์สามารถเรียกกลยุทธ์อื่นๆ เพื่อทำงานได้โดยไม่มีค่าใช้จ่าย ซึ่งใช้ในกลยุทธ์แบบไดนามิกที่เริ่มการดำเนินการทั้งในเครื่องและจากระยะไกล จากนั้นจะใช้กลยุทธ์ที่เสร็จสิ้นก่อน
กลยุทธ์ที่น่าสังเกตอย่างหนึ่งคือกลยุทธ์ที่ใช้กระบวนการทำงานแบบต่อเนื่อง
(WorkerSpawnStrategy
) แนวคิดคือเครื่องมือบางอย่างมีเวลาเริ่มต้นนาน
ดังนั้นจึงควรนำกลับมาใช้ซ้ำระหว่างการดำเนินการแทนที่จะเริ่มใหม่สำหรับ
ทุกการดำเนินการ (ซึ่งอาจทำให้เกิดปัญหาความถูกต้อง เนื่องจาก Bazel
อาศัยสัญญาของกระบวนการทำงานที่ว่ากระบวนการทำงานดังกล่าวจะไม่พกสถานะที่สังเกตได้
ระหว่างคำขอแต่ละรายการ)
หากเครื่องมือมีการเปลี่ยนแปลง คุณจะต้องรีสตาร์ทกระบวนการทำงาน ระบบจะพิจารณาว่าสามารถนำ Worker
กลับมาใช้ซ้ำได้หรือไม่โดยการคำนวณ Checksum สำหรับเครื่องมือที่ใช้โดยใช้
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
แต่เราขอแนะนำให้กฎ
สร้างไฟล์ความครอบคลุมพื้นฐานของตนเองที่มีเนื้อหาที่สื่อความหมายมากกว่า
เพียงชื่อไฟล์ต้นฉบับ
เราติดตามไฟล์ 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
offers ค่อนข้างไม่เป็นระเบียบ อย่า
ใช้เป็นตัวอย่างหลักการออกแบบที่ดี
Event Bus
วิธีหลักที่ BlazeModules สื่อสารกับส่วนอื่นๆ ของ Bazel คือผ่าน Event Bus
(EventBus
) ซึ่งจะสร้างอินสแตนซ์ใหม่ทุกครั้งที่มีการสร้าง ส่วนต่างๆ ของ Bazel
สามารถโพสต์เหตุการณ์ไปยัง Event Bus และโมดูลสามารถลงทะเบียน Listener สำหรับเหตุการณ์ที่สนใจได้
ตัวอย่างเช่น ระบบจะแสดงสิ่งต่อไปนี้เป็นเหตุการณ์
- ระบบได้กำหนดรายการเป้าหมายการสร้างที่จะสร้างแล้ว
(
TargetParsingCompleteEvent
) - ระบบได้กำหนดการกำหนดค่าระดับบนสุดแล้ว
(
BuildConfigurationEvent
) - สร้างเป้าหมายแล้ว ไม่ว่าจะสำเร็จหรือไม่ก็ตาม (
TargetCompleteEvent
) - มีการทดสอบ (
TestAttempt
,TestSummary
)
เหตุการณ์บางอย่างเหล่านี้แสดงอยู่นอก Bazel ใน
Build Event Protocol
(ซึ่งเป็น BuildEvent
) ซึ่งไม่เพียงแต่ช่วยให้ BlazeModule
s แต่ยังช่วยให้สิ่งต่างๆ
ภายนอกกระบวนการ 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
ซึ่งเรียกใช้RepositoryLoaderFunction
RepositoryLoaderFunction
ส่งต่อคำขอไปยัง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
และNativePosixFileSystem
ProcessUtils
WindowsFileOperations
และWindowsFileProcesses
com.google.devtools.build.lib.platform
เอาต์พุตของคอนโซล
การส่งเอาต์พุตของคอนโซลดูเหมือนจะเป็นเรื่องง่าย แต่การทำงานของ หลายกระบวนการ (บางครั้งจากระยะไกล) การแคชแบบละเอียด ความต้องการที่จะ มีเอาต์พุตของเทอร์มินัลที่สวยงามและมีสีสัน และการมีเซิร์ฟเวอร์ที่ทำงานเป็นเวลานานทำให้ เรื่องนี้ไม่ใช่เรื่องง่าย
ทันทีที่การเรียก RPC เข้ามาจากไคลเอ็นต์ ระบบจะสร้างอินสแตนซ์ 2 รายการRpcOutputStream
(สําหรับ stdout และ stderr) ซึ่งจะส่งต่อข้อมูลที่พิมพ์ลงใน
อินสแตนซ์เหล่านั้นไปยังไคลเอ็นต์ จากนั้นจะห่อหุ้มด้วย OutErr
(คู่ (stdout, stderr)
pair) ทุกอย่างที่ต้องพิมพ์ในคอนโซลจะผ่านสตรีมเหล่านี้ จากนั้นระบบจะส่งสตรีมเหล่านี้ไปยัง
BlazeCommandDispatcher.execExclusively()
เอาต์พุตจะพิมพ์ด้วยลำดับการหลีก ANSI โดยค่าเริ่มต้น หากไม่ต้องการใช้ (--color=no
) AnsiStrippingOutputStream
จะนำออก นอกจากนี้ ระบบจะเปลี่ยนเส้นทาง System.out
และ System.err
ไปยังสตรีมเอาต์พุตเหล่านี้
เพื่อให้สามารถพิมพ์ข้อมูลการแก้ไขข้อบกพร่องโดยใช้
System.err.println()
และยังคงแสดงในเอาต์พุตเทอร์มินัลของไคลเอ็นต์
(ซึ่งแตกต่างจากของเซิร์ฟเวอร์) เราจะดูแลให้มั่นใจว่าหากกระบวนการ
สร้างเอาต์พุตไบนารี (เช่น bazel query --output=proto
) จะไม่มีการดัดแปลง stdout
ข้อความสั้นๆ (ข้อผิดพลาด คำเตือน และอื่นๆ) จะแสดงผ่านEventHandler
อินเทอร์เฟซ โปรดทราบว่าสิ่งเหล่านี้แตกต่างจากสิ่งที่ผู้ใช้โพสต์ใน EventBus
(ซึ่งอาจทำให้สับสน) แต่ละ Event
มี EventKind
(ข้อผิดพลาด คำเตือน ข้อมูล และอื่นๆ) และอาจมี Location
(ตำแหน่งใน
ซอร์สโค้ดที่ทำให้เกิดเหตุการณ์)
การติดตั้งใช้งาน EventHandler
บางอย่างจะจัดเก็บเหตุการณ์ที่ได้รับ ซึ่งใช้เพื่อ
เล่นข้อมูลซ้ำใน UI ที่เกิดจากการประมวลผลที่แคชไว้ประเภทต่างๆ
เช่น คำเตือนที่แคชไว้ซึ่งกำหนดค่าเป้าหมายไว้
EventHandler
บางตัวยังอนุญาตให้โพสต์กิจกรรมซึ่งจะปรากฏใน
Event Bus (Event
ปกติจะ_ไม่_ปรากฏในนั้น) ซึ่งเป็นการติดตั้งใช้งาน ExtendedEventHandler
และมีไว้เพื่อเล่นเหตุการณ์ EventBus
ที่แคชไว้ซ้ำ EventBus
เหตุการณ์เหล่านี้ทั้งหมดใช้ Postable
แต่ไม่ใช่
ทุกอย่างที่โพสต์ไปยัง EventBus
จะใช้ส่วนติดต่อนี้
เฉพาะรายการที่แคชโดย ExtendedEventHandler
(จะดีมากและ
ส่วนใหญ่ก็เป็นเช่นนั้น แต่ไม่ได้บังคับ)
เอาต์พุตของเทอร์มินัลส่วนใหญ่จะส่งผ่าน UiEventHandler
ซึ่งมีหน้าที่
รับผิดชอบการจัดรูปแบบเอาต์พุตที่สวยงามทั้งหมดและการรายงานความคืบหน้าที่ Bazel
ทำ โดยมีอินพุต 2 รายการ ได้แก่
- Event Bus
- สตรีมเหตุการณ์ที่ส่งผ่านท่อไปยังเครื่องมือนี้ผ่าน Reporter
การเชื่อมต่อโดยตรงเพียงอย่างเดียวที่กลไกการดำเนินการคำสั่ง (เช่น ส่วนที่เหลือของ
Bazel) มีกับสตรีม RPC ไปยังไคลเอ็นต์คือผ่าน Reporter.getOutErr()
ซึ่งอนุญาตให้เข้าถึงสตรีมเหล่านี้ได้โดยตรง ใช้เฉพาะเมื่อคำสั่งต้องการ
ทิ้งข้อมูลไบนารีจำนวนมากที่เป็นไปได้ (เช่น bazel query
)
การทำโปรไฟล์ Bazel
Bazel ทำงานเร็ว นอกจากนี้ Bazel ยังช้าด้วย เนื่องจากบิลด์มักจะขยายขนาดจนถึงขีดจำกัดที่ยอมรับได้ ด้วยเหตุนี้ Bazel จึงมีโปรไฟล์เลอร์ที่ใช้เพื่อสร้างโปรไฟล์บิลด์และ Bazel เองได้ โดยจะมีการติดตั้งใช้งานในคลาสที่ชื่อ Profiler
โดยค่าเริ่มต้น ฟีเจอร์นี้จะเปิดอยู่ แต่จะบันทึกเฉพาะ
ข้อมูลที่ย่อแล้วเพื่อให้ค่าใช้จ่ายที่เพิ่มขึ้นอยู่ในระดับที่ยอมรับได้ ส่วนบรรทัดคำสั่ง
--record_full_profiler_data
จะทำให้บันทึกทุกอย่างที่ทำได้
โดยจะสร้างโปรไฟล์ในรูปแบบโปรไฟล์ของ Chrome ซึ่งดูได้ดีที่สุดใน Chrome รูปแบบข้อมูลของฟีเจอร์นี้คือสแต็กงาน ซึ่งผู้ใช้สามารถเริ่มและสิ้นสุดงานได้ และงานต่างๆ จะซ้อนกันอย่างเป็นระเบียบ แต่ละเธรด Java จะมี สแต็กงานของตัวเอง สิ่งที่ต้องทำ: วิธีนี้ทำงานร่วมกับการดำเนินการและ รูปแบบการส่งต่อการดำเนินการอย่างไร
Profiler จะเริ่มและหยุดทำงานใน 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
ได้ จากนั้นเมธอดตัวช่วยต่างๆ
จะขอเป้าหมายที่กำหนดค่า เปลี่ยนการกำหนดค่า และยืนยัน
สิ่งต่างๆ เกี่ยวกับผลลัพธ์ของการวิเคราะห์ได้