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

    建议使用不带 @<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 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 注册表中,您可以使用 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 从不同软件包管理器提取依赖项的规则集列表:

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

检测主机上的工具链

当 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 构建规则

    • 在软件包中定义以下目标(例如 //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 生成的文件,会发现许多未在工作区中定义的依赖项。这是因为 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 模块代码库 直接依赖项 直接依赖项 托管模块扩展的模块的直接依赖项 根模块的直接依赖项
模块扩展程序代码库 直接依赖项 直接依赖项 托管模块扩展程序的模块的直接依赖项 + 由同一模块扩展程序生成的所有代码库 根模块的直接依赖项
工作区代码库 全部显示 不显示 不显示 全部显示
@foo

迁移过程

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

    测试模块是一个具有自己的 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 问题列表。欢迎随时提交新问题或功能请求,以帮助您顺利完成迁移!