2022 年 BazelCon 将于 11 月 16 日至 17 日在纽约和线上举办。
立即报名!

使用 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 会执行相应 build 的编译器和其他工具。

在构建执行阶段,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 通配符用于典型的构建,此类构建不需要 _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 标志。请注意,此标志仅适用于不指向本地文件系统中的目录的代码库规则。例如,对 local_repositorynew_local_repository 以及 Android SDK 和 NDK 代码库规则的更改将始终有效,无论值 --fetch 如何。

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

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

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

一旦运行完毕,您就不应再重新运行,直到 WORKSPACE 文件发生更改为止。

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

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

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

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 项目时才会被捆绑并提取。

但是,即使您已供应所有 WORKSPACE 依赖项,这些隐式依赖项也可能导致在设置了缺口的环境中运行 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 压缩文件导出到一个可复制到气隙环境中的目录中。请注意 --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 中,可能有多个配置。假设有一个交叉编译,您为 64 位架构构建 //foo:bin 可执行文件,但您的工作站是 32 位计算机。很显然,该构建将需要使用能够创建 64 位可执行文件的工具链来构建 //foo:bin,但构建系统还必须构建在构建过程中使用的各种工具(例如,先从源代码构建,然后再在 genrule 中使用等工具),并且必须在您的工作站上运行。因此,我们可以确定两种配置:一种是主机配置(用于构建在构建期间运行的工具),另一种是目标配置(或“请求配置”,但我们更常使用“目标配置”,尽管该字词有多种含义,我们用前者更常用来指代其用于构建您最终请求的二进制文件)。

通常,有许多库是所请求的构建目标 (//foo:bin) 和一个或多个主机工具的先决条件,例如一些基本库。此类库必须构建两次,一次针对主机配置,一次针对目标配置。Bazel 会负责确保构建两个变体,并将生成的文件分开以避免干扰;通常,此类目标可以同时构建,因为它们彼此独立。如果您看到表明正在构建指定目标两次的进度消息,则很可能是原因所在。

Bazel 会根据 --distinct_host_configuration 选项,使用以下两种方法之一选择主机配置。此布尔值选项略微巧妙,该设置可能会提高(或加重)构建速度。

--distinct_host_configuration=false

当此选项为 false 时,主机和请求配置完全相同:构建期间所需的所有工具的构建方式与目标程序完全相同。此设置表示在单次构建期间无需构建两次库。

不过,这也意味着对请求配置所做的任何更改也会影响主机配置,导致所有工具都要重新构建,然后依赖于工具输出的任何内容也要重新构建。因此,简单地在 build 之间更改链接器选项可能会导致所有工具重新关联,而所有使用它们的操作都会重新执行,依此类推,从而导致重新构建变得非常庞大。

--distinct_host_configuration=true(默认)

如果此选项为 true,则对主机和请求使用相同的配置,而不是使用完全不同的主机配置。主机配置派生自目标配置,如下所示:

  • 除非指定了 --host_crosstool_top,否则请使用请求配置中指定的同一版本的 Crosstool (--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++ 代码 (-c opt) 的优化 build。
  • 不生成调试信息 (--copt=-g0)。
  • 从可执行文件和共享库 (--strip=always) 中移除调试信息。
  • 将所有派生的文件都放在与所有可能的请求配置不同的位置。
  • 禁止使用 build 数据为二进制文件添加时间戳(请参阅 --embed_* 选项)。
  • 其他所有值仍保留默认值。

从请求配置中选择不同的主机配置可能有很多原因。其中有些太过奇怪,因此不能提及,但其中有两个值得注意。

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

其次,通过分离所有 build 中的主机和请求配置,可以避免由于对请求配置进行细微更改(例如更改链接器选项)而产生的非常昂贵的重新构建。

也就是说,对于某些 build,此选项可能是一种阻碍。特别是,不经常更改配置的 build(尤其是某些 Java build)和必须在主机和目标配置中构建的代码量很大的 build 可能没有好处。

更正增量重建

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

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

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

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

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

使用正确的增量构建可让用户获益:减少因混淆而浪费的时间。(此外,使用 make clean 导致重新构建所花费的时间(无论是必要的还是预先的)更少。)

构建一致性和增量构建

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

此外,还有一种危险的不一致是:稳定不一致。如果 build 达到稳定的不一致状态,那么反复成功调用构建工具不会恢复一致性:build 已卡住,输出仍不正确。使用稳定版本(及其他构建工具)的用户类型是 make clean,主要原因是稳定、不一致的状态。发现构建工具以这种方式失败(然后从中恢复)既耗时又令人沮丧。

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

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

与所有保证一样,也有一些精细打印:通过 Bazel,有一些已知的方法使状态变得稳定。我们无法保证会调查因故意在增量依赖项分析中查找错误而引发的此类问题,但我们会调查并尽最大努力解决常规或“合理”使用构建工具导致的所有稳定的状态。

如果您曾使用 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 的其他规则停用沙盒。此外,请在问题跟踪器中报告错误,并说明您目前使用的 Linux 发行版,以便我们调查并后续版本修正问题。

构建阶段

在 Bazel 中,构建分为三个不同的阶段;作为用户,了解它们之间的差异有助于您深入了解控制构建的各个选项(请参阅下文)。

加载阶段

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

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

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

分析阶段

第二阶段(分析)涉及每个构建规则的语义分析和验证、构建依赖项图的构建以及确定要在构建的每个步骤中完成的工作的确切内容。

与加载一样,完整分析也可能需要几秒钟的时间。 不过,Bazel 会缓存从一个 build 到下一个 build 的依赖项图,并且只会重新分析该 build 包含的内容,这样一来,自上一个 build 以来软件包并未更改时,这可以使增量构建变得非常快速。

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

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

执行阶段

构建过程的第三个阶段(也是最后一个阶段)是“执行”。此阶段可确保 build 中每个步骤的输出与其输入保持一致,并根据需要重新运行编译/链接/工具。此步骤占据了大部分时间,从几秒到一小时以上(对于大型构建)。在此阶段报告的错误包括:源文件缺失、某个构建操作执行的工具出错,或工具无法生成预期输出集。