ฐานของโค้ด Bazel

รายงานปัญหา ดูแหล่งที่มา Nightly · 8.3 · 8.2 · 8.1 · 8.0 · 7.6

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

บทนำ

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

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

ซอร์สโค้ดเวอร์ชันสาธารณะของ Bazel อยู่ใน GitHub ที่ github.com/bazelbuild/bazel นี่ไม่ใช่ "แหล่งข้อมูลที่เชื่อถือได้" แต่เป็นข้อมูลที่ได้จากโครงสร้างแหล่งข้อมูลภายในของ Google ซึ่งมีฟังก์ชันการทำงานเพิ่มเติมที่ไม่เป็นประโยชน์ภายนอก Google เป้าหมายระยะยาวคือการทำให้ GitHub เป็นแหล่งข้อมูลที่เชื่อถือได้

เรายอมรับการมีส่วนร่วมผ่านกลไกคำขอดึงข้อมูลของ GitHub ตามปกติ และ Googler จะนำเข้าด้วยตนเองไปยังโครงสร้างแหล่งข้อมูลภายใน จากนั้น จะส่งออกกลับไปยัง GitHub

สถาปัตยกรรมไคลเอ็นต์/เซิร์ฟเวอร์

ส่วนใหญ่ของ Bazel จะอยู่ในกระบวนการของเซิร์ฟเวอร์ที่อยู่ใน RAM ระหว่างการสร้าง ซึ่งช่วยให้ Bazel รักษาสถานะระหว่างการสร้างได้

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

    bazel --host_jvm_args=-Xmx8G build -c opt //foo:bar

ตัวเลือกบางอย่าง (--host_jvm_args=) จะอยู่ก่อนชื่อคำสั่งที่จะเรียกใช้ และบางอย่างจะอยู่หลัง (-c opt) ตัวเลือกประเภทแรกเรียกว่า "ตัวเลือกการเริ่มต้น" และ ส่งผลต่อกระบวนการของเซิร์ฟเวอร์โดยรวม ในขณะที่ตัวเลือกประเภทหลัง ซึ่งก็คือ "ตัวเลือกคำสั่ง" จะส่งผลต่อคำสั่งเดียวเท่านั้น

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

ตัวเลือกบรรทัดคำสั่งสำหรับการเรียกใช้ Bazel อธิบายไว้ในออบเจ็กต์ OptionsParsingResult ซึ่งมีแผนที่จาก "option classes" ไปยังค่าของตัวเลือก "คลาสตัวเลือก" คือคลาสย่อยของ OptionsBase และจัดกลุ่มตัวเลือกบรรทัดคำสั่งที่เกี่ยวข้องเข้าด้วยกัน เช่น

  1. ตัวเลือกที่เกี่ยวข้องกับภาษาการเขียนโปรแกรม (CppOptions หรือ JavaOptions) ตัวเลือกเหล่านี้ควรเป็นคลาสย่อยของ FragmentOptions และจะรวมเข้ากับ ออบเจ็กต์ BuildOptions ในที่สุด
  2. ตัวเลือกที่เกี่ยวข้องกับวิธีที่ Bazel ดำเนินการ (ExecutionOptions)

ตัวเลือกเหล่านี้ออกแบบมาเพื่อใช้ในระยะการวิเคราะห์และ (ผ่าน RuleContext.getFragment() ใน Java หรือ ctx.fragments ใน Starlark) การตั้งค่าบางอย่าง (เช่น จะสแกนการรวม C++ หรือไม่) จะอ่านในระยะการดำเนินการ แต่ต้องมีการเชื่อมต่อที่ชัดเจนเสมอเนื่องจาก BuildConfiguration จะไม่พร้อมใช้งานในตอนนั้น ดูข้อมูลเพิ่มเติมได้ที่ส่วน "การกำหนดค่า"

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

Bazel จะทราบเกี่ยวกับคลาสตัวเลือกด้วยวิธีต่อไปนี้

  1. บางอย่างจะฝังอยู่ใน Bazel (CommonCommandOptions)
  2. จากคำอธิบายประกอบ @Command ในแต่ละคำสั่ง Bazel
  3. จาก ConfiguredRuleClassProvider (ตัวเลือกบรรทัดคำสั่งที่เกี่ยวข้อง กับภาษาโปรแกรมแต่ละภาษา)
  4. กฎ Starlark ยังกำหนดตัวเลือกของตัวเองได้ด้วย (ดูที่นี่)

