使用 Bazel 构建程序

报告问题 查看源代码

本页介绍了如何使用 Bazel、构建命令语法和目标模式语法构建程序。

快速入门

如需运行 Bazel,请转到基本 workspace 目录或其任何子目录,然后输入 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:显示 info 命令使用的键的列表。

bazel 工具会执行许多称为命令的函数。最常用的方法是 bazel buildbazel 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 会输出所请求的目标、是否已成功构建这些目标;如果构建成功,则会显示输出文件所在的位置。运行构建的脚本可以可靠地解析此输出;如需了解详情,请参阅 --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。由于没有任何变化,因此没有需要重新加载的软件包,也没有需要执行的构建步骤。如果“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/...packages 中的通配符,以递归方式在目录 foo(针对软件包路径的所有根目录)下表示所有软件包。:all目标的通配符,与软件包中的所有规则匹配。如 foo/...:all 中所示,这两个通配符可以组合使用;当同时使用这两个通配符时,它们可以缩写为 foo/...

此外,:*(或 :all-targets)也是与匹配软件包中的所有目标匹配的通配符,包括通常并非由任何规则构建的文件,例如与 java_binary 规则关联的 _deploy.jar 文件。

这意味着 :* 表示 :all超集;虽然可能会令人困惑,但此语法允许将熟悉的 :all 通配符用于典型 build,在这种 build 中,不需要构建 _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 将在构建期间下载外部依赖项并对其进行符号链接。但是,这可能是不可取的,因为您想要知道何时添加新的外部依赖项,或者因为您想要“预提取”依赖项(例如,在您将要离线的广告投放之前)。如果要阻止在构建期间添加新的依赖项,您可以指定 --fetch=false 标志。请注意,此标志仅适用于不指向本地文件系统中目录的代码库规则。无论 --fetch 的值如何,对 local_repositorynew_local_repository 以及 Android SDK 和 NDK 代码库规则等所做的更改都将始终生效。

如果您在构建期间禁止提取,但 Bazel 发现了新的外部依赖项,您的构建将会失败。

您可以通过运行 bazel fetch 手动提取依赖项。如果您在构建期间禁止提取,则需要运行 bazel fetch

  • 首次构建之前。
  • 添加新的外部依赖项后。

运行后,在 WORKSPACE 文件更改之前,您无需再次运行。

fetch 接受要提取依赖项的目标列表。例如,以下代码将提取构建 //foo:bar//bar:baz 所需的依赖项:

bazel fetch //foo:bar //bar:baz

如需提取工作区的所有外部依赖项,请运行以下命令:

bazel fetch //...

使用 Bazel 7 或更高版本,如果您启用了 Bzlmod,还可以通过运行以下命令来获取所有外部依赖项:

bazel fetch

如果您在工作区根目录下拥有要使用的所有工具(从库 jar 文件到 JDK 本身),则根本不需要运行 bazel 提取。但是,如果您在使用工作区目录之外的任何项目,则 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 选项,您可以指定其他只读目录来查找文件,而不是提取文件。如果文件名等于网址的基本名称,并且文件的哈希值等于下载请求中指定的哈希值,就会从此类目录中获取文件。此方法仅在 WORKSPACE 声明中指定文件哈希值时才有效。

虽然文件名条件对于正确性不是必要的,但它可以将候选文件数量减少到每个指定目录一个。通过这种方式,指定分发文件目录仍会保持高效,即使此类目录中的文件数量增加也是如此。

在经过气隙隔离的环境中运行 Bazel

为使 Bazel 的二进制文件保持较小的大小,Bazel 的隐式依赖项在首次运行时通过网络提取。这些隐式依赖项包含可能并非每个人都需要的工具链和规则。例如,Android 工具只有在构建 Android 项目时才不会捆绑和提取。

但是,在经过代理的环境中运行 Bazel 时,这些隐式依赖项可能会导致出现问题,即使您已提供所有外部依赖项的 vendor 方法,也是如此。要解决此问题,您可以在具有网络访问权限的计算机上准备包含这些依赖项的代码库缓存(使用 Bazel 7 或更高版本)或分发目录(使用 Bazel 7 之前的版本),然后通过离线方式将它们转移到经过封装的环境。

代码库缓存(使用 Bazel 7 或更高版本)

如需准备代码库缓存,请使用 --repository_cache 标志。您需要为每个新的 Bazel 二进制版本执行此操作一次,因为每个版本的隐式依赖项可能有所不同。

如需将这些依赖项提取到经过气屏蔽的环境之外,请先创建一个空的工作区:

mkdir empty_workspace && cd empty_workspace
touch MODULE.bazel
touch WORKSPACE

如需提取内置的 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

