Bzlmod 是 Bazel 5.0 中導入的新外部依附元件系統的程式碼名稱。這項技術是為瞭解決舊有系統無法逐步修正的幾個問題。詳情請參閱原始設計文件的「問題陳述」部分。
在 Bazel 5.0 中,預設不會啟用 Bzlmod;必須指定 --experimental_enable_bzlmod
旗標,這項變更才會生效。如同標記名稱所知,這項功能目前仍在實驗階段;在功能正式發布前,API 和行為可能會隨時變動。
Bazel 模組
傳統的 WORKSPACE
外部依附元件系統以存放區 (或存放區) 為中心,透過存放區規則建立 (或「存放區規則」)。存放區仍然是新系統中的重要概念,但「模組」則是依附元件的核心單位。
「模組」基本上是一種可含有多個版本的 Bazel 專案,每個專案都會發布依附於其他模組的相關中繼資料。這與其他依附元件管理系統中的熟悉概念類似:Maven 「Artifact」、npm「Package」、Cargo crate 、Go 模組等。
模組只會使用 name
和 version
組合 (而非 WORKSPACE
中的特定網址) 來指定其依附元件。然後,您就可以在 Bazel 登錄檔中查詢依附元件;根據預設,Bazel Central Registry。在工作區中,每個模組都會變為存放區。
MODULE.bazel
每個模組的每個版本都有一個 MODULE.bazel
檔案,用於宣告其依附元件和其他中繼資料。以下是一個基本範例:
module(
name = "my-module",
version = "1.0",
)
bazel_dep(name = "rules_cc", version = "0.0.1")
bazel_dep(name = "protobuf", version = "3.19.0")
MODULE.bazel
檔案必須位於工作區目錄的根目錄 (位於 WORKSPACE
檔案旁) 中。與 WORKSPACE
檔案不同的是,您不需要指定轉換依附元件;請改為指定「直接」依附元件,並處理依附元件的 MODULE.bazel
檔案以自動探索大眾運輸依附元件。
MODULE.bazel
檔案與 BUILD
檔案類似,因為該檔案不支援任何形式的控制流程;此版本還禁止 load
陳述式。MODULE.bazel
檔案支援的指令如下:
module
,用於指定目前模組的中繼資料,包括名稱、版本等;bazel_dep
,用來指定其他 Bazel 模組的直接依附元件;- 僅供根模組使用 (也就是只做為依附元件使用的模組) 自訂特定直接或間接依附元件的行為:
- 與模組擴充功能相關的指令:
版本格式
Bazel 的生態系統各不相同,且專案使用多種版本管理架構。目前最熱門的功能是 SemVer,但也有一些使用不同配置的顯眼專案,例如 Abseil,其版本為以日期為依據,例如 20210324.2
)。
因此,Bzlmod 採用較寬鬆的 SemVer 規格版本,特別是在版本「發布」部分中的任意數字序列數目,而非 SemVer 規定提供的 3 個數字:MAJOR.MINOR.PATCH
)。此外,系統不會強制執行主要、次要及修補版本增加的語意。但是如要進一步瞭解回溯相容性,請參閱相容性等級的相關說明。 SemVer 規格的其他部分 (例如代表預先發布版的連字號) 則不受影響。
版本解析度
鑽石依附元件問題是版本依附元件管理空間中的缺點。假設您有下列依附元件圖表:
A 1.0
/ \
B 1.0 C 1.1
| |
D 1.0 D 1.1
應使用哪個版本的 D?為解決這個問題,Bzlmod 使用 Go 模組系統中導入的最小版本選取 (MVS) 演算法。MVS 假設模組的所有新版本都具備回溯相容性,所以只會挑選任何相依關係的最高版本 (在本範例中為 1.1)。由於 D 1.1 是可以滿足我們需求的最小版版本,因此稱之為「極低」。即使 D 1.2 或以上版本,我們也不會選取這些版本。這樣的好處是,選取版本為高傳真和可重現。
版本解析度會在本機電腦 (而非登錄檔) 執行。
相容性等級
請注意,MVS 對回溯相容性的假設是可行的,因為這樣做可將回溯不相容的模組版本視為獨立的模組。就 SemVer 而言,這表示 A 1.x 和 A 2.x 可視為不同的模組,並可在解析的依附元件圖表中共存。因此,則會在 Go 的套件路徑中對主要版本進行編碼,因此沒有任何編譯時間或連結時間衝突。
Bazel 不提供這類保證。因此,我們需要一個方法來指出「主要版本」編號,以便偵測回溯不相容的版本。這個數字稱為相容性等級,各模組版本在其 module()
指令中會指定。掌握這些資訊後,如果系統發現同一個模組中的版本具有不同的相容性層級,問題解決機制中就會顯示錯誤。
存放區名稱
在 Bazel 中,每個外部依附元件都有存放區名稱。有時候,相同的存放區可能透過不同的存放區名稱使用 (例如 @io_bazel_skylib
和 @bazel_skylib
都代表 Bazel skylib),或者存放區名稱可用於不同專案中的不同依附元件。
在 Bzlmod 中,存放區可由 Bazel 模組和模組擴充功能產生。為解決存放區名稱衝突,我們正在採用新系統中的存放區對應機制。以下提供兩個重要概念:
標準存放區名稱:每個存放區的全域不重複存放區名稱, 這會是存放區所在的目錄名稱。
其結構如下 (警告:標準名稱格式並非您應該依賴的 API,隨時可能變動):- 如果是 Bazel 模組存放區:
module_name.version
(範例)。@bazel_skylib.1.0.3
) - 模組延伸存放區相關資訊:
module_name.version.extension_name.repo_name
(範例:@rules_cc.0.0.1.cc_configure.local_config_cc
)
- 如果是 Bazel 模組存放區:
本機存放區名稱:存放區中的
BUILD
和.bzl
檔案使用的存放區名稱。同一個依附元件可以依據不同存放區使用不同的本機名稱。
判斷如下:
每個存放區都有直接依附元件的存放區對應字典,該目錄是從本機存放區名稱對應至標準存放區名稱的對應。建構標籤時,我們會使用存放區對應來解析存放區名稱。請注意,標準存放區名稱沒有發生衝突,且只要剖析 MODULE.bazel
檔案即可找出本機存放區名稱的使用情況,因此可輕鬆找出衝突狀態並加以解決,且不影響其他依附元件。
嚴格的報導
我們可以透過新的依附元件規格格式執行更嚴格的檢查作業。請特別注意,我們現在強制要求只能使用由直接依附元件建立的存放區。這有助於在大眾運輸相依性圖表變更時,避免意外且難以偵錯。
系統會根據存放區對應來實作嚴格的深度程序。基本上,每個存放區的存放區對應包含所有直接依附元件,其他存放區不會顯示。每個存放區的可見依附元件如下:
- Bazel 模組存放區可以透過
bazel_dep
和use_repo
查看MODULE.bazel
檔案引入的所有存放區。 - 模組擴充功能存放區可以看見提供該模組的所有可見依附元件,以及相同模組擴充功能產生的所有其他存放區存放區。
登錄檔
Bzlmod 向 Bazel 登錄檔要求取得資訊,藉此探索依附元件。Bazel 註冊資料庫只是 Bazel 模組的資料庫。目前唯一支援的登錄檔類型是索引登錄檔,即特定格式的本機目錄或靜態 HTTP 伺服器。我們預計日後會增加對單一模組登錄檔的支援,簡單來說,這個存放區會包含專案的來源和歷史記錄。
索引登錄
索引註冊資料庫是本機目錄或靜態 HTTP 伺服器,其中包含模組清單的相關資訊,包括首頁的首頁、維護者、各個版本的 MODULE.bazel
檔案,以及如何擷取各個來源的來源版本。值得注意的是,這「不需要」提供來源封存檔。
索引登錄檔必須採用以下格式:
/bazel_registry.json
:包含登錄檔中繼資料的 JSON 檔案。目前只有一個金鑰mirrors
,指定要用於來源封存的鏡像清單。/modules
:包含此登錄檔中每個模組的子目錄。/modules/$MODULE
:包含每個模組各子目錄的目錄和下列檔案:metadata.json
:包含模組相關資訊的 JSON 檔案,內含下列欄位:homepage
:專案首頁的網址。maintainers
:JSON 物件清單,每個物件都會對應到登錄檔中的模組維護人員資訊。請注意,這不一定是專案的作者。versions
:在這個登錄檔中找到的所有模組版本清單。yanked_versions
:這個模組的加密版本清單。這項做法目前還沒有任何功能,但未來推出的版本將遭略過或產生錯誤。
/modules/$MODULE/$VERSION
:包含下列檔案的目錄:MODULE.bazel
:這個模組版本的MODULE.bazel
檔案。source.json
:包含如何擷取此模組版本來源的 JSON 檔案,其中包含下列欄位:url
:來源封存網址。integrity
:封存的子資源完整性檢查碼。strip_prefix
:擷取來源封存時要移除的目錄前置字串。patches
:字串清單,每個字串都會命名要套用至擷取封存檔案的修補檔案。修補程式檔案位於/modules/$MODULE/$VERSION/patches
目錄下。patch_strip
:與 Unix 修補程式的--strip
引數相同。
patches/
:選用修補程式目錄,包含修補檔案。
Bazel Central Registry
Bazel Central Registry (BCR) 是位於 registry.bazel.build 的索引登錄檔, 其內容會由 GitHub 存放區 bazelbuild/bazel-central-registry
支援。
BCR 是由 Bazel 社群維護;歡迎貢獻者提交提取要求。請參閱 Bazel Central Registry 政策與程序。
除了一般索引註冊資料庫的格式以外,BCR 還會為每個模組版本 (/modules/$MODULE/$VERSION/presubmit.yml
) 提供 presubmit.yml
檔案。這個檔案會指定一些必要的建構和測試目標,協助您確認模組版本的有效性,並用於 BCR 的持續整合管道,確保 BCR 中的模組之間的互通性的 Google Ads 新帳戶重新申請驗證。
選取登錄檔
可重複的 Bazel 旗標 --registry
可指定用於要求模組的登錄檔清單,方便您設定專案從第三方或內部登錄檔擷取依附元件。較舊的註冊資料庫會優先採用。為方便起見,您可以在專案的 .bazelrc
檔案中加入 --registry
旗標清單。
模組擴充功能
模組擴充功能可讓您從依附元件圖表中的模組讀取輸入資料、執行必要邏輯以解決依附元件,以及呼叫呼叫存放區來建立存放區,藉此擴充模組系統。這類函式的運作方式與今天的 WORKSPACE
巨集類似,但比較適合用於模組和大眾運輸依附元件。
模組擴充功能會定義在 .bzl
檔案中,就像存放區規則或 WORKSPACE
巨集一樣。不會直接叫用;每個模組皆可指定名為「標記」的資料片段,方便擴充功能讀取。完成模組版本解析後,系統便會執行模組擴充功能。每項擴充功能會在模組解析後執行 (執行期間尚未實際產生),並會讀取整個依附元件圖表中的所有相關標記。
[ A 1.1 ]
[ * maven.dep(X 2.1) ]
[ * maven.pom(...) ]
/ \
bazel_dep / \ bazel_dep
/ \
[ B 1.2 ] [ C 1.0 ]
[ * maven.dep(X 1.2) ] [ * maven.dep(X 2.1) ]
[ * maven.dep(Y 1.3) ] [ * cargo.dep(P 1.1) ]
\ /
bazel_dep \ / bazel_dep
\ /
[ D 1.4 ]
[ * maven.dep(Z 1.4) ]
[ * cargo.dep(Q 1.1) ]
在上方的依附元件圖表中,A 1.1
和 B 1.2
是 Bazel 模組;您可以將每個模組視為 MODULE.bazel
檔案。每個模組都可指定部分模組標記;其中有些指定為「maven」擴充功能,有些則指定為「cargo」 這個依附元件圖表完成轉換後 (例如 B 1.2
其實在 D 1.3
上有 bazel_dep
,但因為 C
而升級為 D 1.4
),執行「maven」擴充功能時,會根據當中的資訊判斷要建立哪個存放區,以便讀取所有 maven.*
標記。與「cargo」額外資訊也同樣適用這項屬性。
擴充功能使用情形
擴充功能是由 Bazel 模組代管,因此如要在模組中使用擴充功能,您必須先在該模組中新增 bazel_dep
,然後呼叫 use_extension
內建函式,可提供給這個範圍。例如,假設 MODULE.bazel
檔案中的程式碼片段使用 rules_jvm_external
模組中定義的假設「maven」假設擴充功能:
bazel_dep(name = "rules_jvm_external", version = "1.0")
maven = use_extension("@rules_jvm_external//:extensions.bzl", "maven")
將擴充功能納入範圍後,您就可以使用點號語法來指定它的標記。請注意,標記必須符合相應標記類別定義的結構定義 (請參閱下方的擴充功能定義)。以下範例說明如何指定部分 maven.dep
和 maven.pom
標記。
maven.dep(coord="org.junit:junit:3.0")
maven.dep(coord="com.google.guava:guava:1.2")
maven.pom(pom_xml="//:pom.xml")
如果擴充功能會產生要在模組中使用的存放區,請使用 use_repo
指令宣告這些存放區。以達到嚴格的 Dep 條件,並避免本機存放區發生衝突。
use_repo(
maven,
"org_junit_junit",
guava="com_google_guava_guava",
)
擴充功能產生的存放區是其 API 的一部分,因此,您應從指定的標記中知道「maven」擴充功能會產生名為「org_junit_junit」的存放區,另一個名為「com_google_guava_guava」 ]。使用 use_repo
時,您可以選擇在模組範圍內將其重新命名,例如這裡的「guava」。
擴充功能定義
模組擴充功能與存放區規則的定義類似,使用 module_extension
函式。兩者都設有導入功能;但是,雖然存放區規則有許多屬性,但模組擴充功能有 tag_class
屬性,每個屬性都有一些屬性。標記類別定義了這項擴充功能使用的標記結構定義。延續上述假設「虛構」擴充功能的範例:
# @rules_jvm_external//:extensions.bzl
maven_dep = tag_class(attrs = {"coord": attr.string()})
maven_pom = tag_class(attrs = {"pom_xml": attr.label()})
maven = module_extension(
implementation=_maven_impl,
tag_classes={"dep": maven_dep, "pom": maven_pom},
)
這些宣告會明確使用上述屬性屬性來指定 maven.dep
和 maven.pom
標記。
實作函式與 WORKSPACE
巨集類似,差別在於其取得 module_ctx
物件後,即會授予依附元件圖表和所有相關標記的存取權。實作函式接著會呼叫存放區規則,以便產生存放區:
# @rules_jvm_external//:extensions.bzl
load("//:repo_rules.bzl", "maven_single_jar")
def _maven_impl(ctx):
coords = []
for mod in ctx.modules:
coords += [dep.coord for dep in mod.tags.dep]
output = ctx.execute(["coursier", "resolve", coords]) # hypothetical call
repo_attrs = process_coursier(output)
[maven_single_jar(**attrs) for attrs in repo_attrs]
在上面的示例中,我們說明瞭依附元件圖表 (ctx.modules
) 中的所有模組,每個模組都是一個採用 tags
的 bazel_module
物件欄位會公開模組中的所有 maven.*
標記。然後,我們叫用 CLI 公用程式,與 Maven 聯絡並解決問題。最後,我們會使用解析度結果建立假設,並使用假設的 maven_single_jar
存放區規則。