本页将介绍封闭性、使用封闭 build 的优势,以及识别 build 中的非封闭行为的策略。
概览
在给定相同的输入源代码和产品配置的情况下,封闭式构建系统始终会将 build 与主机系统的更改隔离开来,从而始终返回相同的输出。
为了隔离 build,封闭 build 对本地或远程主机上安装的库和其他软件无关。它们依赖于特定版本的构建工具(例如编译器)和依赖项(例如库)。这使得构建流程变得独立,因为它不依赖于构建环境外部的服务。
封闭的两个重要方面是:
- 隔离:封闭构建系统将工具视为源代码。他们会下载工具的副本并管理其存储空间,并在托管文件树中使用。这会在主机和本地用户(包括已安装的语言版本)之间创建隔离。
- 源代码标识:封闭构建系统会尝试确保输入源的相同性。Git 等代码库可以识别具有唯一哈希代码的代码更改集。封闭构建系统使用此哈希来识别对 build 输入的更改。
优势
封闭构建的主要优势包括:
- 速度:可以缓存操作的输出;除非输入发生变化,否则无需再次运行该操作。
- 并行执行:对于给定的输入和输出,构建系统可以构建所有操作的图表,以计算高效并行执行。构建系统会加载规则并计算操作图和哈希输入,以便在缓存中查找。
- 多个 build:您可以在同一台机器上构建多个封闭 build,每个 build 使用不同的工具和版本。
- 可重现性:封闭 build 非常适合用于问题排查,因为您知道生成 build 的确切条件。
识别非封闭性
如果您正在准备改用 Bazel,那么可以提前提高现有构建的封闭性,这样迁移过程会更加轻松。build 中一些常见的非封闭性来源有:
- 在
.mk
文件中任意处理 - 以不确定的方式创建文件的操作或工具,通常涉及 build ID 或时间戳
- 不同主机之间不同的系统二进制文件(例如
/usr/bin
二进制文件、绝对路径、用于原生 C++ 规则自动配置的系统 C++ 编译器) - 在构建期间向源代码树写入数据。这可以防止将同一个源代码树用于其他目标。第一个 build 会写入源代码树,从而修复目标 A 的源代码树。那么尝试构建目标 B 可能会失败。
对非封闭 build 进行问题排查
从本地执行开始,影响本地缓存命中的问题会显示非封闭操作。
- 确保 null 顺序构建:如果您运行
make
并获得成功的构建,则再次运行构建不应重新构建任何目标。如果每个构建步骤运行两次或在不同的系统上运行两次,比较文件内容的哈希值并获得不同的结果,则构建无法重现。 - 执行步骤以调试来自各种潜在客户端机器的本地缓存命中,以确保捕获任何客户端环境泄露到操作中的情况。
- 在 Docker 容器中执行构建,该容器仅包含已签出的源代码树和明确的主机工具列表。构建中断和错误消息会捕获隐式系统依赖项。
- 使用远程执行规则发现并修复封闭问题。
- 由于构建中的操作可能是有状态的,并且会影响构建或输出,因此请在每个操作级别启用严格的沙盒机制。
- 工作区规则允许开发者向外部工作区添加依赖项,但它们足够丰富,以允许在此过程中进行任意处理。通过向 Bazel 命令添加
--experimental_workspace_rules_log_file=PATH
标志,您可以获取 Bazel 工作区规则中某些可能的非封闭操作的日志。
Bazel 的封闭性
如需详细了解其他项目如何通过 Bazel 使用封闭构建,获得成功,请参阅以下 BazelCon 讲座:
- 使用 Bazel 构建实时系统 (SpaceX)
- Bazel Remote Execution 和 Remote Caching(Uber 和 TwoSigma)
- 通过 Remote Execution 和缓存加快构建速度
- 融合 Bazel:更快的增量构建
- 远程执行与本地执行
- 提高远程缓存的可用性 (IBM)
- 使用 Bazel 打造自动驾驶型汽车 (BMW)
- 使用 Bazel 打造自动驾驶型汽车 + 问答 (GM Cruise)