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 擷取的外部存放區。
  • exec 根目錄,內含目前版本所有原始碼的符號連結。它位於$OUTPUT_BASE/execroot,在建構期間,工作目錄為 $EXECROOT/<name of main repository>。我們計劃將其變更為 $EXECROOT,但由於這屬於長期計畫,因為該變更非常不相容。
  • 在建構期間建立的檔案。

執行指令的程序

當 Bazel 伺服器取得控制權,並收到該執行指令時,就會發生以下事件順序:

  1. 系統已通知 BlazeCommandDispatcher 有新的要求。此指令可判定指令是否需要執行工作區 (幾乎每個指令除外,只有沒有與原始碼執行任何動作 (例如版本或說明) 的指令除外),以及是否正在執行其他指令。

  2. 找到正確的指令。每個指令都必須實作 BlazeCommand 介面,且必須包含 @Command 註解 (這有點反模式,若 BlazeCommand 上的方法以方法描述了指令所需的所有中繼資料,將更加實用)

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

  4. 已建立事件匯流排。事件匯流排是建構期間事件的串流。其中有些資源會在 Build Event Protocol 的評估下匯出至 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 以難以偵錯的細微方式破壞的好方法。遺憾的是,這些事件實際上無法變更。(在建構完成之後,立即修改 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 的存放區稱為「主要存放區」,其他存放區則稱為「外部存放區」。

存放區會在根目錄中以 WORKSPACE (或 WORKSPACE.bazel) 檔案標示。這個檔案包含整個建構作業的「全域」資訊,例如可用的外部存放區組合。其運作方式如同一般的 Starlark 檔案,代表一個可以 load() 個其他 Starlark 檔案。通常用於提取明確參照的存放區所需的存放區 (我們將此稱為「deps.bzl 模式」)

外部存放區的程式碼已建立符號連結,或透過 $OUTPUT_BASE/external 下載。

執行建構作業時,必須整合整個來源樹狀結構;由 SymlinkForest 來完成,這會將主要存放區中的每個套件分別連結至 $EXECROOT,以及對 $EXECROOT/external$EXECROOT/.. 的所有外部存放區進行符號連結 (先前那樣的話,在主存放區中不可有名為 external 的套件,因此我們會捨棄主要存放區)

套件

每個存放區都是由套件、相關檔案集合和依附元件規格所組成。這些元件是由名為 BUILDBUILD.bazel 的檔案指定。如果兩者皆存在,Bazel 偏好使用 BUILD.bazel;這是因為 Bazel 的祖系「Blaze」仍是接受 BUILD 檔案的原因。然而,結果被認為是常用的路徑區段,在 Windows 上更是如此,因為檔案名稱不區分大小寫。

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

BUILD 檔案的評估作業稱為「套件載入」。這個方法在 PackageFactory 類別中實作,運作時需呼叫 Starlark 解譯器,且需要瞭解可用的規則類別組合。套件載入的結果是 Package 物件。大部分是從字串 (目標名稱) 對應至目標本身。

載入套件期間有很大的複雜問題:Bazel 不需要明確列出每個來源檔案,而是可以執行 glob (例如 glob(["**/*.java"]))。與殼層不同,它支援向下傳遞至子目錄的遞迴 glob (但不支援子套件)。這需要存取檔案系統,而且速度可能很慢,因此我們會實作各種技巧,盡可能平行且有效率地同時執行系統。

以下類別會實作繪圖操作:

  • LegacyGlobber,這是快速且模糊不清的空中景 Globber
  • SkyframeHybridGlobber,這個版本是使用 SkyFrame,並還原為舊版 globber,以免發生「SkyFrame 重新啟動」(如下所述)

Package 類別本身包含一些成員,這些成員只用於剖析 WORKSPACE 檔案,這對實際套件來說並不合理。這屬於設計瑕疵,因為說明一般套件的物件不得包含描述其他內容的欄位。包括:

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

在理想情況下,剖析 WORKSPACE 檔案與剖析一般套件之間會有更多區隔,因此 Package 就不需要同時滿足兩者的需求。但這並不容易,因為兩者之間是完全交錯的。

標籤、指定目標和規則

套件是由目標組成,目標類型如下:

  1. 「Files」:建構的輸入或輸出內容。在 Bazel 比較中,我們將這些物件稱為「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 中。

Starlark 規則類別必須使用 load() 陳述式在 BUILD 檔案的開頭匯入,而 Java 規則類別「自然」是由 Bazel 所知,因為已透過 ConfiguredRuleClassProvider 註冊。

規則類別包含以下資訊:

  1. 其屬性 (例如 srcsdeps):其類型、預設值、限制等。
  2. 各屬性所附加的設定轉換和切面 (如有)
  3. 規則的執行方式
  4. 規則「通常」建立的遞移資訊提供者

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

SkyFrame

Bazel 的基礎評估架構稱為 SkyFrame。其模型是將建構期間建構的每個項目都整理成有向非循環圖,而且邊緣從任何資料指向其依附元件的邊緣,也就是需要知道建構它的其他資料。

圖表中的節點稱為 SkyValue,其名稱稱為 SkyKey。兩者都基本上不可變更,只有不可變動的物件可透過這些物件存取。這幾乎總是處於不定狀態,如果不存在 (例如個別選項類別 BuildOptionsBuildConfigurationValue 和其 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 後方。

基本上,我們需要這些類型的解決方法,因為我們經常有數十萬個傳輸中的 SkyFrame 節點,而 Java 不支援輕量執行緒。

史塔拉克

Starlark 是用於設定及擴充 Bazel 的特定特定語言。它屬於 Python 受限的子集,其類型遠少,對控制流程的限制較多,最重要的是,絕對不變的保證可以啟用並行讀取。這並未完成 Turing-complete,因此會阻礙部分 (但並非全部) 使用者嘗試在某種語言中完成一般程式設計工作。

Starlark 會在 net.starlark.java 套件中實作。如果有獨立的 Go 實作,請按這裡。Bazel 中使用的 Java 實作目前是解譯器。

Starlark 用於多種情境,包括:

  1. BUILD 語言。這時系統會定義新規則。在此結構定義中執行的 Starlark 程式碼僅可存取其本身 BUILD 檔案的內容和其載入的 .bzl 檔案。
  2. 規則定義。這是定義新規則 (例如支援新語言的) 的方式。在此結構定義中執行的 Starlark 程式碼可以存取其直接依附元件提供的設定和資料 (稍後會進一步說明)。
  3. WORKSPACE 檔案。這是定義外部存放區 (不在主要來源樹狀結構中的程式碼) 的位置。
  4. 存放區規則定義。這是定義新外部存放區類型的位置在這種環境中執行的 Starlark 程式碼可在執行 Bazel 的機器上執行任何程式碼,並觸及工作區以外的位置。

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 中實作設定轉換 (說明文件在此)

遞移資訊提供者

遞移資訊提供者是一個方法 (且 _only_way),可讓已設定的目標得知其他已設定的目標。之所以名稱是「遞移」,是因為其名稱通常是設定目標的遞移性關閉作業。

Java 遞移資訊提供者和 Starlark 之間通常會有 1:1 的對應 (例外是 DefaultInfo,這是 FileProviderFilesToRunProviderRunfilesProvider 的組合,因為該 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 的 src 中時建構的檔案。
  2. 其執行檔案、一般資料和資料。
  3. 輸出內容群組。這些是規則可建構的各種「其他檔案」組合。如要存取這類資料,請使用 BUILD 檔案群組規則的 output_group 屬性,並在 Java 中使用 OutputGroupInfo 提供者。

執行檔案

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

一組執行檔案會以 Runfiles 執行個體表示。概念上是從執行檔案樹狀結構中檔案路徑到代表該檔案的 Artifact 執行個體之間的對應關係。這與單一 Map 稍有不同,原因有二:

  • 在大多數情況下,檔案的執行檔案路徑與 execpath 相同。藉此節省一些 RAM。
  • 執行檔案樹狀結構中有許多舊版項目,這也需要表示這些項目。

執行檔案會透過 RunfilesProvider 收集:這個類別的例項代表執行檔案的目標 (例如程式庫) 及其轉換閉包需求,且會像透過巢狀集合一樣收集檔案 (實際上是使用巢狀集合實作):每個目標會聯集其依附元件的執行檔案、新增一些自己的依附元件,然後傳送在依附元件圖表中設定產生的設定。RunfilesProvider 執行個體包含兩個 Runfiles 例項,一個使用「data」屬性做為規則依據時,另一個則適用於其他各種傳入依附元件。這是因為目標有時會仰賴資料屬性,而產生不同的執行檔案。這是我們尚未移除的不理想舊版行為。

二進位檔的執行檔案會表示為 RunfilesSupport 的執行個體。這與 Runfiles 不同,因為 RunfilesSupport 具備實際建構的功能 (與 Runfiles 不同,後者只是對應)。這需要下列額外元件:

  • 輸入執行檔案資訊清單。此為執行檔案樹狀結構的序列化說明。它可做為執行檔案樹狀結構內容的 Proxy,而 Bazel 會假設,只有在資訊清單內容有所變更時,執行檔案樹狀結構才會變更。
  • 輸出執行檔案資訊清單。會用於處理執行檔案樹狀結構 (特別是在 Windows 上) 的執行階段程式庫會使用此金鑰,因為 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 建構通訊協定緩衝區的資訊應向下傳播依附元件圖表。
  4. Aspect 代表計算會傳播依附元件圖的切面所需的所有資料。其中包含切面類別、定義及其參數。
  5. RuleAspect 是可以決定特定規則應傳播哪些層面的函式。其是一個 Rule -> Aspect 函式。

有些非預期的小工具是可將切面附加至其他層面;例如,收集 Java IDE 類別路徑的切面可能想知道類別路徑中的所有 .jar 檔案,但有些是通訊協定緩衝區。在這種情況下,IDE 切面會附加至 (proto_library 規則 + Java proto 切面) 組合。

切面的複雜度擷取在 AspectCollection 類別中。

平台和工具鍊

Bazel 支援多平台建構作業,也就是說,建構時可能有多個用於執行建構動作的架構,以及用於建構程式碼的多個架構。這些架構在 Bazel 差異中稱為「平台」 (如需完整說明文件,請按這裡)

平台是由限制設定 (例如「CPU 架構」的概念) 與限制值 (例如 x86_64 等特定 CPU) 的鍵/值對應來說明。我們在 @platforms 存放區中設置了最常用的限制設定和值「字典」。

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

為此,工具鍊會加註支援的執行作業和目標平台限制組合。為此,工具鍊的定義分為兩個部分:

  1. toolchain() 規則會說明工具鍊支援的一組執行與目標限制,並指出工具鍊支援的工具鍊類型 (例如 C++ 或 Java,後者會以 toolchain_type() 規則表示)
  2. 描述實際工具鍊的語言專屬規則,例如 cc_toolchain()

這是透過這種方式達成的,因為我們需要瞭解每個工具鍊的限制,才能確實完成工具鍊解析和特定語言專用的 *_toolchain() 規則包含的資訊,因此需要更多時間進行載入。

執行平台的指定方式如下:

  1. 在 WORKSPACE 檔案中,使用 register_execution_platforms() 函式
  2. 在指令列中使用 --extra_execution_platforms 指令列選項

可用的執行平台組合會以 RegisteredExecutionPlatformsFunction 計算。

所設定目標的目標平台是由 PlatformOptions.computeTargetPlatform() 決定。由於我們最終會想支援多個目標平台,但實際上並未實作,因此這是平台清單。

設定目標要使用的工具鍊組合取決於 ToolchainResolutionFunction。這個函式的功能如下:

  • 已註冊的工具鍊組合 (位於 WORKSPACE 檔案和設定中)
  • 所需的執行與目標平台 (在設定中)
  • 所設定目標所需的工具鍊類型組合 (位於 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) 的子類別 (EnvironmentGroup) 和預設可透過 Starlark (StarlarkLibrary.environmentGroup()) 使用的函式,最終會建立匿名目標。這是為了避免產生循環依附元件,因為每個環境都需要宣告所屬的環境群組,而且每個環境群組都需要宣告預設環境。

您可以使用 --target_environment 指令列選項,將建構作業限制在特定環境內。

限制檢查的實作位於 RuleContextConstraintSemanticsTopLevelConstraintSemantics

平台限制

現行的「官方」方法可說明目標與哪些平台相容,方法是使用與工具鍊和平台相同的限制。正在使用提取要求 #10945 進行審查。

顯示設定

如果您透過在眾多開發人員 (例如 Google) 參與的大型程式碼集運作,應謹慎避免其他人員因您的程式碼而任意設置。否則,根據Hyrum 的法律,員工必須仰賴您視為實作詳細資料的行為。

Bazel 會透過名為「瀏覽權限」的機制支援此做法:您可以宣告特定目標只能使用 visibility 屬性。這個屬性有點特殊,因為雖然包含標籤清單,但這些標籤可能會在編碼方式而非套件名稱,而非任何特定目標的指標上。(是的,這是設計瑕疵)。

我們會在下列位置實作這項資訊:

  • 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 個檔案,則會有 1+2+...+N 集合成員。

為解決這個問題,我們提出了 NestedSet 的概念。這是一種資料結構,由其他 NestedSet 執行個體和自己的某些成員組成,從而形成一個有向非循環圖。其所屬成員無法改變,成員可以反覆變更。我們定義了多個疊代順序 (NestedSet.Order):preorder、 postorder、topological (節點一律位於其祖系之後) 和「Don't Care, but it 每次每次都必須相同」。

相同的資料結構在 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 相當接近,但由於 .d 檔案剖析及納入掃描功能,C++ 屬於特殊情況。

動作圖表大部分是「嵌入」至 SkyFrame 圖表中:理論上,動作的執行是以 ActionExecutionFunction 的叫用表示。為了減少 SkyFrame 邊緣的數量,動作圖表依附元件邊緣與 SkyFrame 依附元件邊緣的對應說明在 ActionExecutionFunction.getInputDeps()Artifact.key() 中所述。此外,還有一些最佳化調整,以便保持低空框架邊緣的數量:

  • 衍生的構件沒有自己的 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 中重新執行動作,本機動作快取仍可能發生一次命中。它代表本機檔案系統的狀態,並且已經序列化到磁碟,這表示當 SkyFrame 圖無內容時,即使 SkyFrame 圖無內容,它還是能取得本機動作快取命中。

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

有別於名稱,這是從衍生構件的路徑到發出動作的對應路徑。操作說明如下:

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

還有一個高度實驗性的「由上而下動作快取」仍在開發中,使用遞移雜湊以避免多次前往快取。

輸入探索和輸入內容縮減

某些動作比只輸入一組輸入內容來得複雜。動作輸入組合的變更有兩種形式:

  • 動作可能會在執行之前發現新的輸入內容,或判斷其部分輸入內容實際上不是必要項目。此標準範例為 C++,其中較適合預估 C++ 檔案從轉換閉包使用哪些標頭檔案,以免我們重新將每個檔案傳送至遠端執行程式。因此,我們不將每個標頭檔案登錄為「輸入」,而是在傳輸中將每個標頭檔案註冊為「輸入」,而掃描來源檔案只將 #include 做為「輸入內容」的預先輸入標頭檔案。
  • 動作可能會發現執行過程中並未有人用到某些檔案。在 C++ 中,這稱為「.d 檔案」:編譯器會告知在建構完成後使用了哪些標頭檔案,避免 Bazel 使用這種做法,避免產生比 Make 更糟糕的漸進式。這比 include 掃描器依賴編譯器,因此提供更好的預估結果。

這些函式是透過「動作」上的方法來實作:

  1. 系統會呼叫 Action.discoverInputs()。它應會傳回一組確定需要的巢狀 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 的建構函式中進行。動作結構定義類型以 Java Class 執行個體識別,該執行個體會參照 ActionContext 的子介面,以及動作結構定義必須實作的介面。
  2. 系統會從可用的動作內容中選取適當的動作內容,並轉送至 ActionExecutionContextBlazeExecutor
  3. 動作要求背景資訊,使用 ActionExecutionContext.getContext()BlazeExecutor.getStrategy() (執行這其實應該只是其中一種方式...)

策略可以自由呼叫其他策略來執行工作;例如,在動態策略中會在本機和遠端啟動動作,然後以先完成者為準。

值得注意的策略是實作持續性工作站程序 (WorkerSpawnStrategy) 的做法。因為有些工具的啟動時間很長,因此建議在操作之間重複使用,而不是在每個動作之間啟動一個新的 (這可能代表潛在的正確問題,因為 Bazel 仰賴工作站處理程序中的承諾,因為 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() 中進一步處理:系統會篩除分析失敗的目標,並將測試拆分為專屬和非專屬測試。系統會放入 AnalysisResult 中,讓 ExecutionTool 如何知道要執行的測試。

為了讓這個程序更加透明,可以使用 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 叫用執行,要求在「主要」建構之後執行測試。這會在 SkyframeExecutor.runExclusiveTest() 中實作。

一般動作會在動作完成後轉儲終端機輸出內容,但使用者可以要求串流測試的輸出內容,以便收到長時間執行測試的進度通知。這是透過 --test_output=streamed 指令列選項指定,並隱含專屬測試執行,因此不同測試的輸出內容不會交錯。

此方法是在適當命名的 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,由該 QueryFunction 呼叫以取得要傳回的結果。

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

在某些查詢輸出格式 (Pro,proto 來說) 還有一項細微的需求:Bazel 必須發出「所有」_套件載入所提供的資訊,以便差異比較輸出,並判斷特定目標是否有所變更。 因此,屬性值必須可以序列化,因此只有少數屬性類型具有複雜的 Starlark 值。常見的解決方法是使用標籤,並將複雜資訊附加到有該標籤的規則。這個解決方法不好是好事 如果能解除這項規定

模組系統

只要在 Bazel 中新增模組,即可擴充 Bazel。每個模組都必須將 BlazeModule 設為子類別 (名稱是 Bazel 在呼叫 Blaze 時的歷史記錄),並在指令執行期間取得各種事件的相關資訊。

主要用於實作只有部分 Bazel 版本 (例如 Google 使用的版本) 所需的各種「非核心」功能:

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

BlazeModule 提供的延長點數組合可能有些危險。不建議做為優質設計原則的例子。

活動匯流排

BlazeModule 與 Bazel 的其餘部分通訊的主要方式就是事件匯流排 (EventBus):系統會為每個建構作業建立新的執行個體,Bazel 的不同部分可將事件發布至該執行個體,模組也能為感興趣的事件註冊事件監聽器。舉例來說,下列項目會以事件表示:

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

Build Event Protocol (建構事件通訊協定) 中,部分事件會在 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 在 $OUTPUT_BASE/external/<repository name> 下建立目錄。

擷取存放區的步驟如下:

  1. PackageLookupFunction 瞭解其需要一個存放區,並以 SkyKey 的形式建立 RepositoryName,以叫用 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」處理。

代管目錄

有時外部存放區需要修改工作區根目錄下的檔案 (例如套件管理工具,會在來源樹狀結構的子目錄中存放已下載的套件)。這不符合假設 Bazel 的假設,是因為 Bazel 只會由使用者修改來源檔案,而不會自行修改,而且可讓套件參照工作區根目錄底下的每個目錄。為使這類外部存放區正常運作,Bazel 會執行以下兩項作業:

  1. 允許使用者指定不允許存取工作區 Bazel 的子目錄。會在名為 .bazelignore 的檔案中列出,且功能會在 BlacklistedPackagePrefixesFunction 中實作。
  2. 我們會將工作區子目錄中的對應關係編碼到外部存放區,交由 ManagedDirectoriesKnowledge 處理,並處理參照一般外部存放區的 FileStateValue 的方式。

存放區對應

可能會有多個存放區都想依附同一個存放區,但在不同版本中 (這是「鑽石依附元件問題」的執行個體)。例如,如果建構中不同存放區中的兩個二進位檔想要依附 Guava,則它們都會參照開頭為 @guava// 的標籤 Guava,並預期其不同版本。

因此,Bazel 允許一個重新對應外部存放區標籤,讓字串 @guava// 可以參照一個二進位檔存放區中的一個 Guava 存放區 (例如 @guava1//),以及另一個二進位檔存放區 (例如 @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),則必須注意不會進行 stdout。

簡短訊息 (錯誤、警告等類似內容) 會透過 EventHandler 介面呈現。值得注意的是,這些訊息與寄給 EventBus 的貼文不同 (令人困惑)。每個 Event 都有 EventKind (錯誤、警告、資訊和其他幾個項目),且可能具有 Location (在導致事件發生的原始碼中)。

部分 EventHandler 實作會儲存收到的事件。系統會運用各種快取處理程序 (例如由快取設定的目標來發出警示),向 UI 重播資訊。

部分 EventHandler 也會允許發布事件,最終會前往活動匯流排 (一般 Event 「不會」__出現)。這些是 ExtendedEventHandler 的實作內容,其主要用途是重播快取的 EventBus 事件。這些 EventBus 事件全都實作 Postable,但不是所有發布至 EventBus 的內容都會實作這個介面。只有由 ExtendedEventHandler 快取的事件會比較好,大部分動作都會這麼做,但不會強制執行。

終端機輸出內容「主要」會透過 UiEventHandler 發出,這適用於 Bazel 的所有精細輸出格式和進度回報作業。其中包含兩種輸入內容:

  • 活動匯流排
  • 透過回報器將事件串流填入其中

指令執行機器 (例如 Bazel 的其他部分) 只能透過 Reporter.getOutErr() 將遠端程序呼叫 (RPC) 串流至用戶端,藉此直接存取這些串流。只有在指令需要傾印大量可能的二進位資料 (例如 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. 其中一個是使用 src/test/shell 下方精細的 bash 測試架構實作
  2. 以 Java 實作。這些類別會實作為 BuildIntegrationTestCase 的子類別

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

分析測試會以 BuildViewTestCase 的子類別實作。您可以使用暫存檔案系統編寫 BUILD 檔案,然後讓各種輔助方法可以要求設定的目標、變更設定,以及斷言分析結果的各種資訊。