Bzlmod 迁移指南

报告问题 查看源代码

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

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.
    

在 Bazel 中启用 Bzlmod

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

  • .bazelrc

    # Enable Bzlmod for every Bazel command
    common --enable_bzlmod
    

为工作区指定代码库名称

  • 工作区

    workspace 函数用于为工作区指定代码库名称。这样即可以 @<workspace name>//foo:bar 的形式引用工作区中的目标 //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,通常使用 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 中央注册表或自定义 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 注册表中尚不可用,您可以使用 use_repo_rule模块扩展引入该依赖项。

  • 工作区

    使用 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,您可以在 MODULE.bazel 文件中使用 use_repo_rule 指令直接实例化代码库:

    ## MODULE.bazel
    http_file = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "http_file")
    http_file(
        name = "data_file",
        url = "http://example.com/file",
        sha256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
    )
    

    在后台,这是使用模块扩展实现的。如果您需要执行比简单调用 Repo 规则更复杂的逻辑,也可以自行实现模块扩展。您需要将定义移动到 .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,目前正在努力对所有原生代码库规则进行星标化(请参阅 #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 构建规则

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

      ## third_party/BUILD
      alias(
          name = "openssl,
          actual = "@my-ssl//src:openssl-lib",
      )
      
    • 将需要使用的 //external:openssl 替换为 //third_party:openssl-lib

迁移

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

了解 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 查询更方便快捷,但 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 能够控制可从给定代码库中查看哪些其他代码库,如需了解详情,请查看代码库名称和严格依赖项

下面总结了不同类型的代码库的代码库可见性(将工作区考虑在内时)。

从主代码库中 通过 Bazel 模块代码库 来自模块扩展代码库 从 WORKSPACE 代码库
主代码库 Visible 如果根模块是直接依赖项 如果根模块是托管模块扩展的模块的直接依赖项 Visible
Bazel 模块代码库 Direct 依赖项 Direct 依赖项 托管模块扩展的模块的直接依赖项 根模块的直接依赖项
模块扩展代码库 Direct 依赖项 Direct 依赖项 托管模块扩展的模块的直接依赖项 + 由同一模块扩展生成的所有代码库 根模块的直接依赖项
WORKSPACE 代码库 全部可见 不显示 不显示 全部可见

迁移过程

典型的 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 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 的子目录中添加测试模块。

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

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

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

最佳实践

本部分记录了一些最佳实践,有助于您更好地管理外部依赖项。

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

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

指定开发者依赖项

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

社群迁移进度

您可以查看 Bazel Central Registry,确认您的依赖项是否可用。否则,您可以随时加入此 GitHub 讨论,对阻止迁移的依赖项进行投票或发布。

报告问题

请查看 Bazel GitHub 问题列表,了解已知的 Bzlmod 问题。欢迎提交新问题或提出功能请求,这有助于解除迁移的障碍!