使用 Bazel 建構程式

回報問題 查看原始碼

本頁面說明如何使用 Bazel、建構指令語法和目標模式語法來建構程式。

快速入門導覽課程

如要執行 Bazel,請前往基本 workspace 目錄或其任一子目錄,然後輸入 bazel。如需建立新工作區,請參閱建構相關說明。

bazel help
                             [Bazel release bazel version]
Usage: bazel command options ...

可使用的指令

  • analyze-profile:分析建構設定檔資料。
  • aquery:對「分析後」動作圖表執行查詢。
  • build:建構指定目標。
  • canonicalize-flags:將 Bazel 標記標準化。
  • clean:移除輸出檔案,並視需要停止伺服器。
  • cquery:執行後分析依附元件圖查詢。
  • dump:轉儲 Bazel 伺服器程序的內部狀態。
  • help:顯示指令或索引的說明。
  • info:顯示 Bazel 伺服器的執行階段資訊。
  • fetch:擷取目標的所有外部依附元件。
  • mobile-install:在行動裝置上安裝應用程式。
  • query:執行依附關係圖查詢。
  • run:執行指定的目標。
  • shutdown:停止 Bazel 伺服器。
  • test:建構並執行指定的測試目標。
  • version:列印 Bazel 的版本資訊。

取得協助

  • bazel help command:列印 command 的說明和選項。
  • bazel helpstartup_options:託管 Bazel 的選項。
  • bazel helptarget-syntax:說明指定目標的語法。
  • bazel help info-keys:顯示 info 指令使用的按鍵清單。

bazel 工具可執行許多稱為指令的函式。最常用的是 bazel buildbazel test。您可以使用 bazel help 瀏覽線上說明訊息。

建立目標

開始建構前,您需要有工作區。工作區是目錄樹狀結構,其中包含建構應用程式所需的所有來源檔案。Bazel 可讓您透過完全唯讀的磁碟區執行建構作業,

如要使用 Bazel 建構程式,請輸入 bazel build 和您要建構的目標

bazel build //foo

發出指令來建構 //foo 之後,您會看見類似以下的輸出內容:

INFO: Analyzed target //foo:foo (14 packages loaded, 48 targets configured).
INFO: Found 1 target...
Target //foo:foo up-to-date:
  bazel-bin/foo/foo
INFO: Elapsed time: 9.905s, Critical Path: 3.25s
INFO: Build completed successfully, 6 total actions

首先,Bazel 會載入目標依附元件圖表中的所有套件。這包括「已宣告的依附元件」、直接列在目標 BUILD 檔案的檔案,以及「遞移依附元件」,也就是目標依附元件 BUILD 檔案中所列的檔案。找出所有依附元件後,Bazel 會分析這些依附元件的正確性並建立建構動作。最後,Bazel 會執行該建構的編譯器和其他工具。

在建構的執行階段,Bazel 會輸出進度訊息。進度訊息會在開始時提供目前的建構步驟 (例如編譯器或連結器),以及透過建構動作總數套用完成的數字。建構作業開始時,當 Bazel 探索整個動作圖時,操作總數通常會增加,但數量在幾秒內就會穩定下來。

在建構作業結束時,Bazel 會顯示要求的目標 (不論目標是否已建構成功),以及可以找到輸出檔案的位置。執行建構作業的指令碼可以穩定剖析此輸出內容,詳情請參閱 --show_result

如果再次輸入相同指令,建構作業就會更快完成。

bazel build //foo
INFO: Analyzed target //foo:foo (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
Target //foo:foo up-to-date:
  bazel-bin/foo/foo
INFO: Elapsed time: 0.144s, Critical Path: 0.00s
INFO: Build completed successfully, 1 total action

此為「空值」null build版本,由於沒有任何變更,因此不需要重新載入套件,也不需要執行建構步驟。如果「foo」或依附元件發生變更,Bazel 會重新執行部分建構動作,或完成漸進式建構作業

建立多個目標

Bazel 允許以多種方式指定要建構的目標。這些模式統稱為「目標模式」。這個語法用於 buildtestquery 等指令。

