Bazel 程式碼集

回報問題 查看來源

本文件說明程式碼集和 Bazel 的結構。研究對像是願意為 Bazel 貢獻心力的人,而非使用者。

引言

Bazel 的程式碼集相當大 (約 350KLOC 生產程式碼和約 260 KLOC 測試程式碼),而且沒有人熟悉整個環境:每個人都非常瞭解自己的特定山谷,但幾乎沒有人知道每個方向的山丘上有哪些東西。

為了讓即使人在旅途中途中,又因為忘記走路而在森林暗處找不到自己,本文件會嘗試概略介紹程式碼集,讓您能更輕鬆地開始處理。

Bazel 的原始碼公開版本位於 GitHub 的 github.com/bazelbuild/bazel。這不是「可靠資料來源」;它衍生自 Google 內部來源樹狀結構,其中包含對 Google 以外來說不實用的額外功能。我們的長期目標是讓 GitHub 成為可靠資料來源。

系統會透過一般 GitHub 提取要求機制接受捐款,以及由 Google 員工手動匯入內部原始碼樹狀結構,然後再匯出回 GitHub。

用戶端/伺服器架構

大部分的 Bazel 存放在伺服器程序中,這些程序在各建構作業的 RAM 中。如此一來,Bazel 就能在建構之間維持狀態。

因此,Bazel 指令列有兩種選項:啟動和指令。使用類似下方的指令列:

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

某些選項 (--host_jvm_args=) 位於要執行的指令名稱之前,有些則位於 (-c opt);前者稱為「啟動選項」,會影響整個伺服器程序,而後者的「指令選項」只會影響單一指令。

每個伺服器執行個體都有一個相關聯的工作區 (來源樹狀結構的集合,稱為「存放區」),且每個工作區通常都有一個有效的伺服器執行個體。您可以指定自訂輸出基礎來規避這項操作 (詳情請參閱「目錄版面配置」一節)。

Bazel 會以單一 ELF 執行檔的形式發布,該執行檔也是有效的 .zip 檔案。當您輸入 bazel 時,系統會以 C++ (「用戶端」) 中實作的上述 ELF 執行檔進行控制。這項工具會按照下列步驟設定適當的伺服器程序:

  1. 檢查模型是否已自行擷取。如果沒有,則會自動關閉。這就是伺服器實作的來源。
  2. 檢查是否具備有效伺服器執行個體:運作正常、其啟動選項是否正確無誤,且使用正確的工作區目錄。它會查看 $OUTPUT_BASE/server 目錄,找出執行中的伺服器,其中存在鎖定檔案,其中包含伺服器監聽的通訊埠。
  3. 如有需要,終止舊伺服器程序
  4. 視需要啟動新的伺服器程序

適當的伺服器程序準備就緒後,需要執行的指令會透過 gRPC 介面通知該指令,然後 Bazel 的輸出內容就會交回至終端機。一次只能執行一個指令。實作方法是使用精細的鎖定機制搭配 C++ 的部分和 Java 中的部分。有些基礎架構可平行執行多個指令,因為無法同時執行 bazel version 和其他指令。主要攔截器是 BlazeModule 的生命週期和 BlazeRuntime 中的某些狀態。

指令結束時,Bazel 伺服器會傳輸用戶端應傳回的結束代碼。有趣的錯誤是 bazel run 的實作:這個指令的工作是執行剛建構的 Bazel,但由於沒有終端機,因此無法從伺服器程序執行此作業。因此會告訴用戶端它應採用的二進位 ujexec() 以及哪些引數。

按下 Ctrl-C 時,用戶端會將其轉譯為 gRPC 連線上的取消呼叫,嘗試盡快終止指令。在第三個 Ctrl-C 之後,用戶端改為將 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 伺服器執行個體。這通常位於 $OUTPUT_USER_ROOT/<checksum of the path to the workspace>。您可以使用 --output_base 啟動選項變更這項設定。這麼做有助於突破限制,在任何工作區只能執行一個 Bazel 執行個體。

輸出目錄包含下列內容:

  • 擷取的外部存放區位於 $OUTPUT_BASE/external
  • 執行根目錄,這個目錄包含連至目前建構作業所有原始碼的符號連結。地址位於$OUTPUT_BASE/execroot。在建構期間,工作目錄為 $EXECROOT/<name of main repository>。雖然這是長期計畫,因為這是非常不相容的變更,但我們計劃將其變更為 $EXECROOT
  • 在建構期間建立的檔案。

執行指令的程序

一旦 Bazel 伺服器取得控制權,並得知需要執行的指令時,就會發生下列事件:

  1. BlazeCommandDispatcher 收到新要求的相關資訊。它會決定指令是否需要工作區來執行 (幾乎所有指令,除了與原始碼無關的工作,例如版本或說明例外的指令除外),以及另一個指令是否正在執行。

  2. 已找到正確指令。每個指令都必須實作 BlazeCommand 介面,且必須具備 @Command 註解 (這部分為反模式,如果所有中繼資料透過 BlazeCommand 中的方法描述指令需要的所有中繼資料會很實用)

  3. 剖析指令列選項。每個指令都有不同的指令列選項,詳情請參閱 @Command 註解。

  4. 事件匯流排建立完成。事件匯流排是建構期間發生的事件的串流。系統會根據建構事件通訊協定的評估,將部分項目匯出至 Bazel 外部,讓全世界瞭解建構作業的執行情形。

  5. 指令會受控制。最有趣的指令是執行建構的指令:建構、測試、執行、涵蓋範圍等:這項功能是由 BuildTool 實作。

  6. 系統會剖析指令列上的目標模式組合,並解析 //pkg:all//pkg/... 等萬用字元。這會在 AnalysisPhaseRunner.evaluateTargetPatterns() 中實作,並在 SkyFrame 中以 TargetPatternPhaseValue 的形式重新調整。

  7. 系統會執行載入/分析階段以產生動作圖表 (也就是必須針對建構執行的指令的有向循環圖)。

  8. 執行階段將會執行。這意味著,我們會執行必要的所有動作以建構要求的頂層目標。

指令列選項

OptionsParsingResult 物件會說明 Bazel 叫用的指令列選項,這個物件也會包含「選項類別」與選項值的對應。「選項類別」是 OptionsBase 的子類別,將彼此相關的指令列選項分組在一起。例如:

  1. 與程式設計語言 (CppOptionsJavaOptions) 相關的選項。這些應為 FragmentOptions 的子類別,最後會納入 BuildOptions 物件中。
  2. Bazel 執行動作的方式相關選項 (ExecutionOptions)

這些選項可以在分析階段使用,以及 (透過 Java 中的 RuleContext.getFragment() 或 Starlark 中的 ctx.fragments) 使用。其中部分 (例如是否執行 C++ 包含掃描) 會在執行階段中讀取,但由於 BuildConfiguration 尚未提供,一律需要明確的水電處理。詳情請參閱「設定」一節。

警告:我們想假設 OptionsBase 執行個體不可變更,並且以這種方式使用 (例如 SkyKeys 的一部分)。這並非情況,修改 Bazel 是利用難以偵錯的細微方式破壞 Bazel。遺憾的是,使這些元素實際上無法變更。(在建構完成後立即修改 FragmentOptions,使其有機會在有人對其進行參照,並在呼叫 equals()hashCode() 之前就可進行呼叫)。

Bazel 透過以下方式瞭解選項類別:

  1. 有些裝置可透過固定式線連接 Bazel (CommonCommandOptions)
  2. 在每個 Bazel 指令的 @Command 註解中
  3. ConfiguredRuleClassProvider (這些是與個別程式設計語言相關的指令列選項)
  4. Starlark 規則也可以定義自己的選項 (請參閱這裡)

每個選項 (但 Starlark 定義選項除外) 都是 FragmentOptions 子類別的成員變數,具有 @Option 註解,該子類別指定了指令列選項的名稱、類型及一些說明文字。

指令列選項的 Java 類型通常是簡單的 (字串、整數、布林值、標籤等)。不過,我們也支援較複雜類型的選項;在此情況下,從指令列字串轉換為資料類型的工作也可採用 com.google.devtools.common.options.Converter 的實作。

Bazel 所見來源樹狀結構

Bazel 在建構軟體時負責讀取及解讀原始碼,Bazel 在原始碼上運作的總數稱為「工作區」,其結構為存放區、套件和規則。

存放區

