Bazel 使用指南

2021-01-27  本文已影响0人  sarto

https://docs.bazel.build/versions/master/guide.html

使用 Bazel 构建程序

build 命令

输入 bazel build, 然后输入希望构建的 target 名称。这是一个典型的会话:

% bazel build //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 在加载 target 依赖的闭包时会打印进度信息,然后开始分析它们的正确性并创建构建 actions,最后执行编译或其他的构建工具。

Bazel 会在构建的执行阶段打印进度信息,显示当前正在启动的构建动作(编译,链接,等等),以及在构建操作总数中完成的数量。在构建开始时,actions 的总数量会随着 Bazel 发现整个操作图而增加爱,但通常会在几秒钟内稳定下来。

在构建结束时,Bazel 打印请求的 target 是否构建成功,如果是的话,会打印输出文件位置。运行构建的脚本可以可靠的解析此输出。更多的信息可以使用 --show_result。

目标匹配模式

Bazel 允许一些指定目标的方式进行构建。这些被统称为目标匹配模式。这个语法被用于 build,test,query 命令。

由于 labels 被用于指定单独的目标,例如在 BUILD 文件中声明依赖,Bazel 目标模式是以个指定多 target 的语法:它们是使用通配符对目标集的标签语法的概括。最简单的情况下,任何有效的 label 也是一个目标匹配,指定一组恰好一个target。

所有已 // 开始的目标模式都相对于当前工作空间进行解析。

//foo/bar:wiz    对应一个单一目标 //foo/bar:wiz
//foo/bar          相当于 //foo/bar:bar
//foo/bar:all      //foo/bar 中的所有规则
//foo/...            foo 目录下的所有软件包的规则
//foo/...:all        同上
//foo/...:*          foo 目录下的所有软件包的 target(规则和文件) 
//foo/...:all-targets  同上

不以 // 开头的目标匹配路径相对于当前工作目录

默认情况下,目录符号链接遵循目标匹配模式,除非目标指向输出目录,例如在工作空间的更目录中创建符号链接。

除此之外,在包含以下文件的任何目录中评估递归目标模式时,Bazel 不会遵循符号链接DONT_FOLLOW_SYMLINKS_WHEN_TRAVERSING_THIS_DIRECTORY_VIA_A_RECURSIVE_TARGET_PATTERN

许多 Bazel 命令接受目标模式列表作为参数,并且它们都遵从否定运算符 - 。者能从前边的目标中减去指定的目标。这意味着顺序很重要,例如。

bazel build foo/... bar/...

意味着构建 foo bar 下所有target

bazel build -- foo/... -foo/bar/...

意味着构建 foo 下所有目标除了 foo/bar (-- 参数是必备的,是为了防止 - 参数被解释为其他附加参数)

值得一提的是,被减去的 targets 不保证不会构建,因为它们可能作为其他的依赖关系,在构建其他关系时被构建。

有 tags=["manual"] 的 target 将不会包含在目标匹配模式中,如果你希望 Bazel 构建或者测试这些 target,你应该在命令行上显式的指定。

获取外部依赖

默认情况下,Bazel 会在构建过程中下载并链接外部依赖。然而这可能并不是希望的动作,因为你想知道新的外部依赖是什么时间添加的,或者是你想先拉取依赖关系(比如你即将处于离线状态)。如果你想防止在构建的时候添加信依赖的情况,可以指定 --fetch=false 参数。注意这个参数仅在非本地规则中生效,例如 local_repository 和 new_local_repository 还有SKD 和 NDK 仓库不受 --fetch 的影响。

如果你在构建时禁止了 fethe 并且 Bazel 找到了新的依赖,构建将会失败。

你可以通过 bazel fetch 手动获取依赖。如果你禁止了构建时拉取,你需要运行 bazel fetch

一旦它已经开始运行,你无需再次运行直到 WORKSPACE 文件发生更改。

fetch 命令也适用目标匹配模式

如果你的工作区下有所有构建需要使用的工具,则你不需要运行 bazel fetch。然而,如果你是永乐工作区以外的任何东西,Bazel 将会在 bazel build 开始之前运行 bazel fetch。

存储库缓存(The repository cache)

Bazel 会尽力避免多次获取相同的文件,这些情况包括这些相同的文件被不同的工作区所需要,或者在外部存储库中的定义已更改但仍然需要下载这些相同的文件。为了达到这个目的,bazel 在 repository cache 中缓存所有从下载的文件,默认位置为~/.cache/bazel/bazel$USER/cache/repos/v1/。该路径可以通过 --repository_cache 更改。这个缓存是在所有工作区和已安装版本的 bazel 中共享的。 如果Bazel 确信其具有正确文件的副本,即下载文件所请求的SHA256 总合和缓存中的一致,则会从缓存中获取一个条目。从安全的角度看,为每个外部文件指定hash值不仅是个好主意,还有助于避免不必想要的下载。

