封闭

本页将介绍封闭性、使用封闭 build 的好处,以及用于在 build 中识别非封闭行为的策略。

概览

如果给定的输入源代码和产品配置相同,封闭构建系统通过将 build 与主机系统更改隔离开来始终返回相同的输出。

为了隔离构建,封闭构建对本地或远程主机上安装的库和其他软件不敏感。它们依赖于特定版本的构建工具(如编译器)和依赖项(如库)。这会使构建流程保持独立,因为它不依赖于构建环境外部的服务。

封闭的两个重要方面是:

  • 隔离:封闭的构建系统将工具视为源代码。他们可以下载工具副本、管理自己的存储空间,并在代管式文件树中使用。这样会在主机和本地用户(包括已安装的语言版本)之间创建隔离。
  • 来源身份:封闭的构建系统会尝试确保输入的相同性。代码库(如 Git)使用唯一的哈希代码标识代码更改集。封闭的构建系统使用此哈希来识别对 build 输入的更改。

优势

封闭构建的主要优势包括:

  • 速度:操作的输出可以缓存;因此,除非输入发生变化,否则无需再次运行该操作。
  • 并行执行:对于给定的输入和输出,构建系统可以构造所有操作的图,以计算高效的并行执行。构建系统会加载规则并计算操作图和哈希输入,以便在缓存中查找。
  • 多个 build:您可以在同一台机器上构建多个封闭 build,每个 build 使用不同的工具和版本。
  • 可重现性:封闭 build 非常适合排查问题,因为您知道生成相应 build 的确切条件。

识别非封闭性

准备改用 Bazel 时,如果提前改善现有 build 的封闭性,迁移会更轻松。build 中非封闭性的一些常见来源包括:

  • .mk 文件中进行任意处理
  • 用于不确定性地创建文件的操作或工具,通常涉及 build ID 或时间戳
  • 不同主机不同的系统二进制文件(例如 /usr/bin 二进制文件、绝对路径、用于原生 C++ 规则自动配置的系统 C++ 编译器)
  • 在构建期间向源代码树写入源代码。这可以防止将同一源代码树用于其他目标。第一个 build 向源代码树写入数据,从而修复了目标 A 的源代码树。然后,尝试构建目标 B 可能会失败。

排查非封闭构建问题

从本地执行开始,影响本地缓存命中的问题会显示非封闭操作。

  • 确保依序构建为 null:如果您运行 make 并获得成功的构建,则再次运行构建应该不会重新构建任何目标。如果将每个构建步骤运行两次或在不同系统上运行,比较文件内容的哈希值并获得不同的结果,则构建不可重现。
  • 从各种潜在的客户端机器运行步骤来调试本地缓存命中,以确保您能够捕获任何泄露到操作的客户端环境情况。
  • 在 docker 容器中执行构建,该容器仅包含已检出的源代码树和主机工具的显式列表。build 中断和错误消息会捕获隐式系统依赖项。
  • 使用远程执行规则发现并修复封闭问题。
  • 在“每个操作”级别启用严格的沙盒功能,因为 build 中的操作可能是有状态的操作,并会影响 build 或输出。
  • 工作区规则允许开发者向外部工作区添加依赖项,但规则足够丰富,允许在此过程中进行任意处理。您可以将 --experimental_workspace_rules_log_file=PATH 标志添加到 Bazel 命令,以获取 Bazel 工作区规则中一些可能的非封闭操作的日志。

使用 Bazel 确保封闭性

如需详细了解其他项目如何借助 Bazel 和封闭构建取得成功,请参阅以下 BazelCon 讲座: