使用 Bzlmod 管理外部依附元件

Bzlmod 是 Bazel 5.0 中導入的新外部依附元件系統的程式碼名稱。這項技術是為瞭解決舊有系統無法逐步修正的幾個問題。詳情請參閱原始設計文件的「問題陳述」部分

在 Bazel 5.0 中,預設不會啟用 Bzlmod;必須指定 --experimental_enable_bzlmod 旗標,這項變更才會生效。如同標記名稱所知,這項功能目前仍在實驗階段;在功能正式發布前,API 和行為可能會隨時變動。

Bazel 模組

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

「模組」基本上是一種可含有多個版本的 Bazel 專案,每個專案都會發布依附於其他模組的相關中繼資料。這與其他依附元件管理系統中的熟悉概念類似:Maven 「Artifact」、npm「Package」、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 )。此外,系統不會強制執行主要、次要及修補版本增加的語意。但是如要進一步瞭解回溯相容性,請參閱相容性等級的相關說明。 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)
  • 本機存放區名稱:存放區中的 BUILD.bzl 檔案使用的存放區名稱。同一個依附元件可以依據不同存放區使用不同的本機名稱。
    判斷如下:

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

每個存放區都有直接依附元件的存放區對應字典,該目錄是從本機存放區名稱對應至標準存放區名稱的對應。建構標籤時,我們會使用存放區對應來解析存放區名稱。請注意,標準存放區名稱沒有發生衝突,且只要剖析 MODULE.bazel 檔案即可找出本機存放區名稱的使用情況,因此可輕鬆找出衝突狀態並加以解決,且不影響其他依附元件。

嚴格的報導

我們可以透過新的依附元件規格格式執行更嚴格的檢查作業。請特別注意,我們現在強制要求只能使用由直接依附元件建立的存放區。這有助於在大眾運輸相依性圖表變更時,避免意外且難以偵錯。

系統會根據存放區對應來實作嚴格的深度程序。基本上,每個存放區的存放區對應包含所有直接依附元件,其他存放區不會顯示。每個存放區的可見依附元件如下:

  • Bazel 模組存放區可以透過 bazel_depuse_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.1B 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.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 指令宣告這些存放區。以達到嚴格的 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.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) 中的所有模組,每個模組都是一個採用 tagsbazel_module 物件欄位會公開模組中的所有 maven.* 標記。然後,我們叫用 CLI 公用程式,與 Maven 聯絡並解決問題。最後,我們會使用解析度結果建立假設,並使用假設的 maven_single_jar 存放區規則。