使用 Bzlmod 管理外部依附元件

回報問題 查看原始碼 Nightly · 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 規則) 建立。雖然在新的系統中,儲存庫仍是重要的概念,但模組才是依附元件的核心單位。

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

模組只需使用 nameversion 組合指定依附元件,而非 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 檔案支援的指令如下:

版本格式

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)
  • Apparent 存放區名稱:要在存放區內 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 物件清單,每個物件都對應至 在 Registry 中模組維護者的資訊。請注意,這不一定與專案的作者相同。
      • 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 類型為「封存」時才能使用。

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.3bazel_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.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 存放區規則,利用解析結果建立多個存放區。