基于工件的构建系统

<ph type="x-smartling-placeholder"></ph> 报告问题 <ph type="x-smartling-placeholder"></ph> 查看来源 每晚 · 7.2 条 · 7.1敬上 · 7.0 · 6.5 · 6.4

本页面介绍基于工件的构建系统及其背后的理念 创建过程。Bazel 是一个基于工件的构建系统。基于任务的构建作业 相较于构建脚本,它们提供的功能过多, 让个别工程师定义自己的任务。

基于工件的构建系统定义少量任务 工程师能够以有限的方式进行配置工程师仍然会告诉系统 要构建的内容,但构建系统会决定如何构建。与 基于任务的构建系统、基于工件的构建系统(如 Bazel) 但它们的内容截然不同更确切地说 不是图灵完备脚本语言中的一组命令式命令 说明如何生成输出,Bazel 中的 buildfile 是一种声明式的 清单,用于描述要构建的一组工件及其依赖项 仅有有限的几个选项会影响它们的构建方式。当工程师运行 bazel 时 它们指定要构建的一组目标(“内容”),以及 Bazel 负责配置、运行和安排编译 步骤(方法)。因为构建系统现在能够完全控制 能够提供更强有力的保证 同时又能保证正确性。

功能视角

可以轻松地将基于工件的构建系统和功能性 传统的命令式编程语言(如 Java、C 和 Python)指定要在 就像基于任务的构建系统允许程序员定义一系列步骤一样, 调用。函数式编程语言(例如,Haskell 和 ML), 结构更像是一系列数学方程式。在 程序员描述要执行的计算, 将执行该计算的时间和具体方式的详细信息留给 。

这对应于在基于工件的构建系统中声明清单的思路 然后让系统决定如何执行构建许多题目 易于使用函数式编程进行表达,但对 因此,该语言通常能够轻松地并行执行 并有力地保证其正确性, 在祈使式语言中是不可能实现的。使用 函数式编程只涉及将一段代码转换为 将数据转换为另一个数据。这也就是 什么是构建系统:整个系统实际上是一个数学函数, 这种模型将源文件(以及编译器等工具)作为输入, 作为输出的二进制文件。因此,基于构建构建 一种围绕函数式编程原则的系统。

了解基于工件的构建系统

Google 的构建系统 Blaze 是第一个基于工件的构建系统。Bazel 是 Blaze 的开源版本。

build 文件(通常命名为 BUILD)在 Bazel 中如下所示:

java_binary(
    name = "MyBinary",
    srcs = ["MyBinary.java"],
    deps = [
        ":mylib",
    ],
)
java_library(
    name = "mylib",
    srcs = ["MyLibrary.java", "MyHelper.java"],
    visibility = ["//java/com/example/myproduct:__subpackages__"],
    deps = [
        "//java/com/example/common",
        "//java/com/example/myproduct/otherlib",
    ],
)

在 Bazel 中,BUILD 文件定义目标,这里的两种目标类型是 java_binaryjava_library。每个目标都对应一个工件 可以由系统创建:二进制目标会生成 库目标生成的库 二进制文件或其他库每个目标都具有以下特点:

  • name:目标在命令行中以及其他被引用的方式 目标
  • srcs:要编译的源文件,用于为目标创建工件
  • deps:必须在此目标之前构建并关联到 它

