使用 Bazel 构建程序

报告问题 查看来源 每晚 · 7.2。 · 7.1敬上 · 7.0 · 6.5 · 6.4

本页将介绍如何使用 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,后跟 target

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。因为没有任何变化,所以没有要重新加载的软件包 无需执行任何构建步骤。如果“foo”中发生变化或 则 Bazel 会重新执行一些构建操作 增量构建

构建多个目标

在 Bazel 中,您可以通过多种方式指定要构建的目标。 这些称为“目标模式”。此语法用于 buildtestquery

标签则用于指定 目标,例如在 BUILD 文件中声明依赖项时,Bazel 的目标 指定多个目标。目标模式是对 目标的标签语法(使用通配符)。在最简单的情况下, 有效标签也是一个有效的目标模式,用于标识一个正好一个的集合 目标。

// 开头的所有目标模式都会相对于当前模式进行解析 工作区。

//foo/bar:wiz 只有 1 个目标 //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 相当于: <ph type="x-smartling-placeholder">
    </ph>
  • //foo/bar/wiz:wiz(如果 foo/bar/wiz 是一个软件包)
  • //foo/bar:wiz(如果 foo/bar 是一个软件包)
  • 否则返回 //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 通配符用于 不需要构建像 _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,那么后者将构建为 是构建前者的一部分。

具有 tags = ["manual"] 的目标不包含在通配符目标模式中 (例如,...:*:all 等),在如下命令中指定 bazel buildbazel test(但它们包含在 否定通配符目标格式,也就是要减去这些格式)。您应该 如果 需要 Bazel 来构建/测试它们。相比之下,bazel query 则没有 任何此类过滤(否则, bazel query)。

提取外部依赖项

默认情况下,Bazel 将下载外部依赖项并对外部依赖项进行符号链接, build。不过,您可能并不希望这么做,因为您可能想了解 或者因为您想要 “预提取”依赖项(比如在广告投放前,您将处于离线状态)。如果您 想要防止在构建期间添加新的依赖项, 可以指定 --fetch=false 标志。请注意 适用于未指向本地 文件系统例如,更改为 local_repository new_local_repository 以及 Android SDK 和 NDK 代码库规则 无论值 --fetch 为何,该值始终有效。

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

您可以通过运行 bazel fetch 手动提取依赖项。如果 禁止在 build 提取期间执行某些操作,则需要运行 bazel fetch

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

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

fetch 会获取一系列目标的依赖项,以便获取其依赖项。对于 例如,这将获取构建 //foo:bar 所需的依赖项 和 //bar:baz

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

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

bazel fetch //...

使用 Bazel 7.1 或更高版本,如果您启用了 Bzlmod,则还可以提取所有 运行外部依赖项

bazel fetch

如果您拥有所有现有的工具,则根本不需要运行 bazel 提取。 (从库 jar 到 JDK 本身)。 但是,如果你使用的是工作区目录之外的任何内容,则使用 Bazel 将在运行前bazel fetch自动运行 bazel build

代码库缓存

Bazel 会尽量避免多次提取同一个文件,即使提取同一个文件 文件,或者如果外部 代码库已更改,但仍需下载同一文件。为此, bazel 将下载的所有文件缓存在代码库缓存中,默认情况下, 位于 ~/.cache/bazel/_bazel_$USER/cache/repos/v1/。通过 可通过 --repository_cache 选项更改位置。通过 缓存在所有工作区和安装的 bazel 版本之间共享。 如果存在以下情况,将从缓存中获取该条目: Bazel 确定自己有正确文件的副本,也就是说,如果 包含指定文件的 SHA256 总和,以及包含 哈希位于缓存中。为每个外部文件指定一个哈希 不仅从安全角度看是个好主意;还有助于避免 不必要的下载

每次缓存命中时,文件在缓存中的修改时间为 已更新。这样,上次使用缓存目录中的某个文件 例如手动清理缓存。缓存从不 因为其中可能包含非 。

分发文件目录