每次命中高速缓存是,都会更新高速缓存中文件的修改时间。这样可以比较轻松的判断缓存中文件最后的使用时间,手动清楚缓存可以用到。缓存永远不会自动清楚,因为它可能包含上游不再可用的文件副本。

分发文件目录(Distribution files directories)

Distribution files directories 是Bazel另一个避免不必要下载的机制。Bazel 在缓存库之前先搜索 Distribution directories。主要的区别在于 Distribution directoey 需要手动准备。

使用 --distdir=/path/to-directory 选项,你可以指定一个额外的可读目录查找需要的文件而不是直接获取。如果文件名等于 URL 基本名,并且文件的哈系值等于下载请求中指定的哈系值,则从目录中获取文件。仅当在 WORKSPACE 声明中指定了文件HASH 时,这才有效。

机制文件名的条件对于正确性而言不是必须的,但能减少每个指定目录的候选文件数量。这样,即使目录增多,Distribution files 的效率依然能够保证。

在隔离的环境中运行 Bazel

为了使 Bazel 的二进制文件尺寸较小,Bazel 的隐式依赖项是在运行时获取的。这些隐式依赖规则和工具链可能并不会在所有地方用到。例如安卓工具并没有绑定并且仅在构建安卓项目时才获取这些工具。

然而这些隐式依赖可能对在隔离的环境(无网络)中运行的 Bazel 造成问题,即使你已经提供了所有的所有 WORKSPACE 中的依赖。为了解决这个问题,你需要在一个在联网的环境上准备一个包含这些依赖的Distribution directory, 然后使用离线的方式将这些依赖转移到隔离的环境中。

构建配置和交叉编译

对所有给定的构建,其行为和结果的输入文件可以分为两类。第一种是在项目的 BUILD 文件中的内在信息:包括构建规则,参数值,还有一系列的和传递依赖。第二种是有用户或者构建工具提供的外部数据或者环境数据:构建目标的架构,编译和链接选项,以及其他的工具链选项。我们称这些完整的环境数据为配置。

在任意一个给定的构建中,配置文件可能不止一个。考虑一个交叉编译,其中你想要构建64位的可执行程序//foo:bin,但是你的工作机器是32位。很咸然,这个构建需要使用合适的能够构建64位可执行程序的工具链,但是构建系统还必须构建各种在构建构建系统本身时会用到的工具,例如从源码构建工具然后使用它。这些构建必须在你的工作机上运行。因此,我们考虑两种配置:

通常来说,有许多库是 build target 和其他 host tools 的先决条件,例如一些机出库,这些库肯定会被编译两次,一次是用于 host configuration,另一次是用于 target configuration。Bazel 负责确保两个遍体都已构建,并且将派生文件分开存放以避免干扰;一般来说这些 target 能同时构建,只要它们之间是相互独立的。如果您看到进度消息指示给定目标正在被二次构建,则有可能是这些原因。

Bazel 使用一种或多种方式选择 host configuration,基于 --distinct_host_configuration 选项。这个布尔选项有些微妙,设置可能提高或者降低构建速度。

--distinct_host_configuration=false
我们不建议该配置,如果你经常更改请求配置,例如在 -c opt 和 -c dbg,或者在 simple- 和 cross 编译之间切换。通常每次切换都会重新构建大部分代码。

当你将该选项置为false,host configuration 和 target configuraion 是相同的。所有在构建时会用到的工具都会作为相同的目标程序用同样地方法构建。者意味着没有库会被构建两次。

然而,这些意味着任何对 target configuration 的更改也会影响到 host configuration,导致所有的工具都要被重建,任何依赖与这些工具的输出也要被重建。例如,仅更改一个构建选项,可能构建选项可能导致所有工具被重新链接,然后所有使用道德动作都会重建,导致一个非常大的重建。当然,如果你的host 架构和目标二进制不兼容,构建将无法进行。

--distinct_host_configuration=true
如果这个选项为 true,将不会使用相同的 host 和 target 配置文件,而是完全不同的 host configuration。host configuration 是由 target configuration 派生的,如下:

host target
--host_crosstool_top --crosstool_top
--host_cpu --cpu
--host_javabase --javabase
--host_java_toolchain --java_toolchain
-c opt -c opt
--copt=-g0 --copt=-g0
--strip=always
...

从 target configuration 中选择一个 host configuration 由很多原因 。有些原因比较难以理解,但有两点值得指出。
第一,通过使用 stripped,优化二进制,减少了链接和执行工具,磁盘使用和网络io所消耗的时间。
第二,通过去掉 host configuration 和 target configuration 的耦合,避免由于细小更改导致举它的重建工作。

当然,这个选项可能是个障碍,尤其是对那些很少有代码改动,尤其是 java,某些依赖必须构建两次的应用不会有益处

