模块扩展功能允许用户通过以下方式扩展模块系统:从依赖关系图中的模块读取输入数据,执行必要的逻辑来解析依赖项,最后通过调用 repo 规则来创建代码库。这些扩展程序的功能与 repo 规则类似,因此能够执行文件 I/O、发送网络请求等操作。除其他事项外,它们还允许 Bazel 与其他软件包管理系统进行交互,同时遵守由 Bazel 模块构建的依赖关系图。
您可以在 .bzl
文件中定义模块扩展程序,就像定义 repo 规则一样。它们不会被直接调用;相反,每个模块都会指定称为“标记”的数据片段供扩展程序读取。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.install
和 maven.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
指令将扩展程序生成的代码库纳入当前模块的范围。
use_repo(maven, "maven")
由扩展程序生成的代码库是其 API 的一部分。在此示例中,“maven”模块扩展程序承诺生成一个名为 maven
的代码库。通过上述声明,扩展程序可以正确解析 @maven//:org_junit_junit
等标签,以指向由“maven”扩展程序生成的代码库。
扩展程序定义
您可以使用 module_extension
函数定义模块扩展程序,这与定义仓库规则类似。不过,虽然 repo 规则有许多属性,但模块扩展具有 tag_class
es,每个 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.install
和 maven.artifact
标记。
模块扩展的实现函数与代码库规则的实现函数类似,不同之处在于它们会获取一个 module_ctx
对象,该对象可授予对使用扩展的所有模块和所有相关标记的访问权限。然后,实现函数会调用代码库规则来生成代码库。
# @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)
扩展程序身份
模块扩展程序通过名称和 use_extension
调用中显示的 .bzl
文件来标识。在以下示例中,扩展程序 maven
由 .bzl
文件 @rules_jvm_external//:extension.bzl
和名称 maven
标识:
maven = use_extension("@rules_jvm_external//:extensions.bzl", "maven")
从其他 .bzl
文件重新导出扩展程序会为其赋予新的身份;如果传递模块图中使用了扩展程序的这两个版本,则它们将分别进行评估,并且只会看到与该特定身份相关联的标记。
作为扩展模块的作者,您应确保用户仅通过一个 .bzl
文件使用您的模块扩展。
代码库名称和可见性
扩展程序生成的代码库的规范名称采用 module_repo_canonical_name+extension_name+repo_name
格式。请注意,规范名称格式不是您应该依赖的 API,它可能会随时发生变化。
此命名政策意味着每个扩展程序都有自己的“代码库命名空间”;两个不同的扩展程序可以各自定义一个同名代码库,而不会有任何冲突风险。这也意味着,repository_ctx.name
会报告代码库的规范名称,该名称与代码库规则调用中指定的名称不同。
考虑到由模块扩展生成的代码库,有以下几种代码库可见性规则:
- Bazel 模块代码库可以通过
bazel_dep
和use_repo
查看其MODULE.bazel
文件中引入的所有代码库。 - 由模块扩展程序生成的代码库可以查看托管该扩展程序的模块可见的所有代码库,以及由同一模块扩展程序生成的所有其他代码库(使用代码库规则调用中指定的名称作为其显式名称)。
- 这可能会导致冲突。如果模块代码库可以找到名称为
foo
的代码库,并且扩展程序生成了名称为foo
的代码库,那么对于该扩展程序生成的所有代码库,foo
都指前者。
- 这可能会导致冲突。如果模块代码库可以找到名称为
- 同样,在模块扩展的实现函数中,扩展创建的 repo 可以通过属性中的表观名称相互引用,而无需考虑它们的创建顺序。
- 如果标签与模块可见的代码库发生冲突,则传递给代码库规则属性的标签可以封装在对
Label
的调用中,以确保它们引用模块可见的代码库,而不是同名的扩展程序生成的代码库。
- 如果标签与模块可见的代码库发生冲突,则传递给代码库规则属性的标签可以封装在对
替换和注入模块扩展 repo
根模块可以使用 override_repo
和 inject_repo
来替换或注入模块扩展 repo。
示例:将 rules_java
的 java_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,并且对于您的用户而言,这是一个向后不兼容的更改。
指定可重现性
如果您的扩展程序在给定相同输入(扩展程序标记、读取的文件等)的情况下始终定义相同的代码库,并且尤其不依赖于任何不受校验和保护的下载,请考虑返回 extension_metadata
并使用 reproducible = True
。这样,Bazel 在写入锁定文件时就可以跳过此扩展程序。
指定操作系统和架构
如果您的扩展程序依赖于操作系统或其架构类型,请务必使用 os_dependent
和 arch_dependent
布尔值属性在扩展程序定义中指明这一点。这样可确保,如果这两个文件中的任何一个发生更改,Bazel 都会识别出需要重新评估。
由于这种对宿主的依赖关系会使维护相应扩展程序的锁定文件条目变得更加困难,因此请考虑尽可能将扩展程序标记为可重现。
只有根模块应直接影响代码库名称
请注意,当扩展程序创建代码库时,这些代码库是在扩展程序的命名空间内创建的。这意味着,如果不同的模块使用相同的扩展程序,并最终创建了名称相同的代码库,则可能会发生冲突。这通常表现为模块扩展程序的 tag_class
具有作为代码库规则的 name
值传递的 name
实参。
例如,假设根模块 A
依赖于模块 B
。这两个模块都依赖于模块 mylang
。如果 A
和 B
都调用 mylang.toolchain(name="foo")
,则它们都会尝试在 mylang
模块中创建名为 foo
的代码库,并会发生错误。
为避免这种情况,请移除直接设置代码库名称的功能,或仅允许根模块这样做。允许根模块这样做是可以的,因为没有任何内容会依赖于它,因此它不必担心其他模块创建冲突的名称。