依赖项可以位于同一软件包中(例如 MyBinary 的 依赖于 :mylib)或同一源代码层次结构中的其他软件包 (例如 mylib//java/com/example/common 的依赖项)。

与基于任务的构建系统一样,您可以使用 Bazel 的命令行来执行构建 工具。如需构建 MyBinary 目标,请运行 bazel build :MyBinary。更新后 在干净的代码库 Bazel 中首次输入该命令:

  1. 解析工作区中的每个 BUILD 文件以创建依赖关系图 不同工件之间。
  2. 使用图表确定 MyBinary 的传递依赖项;那个 即 MyBinary 所依赖的每个目标,以及这些目标所依赖的每个目标 目标所依赖的变量。
  3. 按顺序构建其中的每个依赖项。首先,Bazel 没有其他依赖项,可跟踪哪些依赖项的 每个目标仍然需要进行构建一旦目标的所有 则 Bazel 会开始构建该目标。此过程 都会持续运行,直到 MyBinary 的每个传递依赖项 。
  4. 构建 MyBinary 以生成最终的可执行二进制文件,该文件会链接到 在第 3 步中构建的依赖项。

从根本上说,这里发生的情况 与使用基于任务的构建系统时的情况不同。事实上, 最终结果是相同的二进制文件, 分析多个步骤以找出它们之间的依赖关系,然后运行 执行这些步骤但两者之间有很大的区别。显示第一条 第三步:由于 Bazel 知道每个目标只会生成一个 Java 库, 它只需要运行 Java 编译器即可, 脚本,这样它就知道并行执行这些步骤是安全的。 与构建容器相比, 而这之所以能够实现这一目标,是因为 基于工件的方法让构建系统掌控自己的执行 策略,以便更好地保证并行性。

不过,其优势不仅限于并行处理。接下来, 通过这种方法,当开发者第二次输入 bazel build :MyBinary 而未进行任何更改时,可以发现 Bazel 的退出时间更短 一秒,其中显示目标为最新消息。这是 这要归功于我们之前介绍过的函数式编程范式, Bazel 知道每个目标都只是运行 Java 代码 而且它知道 Java 编译器的输出仅依赖于 只要输入没有变化,输出就可以重复使用。 这项分析在各个层面都适用。如果 MyBinary.java 发生变化,Bazel 知道 重新构建 MyBinary,但重复使用 mylib。如果某个 //java/com/example/common更改,Bazel 知道要重新构建该库 mylibMyBinary,但重复使用 //java/com/example/myproduct/otherlib。 由于 Bazel 了解其在每个步骤中运行的工具的属性, 每次只重新构建最少的工件集, 以免生成过时的 build

从工件(而不是任务)角度来重新构建构建流程的微妙之处 但功能强大通过降低给程序员的灵活性, 系统可以更详细地了解构建过程中每一步的操作。它可以 利用这些知识,通过并行构建来大幅提高构建效率 并重复使用其输出。但这只是第一步 这些并行处理和重复使用的构建块构成了分布式计算和 以及可伸缩性极强的构建系统

其他实用的 Bazel 技巧

基于工件的构建系统从根本上解决了并行处理问题 和重复使用基于任务的构建系统固有的功能。但 以及之前出现过的几个我们尚未解决的问题。Bazel 非常巧妙 解决每个问题的方法,我们应该先讨论一下,然后再继续。

工具即依赖项

我们之前遇到过的一个问题是,构建依赖于 并且跨系统重现 build 可能很困难, 工具版本或位置不同但这个问题变得更加困难 当您项目使用的语言需要根据不同的语言 (例如,Windows 与 Linux)、 每个平台需要的工具集略有不同 同一个作业。

Bazel 可解决这一问题的第一部分 将工具视为依赖项 。工作区中的每个 java_library 都隐式依赖于 Java 编译器,默认为知名的编译器。每当 Bazel 构建 java_library 时,它会进行检查以确保指定的编译器可用 在已知位置运行与任何其他依赖项一样,如果 Java 编译器 并且依赖它的每个工件都会重新构建。

Bazel 解决了第二部分问题,即平台独立性。 build 配置。而非 目标直接取决于工具,而配置类型则取决于:

  • 主机配置:在构建期间运行的构建工具
  • 目标配置:构建您最终请求的二进制文件

扩展构建系统

Bazel 附带了多种常用编程语言的目标, 但工程师始终希望实现更多目标 - 基于任务的部分优势在于 系统可以灵活支持任何类型的构建流程, 在基于工件的构建系统中最好不要放弃。 幸运的是,Bazel 支持通过 添加自定义规则

要在 Bazel 中定义规则,规则作者需要声明规则的输入, (以 BUILD 文件中传递的属性的形式)和修复了 规则生成的一组输出。作者还定义了 将由该规则生成每个操作都会声明其输入和输出, 运行特定的可执行文件,或将特定字符串写入文件,并且 通过输入和输出连接到其他操作。这意味着 是构建系统中最底层的可组合单元,操作可以执行 只要它仅使用其声明的输入和输出,以及 Bazel 负责安排操作,并视情况缓存其结果。

系统无法做到万无一失,因为没有办法阻止操作开发者 引入不确定性流程等操作, 用户的操作但在实践中,这种情况并不常见, 从而大大降低被滥用的可能性 出错的机会。规则支持许多常用语言和工具 可广泛在线提供,大多数项目永远无需定义自己的 规则。即便是那些拥有该角色的查询,也只需在 位于代码库中,这意味着大多数工程师都可以使用 而无需操心实施方式

隔离环境

看起来,在执行其他任务时,它们可能也会遇到同样的问题。 系统,是否仍有可能编写同时写入同一项的操作 文件并最终相互冲突?Bazel 让这些 沙盒不可能发生冲突。在受支持的设备上 每个操作都通过文件系统与其他操作隔离开来 沙盒实际上,每项操作都只能看到 包含其声明的输入及其包含的任何输出的文件系统 生成的内容。Linux 上的 LXC 等系统强制要求采用相同的技术, 使用 Docker。也就是说,任何操作都不可能 另一个原因是,它们无法读取未声明的任何文件, 如果执行操作, 。Bazel 还使用沙盒来限制操作通过 网络。

使外部依赖项具有确定性

但还有一个问题:构建系统通常需要下载 构建自己的依赖项(无论是工具还是库) 直接构建它们。通过 @com_google_common_guava_guava//jar 依赖项,用于下载 JAR 文件 。

依赖当前工作区之外的文件存在风险。这些文件可能 随时更改,可能需要构建系统不断检查 以及它们是否是新鲜的如果远程文件发生更改,但没有相应的更改 也会导致构建不可重现,即 build 某天可能正常工作,而第二天却因未注意到的原因而失败 依赖项更改。最后,外部依赖项会带来巨大的安全机制, 当数据归第三方所有时:如果攻击者能够入侵 第三方服务器,他们可以将依赖项文件替换为 他们自己的设计,并且有可能让他们完全控制您的作品 环境及其输出。

根本问题在于,我们希望构建系统能够 而无需将其签入源代码控制系统更新依赖项 应是自觉地做出选择,但应在集中精力 而不是由单独的工程师管理,或由 系统。这是因为,即使采用“Live at Head”模型,我们仍然希望 具有确定性,这意味着如果您签出上一个提交 您应该会按当时的状态(而非现在)查看依赖项 。

Bazel 和其他一些构建系统可以通过要求使用 适用于整个工作区的清单文件,其中列出每个外部容器的加密哈希 依赖项。哈希值是用来唯一表示 而无需将整个文件签入源代码控制系统。每当新的 外部依赖项从工作区引用时,该依赖项的哈希为 手动或自动添加到清单中。当 Bazel 运行一个 build 时,它会对照预期检查其缓存依赖项的实际哈希值 哈希值,并且仅在哈希值不同时重新下载文件。

如果我们下载的工件的哈希值与 清单中,则除非更新清单中的哈希值,否则构建将失败。这个 但变更必须获得批准并签入 源代码控制将接受新的依赖项。这意味着 总有一条记录更新依赖项的时间, 对工作区源代码进行相应更改时无法更改依赖项。 这也意味着,在签出较旧版本的源代码时, build 使用的依赖项与它当时使用的依赖项相同 更新版本(否则,如果这些依赖项 )。

当然,如果远程服务器不可用或 开始提供损坏的数据,这可能会导致您的所有构建开始失败 如果您没有该依赖项的其他副本,则会创建该依赖项。为了避免这种情况 对于任何重要的项目,我们都建议您镜像所有项目, 依赖于您信任和控制的服务器或服务。否则, 对于您的构建系统 可用性,即使签入的哈希保证其安全性也是如此。