正确的增量构建

Bazel 的一个主要目标是确保正确的增量构建。以前的构建工具,尤其是基于 make 的,对于增量构建的实现做了一些不合理的假设
首先,文件的时间戳单调性增加,虽然这是很典型的情况,但是很容易违反这一假设。同步更早本本的文件将导致时间减小,make 构建系统将不会重建。

更一般的, Make 能检测到文件更改,但不能检测到命令更改。如果你修改了传递给编译器的参数,Make 也不会重新编译,你必须运行 make clean 来手动清楚以前的构建。

同样地,在该子进程开始写入输出文件之后。make 不能有效的中止自进程的错误。当当前子进程构建失败,make 仍然认为该输出文件是有效的。如果杀掉make进程,也有可能导致相同的问题。

Bazel 避免以上和其他假设。Bazel 维护了一个以前完成工作的数据库,只有在判断输入文件,构建命令,输出文件的时间戳严格一致的情况下才会忽略构建步骤。任何对于输入文件和输出文件的改动或者命令自身,都会重新执行构建动作。

构建一致性和增量构建

构建结果完全由输入文件产出时,我们称其为状态一致。当我们修改了源文件时,此时上一次构建结果和输入文件就不同了,我们称其为不一致,并且这种不一致会持续到下一次构建的成功运行。我们描述这种状态为不稳定的不一致,因为这只是临时的,并且可以通过运行构建恢复其一致性。

还有另外一种有害不一致:稳定不一致。如果构建达到一种稳定的不一致状态,则反复调用构建工具不会恢复一致性:这种构建就像卡住了一样。并且输出也是不正确的。稳定的不一致状态是 Make 用户进行清理的主要原因。构建工具如果以这种方式失败可能即耗时又令人失望。

从概念上来说,最简单的实现一致的方法是扔掉以前的所有构建并重新开始,make build with make clean。这种方法显然很耗时,所以构建工具必须在不损害一致性的基础上进行增量构建。

正确的增量依赖分析很困难,正如前边讲的,许多构建工具在增量构建中避免不一致状态做的很差。相对的,Bazel 提供了以下保证,成功调用未进行编辑的构建工具后,构建状态保持一致。如果你在构建过程中修改了文件,Bazel 无法保证最终的构建一致性,但是Bazel 保证下次构建将会恢复一致状态。

与所有的保证一样,there comes some fine print: 有一些一致的方法可以使 Bazel 进入不稳定的状态。我们不保证故意引发 Bazel 一致性的问题,但我们尽量调查并解决任何合理使用 Bazel 而进入不一致状态的问题。

如果你发现Bazel 不一致的情况,请报告该 bug

沙箱执行

Bazel 使用沙箱来保证动多能够密封且正确的执行。Bazel 在包含构建所需文件的最小的环境中运行 actions。当前 沙箱 能使用 CONFIG_USER_NS 选项开启工作在 linux3.12 以上的linux 中,macos 需要 10.11 及以上。

如果您的系统不支持沙箱,bazel 将提醒你本次构建将不是封闭的并且可能收到主机环境的影响。你可以通过 --ignore_unsupported_sandboxing 标签来禁止。

密封性意味着操作仅使用其声明的输入文件,而不使用文件系统中的其他文件,并且仅产生其声明的额输出文件。(不使用主机环境变量???)

在一些如 kubernetes engine 集群节点,或者 debian 上,出于安全考虑,用户空间默认是被禁止的。这能通过 /proc/sys/kernel/unprivileged_userns_clone。如果为0 的话,用户空间能够通过 sudo sysctl kernel.unprivileged_userns_clone=1 激活。

在一些方面,bazel 沙箱由于系统设置而无法执行。通常会输出类似 namespace-sandbox.c : execvp(atgv[0],argv) no such file or directory。如果这样的话,可以使用 --strategy=Genrule=standalone--spawn_strategy=standalone 停用相关规则。

阶段构建

在 Bazel 中,构建过程被分为三个阶段,作为用户,理解它们之间的区别可以深入了解控制构建的选项。

加载阶段

第一步是加载,在此期间,将加载,解析,评估和缓存初始目标的所有必须 BUILD 文件及其依赖关系的的可传递性闭包。

对于 Bazel 服务启动后的第一次构建,加载阶段由于要从文件系统中获取 BUILD 文件会非常耗时。再随后的构建中,如果 BUILD 文件没有改动,加载将会非常块。

分析阶段

第二个阶段,分析。涉及每个构建规则的语义分析和验证,依赖结构图,决定每一步的构建要做哪些事情。

和构建阶段一样,整个计算也需要几秒钟的时间。但是从一个版本到下一个版本, Bazel 将缓存依赖图,并且重新分析其必须执行的工作,增量构建可以非常快速。

