到底什么是构建系统?

发布: (2025年12月14日 GMT+8 03:58)
9 min read

Source: Hacker News

大局观

在高层次上,构建系统是提供一种 定义执行 一系列从 输入 数据到 输出 数据的 转换 的工具或库,这些转换通过 缓存记忆化 并存放在 对象存储 中。

转换被称为 步骤规则1,它们定义了如何执行一个 任务,该任务可以从零个或多个输入生成零个或多个输出。
规则通常是 缓存单元;即 缓存点 是规则的输出,而 缓存失效 必须在规则的输入上发生。
规则可以依赖先前的输出,形成一个称为 依赖图 的有向图。
形成循环的依赖图被称为 循环依赖,通常被禁止。2

仅被其他规则使用、但对最终用户并不 “有趣” 的输出称为 中间输出

如果一个输出的某个依赖被修改,或者 传递性 地其某个依赖已过时,则该输出被视为 过时陈旧。陈旧的输出会使缓存失效,需要 重新构建。已缓存且未脏的输出是 最新 的。若规则的任何输出过时,则该规则也视为过时。如果规则没有输出,则它始终是过时的。

每一次调用构建工具都称为一次 构建

  • 当缓存为空且所有转换作为 批处理作业 执行时,称为 完整构建清洁构建
  • 若缓存中的所有规则都是最新的,则缓存是 完整 的。
  • 当缓存部分完整但有些输出过时需要重新构建时,称为 增量构建
  • 删除缓存的行为称为 清理

如果所有可能的增量构建得到的结果与完整构建相同,则构建是 正确可靠 的。3
如果每个规则在一次构建中最多只被重新运行一次,并且仅在为可靠性必须时才运行,则构建是 最小化(有时称为 最优)的(参见 Build Systems à la CartePluto)。

为了使构建可靠,所有可能的缓存失效必须被 追踪 作为依赖。

没有缓存的构建系统被称为 任务运行器批处理编译器。需要注意的是,任务运行器即使不支持缓存,通常仍会支持依赖。带缓存的构建系统可以通过只定义零输出的任务来模拟任务运行器,但它们通常并非为此用途设计。4

构建系统示例: makedocker buildrustc
任务运行器示例: just、Shell 脚本、gcc

指定依赖

构建可以是 进程间 的,即任务通常是一次 进程执行,带有输入文件和输出文件;也可以是 进程内 的,即任务通常是一次函数调用,带有参数和返回值。

为了追踪依赖,要么所有输入和输出必须在源码中提前 声明,要么能够从任务的执行中 推断 出它们。

能够追踪规则定义变化的构建系统被称为 自追踪。规则的过去版本称为它的 历史(参见 Build Systems à la Carte)。

从运行时行为中推断依赖的行为称为 追踪
如果被追踪的规则依赖于尚未构建的依赖,构建系统可以选择报错、挂起 任务并在依赖构建完成后 恢复,或 中止 任务并在依赖构建完成后 重新启动(参见同上)。

进程间构建通常会声明其输入输出,而进程内构建则常常推断它们,但这并非定义所固有的。5

进程内构建示例: 电子表格、wild linker 与诸如 Python 的 functools.cache 之类的记忆化库。

应用式与单子式结构

如果所有输入、输出和规则都在事先声明,则构建图是 应用式 的,此时图是 静态已知 的。纯粹的应用式构建系统极少;几乎所有系统都有逃逸通道。

如果并非所有输出事先已知,或规则可以在运行时动态生成其他规则,则构建图是 单子式 的。事先未知的输入称为 动态依赖。动态依赖比完全单子式的构建系统更弱,因为它们能够表达的构建图更少(参见 能力‑可实现性权衡)。6

不需要声明构建规则的构建系统始终是单子式的。

单子式构建系统示例: Shake、ninja 的 dyndeps、Cargo 构建脚本。
应用式构建系统示例: make(在禁止递归 make的前提下)、Bazel(不包括原生规则),以及带记忆化的 map/reduce 库,例如此 Unison 程序

早期截断

如果一个脏规则 R 的输出已过时,重新运行后产生的新输出与旧输出相同,则构建系统有机会避免运行依赖于 R 的后续规则。利用这一机会的行为称为 早期截断

有关早期截断的更多信息,请参见 rustc‑dev‑guide7

重建检测

在不可靠的构建系统中,系统可能无法准确 检测 是否需要重建。这类系统有时提供 强制重新运行 某个目标的方式:保留现有缓存但重新运行单个规则。对于进程间构建系统,这通常涉及 touch 一个文件以将其修改时间设为当前时间。

执行器

构建执行器 负责运行任务并按照尊重所有依赖的顺序调度它们,常使用诸如依赖深度或上一次运行耗时等启发式策略。
它还会检测规则的输入是否被修改,从而将规则标记为过时,这称为 重建检测
在支持此功能的构建系统中,执行器可以重新启动或挂起任务,提供 进度报告,并有时允许 查询 依赖图。
偶尔,执行器会追踪任务使用的输入,以确保它们匹配声明的依赖,或自动将其加入内部依赖图。

进程间构建

在进程间构建的语境中,制品 是规则生成的输出文件。8
源文件 是特定于当前 项目(有时称为 仓库工作区)的输入文件,与在多个项目之间复用的 系统依赖 相对。9


Footnotes

  1. 见原文中的脚注 14。

  2. 见原文中的脚注 5。

  3. 见原文中的脚注 1。

  4. 见原文中的脚注 7。

  5. 见原文中的脚注 8。

  6. 见原文中的脚注 15。

  7. 见原文中的脚注 9。

  8. 见原文中的脚注 6。

  9. 见原文中的脚注 11。

Back to Blog

相关文章

阅读更多 »

包管理器挖掘🪦

封面图片:패키지 매니저 파묘🪦 https://media2.dev.to/dynamic/image/width=1000,height=420,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.amazon...