Bzlmod 迁移指南

7.3 · 7.2 · 7.1 · 7.0 · 6.5

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

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

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

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

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

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

    在后台,这项功能是使用模块扩展实现的。如果您需要执行的逻辑比仅调用代码库规则更复杂,还可以自行实现模块扩展。您需要将该定义移至 .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,我们正在对所有原生代码库规则进行星型化处理(如需了解进度,请查看 #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 生成的文件,会发现许多在 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 模块代码库 直接依赖项 直接依赖项 托管模块扩展的模块的直接依赖项 根模块的直接依赖项
模块扩展程序代码库 直接依赖项 直接依赖项 托管模块扩展程序的模块的直接依赖项 + 同一模块扩展程序生成的所有代码库 根模块的直接依赖项
工作区代码库 全部显示 不显示 不显示 所有可见实体
@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 中央注册库中发布您的项目。

为了能够在 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 问题列表。欢迎随时提交新问题或功能请求,以帮助您顺利完成迁移!