Bzlmod 迁移指南

报告问题 查看源代码 每夜 build · 7.4 .

由于 WORKSPACE 的缺点,Bzlmod 将在未来的 Bazel 版本中替换旧版 WORKSPACE 系统。本指南可帮助您将项目迁移到 Bzlmod,并移除 WORKSPACE 以提取外部依赖项。

WORKSPACE 与 Bzlmod

Bazel 的 WORKSPACE 和 Bzlmod 提供类似的功能,但语法有所不同。本部分介绍了如何从特定 WORKSPACE 功能迁移到 Bzlmod。

定义 Bazel 工作区的根目录

WORKSPACE 文件用于标记 Bazel 项目的源代码根目录,在 Bazel 6.3 及更高版本中,此职责由 MODULE.bazel 取代。如果使用的是低于 6.3 的 Bazel 版本,工作区根目录中应该仍有 WORKSPACEWORKSPACE.bazel 文件,其中可能包含以下类似的注释:

  • 工作区

    # This file marks the root of the Bazel workspace.
    # See MODULE.bazel for external dependencies setup.
    

在 bazelrc 中启用 Bzlmod

.bazelrc 可让您设置每次运行 Bazel 时都应用的标志。如需启用 Bzlmod,请使用 --enable_bzlmod 标志,并将其应用于 common 命令,使其应用于每个命令:

  • .bazelrc

    # Enable Bzlmod for every Bazel command
    common --enable_bzlmod
    

为您的工作区指定代码库名称

  • 工作区

    workspace 函数用于为您的工作区指定代码库名称。这样,您就可以将工作区中的目标 //foo:bar 引用为 @<workspace name>//foo:bar。如果未指定,则工作区的默认代码库名称为 __main__

    ## WORKSPACE
    workspace(name = "com_foo_bar")
    
  • Bzlmod

    建议使用 //foo:bar 语法(不带 @<repo name>)引用同一工作区中的目标。不过,如果您确实需要旧语法,可以使用 module 函数指定的模块名称作为代码库名称。如果模块名称与所需的代码库名称不同,您可以使用 module 函数的 repo_name 属性替换代码库名称。

    ## MODULE.bazel
    module(
        name = "bar",
        repo_name = "com_foo_bar",
    )
    

将外部依赖项作为 Bazel 模块提取

如果您的依赖项是 Bazel 项目,并且也采用了 Bzlmod,您应该能够将其作为 Bazel 模块进行依赖。

  • 工作区

    使用 WORKSPACE 时,通常使用 http_archivegit_repository 代码库规则下载 Bazel 项目的源代码。

    ## WORKSPACE
    load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
    
    http_archive(
        name = "bazel_skylib",
        urls = ["https://github.com/bazelbuild/bazel-skylib/releases/download/1.4.2/bazel-skylib-1.4.2.tar.gz"],
        sha256 = "66ffd9315665bfaafc96b52278f57c7e2dd09f5ede279ea6d39b2be471e7e3aa",
    )
    load("@bazel_skylib//:workspace.bzl", "bazel_skylib_workspace")
    bazel_skylib_workspace()
    
    http_archive(
        name = "rules_java",
        urls = ["https://github.com/bazelbuild/rules_java/releases/download/6.1.1/rules_java-6.1.1.tar.gz"],
        sha256 = "76402a50ae6859d50bd7aed8c1b8ef09dae5c1035bb3ca7d276f7f3ce659818a",
    )
    load("@rules_java//java:repositories.bzl", "rules_java_dependencies", "rules_java_toolchains")
    rules_java_dependencies()
    rules_java_toolchains()
    

    如您所见,这是一种常见的模式,用户需要从依赖项的宏加载传递依赖项。假设 bazel_skylibrules_java 都依赖于 platformplatform 依赖项的确切版本由宏的顺序决定。

  • Bzlmod

    使用 Bzlmod 时,只要您的依赖项在 Bazel Central Registry 或您的自定义 Bazel 注册表中可用,您就可以使用 bazel_dep 指令来依赖于它。

    ## MODULE.bazel
    bazel_dep(name = "bazel_skylib", version = "1.4.2")
    bazel_dep(name = "rules_java", version = "6.1.1")
    

    Bzlmod 使用 MVS 算法以传递方式解析 Bazel 模块依赖项。因此,系统会自动选择所需最高版本的 platform