標籤是用來指定個別目標,例如在 BUILD 檔案中宣告依附元件時,Bazel 的目標模式則會指定多個目標。目標模式是目標組合標籤語法的通用化,使用萬用字元。在最簡單的情況下,任何有效的標籤也是有效的目標模式,也就是只識別一組目標。

所有以 // 開頭的目標模式都會根據目前的工作區解析。

//foo/bar:wiz 只有單一目標 //foo/bar:wiz
//foo/bar 等同於 //foo/bar:bar
//foo/bar:all foo/bar」套件中的所有規則目標。
//foo/... foo 目錄下方所有套件中的所有規則目標。
//foo/...:all foo 目錄下方所有套件中的所有規則目標。
//foo/...:* foo 目錄下所有套件中的所有目標 (規則和檔案)。
//foo/...:all-targets foo 目錄下所有套件中的所有目標 (規則和檔案)。
//... 工作區中套件中的所有目標。但不包含外部存放區的目標。
//:all 如果工作區的根目錄有「BUILD」檔案,則頂層套件中的所有目標。

開頭不是 // 的目標模式會根據目前的「工作目錄」解析。這些範例假設有一個 foo 的工作目錄:

:foo 等同於 //foo:foo
bar:wiz 等同於 //foo/bar:wiz
bar/wiz 等同於:
  • 如果 foo/bar/wiz 為套件,則為 //foo/bar/wiz:wiz
  • 如果 foo/bar 為套件,則為 //foo/bar:wiz
  • //foo:bar/wiz (若以上)
bar:all 等同於 //foo/bar:all
:all 等同於 //foo:all
...:all 等同於 //foo/...:all
... 等同於 //foo/...:all
bar/...:all 等同於 //foo/bar/...:all

根據預設,遞迴目標模式會遵循目錄符號連結,但不包括指向輸出基礎底下的項目,例如在工作區根目錄中建立的便利符號連結。

此外,在包含名稱為 DONT_FOLLOW_SYMLINKS_WHEN_TRAVERSING_THIS_DIRECTORY_VIA_A_RECURSIVE_TARGET_PATTERN 的目錄中評估遞迴目標模式時,Bazel 不會追蹤符號連結:

foo/... 是「套件」上的萬用字元,表示所有套件均以遞迴方式位於目錄 foo (適用於套件路徑的所有根層級)。:all 是「目標」的萬用字元,會比對套件中的所有規則。這兩者可合併,就像在 foo/...:all 中一樣,如果同時使用兩個萬用字元,可以縮寫為 foo/...

此外,:* (或 :all-targets) 是符合相符套件中「每個目標」的萬用字元,包括通常不是由任何規則建構的檔案,例如與 java_binary 規則相關聯的 _deploy.jar 檔案。

這表示 :* 表示 :all 的「超級集合」。雖然這個語法可能會造成混淆,但這個語法允許將熟悉的 :all 萬用字元用於一般建構作業,因為不需要用到 _deploy.jar 等建構目標。

此外,Bazel 也允許使用斜線來代替標籤語法所需的冒號。使用 Bash 檔案名稱擴充時,這個做法通常很方便。舉例來說,foo/bar/wiz 等於 //foo/bar:wiz (如果有 foo/bar 套件) 或 //foo:bar/wiz (如果有套件 foo)。

許多 Bazel 指令都會接受目標模式清單做為引數,這些指令都會套用前置字串否定運算子 -。可用於從上述引數指定的組合中,減去一組目標。請注意,這表示順序很重要。比如

bazel build foo/... bar/...

意指「建立 foo 底下的所有目標以及 bar 底下的所有目標」,

bazel build -- foo/... -foo/bar/...

這表示「建構 foo 底下的所有目標,但 foo/bar 底下的目標除外」(必須使用 -- 引數,以免將以 - 開頭的後續引數解讀為其他選項)。

不過,請特別注意,以這種方式減去目標並不保證它們不會建構,因為它們可能是未減去的目標依附元件。例如,如果有一個 //foo:all-apis 目標依附於 //foo/bar:api,則後者會建構做為前者的一部分。

