使用 Bzlmod 管理外部依赖项

Bzlmod 是 Bazel 5.0 中引入的新外部依赖项系统的代号。它旨在解决旧系统难以解决的几个难题;如需了解详情,请参阅原始设计文档的问题陈述部分

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

Bazel 模块

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

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

模块只是使用 nameversion 对来指定依赖项,而不是 WORKSPACE 中的特定网址。这些依赖项随后会在 Bazel 注册表中查找; Bazel 中央注册表。然后,在您的工作区中,每个模块都会变为代码库。

MODULE.yaml

每个模块的每个版本都有一个 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 规定的 3 个版本):MAJOR.MINOR.PATCH。此外,系统不会强制执行主要版本、次要版本和补丁程序版本增加的语义。(不过,如需详细了解我们如何表示向后兼容性,请参阅兼容性级别。) SemVer 规范的其他部分(例如表示预发布版本的连字符)不会被修改。

版本分辨率

The 形依赖项问题是版本化依赖项管理空间中的主元素。假设您有以下依赖关系图:

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

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

版本解析在您的机器上以本地方式执行,而不是通过注册表执行。

兼容性级别

请注意,MVS 对向后兼容性的假设是可行的,因为它只是将模块向后不兼容的版本视为单独的模块。就 SemVer 而言,这意味着 A 1.x 和 A 2.x 被视为不同的模块,并且可以在解析的依赖项图中共存。这反过来就是由于 Major 版本在 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_namebazel_dep 中的 repo_name 特性指定的名称。
    • 对于模块扩展代码库:通过 use_repo 引入的代码库名称。

每个代码库都有其直接依赖项的代码库映射字典,即从本地代码库名称到规范代码库名称的映射。在构建标签时,我们使用代码库映射来解析代码库名称。请注意,规范代码库名称没有冲突,并且解析 MODULE.bazel 文件就可以发现本地代码库名称的使用情况,因此可以轻松地捕获和解决冲突,而不会影响其他依赖项。

Strict 依赖项

新的依赖项规范格式允许我们执行更严格的检查。特别是,我们现在强制要求模块只能使用通过其直接依赖项创建的代码库。当传递依赖项图中的内容发生变化时,这有助于防止意外和难以调试的中断。

Strict 依赖项基于代码库映射实现。基本上,每个代码库的代码库映射都包含其所有直接依赖项,任何其他代码库都不可见。每个代码库的可见依赖项按以下方式确定:

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

注册表

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

索引注册表

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

索引注册表必须遵循以下格式:

  • /bazel_registry.json:包含注册表元数据的 JSON 文件。目前,它只有一个 mirrors 键,用于指定要用于来源归档的镜像列表。
  • /modules:一个目录,其中包含此注册表中每个模块的子目录。
  • /modules/$MODULE:包含此模块各个版本的子目录以及以下文件的目录:
    • metadata.json:包含模块相关信息的 JSON 文件,包含以下字段:
      • homepage:项目首页的网址。
      • maintainers:JSON 对象列表,其中每个对象都对应于注册表中模块维护人员的信息。请注意,此条目不一定与项目的作者相同。
      • versions:可在此注册表中找到的此模块的所有版本的列表。
      • yanked_versions:此模块的挂起版本列表。此操作当前为空操作,但将来会出现被跳过的版本或导致错误。
  • /modules/$MODULE/$VERSION:包含以下文件的目录:
    • MODULE.bazel:此模块版本的 MODULE.bazel 文件。
    • source.json:一个 JSON 文件,包含有关如何提取此模块版本源代码的信息,并包含以下字段:
      • url:源归档的网址。
      • integrity:归档的子资源完整性校验和。
      • strip_prefix:提取源归档文件时要移除的目录前缀。
      • patches:字符串列表,其中每个字符串均命名一个要应用于提取归档的补丁程序文件。补丁程序文件位于 /modules/$MODULE/$VERSION/patches 目录下。
      • patch_strip:与 Unix 补丁程序的 --strip 参数相同。
    • patches/:包含补丁文件的可选目录。

Bazel 中央注册表

Bazel 中央注册表 (BCR) 是位于 registry.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 文件中定义,就像 repo 规则或 WORKSPACE 宏一样。并非直接调用;相反,每个模块都可以指定供标记读取的各部分数据。 然后,在模块版本解析完成后,将运行模块扩展。每个扩展都会在模块解析后(仍在任何 build 实际运行之前)运行一次,并且会读取整个依赖项图中属于该扩展的所有标记。

          [ 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”。

扩展程序定义

模块扩展的定义方式与 Repo 规则类似,那就是使用 module_extension 函数。两者都具有实现函数;不过,虽然 repo 规则具有许多属性,但模块扩展具有许多 tag_classes,每个属性都有许多属性。标记类为此扩展程序使用的代码定义了架构。继续讨论上述假设的“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 代码库规则。