ตัวเลือกแต่ละรายการ (ยกเว้นตัวเลือกที่กำหนดโดย Starlark) เป็นตัวแปรสมาชิกของคลาสย่อย FragmentOptions ที่มีคำอธิบายประกอบ @Option ซึ่งระบุชื่อและประเภทของตัวเลือกบรรทัดคำสั่งพร้อมกับข้อความช่วยเหลือบางส่วน

โดยปกติแล้ว ประเภท Java ของค่าตัวเลือกบรรทัดคำสั่งมักจะเป็นค่าที่เรียบง่าย (สตริง จำนวนเต็ม บูลีน ป้ายกำกับ ฯลฯ) อย่างไรก็ตาม เรายังรองรับตัวเลือกประเภทที่ซับซ้อนกว่าด้วย ในกรณีนี้ งานของการแปลงจากสตริงบรรทัดคำสั่งเป็นประเภทข้อมูลจะขึ้นอยู่กับการใช้งานของ com.google.devtools.common.options.Converter

โครงสร้างแหล่งที่มาตามที่ Bazel เห็น

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

ที่เก็บ

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

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

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

  • การแมปที่เก็บ
  • The registered toolchains
  • แพลตฟอร์มการดำเนินการที่ลงทะเบียน

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

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

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

  1. ไฟล์: สิ่งที่เป็นอินพุตหรือเอาต์พุตของการสร้าง ใน ภาษาของ Bazel เราเรียกสิ่งเหล่านี้ว่าอาร์ติแฟกต์ (จะกล่าวถึงในส่วนอื่น) ไฟล์ที่สร้างขึ้นระหว่างการบิลด์ไม่ได้เป็นเป้าหมายทั้งหมด โดยปกติแล้วเอาต์พุตของ Bazel จะไม่มีป้ายกำกับที่เชื่อมโยง
  2. กฎ: อธิบายขั้นตอนในการรับเอาต์พุตจากอินพุต โดยทั่วไปจะเชื่อมโยงกับภาษาโปรแกรม (เช่น cc_library, java_library หรือ py_library) แต่ก็มีบางภาษาที่ไม่ขึ้นกับภาษาใดภาษาหนึ่ง (เช่น genrule หรือ filegroup)
  3. กลุ่มแพ็กเกจ: อธิบายไว้ในส่วนระดับการเข้าถึง

ชื่อของเป้าหมายเรียกว่าป้ายกำกับ ไวยากรณ์ของป้ายกำกับคือ @repo//pac/kage:name โดย repo คือชื่อของที่เก็บที่มีป้ายกำกับ pac/kage คือไดเรกทอรีที่มีไฟล์ BUILD และ name คือเส้นทางของ ไฟล์ (หากป้ายกำกับอ้างอิงถึงไฟล์ต้นฉบับ) ที่สัมพันธ์กับไดเรกทอรีของ แพ็กเกจ เมื่ออ้างอิงถึงเป้าหมายในบรรทัดคำสั่ง คุณสามารถละเว้นบางส่วนของป้ายกำกับได้

  1. หากไม่ระบุที่เก็บ ระบบจะถือว่าป้ายกำกับอยู่ในที่เก็บหลัก
  2. หากละเว้นส่วนแพ็กเกจ (เช่น name หรือ :name) ระบบจะถือว่าป้ายกำกับอยู่ในแพ็กเกจของไดเรกทอรีการทำงานปัจจุบัน (ไม่อนุญาตให้ใช้เส้นทางแบบสัมพัทธ์ที่มีการอ้างอิงระดับบน (..))

กฎประเภทหนึ่ง (เช่น "ไลบรารี C++") เรียกว่า "คลาสกฎ" คลาสของกฎอาจ ได้รับการติดตั้งใช้งานใน Starlark (ฟังก์ชัน rule()) หรือใน Java (ที่เรียกว่า "กฎแบบเนทีฟ" ประเภท RuleClass) ในระยะยาว กฎเฉพาะภาษาทุกรายการจะได้รับการติดตั้งใช้งานใน Starlark แต่ตระกูลกฎเดิมบางรายการ (เช่น Java หรือ C++) ยังคงอยู่ใน Java ในขณะนี้

ต้องนำเข้าคลาสกฎ Starlark ที่จุดเริ่มต้นของไฟล์ BUILD โดยใช้คำสั่ง load() ในขณะที่คลาสกฎ Java จะเป็นที่รู้จักของ Bazel โดย "โดยธรรมชาติ" เนื่องจากมีการลงทะเบียนกับ ConfiguredRuleClassProvider

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

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

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

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

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

Starlark

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

การกำหนดค่า

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

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

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

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

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

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

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

คลาสที่เกี่ยวข้องคือ TransitionFactory และ ConfigurationTransition

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

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

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

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