如果在 bazel buildbazel test 等指令中指定,含有 tags = ["manual"] 的目標不包含在萬用字元目標模式 (...:*:all 等) 中 (但會把它們納入排除萬用字元目標模式,之後就會減去)。如要讓 Bazel 進行建構/測試,則應在指令列中使用明確的目標模式指定這類測試目標。相反地,bazel query 不會自動執行這類篩選 (這會破壞 bazel query 的用途)。

擷取外部依附元件

根據預設,Bazel 會在建構期間下載外部依附元件,並使用符號連結。不過,這並非理想的情況,因為您想知道何時新增外部依附元件,或是想「預先擷取」依附元件 (例如,您會在航班離線之前)。如要在建構期間避免新增依附元件,您可以指定 --fetch=false 標記。請注意,此標記僅適用於未指向本機檔案系統中的目錄的存放區規則。舉例來說,對 local_repositorynew_local_repository,以及 Android SDK 和 NDK 存放區規則的變更,無論 --fetch 值為何都會生效。

如果您在建構期間禁止擷取,且 Bazel 找到新的外部依附元件,建構作業就會失敗。

執行 bazel fetch 即可手動擷取依附元件。如果您在建構擷取期間禁止使用,就必須執行 bazel fetch

  • 首次建構前的注意事項
  • 新增外部依附元件後。

執行後,您只有在 WORKSPACE 檔案變更之前都不需要再執行一次。

fetch 會擷取要擷取依附元件的目標清單。舉例來說,這個指令會擷取建構 //foo:bar//bar:baz 所需的依附元件:

bazel fetch //foo:bar //bar:baz

如要擷取工作區的所有外部依附元件,請執行以下指令:

bazel fetch //...

使用 Bazel 7.1 以上版本 (如果已啟用 Bzlmod),您也可以執行

bazel fetch

如果工作區根目錄底下已經有使用的所有工具 (例如程式庫 jar 到 JDK 本身),就不必執行 bazel 擷取。不過,如果您使用工作區目錄以外的任何項目,Bazel 將自動在執行 bazel build 之前執行 bazel fetch

存放區快取

Bazel 會嘗試多次擷取相同的檔案,即使不同的工作區需要相同的檔案,或是外部存放區的定義有所變更,但仍需相同的檔案才能下載。為此,bazel 會快取存放區快取中的所有檔案 (根據預設,這些檔案位於 ~/.cache/bazel/_bazel_$USER/cache/repos/v1/)。您可以使用 --repository_cache 選項變更位置。這個快取會在所有工作區和已安裝的 Bazel 版本之間共用。 如果 Bazel 知道項目是否包含正確檔案副本,就會從快取中擷取項目;也就是說,如果下載要求具有指定檔案的 SHA256 總和,且含有該雜湊的檔案就會存在快取中。因此,從安全性的角度來看,為每個外部檔案指定雜湊只是個好方法,也有助於避免不必要的下載作業。

每次快取命中時,系統就會更新快取中檔案的修改時間。透過這種方式,您可以輕鬆判斷快取目錄中檔案的上次使用狀況,例如手動清除快取。系統一律不會自動清理快取,因為其中可能包含已無法再上游使用的檔案副本。

發布檔案目錄

發布目錄是另一種 Bazel 機制,可避免不必要的下載作業。Bazel 會先搜尋發布目錄,再搜尋存放區快取。主要差異在於發布目錄需要手動做好準備。

透過 --distdir=/path/to-directory 選項,您可以指定尋找檔案的其他唯讀目錄,而不用擷取檔案。如果檔案名稱等於網址的基本名稱,且檔案的雜湊與下載要求中指定的檔案相同,就會從這類目錄擷取檔案。只有在 WORKSPACE 宣告中指定了檔案雜湊時,才能使用這個方法。

雖然檔案名稱中的條件並非必要,但此做法會將候選檔案數量減少為每個指定目錄一個。透過這種方式,即使這類目錄中的檔案數量變大,指定發布版本檔案目錄仍能維持效率。

在氣隙環境中執行 Bazel