将依赖项作为 Bazel 模块替换

作为根模块,您可以通过不同的方式替换 Bazel 模块依赖项。

如需了解详情,请参阅替换项部分。

您可以在 examples 代码库中找到一些用法示例。

使用模块扩展提取外部依赖项

如果您的依赖项不是 Bazel 项目,或者尚未在任何 Bazel 注册库中提供,您可以使用模块扩展引入它。

  • 工作区

    使用 http_file 代码库规则下载文件。

    ## WORKSPACE
    load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_file")
    
    http_file(
        name = "data_file",
        url = "http://example.com/file",
        sha256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
    )
    
  • Bzlmod

    使用 Bzlmod,您必须将定义移动到 .bzl 文件中,这还可以在迁移期间在 WORKSPACE 和 Bzlmod 之间共享定义。

    ## repositories.bzl
    load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_file")
    def my_data_dependency():
        http_file(
            name = "data_file",
            url = "http://example.com/file",
            sha256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
        )
    

    实现模块扩展以加载依赖项宏。您可以在宏的同一 .bzl 文件中定义此变量,但为了与较低版本的 Bazel 兼容,最好在单独的 .bzl 文件中定义此变量。

    ## extensions.bzl
    load("//:repositories.bzl", "my_data_dependency")
    def _non_module_dependencies_impl(_ctx):
        my_data_dependency()
    
    non_module_dependencies = module_extension(
        implementation = _non_module_dependencies_impl,
    )
    

    如需将代码库公开给根项目,您应在 MODULE.bazel 文件中声明模块扩展程序和代码库的用法。

    ## MODULE.bazel
    non_module_dependencies = use_extension("//:extensions.bzl", "non_module_dependencies")
    use_repo(non_module_dependencies, "data_file")
    

解决与模块扩展的外部依赖项冲突

项目可以提供一个宏,用于根据其调用方的输入来引入外部代码库。但是,如果依赖项图中有多个调用方,并且它们之间存在冲突,该怎么办?

假设项目 foo 提供了以下宏,该宏将 version 作为参数。

## repositories.bzl in foo {:#repositories.bzl-foo}
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_file")
def data_deps(version = "1.0"):
    http_file(
        name = "data_file",
        url = "http://example.com/file-%s" % version,
        # Omitting the "sha256" attribute for simplicity
    )
  • 工作区

    借助 WORKSPACE,您可以从 @foo 加载宏并指定所需数据依赖项的版本。假设您还有另一个依赖项 @bar,它也依赖于 @foo,但需要数据依赖项的其他版本。

    ## WORKSPACE
    
    # Introduce @foo and @bar.
    ...
    
    load("@foo//:repositories.bzl", "data_deps")
    data_deps(version = "2.0")
    
    load("@bar//:repositories.bzl", "bar_deps")
    bar_deps() # -> which calls data_deps(version = "3.0")
    

    在这种情况下,最终用户必须仔细调整工作区中的宏顺序,才能获取所需的版本。这是 WORKSPACE 的最大痛点之一,因为它实际上无法提供一种合理的方法来解析依赖项。

  • Bzlmod

    借助 Bzlmod,项目 foo 的作者可以使用模块扩展来解决冲突。例如,假设始终从所有 Bazel 模块中选择数据依赖项的最大所需版本是合理的。

    ## extensions.bzl in foo
    load("//:repositories.bzl", "data_deps")
    
    data = tag_class(attrs={"version": attr.string()})
    
    def _data_deps_extension_impl(module_ctx):
        # Select the maximal required version in the dependency graph.
        version = "1.0"
        for mod in module_ctx.modules:
            for data in mod.tags.data:
                version = max(version, data.version)
        data_deps(version)
    
    data_deps_extension = module_extension(
        implementation = _data_deps_extension_impl,
        tag_classes = {"data": data},
    )
    
    ## MODULE.bazel in bar
    bazel_dep(name = "foo", version = "1.0")
    
    foo_data_deps = use_extension("@foo//:extensions.bzl", "data_deps_extension")
    foo_data_deps.data(version = "3.0")
    use_repo(foo_data_deps, "data_file")
    
    ## MODULE.bazel in root module
    bazel_dep(name = "foo", version = "1.0")
    bazel_dep(name = "bar", version = "1.0")
    
    foo_data_deps = use_extension("@foo//:extensions.bzl", "data_deps_extension")
    foo_data_deps.data(version = "2.0")
    use_repo(foo_data_deps, "data_file")
    

    在本例中,根模块需要数据版本 2.0,而其依赖项 bar 需要 3.0foo 中的模块扩展可以正确解决此冲突,并自动为数据依赖项选择版本 3.0

