本页回答了有关 Bazel 中外部依赖项的一些常见问题。
MODULE.bazel
如何为 Bazel 模块设置版本?
如果在源归档文件 MODULE.bazel
中使用 module
指令设置 version
,则若不仔细管理,可能会产生一些缺点和意外的副作用:
重复:发布模块的新版本通常涉及在
MODULE.bazel
中递增版本和标记版本,这两个单独的步骤可能会失去同步。虽然自动化可以降低这种风险,但更简单、更安全的方法是完全避免这种风险。不一致:使用非注册替换以特定提交替换模块的用户会看到错误的版本。例如,如果源归档中的
MODULE.bazel
设置了version = "0.3.0"
,但自该版本发布以来又进行了其他提交,则使用其中一个提交进行替换的用户仍会看到0.3.0
。实际上,版本应反映其领先于发布版本,例如0.3.1-rc1
。非注册替换问题:当用户使用非注册替换来替换模块时,使用占位符值可能会导致问题。例如,
0.0.0
不会作为最高版本进行排序,而这通常是用户在进行非注册表替换时期望的行为。
因此,最好避免在源归档文件 MODULE.bazel
中设置版本。而是应在注册表中存储的 MODULE.bazel
(例如 Bazel 中央注册表)中设置,该 MODULE.bazel
是 Bazel 在解析外部依赖项期间模块版本的实际可靠来源(请参阅 Bazel 注册表)。
此过程通常是自动化的,例如,rules-template
示例规则代码库使用 bazel-contrib/publish-to-bcr publish.yaml GitHub Action 将版本发布到 BCR。此操作会生成包含发布版本的源归档 MODULE.bazel
的补丁。此补丁存储在注册表中,并在 Bazel 的外部依赖项解析期间提取模块时应用。
这样一来,注册表中版本的发布版本将正确设置为已发布版本,因此 bazel_dep
、single_version_override
和 multiple_version_override
将按预期运行,同时避免在进行非注册表替换时出现潜在问题,因为源归档中的版本将为默认值 (''
),该值始终会得到正确处理(毕竟它是默认版本值),并且在排序时会按预期运行(空字符串被视为最高版本)。
何时应递增兼容性级别?
Bazel 模块的 compatibility_level
应在引入向后不兼容的(“重大”)变更的同一提交中递增。
不过,如果 Bazel 检测到已解析的依赖关系图中存在同一模块的不同兼容性级别的版本,则可能会抛出错误。例如,如果两个模块依赖于兼容性级别不同的第三个模块的版本,就会发生这种情况。
因此,过于频繁地递增 compatibility_level
可能会造成严重干扰,不建议这样做。为避免这种情况,只有当重大变更影响大多数使用情形,并且不容易迁移和/或解决时,才应将 compatibility_level
递增 only。
为什么 MODULE.bazel 不支持 load
s?
在依赖项解析期间,系统会从注册表中提取所有被引用的外部依赖项的 MODULE.bazel 文件。在此阶段,依赖项的源归档文件尚未提取;因此,如果 MODULE.bazel 文件是另一个文件,那么在不提取整个源归档文件的情况下,Bazel 无法实际提取该文件。load
请注意,MODULE.bazel 文件本身是特殊的,因为它直接托管在注册表中。
在 MODULE.bazel 中请求 load
的用户通常对以下几种用例感兴趣,而这些用例无需 load
即可解决:
- 确保 MODULE.bazel 中列出的版本与存储在其他位置(例如 .bzl 文件中)的 build 元数据保持一致:这可以通过在从 BUILD 文件加载的 .bzl 文件中使用
native.module_version
方法来实现。 - 将非常大的 MODULE.bazel 文件拆分为可管理的部分,尤其适用于单体代码库:根模块可以使用
include
指令将其 MODULE.bazel 文件拆分为多个段。出于与不允许在 MODULE.bazel 文件中使用load
相同的原因,include
不能在非根模块中使用。 - 旧版 WORKSPACE 系统的用户可能还记得,他们会先声明一个代码库,然后立即从该代码库进行
load
以执行复杂的逻辑。此功能已被模块扩展程序取代。
我可以为 bazel_dep
指定 SemVer 范围吗?
否。其他一些软件包管理器(例如 npm 和 Cargo)支持版本范围(隐式或显式),这通常需要约束求解器(使得用户更难预测输出),并且在没有锁定文件的情况下,版本解析无法重现。
相比之下,Bazel 使用与 Go 类似的最小版本选择,这使得输出易于预测并保证了可重现性。这种权衡符合 Bazel 的设计目标。
此外,Bazel 模块版本是 SemVer 的超集,因此在严格的 SemVer 环境中合理的内容并不总是适用于 Bazel 模块版本。
我可以自动获取 bazel_dep
的最新版本吗?
有些用户偶尔会要求指定 bazel_dep(name = "foo",
version = "latest")
以自动获取依赖项的最新版本。这与有关 SemVer 范围的问题类似,答案也是否定的。
建议的解决方案是让自动化功能来处理此问题。例如,Renovate 支持 Bazel 模块。
有时,提出此问题的用户实际上是在寻找一种在本地开发期间快速迭代的方法。您可以使用 local_path_override
实现此功能。
为什么有这么多 use_repo
?
MODULE.bazel 文件中的模块扩展程序使用情况有时会附带一个大的 use_repo
指令。例如,gazelle
中 go_deps
扩展程序的典型用法可能如下所示:
go_deps = use_extension("@gazelle//:extensions.bzl", "go_deps")
go_deps.from_file(go_mod = "//:go.mod")
use_repo(
go_deps,
"com_github_gogo_protobuf",
"com_github_golang_mock",
"com_github_golang_protobuf",
"org_golang_x_net",
... # potentially dozens of lines...
)
较长的 use_repo
指令可能看起来是多余的,因为相关信息可能已在引用的 go.mod
文件中。
Bazel 之所以需要此 use_repo
指令,是因为它会延迟运行模块扩展程序。也就是说,只有在观察到模块扩展的结果时,才会运行该模块扩展。由于模块扩展程序的“输出”是代码库定义,这意味着只有在请求模块扩展程序定义的代码库时(例如,如果构建了目标 @org_golang_x_net//:foo
,如上例所示),我们才会运行模块扩展程序。不过,在运行模块扩展之前,我们不知道它会定义哪些 repo。这时,use_repo
指令就派上用场了;用户可以告知 Bazel 他们希望扩展程序生成哪些代码库,然后 Bazel 就只会在使用这些特定代码库时运行扩展程序。
为了帮助维护此 use_repo
指令,模块扩展程序可以从其实现函数返回 extension_metadata
对象。用户可以运行 bazel mod tidy
命令来更新这些模块扩展的 use_repo
指令。
Bzlmod 迁移
哪个文件先进行评估,是 MODULE.bazel 还是 WORKSPACE?
如果同时设置了 --enable_bzlmod
和 --enable_workspace
,您自然会想知道系统会先咨询哪个系统。简而言之,系统会先评估 MODULE.bazel (Bzlmod)。
从长远来看,“哪个先评估”并不是正确的问题;相反,正确的问题应该是:在具有规范名称 @@foo
的代码库的上下文中,表观代码库名称 @bar
会解析为哪个代码库?或者,@@base
的代码库映射是什么?
具有明显代码库名称(单个前导 @
)的标签可以指代不同的事物,具体取决于它们解析自的上下文。当您看到标签 @bar//:baz
并想知道它实际指向什么时,您需要先找出上下文代码库是什么:例如,如果标签位于代码库 @@foo
中的 BUILD 文件中,则上下文代码库为 @@foo
。
然后,根据上下文代码库的不同,可以使用迁移指南中的“代码库可见性”表格来确定表观名称实际解析为哪个代码库。
- 如果上下文代码库是主代码库 (
@@
):- 如果
bar
是根模块的 MODULE.bazel 文件(通过bazel_dep
、use_repo
、module
、use_repo_rule
中的任何一个)引入的明显代码库名称,则@bar
会解析为相应 MODULE.bazel 文件所声明的内容。 - 否则,如果
bar
是在 WORKSPACE 中定义的代码库(这意味着其规范名称为@@bar
),则@bar
解析为@@bar
。 - 否则,
@bar
会解析为类似@@[unknown repo 'bar' requested from @@]
的内容,最终会导致错误。
- 如果
- 如果上下文代码库是 Bzlmod 世界代码库(即,它对应于非根 Bazel 模块,或由模块扩展生成),那么它将始终只看到其他 Bzlmod 世界代码库,而不会看到 WORKSPACE 世界代码库。
- 值得注意的是,这包括根模块中
non_module_deps
类模块扩展程序中引入的任何代码库,或根模块中的use_repo_rule
实例化。
- 值得注意的是,这包括根模块中
- 如果上下文代码库是在 WORKSPACE 中定义的:
- 首先,检查上下文代码库定义是否具有神奇的
repo_mapping
属性。如果存在,请先完成映射(因此,对于使用repo_mapping = {"@bar": "@baz"}
定义的 repo,我们将查看下面的@baz
)。 - 如果
bar
是根模块的 MODULE.bazel 文件引入的明显代码库名称,则@bar
会解析为相应 MODULE.bazel 文件声明的内容。(这与主代码库情形中的第 1 项相同。) - 否则,
@bar
解析为@@bar
。这很可能会指向 WORKSPACE 中定义的代码库bar
;如果未定义此类代码库,Bazel 将抛出错误。
- 首先,检查上下文代码库定义是否具有神奇的
更简洁的版本:
- Bzlmod-world 代码库(不包括主代码库)将仅看到 Bzlmod-world 代码库。
- WORKSPACE-world 代码库(包括主代码库)将首先查看 Bzlmod 世界中的根模块定义的内容,然后回退到查看 WORKSPACE-world 代码库。
请注意,Bazel 命令行中的标签(包括 Starlark 标志、标签类型标志值以及 build/test 目标模式)会被视为以主代码库作为上下文代码库。
其他
如何准备和运行离线 build?
使用 bazel fetch
命令预提取代码库。您可以使用 --repo
标志(如 bazel fetch --repo @foo
)仅提取仓库 @foo
(在主仓库的上下文中解析,请参阅上文中的问题),也可以使用目标模式(如 bazel fetch @foo//:bar
)提取 @foo//:bar
的所有传递依赖项(这相当于 bazel build --nobuild @foo//:bar
)。
为确保在 build 期间不发生提取,请使用 --nofetch
。更确切地说,这会导致任何运行非本地代码库规则的尝试失败。
如果您想提取代码库并修改它们以在本地进行测试,请考虑使用 bazel vendor
命令。
如何使用 HTTP 代理?
Bazel 支持其他程序(例如 curl)通常接受的 http_proxy
和 HTTPS_PROXY
环境变量。
如何在双栈 IPv4/IPv6 设置中让 Bazel 优先使用 IPv6?
在仅限 IPv6 的机器上,Bazel 可以下载依赖项,而无需进行任何更改。不过,在双栈 IPv4/IPv6 机器上,Bazel 遵循与 Java 相同的惯例,如果启用 IPv4,则优先使用 IPv4。在某些情况下(例如 IPv4 网络无法解析/访问外部地址时),这可能会导致 Network
unreachable
异常和 build 失败。在这种情况下,您可以使用 java.net.preferIPv6Addresses=true
系统属性来替换 Bazel 的行为,使其优先使用 IPv6。
具体而言:
使用
--host_jvm_args=-Djava.net.preferIPv6Addresses=true
启动选项,例如通过在.bazelrc
文件中添加以下行:startup --host_jvm_args=-Djava.net.preferIPv6Addresses=true
运行需要连接到互联网的 Java build 目标(例如用于集成测试)时,请使用
--jvmopt=-Djava.net.preferIPv6Addresses=true
工具标志。例如,在.bazelrc
文件中包含以下内容:build --jvmopt=-Djava.net.preferIPv6Addresses
如果您使用
rules_jvm_external
来解析依赖项版本,还需将-Djava.net.preferIPv6Addresses=true
添加到COURSIER_OPTS
环境变量,以便为 Coursier 提供 JVM 选项。
能否通过远程执行远程运行 repo 规则?
不能;或者至少目前还不能。使用远程执行服务来加快构建速度的用户可能会注意到,代码库规则仍会在本地运行。例如,http_archive
会先下载到本地计算机(如果适用,使用任何本地下载缓存),然后进行提取,之后每个源文件都会作为输入文件上传到远程执行服务。您可能会自然而然地问,为什么远程执行服务不直接下载并提取该归档文件,从而避免无用的往返。
部分原因是,代码库规则(和模块扩展程序)类似于由 Bazel 本身运行的“脚本”。远程执行器甚至不一定安装了 Bazel。
另一个原因是,Bazel 通常需要下载并提取归档文件中的 BUILD 文件才能执行加载和分析,而这些操作是在本地执行的。
目前有一些初步的想法,旨在通过将 repo 规则重新构想为 build 规则来解决此问题,这样自然可以远程运行它们,但反过来也会引发新的架构问题(例如,query
命令可能需要运行操作,从而使它们的设计变得复杂)。
如需详细了解此主题,请参阅一种支持需要 Bazel 才能提取的代码库的方法。