使用 Bzlmod 管理外部依附元件

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

Bzlmod 是 Bazel 5.0 中推出的新外部依附元件系統的代號。這項功能的推出目的,是為瞭解決舊系統中幾個無法逐步修正的問題。詳情請參閱原始設計文件的問題陳述書

在 Bazel 5.0 中,Bzlmod 並未預設為開啟;您必須指定標記 --experimental_enable_bzlmod,才能讓下列設定生效。如同旗標名稱所示,這項功能目前為實驗功能;在正式推出前,API 和行為可能會有所變更。

如要將專案遷移至 Bzlmod,請按照 Bzlmod 遷移指南操作。您也可以在 examples 存放區中找到 Bzlmod 使用範例。

Bazel 模組

舊版的 WORKSPACE 外部依附元件系統以存放區 (或 repos) 為中心,透過存放區規則 (或 repo 規則) 建立。雖然在新的系統中,Repo 仍是一項重要的概念,但模組才是依附元件的核心單位。

模組基本上是可以擁有多個版本的 Bazel 專案,每個版本都會發布關於所依附其他模組的中繼資料。這與其他依附元件管理系統中的常見概念類似:Maven 構件、npm 套件、Cargo crate、Go 模組等。

模組只需使用 nameversion 組合指定依附元件,而非 WORKSPACE 中的特定網址。接著,系統會在 Bazel 註冊中心中查詢依附元件,預設為 Bazel 中央註冊中心。在工作區中,每個模組都會轉換為一個存放區。

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 檔案支援的內容如下:

版本格式

Bazel 擁有多元的生態系統,專案會使用各種版本管理方案。目前最受歡迎的是 SemVer,但也有知名專案使用其他配置,例如 Abseil (其版本以日期為準,例如 20210324.2)。

因此,Bzlmod 採用了較為寬鬆的 SemVer 規格版本。差異包括:

  • SemVer 規定版本的「發布」部分必須包含 3 個部分:MAJOR.MINOR.PATCH。在 Bazel 中,這項規定已放寬,因此可允許任意數量的區段。
  • 在 SemVer 中,「release」部分的每個區段都必須只包含數字。在 Bazel 中,這項限制已放寬,因此也允許使用字母,且比較語義會與「預發布」部分的「ID」相符。
  • 此外,系統不會強制執行主要、次要和修補版本的語意。(不過,請參閱「相容性級別」,進一步瞭解我們如何標示回溯相容性)。

任何有效的 SemVer 版本都是有效的 Bazel 模組版本。此外,如果兩個 SemVer 版本 ab 以 Bazel 模組版本進行比較,則如果兩者相同,就會比較 a < b

版本解決方案

菱形依附元件問題是版本化依附元件管理空間中的常見問題。假設您有下列依附元件圖表:

       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)
    • 模組擴充功能 repo:module_name~version~extension_name~repo_name
      (範例@rules_cc~0.0.1~cc_configure~local_config_cc)
  • 明顯的存放區名稱:在存放區內的 BUILD.bzl 檔案中使用的存放區名稱。同一個依附元件在不同存放區中可能會有不同的顯示名稱。
    其判斷方式如下:

    • 針對 Bazel 模組 repos:預設為 module_name,或 bazel_deprepo_name 屬性指定的名稱。
    • 模組擴充功能存放區:透過 use_repo 引入的存放區名稱。

每個存放區都有其直接依附元件的存放區對應字典,這是從明顯的存放區名稱對應到標準存放區名稱的對應項目。我們會在建構標籤時使用存放區對應項目來解析存放區名稱。請注意,正規存放區名稱不會發生衝突,而且您可以透過剖析 MODULE.bazel 檔案,找出明顯存放區名稱的用法,因此您可以輕鬆偵測到衝突並加以解決,而不會影響其他依附元件。

嚴格 deps

新的依附元件規格格式可讓我們執行更嚴格的檢查。特別是,我們現在會強制規定模組只能使用從其直接依附元件建立的存放區。這有助於在傳遞式依附元件圖表中的某些內容發生變更時,避免意外發生且難以偵錯的錯誤。

嚴格依附元件是根據存放區對應實作。基本上,每個存放區的存放區對應都包含所有直接依附元件,其他存放區則不會顯示。每個存放區的可見依附元件會依下列方式決定:

  • Bazel 模組存放區可以透過 bazel_depuse_repo,查看 MODULE.bazel 檔案中引入的所有存放區。
  • 模組擴充功能存放區可以查看提供擴充功能的模組所有可見依附元件,以及同一個模組擴充功能產生的所有其他存放區。