為縮減 Bazel 的二進位檔大小,系統會在首次執行時透過網路擷取 Bazel 的隱含依附元件。這些隱含依附元件包含不一定每個人都需要的工具鍊和規則。舉例來說,Android 工具只有在建構 Android 專案時才會拆分,並且受到擷取。

不過,即使您已為所有的 WORKSPACE 依附元件執行廠商,這些隱含依附元件在在冷氣的環境中執行 Bazel 也可能會發生問題。為解決此問題,您可以在具有網路存取權的機器上準備一個包含這些依附元件的發行目錄,然後使用離線方法將這些依附元件移至封裝的環境。

如要準備發布目錄,請使用 --distdir 標記。您必須為每個新的 Bazel 二進位檔版本執行一次操作,因為每個版本的隱含依附元件可能不同。

如要在傳輸的環境外建構這些依附元件,請先查看正確的 Bazel 來源樹狀結構:

git clone https://github.com/bazelbuild/bazel "$BAZEL_DIR"
cd "$BAZEL_DIR"
git checkout "$BAZEL_VERSION"

然後建構包含該特定 Bazel 版本隱含執行階段依附元件的 tarball:

bazel build @additional_distfiles//:archives.tar

將此 tarball 匯出到可複製到播出的環境中的目錄。請注意 --strip-components 旗標,因為 --distdir 與目錄巢狀層級可能相當精細:

tar xvf bazel-bin/external/additional_distfiles/archives.tar \
  -C "$NEW_DIRECTORY" --strip-components=3

最後,在轉換的環境中使用 Bazel 時,請傳送指向該目錄的 --distdir 標記。為了方便起見,您可以將其新增為 .bazelrc 項目:

build --distdir=path/to/directory

建構設定和跨編譯

所有指定特定版本行為和結果的輸入內容,都可分為兩個不同的類別。第一種是儲存在專案 BUILD 檔案中的內建資訊:建構規則、其屬性值,以及其遞移依附元件的完整組合。第二種是由使用者或建構工具提供的外部或環境資料:選擇目標架構、編譯和連結選項,以及其他工具鍊設定選項。我們將完整的環境資料稱為「設定」

任一版本都可能會有多個設定。假設您使用跨平台程式碼編譯為 64 位元架構的 //foo:bin 執行檔,但您的工作站是 32 位元機器,很明顯,建構作業必須使用能夠建立 64 位元執行檔的工具鍊建構 //foo:bin,但建構系統也會自行建構在建構期間使用的各種工具 (例如透過原始碼建構的工具,隨後再用於 Genrule),而必須建構這些工具才能在工作站上執行。因此,我們可以找出兩種設定:執行設定、用於建構在建構期間執行的工具,以及「目標設定」(或「目標設定」,但即使該字詞已有許多含意,我們仍要更頻繁地說「目標設定」),後者可用來建構您最終要求的二進位檔。

