本頁面說明如何使用 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 help
startup_options
:JVM 託管 Bazel 的選項。bazel help
target-syntax
:說明指定目標的語法。bazel help info-keys
:顯示資訊指令使用的索引鍵清單。
bazel
工具會執行許多功能,稱為指令。最常用的是 bazel build
和 bazel 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
此為空值建構。由於沒有變更,因此沒有可重新載入的套件,也沒有可執行的建構步驟。如果「foo」或其依附元件有所變更,Bazel 會重新執行部分建構動作,或完成漸進式建構。
建立多個目標
Bazel 允許透過多種方式指定要建構的目標。這些統稱為「目標模式」。這個語法用於 build
、test
或 query
等指令。
標籤是用來指定個別目標,例如在 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 |
等同於:
|
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/...
是「套件」上的萬用字元,表示所有套件會以遞迴方式位於 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 build
和 bazel test
等指令指定時,萬用字元目標模式 (...
、:*
、:all
等) 不會納入萬用字元目標模式 (...
、:*
、:all
等)。如果您想讓 Bazel 建構/測試這類目標,應在指令列中指定這類測試目標。tags = ["manual"]
相反地,bazel query
不會自動執行這類篩選 (否則會失去 bazel query
的用途)。
擷取外部依附元件
根據預設,Bazel 會在建構期間下載外部依附元件的連結符號。不過,這可能不是理想做法,因為您可能想知道何時新增外部依附元件,或是想「預先擷取」依附元件 (例如在您離線的航班起飛前)。如要避免在建構期間新增依附元件,您可以指定 --fetch=false
標記。請注意,這個標記只適用於未指向本機檔案系統目錄的存放區規則。例如,local_repository
、new_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 //...
如果工作區根目錄下有您使用的所有工具 (從程式庫 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 時,仍可能會發生問題。如要解決這個問題,您可以在具備網路存取權的機器上準備包含這些依附元件的發布目錄,然後使用離線方法將這些依附元件轉移至 Airgapped 環境。
如要準備分佈目錄,請使用 --distdir
標記。每個新的 Bazel 二進位檔版本都必須執行此操作,因為每個版本的隱含依附元件可能都不同。
如要在 Airgapped 環境之外建構這些依附元件,請先檢查適當版本的 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 匯出到可以複製到 Airgapped 環境的目錄。請注意 --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 會確保兩個變數建構完成,並將衍生檔案分開以免造成乾擾;由於這類目標彼此獨立,通常可以並行建構這類目標。如果您看到進度訊息,指出系統正在兩次建構特定目標,這很可能是原因。
Bazel 會根據 --distinct_host_configuration
選項,使用兩種方式之一選取主機設定。這個布林值選項比較不明顯,而且設定可能會改善 (或降低) 建構速度。
--distinct_host_configuration=false
如果此選項為 false,主機和要求設定會相同:建構期間需要的所有工具都會採用與目標程式相同的建構方式。這項設定表示在單一建構作業中不必建構兩次程式庫。
不過,這也表示對要求設定進行任何變更也會影響主機設定,導致所有工具都必須重建,然後任何依賴工具輸出的項目也必須重建。因此,舉例來說,在建構作業之間只需變更連結器選項,就可能導致所有工具重新連結,然後使用這些工具的所有動作重新執行,如此一來,就會導致重新建構作業的規模變得非常大。
--distinct_host_configuration=true
(預設)
如果此選項為 true,則會使用完全不同的主機設定,而不是針對主機和要求使用相同的設定。主機設定會從目標設定衍生而來,如下所示:
- 除非已指定
--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 時間。
其次,將所有建構作業中的主機和要求設定分離,可避免因要求設定有些微變更而產生高昂的重新建構作業,例如變更連結器選項的行為,如前文所述。
但在某些版本上,這個選項可能比較複雜。特別是設定變更頻率不高的版本 (尤其是特定 Java 版本),以及在主機和目標設定中必須建構的程式碼量龐大的版本,可能不會有任何好處。
修正漸進式重建錯誤
Bazel 專案的其中一個主要目標是確保正確的漸進式重新建構。在實作漸進式建構作業時,先前的建構工具 (尤其是以 Make 為基礎的工具) 會在實作過程中做出幾項不公平的假設。
首先,檔案的時間戳記會隨時間增加。雖然這是一般情況,但很容易做出這項假設;如果與檔案較早的修訂版本同步,會導致檔案的修改時間縮短;「Make 型系統」不會重新建構。
更廣泛來說,雖然 Make 可以偵測檔案變更,但不會偵測指令變更。如果您在特定建構步驟中修改傳遞至編譯器的選項,Make 不會重新執行編譯器,因此您必須使用 make clean
手動捨棄先前建構的無效輸出內容。
此外,當子程序開始寫入其輸出檔案後,對於其中一個子程序的終止失敗情形,Make 會堅持不穩。雖然 Make 目前的執行作業會失敗,但後續的 Make 叫用會盲目地假設截斷的輸出檔案有效 (因為它比輸入內容新),因此不會重新建構。同樣地,如果 Make 程序遭到終止,也會發生類似情況。
Bazel 會避免這些假設和其他假設。Bazel 會維護一個資料庫,其中包含先前完成的所有工作,並且只會在發現該建構步驟的輸入檔案集 (及其時間戳記) 和該建構步驟的編譯指令完全符合資料庫中的一個項目,以及該資料庫項目的輸出檔案集 (及其時間戳記) 完全符合磁碟上的檔案時間戳記時,才會略過該建構步驟。只要輸入檔案、輸出檔案或指令本身有任何變更,系統就會重新執行建構步驟。
正確漸進式建構對使用者的好處是:比較不容易造成混淆。(此外,無論是有必要或先佔情況,使用 make
clean
導致的重新建構時間也縮短了)。
建構一致性及漸進式建構
正式來說,當所有預期的輸出檔案都存在,且內容正確 (如建立這些檔案所需的步驟或規則所指定) 時,我們會將建構作業的狀態定義為「一致」。當您編輯來源檔案時,系統會認為建構狀態不一致,並保持一致,直到您下次執行建構工具以順利完成為止。我們會將這種情況描述為不穩定不一致的情況,因為這只是暫時性的,而且透過執行建構工具可以還原一致性。
還有另一種常見的不一致情形:「穩定不一致」。如果建構作業達到穩定的不一致狀態,重複成功叫用建構工具不會恢復一致性:建構作業已「卡住」,且輸出內容仍不正確。至於穩定的狀態,是使用者採用 Make (及其他建構工具) 類型 make clean
的主要原因。發現建構工具以這種方式失敗 (然後從中復原) 可能會耗費許多時間,而且令人非常沮喪。
從概念上來說,要達到一致的建構作業,最簡單的方法就是丟棄先前的所有建構作業輸出內容,然後重新開始:讓每個建構作業都是乾淨的建構作業。這種做法顯然耗時太長,因此不切實際 (除非是發布工程師)。因此,建構工具必須能夠執行增量建構作業,且不犧牲一致性。
正確執行增量依附元件分析相當困難,而且如上所述,許多其他建構工具在增量建構期間,無法有效避免穩定的不一致狀態。相反地,Bazel 提供以下保證:在您未進行任何編輯的情況下,成功叫用建構工具後,建構作業會處於一致的狀態。(如果您在建構期間編輯原始碼檔案,Bazel 不會保證目前建構結果的一致性。但可以保證下一個版本的結果會恢復一致性)。
與所有保證一樣,有一些注意事項:在某些情況下,某些已知方法會讓 Bazel 發生狀態不一致的狀態。我們不保證會調查因故意在漸進式依附元件分析中發現錯誤而導致的問題,但我們會調查並盡力修正因正常或「合理」使用建構工具而產生的所有穩定不一致狀態。
如果您透過 Bazel 偵測到穩定不一致的狀態,請回報錯誤。
沙箱執行
Bazel 會使用沙箱,確保動作能以密封且正確的方式執行。Bazel 會在沙箱中執行 spawns (大致說法:動作),僅包含工具執行工作所需的最少檔案組合。目前沙箱功能適用於已啟用 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
和 --spawn_strategy=standalone
停用其他 Genrules 的沙箱。此外,請在我們的 Issue Tracker 中回報錯誤,並提及您使用的 Linux 發行版本,以便我們進行調查,並在後續版本中提供修正方式。
建構作業的階段
在 Bazel 中,建構作業會分成三個階段進行。使用者若瞭解這三個階段的差異,就能掌握控制建構作業的選項 (請參閱下文)。
載入階段
第一個階段是「載入」,在此階段中,系統會載入、剖析、評估及快取初始目標的所有必要 BUILD 檔案,以及這些檔案的依附元件傳遞關閉。
對於啟動 Bazel 伺服器後的第一個建構作業,載入階段通常需要幾秒鐘的時間,才能從檔案系統載入許多 BUILD 檔案。在後續的建構作業中,特別是在沒有 BUILD 檔案變更的情況下,載入速度就會非常快。
這個階段會回報的錯誤包括:找不到套件、找不到目標、BUILD 檔案中的字彙和文法錯誤,以及評估錯誤。
分析階段
第二個階段是「分析」,包括對每個建構規則進行語意分析和驗證、建構建構相依性圖表,以及確定在建構過程的每個步驟中要執行哪些工作。
與載入作業一樣,完整的分析作業也需要幾秒鐘才能完成。然而,Bazel 會將依附元件圖表從一個建構快取至下一個版本,而且只會重新分析其所需內容,這樣即使在先前建構後未變更套件,漸進式建構作業也能讓漸進式建構作業的執行速度非常快。
這個階段會回報的錯誤包括:不當的依附元件、規則的無效輸入內容,以及所有規則專屬的錯誤訊息。
由於 Bazel 會在這個階段避免不必要的檔案 I/O,因此只會讀取 BUILD 檔案,藉此判斷要完成的工作,因此載入和分析階段的速度很快。這是在設計上,讓 Bazel 成為分析工具的良好基礎,例如於載入階段上方實作的 Bazel 的 query 指令。
執行階段
建構作業的第三個也是最後一個階段是執行。這個階段可確保建構中每個步驟的輸出內容與輸入內容一致,並視需要重新執行編譯/連結等工具。這個步驟是建構作業花費大部分時間的階段,從幾秒到大規模建構作業的一個多小時不等。在此階段回報的錯誤包括:缺少來源檔案、某些建構動作執行的工具錯誤,或工具未能產生預期的輸出內容組合。