「存放區」是開發人員運作的來源樹狀結構,通常代表單一專案。Bazel 的祖系 Blaze 於單聲道存放區運作,也就是單一原始碼樹狀結構,其中包含用於執行建構作業的所有原始碼。Bazel 則支援原始碼橫跨多個存放區的專案。叫用 Bazel 的存放區稱為「主存放區」,其他存放區則稱為「外部存放區」。

存放區會在其根目錄中,以存放區邊界檔案 (MODULE.bazelREPO.bazel,或舊版環境、WORKSPACEWORKSPACE.bazel) 標示。主要存放區是您叫用 Bazel 的來源樹狀結構。外部存放區以各種方式定義,詳情請參閱外部依附元件總覽

外部存放區的程式碼是透過 $OUTPUT_BASE/external 進行符號連結或下載。

執行建構作業時,整個原始碼樹狀結構必須分離;這會由 SymlinkForest 執行,將主要存放區中的每個套件與 $EXECROOT 以及每個外部存放區的符號連結至 $EXECROOT/external$EXECROOT/..

套裝組合

每個存放區都包含套件、相關檔案集合和依附元件規格。這些類型是由名為 BUILDBUILD.bazel 的檔案指定。如果兩者同時存在,Bazel 偏好使用 BUILD.bazel;這是因為 Bazel 的祖系 Blaze 就是使用此檔案名稱,因為系統仍接受 BUILD 檔案。然而,這已經成為常用的路徑片段,尤其是在 Windows 上,因為檔案名稱不區分大小寫。

套件彼此獨立:變更套件的 BUILD 檔案不會導致其他套件變更。新增或移除 BUILD 檔案_可以_變更其他套件,因為遞迴 glob 會在套件邊界停止,因此存在 BUILD 檔案會停止遞迴。

BUILD 檔案的評估稱為「套件載入」。此方法已在 PackageFactory 類別中實作,藉由呼叫 Starlark 解譯器,並需要瞭解可用的規則類別組合。套件載入的結果為 Package 物件。主要是指從字串 (目標名稱) 到目標本身的對應。

載入套件時,出現一大塊複雜性問題:Bazel 不需要明確列出每個來源檔案,而且可以執行 glob (例如 glob(["**/*.java"]))。與殼層不同的是,它支援遞向子目錄的遞迴 glob,而非子套件。這需要存取檔案系統,由於處理速度可能相當慢,因此我們實施各種技巧,使它能夠同時以有效率的方式同時執行。

Globbing 會在下列類別中實作:

  • LegacyGlobber,快速便捷的 SkyFrame 無法感知的纖維
  • SkyframeHybridGlobber,這是使用 SkyFrame 並還原為舊版 globber 的版本,可避免發生「出現畫面更新重新啟動」的情況 (如下所述)

Package 類別本身包含部分成員,專門用於剖析「外部」套件 (與外部依附元件相關),而且不適用於實際套件。這是設計瑕疵,因為說明一般套件的物件不應包含描述其他內容的欄位。包括:

  • 存放區對應
  • 已註冊的工具鍊
  • 已註冊的執行平台

在理想情況下,剖析「外部」套件與剖析一般套件之間會有更多區隔,讓 Package 不必同時滿足兩者的需求。這樣做並不容易,因為這兩者之間的深度互動相當深。

標籤、指定目標和規則

套件是由目標組成,具有下列類型:

  1. 檔案:輸入或建構輸出內容的元件。在 Bazel Parlance 中,我們會稱為「Artifacts」 (在其他地方討論)。建構期間建立的不是所有檔案都是目標。Bazel 輸出內容通常會沒有相關聯的標籤,
  2. 規則:以下步驟說明如何從輸入中擷取輸出。通常與程式設計語言 (例如 cc_libraryjava_librarypy_library) 相關聯,但有些語言不通 (例如 genrulefilegroup)
  3. 套件群組:請參閱「瀏覽權限」一節。

目標的名稱稱為標籤。標籤的語法為 @repo//pac/kage:name,其中 repo 是標籤所在的存放區名稱,pac/kage 是其 BUILD 檔案所在的目錄,name 則是檔案 (如果標籤參照來源檔案) 相對於套件目錄的路徑。在指令列中參照目標時,標籤的某些部分可以省略:

  1. 如果省略存放區,標籤會位於主要存放區中。
  2. 如果省略套件部分 (例如 name:name),系統會將標籤置於目前工作目錄的套件中 (不允許包含提升層級參照 (..) 的相對路徑)

其中一種規則 (例如「C++ 程式庫」) 稱為「規則類別」。規則類別可以在 Starlark (rule() 函式) 或 Java (稱為「原生規則」,類型為 RuleClass) 中實作。長期來說,所有語言專屬規則都會在 Starlark 中實作,但某些舊版規則系列 (例如 Java 或 C++) 目前仍在 Java 中。

您需要使用 load() 陳述式,在 BUILD 檔案開頭匯入 Starlark 規則類別,而 Java 規則類別則是由 Bazel 所註冊,因為 Java 規則類別已由 ConfiguredRuleClassProvider 註冊。

規則類別包含以下資訊:

  1. 其屬性 (例如 srcsdeps):其類型、預設值、限制等。
  2. 每個屬性所連結的設定轉換作業和切面 (如有)
  3. 規則的導入方式
  4. 「通常」規則會建立遞移資訊提供者

術語附註:在程式碼集中,我們通常會使用「規則」來表示由規則類別建立的目標。但在 Starlark 和麵向使用者的說明文件中,「規則」只應用於參照規則類別本身,而目標只是「目標」。另請注意,儘管 RuleClass 的名稱中有「class」,但規則類別與該類型的目標之間並沒有 Java 繼承關係。

空架

基礎 Bazel 的評估架構稱為 SkyFrame。它的模型是,建構期間需要建構的所有內容都會整理成有向非循環圖,而邊緣則指向任何資料片段或依附元件,也就是建構這個模型時需要知道的其他資料片段。

圖表中的節點稱為 SkyValue,其名稱稱為 SkyKey。兩者都極為不可變更;只有不可變更的物件應該從這些物件存取。這幾乎隨時都能保持不變,但如果個別選項類別 BuildOptions 屬於 BuildConfigurationValue 及其 SkyKey 的成員,我們會盡量不要變更這些選項,或僅以無法從外部觀測方式變更的方式變更選項。因此,從 SkyFrame 計算的所有內容 (例如設定的目標) 也必須不可變更。

觀察 SkyFrame 圖表最方便的方式就是執行 bazel dump --skyframe=deps,這會傾印圖表,每行一個 SkyValue。建議您對較小的版本執行此操作,因為這樣可能會非常龐大。

SkyFrame 位於 com.google.devtools.build.skyframe 套件中。名稱類似的套件 com.google.devtools.build.lib.skyframe 包含在 SkyFrame 之上實作 Bazel。如要進一步瞭解 SkyFrame,請參閱這裡

為了將指定的 SkyKey 評估為 SkyValue,SkyFrame 會叫用與金鑰類型對應的 SkyFunction。在函式評估期間,可能會呼叫 SkyFunction.Environment.getValue() 的各種超載,向 SkyFrame 要求其他依附元件。這會產生將這些依附元件註冊到 SkyFrame 的內部圖形中,如此一來,SkyFrame 就能在任何依附元件變更時重新評估該函式。換句話說,SkyFrame 的快取和漸進式運算作業是以 SkyFunctionSkyValue 的精細程度執行。

每當 SkyFunction 要求無法使用的依附元件時,getValue() 會傳回空值。接著,函式應自行傳回空值,將控制項傳回 SkyFrame。稍後,SkyFrame 會評估無法使用的依附元件,然後從頭開始重新啟動函式;只有這次 getValue() 呼叫會成功執行非空值的結果。

這樣會導致在重新啟動前,在 SkyFunction 內執行的任何運算都必須重複。但這不包括評估用於快取依附元件 SkyValues 的工作。因此,我們通常會透過以下方式解決這個問題:

  1. 使用 getValuesAndExceptions() 分批宣告依附元件,藉此限制重新啟動次數。
  2. SkyValue 拆分為由不同 SkyFunction 計算的個別片段,以便獨立計算和快取。由於這麼做可能會增加記憶體用量,因此請策略性地完成。
  3. 在重新啟動之間儲存狀態,可以使用 SkyFunction.Environment.getState(),或用臨時靜態快取在「SkyFrame 後方」後方。由於 SkyFunctions 相當複雜,因此重新啟動之間的狀態管理可能會變得困難,因此導入 StateMachine 可用於結構化並行處理方法,包括在 SkyFunction 中暫停和繼續階層運算的掛鉤。範例:DependencyResolver#computeDependencies 使用 StateMachine 搭配 getState(),計算設定目標的可能龐大直接依附元件組合,否則可能導致高昂的重新啟動作業。

