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