模組擴充功能

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

模組擴充功能可讓使用者透過以下方式擴充模組系統:讀取依附元件圖表中模組的輸入資料、執行必要的邏輯來解決依附元件,最後再透過呼叫存放區規則建立存放區。這些擴充功能的功能類似於 repo 規則,可執行檔案 I/O、傳送網路要求等。除了其他要素,Bazel 也能協助 Bazel 與其他套件管理系統互動,同時遵循透過 Bazel 模組建構的依附元件圖表。

您可以在 .bzl 檔案中定義模組擴充功能,就像設定檔規則一樣。這些模組並非直接叫用,而是由各模組指定稱為「標記」的資料片段,供擴充功能讀取。Bazel 會先執行模組解析作業,再評估任何擴充功能。擴充功能會讀取整個依附元件圖表中屬於該擴充功能的所有標記。

擴充功能用法

擴充功能是由 Bazel 模組代管。如要在模組中使用擴充功能,請先在代管擴充功能的模組中新增 bazel_dep,然後呼叫 use_extension 內建函式來納入該擴充功能的範圍。請參考以下範例:MODULE.bazel 檔案中的程式碼片段,可使用 rules_jvm_external 模組中定義的「maven」擴充功能:

bazel_dep(name = "rules_jvm_external", version = "4.5")
maven = use_extension("@rules_jvm_external//:extensions.bzl", "maven")

這會將 use_extension 的傳回值繫結至變數,讓使用者可使用 dot-syntax 指定擴充功能的標記。標記必須遵循擴充功能定義中指定的對應標記類別定義的結構定義。以下是指定部分 maven.installmaven.artifact 標記的範例:

maven.install(artifacts = ["org.junit:junit:4.13.2"])
maven.artifact(group = "com.google.guava",
               artifact = "guava",
               version = "27.0-jre",
               exclusions = ["com.google.j2objc:j2objc-annotations"])

使用 use_repo 指令,將擴充功能產生的 repos 納入目前模組的範圍。

use_repo(maven, "maven")

由擴充功能產生的重新產生的是其 API 的一部分。在這個範例中,「maven」模組擴充功能會產生名為 maven 的存放區。有了上述宣告,擴充功能就能正確解析 @maven//:org_junit_junit 等標籤,指向由「maven」擴充功能產生的 repo。

擴充功能定義

您可以使用 module_extension 函式,定義模組擴充功能,類似於設定存放區規則。不過,雖然存放區規則有多個屬性,但模組擴充功能有 tag_class,每個擴充功能都具備多個屬性。標記類別會為這個擴充功能使用的標記定義結構定義。舉例來說,上述的「maven」擴充功能可能會定義如下:

# @rules_jvm_external//:extensions.bzl

_install = tag_class(attrs = {"artifacts": attr.string_list(), ...})
_artifact = tag_class(attrs = {"group": attr.string(), "artifact": attr.string(), ...})
maven = module_extension(
  implementation = _maven_impl,
  tag_classes = {"install": _install, "artifact": _artifact},
)

這些宣告顯示,可以使用指定的屬性結構定義來指定 maven.installmaven.artifact 標記。

模組擴充功能的實作函式與存放區規則的實作函式類似,差別在於會取得 module_ctx 物件,可使用擴充功能和所有相關標記授予所有模組的存取權。實作函式會呼叫 repo 規則來產生 repo。

# @rules_jvm_external//:extensions.bzl

load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_file")  # a repo rule
def _maven_impl(ctx):
  # This is a fake implementation for demonstration purposes only

  # collect artifacts from across the dependency graph
  artifacts = []
  for mod in ctx.modules:
    for install in mod.tags.install:
      artifacts += install.artifacts
    artifacts += [_to_artifact(artifact) for artifact in mod.tags.artifact]

  # call out to the coursier CLI tool to resolve dependencies
  output = ctx.execute(["coursier", "resolve", artifacts])
  repo_attrs = _process_coursier_output(output)

  # call repo rules to generate repos
  for attrs in repo_attrs:
    http_file(**attrs)
  _generate_hub_repo(name = "maven", repo_attrs)

擴充功能身分

模組擴充功能可透過名稱和 .bzl 檔案識別,這些內容會顯示在 use_extension 呼叫中。在以下範例中,系統會透過 .bzl 檔案 @rules_jvm_external//:extension.bzl 和名稱 maven 識別擴充功能 maven

maven = use_extension("@rules_jvm_external//:extensions.bzl", "maven")

從不同的 .bzl 檔案重新匯出擴充功能會取得新的身分,如果兩個版本的擴充功能都用於遞移模組圖表中,系統會分別評估兩者,而且只會看到與該特定身分相關聯的標記。

做為擴充功能作者,請確保使用者只會使用單一 .bzl 檔案中的模組擴充功能。

存放區名稱和瀏覽權限

擴充功能產生的回覆會採用 module_repo_canonical_name~extension_name~repo_name 形式的標準名稱。如果是代管在根模組中的擴充功能,module_repo_canonical_name 部分會替換為字串 _main。請注意,標準名稱格式並非您應依附的 API,且名稱隨時可能變更。

