使用 Bazel 建構程式

回報問題 查看原始碼 Nightly · 8.0 . 7.4 . 7.3 · 7.2 · 7.1 · 7.0 · 6.5

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

快速入門導覽課程

如要執行 Bazel,請前往基礎 workspace 目錄或其任何子目錄,然後輸入 bazel。如需建立新工作區,請參閱build

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 的 JVM 選項。
  • 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 會executes編譯器和其他建構工具。

在建構執行階段中,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

這是空白版本。由於沒有任何變更,因此沒有要重新載入的套件,也沒有要執行的建構步驟。如果「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:wiz 如果 foo/bar/wiz 是套件
  • //foo/bar:wiz 如果 foo/bar 是套件
  • 其他情況則為 //foo:bar/wiz
bar:all 等同於 //foo/bar:all
:all 等同於 //foo:all
...:all 等同於 //foo/...:all
... 等同於 //foo/...:all
bar/...:all 等同於 //foo/bar/...:all

根據預設,系統會追蹤目錄符號連結的遞迴目標模式,但會略過指向輸出基礎下方的模式,例如在工作區根目錄中建立的便利符號連結。

此外,如果目錄中含有以下名稱的檔案,Bazel 在評估遞迴目標模式時,不會追蹤符號連結:DONT_FOLLOW_SYMLINKS_WHEN_TRAVERSING_THIS_DIRECTORY_VIA_A_RECURSIVE_TARGET_PATTERN

foo/...packages 的萬用字元,表示目錄 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"] 時,含有 tags = ["manual"] 的目標不會包含在萬用字元目標模式 (...:*:all 等) 中 (但會包含在負值萬用字元目標模式中,也就是會減去)。如果您希望 Bazel 建構/測試這些測試目標,請在指令列上使用明確的目標模式指定這些測試目標。相反地,bazel query 不會自動執行任何這類篩選作業 (否則就會違背 bazel query 的用途)。

擷取外部依附元件

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

如果您在建構期間禁止擷取,且 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 fetch。不過,如果您使用的是工作區目錄以外的任何項目,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"

接著,請建構 tarball,其中包含特定 Bazel 版本的隱含執行階段依附元件:

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)。
  • 請將 --host_cpu 的值用於 --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 時間。

其次,您可以透過在所有建構中分離執行和要求設定,避免因要求設定 (例如變更連結器選項) 發生輕微變更而導致的昂貴重建作業,如前文所述。

修正增量重建作業

Bazel 專案的主要目標之一,就是確保正確的增量重建作業。先前的建構工具 (尤其是以 Make 為基礎的工具) 在實作增量建構時,會做出一些不正確的假設。

首先,檔案的時間戳記會單調遞增。雖然這是一般情況,但很容易違反這項假設;如果同步處理檔案的舊修訂版本,會導致該檔案的修改時間減少;以 Make 為基礎的系統不會重建。

更一般來說,Make 會偵測檔案的變更,但不會偵測指令的變更。如果您在特定建構步驟中變更傳遞至編譯器的選項,Make 就不會重新執行編譯器,您必須使用 make clean 手動捨棄先前建構作業的無效輸出內容。

此外,Make 無法在子程序開始寫入輸出檔案後,針對其中一個子程序的結束作業失敗提供完善的防護。雖然 Make 目前的執行作業會失敗,但後續的 Make 叫用會盲目地假設截斷的輸出檔案有效 (因為它比輸入內容新),因此不會重新建構。同樣地,如果 Make 程序遭到終止,也會發生類似情況。

Bazel 會避免這些假設和其他假設。Bazel 會維護一個資料庫,其中包含先前完成的所有工作,並且只會在發現該建構步驟的輸入檔案集合 (及其時間戳記) 和該建構步驟的編譯指令完全符合資料庫中的一個項目,以及該資料庫項目的輸出檔案集合 (及其時間戳記) 完全符合磁碟上的檔案時間戳記時,才會略過該建構步驟。只要輸入檔案、輸出檔案或指令本身有任何變更,系統就會重新執行建構步驟。

使用者使用正確的漸進式建構功能,可避免因混淆而浪費時間。(此外,使用 make clean 時,無論是否為必要或預防性,等待重建作業的時間也會縮短)。

建構一致性和漸進式建構