加载和分析阶段非常块,因为Bazel 避免不必要的文件读取阶段。通过这种涉及,Bazel 也是一个良好的分析工具,例如 Bazel 的query 命令,这是在加载阶段之上实现的。

执行阶段

最后阶段是执行。这个阶段确保每一步的输出和输入是一致的。重复运行必要的编译,链接工具等等。这一步是耗费的主要时间,从几秒到几个小时不等,取决于构建工程的大小。

Client/Server 实现

Bazel 系统的实现是以个长期存在的服务器进程。这样就能允许许多批处理无法实现的功能,例如缓存BUILD 文件,依赖图,和构建的其他元数据。这能提高构建速度,允许不同的构建命令例如 build 和 query 使用相同的缓存,查询也非常快速。

当你允许 bazel 的时候,你是在使用 client。client 会基于输出目录找到 server 地址,这取决于工作区的路径,因此,当你在多工作区构建时,你将有多个输出目录并且启动多个Bazel 服务实例。多用户能够在同一个工作区并行工作,因为它们的输出路径是不同的(不同的 userids)。如果客户端不能找到服务实例,他将启动一个。服务将在不活跃的一段时间之后停止,默认3小时,可以通过 --max_idle_secs 在启动时设置。

大多数情况下,用户看不到服务运行的实时,但是记住这一点将会非常有用。例如,如果你在不同的工作空间运行了大量的自动构建脚本,你应该确保不会占用大量的空间服务器,你应该在使用完成后关闭它们,或者使用一个较短的存活周期。

bazel 服务器的名称用 ps x 或者 ps -e f 可以看到。注意 ps 的参数,可能仅仅被识别为 java。(从这里可以看出来,bazel 的cs模型主程序实际上是 一个 cpp/java 的模型,是两个独立的进程,大概率采用 grpc 协议通信)。

运行bazel时,客户端首先检查服务器是否为正确的版本; 如果不是,则服务器停止并启动新服务器。 这样可以确保使用长时间运行的服务器进程不会干扰正确的版本控制。

.bazelrc,Bazel 的配置文件

Bazel 有许多选项参数,一些参数是经常变化的,例如 --subcommands 还有一些是不怎么变化的。为了避免每次构建都写参数,你能够指定配置文件。

.bazelrc 文件位置

Bazel 配置文件在下列地方,按照先后顺序加载,所以后边的可以覆盖前边的选项。控制那个文件被加载是启动选项。

  1. system rc
    /etc/bazel.bazelrc
    如果其他系统需要,你必须自己构建 Bazel 二进制,并且使用 BAZEL_SYSTEM_BAZELRC_PATH 值 在 //src/main/cpp:option_processor.
  2. workspace rc
    $workspace/.bazelrc
  3. home rc
    $HOME/.bazelrc
  4. 用户指定 rc
    --bazelrc=file
    除此之外,bazel 还会查找全局 rc 文件,参见 global bazelrc section

.bazelrc 的句法和语义

和所有 UNIX rc 文件一样,.bazelrc 文件是基于行语法的文本文件。空行和以 # 开始的会被忽略。每行包含一系列的单词,这些单词根据和 borune shell 相同的规则进行标记。

imports

以 import 或者 try-import 开头的行是页数的,使用这个关键字加载其他的 rc 文件。指定的路径是相对于工作区的。 import %workspace%/path/to/bazelrc

二者之间的不同点在于 import 如果找不到文件或者无法读取会失败,而 try不会

导入优先级,按语句顺序来

默认选项

大多数行定义默认值。第一个的那次指定何时应用这些默认值:

这些行可以进行组合,例如

build --test_tmpdir=/tmp/foo --verbose_failures
build --test_tmpdir=/tmp/bar

组合起来就是

build --test_tmpdir=/tmp/foo --verbose_failures --test_tmpdir=/tmp/bar

所以生效的命令是

build --verbose_failures --test_tmpdir=/tmp/bar

选项优先级

--config

除了默认选项外,rc 文件也能用于提供分组。可以通过在命令后添加 :name后缀来完成。默认情况下,这些选项是被忽略的,只有在命令行或者文件中使用 --config=name 时会生效。

其他控制 bazel 行为的文件

.bazelignore

可以在 workspace 根目录下的 .bazelignore 文件中写明不受 bazel 管理的文件。

全局 bazelrc

除了个人的 .bazelrc,bazel 读取全局配置文件在 $workspace/tools/bazel.rc,bazel 二进制附加的.bazel/etc/bazel.bazelrc

你可以使用 --nomaster_bazelrc 来指定忽略全局配置文件。

在脚本中调用Bazel

可以从脚本中调用 Bazel,以执行构建,运行测试或查询依赖关系图。Bazel 旨在支持有效的脚本编写,本节列除了一些需要记住的细节,以使您的脚本更加健壮。

todo

上一篇下一篇

猜你喜欢

热点阅读