這個命名政策代表每個擴充功能都有自己的「存放區命名空間」;兩個不同的擴充功能可分別定義名稱相同的存放區,而不會產生任何衝突。這也表示 repository_ctx.name 會回報原始碼存放區的正規名稱,而這與原始碼存放區規則呼叫中指定的名稱「不一樣」

將模組擴充功能產生的存放區納入考量,有幾個存放區瀏覽權限規則:

  • Bazel 模組存放區可以透過 bazel_depuse_repo,查看 MODULE.bazel 檔案中引入的所有存放區。
  • 模組擴充功能產生的存放區可以查看代管擴充功能的模組可見的所有存放區,加上由相同模組擴充功能產生的所有其他存放區 (使用存放區規則呼叫中指定的名稱做為其顯示名稱)。
    • 這可能會造成衝突。如果模組存放區可以看到名稱為 foo 的存放區,而擴充功能會產生名稱為 foo 的存放區,那麼對於該擴充功能產生的所有存放區,foo 會參照前者。
  • 同樣地,在模組擴充功能的實作函式中,擴充功能建立的 repos 可以透過屬性中的明顯名稱彼此參照,不受建立順序影響。
    • 如果與模組可見的存放區發生衝突,傳遞至存放區規則屬性的標籤可以納入對 Label 的呼叫中,以確保這些會參照模組可見的存放區,而不是由擴充功能產生的相同名稱存放區。

覆寫及插入模組擴充功能存放區

根模組可以使用 override_repoinject_repo 覆寫或插入模組擴充功能存放區。

範例:用供應商提供的文案取代 rules_javajava_tools

# MODULE.bazel
local_repository = use_repo_rule("@bazel_tools//tools/build_defs/repo:local.bzl", "local_repository")
local_repository(
  name = "my_java_tools",
  path = "vendor/java_tools",
)

bazel_dep(name = "rules_java", version = "7.11.1")
java_toolchains = use_extension("@rules_java//java:extension.bzl", "toolchains")

override_repo(java_toolchains, remote_java_tools = "my_java_tools")

範例:修補 Go 依附元件,使其依附 @zlib 而非系統 zlib

# MODULE.bazel
bazel_dep(name = "gazelle", version = "0.38.0")
bazel_dep(name = "zlib", version = "1.3.1.bcr.3")

go_deps = use_extension("@gazelle//:extensions.bzl", "go_deps")
go_deps.from_file(go_mod = "//:go.mod")
go_deps.module_override(
  patches = [
    "//patches:my_module_zlib.patch",
  ],
  path = "example.com/my_module",
)
use_repo(go_deps, ...)

inject_repo(go_deps, "zlib")
# patches/my_module_zlib.patch
--- a/BUILD.bazel
+++ b/BUILD.bazel
@@ -1,6 +1,6 @@
 go_binary(
     name = "my_module",
     importpath = "example.com/my_module",
     srcs = ["my_module.go"],
-    copts = ["-lz"],
+    cdeps = ["@zlib"],
 )

最佳做法

本節說明撰寫擴充功能時的最佳做法,讓擴充功能易於使用、可維護,並能隨時間變化而調整。

將每個擴充功能放在個別檔案中

當擴充功能位於不同檔案中時,會允許某個擴充功能載入其他擴充功能產生的存放區。即使您不使用這項功能,還是建議將這些資訊放在個別檔案中,以備日後使用。這是因為擴充功能的識別是以其檔案為基礎,因此將擴充功能移至其他檔案之後,會變更您的公用 API,而且對使用者而言,不具回溯相容性。

指定可複製性

如果您的擴充功能在相同輸入內容 (擴充功能標記、讀取的檔案等) 下,一律定義相同的存放區,且特別不依賴任何未受總和檢查碼保護的下載內容,請考慮使用 reproducible = True 傳回 extension_metadata。這可讓 Bazel 在寫入 Lockfile 時略過這項副檔名。

指定作業系統和架構

如果您的擴充功能必須使用作業系統或架構類型,請務必在擴充功能定義中使用 os_dependentarch_dependent 布林值屬性來表示。這樣可以確保 Bazel 知道有任何變更出現異動時,必須重新評估。

由於這種依附關係會讓主機更難以維護這個擴充功能的 Lockfile 項目,因此如果可以,請考慮將擴充功能標示為可重現

只有根模組應直接影響存放區名稱

請記住,當擴充功能建立存放區時,即會在擴充功能的命名空間中建立。也就是說,如果不同模組使用相同的擴充功能,並最終建立名稱相同的存放區,就可能發生衝突。這通常會導致模組擴充功能的 tag_class 具有 name 引數,而該引數會做為存放區規則的 name 值傳遞。

舉例來說,假設根模組 A 依附於模組 B。這兩個模組都依附於 mylang 模組。如果 AB 都呼叫 mylang.toolchain(name="foo"),兩者都會嘗試在 mylang 模組中建立名為 foo 的存放區,並發生錯誤。

為避免這種情況,請移除直接設定存放區名稱的功能,或是只允許根模組執行這項操作。您可以允許根模組達成這個目標,因為沒有任何依附的項目,所以無需擔心其他模組會產生衝突的名稱。