ผู้ให้บริการข้อมูลการขนส่งสาธารณะ

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

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

  1. ออบเจ็กต์คลาส Java ซึ่งใช้ได้เฉพาะกับผู้ให้บริการที่เข้าถึงจาก Starlark ไม่ได้ ผู้ให้บริการเหล่านี้เป็นคลาสย่อยของ TransitiveInfoProvider
  2. สตริง นี่คือรูปแบบเดิมและไม่แนะนำอย่างยิ่งเนื่องจากอาจเกิด การตั้งชื่อซ้ำ ผู้ให้บริการข้อมูลแบบทรานซิทีฟดังกล่าวเป็นคลาสย่อยโดยตรงของ build.lib.packages.Info
  3. สัญลักษณ์ผู้ให้บริการ คุณสร้างได้จาก Starlark โดยใช้ฟังก์ชัน provider() และเป็นวิธีที่แนะนำในการสร้างผู้ให้บริการใหม่ สัญลักษณ์นี้แสดงโดยอินสแตนซ์ Provider.Key ใน Java

ผู้ให้บริการรายใหม่ที่ติดตั้งใช้งานใน Java ควรติดตั้งใช้งานโดยใช้ BuiltinProvider NativeProvider เลิกใช้งานแล้ว (เรายังไม่มีเวลาที่จะนำออก) และเข้าถึงคลาสย่อยของ TransitiveInfoProvider จาก Starlark ไม่ได้

เป้าหมายที่กำหนดค่า

ระบบจะใช้เป้าหมายที่กำหนดค่าเป็น RuleConfiguredTargetFactory มี คลาสย่อยสำหรับคลาสกฎแต่ละคลาสที่ใช้ใน Java เป้าหมายที่กำหนดค่า Starlark จะสร้างผ่าน StarlarkRuleConfiguredTargetUtil.buildRule()

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

  1. filesToBuild ซึ่งเป็นแนวคิดที่คลุมเครือของ "ชุดไฟล์ที่กฎนี้ แสดง" ไฟล์เหล่านี้จะสร้างขึ้นเมื่อเป้าหมายที่กำหนดค่า อยู่ในบรรทัดคำสั่งหรือใน srcs ของ genrule
  2. ไฟล์ที่เรียกใช้ ไฟล์ปกติ และไฟล์ข้อมูล
  3. กลุ่มเอาต์พุต "ชุดไฟล์อื่นๆ" ต่างๆ ที่กฎสร้างได้มีดังนี้ โดยจะเข้าถึงได้โดยใช้แอตทริบิวต์ output_group ของ กฎ filegroup ใน BUILD และใช้OutputGroupInfoใน Java

ไฟล์ที่เรียกใช้

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

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

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

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

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

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