正式來說,如果所有預期的輸出檔案都存在,且內容正確,則我們會將建構作業的狀態定義為「一致」,這項定義是根據建立這些檔案所需的步驟或規則所指定。編輯來源檔案時,建構狀態會變得「不一致」,直到下次執行建構工具並成功完成為止。我們將這種情況稱為「不穩定的一致性」,因為這只是暫時性的,且執行建構工具即可恢復一致性。

還有另一種不一致性會造成嚴重後果:穩定的不一致性。如果建構作業達到穩定的不一致狀態,重複成功叫用建構工具不會恢復一致性:建構作業已「卡住」,且輸出內容仍不正確。穩定的不一致狀態是使用者使用 Make (和其他建構工具) 類型 make clean 的主要原因。發現建構工具以這種方式失敗 (然後從中復原) 可能會耗費許多時間,而且非常令人沮喪。

從概念上來說,要達到一致的建構作業,最簡單的方法就是丟棄先前的所有建構作業輸出內容,然後重新開始:讓每個建構作業都是乾淨的建構作業。這種做法顯然耗時太久,因此不切實際 (除非是發布工程師)。因此,建構工具必須能夠執行增量建構作業,且不犧牲一致性。

正確執行增量依附元件分析相當困難,而且如上所述,許多其他建構工具在增量建構期間,無法有效避免穩定的不一致狀態。相反地,Bazel 提供以下保證:在您未進行任何編輯的情況下,成功叫用建構工具後,建構作業會處於一致的狀態。(如果您在建構期間編輯原始碼檔案,Bazel 不會保證目前建構結果的一致性。但可以保證下一個版本的結果會恢復一致性)。

如同所有保證事項,這裡也有一些細則:有幾種已知的方法可讓 Bazel 進入穩定的不一致狀態。我們不會保證針對在逐步依附元件分析中刻意找出錯誤而產生的問題進行調查,但我們會調查並盡力修正因正常或「合理」使用建構工具而產生的所有穩定不一致狀態。

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

沙箱執行

Bazel 會使用沙箱,確保動作能以密封且正確的方式執行。Bazel 會在沙箱中執行產生 (廣義來說:動作),而沙箱中只包含工具執行工作所需的最少檔案集合。目前沙箱功能適用於已啟用 CONFIG_USER_NS 選項的 Linux 3.12 以上版本,以及 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 停用其他規則。另外,請在問題追蹤工具中回報錯誤,並提及您使用的 Linux 發行版本,以便我們進行調查,並在後續版本中提供修正方式。

建構作業的階段

在 Bazel 中,建構作業會分成三個階段進行。使用者只要瞭解這三個階段的差異,就能掌握控制建構作業的選項 (請參閱下文)。

載入階段

第一個階段是載入,在此階段中,系統會載入、剖析、評估及快取初始目標的所有必要 BUILD 檔案,以及這些檔案的依附元件的傳遞閉包。

在 Bazel 伺服器啟動後進行第一次建構時,由於許多 BUILD 檔案會從檔案系統載入,因此載入階段通常需要花費數秒的時間。在後續建構作業中,如果沒有任何 BUILD 檔案變更,載入作業會非常快速。

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

分析階段

第二個階段是「分析」,包括對每個建構規則進行語意分析和驗證、建構建構相依性圖表,以及確定在建構過程的每個步驟中要執行哪些工作。

與載入作業一樣,完整的分析作業也需要幾秒鐘才能完成。不過,Bazel 會快取從一個版本到下一個版本的相依關係圖,並只重新分析必要的部分,因此如果套件自上一個版本以來未變更,就能讓漸進式建構作業的速度大幅提升。

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

載入和分析階段的速度很快,因為 Bazel 會在此階段避免不必要的檔案 I/O,只讀取 BUILD 檔案,以便判斷要執行的工作。這是設計上的考量,可讓 Bazel 成為分析工具的良好基礎,例如 Bazel 的 query 指令,這是在載入階段實作的。

執行階段

建構作業的第三個也是最後一個階段是執行。這個階段可確保建構作業中每個步驟的輸出內容與輸入內容一致,並視需要重新執行編譯/連結等工具。這個步驟是建構作業花費大部分時間的階段,從幾秒到大規模建構作業的一個多小時不等。這個階段會回報的錯誤包括:缺少來源檔案、某些建構動作執行工具中的錯誤,或是工具無法產生預期的輸出集合。