基本上,Bazel 需要這些類型的解決方案,原因是從 2023 年起,就有數十萬個飛行中 SkyFrame 節點很常見,且 Java 對輕量執行緒的支援不超越 StateMachine 實作。

史塔拉克

Starlark 是使用者設定和擴充 Bazel 時所用的特定網域語言。它屬於受限制的 Python 子集,其類型大幅較少,對控制流程的限制更加嚴格,最重要的是,保證能啟用並行讀取。這不是圖靈完成 (Turing-complete) 也會導致部分 (但並非所有) 使用者嘗試在語言內部完成一般程式設計工作。

Starlark 是在 net.starlark.java 套件中實作。此外,這裡也有獨立的 Go 實作項目。在 Bazel 中使用的 Java 實作目前為解譯器。

Starlark 可用於許多情境,包括:

  1. BUILD 個檔案。您可以在這個位置定義新的建構目標。在結構定義中執行的 Starlark 程式碼只能存取 BUILD 檔案本身的內容,以及該檔案所載入的 .bzl 檔案。
  2. MODULE.bazel 檔案。這是定義外部依附元件的位置。在這個結構定義中執行的 Starlark 程式碼僅對幾個預先定義的指令有相當有限的存取權。
  3. .bzl 個檔案。您可以在這裡定義新的建構規則、存放區規則、模組擴充功能。這裡的 Starlark 程式碼可以定義新函式,並從其他 .bzl 檔案載入。

BUILD.bzl 檔案適用的方言稍有不同,因為兩者表達的方式不同。如要查看差異清單,請按這裡

如要進一步瞭解 Starlark,請參閱這篇文章

載入/分析階段

根據載入/分析階段,Bazel 判斷建構特定規則所需的動作。其基本單位是「設定的目標」,相當合理,也就是 (目標、設定) 的組合。

這稱為「載入/分析階段」,因為這個階段可以分成兩個不同的部分,過去是序列化,但現在能在時間重疊:

  1. 載入套件,也就是將 BUILD 檔案轉換成代表這些套件的 Package 物件
  2. 分析設定的目標,也就是執行規則實作以產生動作圖表

在指令列上要求所設定目標之後,每個已設定的目標都必須由下而上分析;也就是說,分葉節點和指令列中的設定目標會先由下而上分析。單一設定目標分析的輸入值如下:

  1. 設定。(「如何」建構該規則;例如目標平台,以及使用者想傳遞至 C++ 編譯器的指令列選項等項目)
  2. 直接依附元件。這些遞移資訊提供者可供要分析的規則使用。會呼叫這些函式,是因為它們可在已設定目標的遞移關閉期間提供資訊的「綜覽」,例如類別路徑上的所有 .jar 檔案,或是需要連結至 C++ 二進位檔的所有 .o 檔案)
  3. 目標本身。這是載入目標套件的結果。對規則來說,這也包含其屬性,通常是這些屬性。
  4. 設定目標的實作方式。至於規則,這可以位於 Starlark 或 Java 中。所有非規則設定的目標則會在 Java 中導入。

分析設定的目標後,輸出結果如下:

  1. 設定具有依附關係的遞移資訊提供者
  2. 系統可以建立的構件和產生這些構件的動作。

提供給 Java 規則的 API 為 RuleContext,相當於 Starlark 規則的 ctx 引數。此 API 的功能更加強大,但同時也能執行 Bad ThingsTM 功能 (例如編寫時間或空間複雜性屬於二次性 (或更糟) 的程式碼),讓 Bazel 伺服器當機並發生 Java 例外狀況,或者違反不變數 (例如意外修改 Options 執行個體,或設定可變的目標可變動)

判斷設定目標直接依附元件的演算法保存在 DependencyResolver.dependentNodeMap() 中。

設定

設定是指建構目標的「方式」,適用於哪個平台、指令列選項等。

同一個版本可以針對多項設定建立相同的目標。舉例來說,如果相同的程式碼用於在建構期間執行的工具和目標程式碼上執行的工具,而且我們正在跨平台編譯或建構笨重的 Android 應用程式 (其中含有適用於多個 CPU 架構的原生程式碼),這項功能就會很實用。

概念上來說,設定是 BuildOptions 執行個體。不過,實際上,BuildOptions 會納入 BuildConfiguration,可提供額外的額外功能。它會從依附元件圖表頂端傳播到底部。如果版本有變,就需要重新分析建構。

這樣會導致異常狀況。舉例來說,如果要求的測試執行次數有所變更,即使只有測試目標會影響測試目標 (我們已計劃「修剪」設定,所以只是尚未準備就緒),

如果規則實作需要部分設定,需要使用 RuleClass.Builder.requiresConfigurationFragments() 在定義中宣告。這樣做是為了避免錯誤 (例如使用 Java 片段的 Python 規則) 和協助調整設定,例如當 Python 選項變更時,C++ 目標無需重新分析。

規則的設定不一定會與規則的「父項」規則設定相同。在依附元件邊緣中變更設定的程序稱為「設定轉換」。相關作業可能在以下兩個位置發生:

  1. 位於依附元件邊緣。這些轉場效果會在 Attribute.Builder.cfg() 中指定,同時也是從 Rule (轉換發生時) 和 BuildOptions (原始設定) 到一或多個 BuildOptions (輸出設定) 的函式。
  2. 在任何傳入邊緣至已設定的目標上。這些項目會在 RuleClass.Builder.cfg() 中指定。

相關類別為 TransitionFactoryConfigurationTransition

設定轉換的用途如下:

  1. 為了宣告特定依附元件會在建構期間使用,因此應在執行架構中建構
  2. 如要宣告,必須針對多個架構建構特定依附元件 (例如脂肪 Android APK 中的原生程式碼)

如果設定轉換會產生多項設定,則稱為「分割轉換」

您也可以在 Starlark 中實作設定轉換 (請參閱這裡的說明文件)

遞移資訊提供者

遞移資訊供應器是設定目標的一種方法 (和「只」_way),可用來傳達其他已設定的目標。名稱之所以採用「遞移」的原因,通常是因為所設定目標的過渡性關閉作業。

一般來說,Java 遞移資訊供應器和 Starlark 兩者之間會有 1:1 的對應 (例外為 DefaultInfo,這是 FileProviderFilesToRunProviderRunfilesProvider 的正規化方法,因為該 API 認為 API 比 Java 的直接音譯更複雜)。其中的關鍵在於:

  1. Java 類別物件。這僅適用於無法透過 Starlark 存取的供應商。這些提供者是 TransitiveInfoProvider 的子類別。
  2. 字串。這是舊版且極力不建議使用,因為很容易有名稱衝突。這類遞移資訊提供者是 build.lib.packages.Info 的直接子類別。
  3. 供應商符號。這項操作可使用 provider() 函式從 Starlark 建立,因此是建立新供應商的建議方式。在 Java 中,這個符號以 Provider.Key 執行個體表示。

使用 Java 實作的新供應商應使用 BuiltinProvider 實作。NativeProvider 已淘汰 (還沒有時間移除),且無法透過 Starlark 存取 TransitiveInfoProvider 子類別。

已設定的目標

已設定的目標會實作為 RuleConfiguredTargetFactory。以 Java 實作的每個規則類別都有一個子類別。Starlark 設定的目標是透過 StarlarkRuleConfiguredTargetUtil.buildRule() 建立。

已設定的目標工廠應使用 RuleConfiguredTargetBuilder 建構傳回值。內容如下:

  1. filesToBuild,即「這項規則代表的檔案組合」的朦朧概念。這些是設定的目標位於指令列或 genrule 的 srcs 時建構的檔案。
  2. 其執行檔案、一般和資料。
  3. 他們的輸出群組。這些是規則可建構的不同「其他檔案組合」。如要存取這類屬性,請使用 BUILD 檔案群組規則的 output_group 屬性,並在 Java 中使用 OutputGroupInfo 提供者。

執行檔案

部分二進位檔需要資料檔案才能執行。最明顯的例子是需要輸入檔案的測試。在 Bazel 中,這會以「執行檔案」的概念表示。「執行檔案樹狀結構」是指特定二進位檔的資料檔案的目錄樹狀結構。在檔案系統中,這會以符號連結樹狀結構的形式建立,且個別符號連結指向輸出樹狀結構中的檔案。

