使用 Bazel 构建程序

报告问题 查看来源 Nightly · 8.3 · 8.2 · 8.1 · 8.0 · 7.6

本页介绍了如何使用 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 helpstartup_options:托管 Bazel 的 JVM 的选项。
  • bazel helptarget-syntax:说明指定目标的语法。
  • bazel help info-keys:显示信息命令所用键的列表。

bazel 工具可执行许多功能,称为命令。最常用的有 bazel buildbazel test。您可以使用 bazel help 浏览在线帮助消息。

构建单个目标

您需要先创建工作区,然后才能开始构建。工作区是一个目录树,其中包含构建应用所需的所有源文件。Bazel 允许您从完全只读的卷执行 build。

如需使用 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 执行构建的编译器和其他工具。

在 build 的执行阶段,Bazel 会输出进度消息。进度消息包括当前正在启动的 build 步骤(例如,编译器或链接器),以及已完成的 build 操作数与 build 操作总数的比值。随着 build 开始,总操作数通常会随着 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

这是空 build。由于没有任何变化,因此无需重新加载任何软件包,也无需执行任何 build 步骤。如果“foo”或其依赖项发生了更改,Bazel 会重新执行一些构建操作,或者完成增量构建

构建多个目标

Bazel 允许通过多种方式指定要构建的目标。这些统称为“目标模式”。此语法用于 buildtestquery 等命令中。

