使用 Bzlmod 管理外部依赖项

报告问题 查看源代码 每夜 build · 8.0 7.4 . 7.3 · 7.2 · 7.1 · 7.0 · 6.5

Bzlmod 是 Bazel 5.0 中引入的新外部依赖项系统的代号。我们引入该模型是为了解决旧系统中无法通过增量方式解决的几个痛点;如需了解详情,请参阅原始设计文档的“问题陈述”部分

在 Bazel 5.0 中,Bzlmod 默认处于关闭状态;需要指定标志 --experimental_enable_bzlmod 才能使以下内容生效。正如标志名称所示,此功能目前处于实验阶段;在该功能正式发布之前,API 和行为可能会发生变化。

如需将项目迁移到 Bzlmod,请按照 Bzlmod 迁移指南操作。 您还可以在 examples 代码库中找到 Bzlmod 用法示例。

Bazel 模块

基于 WORKSPACE 的旧版外部依赖项系统以通过代码库规则(或代码库规则)创建的代码库(或代码库)为中心。虽然代码库在新系统中仍然是一项重要概念,但模块是依赖项的核心单元。

模块本质上是一个可以有多个版本的 Bazel 项目,每个版本都会发布有关其依赖的其他模块的元数据。这类似于其他依赖项管理系统中常见的概念:Maven 工件、npm 软件包、Cargo crate、Go 模块等。

模块只需使用 nameversion 对(而非 WORKSPACE 中的特定网址)指定其依赖项。然后,系统会在 Bazel 注册库中查找依赖项;默认情况下,是 Bazel Central 注册库。然后,在您的工作区中,每个模块都会转换为一个代码库。

MODULE.bazel

每个模块的每个版本都有一个 MODULE.bazel 文件,用于声明其依赖项和其他元数据。下面是一个基本示例:

module(
    name = "my-module",
    version = "1.0",
)

bazel_dep(name = "rules_cc", version = "0.0.1")
bazel_dep(name = "protobuf", version = "3.19.0")

MODULE.bazel 文件应位于工作区目录的根目录下(位于 WORKSPACE 文件旁边)。与 WORKSPACE 文件不同,您无需指定传递依赖项;而是应仅指定直接依赖项,系统会处理依赖项的 MODULE.bazel 文件以自动发现传递依赖项。

MODULE.bazel 文件与 BUILD 文件类似,因为它不支持任何形式的控制流;此外,它还禁止使用 load 语句。MODULE.bazel 文件支持的说明如下:

版本格式

Bazel 拥有多元化的生态系统,项目使用各种版本控制方案。到目前为止,最受欢迎的是 SemVer,但也有一些知名项目使用不同的方案,例如 Abseil,其版本基于日期(例如 20210324.2)。

因此,Bzlmod 采用了更宽松的 SemVer 规范版本。差异包括:

  • SemVer 规定,版本的“release”部分必须由 3 个部分组成:MAJOR.MINOR.PATCH。在 Bazel 中,此要求已放宽,允许使用任意数量的段。
  • 在 SemVer 中,“release”部分中的每个部分都必须是纯数字。在 Bazel 中,此限制已放宽,也允许使用字母,并且比较语义与“预发布”部分中的“标识符”相匹配。
  • 此外,系统不会强制执行主要版本号、次要版本号和补丁版本号递增的语义。(不过,如需详细了解我们如何表示向后兼容性,请参阅兼容性级别。)

任何有效的 SemVer 版本都是有效的 Bazel 模块版本。此外,如果将两个 SemVer 版本 ab 作为 Bazel 模块版本进行比较,则只有在它们的比较结果相同时,a < b 才会是它们的比较结果。

版本解析

钻石形依赖项问题是版本化依赖项管理领域的常见问题。假设您有以下依赖项图:

       A 1.0
      /     \
   B 1.0    C 1.1
     |        |
   D 1.0    D 1.1

应使用哪个版本的 D?为了解决此问题,Bzlmod 使用 Go 模块系统中引入的最小版本选择 (MVS) 算法。MVS 假定模块的所有新版本都向后兼容,因此只会选择任何依赖项指定的最高版本(在我们的示例中为 D 1.1)。之所以称为“最低”,是因为此处的 D 1.1 是能满足我们要求的最低版本;即使存在 D 1.2 或更高版本,我们也不会选择它们。这样做的另一个好处是,版本选择是高保真可重现的。