将此 tar 压缩文件导出到一个目录,以便将其复制到经过空气屏蔽的环境中。请注意 --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 中,都可以有多个配置。设想一下,在交叉编译中,您将为 64 位架构构建 //foo:bin 可执行文件,但工作站是 32 位机器。很明显,构建需要使用能够创建 64 位可执行文件的工具链来构建 //foo:bin,但构建系统还必须构建在构建过程中使用的各种工具(例如,在源代码的基础上构建,随后又用于 genrule 等工具),并且这些工具必须构建为在工作站上运行。因此,我们可以确定两种配置:一种是 exec 配置,用于构建在构建期间运行的工具;另一种是目标配置(或“请求配置”,但我们经常说“目标配置”,尽管这个词已经有多种含义),它用于构建您最终请求的二进制文件。

通常,有许多库是请求的构建目标 (//foo:bin) 以及一个或多个执行工具(例如,一些基本库)的前提条件。此类库必须构建两次,一次针对 exec 配置,一次针对目标配置。Bazel 会确保构建这两个变体,并单独保存派生文件以避免干扰;这些目标通常可以并发构建,因为它们是彼此独立的。如果您看到进度消息,指示给定目标正在构建两次,这很可能就是原因。

exec 配置派生自目标配置,如下所示:

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

最好从请求配置中选择不同的执行配置,原因有很多。最重要的一点是:

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

其次,通过分离所有 build 中的 exec 和请求配置,您可以避免因对请求配置(例如更改链接器选项)进行小幅更改而导致成本非常高昂的重新构建,如前所述。

更正增量重建

Bazel 项目的一个主要目标是确保正确进行增量重新构建。以前的构建工具(尤其是基于 Make 的构建工具)在实现增量构建时做出一些不合理的假设。

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

更笼统地说,虽然 Make 会检测文件更改,但不会检测到命令的更改。如果您在给定的构建步骤中更改传递给编译器的选项,Make 不会重新运行编译器,并且需要使用 make clean 手动舍弃上一个构建的无效输出。

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

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

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

构建一致性和增量构建

正式地说,当所有预期的输出文件都存在且文件内容正确(按照创建文件所需的步骤或规则规定)时,我们将 build 的状态定义为一致。当您修改源文件时,系统会认为构建的状态不一致,并在下次运行构建工具成功完成之前保持不一致。我们将这种情况称为“不稳定的不一致性”,因为它只是暂时性的,可以通过运行构建工具来恢复一致性。

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

从概念上讲,实现一致 build 的最简单方法是舍弃之前的所有 build 输出并重新开始:让每个 build 都是干净的 build。这种方法显然太耗时,不切实际(对发布工程师来说除外)。因此,构建工具必须能够在不影响一致性的情况下执行增量构建,这样才能派上用场。

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

与所有保证一样,有一些细则:使用 Bazel 时,有一些已知的方法可以使 Bazel 保持稳定状态不一致。对于因故意在增量依赖项分析中找出 bug 而引起的此类问题,我们无法保证进行调查,但我们会开展调查并尽最大努力修复因正常或“合理”使用构建工具而产生的所有稳定的不一致状态。

如果您在使用 Bazel 时检测到稳定的状态不一致,请报告 bug。

沙盒化执行

Bazel 使用沙盒来保证操作能够封闭且正确运行。Bazel 会在沙盒中运行“spawns”(泛指操作)。目前,沙盒适用于启用了 CONFIG_USER_NS 选项的 Linux 3.12 或更高版本,以及 macOS 10.11 或更高版本。

如果您的系统不支持沙盒,Bazel 将会输出一条警告来提醒您,build 不一定是封闭的,并且可能会以未知方式影响主机系统。如需停用此警告,您可以将 --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 发行版,以便我们进行调查,并在后续版本中提供解决方案。

构建的阶段

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

加载阶段

第一种方法是加载,在此期间,系统会加载、解析、评估和缓存初始目标的所有必要 BUILD 文件及其传递的依赖项闭包。

对于 Bazel 服务器启动后的首次构建,加载阶段通常需要几秒钟的时间,因为从文件系统中加载了很多 BUILD 文件。在后续 build 中,尤其是在没有任何 build 文件发生更改的情况下,加载速度会非常快。

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

分析阶段

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

与加载一样,完整计算时,分析也需要几秒钟的时间。但是,Bazel 会将依赖项图缓存到下一次构建,并且仅重新分析其需要的内容;这样一来,如果软件包自上次构建以来未发生变化,那么增量构建能够以极快的速度进行增量构建。

此阶段报告的错误包括:不恰当的依赖项、对规则的无效输入以及所有针对特定规则的错误消息。

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

执行阶段

构建的第三个阶段,也是最后一个阶段是执行。此阶段可确保构建中每个步骤的输出与其输入一致,并根据需要重新运行编译/关联等工具。这个步骤是构建工作大部分时间(大型构建从几秒到一小时不等)的环节。在此阶段报告的错误包括:缺少源文件、某个构建操作执行的工具中出现错误,或者工具无法生成一组预期的输出。