集成第三方软件包管理器

在上一部分之后,由于模块扩展提供了一种从依赖关系图中收集信息、执行自定义逻辑来解析依赖项以及调用仓库规则来引入外部仓库的方法,因此这为规则作者提供了一种很好的方法,可以增强集成了特定语言的软件包管理器的规则集。

如需详细了解如何使用模块扩展,请参阅模块扩展页面。

以下是已采用 Bzlmod 从不同软件包管理器提取依赖项的规则集列表:

示例代码库中提供了一个集成了伪软件包管理器的最小示例。

检测主机上的工具链

当 Bazel 构建规则需要检测主机上可用的工具链时,它们会使用代码库规则检查主机,并将工具链信息生成为外部代码库。

  • 工作区

    给定以下用于检测 shell 工具链的代码库规则。

    ## local_config_sh.bzl
    def _sh_config_rule_impl(repository_ctx):
        sh_path = get_sh_path_from_env("SH_BIN_PATH")
    
        if not sh_path:
            sh_path = detect_sh_from_path()
    
        if not sh_path:
            sh_path = "/shell/binary/not/found"
    
        repository_ctx.file("BUILD", """
    load("@bazel_tools//tools/sh:sh_toolchain.bzl", "sh_toolchain")
    sh_toolchain(
        name = "local_sh",
        path = "{sh_path}",
        visibility = ["//visibility:public"],
    )
    toolchain(
        name = "local_sh_toolchain",
        toolchain = ":local_sh",
        toolchain_type = "@bazel_tools//tools/sh:toolchain_type",
    )
    """.format(sh_path = sh_path))
    
    sh_config_rule = repository_rule(
        environ = ["SH_BIN_PATH"],
        local = True,
        implementation = _sh_config_rule_impl,
    )
    

    您可以在 WORKSPACE 中加载代码库规则。

    ## WORKSPACE
    load("//:local_config_sh.bzl", "sh_config_rule")
    sh_config_rule(name = "local_config_sh")
    
  • Bzlmod

    借助 Bzlmod,您可以使用模块扩展来引入相同的代码库,这类似于在上一部分中引入 @data_file 代码库。

    ## local_config_sh_extension.bzl
    load("//:local_config_sh.bzl", "sh_config_rule")
    
    sh_config_extension = module_extension(
        implementation = lambda ctx: sh_config_rule(name = "local_config_sh"),
    )
    

    然后,在 MODULE.bazel 文件中使用该扩展。

    ## MODULE.bazel
    sh_config_ext = use_extension("//:local_config_sh_extension.bzl", "sh_config_extension")
    use_repo(sh_config_ext, "local_config_sh")
    

注册工具链和执行平台