版本解析是在您的机器本地执行的,而不是由注册表执行。

兼容性级别

请注意,MVS 对向后兼容性的假设是可行的,因为它只是将模块的向后不兼容版本视为单独的模块。从 SemVer 的角度来看,这意味着 A 1.x 和 A 2.x 被视为不同的模块,可以在解析后的依赖项图中共存。反过来,这得益于 Go 中主要版本在软件包路径中编码,因此不会出现任何编译时或链接时冲突。

在 Bazel 中,我们无法做出这样的保证。因此,我们需要一种方法来表示“主要版本”号,以便检测向后不兼容的版本。此编号称为兼容性级别,由每个模块版本在其 module() 指令中指定。有了这些信息,当我们检测到已解析的依赖项图中存在具有不同兼容性级别的同一模块的版本时,就可以抛出错误。

代码库名称

在 Bazel 中,每个外部依赖项都有一个仓库名称。有时,同一依赖项可能会通过不同的代码库名称使用(例如,@io_bazel_skylib@bazel_skylib 都表示 Bazel skylib),或者同一代码库名称可能会用于不同项目中的不同依赖项。

在 Bzlmod 中,存储库可以由 Bazel 模块和模块扩展生成。为了解决代码库名称冲突问题,我们在新系统中采用了代码库映射机制。以下是两个重要概念:

  • 规范代码库名称:每个代码库的全局唯一代码库名称。这将是代码库所在的目录名称。
    其构建方式如下(警告:规范名称格式不是您应依赖的 API,可能会随时更改):

    • 对于 Bazel 模块代码库:module_name~version
      示例@bazel_skylib~1.0.3)
    • 对于模块扩展程序代码库:module_name~version~extension_name~repo_name
      示例@rules_cc~0.0.1~cc_configure~local_config_cc)
  • 显式代码库名称:要在代码库内的 BUILD.bzl 文件中使用的代码库名称。同一依赖项在不同的代码库中可能具有不同的显示名称。
    其确定方式如下:

    • 对于 Bazel 模块代码库:默认为 module_name,或 bazel_dep 中的 repo_name 属性指定的名称。
    • 对于模块扩展程序代码库:通过 use_repo 引入的代码库名称。

每个代码库都有一个存储其直接依赖项的代码库映射字典,该字典是从显式代码库名称到规范代码库名称的映射。在构建标签时,我们使用代码库映射来解析代码库名称。请注意,规范代码库名称不会发生冲突,并且可以通过解析 MODULE.bazel 文件来发现明显代码库名称的用法,因此可以轻松发现和解决冲突,而不会影响其他依赖项。

严格依赖项

借助新的依赖项规范格式,我们可以执行更严格的检查。具体而言,我们现在强制要求模块只能使用通过其直接依赖项创建的代码库。这有助于防止在传递依赖项图中发生更改时出现意外且难以调试的故障。

严格依赖项是基于代码库映射实现的。基本上,每个代码库的代码库映射都包含其所有直接依赖项,任何其他代码库都不可见。每个代码库的可见依赖项如下所示:

  • Bazel 模块代码库可以通过 bazel_depuse_repo 查看 MODULE.bazel 文件中引入的所有代码库。
  • 模块扩展程序代码库可以查看提供扩展程序的模块的所有可见依赖项,以及由同一模块扩展程序生成的所有其他代码库。

注册表

Bzlmod 会通过向 Bazel 注册库请求其信息来发现依赖项。Bazel 注册表只是 Bazel 模块的数据库。唯一受支持的注册表形式是索引注册表,它是采用特定格式的本地目录或静态 HTTP 服务器。未来,我们计划添加对单模块注册库的支持,这只是包含项目源代码和历史记录的 Git 代码库。

索引注册表

索引注册表是本地目录或静态 HTTP 服务器,其中包含有关模块列表的信息,包括其首页、维护者、每个版本的 MODULE.bazel 文件,以及如何提取每个版本的源代码。值得注意的是,它需要自行提供源代码归档文件。

