模块扩展

报告问题 查看源代码

通过模块扩展,用户可以扩展模块系统,方法是从依赖关系图中的模块读取输入数据,执行必要的逻辑来解析依赖项,最后通过调用 Repo 规则来创建代码库。这些扩展程序具有与 Repo 规则类似的功能,能够执行文件 I/O、发送网络请求等。此外,借助这些 API,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.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 函数,以类似于 Repo 规则的方式定义模块扩展。不过,虽然 Repo 规则有许多属性,但模块扩展具有 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 规则来生成代码库。

# @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 形式的规范名称。对于托管在根模块中的扩展程序,module_repo_canonical_name 部分会被替换为字符串 _main。请注意,规范名称格式不是您应该依赖的 API,它随时可能更改。

此命名政策意味着每个扩展程序都有自己的“代码库命名空间”;两个不同的扩展程序可以各自定义一个同名代码库,而不会产生任何冲突。这也意味着 repository_ctx.name 会报告代码库的规范名称,该名称与代码库规则调用中指定的名称不同

考虑到模块扩展生成的代码库,存在以下几种代码库可见性规则:

  • Bazel 模块代码库可以通过 bazel_depuse_repo 查看其 MODULE.bazel 文件中引入的所有代码库。
  • 由模块扩展程序生成的代码库可以查看对托管该扩展程序的模块可见的所有代码库,以及同一模块扩展程序生成的所有其他代码库(使用 Repo 规则调用中指定的名称作为其表观名称)。
    • 这可能会导致冲突。如果模块代码库可以看到表观名称为 foo 的代码库,并且该扩展程序生成了一个具有指定名称 foo 的代码库,则对于该扩展程序生成的所有代码库,foo 将引用前者。

最佳实践

本部分介绍了编写扩展程序时的最佳实践,确保扩展程序易于使用、可维护,并且能够很好地适应随时间变化的情况。

将每个扩展程序放在单独的文件中

当多个扩展程序位于不同的文件中时,它允许一个扩展程序加载由另一个扩展程序生成的代码库。即使您不使用该功能,最好也将其放在单独的文件中,以备日后需要。这是因为扩展程序的标识基于其文件,因此将扩展程序移至另一个文件之后会更改您的公共 API,对用户来说是一种向后不兼容的更改。

指定操作系统和架构

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

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

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

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

为避免这种情况,请移除直接设置代码库名称的功能,或仅允许根模块执行此操作。您可以允许根模块执行此操作,因为没有任何内容依赖于根模块,因此它无需担心其他模块会创建有冲突的名称。