Bzlmod 是 Bazel 5.0 中導入的新外部依附元件系統的代號。推出這項功能是為瞭解決舊系統的幾個痛點,這些問題無法逐步修正;詳情請參閱原始設計文件的「問題陳述」部分。
在 Bazel 5.0 中,Bzlmod 預設不會開啟,因此必須指定 --experimental_enable_bzlmod
標記,才能讓下列項目生效。如旗標名稱所示,這項功能目前為實驗性;API 和行為可能會變更,直到功能正式推出為止。
如要將專案遷移至 Bzlmod,請按照 Bzlmod 遷移指南操作。 您也可以在 examples 存放區中找到 Bzlmod 用法範例。
Bazel 模組
舊版以 WORKSPACE
為基礎的外部依附元件系統,是以存放區 (或存放區) 為中心,透過存放區規則 (或存放區規則) 建立。
雖然在新的系統中,存放區仍是重要的概念,但模組是依附元件的核心單元。
模組基本上是 Bazel 專案,可以有多個版本,每個版本都會發布所依附其他模組的中繼資料。這與其他依附元件管理系統中的常見概念類似:Maven 構件、npm 套件、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
。在 Bazel 中,這項規定會放寬,允許任意數量的區隔。 - 在 SemVer 中,「發布」部分的每個區隔都只能是數字。 在 Bazel 中,這項限制會放寬,允許使用字母,且比較語意會與「prerelease」部分的「identifiers」相符。
- 此外,系統不會強制執行主要、次要和修補程式版本遞增的語意。(不過,請參閱相容性等級,瞭解我們如何標示回溯相容性。)
任何有效的 SemVer 版本都是有效的 Bazel 模組版本。此外,兩個 SemVer 版本 a
和 b
比較 a < b
iff 時,如果以 Bazel 模組版本比較時相同,則比較結果相同。
版本解析度
菱形依附元件問題是版本化依附元件管理領域的常見問題。假設您有下列依附元件圖表:
A 1.0
/ \
B 1.0 C 1.1
| |
D 1.0 D 1.1
應使用哪個版本的 D?為解決這個問題,Bzlmod 會使用 Go 模組系統中導入的最低版本選取 (MVS) 演算法。MVS 會假設模組的所有新版本都可向後相容,因此只會挑選任何依附元件指定的最高版本 (在本例中為 D 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 伺服器。我們計畫在未來支援單一模組登錄檔,也就是包含專案來源和記錄的 Git 存放區。
索引登錄
索引登錄檔是本機目錄或靜態 HTTP 伺服器,內含模組清單的相關資訊,包括首頁、維護者、各版本的 MODULE.bazel
檔案,以及如何擷取各版本的來源。值得注意的是,它「不需要」自行提供來源封存檔。
索引登錄檔必須採用以下格式:
/bazel_registry.json
:包含登錄中繼資料的 JSON 檔案,例如:mirrors
,指定要用於來源封存檔的鏡像清單。- ,在
source.json
檔案中指定local_repository
類型模組的基本路徑。module_base_path
/modules
:這個目錄包含此登錄檔中每個模組的子目錄。/modules/$MODULE
:這個目錄包含每個模組版本的子目錄,以及下列檔案:metadata.json
:包含模組相關資訊的 JSON 檔案, 具有下列欄位:homepage
:專案首頁的網址。maintainers
:JSON 物件清單,每個物件都對應至登錄檔中模組維護者的資訊。請注意,這不一定與專案的作者相同。versions
:這個登錄檔中所有模組版本的清單。yanked_versions
:這個模組的撤銷版本清單。目前這項作業不會執行任何動作,但日後系統會略過已撤銷的版本,或產生錯誤。
/modules/$MODULE/$VERSION
:包含下列檔案的目錄:MODULE.bazel
:這個模組版本的MODULE.bazel
檔案。source.json
:JSON 檔案,內含如何擷取這個模組版本來源的資訊。- 預設類型為「archive」,包含下列欄位:
url
:來源封存檔的網址。integrity
:封存檔的子資源完整性總和檢查碼。strip_prefix
:解壓縮來源封存檔時要移除的目錄前置字串。patches
:字串清單,每個字串都命名要套用至已擷取封存檔的修補程式檔案。修補程式檔案位於/modules/$MODULE/$VERSION/patches
目錄下。patch_strip
:與 Unix 修補程式的--strip
引數相同。
- 您可以變更類型,透過下列欄位使用本機路徑:
type
:local_path
path
:存放區的本機路徑,計算方式如下:- 如果路徑是絕對路徑,則會照常使用。
- 如果路徑是相對路徑,且
module_base_path
是絕對路徑,則路徑會解析為<module_base_path>/<path>
- 如果路徑和
module_base_path
都是相對路徑,路徑會解析為<registry_path>/<module_base_path>/<path>
。登錄檔必須在本機託管,並由--registry=file://<registry_path>
使用。 否則 Bazel 會擲回錯誤。
- 預設類型為「archive」,包含下列欄位:
patches/
:包含修補程式檔案的選用目錄,僅在source.json
具有「封存」類型時使用。
Bazel Central Registry
Bazel Central Registry (BCR) 是位於 bcr.bazel.build 的索引登錄檔。其內容由 GitHub 存放區 bazelbuild/bazel-central-registry
提供支援。
BCR 由 Bazel 社群維護,歡迎貢獻者提交提取要求。請參閱「Bazel Central Registry Policies and Procedures」。
除了遵循一般索引登錄檔的格式,BCR 也需要每個模組版本 (/modules/$MODULE/$VERSION/presubmit.yml
) 的 presubmit.yml
檔案。這個檔案會指定幾個重要的建構和測試目標,可用於健全性檢查這個模組版本的有效性,並由 BCR 的 CI 管道使用,確保 BCR 中模組之間的互通性。
選取登錄檔
可重複使用的 Bazel 旗標 --registry
可用來指定要從中要求模組的登錄檔清單,因此您可以設定專案,從第三方或內部登錄檔擷取依附元件。較早的登錄記錄優先。為方便起見,您可以將 --registry
旗標清單放在專案的 .bazelrc
檔案中。
模組擴充功能
模組擴充功能可讓您透過讀取依附元件圖中模組的輸入資料、執行必要邏輯來解析依附元件,並最終透過呼叫 repo 規則來建立 repo,進而擴充模組系統。它們的功能與現今的 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
實際上可能具有 bazel_dep
,但由於 C
而升級至 D 1.4
),系統會執行「maven」擴充功能,並讀取所有 maven.*
標記,然後使用其中的資訊決定要建立哪些 repo。D 1.3
「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
指令宣告這些存放區。這是為了滿足嚴格的依附元件條件,並避免本機存放區名稱衝突。
use_repo(
maven,
"org_junit_junit",
guava="com_google_guava_guava",
)
擴充功能產生的 repos 是其 API 的一部分,因此從您指定的標記,您應該知道「maven」擴充功能會產生名為「org_junit_junit」的 repo,以及名為「com_google_guava_guava」的 repo。使用 use_repo
時,您可以視需要在模組範圍內重新命名,例如這裡的「guava」。
擴充功能定義
模組擴充功能是使用 module_extension
函式定義,類似於存放區規則。兩者都有實作函式,但存放區規則具有多個屬性,模組擴充功能則具有多個 tag_class
,每個擴充功能都有多個屬性。標記類別會為這個擴充功能使用的標記定義結構定義。繼續上述假設的「maven」擴充功能範例:
# @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
物件,可授予依附元件圖表和所有相關標記的存取權。實作函式接著應呼叫 repo 規則來產生 repo:
# @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
) 中的所有模組,每個模組都是 bazel_module
物件,其 tags
欄位會公開模組上的所有 maven.*
標記。接著,我們會叫用 CLI 公用程式 Coursier,與 Maven 聯絡並執行解析作業。最後,我們使用解析結果,透過假設的 maven_single_jar
存放區規則建立多個存放區。