Bzlmod 迁移指南

报告问题 查看源代码

由于 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.
    

指定工作区的代码库名称

  • 工作区

    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 都依赖于 platoformplatform 依赖项的确切版本由宏的顺序决定。

  • 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 模块依赖项。

如需了解详情,请参阅overrides部分。

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

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

如果您的依赖项不是 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")
    

引入本地代码库

当您需要使用依赖项的本地版本进行调试,或者想要将工作区中的目录作为外部代码库合并到一起时,可能需要将某个依赖项作为本地代码库引入。

  • 工作区

    对于 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,我们一直在努力对所有原生代码库规则进行星型化处理(请查看 #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 构建规则

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

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

迁移

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

了解 WORKSPACE 中的依赖项

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

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

幸运的是,--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 生成的文件,会发现许多未在工作区中定义的依赖项。这是因为 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 代码库
主代码库 Visible 如果根模块是直接依赖项 如果根模块是托管模块扩展程序的模块的直接依赖项 Visible
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 问题列表。欢迎随时提出新问题或提出功能请求,以帮助我们顺利完成迁移!