使用 Bazel 构建程序

报告问题 查看源代码 每夜 build · 7.4 . 7.3 · 7.2 · 7.1敬上 · 7.0 · 6.5

本页介绍了如何使用 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 浏览在线帮助消息。

建立一个目标

在开始构建之前,您需要一个工作区。工作区是 目录树中包含构建您的 API 所需的所有源文件, 应用。借助 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 target 的依赖项。识别所有依赖项后,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。由于没有任何更改,因此没有要重新加载的软件包,也没有要执行的 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:wiz(如果 foo/bar/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_repository new_local_repository 以及 Android SDK 和 NDK 代码库规则 无论值 --fetch 为何,该值始终有效。

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

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

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

运行脚本之后,在进入工作区之前, 文件更改。

fetch 接受要提取依赖项的目标列表。对于 例如,这将获取构建 //foo:bar 所需的依赖项 和 //bar:baz

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

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

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 选项,您可以指定其他只读目录来查找文件,而不是提取文件。如果文件名等于网址的基础名称,并且文件的哈希等于下载请求中指定的哈希,系统就会从此类目录中提取文件。只有在 WORKSPACE 声明中指定了文件哈希时,此操作才有效。

虽然文件名中的条件对于确保正确性并非必要条件, 将候选文件数量减少到每个指定目录一个。在本课中, 指定分发文件目录仍然非常高效,即使 文件的数量会越来越大。

在隔离的环境中运行 Bazel

为了让 Bazel 的二进制文件保持小巧,系统会提取 Bazel 的隐式依赖项 通过网络传输数据这些隐式依赖项包含的工具链和规则可能并不适用于所有人。对于 例如,Android 工具仅在构建 Android 时才会进行捆绑, 项目。

不过,这些隐式依赖项可能会导致 即使你已经为所有应用场景 WORKSPACE 依赖项。要解决此问题,您可以准备一个发行目录 在具有网络访问权限的机器上包含这些依赖项 通过离线方法将它们转移到隔离的环境中。

要准备分发目录,请使用 --distdir 标志。您需要针对每个新的 Bazel 二进制文件版本执行此操作,因为 隐式依赖项可能因版本而异。

如需在 Airgapp 环境之外构建这些依赖项,请先 查看正确版本的 Bazel 源代码树:

git clone https://github.com/bazelbuild/bazel "$BAZEL_DIR"
cd "$BAZEL_DIR"
git checkout "$BAZEL_VERSION"

然后,构建包含其隐式运行时依赖项的 tar 压缩文件 特定 Bazel 版本:

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

最后,在 Airgapped 环境中使用 Bazel 时,将 --distdir 标志。为方便起见,您可以将其添加为 .bazelrc 条目:

build --distdir=path/to/directory

build 配置和交叉编译

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

在任何给定的 build 中,都可能有多个配置。假设您要进行交叉编译,即为 64 位架构构建 //foo:bin 可执行文件,但您的工作站是 32 位机器。显然,构建会 需要使用能够创建 64 位架构的工具链构建 //foo:bin 可执行文件,但是,构建系统也必须构建 例如在源代码的基础上构建的工具, 例如 Genrule 中,并且这些函数必须构建为在您的工作站上运行。因此,我们可以识别出两种配置:执行配置(用于构建在构建期间运行的工具)和目标配置(或请求配置,但我们更常说“目标配置”,即使这个词已经有很多含义),用于构建您最终请求的二进制文件。

通常,许多库都是所请求的 build 目标 (//foo:bin) 和一个或多个执行工具(例如某些基础库)的先决条件。此类库必须构建两次,一次是针对 exec 一次是针对目标配置Bazel 会负责确保构建这两个变体,并将派生文件分开以避免干扰;通常,由于这些目标彼此独立,因此可以并发构建。如果您看到进度消息 表示给定目标构建了两次,这很可能是 解释。

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

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

选择不同的执行 从请求配置中删除配置最重要的是:

首先,通过使用经过剥离和优化的二进制文件,您可以缩短所花费的时间, 关联和执行工具、工具占用的磁盘空间以及 分布式 build 中的网络 I/O 时间。

其次,通过分离所有构建中的 exec 和请求配置, 避免由于对 请求配置(例如更改链接器选项) 。

正确执行增量重新编译

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

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

更笼统地说,虽然 Make 可以检测文件更改,但不会检测更改 命令。如果您更改了在给定 build 中传递给编译器的选项 Make 不会重新运行编译器,您必须手动舍弃 使用 make clean 检查上一次构建的无效输出。

此外,即使某个操作系统的某一个实例未成功终止,Make 也并不可靠。 之后,该子进程开始写入其输出文件。虽然当前的 Make 执行将失败,但 Make 的后续调用将盲目地假定截断的输出文件有效(因为它比其输入更新),并且不会重新构建。同样,如果 Make 进程被终止,也可能会出现类似的情况。

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

使用正确的增量 build 给用户带来的好处是: 造成混淆。(此外,由于使用 make clean(无论是必要的还是预防性的),等待重新构建的时间也会缩短。)

构建一致性和增量构建

正式地说,如果所有预期的输出文件都存在且内容正确(如创建这些文件所需的步骤或规则所指定),我们将构建状态定义为一致。修改源文件时, build 即是不一致的,并且在您下次运行之前将保持此一致性 才能成功完成构建工具。我们将这种情况描述为不稳定 不一致,因为这种一致性只是暂时性的,而一致性是通过 运行构建工具。

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

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

正确进行增量依赖项分析很难,正如上文所述,许多其他构建工具在避免增量构建期间出现稳定不一致的状态方面做得不好。相比之下,Bazel 提供了以下保证: 成功调用了构建工具,但在此期间未进行任何修改, build 将处于一致的状态(如果您在 则 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。 在这种情况下,请尝试使用以下代码为 Genrule 停用沙盒 --strategy=Genrule=standalone,以及使用 --spawn_strategy=standalone。此外,请在我们的问题跟踪器上报告 bug,并注明您使用的 Linux 发行版,以便我们进行调查并在后续版本中提供修复程序。

build 的阶段

在 Bazel 中,构建分为三个不同的阶段:了解 它们之间的区别可让您深入了解 (见下文)。

加载阶段

第一种是加载,在此期间,加载文件所需的所有 BUILD 文件 初始目标及其依赖项的传递闭包, 已解析、评估和缓存

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

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

分析阶段

第二个阶段是分析,包括语义分析和验证 每个构建规则、构建依赖关系图的构建,以及 确定在构建过程中每一步具体要完成的工作。

与加载一样,如果要完整计算分析,也需要几秒钟的时间。但是,Bazel 会将依赖关系图从一个构建缓存到下一个构建, 它会重新分析其内容,这样可以使增量构建以 软件包自上次构建以来未发生更改的情况。

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

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

执行阶段

构建的第三个也是最后一个阶段是执行。此阶段可确保 build 中每个步骤的输出与其输入一致,并根据需要重新运行编译/链接等工具。在这一步中 而对于大型电视而言, build。在此阶段报告的错误包括:缺少源文件、错误 或工具无法生成 预期输出集。