索引注册表必须采用以下格式:

  • /bazel_registry.json:包含注册表元数据的 JSON 文件,例如:
    • mirrors,用于指定要用于源代码归档的镜像列表。
    • module_base_path,用于在 source.json 文件中为类型为 local_repository 的模块指定基路径。
  • /modules:一个目录,其中包含此注册表中每个模块的子目录。
  • /modules/$MODULE:一个目录,其中包含此模块的每个版本对应的子目录,以及以下文件:
    • metadata.json:包含模块相关信息的 JSON 文件,其中包含以下字段:
      • homepage:项目首页的网址。
      • maintainers:一个 JSON 对象列表,其中每个对象都对应于注册库中模块的维护者的相关信息。请注意,这不一定与项目的作者相同。
      • versions:此注册表中可找到的此模块的所有版本的列表。
      • yanked_versions:此模块的已移除版本的列表。这目前没有任何操作,但将来,系统会跳过已撤消的版本或产生错误。
  • /modules/$MODULE/$VERSION:包含以下文件的目录:
    • MODULE.bazel:此模块版本的 MODULE.bazel 文件。
    • source.json:一个 JSON 文件,其中包含有关如何提取此模块版本源代码的信息。
      • 默认类型为“archive”,包含以下字段:
        • url:源代码归档的网址。
        • integrity:归档文件的子资源完整性校验和。
        • strip_prefix:解压缩源代码归档文件时要剥离的目录前缀。
        • patches:一个字符串列表,其中每个字符串都指定要应用于解压缩的归档文件的补丁文件的名称。补丁文件位于 /modules/$MODULE/$VERSION/patches 目录下。
        • patch_strip:与 Unix 补丁的 --strip 参数相同。
      • 您可以更改类型,以使用以下字段中的本地路径:
        • typelocal_path
        • path:代码库的本地路径,按如下方式计算:
          • 如果路径是绝对路径,则会按原样使用。
          • 如果 path 是相对路径,而 module_base_path 是绝对路径,则 path 会解析为 <module_base_path>/<path>
          • 如果 path 和 module_base_path 都是相对路径,path 会解析为 <registry_path>/<module_base_path>/<path>。注册表必须在本地托管,并由 --registry=file://<registry_path> 使用。否则,Bazel 会抛出错误。
    • patches/:包含补丁文件的可选目录,仅在 source.json 的类型为“归档”时使用。

Bazel 中央注册库

Bazel 中央注册表 (BCR) 是位于 bcr.bazel.build 的索引注册表。其内容由 GitHub 代码库 bazelbuild/bazel-central-registry 提供支持。

BCR 由 Bazel 社区维护;欢迎贡献者提交拉取请求。请参阅 Bazel 中央注册库政策和流程

除了遵循常规索引注册表的格式之外,BCR 还要求为每个模块版本 (/modules/$MODULE/$VERSION/presubmit.yml) 提供一个 presubmit.yml 文件。此文件指定了一些基本构建和测试目标,可用于对此模块版本的有效性进行完整性检查,并且 BCR 的 CI 流水线会使用此文件来确保 BCR 中的模块之间能够互操作。

选择注册表

可重复使用的 Bazel 标志 --registry 可用于指定要从中请求模块的注册表列表,以便您将项目设置为从第三方或内部注册表提取依赖项。先前的注册库优先。为方便起见,您可以在项目的 .bazelrc 文件中放置 --registry 标志列表。

模块扩展

借助模块扩展,您可以通过从依赖项图中的模块读取输入数据、执行必要的逻辑来解析依赖项,最后通过调用代码库规则创建代码库来扩展模块系统。它们的功能与现有的 WORKSPACE 宏类似,但更适合在模块和传递依赖项的世界中使用。

模块扩展程序在 .bzl 文件中定义,就像代码库规则或 WORKSPACE 宏一样。这些方法不会直接调用;而是每个模块都可以指定一些称为“标记”的数据,以供扩展程序读取。然后,在模块版本解析完成后,系统会运行模块扩展。每个扩展程序都会在模块解析后运行一次(仍在实际进行任何构建之前),并且可以读取整个依赖项图中属于它的所有标记。

          [ A 1.1                ]
          [   * maven.dep(X 2.1) ]
          [   * maven.pom(...)   ]
              /              \
   bazel_dep /                \ bazel_dep
            /                  \
