Bzlmod 迁移指南

报告问题 查看来源 Nightly · 8.3 · 8.2 · 8.1 · 8.0 · 7.6

由于 WORKSPACE 存在缺点,Bzlmod 将在未来的 Bazel 版本中取代旧版 WORKSPACE 系统。本指南可帮助您将项目迁移到 Bzlmod,并放弃使用 WORKSPACE 来提取外部依赖项。

WORKSPACE 与 Bzlmod

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

定义 Bazel 工作区的根目录

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

  • WORKSPACE

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

为工作区指定代码库名称

  • WORKSPACE

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

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

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

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

以 Bazel 模块的形式提取外部依赖项

如果您的依赖项是 Bazel 项目,那么当该项目也采用 Bzlmod 时,您应该能够将其作为 Bazel 模块依赖项。

  • WORKSPACE

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

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

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

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

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

  • WORKSPACE

    使用 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

    借助 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 中宏的顺序,才能获得所需的版本。这是 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 从不同软件包管理器获取依赖项的规则集列表:

您可以在 examples 代码库中找到集成伪软件包管理器的最简示例。

检测主机上的工具链

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

  • WORKSPACE

    假设有以下用于检测 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

    对于 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

    对于 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 软件包中为目标提供别名。依赖于此的所有用户都应迁移。

举例来说,如果你的

## 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 模块代码库 直接依赖项 直接依赖项 托管模块扩展的模块的直接依赖项 根模块的直接依赖项
模块扩展代码库 直接依赖项 直接依赖项 托管模块扩展程序的模块的直接依赖项 + 由同一模块扩展程序生成的所有代码库 根模块的直接依赖项
工作区代码库 所有可见 不显示 不显示 所有可见

迁移过程

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

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

迁移工具

我们提供了一个交互式 Bzlmod 迁移辅助脚本,可帮助您开始迁移。

该脚本将执行以下操作:

  • 生成并解析 WORKSPACE 已解析文件。
  • 以简单易懂的方式输出已解析文件的代码库信息。
  • 运行 bazel build 命令,检测识别的错误消息,并推荐迁移方法。
  • 检查依赖项是否已在 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 中央注册表中发布您的项目。

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

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

    BCR 只能接受已纳入版本控制的源归档,因为 Bzlmod 需要在依赖项解析期间进行版本比较。

  • 确保归档网址稳定。

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

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

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

  • 在子目录中包含一个测试最常用 API 的测试模块。

    测试模块是一个 Bazel 项目,它在源归档文件的子目录中拥有自己的 WORKSPACE 和 MODULE.bazel 文件,并且依赖于要发布的实际模块。它应包含涵盖最常用 API 的示例或一些集成测试。请参阅测试模块,了解如何设置。

准备好源代码归档网址后,请按照 BCR 贡献指南通过 GitHub Pull Request 将您的模块提交到 BCR。

强烈建议为代码库设置发布到 BCR GitHub 应用,以自动执行向 BCR 提交模块的流程。

最佳做法

本部分介绍了一些最佳实践,您应遵循这些实践来更好地管理外部依赖项。

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

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

指定开发依赖项

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

社群迁移进度

您可以查看 Bazel 中央注册表,了解您的依赖项是否已可用。否则,欢迎加入此 GitHub 讨论,对阻碍迁移的依赖项进行投票或发布相关信息。

报告问题

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