本页介绍了如何使用 Bazel 构建程序、构建命令语法和目标模式语法。
快速入门
如需运行 Bazel,请前往您的基本工作区目录或其任何子目录,然后输入 bazel
。如需创建新的工作区,请参阅构建。
bazel help
[Bazel release bazel version]
Usage: bazel command options ...
可用指令
analyze-profile
:分析 build 配置数据。aquery
:对分析后操作图执行查询。build
:构建指定的目标。canonicalize-flags
:规范化 Bazel 标志。clean
:移除输出文件,并可选择停止服务器。cquery
:执行分析后依赖项图查询。dump
:转储 Bazel 服务器进程的内部状态。help
:输出命令或索引的帮助信息。info
:显示有关 bazel 服务器的运行时信息。fetch
:提取目标的所有外部依赖项。mobile-install
:在移动设备上安装应用。query
:执行依赖项图查询。run
:运行指定的目标。shutdown
:停止 Bazel 服务器。test
:构建并运行指定的测试目标。version
:输出 Bazel 的版本信息。
获取帮助
bazel help command
:输出command
的帮助和选项。bazel help
startup_options
:用于托管 Bazel 的 JVM 的选项。bazel help
target-syntax
:介绍指定目标的语法。bazel help info-keys
:显示 info 命令使用的键的列表。
bazel
工具可执行许多功能(称为命令)。最常用的是 bazel build
和 bazel test
。您可以使用 bazel help
浏览在线帮助消息。
建立一个目标
您需要先创建一个工作区,然后才能开始构建。工作区是一个目录树,其中包含构建应用所需的所有源文件。借助 Bazel,您可以从完全只读卷执行构建。
如需使用 Bazel 构建程序,请输入 bazel build
,后跟要构建的目标。
bazel build //foo
发出用于构建 //foo
的命令后,您会看到类似于以下内容的输出:
INFO: Analyzed target //foo:foo (14 packages loaded, 48 targets configured).
INFO: Found 1 target...
Target //foo:foo up-to-date:
bazel-bin/foo/foo
INFO: Elapsed time: 9.905s, Critical Path: 3.25s
INFO: Build completed successfully, 6 total actions
首先,Bazel 会加载目标的依赖项图中的所有软件包。这包括声明的依赖项(直接在目标的 BUILD
文件中列出的文件)和传递依赖项(目标依赖项的 BUILD
文件中列出的文件)。识别所有依赖项后,Bazel 会分析这些依赖项是否正确,并创建构建操作。最后,Bazel 会执行构建的编译器和其他工具。
在构建的执行阶段,Bazel 会输出进度消息。进度消息包括当前构建步骤(例如编译器或链接器)在启动时的信息,以及已完成的构建操作数与总构建操作数的比率。构建开始后,随着 Bazel 发现整个操作图,总操作数通常会增加,但该数目会在几秒内稳定下来。
在构建结束时,Bazel 会输出请求的目标、这些目标是否成功构建,以及如果成功构建,输出文件的位置。运行 build 的脚本可以可靠地解析此输出;如需了解详情,请参阅 --show_result
。
如果您再次输入相同的命令,构建速度会快得多。
bazel build //foo
INFO: Analyzed target //foo:foo (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
Target //foo:foo up-to-date:
bazel-bin/foo/foo
INFO: Elapsed time: 0.144s, Critical Path: 0.00s
INFO: Build completed successfully, 1 total action
这是 null build。由于没有任何变化,因此没有要重新加载的软件包,也没有要执行的 build 步骤。如果“foo”或其依赖项发生了变化,Bazel 会重新执行某些构建操作,或完成增量构建。
构建多个目标
Bazel 支持多种方式来指定要构建的目标。这些统称为“目标模式”。此语法用于 build
、test
或 query
等命令。
标签用于指定单个目标(例如在 BUILD
文件中声明依赖项),而 Bazel 的目标模式用于指定多个目标。目标模式是使用通配符对目标集的标签语法的概括。在最简单的情况下,任何有效标签也是有效的目标模式,用于标识一组恰好包含一个目标的集合。
以 //
开头的所有目标模式都相对于当前工作区进行解析。
//foo/bar:wiz |
仅包含单个目标 //foo/bar:wiz 。 |
//foo/bar |
等同于 //foo/bar:bar 。 |
//foo/bar:all |
软件包 foo/bar 中的所有规则目标。 |
//foo/... |
目录 foo 下所有软件包中的所有规则目标。 |
//foo/...:all |
目录 foo 下所有软件包中的所有规则目标。 |
//foo/...:* |
foo 目录下所有软件包中的所有目标(规则和文件)。 |
//foo/...:all-targets |
foo 目录下所有软件包中的所有目标(规则和文件)。 |
//... |
主代码库中软件包中的所有规则目标。不包括外部代码库中的目标。 |
//:all |
顶级软件包中的所有规则目标(如果工作区的根目录中有 `BUILD` 文件)。 |
不以 //
开头的目标模式会相对于当前工作目录解析。以下示例假定工作目录为 foo
:
:foo |
等同于 //foo:foo 。 |
bar:wiz |
等同于 //foo/bar:wiz 。 |
bar/wiz |
等同于:
|
bar:all |
等同于 //foo/bar:all 。 |
:all |
等同于 //foo:all 。 |
...:all |
等同于 //foo/...:all 。 |
... |
等同于 //foo/...:all 。 |
bar/...:all |
等同于 //foo/bar/...:all 。 |
默认情况下,对于递归目标模式,系统会遵循目录符号链接,但指向输出基底下的符号链接除外,例如在工作区的根目录中创建的便捷符号链接。
此外,在任何包含以下名称的文件的目录中评估递归目标模式时,Bazel 都不会遵循符号链接:DONT_FOLLOW_SYMLINKS_WHEN_TRAVERSING_THIS_DIRECTORY_VIA_A_RECURSIVE_TARGET_PATTERN
foo/...
是软件包的通配符,表示目录 foo
下递归的所有软件包(适用于软件包路径的所有根目录)。:all
是对目标的通配符,可匹配软件包中的所有规则。这两者可以组合使用,如 foo/...:all
所示;如果同时使用这两个通配符,则可以缩写为 foo/...
。
此外,:*
(或 :all-targets
)是一个通配符,用于匹配匹配软件包中的每个目标,包括通常未通过任何规则构建的文件,例如与 java_binary
规则关联的 _deploy.jar
文件。
这意味着 :*
表示 :all
的超集;虽然可能会造成混淆,但这种语法确实允许在典型 build 中使用熟悉的 :all
通配符,在这种情况下,不希望构建 _deploy.jar
等目标。
此外,Bazel 允许使用斜线,而不是标签语法所需的分号;这在使用 Bash 文件名扩展时通常很方便。例如,foo/bar/wiz
等同于 //foo/bar:wiz
(如果存在软件包 foo/bar
)或 //foo:bar/wiz
(如果存在软件包 foo
)。
许多 Bazel 命令接受目标模式列表作为参数,并且它们都支持前缀否定运算符 -
。这可用于从前面的参数指定的集合中减去一组目标。请注意,这意味着顺序很重要。例如,
bazel build foo/... bar/...
表示“在 foo
下构建所有目标,并且在 bar
下构建所有目标”,而
bazel build -- foo/... -foo/bar/...
表示“构建 foo
下的所有目标(foo/bar
下的目标除外)”。(必须使用 --
参数,以防止以 -
开头的后续参数被解读为其他选项。)
不过,需要指出的是,以这种方式减去目标无法保证这些目标不会被构建,因为它们可能是未减去的目标的依赖项。例如,如果有一个目标 //foo:all-apis
依赖于 //foo/bar:api
,那么在构建前者时,系统会一并构建后者。
在 bazel build
和 bazel test
等命令中指定包含 tags = ["manual"]
的目标时,这些目标不会包含在通配符目标模式(...
、:*
、:all
等)中(但会包含在负数通配符目标模式中,即会被减去)。如果您希望 Bazel 构建/测试此类测试目标,则应在命令行中使用明确的目标模式指定此类测试目标。与之相反,bazel query
不会自动执行任何此类过滤(这会破坏 bazel query
的用途)。
提取外部依赖项
默认情况下,Bazel 会在构建期间下载并符号链接外部依赖项。不过,这可能不符合您的预期,因为您可能希望知道何时添加了新的外部依赖项,或者希望“预加载”依赖项(例如,在离线运行的测试之前)。如果您想在构建期间阻止添加新的依赖项,可以指定 --fetch=false
标志。请注意,此标志仅适用于不指向本地文件系统中目录的代码库规则。例如,对 local_repository
、new_local_repository
以及 Android SDK 和 NDK 代码库规则所做的更改始终会生效,无论 --fetch
的值如何。
如果您禁止在构建期间提取,并且 Bazel 找到新的外部依赖项,您的 build 将会失败。
您可以通过运行 bazel fetch
手动提取依赖项。如果您在构建提取期间禁止提取,则需要运行 bazel fetch
:
- 在首次构建之前。
- 添加新的外部依赖项后。
运行该模块后,除非 MODULE.bazel 文件发生更改,否则您不需要再次运行该模块。
fetch
会获取一系列目标的依赖项,以便获取其依赖项。例如,这会提取构建 //foo:bar
和 //bar:baz
所需的依赖项:
bazel fetch //foo:bar //bar:baz
如需提取工作区的所有外部依赖项,请运行以下命令:
bazel fetch //...
在 Bazel 7 或更高版本中,如果您启用了 Bzlmod,还可以通过运行以下命令提取所有外部依赖项
bazel fetch
如果您使用的所有工具(从库 jar 到 JDK 本身)都在工作区根目录下,则无需运行 bazel fetch。不过,如果您使用的是工作区目录之外的任何内容,则 Bazel 会在运行 bazel build
之前自动运行 bazel fetch
。
代码库缓存
Bazel 会尝试避免多次提取同一文件,即使不同的工作区需要同一文件,或者外部代码库的定义已更改,但仍需要下载同一文件也是如此。为此,bazel 会将下载的所有文件缓存在代码库缓存中,代码库缓存默认位于 ~/.cache/bazel/_bazel_$USER/cache/repos/v1/
。您可以使用 --repository_cache
选项更改该位置。此缓存在所有工作区和已安装的 bazel 版本之间共享。如果 Bazel 确信自己拥有正确文件的副本,即下载请求包含指定文件的 SHA256 校验和,并且缓存中存在具有该哈希值的文件,则会从缓存中提取条目。因此,从安全角度来看,为每个外部文件指定哈希值不仅是个好主意,还有助于避免不必要的下载。
每次命中缓存时,缓存中文件的修改时间都会更新。这样,便可轻松确定缓存目录中文件的上次使用时间,例如手动清理缓存。系统绝不会自动清理缓存,因为其中可能包含上游不再提供的文件的副本。
[已废弃] 分发文件目录
已弃用:如需实现离线构建,建议使用代码库缓存。
分发目录是避免不必要的下载的另一种 Bazel 机制。Bazel 会在代码库缓存之前搜索分发目录。 主要区别在于,分发目录需要手动准备。
使用 --distdir=/path/to-directory
选项,您可以指定其他只读目录来查找文件,而不是提取文件。如果文件名等于网址的基础名称,并且文件的哈希等于下载请求中指定的哈希,系统就会从此类目录中提取文件。只有在代码库规则声明中指定了文件哈希时,此方法才有效。
虽然文件名条件对正确性而言并非必需,但它会将每个指定目录的候选文件数量减少到 1 个。这样,即使此类目录中的文件数量很大,指定分发文件目录仍然高效。
在空气隔离环境中运行 Bazel
为了使 Bazel 的二进制文件保持小巧,Bazel 的隐式依赖项会在首次运行时通过网络提取。这些隐式依赖项包含的工具链和规则可能并不适用于所有人。例如,只有在构建 Android 项目时,才会解封装和提取 Android 工具。
不过,在空中隔离环境中运行 Bazel 时,这些隐式依赖项可能会导致问题,即使您已将所有外部依赖项都作为供应商依赖项提供也是如此。为解决此问题,您可以在具有网络访问权限的机器上准备包含这些依赖项的代码库缓存(使用 Bazel 7 或更高版本)或分发目录(使用 Bazel 7 之前的版本),然后使用离线方法将其传输到空气隔离环境。
代码库缓存(使用 Bazel 7 或更高版本)
如需准备代码库缓存,请使用 --repository_cache
标志。您需要针对每个新的 Bazel 二进制文件版本执行此操作,因为每个版本的隐式依赖项可能各不相同。
如需从空气隔离环境外提取这些依赖项,请先创建一个空的工作区:
mkdir empty_workspace && cd empty_workspace
touch MODULE.bazel
如需提取内置的 Bzlmod 依赖项,请运行
bazel fetch --repository_cache="path/to/repository/cache"
如果您仍依赖于旧版 WORKSPACE 文件,请运行以下命令以提取内置 WORKSPACE 依赖项
bazel sync --repository_cache="path/to/repository/cache"
最后,在隔离环境中使用 Bazel 时,请传递相同的 --repository_cache
标志。为方便起见,您可以将其添加为 .bazelrc
条目:
common --repository_cache="path/to/repository/cache"
此外,您可能还需要在本地克隆 BCR,并使用 --registry
标志指向本地副本,以防止 Bazel 通过互联网访问 BCR。将以下行添加到 .bazelrc
中:
common --registry="path/to/local/bcr/registry"
分发目录(使用版本 7 之前的 Bazel)
如需准备分发目录,请使用 --distdir
标志。您需要为每个新的 Bazel 二进制版本执行此操作一次,因为每个版本的隐式依赖项可能不同。
如需在空气隔离环境之外构建这些依赖项,请先检出正确版本的 Bazel 源代码树:
git clone https://github.com/bazelbuild/bazel "$BAZEL_DIR"
cd "$BAZEL_DIR"
git checkout "$BAZEL_VERSION"
然后,构建包含该特定 Bazel 版本的隐式运行时依赖项的 tar 压缩文件:
bazel build @additional_distfiles//:archives.tar
将此 tar 压缩文件导出到可以复制到 Airgapped 环境中的目录。请注意 --strip-components
标志,因为 --distdir
对目录嵌套层级可能非常挑剔:
tar xvf bazel-bin/external/additional_distfiles/archives.tar \
-C "$NEW_DIRECTORY" --strip-components=3
最后,在 Airgapped 环境中使用 Bazel 时,传递指向该目录的 --distdir
标志。为方便起见,您可以将其添加为 .bazelrc
条目:
build --distdir=path/to/directory
构建配置和交叉编译
用于指定给定 build 的行为和结果的所有输入都可以分为两类。第一种是存储在项目的 BUILD
文件中的固有信息:构建规则、其属性的值及其完整的传递依赖项集。第二种是外部或环境数据,由用户或构建工具提供:目标架构的选择、编译和链接选项,以及其他工具链配置选项。我们将完整的环境数据称为“配置”。
在任何给定 build 中,都可能有多个配置。考虑进行交叉编译,其中为 64 位架构构建 //foo:bin
可执行文件,但您的工作站是 32 位机器。显然,构建过程需要使用能够创建 64 位可执行文件的工具链构建 //foo:bin
,但构建系统还必须构建构建过程中使用的各种工具(例如从源代码构建的工具,然后在 genrule 等中使用),并且这些工具必须构建为在工作站上运行。因此,我们可以确定两种配置:一个是“exec”配置,用于构建在构建期间运行的工具;“目标配置”(或“请求配置”,但“目标配置”的意思更广,虽然“目标配置”的意思更广),它用于构建您最终请求的二进制文件。
通常情况下,有许多库是所请求的构建目标 (//foo:bin
) 以及一个或多个执行工具的先决条件,例如某些基础库。必须构建两次此类库,一次针对 exec 配置,一次针对目标配置。Bazel 会负责确保构建这两个变体,并将派生文件分开以避免干扰;通常,由于这些目标彼此独立,因此可以并发构建。如果您看到进度消息,其中指出系统正在两次构建给定目标,那么这很可能是原因所在。
exec 配置派生自目标配置,如下所示:
- 除非指定了
--host_crosstool_top
,否则请使用请求配置中指定的同一版本的 Crosstool (--crosstool_top
)。 - 将
--host_cpu
的值用作--cpu
(默认值:k8
)。 - 使用与请求配置中指定的选项相同的值:
--compiler
、--use_ijars
;如果使用了--host_crosstool_top
,则使用--host_cpu
的值在 Crosstool 中查找 exec 配置的default_toolchain
(忽略--compiler
)。 - 使用
--host_javabase
作为--javabase
的值 - 使用
--host_java_toolchain
作为--java_toolchain
的值 - 针对 C++ 代码使用经过优化的 build (
-c opt
)。 - 不生成任何调试信息 (
--copt=-g0
)。 - 从可执行文件和共享库中剥离调试信息 (
--strip=always
)。 - 将所有派生文件放置在一个特殊的位置,该位置与任何可能的请求配置所使用的位置不同。
- 禁止使用 build 数据对二进制文件进行加盖章(请参阅
--embed_*
选项)。 - 所有其他值仍为默认值。
从请求配置中选择不同的执行配置可能有很多优点。最重要的是:
首先,通过使用经过剥离和优化的二进制文件,您可以缩短关联和执行工具所花的时间、减少工具占用的磁盘空间,以及分布式 build 中的网络 I/O 时间。
其次,通过解耦所有 build 中的执行配置和请求配置,您可以避免因对请求配置进行细微更改(例如更改链接器选项)而导致的非常昂贵的重新构建,如前所述。
更正增量重新构建
Bazel 项目的主要目标之一是确保正确的增量重新构建。以前的构建工具(尤其是基于 Make 的工具)在实现增量 build 时会做出一些不合理的假设。
首先,文件的时间戳单调递增。虽然这是典型情况,但很容易违反此假设;同步到文件的较早修订会导致该文件的修改时间缩短;基于 make 的系统不会重新构建。
更笼统地说,虽然 Make 可以检测对文件的更改,但无法检测对命令的更改。如果您在给定构建步骤中更改传递给编译器的选项,Make 将不会重新运行编译器,您必须使用 make clean
手动舍弃之前构建的无效输出。
此外,如果某个子进程在开始写入其输出文件后终止失败,Make 对此不具备强大容错性。虽然当前的 Make 执行将失败,但 Make 的后续调用将盲目地假定截断的输出文件有效(因为它比其输入更新),并且不会重新构建。同样,如果 Make 进程被终止,也可能会出现类似的情况。
Bazel 避免了这些假设以及其他假设。Bazel 维护着一个包含先前完成的所有工作的数据库,并且仅在发现该构建步骤的一组输入文件(及其时间戳)和该构建步骤的编译命令与数据库中的一种完全匹配,且该数据库条目的一组输出文件(及其时间戳)与磁盘上文件的时间戳完全匹配时才忽略构建步骤。对输入文件、输出文件或命令本身进行任何更改都会导致重新执行构建步骤。
正确的增量 build 对用户的好处在于:减少因混淆而浪费的时间。(此外,由于使用 make
clean
(无论是必要的还是预防性的),等待重新构建的时间也会缩短。)
构建一致性和增量 build
正式地说,如果所有预期的输出文件都存在且内容正确(如创建这些文件所需的步骤或规则所指定),我们会将 build 的状态定义为一致。修改源文件后,构建状态会被视为不一致,并且在下次成功运行构建工具之前保持不一致状态。我们将这种情况描述为“不稳定的不一致性”,因为它只是暂时的,可通过运行构建工具来恢复一致性。
还有一种有害的不一致性:稳定的不一致性。如果 build 达到稳定的不一致状态,则反复成功调用 build 工具无法恢复一致性:build 已“卡住”,并且输出仍然不正确。稳定的不一致状态是 Make(和其他构建工具)的用户类型为 make clean
的主要原因。发现构建工具以这种方式失败(然后从中恢复)可能非常耗时且令人沮丧。
从概念上讲,实现一致 build 的最简单方法是丢弃所有之前的 build 输出,然后重新开始:使每个 build 都是干净 build。这种方法显然太耗时,无法实际使用(可能只有发布工程师除外)。因此,为了发挥作用,构建工具必须能够在不影响一致性的情况下执行增量构建。
正确的增量依赖项分析很难,并且如上所述,许多其他构建工具在避免增量构建期间的稳定不一致状态方面表现不佳。与之相反,Bazel 提供以下保证:在成功调用构建工具(期间您未进行任何修改)后,构建将处于一致状态。(如果您在构建期间修改源文件,Bazel 无法保证当前构建结果的一致性。但它可以保证下一个 build 的结果会恢复一致性。)
与所有保证一样,这也有一些细则:有一些已知的方法会导致 Bazel 进入稳定的不一致状态。我们不保证会调查因刻意尝试在增量依赖项分析中查找 bug 而导致的此类问题,但我们会调查并尽力修复因正常或“合理”使用构建工具而导致的所有稳定不一致状态。
如果您发现 Bazel 存在稳定的不一致状态,请报告 bug。
沙盒化执行
Bazel 使用沙盒来保证操作能够密封且正确地运行。Bazel 会在沙盒中运行生成的子进程(粗略地说:操作),沙盒中仅包含该工具执行其任务所需的一组最少文件。目前,沙盒功能适用于启用了 CONFIG_USER_NS
选项的 Linux 3.12 或更高版本,以及 macOS 10.11 或更高版本。
如果您的系统不支持沙盒化,Bazel 会输出警告,提醒您构建不保证是密封的,并且可能会以未知的方式影响主机系统。如需停用此警告,您可以将 --ignore_unsupported_sandboxing
标志传递给 Bazel。
出于安全考虑,在某些平台(例如 Google Kubernetes Engine 集群节点或 Debian)上,用户命名空间默认处于停用状态。您可以通过查看文件 /proc/sys/kernel/unprivileged_userns_clone
来检查这一点:如果该文件存在且包含 0,则可以使用 sudo sysctl kernel.unprivileged_userns_clone=1
激活用户命名空间。
在某些情况下,由于系统设置,Bazel 沙盒无法执行规则。此问题的典型症状是失败,并输出类似于 namespace-sandbox.c:633: execvp(argv[0], argv): No such file or directory
的消息。在这种情况下,请尝试使用 --strategy=Genrule=standalone
为 genrule 停用沙盒,并使用 --spawn_strategy=standalone
为其他规则停用沙盒。此外,请在我们的问题跟踪器上报告 bug,并注明您使用的 Linux 发行版,以便我们进行调查并在后续版本中提供修复程序。
build 的阶段
在 Bazel 中,构建分为三个不同的阶段;作为用户,了解它们之间的差异可以深入了解控制构建的选项(见下文)。
加载阶段
第一种是加载,在此过程中,系统会加载、解析、评估和缓存初始目标的所有必要 BUILD 文件及其依赖项的传递闭包。
对于启动 Bazel 服务器后的首次构建,加载阶段通常需要几秒钟,因为系统需要从文件系统加载许多 BUILD 文件。在后续 build 中(尤其是没有任何 BUILD 文件发生更改时),加载速度非常快。
在此阶段报告的错误包括:找不到软件包、找不到目标、BUILD 文件中的词汇和语法错误,以及评估错误。
分析阶段
第二阶段“分析”包括对每条 build 规则的语义分析和验证、构建 build 依赖关系图,以及确定在 build 的每个步骤中要完成的具体工作。
与加载一样,完整计算分析也需要几秒钟的时间。不过,Bazel 会将一个 build 的依赖关系图缓存到下一个 build,并且只会重新分析必须重新分析的内容,这使得在软件包自上一个 build 以来没有更改的情况下,增量 build 速度可以非常快。
此阶段报告的错误包括:不适当的依赖项、规则的无效输入,以及所有特定于规则的错误消息。
加载和分析阶段速度很快,因为 Bazel 会在此阶段避免不必要的文件 I/O,只读取 BUILD 文件以确定要执行的工作。这是设计使 Bazel 奠定了良好基础,可作为分析工具(如 Bazel 的 query 命令在加载阶段之际实现的)的基础。
执行阶段
构建的第三个也是最后一个阶段是执行。此阶段可确保 build 中每个步骤的输出与其输入一致,并根据需要重新运行编译/链接等工具。构建会花费大部分时间在此步骤,对于大型 build,此步骤可能需要几秒钟到一个多小时。在此阶段报告的错误包括:缺少源文件、某个构建操作执行的工具中存在的错误,或工具无法生成一组预期输出。