登錄檔

Bzlmod 會向 Bazel 註冊表索取資訊,藉此找出依附元件。Bazel 登錄只是 Bazel 模組的資料庫。系統僅支援索引註冊表,也就是採用特定格式的本機目錄或靜態 HTTP 伺服器。我們未來會支援單一模組註冊表,這類註冊表只是包含專案來源和記錄的 Git 存放區。

索引登錄

索引註冊是本機目錄或靜態 HTTP 伺服器,其中包含模組清單的相關資訊,包括模組的首頁、維護者、各版本的 MODULE.bazel 檔案,以及如何擷取各版本的來源。值得注意的是,這項服務「不需要」提供原始封存檔案。

索引註冊表必須符合下列格式:

  • /bazel_registry.json:包含註冊表中繼資料的 JSON 檔案,例如:
    • mirrors,指定用於來源封存檔的鏡像清單。
    • module_base_path:在 source.json 檔案中,指定具有 local_repository 類型的模組的基本路徑。
  • /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 引數相同。
      • 您可以變更類型,使用內部路徑搭配下列欄位:
        • typelocal_path
        • path:存放區的本機路徑,計算方式如下:
          • 如果路徑是絕對路徑,系統會直接使用該路徑。
          • 如果 path 是相對路徑,而 module_base_path 是絕對路徑,path 會解析為 <module_base_path>/<path>
          • 如果 path 和 module_base_path 都是相對路徑,path 會解析為 <registry_path>/<module_base_path>/<path>。註冊服務必須在本機託管,並由 --registry=file://<registry_path> 使用。否則,Bazel 會擲回錯誤。
    • patches/:可選的目錄,其中包含修補檔案,僅在 source.json 為「archive」類型時才會使用。

Bazel 中央登錄表

Bazel Central Registry (BCR) 是位於 bcr.bazel.build 的索引註冊表。其內容由 GitHub 存放區 bazelbuild/bazel-central-registry 提供支援。

BCR 由 Bazel 社群維護;歡迎貢獻者提交合併要求。請參閱 Bazel 中央註冊政策和程序

除了遵循一般索引註冊表的格式,BCR 還需要為每個模組版本 (/modules/$MODULE/$VERSION/presubmit.yml) 建立 presubmit.yml 檔案。這個檔案會指定幾個必要的建構和測試目標,可用於驗證此模組版本的有效性,並由 BCR 的 CI 管道使用,確保 BCR 中模組之間的互通性。

選取登錄檔

可重複使用的 Bazel 標記 --registry 可用來指定要從中要求模組的註冊表清單,因此您可以設定專案,從第三方或內部註冊表擷取依附元件。較早的註冊項目優先。為方便起見,您可以在專案的 .bazelrc 檔案中放入 --registry 旗標清單。

模組擴充功能

模組擴充功能可讓您透過讀取依附元件圖表中模組的輸入資料、執行必要邏輯來解析依附元件,並最終透過呼叫 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.1B 1.2 等是 Bazel 模組;您可以將每個模組視為 MODULE.bazel 檔案。每個模組都可以為模組擴充功能指定一些標記;其中有些是為擴充功能「maven」指定,有些則是為「cargo」指定。當這個依附元件圖表定案後 (例如,B 1.2 實際上在 D 1.3 上有 bazel_dep,但因 C 而升級為 D 1.4),就會執行「maven」擴充功能,並讀取所有 maven.* 標記,利用其中的資訊決定要建立哪些 repos。同樣地,也適用於「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.depmaven.pom 標記的範例。

maven.dep(coord="org.junit:junit:3.0")
maven.dep(coord="com.google.guava:guava:1.2")
maven.pom(pom_xml="//:pom.xml")

如果擴充功能會產生您要在模組中使用的 repo,請使用 use_repo 指令宣告這些 repo。這是為了滿足嚴格依附元件條件,並避免本機存放區名稱發生衝突。

use_repo(
    maven,
    "org_junit_junit",
    guava="com_google_guava_guava",
)

擴充功能產生的 repo 是其 API 的一部分,因此從您指定的標記,您應該知道「maven」擴充功能會產生名為「org_junit_junit」和「com_google_guava_guava」的 repo。有了 use_repo,您可以選擇在模組的範圍內重新命名這些類別,例如將其改為「guava」。

擴充功能定義

模組擴充功能的定義與 repo 規則類似,使用 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.depmaven.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 存放區規則,利用解析結果建立多個存放區。