[ B 1.2                ]     [ C 1.0                ]
[   * maven.dep(X 1.2) ]     [   * maven.dep(X 2.1) ]
[   * maven.dep(Y 1.3) ]     [   * cargo.dep(P 1.1) ]
            \                  /
   bazel_dep \                / bazel_dep
              \              /
          [ D 1.4                ]
          [   * maven.dep(Z 1.4) ]
          [   * cargo.dep(Q 1.1) ]

在上面的示例依赖项图中,A 1.1B 1.2 等是 Bazel 模块;您可以将每个模块视为一个 MODULE.bazel 文件。每个模块都可以为模块扩展指定一些标记;此处,一些标记指定给扩展“maven”,一些标记指定给“cargo”。当此依赖项图表确定下来后(例如,B 1.2 实际上在 D 1.3 上有 bazel_dep,但由于 C 而已升级为 D 1.4),系统会运行扩展程序“maven”,并读取所有 maven.* 标记,使用其中的信息来决定要创建哪些代码库。“cargo”扩展也是如此。

扩展程序使用情况

扩展程序托管在 Bazel 模块本身中,因此如需在模块中使用扩展程序,您需要先在该模块上添加 bazel_dep,然后调用 use_extension 内置函数以将其纳入作用域。请考虑以下示例,其中 MODULE.bazel 文件中的代码段用于使用 rules_jvm_external 模块中定义的假设“maven”扩展程序:

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

将扩展程序纳入范围后,您可以使用点语法为其指定标记。请注意,这些代码需要遵循相应代码类(请参阅下文中的扩展程序定义)定义的架构。下面的示例指定了一些 maven.depmaven.pom 标记。

maven.dep(coord="org.junit:junit:3.0")
maven.dep(coord="com.google.guava:guava:1.2")
maven.pom(pom_xml="//:pom.xml")

如果扩展程序生成您要在模块中使用的代码库,请使用 use_repo 指令声明这些代码库。这是为了满足严格的依赖项条件并避免本地代码库名称冲突。

use_repo(
    maven,
    "org_junit_junit",
    guava="com_google_guava_guava",
)

扩展程序生成的代码库是其 API 的一部分,因此,根据您指定的标记,您应该知道“maven”扩展程序将生成一个名为“org_junit_junit”的代码库,以及一个名为“com_google_guava_guava”的代码库。使用 use_repo,您可以选择在模块级别重命名它们,例如将其重命名为“guava”。

扩展定义

模块扩展的定义与代码库规则类似,使用 module_extension 函数。这两者都有实现函数;但虽然代码库规则具有多个属性,但模块扩展具有多个 tag_class,每个 tag_class 都有多个属性。代码类用于定义此扩展程序使用的代码的架构。继续上文中假设的“maven”扩展程序示例:

# @rules_jvm_external//:extensions.bzl
maven_dep = tag_class(attrs = {"coord": attr.string()})
maven_pom = tag_class(attrs = {"pom_xml": attr.label()})
maven = module_extension(
    implementation=_maven_impl,
    tag_classes={"dep": maven_dep, "pom": maven_pom},
)

这些声明明确说明,可以使用上面定义的属性架构指定 maven.depmaven.pom 标记。

实现函数与 WORKSPACE 宏类似,但它会获取 module_ctx 对象,该对象会授予对依赖项图和所有相关标记的访问权限。然后,实现函数应调用代码库规则以生成代码库:

# @rules_jvm_external//:extensions.bzl
load("//:repo_rules.bzl", "maven_single_jar")
def _maven_impl(ctx):
  coords = []
  for mod in ctx.modules:
    coords += [dep.coord for dep in mod.tags.dep]
  output = ctx.execute(["coursier", "resolve", coords])  # hypothetical call
  repo_attrs = process_coursier(output)
  [maven_single_jar(**attrs) for attrs in repo_attrs]

在上面的示例中,我们会遍历依赖项图 (ctx.modules) 中的所有模块,每个模块都是一个 bazel_module 对象,其 tags 字段会公开模块上的所有 maven.* 标记。然后,我们调用 CLI 实用程序 Coursier 来与 Maven 联系并执行解析。最后,我们使用假设的 maven_single_jar 代码库规则,使用解析结果创建多个代码库。