一組執行檔案會表示為 Runfiles 執行個體。從概念上來說,這是從執行檔案樹狀結構中檔案路徑到代表該檔案的 Artifact 執行個體之間的對應。它比單一 Map 複雜一些,原因如下:

  • 在多數情況下,檔案的執行檔案路徑與執行路徑相同。我們利用此程式庫節省一些 RAM。
  • 執行檔案樹狀結構有各種舊版項目,也需要表示這些項目。

使用 RunfilesProvider 收集執行檔案:此類別的例項代表已設定的目標 (例如程式庫) 及其遞移需求,且會依照巢狀結構集進行 (實際上,這些目標會使用其依附元件的巢狀集實作):每個目標會聯集其依附元件的執行檔案,新增一些專屬檔案,然後將產生的檔案在依附元件圖表中傳送。RunfilesProvider 執行個體包含兩個 Runfiles 執行個體,一個用於透過「資料」屬性依附規則時,另一個則用於所有其他類型的傳入依附元件。這是因為如果依附於資料屬性,目標有時會顯示不同的執行檔案。這是我們尚未移除的並不需要的舊版行為。

二進位檔的執行檔案會以 RunfilesSupport 的執行個體表示。這與 Runfiles 不同,因為 RunfilesSupport 實際上具有建構功能 (與僅用於對應的 Runfiles 不同)。此操作需要下列額外元件:

  • 輸入的執行檔案資訊清單。這是執行檔案樹狀結構的序列化說明。這項工具會做為執行檔案樹狀結構內容的 Proxy,而 Bazel 會假設執行檔案樹狀結構只有在資訊清單內容有所變更時,才會變更執行檔案樹狀結構。
  • 輸出執行檔案資訊清單。處理檔案樹狀結構的執行階段程式庫主要使用於此,特別是 Windows 系統,後者有時不支援符號連結。
  • 執行檔案中間人。為了要存在執行檔案樹狀結構,您必須建構符號連結樹狀結構,以及符號連結指向的成果。為了減少依附元件邊緣的數量,執行檔案中間人可用來代表所有這些元件。
  • 指令列引數,用於執行 RunfilesSupport 物件代表的執行檔案。

切面

切面是一種「在相依關係圖的計算結果」的方法。如需 Bazel 使用者的說明,請參閱這裡。以下為通訊協定緩衝區的一個不錯的例子:proto_library 規則不應知道任何特定語言,但透過任何程式設計語言建構通訊協定緩衝區訊息 (通訊協定緩衝區的「基本單位」) 應結合使用 proto_library 規則,這樣當相同語言中的兩個目標依附於同一個通訊協定緩衝區時,系統只會建構一次。

和設定的目標一樣,SkyFrame 會以 SkyValue 的形式呈現這些目標,且建構方式與設定目標非常類似:他們有名為 ConfiguredAspectFactory 的工廠類別,且該類別可存取 RuleContext,但與設定的目標工廠不同,它也知道其附加至設定的目標及其供應商。

依附元件圖表會隨各個屬性使用 Attribute.Builder.aspects() 函式來指定。以下為幾個名稱命名的類別會參與這個程序:

  1. AspectClass 是該切面的實作。它可以是 Java (例如子類別) 或 Starlark (在本例中為 StarlarkAspectClass 的執行個體)。它類似於 RuleConfiguredTargetFactory
  2. AspectDefinition 是面向的定義,包含所需的供應器、提供的供應器,並包含其實作的參照,例如適當的 AspectClass 執行個體。與 RuleClass 類似。
  3. AspectParameters 可讓您參數對向下傳播依附元件圖表的某個面向進行參數處理。目前是字串對應字串。通訊協定緩衝區的好處是通訊協定緩衝區的一個好處:如果語言有多個 API,則應將通訊協定緩衝區建構目標的 API 往依附元件圖傳播至哪個 API。
  4. Aspect 代表計算依附依附元件圖表的某個面向所需的所有資料。其中包含顯示類別、定義和參數。
  5. RuleAspect 是一種函式,可判斷特定規則應傳播的層面。這是 Rule -> Aspect 函式。

非預期的小工具是,它可以附加至其他方面;舉例來說,收集 Java IDE 類別路徑的部分可能會想瞭解類別路徑中的所有 .jar 檔案,但其中一些是通訊協定緩衝區。在這種情況下,IDE 部分會想附加至 (proto_library 規則 + Java proto 顯示內容) 組合。

與層面的複雜度有關,可採用 AspectCollection 類別擷取。

平台和工具鍊

Bazel 支援多平台建構作業,也就是說,建構作業可能有多個架構會執行建構動作,而建構動作則採用多個架構。在 Bazel Parlance 中,這些架構稱為「平台」 (完整說明文件請參閱這裡)

平台是由限制設定 (例如「CPU 架構」的概念) 與「限制值」 (例如特定 CPU,例如 x86_64) 的鍵/值對應描述。我們在 @platforms 存放區中提供常用的限制設定和值「字典」,

「工具鍊」的概念源自於以下事實:視建構的執行平台和目標平台而定,可能需要使用不同的編譯器。舉例來說,特定的 C++ 工具鍊可能會在特定 OS 上執行,且能夠以其他 OS 為目標。Bazel 必須根據設定的執行作業和目標平台決定使用的 C++ 編譯器 (這裡的工具鍊說明文件)。

為了達到這個目的,工具鍊會標註其支援的執行和目標平台限制。為了達到這個目的,工具鍊的定義分為兩個部分:

  1. 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) 中)
  • UnloadedToolchainContextKey 中設定的目標 (exec_compatible_with 屬性) 和設定 (--experimental_add_exec_constraints_to_targets) 的執行平台限制組合

最終會產生 UnloadedToolchainContext,基本上是從工具鍊類型 (以 ToolchainTypeInfo 執行個體) 到所選工具鍊標籤的對應。由於其中並未包含工具鍊本身,只有標籤,因此稱為「卸載」。

接著,工具鍊會使用 ResolvedToolchainContext.load() 載入,並在已要求的目標實作中使用該工具鍊。

我們也擁有一個舊版系統,其必須使用單一「主機」設定,以及由各種設定旗標代表的目標設定,例如 --cpu。我們正在逐步轉換至上述系統。為處理使用者依賴舊版設定值的情況,我們實作了平台對應,以便在舊版標記和新式平台限制之間進行轉譯。這些程式碼位於 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 指令列選項,將建構作業限制在特定環境中。

限制檢查的實作位於 RuleContextConstraintSemanticsTopLevelConstraintSemantics

平台限制

目前「官方」方式表示與目標相容的平台,做法是使用與工具鍊和平台相同的限制來描述。這項產品正在對提取要求 #10945 進行審核。

顯示設定

如果您與眾多開發人員 (例如 Google) 合作處理大型程式碼集,必須注意避免其他使用者因程式碼而異。否則,根據Hyrum 的法律規定,人們「會」仰賴您視為實作詳細資料的行為。

Bazel 透過稱為「瀏覽權限」的機制支援這一點:您可以透過瀏覽權限屬性宣告特定目標只能依附於特定目標。這個屬性不太特別,因為雖然其中包含標籤清單,但這些標籤可能會將模式編碼套用至套件名稱,而不是以指標指向任何特定目標。(這是設計瑕疵)。

