使用 Bzlmod 管理外部依附元件

Bzlmod 是 Bazel 5.0 中引入的新外部依附元件系統代號。本文介紹了舊系統的幾個無法漸進式修正的問題點,詳情請參閱原始設計文件中的「問題陳述式」一節

在 Bazel 5.0 中,系統不會預設啟用 Bzlmod,您必須指定 --experimental_enable_bzlmod 旗標,下列設定才會生效。根據標記名稱的建議,這項功能目前還在「實驗階段」。在這項功能正式發布前,API 和行為可能會有所變動。

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

Bazel 模組

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

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

模組只會使用 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 中,這已經放寬,不允許使用字母,且比較語意符合「prerelease」部分的「identifiers」。
  • 此外,系統不會強制增加主要版本、次要版本和修補程式版本的語意。(不過,請參閱相容性等級,進一步瞭解我們如何表示回溯相容性)。

任何有效的 SemVer 版本都是有效的 Bazel 模組版本。此外,如果與 Bazel 模組版本相比,兩者的保留相同,有兩個 SemVer 版本 ab 會比較 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 的回溯相容性假設是可行的,因為 MVS 會將回溯不相容的模組版本視為個別模組。就 SemVer 而言,1.x 和 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)
  • 顯式存放區名稱:要用於存放區的 BUILD.bzl 檔案中的存放區名稱。同一個依附元件在不同存放區中可能有不同的名稱。
    審查進度如下:

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

每個存放區都有一個存放區,對應其直接依附元件的字典,這是從顯目存放區名稱到標準存放區名稱的對應。建構標籤時,我們會使用存放區對應來解析存放區名稱。請注意,標準存放區名稱不會發生衝突,並且可透過剖析 MODULE.bazel 檔案找出顯而易見的存放區名稱使用情形,因此很容易發現並解決衝突,而不會影響其他依附元件。

嚴格依附元件

新的依附元件規格格式可讓我們執行更嚴格的檢查。值得注意的是,現在我們強制模組只能使用透過其直接依附元件建立的存放區。如此一來,當遞移依附元件圖中的項目變更時,就能防止意外和難以偵錯的故障情形。

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

  • 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:此模組的 yanked 版本清單。這目前為免人工管理,但日後,系統會略過短暫版本或產生錯誤。
  • /modules/$MODULE/$VERSION:含有下列檔案的目錄:
    • MODULE.bazel:此模組版本的 MODULE.bazel 檔案。
    • source.json:包含如何擷取此模組版本來源資訊的 JSON 檔案。
      • 預設類型為「封存」,包含下列欄位:
        • url:來源封存檔案的網址。
        • integrity:封存的子資源完整性檢查碼。
        • strip_prefix:擷取來源封存時要移除的目錄前置字串。
        • patches:字串清單,每個字串都會命名要套用至擷取封存的修補程式檔案。修補程式檔案位於 /modules/$MODULE/$VERSION/patches 目錄下。
        • patch_strip:與 Unix 修補程式的 --strip 引數相同。
      • 可將類型變更為含有以下欄位的本機路徑:
        • typelocal_path
        • path:存放區的本機路徑,計算方式如下:
          • 如果路徑是絕對路徑,則會依原樣使用。
          • 如果路徑是相對路徑,且 module_base_path 是絕對路徑,則路徑會解析為 <module_base_path>/<path>
          • 如果路徑和 module_base_path 都是相對路徑,路徑會解析為 <registry_path>/<module_base_path>/<path>。註冊資料庫必須在本機託管,並由 --registry=file://<registry_path> 使用。否則 Bazel 會擲回錯誤。
    • patches/:包含修補程式檔案的選用目錄,只有在 source.json 類型為「封存」時才能使用。

Bazel Central Registry

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

BCR 是由 Bazel 社群維護;協作者歡迎提交提取要求。請參閱 Bazel Central Registry 政策與程序

除了遵循一般索引註冊資料庫的格式之外,BCR 還需要每個模組版本 (/modules/$MODULE/$VERSION/presubmit.yml) 的 presubmit.yml 檔案。這個檔案指定幾個必要的建構和測試目標,可用於檢查這個模組版本是否有效,並由 BCR 的持續整合管道用於確保 BCR 中的模組之間互通。

選取註冊資料庫

重複的 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.1B 1.2 等都是 Bazel 模組,您可以將每個模組想成 MODULE.bazel 檔案。每個模組都可以指定模組擴充功能的一些標記;這裡有為「maven」擴充功能指定標記,有些則是為「cargo」指定標記。完成這個依附元件圖表後 (例如,也許 B 1.2D 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")

將擴充功能加入範圍後,您就可以使用 dot-syntax 指定擴充功能的標記。請注意,標記必須遵循對應標記類別所定義的結構定義 (請參閱下方的擴充功能定義)。以下為指定 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")

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

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,每個項目都有多項屬性。標記類別定義了這項擴充功能使用的標記結構定義。延續上例中的假設性「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 物件,該物件會授予依附元件圖表和所有相關標記的存取權。實作函式接著應呼叫存放區規則來產生存放區:

# @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 存放區規則。