模块扩展

借助模块扩展程序,用户可以通过以下方式扩展模块系统:从依赖关系图中的模块读取输入数据 ,执行必要的逻辑来解析 依赖项,最后通过调用 代码库 规则来创建代码库。这些扩展程序的功能与代码库规则类似,因此能够执行文件 I/O、发送网络请求等操作。 除此之外,它们还允许 Bazel 与其他软件包管理系统进行交互,同时遵守由 Bazel 模块构建的依赖关系图。

您可以像代码库规则一样在 .bzl 文件中定义模块扩展程序。它们不会被直接调用;相反,每个模块都会指定一些称为 标记 的数据,供扩展程序读取。Bazel 会先运行模块解析,然后再评估任何扩展程序。扩展程序会读取整个依赖关系图中属于它的所有标记。

扩展程序使用情况

扩展程序托管在 Bazel 模块本身中。如需在 模块中使用扩展程序,请先在托管扩展程序的模块上添加 bazel_dep,然后 调用 use_extension 内置函数 将其纳入范围。请看以下示例,这是 MODULE.bazel文件中的代码段,用于使用“maven”扩展程序,该扩展程序在 rules_jvm_external 模块中定义:

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 指令将扩展程序生成的代码库 纳入当前模块的范围。

use_repo(maven, "maven")

扩展程序生成的代码库是其 API 的一部分。在此示例中,“maven”模块扩展程序承诺生成一个名为 maven 的代码库。通过上述声明,扩展程序可以正确解析标签(例如 @maven//:org_junit_junit),以指向“maven”扩展程序生成的代码库。

扩展程序定义

您可以使用 代码库规则 定义模块扩展程序, 这与module_extension 函数类似。不过,代码库规则有许多属性,而模块 扩展程序有 tag_classes,每个 都有许多属性。标记类定义了此扩展程序使用的标记的架构。例如,上面的“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 对象, 该对象授予对使用该扩展程序的所有模块和所有相关标记的访问权限。 然后,实现函数会调用代码库规则来生成代码库。

# @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 模块代码库可以通过其 MODULE.bazel 文件 通过 bazel_depuse_repo 查看其引入的所有代码库。
  • 模块扩展程序生成的代码库可以查看托管该扩展程序的模块可见的所有代码库,以及同一模块扩展程序生成的所有其他代码库(使用代码库规则调用中指定的名称作为其显示名称)。
    • 这可能会导致冲突。如果模块代码库可以查看显示名称为 foo 的代码库,并且扩展程序生成一个指定名称为 foo 的代码库,那么对于该扩展程序生成的所有代码库,foo 都指的是前者。
  • 同样,在模块扩展程序的实现函数中,扩展程序创建的代码库可以在属性中通过其显示名称相互引用,而无论它们的创建顺序如何。
    • 如果与模块可见的仓库发生冲突,传递给仓库规则属性的标签可以封装在对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,并且对您的用户来说是向后不兼容的更改。

指定可重现性并使用事实

如果您的扩展程序始终根据相同的输入 (扩展程序标记、它读取的文件等)定义相同的代码库,并且尤其不依赖于 任何 下载(不受 校验和保护),请考虑返回 extension_metadata,并将 reproducible = True。这样一来,Bazel 在写入 MODULE.bazel 锁定文件时可以跳过此扩展程序,这有助于保持锁定文件较小,并减少合并冲突的可能性。请注意,Bazel 仍会以在服务器重启后保持不变的方式缓存可重现扩展程序的结果,因此即使是长时间运行的扩展程序也可以标记为可重现,而不会影响性能。

如果您的扩展程序依赖于从 build 外部(最常见的是从网络)获取的实际上不可变的数据,但您没有可用于保护下载的校验和,请考虑使用 facts 参数来持久记录此类数据,从而使您的扩展程序成为可重现的。extension_metadatafacts 预计是一个字典,其中包含字符串键和 任意类似 JSON 的 Starlark 值,这些值始终保留在锁定文件中,并且 可通过 factsmodule_ctx 字段供扩展程序日后评估使用。

即使模块扩展程序的代码发生更改,facts 也不会失效,因此请准备好处理 facts 的结构发生更改的情况。Bazel 还假定,同一扩展程序的两次不同评估生成的两个不同的 facts 字典可以浅层合并(即,就像对两个字典使用 | 运算符一样)。module_ctx.facts 不支持枚举其条目,而仅支持按键查找,这在一定程度上强制执行了此操作。

使用 facts 的一个示例是记录从某些 SDK 的版本号到包含该版本的下载网址和校验和的对象的映射。首次评估扩展程序时,它可以从网络提取此映射,但在后续评估中,它可以从 facts 使用该映射,以避免网络请求。

指定对操作系统和架构的依赖性

如果您的扩展程序依赖于操作系统或其架构类型,请务必使用 os_dependentarch_dependent 布尔值属性在扩展程序定义中指明这一点。这可确保 Bazel 在其中任何一个发生更改时识别出需要重新评估。

由于这种对主机的依赖性使得维护 此扩展程序的锁定文件条目更加困难,因此请考虑 尽可能将扩展程序标记为可重现

只有根模块应直接影响代码库名称

请注意,当扩展程序创建代码库时,它们是在扩展程序的命名空间内创建的。这意味着,如果不同的模块使用相同的扩展程序并最终创建具有相同名称的代码库,则可能会发生冲突。这通常表现为模块扩展程序的 tag_class 具有一个 name 实参,该实参作为代码库规则的 name 值传递。

例如,假设根模块 A 依赖于模块 B。这两个模块都依赖于模块 mylang。如果 AB 都调用 mylang.toolchain(name="foo"),它们都会尝试在 mylang 模块中创建一个名为 foo 的代码库,并且会发生错误。

为避免这种情况,请移除直接设置代码库名称的功能,或仅允许根模块执行此操作。允许根模块执行此操作是没问题的,因为没有任何内容会依赖于它,因此它不必担心另一个模块创建冲突的名称。