在上一小节中,我们介绍了托管工具链信息(例如 local_config_sh)的代码库,接下来,您可能需要注册工具链。

  • 工作区

    借助 WORKSPACE,您可以通过以下方式注册工具链。

    1. 您可以在 .bzl 文件中注册工具链,并在 WORKSPACE 文件中加载宏。

      ## local_config_sh.bzl
      def sh_configure():
          sh_config_rule(name = "local_config_sh")
          native.register_toolchains("@local_config_sh//:local_sh_toolchain")
      
      ## WORKSPACE
      load("//:local_config_sh.bzl", "sh_configure")
      sh_configure()
      
    2. 或者直接在 WORKSPACE 文件中注册工具链。

      ## WORKSPACE
      load("//:local_config_sh.bzl", "sh_config_rule")
      sh_config_rule(name = "local_config_sh")
      register_toolchains("@local_config_sh//:local_sh_toolchain")
      
  • Bzlmod

    使用 Bzlmod 时,register_toolchainsregister_execution_platforms API 仅在 MODULE.bazel 文件中可用。您不能在模块扩展中调用 native.register_toolchains

    ## MODULE.bazel
    sh_config_ext = use_extension("//:local_config_sh_extension.bzl", "sh_config_extension")
    use_repo(sh_config_ext, "local_config_sh")
    register_toolchains("@local_config_sh//:local_sh_toolchain")
    

在工具链选择期间,WORKSPACEWORKSPACE.bzlmod 和每个 Bazel 模块的 MODULE.bazel 文件中注册的工具链和执行平台遵循以下优先级顺序(从高到低):

  1. 在根模块的 MODULE.bazel 文件中注册的工具链和执行平台。
  2. WORKSPACEWORKSPACE.bzlmod 文件中注册的工具链和执行平台。
  3. 由根模块的(传递)依赖项模块注册的工具链和执行平台。
  4. 不使用 WORKSPACE.bzlmod 时:在 WORKSPACE 后缀中注册的工具链。

引入本地代码库

