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