ลักษณะ

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  • ชุดเครื่องมือที่ลงทะเบียน (ในไฟล์ MODULE.bazel และ การกำหนดค่า)
  • แพลตฟอร์มการดำเนินการและเป้าหมายที่ต้องการ (ในการกำหนดค่า)
  • ชุดประเภทเครื่องมือที่เป้าหมายที่กำหนดค่าไว้ต้องการ (ใน 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()

คุณระบุสภาพแวดล้อมที่รองรับสำหรับกฎได้หลายวิธี ดังนี้

  1. ผ่านแอตทริบิวต์ restricted_to= นี่คือรูปแบบที่ตรงที่สุดของ ข้อกำหนด โดยจะประกาศชุดสภาพแวดล้อมที่แน่นอนที่กฎรองรับ
  2. ผ่านแอตทริบิวต์ compatible_with= ประกาศสภาพแวดล้อมที่กฎรองรับนอกเหนือจากสภาพแวดล้อม "มาตรฐาน" ที่ระบบรองรับโดย ค่าเริ่มต้น
  3. ผ่านแอตทริบิวต์ระดับแพ็กเกจ default_restricted_to= และ default_compatible_with=
  4. ผ่านข้อกำหนดเริ่มต้นในกฎของ environment_group() สภาพแวดล้อมทุกรายการ จะอยู่ในกลุ่มของสภาพแวดล้อมที่เกี่ยวข้องตามธีม (เช่น "สถาปัตยกรรม CPU", "เวอร์ชัน JDK" หรือ "ระบบปฏิบัติการบนอุปกรณ์เคลื่อนที่") คำจำกัดความของกลุ่มสภาพแวดล้อมรวมถึงสภาพแวดล้อมใดที่ควรได้รับการ รองรับโดย "ค่าเริ่มต้น" หากไม่ได้ระบุไว้เป็นอย่างอื่นโดยแอตทริบิวต์ restricted_to= / environment() กฎที่ไม่มีแอตทริบิวต์ดังกล่าวจะรับค่าเริ่มต้นทั้งหมด
  5. ผ่านค่าเริ่มต้นของคลาสกฎ การดำเนินการนี้จะลบล้างค่าเริ่มต้นส่วนกลางสำหรับอินสแตนซ์ทั้งหมดของคลาสกฎที่ระบุ ตัวอย่างเช่น คุณสามารถใช้การตั้งค่านี้เพื่อทำให้*_testกฎทั้งหมดทดสอบได้โดยไม่ต้องให้แต่ละอินสแตนซ์ประกาศความสามารถนี้อย่างชัดเจน

environment() ได้รับการติดตั้งใช้งานเป็นกฎปกติ ส่วน environment_group() เป็นทั้งคลาสย่อยของ Target แต่ไม่ใช่ Rule (EnvironmentGroup) และฟังก์ชันที่พร้อมใช้งานโดยค่าเริ่มต้นจาก Starlark (StarlarkLibrary.environmentGroup()) ซึ่งจะสร้างเป้าหมายที่มีชื่อเดียวกันในที่สุด เพื่อหลีกเลี่ยงการขึ้นต่อกันแบบวงกลมซึ่งจะเกิดขึ้นเนื่องจากแต่ละ สภาพแวดล้อมต้องประกาศกลุ่มสภาพแวดล้อมที่ตนเป็นสมาชิก และแต่ละ กลุ่มสภาพแวดล้อมต้องประกาศสภาพแวดล้อมเริ่มต้นของตน

คุณจำกัดบิลด์ให้ใช้ได้ในสภาพแวดล้อมที่เฉพาะเจาะจงได้ด้วยตัวเลือกบรรทัดคำสั่ง --target_environment

การติดตั้งใช้งานการตรวจสอบข้อจำกัดอยู่ใน RuleContextConstraintSemantics และ TopLevelConstraintSemantics

ข้อจำกัดของแพลตฟอร์ม

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

ระดับการแชร์

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

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

โดยมีการใช้งานในตำแหน่งต่อไปนี้

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

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

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

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

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

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

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

สิ่งประดิษฐ์และการกระทำ

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

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

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

ไม่มีเหตุผลพื้นฐานที่อาร์ติแฟกต์แหล่งที่มาจะเป็นอาร์ติแฟกต์แบบทรีหรืออาร์ติแฟกต์ Symlink ที่ยังไม่ได้รับการแก้ไขไม่ได้ เพียงแต่เรายังไม่ได้นำไปใช้ (เราควรทำเช่นนั้น เนื่องจากอ้างอิงไดเรกทอรีแหล่งที่มาในไฟล์ 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()

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

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

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

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

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

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

โดยจะใช้เมธอดในการดำเนินการต่อไปนี้

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

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

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

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

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

วงจรของบริบทการดำเนินการมีดังนี้

  1. เมื่อเริ่มระยะการดำเนินการ ระบบจะถามอินสแตนซ์ BlazeModule ว่ามีบริบทการดำเนินการใดบ้าง ซึ่งเกิดขึ้นในตัวสร้างของ ExecutionTool ประเภทบริบทการดำเนินการจะระบุโดยClass อินสแตนซ์ Java ที่อ้างอิงถึงอินเทอร์เฟซย่อยของ ActionContext และอินเทอร์เฟซที่บริบทการดำเนินการต้องใช้
  2. ระบบจะเลือกบริบทการดำเนินการที่เหมาะสมจากบริบทที่มีอยู่และส่งต่อไปยัง ActionExecutionContext และ BlazeExecutor
  3. บริบทคำขอการดำเนินการโดยใช้ 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 อย่าง ได้แก่

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

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

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

การทดสอบ

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

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

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

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

การพิจารณาว่าจะเรียกใช้การทดสอบใด

การพิจารณาว่าจะทำการทดสอบใดเป็นกระบวนการที่ซับซ้อน

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

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

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

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

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

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

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

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

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

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

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

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

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

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

การรวบรวมความครอบคลุม

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

ระบบโมดูล

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

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

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

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

Event Bus

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

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

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

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

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

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

ไฟล์ WORKSPACE

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

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

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

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

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

ก่อนที่ Bazel จะใช้โค้ดของที่เก็บได้ คุณต้องดึงข้อมูลก่อน ซึ่งจะทำให้ Bazel สร้างไดเรกทอรีภายใต้ $OUTPUT_BASE/external/<repository name>

การดึงข้อมูลที่เก็บจะเกิดขึ้นในขั้นตอนต่อไปนี้

  1. PackageLookupFunction ตระหนักว่าต้องมีที่เก็บและสร้าง RepositoryName เป็น SkyKey ซึ่งเรียกใช้ RepositoryLoaderFunction
  2. RepositoryLoaderFunction ส่งต่อคำขอไปยัง RepositoryDelegatorFunction โดยไม่ทราบเหตุผล (โค้ดระบุว่าเพื่อ หลีกเลี่ยงการดาวน์โหลดซ้ำในกรณีที่ Skyframe รีสตาร์ท แต่ก็ไม่ใช่ เหตุผลที่หนักแน่นนัก)
  3. RepositoryDelegatorFunction จะค้นหากฎของที่เก็บที่ได้รับคำขอให้ดึงข้อมูลโดยการวนซ้ำในก้อนข้อมูลของไฟล์ WORKSPACE จนกว่าจะพบที่เก็บที่ขอ
  4. พบ RepositoryFunction ที่เหมาะสมซึ่งใช้ที่เก็บ การดึงข้อมูล โดยอาจเป็นการติดตั้งใช้งานที่เก็บใน Starlark หรือ แผนที่ที่ฮาร์ดโค้ดสำหรับที่เก็บที่ติดตั้งใช้งานใน Java

การแคชมีหลายเลเยอร์เนื่องจากการดึงข้อมูลที่เก็บอาจมีค่าใช้จ่ายสูงมาก

  1. มีแคชสำหรับไฟล์ที่ดาวน์โหลดซึ่งมีคีย์เป็นผลรวมตรวจสอบ (RepositoryCache) ซึ่งต้องมีผลรวมตรวจสอบในไฟล์ WORKSPACE แต่ก็ดีต่อการแยกตัวอยู่แล้ว ซึ่งอินสแตนซ์เซิร์ฟเวอร์ Bazel ทุกรายการในเวิร์กสเตชันเดียวกันจะแชร์ข้อมูลนี้ ไม่ว่าอินสแตนซ์จะทำงานในเวิร์กสเปซหรือเอาต์พุตเบสใดก็ตาม
  2. ระบบจะเขียน "ไฟล์เครื่องหมาย" สำหรับแต่ละที่เก็บภายใต้ $OUTPUT_BASE/external ซึ่งมีผลรวมตรวจสอบของกฎที่ใช้ในการดึงข้อมูล หากเซิร์ฟเวอร์ Bazel รีสตาร์ท แต่ผลรวมตรวจสอบไม่เปลี่ยนแปลง ระบบจะไม่ดึงข้อมูลอีก ฟีเจอร์นี้ ใช้งานได้ใน RepositoryDelegatorFunction.DigestWriter
  3. ตัวเลือกบรรทัดคำสั่ง --distdir จะกำหนดแคชอื่นที่ใช้เพื่อ ค้นหาสิ่งประดิษฐ์ที่จะดาวน์โหลด ซึ่งจะมีประโยชน์ในการตั้งค่าระดับองค์กร ที่ Bazel ไม่ควรดึงข้อมูลแบบสุ่มจากอินเทอร์เน็ต DownloadManager เป็นผู้ใช้ฟีเจอร์นี้

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

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

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

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

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

การแมปจะระบุไว้ในไฟล์ WORKSPACE เป็นrepo_mappingแอตทริบิวต์ ของคำนิยามที่เก็บแต่ละรายการ จากนั้นจะปรากฏใน Skyframe ในฐานะสมาชิกของ WorkspaceFileValue ซึ่งเชื่อมต่อกับ

  • Package.Builder.repositoryMapping ซึ่งใช้เพื่อเปลี่ยนรูปแบบแอตทริบิวต์ที่มีค่าป้ายกำกับ ของกฎในแพ็กเกจโดย RuleClass.populateRuleAttributeValues()
  • Package.repositoryMapping ซึ่งใช้ในระยะการวิเคราะห์ (สําหรับการแก้ไขสิ่งต่างๆ เช่น $(location) ซึ่งไม่ได้แยกวิเคราะห์ในระยะการโหลด)
  • BzlLoadFunction สำหรับการแก้ปัญหาป้ายกำกับในคำสั่ง load()

บิต JNI

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

โค้ด C++ อยู่ภายใต้ src/main/native และคลาส Java ที่มีเมธอดเนทีฟ คือ

  • NativePosixFiles และ NativePosixFileSystem
  • ProcessUtils
  • WindowsFileOperations และ WindowsFileProcesses
  • com.google.devtools.build.lib.platform

เอาต์พุตของคอนโซล

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

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

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

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

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

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

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

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

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

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

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

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

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

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

การทดสอบ Bazel

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

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

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

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

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