如果您需要依赖项的本地版本进行调试,或者想要将工作区中的某个目录作为外部代码库纳入,则可能需要将依赖项引入为本地代码库。

  • 工作区

    对于 WORKSPACE,这通过两个原生代码库规则(local_repositorynew_local_repository)来实现。

    ## WORKSPACE
    local_repository(
        name = "rules_java",
        path = "/Users/bazel_user/workspace/rules_java",
    )
    
  • Bzlmod

    使用 Bzlmod 时,您可以使用 local_path_override 使用本地路径替换模块。

    ## MODULE.bazel
    bazel_dep(name = "rules_java")
    local_path_override(
        module_name = "rules_java",
        path = "/Users/bazel_user/workspace/rules_java",
    )
    

    还可以引入具有模块扩展的本地代码库。不过,您无法在模块扩展中调用 native.local_repository,我们正在努力将所有原生代码库规则转换为 Starlark 规则(请查看 #18285 了解进展)。然后,您可以在模块扩展程序中调用相应的 Starlark local_repository。如果这给您带来了阻碍性问题,实现自定义版本的 local_repository 代码库规则也很简单。

绑定目标

WORKSPACE 中的 bind 规则已废弃,Bzlmod 不支持该规则。它旨在为目标在特殊的 //external 软件包中提供别名。所有依赖于此 API 的用户都应进行迁移。

举例来说,如果你的

## WORKSPACE
bind(
    name = "openssl",
    actual = "@my-ssl//src:openssl-lib",
)

这样,其他目标就可以依赖于 //external:openssl。您可以通过以下方式迁移:

  • 将对 //external:openssl 的所有使用替换为 @my-ssl//src:openssl-lib

  • 或者使用 alias build 规则

    • 在软件包(例如 //third_party)中定义以下目标

      ## third_party/BUILD
      alias(
          name = "openssl,
          actual = "@my-ssl//src:openssl-lib",
      )
      
    • //external:openssl 的所有用法替换为 //third_party:openssl-lib

迁移

本部分提供了有关 Bzlmod 迁移流程的实用信息和指南。

了解 WORKSPACE 中的依赖项

迁移的第一步是了解您拥有哪些依赖项。由于传递依赖项通常是使用 *_deps 宏加载的,因此很难确定 WORKSPACE 文件中引入了哪些确切依赖项。

使用工作区解析的文件检查外部依赖项

幸运的是,--experimental_repository_resolved_file 标志可以提供帮助。此标志实际上会为您上一个 Bazel 命令中所有提取的外部依赖项生成一个“锁文件”。如需了解详情,请参阅这篇博文

它可以通过以下两种方式使用:

  1. 获取构建特定目标所需的外部依赖项信息。

    bazel clean --expunge
    bazel build --nobuild --experimental_repository_resolved_file=resolved.bzl //foo:bar
    
  2. 用于提取 WORKSPACE 文件中定义的所有外部依赖项的信息。

    bazel clean --expunge
    bazel sync --experimental_repository_resolved_file=resolved.bzl
    

    借助 bazel sync 命令,您可以提取 WORKSPACE 文件中定义的所有依赖项,包括:

    • bind 用法
    • register_toolchainsregister_execution_platforms 用法

    但是,如果您的项目是跨平台的,则 Bazel 同步在某些平台上可能会中断,因为某些仓库规则只能在支持的平台上正常运行。

运行该命令后,resolved.bzl 文件中应包含外部依赖项的信息。

使用 bazel query 检查外部依赖项

您可能还知道,bazel query 可用于通过以下命令检查代码库规则

bazel query --output=build //external:<repo name>

虽然更方便且速度更快,但 bazel 查询可能会谎报外部依赖项版本,因此请谨慎使用!使用 Bzlmod 查询和检查外部依赖项将通过新的子命令实现。

内置默认依赖项

如果您检查 --experimental_repository_resolved_file 生成的文件,会发现许多在 WORKSPACE 中未定义的依赖项。这是因为 Bazel 实际上会向用户的 WORKSPACE 文件内容添加前缀和后缀,以注入一些默认依赖项,这些依赖项通常是原生规则(例如 @bazel_tools@platforms@remote_java_tools)所需的。使用 Bzlmod 时,这些依赖项会通过内置模块 bazel_tools 引入,该模块是所有其他 Bazel 模块的默认依赖项。

用于逐步迁移的混合模式

Bzlmod 和 WORKSPACE 可以并排运行,这样就可以逐步将依赖项从 WORKSPACE 文件迁移到 Bzlmod。

WORKSPACE.bzlmod

在迁移过程中,Bazel 用户可能需要在启用和未启用 Bzlmod 的 build 之间切换。实现了 WORKSPACE.bzlmod 支持,使过程更顺畅。

WORKSPACE.bzlmod 的语法与 WORKSPACE 完全相同。启用 Bzlmod 后,如果工作区根目录中也存在 WORKSPACE.bzlmod 文件:

  • WORKSPACE.bzlmod 会生效,并且 WORKSPACE 的内容会被忽略。
  • 没有在 WORKSPACE.bzlmod 文件中添加任何前缀或后缀

使用 WORKSPACE.bzlmod 文件可以简化迁移,因为:

  • 停用 Bzlmod 后,您将回退到从原始 WORKSPACE 文件提取依赖项。
  • 启用 Bzlmod 后,您可以使用 WORKSPACE.bzlmod 更好地跟踪要迁移的依赖项。

代码库公开范围

Bzlmod 能够控制从给定仓库中可见的其他仓库,如需了解详情,请参阅仓库名称和严格依赖项

下面总结了在考虑 WORKSPACE 时,不同类型代码库的代码库可见性。

从主代码库中 从 Bazel 模块代码库中 从模块扩展程序代码库 来自 WORKSPACE 代码库
主代码库 可见 如果根模块是直接依赖项 如果根模块是托管模块扩展程序的模块的直接依赖项 可见
Bazel 模块代码库 直接依赖项 直接依赖项 托管模块扩展的模块的直接依赖项 根模块的直接依赖项
模块扩展程序代码库 直接依赖项 直接依赖项 托管模块扩展程序的模块的直接依赖项 + 同一模块扩展程序生成的所有代码库 根模块的直接依赖项
WORKSPACE 代码库 所有可见实体 不显示 不显示 所有可见实体
@foo

迁移过程

典型的 Bzlmod 迁移过程可能如下所示:

  1. 了解您在 WORKSPACE 中拥有哪些依赖项。
  2. 在项目根目录中添加一个空的 MODULE.bazel 文件。
  3. 添加一个空的 WORKSPACE.bzlmod 文件以替换 WORKSPACE 文件内容。
  4. 启用 Bzlmod 构建目标,然后检查缺少哪个代码库。
  5. 在已解析的依赖项文件中检查缺失代码库的定义。
  6. 通过模块扩展程序以 Bazel 模块的形式引入缺失的依赖项,或将其保留在 WORKSPACE.bzlmod 中供后续迁移使用。
  7. 返回到 4 并重复,直到所有依赖项都可用。

迁移工具

使用交互式 Bzlmod 迁移帮助程序脚本可以开始使用。

该脚本会执行以下操作:

  • 生成并解析 WORKSPACE 解析文件。
  • 以直观易读的方式输出已解析文件中的代码库信息。
  • 运行 bazel 构建命令,检测识别出的错误消息,并建议迁移方法。
  • 检查 BCR 中是否已经提供依赖项。
  • 向 MODULE.bazel 文件添加依赖项。
  • 通过模块扩展添加依赖项。
  • 将依赖项添加到 WORKSPACE.bzlmod 文件。

如需使用它,请确保您已安装最新的 Bazel 版本,然后运行以下命令:

git clone https://github.com/bazelbuild/bazel-central-registry.git
cd <your workspace root>
<BCR repo root>/tools/migrate_to_bzlmod.py -t <your build targets>

发布 Bazel 模块

如果您的 Bazel 项目依赖于其他项目,您可以在 Bazel Central Registry 中发布该项目。

如需在 BCR 中签入项目,您需要提供项目的源代码归档网址。创建源归档时,请注意以下几点:

  • 确保归档指向特定版本。

    BCR 只能接受版本化源代码归档,因为 Bzlmod 需要在依赖项解析期间进行版本比较。

  • 确保归档网址稳定。

    Bazel 会通过哈希值验证归档文件的内容,因此您应确保下载的文件的校验和从不更改。如果该网址来自 GitHub,请在版本页面中创建并上传版本归档文件。GitHub 不保证按需生成的源代码归档的校验和。简而言之,采用 https://github.com/<org>/<repo>/releases/download/... 格式的网址被视为稳定,而采用 https://github.com/<org>/<repo>/archive/... 格式的网址则不稳定。如需了解更多背景信息,请参阅 GitHub 归档校验和中断

  • 确保源代码树遵循原始代码库的布局。

    如果您的代码库非常大,并且您希望通过删除不必要的源代码来创建缩减大小的分发归档,请确保被删除的源代码树是原始源代码树的子集。这样,最终用户就可以更轻松地通过 archive_overridegit_override 将模块替换为非发布版本。

  • 在用于测试最常用 API 的子目录中添加测试模块。

    测试模块是一个具有自己的 WORKSPACE 和 MODULE.bazel 文件的 Bazel 项目,该文件位于源归档文件的子目录中,该文件取决于要发布的实际模块。其中应包含涵盖您最常用 API 的示例或一些集成测试。请参阅测试模块,了解如何进行设置。

准备好源代码归档网址后,请按照 BCR 贡献准则,通过 GitHub 拉取请求将您的模块提交到 BCR。

强烈建议为您的代码库设置 Publish to BCR GitHub 应用,以自动执行将模块提交到 BCR 的过程。

最佳做法

本部分介绍了一些最佳实践,供您参考以更好地管理外部依赖项。

将目标拆分为不同的软件包,以避免提取不必要的依赖项。

请查看 #12835,其中指出系统会强制提取测试的开发依赖项,以便构建不需要这些依赖项的目标。这实际上并非特定于 Bzlmod,但遵循这些做法有助于更轻松地正确指定开发依赖项。

指定开发依赖项

您可以将 bazel_depuse_extension 指令的 dev_dependency 属性设置为 true,以便它们不会传播到依赖项项目。作为根模块,您可以使用 --ignore_dev_dependency 标志来验证您的目标是否仍会在不使用开发依赖项的情况下进行构建。

社群迁移进度

您可以查看 Bazel 中央注册库,了解您的依赖项是否已可用。否则,欢迎随时加入此 GitHub 讨论,对阻止迁移的依赖项投支持票或发布这些依赖项。

报告问题

如需了解已知的 Bzlmod 问题,请查看 Bazel GitHub 问题列表。欢迎随时提出新问题或提出功能请求,以帮助我们顺利完成迁移!