标签用于指定单个目标(例如在 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 等效于:
  • 如果 foo/bar/wiz 是软件包,则为 //foo/bar/wiz:wiz
  • 如果 foo/bar 是软件包,则为 //foo/bar:wiz
  • 否则返回 //foo: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超集;虽然这种语法可能会令人困惑,但它确实允许将熟悉的 :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 buildbazel test 等命令中指定时,具有 tags = ["manual"] 的目标不会包含在通配符目标格式(...:*:all 等)中(但会包含在负通配符目标格式中,也就是说会被减去)。如果您希望 Bazel 构建/测试此类测试目标,则应在命令行中使用明确的目标模式指定这些测试目标。相比之下,bazel query 不会自动执行任何此类过滤(这会违背 bazel query 的目的)。

提取外部依赖项

默认情况下,Bazel 会在构建期间下载外部依赖项并创建符号链接。不过,这可能并不理想,因为您可能希望在添加新的外部依赖项时收到通知,或者希望“预提取”依赖项(例如,在您即将离线的情况下)。如果您想阻止在 build 期间添加新的依赖项,可以指定 --fetch=false 标志。请注意,此标志仅适用于不指向本地文件系统中目录的代码库规则。例如,对 local_repositorynew_local_repository 以及 Android SDK 和 NDK 代码库规则的更改始终会生效,无论 --fetch 的值是多少。

如果您禁止在 build 期间提取,并且 Bazel 找到新的外部依赖项,则 build 将失败。

您可以通过运行 bazel fetch 手动提取依赖项。如果您禁止在 build 期间提取,则需要运行 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 版本的隐式运行时依赖项的 tarball:

bazel build @additional_distfiles//:archives.tar

将此 tarball 导出到可复制到气隙环境的目录。请注意 --strip-components 标志,因为 --distdir 对目录嵌套级别可能非常挑剔:

tar xvf bazel-bin/external/additional_distfiles/archives.tar \
  -C "$NEW_DIRECTORY" --strip-components=3

最后,在气隙环境中使用 Bazel 时,请传递指向该目录的 --distdir 标志。为方便起见,您可以将其添加为 .bazelrc 条目:

build --distdir=path/to/directory

构建配置和交叉编译

指定给定 build 的行为和结果的所有输入可分为两个不同的类别。第一种是存储在项目 BUILD 文件中的内在信息:build 规则、其属性的值以及其传递依赖项的完整集。第二种是外部数据或环境数据,由用户或 build 工具提供:目标架构的选择、编译和链接选项以及其他工具链配置选项。我们将一整套环境数据称为“配置”

在任何给定的 build 中,可能存在多个配置。以交叉编译为例,您要为 64 位架构构建 //foo:bin 可执行文件,但您的工作站是 32 位机器。显然,该 build 将需要使用能够创建 64 位可执行文件的工具链来构建 //foo:bin,但 build 系统还必须构建 build 本身期间使用的各种工具(例如从源代码构建的工具,然后用于 genrule 中),并且这些工具必须构建为可在工作站上运行。因此,我们可以确定两种配置:执行配置(用于构建在 build 期间运行的工具)和目标配置(或请求配置,不过我们更常说“目标配置”,即使该词已有多种含义),用于构建您最终请求的二进制文件。

通常,有许多库是所请求的 build 目标 (//foo:bin) 和一个或多个执行工具(例如某些基本库)的前提条件。此类库必须构建两次,一次用于执行配置,一次用于目标配置。Bazel 会负责确保构建这两个变体,并确保派生文件保持分离以避免干扰;通常,此类目标可以并发构建,因为它们彼此独立。如果您看到进度消息表明某个指定目标正在构建两次,则很可能就是这种情况。

执行配置从目标配置派生而来,如下所示:

  • 使用与请求配置中指定的相同的 Crosstool 版本 (--crosstool_top),除非指定了 --host_crosstool_top
  • --host_cpu 的值用于 --cpu(默认值:k8)。
  • 使用与请求配置中指定的相同的值:--compiler--use_ijars,如果使用 --host_crosstool_top,则使用 --host_cpu 的值在 Crosstool 中查找执行配置的 default_toolchain(忽略 --compiler)。
  • 使用 --host_javabase 的值作为 --javabase 的值
  • 使用 --host_java_toolchain 的值作为 --java_toolchain 的值
  • 针对 C++ 代码使用优化后的 build (-c opt)。
  • 不生成任何调试信息 (--copt=-g0)。
  • 从可执行文件和共享库中剥离调试信息 (--strip=always)。
  • 将所有派生文件放置在与任何可能的请求配置所使用的位置不同的特殊位置。
  • 禁止使用 build 数据对二进制文件进行标记(请参阅 --embed_* 选项)。
  • 所有其他值都保留为默认值。

在许多情况下,选择与请求配置不同的执行配置可能更合适。最重要的是:

首先,通过使用精简的优化二进制文件,您可以减少链接和执行工具所花费的时间、工具占用的磁盘空间以及分布式构建中的网络 I/O 时间。

其次,通过在所有 build 中分离执行配置和请求配置,您可以避免因对请求配置进行细微更改(例如更改链接器选项)而导致非常耗时的重新 build,如前所述。

修正增量重建

Bazel 项目的主要目标之一是确保正确的增量重建。之前的 build 工具(尤其是基于 Make 的工具)在实现增量 build 时做出了几个不合理的假设。

首先,文件的时间戳单调递增。虽然这是典型情况,但很容易违反此假设;同步到文件的早期修订版本会导致该文件的修改时间减少;基于 Make 的系统将不会重新构建。

更一般地说,虽然 Make 可以检测到文件更改,但无法检测到命令更改。如果您在给定的 build 步骤中更改传递给编译器的选项,Make 将不会重新运行编译器,并且必须使用 make clean 手动舍弃之前 build 的无效输出。

此外,如果某个子进程在开始写入其输出文件后未能成功终止,Make 也无法应对。虽然当前执行的 Make 会失败,但后续调用 Make 时会盲目地假设截断的输出文件有效(因为它比输入文件新),并且不会重新构建。同样,如果 Make 进程被终止,也会发生类似情况。

Bazel 会避免这些假设以及其他假设。Bazel 会维护一个包含之前完成的所有工作的数据库,并且仅当它发现某个 build 步骤的输入文件集(及其时间戳)和该 build 步骤的编译命令与数据库中的某个条目完全匹配,并且该数据库条目的输出文件集(及其时间戳)与磁盘上文件的时间戳完全匹配时,才会省略该 build 步骤。对输入文件、输出文件或命令本身所做的任何更改都会导致重新执行 build 步骤。

正确的增量 build 对用户的好处是:减少因混淆而浪费的时间。(此外,由于使用 make clean 而导致的重建等待时间也更少,无论重建是必要的还是抢占式的。)

构建一致性和增量构建

从形式上讲,当所有预期输出文件都存在且其内容正确(如创建这些文件所需的步骤或规则所指定的那样)时,我们将 build 的状态定义为一致。当您修改源文件时,构建的状态称为不一致,并且在您下次运行构建工具并成功完成之前一直保持不一致。我们将这种情况称为不稳定的不一致,因为这种情况只是暂时的,并且通过运行 build 工具可以恢复一致性。

还有一种有害的不一致性:稳定不一致性。如果 build 达到稳定的不一致状态,那么重复成功调用 build 工具也无法恢复一致性:build 已“卡住”,输出仍然不正确。稳定的不一致状态是 Make(和其他 build 工具)用户输入 make clean 的主要原因。 发现构建工具以这种方式失败(然后从中恢复)可能非常耗时且令人沮丧。

从概念上讲,实现一致 build 的最简单方法是舍弃所有之前的 build 输出并重新开始:使每个 build 都是干净的 build。这种方法显然过于耗时,不切实际(或许发布工程师除外),因此,为了实用起见,构建工具必须能够在不影响一致性的前提下执行增量构建。

正确的增量依赖项分析很难,如上所述,许多其他 build 工具在增量 build 期间避免稳定不一致状态方面做得不好。相比之下,Bazel 提供以下保证:在成功调用构建工具(期间您未进行任何编辑)后,构建将处于一致状态。(如果您在构建期间修改源文件,Bazel 不会保证当前构建结果的一致性。但它确实保证了下一个 build 的结果将恢复一致性。)

与所有保证一样,这里也有一些细则:有一些已知的方法会导致 Bazel 进入稳定的不一致状态。我们不会保证调查因故意尝试在增量依赖关系分析中查找 bug 而导致的问题,但我们会调查并尽力修复因正常或“合理”使用 build 工具而导致的所有稳定的不一致状态。

如果您发现 Bazel 处于稳定的不一致状态,请报告 bug。

沙盒化执行

Bazel 使用沙盒来保证操作以封闭且正确的方式运行。Bazel 在沙盒中运行 spawn(粗略地说:操作),这些沙盒仅包含工具完成其工作所需的最少文件集。目前,沙盒机制适用于启用了 CONFIG_USER_NS 选项的 Linux 3.12 或更高版本,以及 macOS 10.11 或更高版本。

如果您的系统不支持沙盒,Bazel 将打印一条警告,提醒您构建无法保证是密封的,并且可能会以未知方式影响宿主系统。如需停用此警告,您可以向 Bazel 传递 --ignore_unsupported_sandboxing 标志。

在某些平台(例如 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 发行版,以便我们调查并在后续版本中提供修复。

构建阶段

在 Bazel 中,构建分为三个不同的阶段;作为用户,了解它们之间的区别有助于深入了解控制构建的选项(见下文)。

加载阶段

第一个阶段是加载,在此阶段,系统会加载、解析、评估初始目标及其传递闭包依赖项的所有必要 BUILD 文件,并将其缓存。

在启动 Bazel 服务器后,对于第一次 build,加载阶段通常需要花费数秒时间,因为系统会从文件系统中加载许多 BUILD 文件。在后续构建中,尤其是当没有 BUILD 文件发生更改时,加载速度会非常快。

此阶段报告的错误包括:找不到软件包、找不到目标、BUILD 文件中存在词法和语法错误,以及评估错误。

分析阶段

第二阶段是分析,涉及对每个 build 规则进行语义分析和验证、构建 build 依赖关系图,以及确定在 build 的每个步骤中要完成的确切工作。

与加载一样,如果完整计算,分析也需要几秒钟的时间。 不过,Bazel 会将依赖关系图从一个 build 缓存到下一个 build,并且仅重新分析必须重新分析的内容,这使得增量 build 在软件包自上一个 build 以来未发生变化的情况下非常快速。

此阶段报告的错误包括:不合适的依赖项、规则的无效输入,以及所有特定于规则的错误消息。

加载和分析阶段速度很快,因为 Bazel 在此阶段会避免不必要的文件 I/O,仅读取 BUILD 文件以确定要完成的工作。这是有意为之,这使得 Bazel 成为分析工具(例如 Bazel 的 query 命令,该命令是在加载阶段之上实现的)的良好基础。

执行阶段

构建的第三个阶段也是最后一个阶段是执行。此阶段可确保 build 中每个步骤的输出与其输入保持一致,并根据需要重新运行编译/链接等工具。此步骤是 build 花费时间最多的步骤,对于大型 build,花费的时间从几秒到一小时不等。此阶段报告的错误包括:缺少源文件、由某些 build 操作执行的工具中存在错误,或工具未能生成预期的一组输出。