一般而言,許多程式庫同時是要求的建構目標 (//foo:bin) 和一或多個執行工具的必備條件,例如一些基礎程式庫。這類程式庫必須建構兩次,一次用於執行設定,另一次則用於目標設定。Bazel 會負責確保兩個變數都會建構,且衍生檔案會分開保留,以避免干擾;這類目標通常可以同時建構,因為它們彼此獨立。如果您看到進度訊息,指出指定目標正在建構兩次,這很可能是說明原因。

執行設定衍生自目標設定,如下所示:

  • 除非已指定 --host_crosstool_top,否則請使用要求設定中指定的 Crosstool (--crosstool_top) 版本。
  • 針對 --cpu 使用 --host_cpu 的值 (預設值:k8)。
  • 請使用要求設定中指定的與這些選項中的相同值:--compiler--use_ijars;如果使用 --host_crosstool_top,則系統會使用 --host_cpu 的值,在 Crosstool 中查詢執行設定的 default_toolchain (忽略 --compiler)。
  • 針對 --javabase,使用 --host_javabase 的值
  • 針對 --java_toolchain,使用 --host_java_toolchain 的值
  • 使用 C++ 程式碼 (-c opt) 的最佳化版本。
  • 不產生偵錯資訊 (--copt=-g0)。
  • 從執行檔和共用程式庫中清除偵錯資訊 (--strip=always)。
  • 請將所有衍生檔案放在特殊位置,與任何可能要求設定所使用的檔案不同。
  • 抑制含有建構資料的二進位檔 (請參閱 --embed_* 選項)。
  • 其他設定值則全部保留預設值。

基於許多原因,我們建議您在要求設定中選取不同的執行設定。最重要的是:

首先,使用經過最佳化的二進位檔,您可以減少連結和執行工具所需的時間、工具佔用的磁碟空間,以及分散式建構作業中的網路 I/O 時間。

其次,將 exec 和要求設定分離所有建構中的 exec 和要求設定,可避免因要求設定進行小幅變更 (例如變更連結器選項),而必須進行小幅變更 (例如變更連結器選項) 的重新建構作業,如前所述。

修正漸進式重新建構功能

Bazel 專案的主要目標之一,就是確保以正確的方式進行漸進式重新建構作業。先前的建構工具 (特別是以 Make 為基礎的工具) 在實作漸進式版本的實作時,會做出幾項無聲的假設。

首先,檔案中的時間戳記只會遞增。雖然這是典型案例,但很容易出錯;同步處理檔案較早的修訂版本會縮短檔案的修改時間;Make 型系統不會重新建構。

更廣泛來說,Make 偵測檔案變更時,不會偵測指令的變更。如果您修改了在特定建構步驟中傳遞至編譯器的選項,Make 不會重新執行編譯器,而且必須使用 make clean 手動捨棄先前版本的無效輸出內容。

此外,如果子程序開始寫入輸出檔案,Make 本身無法成功終止其其中一個子程序。目前的 Make 執行作業會失敗,但後續叫用 Make 時將無法得知截斷的輸出檔案是否有效 (因為比其輸入內容新),而且不會重新建構。同樣地,如果終止 Make 程序,也可能會發生類似情況。

Bazel 可避免這些假設等等。Bazel 會保留先前完成的所有工作的資料庫,只有在找到該建構步驟的輸入檔案 (及其時間戳記) 的組合、該建構步驟的編譯指令,以及資料庫中的一個完全符合,且資料庫項目的輸出檔案 (及其時間戳記) 與磁碟上的檔案時間戳記組合時,才會省略建構步驟。對輸入檔案、輸出檔案或指令本身做出任何變更,都會重新執行建構步驟。

使用正確的漸進式版本對使用者的好處是:您因為混淆而浪費時間。(此外,無論是必要或預先啟動,使用 make clean 所造成的重建作業也更少了)。

建構一致性與漸進式建構作業

在表單上,如果所有預期的輸出檔案都存在,且其內容正確無誤,我們正式將建構的狀態定義為「一致」,具體取決於建立這些檔案的步驟或規則。當您編輯來源檔案時,系統會將建構作業的狀態顯示為不一致,而且在您下次執行建構工具成功完成前會保持一致。我們將這種情況描述為不穩定的一致性,因為它只是暫時的,而執行建構工具即可還原一致性。

還有另一種常見的不一致問題:穩定的不一致。如果版本達到穩定不一致的狀態,則重複成功叫用建構工具就無法還原一致性:建構作業遭到「阻斷」,且輸出內容仍不正確。穩定狀態不一致是 Make (和其他建構工具) 類型 make clean 使用者的主要原因。發現建構工具無法以這種方式執行 (然後復原) 會耗費大量時間,而且相當令人困擾。

從概念上來說,如要達成一致的版本,最簡單的方法就是捨棄所有先前的建構輸出內容並重新開始:讓每個建構作業都變成乾淨的建構作業。這個做法顯然非常耗時 (發布工程師可能除外),因此為實用,因此建構工具必須在不影響一致性的情況下執行漸進式建構作業。

修正漸進式依附元件分析並不容易,如上所述,許多其他建構工具在逐步建構期間避免穩定不一致的狀態。相較之下,Bazel 會提供以下保證:成功叫用您未進行任何編輯的建構工具後,建構作業就會呈現一致的狀態。(如果您在建構期間編輯來源檔案,Bazel 無法保證目前建構作業的結果的一致性。但不保證 next 版本的結果能夠還原一致性)。

和所有保證一樣,還有一些小細節:有些已知的方式讓 Bazel 處於穩定不一致的狀態。我們無法保證調查因嘗試透過漸進式依附元件分析找出錯誤而造成的這類問題,但我們會調查並盡力修正因正常使用或「合理」使用建構工具而產生且不一致的所有狀態。

如果您發現 Bazel 偵測到穩定的狀態不一致,請回報錯誤。

沙箱機制執行

Bazel 會使用沙箱確保動作可正常運作。Bazel 會在沙箱中執行 spawns (正常說:動作),而沙箱中僅包含執行工具所需的最少檔案組合。目前沙箱功能適用於 Linux 3.12 以上版本 (已啟用 CONFIG_USER_NS 選項) 以及 macOS 10.11 以上版本。

如果您的系統不支援沙箱機制,Bazel 就會輸出警告,讓您瞭解建構作業不保證會具有密封性,而且可能會以不明方式影響主機系統。如要停用此警告,請將 --ignore_unsupported_sandboxing 標記傳送給 Bazel。

在部分平台 (例如 Google Kubernetes Engine 叢集節點或 Debian) 上,使用者命名空間預設會基於安全性考量停用。您可以查看 /proc/sys/kernel/unprivileged_userns_clone 檔案來確認這一點:如果檔案存在且包含 0,則可使用 sudo sysctl kernel.unprivileged_userns_clone=1 啟用使用者命名空間。

在某些情況下,Bazel 沙箱可能會因系統設定而無法執行規則。這些問題通常是輸出類似 namespace-sandbox.c:633: execvp(argv[0], argv): No such file or directory 的訊息失敗。在這種情況下,請嘗試使用 --strategy=Genrule=standalone 停用 Genrules 的沙箱,然後使用 --spawn_strategy=standalone 停用其他規則。此外,請透過我們的 Issue Tracker 回報錯誤,並註明所使用的 Linux 發行版,以便我們調查問題並在後續版本中提供修正。

建構階段

在 Bazel 中,建構作業會在三個不同的階段發生。身為使用者,瞭解各版本之間的差異後,即可深入瞭解控管建構作業的選項 (請參閱下方說明)。

載入階段

第一種是「載入」,其中初始目標的所有必要 BUILD 檔案以及其對依附元件的遞移性關閉,都會在此期間載入、剖析、評估及快取。

在 Bazel 伺服器啟動後第一次建構時,載入階段通常需要幾秒鐘的時間,與從檔案系統載入的 BUILD 檔案一樣多。在後續建構作業中,載入速度非常快,尤其是在沒有變更 BUILD 檔案的情況下。

這個階段回報的錯誤包括:找不到套件、找不到目標、BUILD 檔案中的詞法與文法錯誤,以及評估錯誤。

分析階段

第二階段是「分析」,涉及每個建構規則的語意分析和驗證、建構依附元件圖的建構,以及確定建構的每個步驟所需要完成的工作。

就像載入一樣,進行完整計算時也需要幾秒鐘的分析時間。然而,Bazel 會將依附元件圖表從一個建構快取到下一版本,並且只重新分析所需的內容,因此如果套件自上次建構後未變更,就能以極快的速度逐步進行漸進式建構作業。

這個階段回報的錯誤包括:不適當的依附元件、規則輸入無效,以及所有規則專屬錯誤訊息。

載入和分析階段的速度很快,因為 Bazel 會在這個階段避免使用不必要的檔案 I/O,並僅讀取 BUILD 檔案以判斷要完成的工作。這屬於設計性質,也讓 Bazel 成為分析工具 (例如 Bazel 的 query 指令) 的絕佳基礎,該指令會在載入階段上層實作。

執行階段

建構作業的第三及最後階段是「執行」。這個階段可確保建構中每個步驟的輸出內容與其輸入一致,並視需要重新執行編譯/連結等工具。這個步驟是建構作業花費大部分時間的位置,針對大型建構作業需要數秒鐘到超過一小時的時間。這個階段回報的錯誤包括:缺少來源檔案、某些建構動作所執行工具中的錯誤,或工具無法產生預期的輸出內容組合。