這會在以下位置實作:

  • RuleVisibility 介面代表瀏覽權限宣告。它可以是常數 (完全公開或完全不公開) 或標籤清單。
  • 標籤可以參照套件群組 (預先定義的套件清單),也可以直接參照套件 (//pkg:__pkg__) 或套件的子樹狀結構 (//pkg:__subpackages__)。這與使用 //pkg:*//pkg/... 的指令列語法不同。
  • 套件群組會實作做為各自的目標 (PackageGroup) 和設定目標 (PackageGroupConfiguredTarget)。如有需要,我們可以將這些群組替換為簡單的規則。實作這些邏輯時,您會用到 PackageSpecification (對應於 //pkg/... 等單一模式);PackageGroupContents 對應到單一 package_grouppackages 屬性;以及 PackageSpecificationProvider 匯總 package_group 和其遞移 includes
  • 從瀏覽權限標籤清單轉換為依附元件的作業會在 DependencyResolver.visitTargetVisibility 和其他幾個位置完成。
  • 實際檢查作業是在 CommonPrerequisiteValidator.validateDirectPrerequisiteVisibility() 中完成

巢狀集合

通常,已設定的目標會匯總依附元件中的一組檔案、新增專屬檔案,並將匯總集包裝到遞移資訊供應器中,讓設定的目標依附於該資訊平台也能執行相同操作。例:

  • 用於建構的 C++ 標頭檔案
  • 代表 cc_library 轉換關閉情形的物件檔案
  • 要編譯或執行 Java 規則的類別路徑中需要位於的一組 .jar 檔案
  • Python 規則連帶關閉中的一組 Python 檔案

若是使用 ListSet 等簡易方式執行此操作,最後就會產生二次的記憶體用量:如果存在 N 規則鏈結,而每個規則新增了檔案,就會有 1+2...+N 個集合成員。

為解決這個問題,我們提出 NestedSet 的概念。它的資料結構由其他 NestedSet 執行個體及其本身的部分成員組成,進而形成組合的有向非循環圖。這些元件不可變更,成員可以疊代。我們定義了多個疊代順序 (NestedSet.Order):預購、後序、色調 (節點一律位於其祖系之後) 以及「別擔心,但每次都應相同」。

相同的資料結構在 Starlark 中稱為 depset

構件與動作

實際建構作業包含一組指令,您必須執行這些指令才能產生使用者想要的輸出內容。這些指令會以 Action 類別的執行個體表示,檔案則以 Artifact 類別的執行個體表示。它們是排列在「動作圖表」的雙部分、有向非循環圖,

構件分為兩種類型:來源構件 (在 Bazel 開始執行前即可供使用) 和衍生構件 (需要建構的成果)。衍生構件本身可以是多種種類:

  1. **一般成品。**系統會計算其總和檢查碼,並使用 mtime 做為捷徑來檢查這些項目是否處於最新狀態;如果檔案的時間沒有變更,我們就不會檢查檔案。
  2. 未解決的符號連結構件。系統會呼叫 readlink() 來檢查這些元件是否處於最新狀態。這與一般構件不同,這些符號可能是具有元素符號的連結。通常用於一個先將部分檔案封裝為某種封存檔案的情況。
  3. 樹系構件這些並非單一檔案,而是目錄樹狀結構。系統會檢查其中的檔案組合及其內容,確認是否含有最新內容。這些類別會表示為 TreeArtifact
  4. 常數中繼資料構件變更這些構件並不會觸發重新建構作業。這項資訊只會用於建構戳記資訊:我們不想因為目前的時間變更而重新建構。

沒有根本原因,為何原始碼構件不得為樹狀結構成果或未解析的符號連結構件,只是我們尚未實作該檔案 (但應該只是,在 BUILD 檔案中參照來源目錄,也是在 Bazel 所已知的長期錯誤問題之一;我們採用可經由 BAZEL_TRACK_SOURCE_DIRECTORIES=1 JVM 屬性啟用的工作實作方式)

顯而易見的Artifact是中間人。這類例項會由 Artifact 例項表示,為 MiddlemanAction 的輸出內容。這些函式的用途有下列特殊用途:

  • 使用匯總中間人將構件分組。因此,如果許多動作使用同一組大量輸入,我們就不會有 N*M 依附元件邊緣,只有 N+M (會替換為巢狀集)
  • 為依附元件中間排程時,可確保動作先執行。這些程式庫主要用於程式碼檢查,但也適用於 C++ 編譯 (請參閱 CcCompilationContext.createMiddleman() 瞭解相關說明)
  • 使用執行檔案中介者,確保包含執行檔案樹狀結構,因此不需要另外依附輸出資訊清單,以及執行檔案樹狀結構參照的每個成果。

最適合將動作解讀為需要執行的指令、所需環境,以及該動作產生的輸出內容組合。以下說明動作說明的主要組成部分:

  • 需要執行的指令列
  • 所需的輸入構件
  • 需要設定的環境變數
  • 註解,說明需要執行哪個環境 (例如平台)\

除此之外,還有一些其他特殊情況,例如編寫內容已知至 Bazel 的檔案。這些是 AbstractAction 的子類別。大部分的動作都是 SpawnActionStarlarkAction (相同,但應該不是由獨立類別),但 Java 和 C++ 有專屬的動作類型 (JavaCompileActionCppCompileActionCppLinkAction)。

我們最終會要將所有內容移至 SpawnActionJavaCompileAction 相當接近,但 C++ 卻因 .d 檔案剖析和包含掃描而有些特殊情況。

動作圖表主要「內嵌」入 SkyFrame 圖表:從概念來看,動作的執行作業會以叫用 ActionExecutionFunction 表示。從動作圖依附元件邊緣到 SkyFrame 依附元件邊緣的說明,請參閱 ActionExecutionFunction.getInputDeps()Artifact.key(),並採取幾項最佳化調整,以盡量減少 SkyFrame 邊緣數量:

  • 衍生構件沒有自己的 SkyValue。而應使用Artifact.getGeneratingActionKey()找出產生動作的動作金鑰
  • 巢狀組合有專屬的 SkyFrame 鍵。

共用動作

部分動作是由多個設定的目標產生;Starlark 規則受到限制,因為這類規則只能將衍生動作放入其設定及其套件決定的目錄 (但即使在同一個套件中實作的規則可能會發生衝突),但在 Java 中實作的規則可以在任何位置放置衍生構件。

這就算是錯誤功能,但移除並不容易,因為如果來源檔案需要某種方式的處理,且多個規則 (手波浪形) 參照該檔案,便可大幅節省執行時間。這會產生一些 RAM 的費用:共用動作的每個執行個體必須分別儲存在記憶體中。

如果兩個動作產生的輸出檔案必須完全相同:具備相同輸入內容、相同的輸出內容,以及執行相同的指令列。此對等關係會在 Actions.canBeShared() 中實作,系統會查看每個動作,在分析和執行階段之間進行驗證。這會在 SkyframeActionExecutor.findAndStoreArtifactConflicts() 中實作,也是 Bazel 中少數需要建構的「全球」檢視畫面。

執行階段

Bazel 實際會執行建構動作 (例如產生輸出的指令)。

Bazel 會在分析階段結束後著手決定要建構哪些構件。這項程序的邏輯是以 TopLevelArtifactHelper 編碼。大致來說,這就是指令列上設定目標的 filesToBuild,以及特殊輸出群組的內容,以明確表示「如果此目標位於指令列,建構這些構件」。

下一步是建立執行作業根層級。由於 Bazel 可選擇從檔案系統中的不同位置讀取來源套件 (--package_path),因此需要以完整的來源樹狀結構提供在本機執行的動作。這項工作是由類別 SymlinkForest 處理,並且會記下分析階段中使用的每個目標,並建構單一目錄樹狀結構,將每個套件與其實際位置所用的目標連結在一起。另一個替代方法是將正確的路徑傳遞至指令 (將 --package_path 納入考量)。這不理想,原因如下:

  • 套件從套件路徑項目移至另一個套件時,會變更動作指令列 (這是常見的情況)
  • 如果動作是從遠端執行,與在本機執行動作相同,則指令列會產生不同的指令列
  • 這需要針對目前使用的工具進行專屬的指令列轉換 (考量 Java 類別路徑和 C++ include 路徑等) 之間的差異
  • 變更動作的指令列會使動作快取項目失效
  • --package_path 的運作速度緩慢,目前正在穩定淘汰

然後,Bazel 就會開始掃遍動作圖表 (兩件式的有向圖,由動作及其輸入和輸出成果組成),以及執行動作。每個動作的執行都會以 SkyValue 類別 ActionExecutionValue 的例項表示。

由於執行動作的費用高昂,因此我們有幾層快取層級可能會落入 SkyFrame 後:

  • ActionExecutionFunction.stateMap 包含相關資料,可以降低 ActionExecutionFunction 的重新啟動 SkyFrame 成本
  • 本機動作快取包含檔案系統的狀態相關資料
  • 遠端執行系統通常也包含自己的快取

本機動作快取

這個快取是位於 SkyFrame 後方的另一層;即使已在 SkyFrame 中重新執行某項動作,它仍可能在本機動作快取中發揮作用。它代表本機檔案系統的狀態,且已經過序列化處理到磁碟。也就是說,當一個啟動新的 Bazel 伺服器時,即使 Skyframe 圖形是空的,一個伺服器仍能獲得本機動作快取成功。

系統會使用 ActionCacheChecker.getTokenIfNeedToExecute() 方法檢查這個快取是否有命中。

與 API 的名稱相反,這是一種從衍生構件的路徑到發出的動作。相關動作說明:

  1. 其輸入和輸出檔案的組合,以及其總和檢查碼
  2. 這個類別的「動作鍵」通常是執行的指令列,但一般而言,代表輸入檔案檢查碼未擷取的所有內容 (例如 FileWriteAction,而是寫入資料的總和檢查碼)

此外,還有一個高度實驗性的「由上而下動作快取」仍處於開發階段,其會使用遞移雜湊來避免多次快取。

輸入探索和輸入修剪

有些動作比只有一組輸入內容還要複雜。動作的輸入組合變更分為兩種形式:

  • 動作可能會在執行前發現新的輸入內容,或判定部分輸入內容並非實際的必要項目。標準範例是 C++,最好能合理推斷 C++ 檔案使用哪些標頭檔案,判斷其使用何種標頭檔案,進而不必將每個檔案傳送至遠端執行工具。因此,我們不想將每個標頭檔案註冊為「輸入」,而且僅掃描來源檔案,不要將每個標頭檔案註冊為「輸入」。在這種情況下,我們僅會掃描來源檔案,不要將每個標頭檔案註冊為「輸入」。在這種情況下,我們只需要在 C++ 中將每個標頭檔案註冊為「輸入」即可,因為 C++ 只會將每個標頭檔案註冊為「輸入」。#include
  • 動作可能會發現執行期間並未使用部分檔案。在 C++ 中稱為「.d files」:編譯器會告知在事後使用了哪些標頭檔案,為了避免犯錯比 Make 更嚴重的麻煩,Bazel 會利用這項資訊。這比 include 掃描器還得更好,因為它依賴編譯器。

這些事件是透過動作中的方法實作:

  1. 系統會呼叫 Action.discoverInputs()。API 應會傳回一組巢狀 Artifact 組合判定為需要。這些構件必須是來源構件,這樣動作圖表中就不會在設定的目標圖表中沒有對等的依附元件邊緣。
  2. 系統會呼叫 Action.execute() 來執行動作。
  3. Action.execute() 結束時,動作會呼叫 Action.updateInputs(),告知 Bazel 不需要所有輸入內容。如果將使用的輸入內容回報為未使用,可能會導致漸進式建構作業不正確。

動作快取在新的動作執行個體 (例如伺服器重新啟動後建立) 傳回命中時,Bazel 會自行呼叫 updateInputs(),讓這組輸入能反映之前完成的輸入探索和裁舊作業的結果。

Starlark 動作可利用設施,透過 ctx.actions.run()unused_inputs_list= 引數,將某些輸入宣告為未使用的輸入內容。

各種執行動作的方法:策略/ActionContext

部分動作可以透過不同方式執行。舉例來說,指令列可在本機執行,但可在本機、不同類型的沙箱中執行,也能從遠端執行。這類概念稱為 ActionContext (或 Strategy,因為我們成功只完成重新命名的一半...)

動作背景資訊的生命週期如下:

  1. 執行階段開始時,系統會詢問 BlazeModule 執行個體當時的動作背景資訊。此操作會在 ExecutionTool 的建構函式中進行。動作結構定義類型是由參照 ActionContext 子介面的 Java Class 執行個體識別,這個執行個體必須實作動作結構定義。
  2. 系統會從可用的動作環境中選取適當的動作結構定義,並轉送至 ActionExecutionContextBlazeExecutor
  3. 使用 ActionExecutionContext.getContext()BlazeExecutor.getStrategy() 的動作要求結構定義 (實際上應該只是其中一種方式...)

您可以自由呼叫其他策略來執行工作;舉例來說,在可在本機和遠端啟動動作的動態策略中,會使用任何先完成的策略。

其中一個值得注意的策略是實作永久工作站程序 (WorkerSpawnStrategy)。概念是,部分工具的啟動時間較長,因此應在動作之間重複使用,而不是為每個動作啟動新程序 (這確實是潛在的正確問題,因為 Bazel 顧及個別要求之間不會傳輸可觀測的狀態,因此這確實是潛在的正確問題)

如果工具發生變更,您必須重新啟動工作站程序。是否可以重複使用工作站,取決於使用 WorkerFilesHash 計算所用工具的總和檢查碼。這需要知道動作的輸入內容代表工具的一部分,且代表輸入內容;這取決於動作的建立者:Spawn.getToolFiles(),而 Spawn 的執行檔案將計為工具的某些部分。

進一步瞭解策略 (或操作情境!):

  • 如要瞭解執行動作的各種策略,請參閱這裡的資訊。
  • 動態策略的相關資訊。動態策略會在本機或遠端執行動作,查看先完成的項目,請見此處
  • 如需在本機執行動作的細節,請參閱這裡

本機資源管理員

Bazel「可以」平行執行許多動作。「應」同時執行的本機動作數量與動作不同:動作所需的資源越多,應同時執行的執行個體越少,避免本機電腦超載。

這會在 ResourceManager 類別中實作:每個動作都必須以 ResourceSet 執行個體 (CPU 和 RAM) 格式為需要的本機資源預估值加上註解。當動作結構定義執行需要本機資源的項目時,會呼叫 ResourceManager.acquireResources() 並遭到封鎖,直到取得所需資源為止。

如要進一步瞭解本機資源管理,請參閱這裡的說明。

輸出目錄的結構

每個動作都需要在輸出目錄中獨立位置。衍生構件的位置通常如下:

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

系統如何決定與特定設定相關聯的目錄名稱?有兩個衝突的需要屬性:

  1. 如果同一個建構作業中可以發生兩項設定,則兩者應有不同的目錄,這樣兩邊才能有相同動作的版本;否則,如果這兩個設定並不一致 (例如,當兩個設定與產生相同輸出檔案的動作的指令列不一致),Bazel 就無法知道該選擇哪一個動作 (「動作衝突」)
  2. 如果兩個設定代表「大致」相同,則應使用相同的名稱,才能在指令列選項中針對另一個設定執行重複執行。例如,變更 Java 編譯器的指令列選項時,不應導致 C++ 編譯動作重新執行。

到目前為止,我們尚未制定出這個問題的解決原則,這個方法與設定剪輯問題相似。有關選項的較長討論,請參閱這裡。主要的問題領域是 Starlark 規則 (作者通常不完全熟悉 Bazel) 和麵向,對於可能會產生「相同」輸出檔案的項目空間,新增了另一個維度。

目前的做法是,設定的路徑片段為 <CPU>-<compilation mode> 且新增了多個後置字串,因此在 Java 中實作的設定轉換不會導致動作衝突。此外,系統也會新增一組 Starlark 設定轉換檢查碼,確保使用者不會造成動作衝突。這並不是完美的。這會在 OutputDirectories.buildMnemonic() 中實作,且需要每個設定片段將其專屬的部分新增至輸出目錄的名稱。

測試命名空間

Bazel 針對執行測試提供豐富的支援。包括:

  • 從遠端執行測試 (如果可以使用遠端執行後端)
  • 多次同時執行測試 (用於去分區或收集時間資料)
  • 資料分割測試 (為了加快執行速度,在相同測試中將測試案例拆分為一組)
  • 重新執行不穩定的測試
  • 將測試組合成測試套件

測試是指具備 TestProvider 的一般設定目標,其描述應如何執行測試:

  • 正在執行測試的建構結果所屬的構件。這是一個「快取狀態」檔案,其中包含序列化的 TestResultData 訊息
  • 測試應執行次數
  • 測試拆分的資料分割數量
  • 關於測試執行方式的一些參數 (例如測試逾時)

決定要執行哪些測試

決定執行的測試是相當複雜的程序。

首先,剖析目標模式時,測試套件會以遞迴方式展開。展開功能會在 TestsForTargetPatternFunction 中實作。令人驚訝的是,如果測試套件未宣告任何測試,指的是套件中的「每」測試。這會透過新增名為 $implicit_tests 的隱含屬性至測試套件規則,在 Package.beforeBuild() 中實作。

然後,系統會根據指令列選項,依大小、標記、逾時和語言篩選測試。這會在 TestFilter 中實作,並在目標剖析期間從 TargetPatternPhaseFunction.determineTests() 呼叫,且結果會進入 TargetPatternPhaseValue.getTestsToRunLabels()。無法設定篩選的規則屬性的原因,是因為這個在分析階段之前發生,所以無法使用該設定。

系統會在 BuildView.createResult() 中進一步處理:系統會篩除分析失敗的目標,並將測試拆分為專屬和非專屬的測試。然後附加至 AnalysisResultExecutionTool 就是如何得知要執行哪些測試。

為了向這個精密程序提供資訊公開程度,您可以使用 tests() 查詢運算子 (於 TestsFunction 中實作) 瞭解在指令列指定特定目標時,執行了哪些測試。最不幸的是,重新實作,因此與上述內容之間可能有細微差異。

執行測試

測試的執行方式是要求快取狀態構件。接著,這會導致執行 TestRunnerAction,而最終會呼叫 --test_strategy 指令列選項所選擇的 TestActionContext,該選項會按照要求的方式執行測試。

測試會根據精密的通訊協定執行,且該通訊協定使用環境變數來測試預期情況。您可以在這裡查看 Bazel 的測試預期目標,以及 Bazel 的測試作業詳細說明。結束代碼為 0 時最簡單,表示成功,否則任何都表示失敗。

除了快取狀態檔案以外,每個測試程序也會發出許多其他檔案。系統會將這些來源儲存在「測試記錄目錄」中,這是目標設定輸出目錄 testlogs 的子目錄:

  • test.xml:詳細說明測試資料分割中個別測試案例的 JUnit 樣式 XML 檔案
  • test.log,測試的主控台輸出內容。不會分隔 stdout 和 stderr。
  • test.outputs,是「未宣告的輸出目錄」。測試除了用來輸出檔案外,還可用於在終端機中列印內容。

在測試執行期間,可能會發生在建構一般目標時無法發生的兩種情況:專屬測試執行和輸出串流。

某些測試必須在專屬模式下執行,例如不會與其他測試同時執行。方法是將 tags=["exclusive"] 新增至測試規則,或使用 --test_strategy=exclusive 執行測試。每項專屬測試都會單獨執行 SkyFrame 叫用,要求在「main」建構後執行測試。這會在 SkyframeExecutor.runExclusiveTest() 中實作。

與一般動作不同,一般動作會在動作完成後傾印的終端機輸出內容,使用者可以要求串流輸出測試,這樣就能瞭解長時間執行測試的進度。這是透過 --test_output=streamed 指令列選項指定,且暗指獨有測試執行作業,因此不同測試的輸出內容不會交錯。

這項操作會在名為 apted 的 StreamedTestOutput 類別中實作,運作方式為輪詢相關測試的 test.log 檔案,並將新的位元組轉儲到 Bazel 規則的終端機。

藉由觀察各種事件 (例如 TestAttemptTestResultTestingCompleteEvent),事件匯流排上會顯示已執行測試的結果。這些結果會轉儲到建構事件通訊協定,並由 AggregatingTestListener 傳送至主控台。

涵蓋範圍收集

涵蓋率由檔案中以 LCOV 格式的測試回報 bazel-testlogs/$PACKAGE/$TARGET/coverage.dat .

為了收集涵蓋率,每次測試執行作業都會包裝在名為 collect_coverage.sh 的指令碼中。

這個指令碼會設定測試環境,以便啟用涵蓋率收集功能,並確定涵蓋率執行階段寫入涵蓋率檔案的位置。然後執行測試。測試可能會自行執行多個子程序,並由以多種不同程式設計語言編寫的部分組成 (有獨立的涵蓋範圍收集執行階段)。包裝函式指令碼會視需要將產生的檔案轉換為 LCOV 格式,並將這些結果合併為單一檔案。

collect_coverage.sh 的交錯是由測試策略執行,且需要在測試的輸入內容上執行 collect_coverage.sh。這會透過隱含屬性 :coverage_support 滿足,解析為設定標記 --coverage_support 的值 (請參閱 TestConfiguration.TestOptions.coverageSupport)

某些語言會執行離線檢測,也就是說,涵蓋範圍檢測會在編譯時間 (例如 C++) 加入,而有些語言會進行線上檢測,也就是說,會在執行階段新增涵蓋範圍檢測。

另一個核心概念是基準涵蓋範圍。這是指程式庫、二進位檔或測試中未執行任何程式碼的涵蓋範圍。解決方法是,如果您要計算二進位檔的測試涵蓋率,就不能將所有測試的涵蓋範圍合併,因為二進位檔中的程式碼可能未連結至任何測試。因此,我們會為每個二進位檔產生一個涵蓋範圍檔案,其中只包含我們針對其中沒有涵蓋線收集的檔案。目標的基準涵蓋率檔案位於 bazel-testlogs/$PACKAGE/$TARGET/baseline_coverage.dat . 除了測試外,如果您將 --nobuild_tests_only 標記傳遞至 Bazel,系統也會針對二進位檔和程式庫產生此程式碼。

基準涵蓋範圍目前已失效。

我們會追蹤兩組檔案,用於每個規則的涵蓋範圍:一組檢測檔案,以及一組檢測中繼資料檔案。

一組檢測檔案只是要進行檢測的一組檔案。針對線上涵蓋範圍執行階段,這個項目可在執行階段使用,決定要檢測的檔案。這也用於導入基準涵蓋率。

一組檢測中繼資料檔案是測試所需的一組額外檔案,這些檔案用於產生 Bazel 所需的 LCOV 檔案。實際上,這個檔案包含執行階段專屬的檔案;例如,gcc 會在編譯期間發出 .gcno 檔案。啟用涵蓋率模式後,這些變數會新增至測試動作的輸入項目組合。

是否正在收集涵蓋率資訊都儲存在 BuildConfiguration 中。這項做法很實用,因為這是變更測試動作和取決於此位元的操作圖表的簡便方法,但也代表如果這個位元經過翻轉,也必須重新分析所有目標 (某些語言 (例如 C++) 需要不同的編譯器選項產生可以收集涵蓋率的程式碼,因此即使接著重新分析就能降低這個問題的影響)。

支援檔案取決於隱性依附元件中的標籤,因此叫用政策可以覆寫這些檔案,以便在不同 Bazel 版本間產生差異。在理想情況下,這些差異將遭到移除,並將其中一個差異標準化。

我們也會產生「涵蓋範圍報表」,並將 Bazel 叫用中每項測試收集到的涵蓋範圍合併。這項操作由 CoverageReportActionFactory 處理,並從 BuildView.createResult() 呼叫。這項工具會查看所執行第一個測試的 :coverage_report_generator 屬性,藉此存取所需的工具。

查詢引擎

Bazel 採用少許語言,用於詢問各種圖表的各種問題。提供的查詢種類如下:

  • bazel query 用於調查目標圖表
  • bazel cquery 用於調查設定的目標圖表
  • bazel aquery 用於調查動作圖表

每種方法都是透過將 AbstractBlazeQueryEnvironment 設為子類別來實作的。將 QueryFunction 設為子類別,即可執行其他查詢函式。為了允許串流查詢結果,而不是將結果收集到某些資料結構,系統會將 query2.engine.Callback 傳遞至 QueryFunction,並呼叫其來呼叫其想傳回的結果。

查詢結果可透過多種方式發出:標籤、標籤和規則類別、XML、protobuf 等。這些類別會實作為 OutputFormatter 的子類別。

對某些查詢輸出格式 (明確來說),Bazel 必須發出「所有」___「套件載入」提供的資訊,這樣就可以在該套件之間差異輸出,並判斷特定目標是否已變更。因此,屬性值必須可以序列化,因此只有少數屬性類型而不具有複雜 Starlark 值的屬性類型。常見的解決方法是使用標籤,將複雜資訊附加到有該標籤的規則中。這並非令人滿意的解決方法 因此解除這項規定會非常好

模組系統

您可以新增模組來擴充 Bazel。每個模組都必須對 BlazeModule (名稱是 Bazel 過去稱為 Blaze 的歷史) 建立子類別,並在執行指令期間取得各種事件的相關資訊。

這類函式主要用於實作多項「非核心」功能,而且僅部分版本 Bazel (例如 Google 使用的版本) 需要:

  • 遠端執行系統的介面
  • 新指令

BlazeModule 提供的擴充點組合有點困難。請勿將此設計當做良好設計原則的例子。

事件匯流排

BlazeModules 主要透過事件匯流排與其他 Bazel 進行通訊的主要方式是透過事件匯流排 (EventBus):系統會為每個建構作業建立新的執行個體,Bazel 的不同部分都可以將事件張貼到該執行個體,模組則可為感興趣的事件註冊事件監聽器。舉例來說,以下內容以事件表示:

  • 已決定要建構的建構目標清單 (TargetParsingCompleteEvent)
  • 系統已判定頂層設定 (BuildConfigurationEvent)
  • 已建立、成功或未建構目標 (TargetCompleteEvent)
  • 已執行測試 (TestAttemptTestSummary)

其中有些事件會在建構事件通訊協定中的 Bazel 之外表示 (這些事件為 BuildEvent)。這不僅可讓 BlazeModule 與 Bazel 程序以外的東西觀察建構作業。存取路徑包含通訊協定訊息的檔案,或 Bazel 可以連線至伺服器 (稱為「建構事件服務」) 來串流事件。

這是在 build.lib.buildeventservicebuild.lib.buildeventstream Java 套件中實作的功能。

外部存放區

Bazel 原本是設計用來在單聲道存放區中 (也就是單一原始碼樹狀結構,其中包含建構所需的所有內容),而 Bazel 實際生活在世界上並不一定是如此。「外部存放區」是連結這兩個世界的抽象層:它們代表建構所需的程式碼,但不位於主要原始碼樹狀結構。

WORKSPACE 檔案

一組外部存放區取決於剖析 WORKSPACE 檔案。例如,加入類似下方的宣告:

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

可以使用名為 @foo 的存放區中的結果。複雜之處是指可在 Starlark 檔案中定義新的存放區規則,然後可用來載入新的 Starlark 程式碼,該程式碼可用於定義新的存放區規則等等...

為了處理這種情況,WORKSPACE 檔案的剖析 (在 WorkspaceFileFunction 中) 會分成多個區塊,並由 load() 陳述式加以分隔。區塊索引會由 WorkspaceFileKey.getIndex() 表示,並計算 WorkspaceFileFunction,直到索引 X 為止,構成索引直到第 X load() 陳述式為止。

正在擷取存放區

Bazel 必須先經過「擷取」fetched,才能將存放區的程式碼提供給 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 處理。

存放區對應

這種情形可能導致多個存放區想依附於同一個存放區,但在不同版本中 (這就是「鑽石依附元件問題」的執行個體)。例如,如果版本中不同存放區中的兩個二進位檔想要依附於 Guava,則這些二進位檔應該都參照開頭為 @guava// 的標籤,並預期代表不同的版本。

因此,Bazel 允許其重新對應外部存放區標籤,以便 @guava// 字串參照其中一個二進位檔的存放區中的某個 Guava 存放區 (例如 @guava1//),以及另一個 Guava 存放區 (例如 @guava2//) 的存放區。

這也可以用來「加入」join鑽石。如果存放區依賴 @guava1//,而另一個存放區依附於 @guava2//,則存放區對應可讓其中一個存放區重新對應,以使用標準 @guava// 存放區。

對應關係在 WORKSPACE 檔案中被指定為個別存放區定義的 repo_mapping 屬性。這會在 SkyFrame 中顯示為 WorkspaceFileValue 的成員,並在該處產生:

  • Package.Builder.repositoryMapping,用於透過 RuleClass.populateRuleAttributeValues() 轉換套件中規則的標籤值屬性
  • 在分析階段中使用的 Package.repositoryMapping (用於解決載入階段未剖析的 $(location) 這類項目)
  • BzlLoadFunction:用於解析 load() 陳述式中的標籤

JNI 位元

Bazel 的伺服器「主要」以 Java 編寫。例外狀況是 Java 無法自行執行或實作時無法執行的某些部分。這主要受限於與檔案系統的互動、程序控制,以及其他各種低階作業。

C++ 程式碼位於 src/main/native 及原生方法的 Java 類別下:

  • NativePosixFilesNativePosixFileSystem
  • ProcessUtils
  • WindowsFileOperationsWindowsFileProcesses
  • com.google.devtools.build.lib.platform

控制台輸出

傳送主控台輸出內容看似簡單,但執行多個程序 (有時是遠端) 和精細快取的共用,渴望擁有優質且色彩繽紛的終端機輸出內容,以及擁有長時間執行的伺服器,是一大挑戰。

從用戶端傳入遠端程序呼叫 (RPC) 後,就會建立兩個 RpcOutputStream 執行個體 (適用於 stdout 和 stderr),將輸出的資料轉送至用戶端。接著,這些會納入 OutErr (stdout、stderr) 配對中。任何要在主控台上列印的內容都會經過這些串流。接著,系統會將這些串流轉交給 BlazeCommandDispatcher.execExclusively()

根據預設,系統會用 ANSI 逸出序列列印輸出內容。不符合這些需要 (--color=no) 時,AnsiStrippingOutputStream 會將其移除。此外,System.outSystem.err 也會重新導向至這些輸出串流。因此,使用 System.err.println() 列印偵錯資訊後,仍會顯示在用戶端的終端機輸出內容 (與伺服器不同)。如果程序產生二進位檔輸出 (例如 bazel query --output=proto),請小心不要進行通訊,

簡短訊息 (錯誤、警告和類似訊息) 會透過 EventHandler 介面表示。值得注意的是,這與對 EventBus 的貼文內容不同 (這會造成混淆)。每個 Event 都有 EventKind (錯誤、警告、資訊和其他幾個選項),而且可能會有一個 Location (在原始碼中導致事件發生的位置)。

部分 EventHandler 實作會儲存收到的事件。這會將各種快取處理程序 (例如快取設定的目標) 發出的警告重新傳送至 UI。

部分 EventHandler 也允許發布事件,最終能找到其前往事件匯流排 (一般 Event 並「不會」在此處出現)。這些是 ExtendedEventHandler 的實作,其主要用途是重播快取的 EventBus 事件。這些 EventBus 事件全都實作 Postable,但並非發布至 EventBus 的所有內容都會實作這個介面,只有由 ExtendedEventHandler 快取的項目才會實作這個介面 (這是良好且大部分的作業都會執行,但並不會強制執行)

終端機輸出內容「主要」透過 UiEventHandler 傳送,而 Bazel 是執行所有最先進的輸出格式和進度回報。其中包含兩項輸入內容:

  • 事件匯流排
  • 事件串流透過回報者整合

指令執行機器 (例如 Bazel 的其餘部分) 只能透過 Reporter.getOutErr() 直接連線至用戶端,且可允許直接存取這些串流。只有在指令需要傾印大量可能的二進位資料 (例如 bazel query) 時,才能使用這個值。

剖析 Bazel

Bazel 運作速度飛快。Bazel 的運作速度也很慢,因為建構作業只有在最可用時才會成長。因此,Bazel 包含一個分析器,可用來分析建構作業和 Bazel 本身。其會在正確命名為 Profiler 的類別中實作。系統預設會開啟這項功能。不過,雖然系統只會記錄橋接資料,因此可容許額外負擔;指令列 --record_full_profiler_data 則會記錄所有可能的資料。

這項功能會發出 Chrome 分析器格式的設定檔,建議您使用 Chrome 瀏覽。 它的資料模型是工作堆疊:一個可以用來啟動工作和結束工作,且彼此之間應該以妥善的巢狀結構放置。每個 Java 執行緒都有自己的工作堆疊。待辦事項:如何與動作和接續傳遞樣式搭配運作?

分析器會在 BlazeRuntime.initProfiler()BlazeRuntime.afterCommand() 中分別啟動及停止,並嘗試盡可能維持運作,以便剖析所有項目。如要在設定檔中新增內容,請呼叫 Profiler.instance().profile()。它會傳回 Closeable,其閉包代表工作結束時。這項功能最適合與 try-with-resources 陳述式搭配使用。

我們也會在 MemoryProfiler 中執行累積記憶體剖析作業。系統也會持續開啟這個模式,主要會記錄最大堆積大小和 GC 行為。

測試 Bazel

Bazel 主要有兩種測試:一種會將 Bazel 視為「黑箱」,而測試則是只執行分析階段。我們將先前的「整合測試」與後者稱為「單元測試」,雖然比較類似於整合測試程度較低,但整合程度較低。我們也會提供一些實際單元測試

整合測試有兩種類型:

  1. 系統會利用非常精密的 bash 測試架構,在 src/test/shell 下實作
  2. 在 Java 中實作的 API。這些類別會實作為 BuildIntegrationTestCase 的子類別

BuildIntegrationTestCase 是偏好的整合測試架構,因為適用於大多數測試情境。它是 Java 架構,可以提供偵錯功能,並與許多常見的開發工具完美整合。Bazel 存放區中有許多 BuildIntegrationTestCase 類別的範例。

分析測試會以 BuildViewTestCase 的子類別實作。有一個暫存檔案系統可用來寫入 BUILD 檔案,然後各種輔助方法可以要求設定的目標、變更設定,並聲明各種與分析結果相關的事項。