由于 WORKSPACE 的缺点,Bzlmod 将在未来的 Bazel 版本中取代旧版 WORKSPACE 系统。本指南可帮助您将项目迁移到 Bzlmod 并舍弃工作区,以提取外部依赖项。
WORKSPACE 与 Bzlmod
Bazel 的 WORKSPACE 和 Bzlmod 提供的功能类似,但语法不同。本部分介绍如何从特定的 WORKSPACE 功能迁移到 Bzlmod。
定义 Bazel 工作区的根目录
WORKSPACE 文件用于标记 Bazel 项目的源根目录,在 Bazel 6.3 及更高版本中,此责任已替换为 MODULE.bazel。使用 6.3 之前的 Bazel 版本时,工作区根目录中应该仍有 WORKSPACE
或 WORKSPACE.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
函数用于为工作区指定代码库名称。这样即可以@<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 模块来依赖。
工作区
对于工作区,通常使用
http_archive
或git_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_skylib
和rules_java
都依赖于platform
,则platform
依赖项的确切版本由宏的顺序决定。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 注册表中提供,您可以使用 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
文件中,这还可让您在迁移期间在工作区和 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.0
。foo
中的模块扩展可以正确解决此冲突,并自动选择版本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 时,您可以通过以下方式注册工具链。
您可以向该工具链注册
.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()
或者,直接在 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_toolchains
和register_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.bzlmod
以及每个 Bazel 模块的 MODULE.bazel
文件内注册的工具链和执行平台将遵循以下优先顺序(从高到低):
- 在根模块的
MODULE.bazel
文件中注册的工具链和执行平台。 - 在
WORKSPACE
或WORKSPACE.bzlmod
文件中注册的工具链和执行平台。 - 工具链和执行平台;由作为根模块(传递)依赖项的模块注册。
- 不使用
WORKSPACE.bzlmod
时:在WORKSPACE
后缀中注册的工具链。
引入本地代码库
如果您需要使用某个依赖项的本地版本进行调试,或者希望在工作区中将目录添加为外部仓库,则可能需要将依赖项作为本地仓库来引入。
工作区
借助 WORKSPACE,这通过两个原生仓库规则(
local_repository
和new_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)。然后,您可以在模块扩展中调用相应的 starlarklocal_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
。
提取与同步
提取和同步命令用于在本地下载外部代码库并使其保持更新。有时,还可在提取构建所需的所有代码库后,使用 --nofetch
标志进行离线构建。
工作区
同步操作会为所有代码库或一组特定的已配置代码库执行强制提取操作,而提取操作仅用于针对特定目标进行提取。
Bzlmod
同步命令不再适用,但提取提供了各种选项。您可以提取依赖项解析和模块扩展中涉及的目标、代码库、一组已配置的代码库或所有代码库。提取结果会缓存;如需强制提取,您必须在提取过程中添加
--force
选项。
迁移
本部分提供有关 Bzlmod 迁移过程的实用信息和指导。
了解 WORKSPACE 中的依赖项
迁移的第一步是了解您的依赖项。可能很难弄清楚工作区文件中引入了哪些依赖项,因为传递依赖项通常使用 *_deps
宏加载。
使用工作区解析文件检查外部依赖项
幸运的是,--experimental_repository_resolved_file
标志可以助您一臂之力。实际上,此标志会在上一个 Bazel 命令中生成包含所有提取的外部依赖项的“锁定文件”。如需了解更多详情,请参阅此博文。
它可以通过两种方式使用:
获取构建特定目标所需的外部依赖项的信息。
bazel clean --expunge bazel build --nobuild --experimental_repository_resolved_file=resolved.bzl //foo:bar
提取 WORKSPACE 文件中定义的所有外部依赖项的信息。
bazel clean --expunge bazel sync --experimental_repository_resolved_file=resolved.bzl
使用
bazel sync
命令,您可以提取 WORKSPACE 文件中定义的所有依赖项,包括:bind
种使用情况register_toolchains
和register_execution_platforms
用法
但是,如果您的项目是跨平台的,则 bazel 同步可能会在某些平台上中断,因为某些代码库规则只能在受支持的平台上正常运行。
运行该命令后,您应该会在 resolved.bzl
文件中看到外部依赖项的信息。
使用 bazel query
检查外部依赖项
您可能还知道,bazel query
可用于检查代码库规则,
bazel query --output=build //external:<repo name>
虽然使用 bazel 查询更加方便快捷,但 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 后,会回退到从原始工作区文件提取依赖项。
- 启用 Bzlmod 后,您可以使用 WORKSPACE.bzlmod 更好地跟踪剩余要迁移的依赖项。
代码库可见性
Bzlmod 能够控制可以从给定代码库中查看哪些其他代码库,如需了解详情,请查看代码库名称和严格依赖项。
下面总结了不同类型代码库的代码库可见性(还考虑了工作区)。
从主代码库中 | 通过 Bazel 模块代码库 | 来自模块扩展代码库 | 来自 WORKSPACE 代码库 | |
---|---|---|---|---|
主代码库 | Visible | 如果根模块是直接依赖项 | 如果根模块是托管模块扩展的模块的直接依赖项 | Visible |
Bazel 模块代码库 | Direct 依赖项 | Direct 依赖项 | 托管模块扩展的模块的直接依赖项 | 根模块的直接依赖项 |
模块扩展代码库 | Direct 依赖项 | Direct 依赖项 | 托管模块扩展的模块的直接依赖项 + 同一模块扩展生成的所有代码库 | 根模块的直接依赖项 |
工作区代码库 | 全部可见 | 不显示 | 不显示 | 全部可见 |
@bar
迁移过程
典型的 Bzlmod 迁移过程如下所示:
- 了解 WORKSPACE 中有哪些依赖项。
- 在项目根目录中添加一个空的 MODULE.bazel 文件。
- 添加空的 WORKSPACE.bzlmod 文件以覆盖 WORKSPACE 文件的内容。
- 在启用 Bzlmod 的情况下构建目标,并检查缺少哪个代码库。
- 请检查已解析的依赖项文件中缺失代码库的定义。
- 通过模块扩展程序将缺少的依赖项作为 Bazel 模块引入,也可以将其保留在 WORKSPACE.bzlmod 中以供后续迁移。
- 返回 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_override
和git_override
将模块替换为非发布版本。在子目录中添加一个测试模块,用于测试最常用的 API。
测试模块是一个 Bazel 项目,拥有自己的 WORKSPACE 和 MODULE.bazel 文件(位于源代码归档的子目录中,具体取决于要发布的实际模块)。其中应包含涵盖您最常用的 API 的示例或一些集成测试。如需了解如何设置,请查看测试模块。
准备好源归档网址后,请按照 BCR 贡献准则,通过 GitHub 拉取请求将模块提交到 BCR。
强烈建议为您的代码库设置发布到 BCR GitHub 应用,以自动执行将模块提交到 BCR 的过程。
最佳实践
本部分介绍了您在更好地管理外部依赖项时应遵循的一些最佳实践。
将目标拆分为不同的软件包,以避免提取不必要的依赖项。
请查看 #12835,其中系统会强制提取测试的开发依赖项,以构建不需要这些目标的目标。这实际上并不特定于 Bzlmod,但遵循此做法可让您更轻松地正确指定开发依赖项。
指定开发者依赖项
您可以将 bazel_dep
和 use_extension
指令的 dev_dependency
属性设置为 true,以防止这些指令传播到从属项目。作为根模块,您可以使用 --ignore_dev_dependency
标志来验证目标是否仍会在没有开发依赖项的情况下进行构建。
社区迁移进度
您可以查看 Bazel Central Registry,确认您的依赖项是否可用。如果不提供,则可以随时加入此 GitHub 讨论,对阻碍迁移的依赖项进行投票或发布。
报告问题
请查看 Bazel GitHub 问题列表,了解 Bzlmod 已知问题。欢迎提交新问题或提出功能请求,以帮助解锁迁移!