模組擴充功能

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

模組擴充功能可讓使用者透過讀取依附元件圖中模組的輸入資料、執行必要邏輯來解析依附元件,並最終透過呼叫 repo 規則來建立 repo,進而擴充模組系統。這些擴充功能的功能類似於存放區規則,可執行檔案 I/O、傳送網路要求等。除了其他功能外,這些模組還可讓 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 的傳回值繫結至變數,讓使用者可以使用點號語法指定擴充功能的標記。標記必須遵循擴充功能定義中指定的對應標記類別所定義的結構定義。以下是指定部分 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")

擴充功能產生的 repos 是其 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 標記。

模組擴充功能的實作函式與 repo 規則的實作函式類似,唯一的差異在於前者會取得 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 檔案使用模組擴充功能。

存放區名稱和瀏覽權限

由擴充功能產生的 repo 具有 module_repo_canonical_name~extension_name~repo_name 格式的標準名稱。如果是根模組代管的擴充功能,module_repo_canonical_name 部分會替換為字串 _main。請注意,標準名稱格式並非您應依賴的 API,因為格式隨時可能會變更。

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

考量模組擴充功能產生的 repo,有幾個 repo 檢視權限規則:

  • 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"],
 )

最佳做法

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

將每個擴充功能放在不同的檔案中

當擴充功能位於不同檔案中時,可讓一個擴充功能載入由另一個擴充功能產生的存放區。即使您不使用這項功能,最好還是將這些資訊放在個別檔案中,以備日後使用。這是因為擴充功能的 ID 會根據其檔案而定,因此日後將擴充功能移至其他檔案會變更公開 API,並對使用者造成不相容的變更。

指定可複製性

如果在相同的輸入內容 (擴充功能標記、讀取的檔案等) 下,您的擴充功能一律會定義相同的存放區,且特別不會依賴任何未受總和檢查碼保護的下載內容,建議您使用 reproducible = True 傳回 extension_metadata。這樣一來,Bazel 在寫入鎖定檔案時,就能略過這個擴充功能。

指定作業系統和架構

如果擴充功能需要依賴作業系統或其架構類型,請務必使用 os_dependentarch_dependent 布林值屬性,在擴充功能定義中指出這項資訊。這樣一來,如果其中任何一個項目有變更,Bazel 就會知道需要重新評估。

由於這種對主機的依附性會使這類擴充功能的鎖定檔案項目更難維護,因此請盡可能將擴充功能標示為可重現

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

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

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

為避免這種情況,請移除直接設定存放區名稱的功能,或是只允許根模組執行此操作。允許根模組執行此操作是安全的,因為沒有任何東西會依附於此模組,因此不必擔心其他模組會建立相衝突的名稱。