分发目录是另一种 Bazel 机制,可避免不必要的 下载。Bazel 会在代码库缓存之前搜索分发目录。 主要区别在于,分发目录需要手动创建 准备。

使用 --distdir=/path/to-directory 选项,您可以指定其他只读目录以查找 而不是提取它们。如果某个文件的名称为 等于该网址的基本名称,此外,该文件的哈希值为 与下载请求中指定的那个 ID 相同。仅当 文件哈希值是在 WORKSPACE 声明中指定的。

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

在隔离的环境中运行 Bazel

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

不过,这些隐式依赖项可能会导致 即使您已经为 Kubernetes 环境 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

将此 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 配置, (用于构建在构建期间运行的工具),以及目标配置 (或请求配置,但我们更常用的是“目标配置” (尽管这个词已有许多含义), 您最终请求的二进制文件。

通常情况下,有许多库同时满足所请求的 build 目标 (//foo:bin) 和一个或多个执行工具,例如 库。此类库必须构建两次,一次是针对 exec 一次是针对目标配置Bazel 会负责 确保已构建两个变体,并保留派生文件 以免造成干扰;这些目标通常可以同时构建 因为它们相互独立如果您看到进度消息 表示给定目标构建了两次,这很可能是 解释。

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

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

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

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

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

正确执行增量重新编译

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

首先,文件的时间戳单调递增。虽然这是 我们很容易就会与这种假设相悖;同步到 文件的早期修订版本会导致文件的修改时间缩短; 基于 Make 的系统不会重新构建。

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

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

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

使用正确的增量 build 给用户带来的好处是: 造成混淆。(此外,无论是必要还是预先执行,因使用 make clean 而导致的等待重新构建所需的时间更短。)

构建一致性和增量构建

正式地说,当所有预期结果都符合时,我们将构建状态定义为“一致” 输出文件存在,并且文件内容正确无误, 创建它们所需的规则修改源文件时, build 据说是不一致的,并且在您下次运行之前将保持一致 才能成功完成构建工具。我们将这种情况描述为不稳定 不一致,因为这种一致性只是暂时性的,而一致性是通过 运行构建工具。

还有一种比较恶劣的不一致性:stable 不一致性。如果 build 达到稳定的不一致状态,然后重复 成功调用构建工具不会恢复一致性:构建 出现“卡住”问题,且输出仍不正确。稳定的不一致状态 是 Make(和其他构建工具)的用户类型为 make clean 的主要原因。 发现构建工具会以这种方式失败(然后恢复 可能非常耗时且令人沮丧。

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

正确的增量依赖项分析很难,如上文所述, 其他构建工具在避免稳定不一致状态方面表现不佳, 增量构建。相比之下,Bazel 提供了以下保证: 成功调用了构建工具,但在此期间未进行任何修改, build 将处于一致的状态(如果您在 则 Bazel 无法保证构建结果的一致性 当前版本。但可以保证下一个构建的结果 恢复一致性。)

与所有保证一样,也有一些细则: 使用 Bazel 进入不一致的稳定状态我们不保证 调查因故意试图在 Google Play 中 增量依赖项分析,但我们会进行调查并尽最大努力修复 所有由正常或“合理”引起的稳定的不一致状态使用 构建工具。

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

沙盒化执行

Bazel 使用沙盒来保证操作封闭运行 正确。Bazel 可在会持续运行 Spawns(概括来说就是操作) 仅包含工具执行其工作所需的最小文件集。目前 沙盒适用于采用 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。此外,请在我们的 问题跟踪工具,并说明您使用的 Linux 发行版,以便我们 并在后续版本中提供修复。

build 的阶段

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

加载阶段

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

对于启动 Bazel 服务器后的第一个构建,加载阶段通常 需要几秒钟的时间,因为从文件系统加载了很多 BUILD 文件。在 后续 build,尤其是在 BUILD 文件未更改的情况下,应用会加载

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

分析阶段

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

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

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

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

执行阶段

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