构建系统的权衡 ylc3000 2025-11-13 0 浏览 0 点赞 长文 # build system tradeoffs / 构建系统的权衡 **an overview of what builds for complicated projects have to think about** **关于复杂项目的构建需要考虑的问题的概述** *2025-11-02* • [build-systems / 构建系统](https://jyn.dev/tags/build-systems/) --- I am currently employed to work on [the build system for the Rust compiler](https://rustc-dev-guide.rust-lang.org/building/bootstrapping/intro.html) (often called `x.py` or `bootstrap`). As a result, I think about a lot of build system weirdness that most people don't have to. This post aims to give an overview of what builds for complicated projects have to think about, as well as vaguely gesture in the direction of build system ideas that I like. 我目前受雇于 [Rust 编译器的构建系统](https://rustc-dev-guide.rust-lang.org/building/bootstrapping/intro.html)(通常称为 `x.py` 或 `bootstrap`)。因此,我会思考很多大多数人不必接触的构建系统怪癖。本文旨在概述复杂项目的构建需要考虑的问题,并模糊地指出我喜欢的构建系统理念的方向。 This post is generally designed to be accessible to the working programmer, but I have a lot of expert blindness in this area, and sometimes assume that ["of *course* people know what a feldspar is!"](https://xkcd.com/2501/). Apologies in advance if it's hard to follow. 这篇文章通常是为在职程序员设计的,但我在这个领域有很多专家盲点,有时会假设 ["*当然*人们知道长石是什么!"](https://xkcd.com/2501/)。如果难以理解,我提前道歉。 ## build concerns / 构建关注点 What makes a project’s build complicated? 是什么让一个项目的构建变得复杂? ### running generated binaries / 运行生成的可执行文件 The first semi-complicated thing people usually want to do in their build is write an integration test. Here's a rust program which does so: 人们通常想在他们的构建中做的第一件半复杂的事情是编写一个集成测试。下面是一个实现此功能的 Rust 程序: ```rust // tests/integration.rs use std::process::Command; fn assert_success(cmd: Command) { assert!(cmd.status().unwrap().success()); } #[test] fn test_my_program() { assert_success(Command::new("cargo").arg("build")); assert_success(Command::new("target/debug/my-program") .arg("assets/test-input.txt")); } ``` This instructs [cargo](https://doc.rust-lang.org/cargo/) to, when you run `cargo test`, compile `tests/integration.rs` as a standalone program and run it, with `test_my_program` as the entrypoint. 这指示 [cargo](https://doc.rust-lang.org/cargo/),当您运行 `cargo test` 时,将 `tests/integration.rs` 编译为一个独立的程序并运行它,其中 `test_my_program` 作为入口点。 We'll come back to this program several times in this post. For now, notice that we are invoking `cargo build` inside of `cargo test`. 在本文中,我们将多次回到这个程序。现在,请注意我们在 `cargo test` 内部调用了 `cargo build`。 ### correct dependency tracking / 正确的依赖跟踪 I actually forgot this one in the first draft because Cargo solves this so well in the common case [^14]. In many hand-written builds (*cough* `make` *cough*), specifying dependencies by hand is very broken, `-j` for parallelism simply doesn't work, and running `make clean` on errors is common. Needless to say, this is a bad experience. 实际上,我在第一稿中忘记了这一点,因为 Cargo 在常见情况下解决得非常好 [^14]。在许多手写的构建中(*咳* `make` *咳*),手动指定依赖项非常容易出错,用于并行的 `-j` 根本不起作用,并且在出错时运行 `make clean` 是很常见的。不用说,这是一种糟糕的体验。 ### cross-compiling / 交叉编译 The next step up in complexity is to cross-compile code. At this point, we already start to get some idea of how involved things get: 复杂性的下一个层次是交叉编译代码。此时,我们已经开始了解事情会变得多么复杂: ``` $ cargo test --target aarch64-unknown-linux-gnu Compiling my-program v0.1.0 (/home/jyn/src/build-system-overview) error[E0463]: can't find crate for `std` | = note: the `aarch64-unknown-linux-gnu` target may not be installed = help: consider downloading the target with `rustup target add aarch64-unknown-linux-gnu` = help: consider building the standard library from source with `cargo build -Zbuild-std` ``` How hard it is to cross-compile code depends greatly on not just the build system, but the language you're using and the exact platform you're targeting. The particular thing I want to point out is *your standard library has to come from somewhere*. In Rust, it's usually downloaded from the same place as the compiler. In bytecode and interpreted languages, like Java, JavaScript, and Python, there's no concept of cross-compilation because there is only one possible target. In C, you usually don't install the library itself, but only the headers that record the API [^1]. That brings us to our next topic: 交叉编译代码的难度很大程度上不仅取决于构建系统,还取决于您使用的语言和您所针对的确切平台。我想特别指出的一点是,*您的标准库必须来自某个地方*。在 Rust 中,它通常与编译器从同一个地方下载。在字节码和解释型语言(如 Java、JavaScript 和 Python)中,没有交叉编译的概念,因为只有一个可能的目标。在 C 语言中,您通常不安装库本身,而只安装记录 API 的头文件 [^1]。这就引出了我们的下一个主题: #### libc Generally, people refer to one of two things when they say "libc". Either they mean the C standard library, `libc.so`, or the C runtime, `crt1.o`. 通常,当人们说“libc”时,他们指的是两件事之一。他们要么指的是 C 标准库 `libc.so`,要么指的是 C 运行时 `crt1.o`。 Libc matters a lot for two reasons. Firstly, [C is no longer a language](https://faultlore.com/blah/c-isnt-a-language/), so generally the first step to porting *any* language to a new platform is to make sure you have a C [toolchain](https://stackoverflow.com/a/69006179) [^2]. Secondly, because libc is effectively the interface to a platform, [Windows](https://j00ru.vexillium.org/syscalls/nt/64/), [macOS](https://developer.apple.com/library/archive/qa/qa1118/_index.html), and [OpenBSD](https://marc.info/?l=openbsd-tech&m=169841790407370&w=2) have no stable syscall boundary—you are only allowed to talk to the kernel through their stable libraries (libc, and in the case of Windows several others too). Libc 之所以如此重要,有两个原因。首先,[C 不再是一种语言](https://faultlore.com/blah/c-isnt-a-language/),因此通常将*任何*语言移植到新平台的第一步是确保您有一个 C [工具链](https://stackoverflow.com/a/69006179) [^2]。其次,由于 libc 实际上是平台的接口,[Windows](https://j00ru.vexillium.org/syscalls/nt/64/)、[macOS](https://developer.apple.com/library/archive/qa/qa1118/_index.html) 和 [OpenBSD](https://marc.info/?l=openbsd-tech&m=169841790407370&w=2) 没有稳定的系统调用边界——您只能通过它们的稳定库(libc,在 Windows 的情况下还有其他几个库)与内核对话。 To talk about *why* they've done this, we have to talk about: 要讨论他们*为什么*这么做,我们必须谈谈: #### dynamic linking and platform maintainers / 动态链接和平台维护者 [Many](https://clojure.org/reference/vars) [languages](https://en.wikipedia.org/wiki/Dynamic_dispatch) have a concept of "[early binding](https://en.wikipedia.org/wiki/Name_binding)", where all variable and function references are resolved at compile time, and "[late binding](https://en.wikipedia.org/wiki/Late_binding)", where they are resolved at runtime. C has this concept too, but it calls it "linking" instead of "binding". "late binding" is called "dynamic linking"[^3]. References to late-bound variables are resolved by the ["dynamic loader"](https://linux.die.net/man/8/ld.so) at program startup. Further binding can be done at runtime using [`dlopen`](https://man7.org/linux/man-pages/man3/dlopen.3.html) and friends. [许多](https://clojure.org/reference/vars) [语言](https://en.wikipedia.org/wiki/Dynamic_dispatch)都有“[早期绑定](https://en.wikipedia.org/wiki/Name_binding)”的概念,其中所有变量和函数引用都在编译时解析;以及“[后期绑定](https://en.wikipedia.org/wiki/Late_binding)”,其中它们在运行时解析。C 也有这个概念,但它称之为“链接”而不是“绑定”。“后期绑定”被称为“动态链接”[^3]。对后期绑定变量的引用由程序启动时的[“动态加载器”](https://linux.die.net/man/8/ld.so)解析。可以使用[`dlopen`](https://man7.org/linux/man-pages/man3/dlopen.3.html)及其相关函数在运行时进行进一步的绑定。 [Platform maintainers](https://wiki.debian.org/SoftwarePackaging#External_libraries) [really like dynamic linking](https://wiki.debian.org/StaticLinking#Downsides), for the same reason [they dislike vendoring](https://blogs.gentoo.org/mgorny/2021/02/19/the-modern-packagers-security-nightmare/): late-binding allows them to update a library for all applications on a system at once. This matters a lot for security disclosures, where there is a very short timeline between when a vulnerability is patched and announced and when attackers start exploiting it in the wild. [平台维护者](https://wiki.debian.org/SoftwarePackaging#External_libraries) [非常喜欢动态链接](https://wiki.debian.org/StaticLinking#Downsides),原因与他们[不喜欢供应商化](https://blogs.gentoo.org/mgorny/2021/02/19/the-modern-packagers-security-nightmare/)的原因相同:后期绑定允许他们一次性为系统上的所有应用程序更新一个库。这对于安全披露非常重要,因为在漏洞被修补和公布到攻击者开始在野外利用它之间的时间非常短。 Application developers [dislike dynamic linking](https://vagabond.github.io/rants/2013/06/21/z_packagers-dont-know-best) for basically the same reason: it requires them to trust the platform maintainers to do a good job packaging all their dependencies, and it results in their application being deployed in scenarios that [they haven't considered or tested](https://blog.yossarian.net/2021/02/28/Weird-architectures-werent-supported-to-begin-with). For example, installing openssl on Windows is really quite hard. Actually, while I was writing this, a friend overheard me say "dynamically linking openssl" and said "oh god you're giving me nightmares". 应用程序开发人员[不喜欢动态链接](https://vagabond.github.io/rants/2013/06/21/z_packagers-dont-know-best),原因基本相同:这要求他们相信平台维护者能很好地打包他们所有的依赖项,并导致他们的应用程序部署在他们[没有考虑或测试过](https://blog.yossarian.net/2021/02/28/Weird-architectures-werent-supported-to-begin-with)的场景中。例如,在 Windows 上安装 openssl 真的很难。实际上,在我写这篇文章的时候,一个朋友无意中听到我说“动态链接 openssl”,然后说“天哪,你让我做噩梦了”。 Perhaps a good way to think about dynamically linking as commonly used is *a mechanism for devendoring libraries in a compiled program*. Dynamic linking has other use cases, but they are comparatively rare. 也许,将动态链接(通常使用的方式)视为*在编译程序中去供应商化库的一种机制*是一个很好的思考方式。动态链接还有其他用例,但相对较少。 Whether a build system (or language) makes it easy or hard to dynamically link a program is one of the major things that distinguishes it. More about that later. 一个构建系统(或语言)是让动态链接程序变得容易还是困难,是区分它的主要因素之一。稍后会详细介绍。 ### toolchains / 工具链 Ok. So. Back to cross-compiling. 好的。那么。回到交叉编译。 To cross-compile a program, you need: 要交叉编译一个程序,你需要: * A compiler for that target. If you're using clang or Rust, this is as simple as passing `--target`. If you're using gcc, you need a [whole-ass extra compiler installed](https://gcc.gnu.org/onlinedocs/gcc-15.2.0/gcc.pdf#Invoking%20GCC). 该目标的一个编译器。如果你正在使用 clang 或 Rust,这就像传递 `--target` 一样简单。如果你正在使用 gcc,你需要[安装一个完整的额外编译器](https://gcc.gnu.org/onlinedocs/gcc-15.2.0/gcc.pdf#Invoking%20GCC)。 * A standard library for that target. This is very language-specific, but at a minimum requires a working C toolchain [^4]. 该目标的一个标准库。这非常依赖于语言,但至少需要一个可工作的 C 工具链 [^4]。 * A linker for that target. This is usually shipped with the compiler, but I mention it specifically because it's usually the most platform specific part. For example, "not having the macOS linker" is *the* reason that [cross-compiling to macOS is hard](https://github.com/tpoechtrager/osxcross?tab=readme-ov-file#how-it-works). 该目标的一个链接器。这通常与编译器一起提供,但我特别提到它,因为它通常是平台最特定的部分。例如,“没有 macOS 链接器”是[交叉编译到 macOS 很难](https://github.com/tpoechtrager/osxcross?tab=readme-ov-file#how-it-works)的*根本*原因。 Where does your toolchain come from? ... ... ... ... It turns out [this is a hard problem](https://rustc-dev-guide.rust-lang.org/building/bootstrapping/what-bootstrapping-does.html#stages-of-bootstrapping). Most build systems sidestep it by "not worrying about it"; basically any Makefile you find is horribly broken if you update your compiler without running `make clean` afterwards. Cargo is a lot smarter—it caches output in `target/.rustc_info.json`, and rebuilds if `rustc --version --verbose` changes. "How do you deal with toolchain invalidations" is another important things that distinguishes a build system, as we'll see later. 你的工具链从哪里来? ... ... ... ... 事实证明[这是一个难题](https://rustc-dev-guide.rust-lang.org/building/bootstrapping/what-bootstrapping-does.html#stages-of-bootstrapping)。大多数构建系统通过“不担心它”来回避它;基本上,你找到的任何 Makefile 如果在更新编译器后不运行 `make clean` 就会严重损坏。Cargo 要聪明得多——它将输出缓存到 `target/.rustc_info.json` 中,如果 `rustc --version --verbose` 发生变化,它会重新构建。“你如何处理工具链失效”是区分构建系统的另一个重要因素,我们稍后会看到。 ### environments / 环境 toolchains are a special case of a more general problem: your build depends on your *whole build environment*, not just the files passed as inputs to your compiler. That means, for instance, that people can—and often do—download things off the internet, [embed previous build artifacts in later ones](https://rustc-dev-guide.rust-lang.org/rustdoc.html#multiple-runs-same-output-directory), and [run entire nested compiler invocations](https://docs.rs/openssl/latest/openssl/#vendored). 工具链是一个更普遍问题的特例:您的构建依赖于您的*整个构建环境*,而不仅仅是作为输入传递给编译器的文件。这意味着,例如,人们可以——而且经常——从互联网上下载东西,[将以前的构建工件嵌入到后续的工件中](https://rustc-dev-guide.rust-lang.org/rustdoc.html#multiple-runs-same-output-directory),以及[运行整个嵌套的编译器调用](https://docs.rs/openssl/latest/openssl/#vendored)。 ### reproducible builds / 可重现构建 Once we get towards these higher levels of complexity, people want to start doing quite complicated things with caching. In order for caching to be sound, we need the same invocation of the compiler to emit the same output every time, which is called a **reproducible build**. This is much harder than it sounds! There are many things programs do that cause non-determinism that programmers often don’t think about (for example, iterating a hashmap or a directory listing). 一旦我们达到这些更高的复杂性水平,人们就开始希望使用缓存来做一些相当复杂的事情。为了使缓存有效,我们需要每次调用编译器时都发出相同的输出,这被称为**可重现构建**。这比听起来要困难得多!程序做的很多事情都会导致非确定性,而程序员通常不会想到这些(例如,迭代哈希映射或目录列表)。 At the very highest end, people want to conduct builds across multiple machines, and combine those artifacts. At this point, we can’t even allow reading absolute paths, since those will be different between machines. The common tool for this is a compiler flag called `--remap-path-prefix`, and allows the build system to map an absolute path to a relative one. `--remap-path-prefix` is also how rustc is able to print the sources of the standard library when emitting diagnostics, even when running on a different machine than where it was built. 在最高端,人们希望跨多台机器进行构建,并组合这些工件。在这一点上,我们甚至不能允许读取绝对路径,因为这些路径在不同机器之间是不同的。这方面的常用工具是一个名为 `--remap-path-prefix` 的编译器标志,它允许构建系统将绝对路径映射为相对路径。`--remap-path-prefix` 也是 rustc 能够在发出诊断信息时打印标准库源代码的原因,即使它是在与构建它的机器不同的机器上运行。 ## tradeoffs / 权衡 At this point, we have enough information to start talking about the space of tradeoffs for a build system. 在这一点上,我们有足够的信息开始讨论构建系统的权衡空间。 ### configuration language / 配置语言 > Putting your config in a YAML file does not make it declarative! Limiting yourself to a Turing-incomplete language does not automatically make your code easier to read! > — [jyn](https://tech.lgbt/@jyn/112617315565817787) > 将您的配置放在 YAML 文件中并不会使其成为声明式的!将自己限制在图灵不完备的语言中并不会自动使您的代码更易于阅读! > — [jyn](https://tech.lgbt/@jyn/112617315565817787) The most common unforced error I see build systems making is forcing the build configuration to be written in a custom language[^6]. There are basically two reasons they do this: 我看到构建系统犯下的最常见的非强制性错误是强制将构建配置写入自定义语言[^6]。他们这样做基本上有两个原因: * Programmers aren't used to treating build system code as code. This is a culture issue that's hard to change, but it's worthwhile to try anyway. 程序员不习惯将构建系统代码视为代码。这是一个难以改变的文化问题,但无论如何都值得尝试。 * There is some idea of making builds "declarative". (In fact, observant readers may observe that this corresponds to the idea of ["constrained languages"](https://jyn.dev/constrained-languages-are-easier-to-optimize/) I talk about in an earlier post.) This is not by itself a bad idea! The problem is it doesn't give them the properties you might want. For example, one property you might want is "another tool can reimplement the build algorithm". Unfortunately this quickly becomes [infeasible for complicated algorithms](https://docs.jade.fyi/gnu/make.html#Features-of-GNU-make). Another you might want is "what will rebuild next time a build occurs?". You can't get this from the configuration without—again—reimplementing the algorithm. 有一种让构建“声明式”的想法。(事实上,细心的读者可能会发现这与我在之前一篇文章中谈到的[“受限语言”](https://jyn.dev/constrained-languages-are-easier-to-optimize/)的想法相对应。)这本身不是一个坏主意!问题在于它并不能给你你可能想要的属性。例如,你可能想要的一个属性是“另一个工具可以重新实现构建算法”。不幸的是,对于复杂的算法来说,这很快就变得[不可行](https://docs.jade.fyi/gnu/make.html#Features-of-GNU-make)。你可能想要的另一个属性是“下次构建发生时会重建什么?”。你无法从配置中获得这个信息,除非——再次——重新实现算法。 Right. So, given that making a build "declarative" is a lie, you may as well give programmers a real language. Some common choices are: 好的。所以,既然让构建“声明式”是一个谎言,你不妨给程序员一种真正的语言。一些常见的选择是: * [Starlark](https://starlark-lang.org/) [^7] * [Groovy](https://groovy-lang.org/) * “the same language that the build system was written in” (examples: [Clojure](https://boot-clj.github.io/), [Zig](https://ziglang.org/learn/build-system/), [JavaScript](https://gruntjs.com/sample-gruntfile)) “与构建系统编写的语言相同”(例如:[Clojure](https://boot-clj.github.io/)、[Zig](https://ziglang.org/learn/build-system/)、[JavaScript](https://gruntjs.com/sample-gruntfile)) ### reflection / 反射 "But wait, jyn!", you may say. "Surely you aren't suggesting a build system where you have to run a whole program every time you figure out what to rebuild??" “但是等等,jyn!”,你可能会说。“你肯定不是在建议一个每次都要运行整个程序来确定要重建什么的构建系统吧??” I mean ... people are doing it. But just because they're doing it doesn't mean it's a good idea, so let's look at the alternative, which is to *serialize your build graph*. This is easier to see than explain, so let's look at an example using the [Ninja build system](https://ninja-build.org/) [^8]: 我的意思是……人们正在这么做。但这并不意味着这是一个好主意,所以让我们看看另一种选择,那就是*序列化你的构建图*。这比解释更容易看出来,所以让我们用 [Ninja 构建系统](https://ninja-build.org/) [^8] 来看一个例子: ```ninja rule svg2pdf command = inkscape $in --export-text-to-path --export-pdf=$out description = svg2pdf $in $out build pdfs/variables.pdf: svg2pdf variables.svg ``` Ninjafiles give you the absolute bare minimum necessary to express your build dependencies: You get "rules", which explain *how* to build an output; "build edges", which state *when* to build; and "variables", which say *what* to build[^9]. That's basically it. There's some subtleties about "depfiles" which can be used to dynamically add build edges while running the build rule. Ninjafiles 给你表达构建依赖关系所需的绝对最少的信息:你有“规则”,它解释*如何*构建一个输出;“构建边”,它说明*何时*构建;以及“变量”,它说明*构建什么*[^9]。基本上就是这些。关于“depfiles”有一些微妙之处,可以用来在运行构建规则时动态添加构建边。 Because the features are so minimal, the files are intended to be generated, using a configure script written in one of the languages we talked about earlier. The most common generators are CMake and [GN](https://gn.googlesource.com/gn/#gn), but you can use any language you like because the format is so simple. 因为功能如此之少,这些文件旨在通过我们前面讨论的一种语言编写的配置脚本来生成。最常见的生成器是 CMake 和 [GN](https://gn.googlesource.com/gn/#gn),但你可以使用任何你喜欢的语言,因为格式非常简单。 What's really cool about this is that it's trivial to parse, which means that it's very easy to write your own implementation of ninja if you want. It also means that you *can* get a lot of the properties we discussed before, i.e.: 这真正酷的地方在于它解析起来非常简单,这意味着如果你想的话,编写自己的 ninja 实现非常容易。这也意味着你*可以*获得我们之前讨论过的许多属性,即: * "Show me all commands that are run on a full build" (`ninja -t commands`) “向我展示在完整构建中运行的所有命令”(`ninja -t commands`) * "Show me all commands that will be run the next time an incremental build is run" (`ninja -n -d explain`) “向我展示下一次增量构建时将运行的所有命令” (`ninja -n -d explain`) * "If this *particular* source file is changed, what will need to be rebuilt?" (`ninja -t query variables.svg`) “如果这个*特定*的源文件被更改,需要重新构建什么?” (`ninja -t query variables.svg`) It turns out these properties are very useful. 事实证明,这些特性非常有用。 > "jyn, you're taking too long to get to the point!" look I’m getting there, I promise. > “jyn,你讲到重点太慢了!”看,我正在讲,我保证。 The main downsides to this approach is that it has to be *possible* to serialize your build graph. In one sense, I see this as good, actually, because you have to think through everything your build does ahead of time. But on the other hand, if you have things like nested ninja invocations, or like our `cargo test` -> `cargo build` example [from earlier](#running-generated-binaries) [^10], all the tools to query the build graph don't include the information you expect. 这种方法的主要缺点是,它必须*能够*序列化你的构建图。从某种意义上说,我认为这实际上是件好事,因为你必须提前考虑好你的构建所做的一切。但另一方面,如果你有像嵌套的 ninja 调用,或者像我们[之前](#running-generated-binaries)的 `cargo test` -> `cargo build` 示例 [^10],所有查询构建图的工具都不包含你期望的信息。 ### file watching / 文件监视 Re-stat'ing all files in a source directory is expensive. It would be much nicer if we could have a pull model instead of a push model, where the build tool gets notified of file changes and rebuilds exactly the necessary files. There are some tools with native integration for this, like [Tup](https://gittup.org/tup/manual.html#:~:text=monitor), [Ekam](https://github.com/capnproto/ekam/), [jj](https://jj-vcs.github.io/jj/latest/config/#filesystem-monitor), and [Buck2](https://engineering.fb.com/2023/04/06/open-source/buck2-open-source-large-scale-build-system/#:~:text=integrate%20with%20virtual%20file), but generally it's pretty rare. 重新统计源目录中的所有文件是昂贵的。如果我们能有一种拉取模型而不是推送模型,那会好得多,构建工具会收到文件更改的通知,并精确地重建必要的文件。有一些工具对此有原生集成,比如 [Tup](https://gittup.org/tup/manual.html#:~:text=monitor)、[Ekam](https://github.com/capnproto/ekam/)、[jj](https://jj-vcs.github.io/jj/latest/config/#filesystem-monitor) 和 [Buck2](https://engineering.fb.com/2023/04/06/open-source/buck2-open-source-large-scale-build-system/#:~:text=integrate%20with%20virtual%20file),但总的来说,这非常罕见。 That's ok if we have reflection, though! We can write our own file monitoring tool, ask the build system which files need to rebuilt for the changed inputs, and then tell it to rebuild only those files. That prevents it from having to recursively stat all files in the graph. 不过,如果我们有反射,那就没关系了!我们可以编写自己的文件监视工具,询问构建系统哪些文件需要为已更改的输入重新构建,然后告诉它只重新构建那些文件。这样可以防止它必须递归地统计图中的所有文件。 See [Tup's paper](https://gittup.org/tup/build_system_rules_and_algorithms.pdf) for more information about the big idea here. 有关这里的大致想法,请参阅 [Tup 的论文](https://gittup.org/tup/build_system_rules_and_algorithms.pdf)。 ### dependency tracking / 依赖跟踪 Ok, let's assume we have some build system that uses some programming language to generate a build graph, and it rebuilds exactly the necessary outputs on changes to our inputs. What exactly are our inputs? 好吧,让我们假设我们有一个构建系统,它使用某种编程语言来生成一个构建图,并且它会在我们的输入发生变化时精确地重新构建必要的输出。我们的输入到底是什么? There are basically four major approaches to dependency tracking in the build space. 在构建领域,依赖跟踪基本上有四种主要方法。 #### "not my problem, lol" / “关我屁事,哈哈” This kind of build system externalizes all concerns out to you, the programmer. When I say "externalizes all concerns", I mean that you are required to write in all your dependencies by hand, and the tool doesn't help you get them right. Some examples: 这种构建系统将所有问题都外化给你,也就是程序员。当我说“外化所有问题”时,我的意思是,你被要求手动写入所有依赖项,而工具不会帮助你确保它们是正确的。一些例子: * `make` * Github Actions [cache actions](https://github.com/actions/cache) (and in general, most CI caching I'm aware of requires you to manually write out the files your cache depends on) Github Actions [缓存操作](https://github.com/actions/cache)(总的来说,我所知道的大多数 CI 缓存都要求你手动写出缓存所依赖的文件) * Ansible playbooks * Basically most build systems, this is extremely common 基本上大多数构建系统,这非常普遍 A common problem with this category of build system is that people forget to mark *the build rule itself* as an input to the graph, resulting in dead artifacts left laying around, and as a result, [unsound](https://jacko.io/safety_and_soundness.html) builds. 这类构建系统的一个常见问题是,人们忘记将*构建规则本身*标记为图的输入,导致死工件到处乱放,结果导致[不健全](https://jacko.io/safety_and_soundness.html)的构建。 In my humble opinion, this kind of tool is only useful as a serialization layer for a build graph, or if you have no other choice. Here's a nickel, kid, [get yourself a better build system](https://www.microsoft.com/en-us/research/wp-content/uploads/2016/03/hadrian.pdf). 在我看来,这种工具只适合用作构建图的序列化层,或者在你别无选择的情况下使用。给你五分钱,孩子,[给自己买个更好的构建系统](https://www.microsoft.com/en-us/research/wp-content/uploads/2016/03/hadrian.pdf)。 ##### compiler support / 编译器支持 Sometimes build systems (CMake, Cargo, maybe others I don't know) do a little better and use the compiler's built-in support for dependency tracking (e.g. [`gcc -M`](https://gcc.gnu.org/onlinedocs/gcc/Preprocessor-Options.html#index-M) or [`rustc --emit=dep-info`](https://doc.rust-lang.org/rustc/command-line-arguments.html#--emit-specifies-the-types-of-output-files-to-generate)), and automatically add dependencies on the build rules themselves. This is a lot better than nothing, and much more reliable than tracking dependencies by hand. But it still fundamentally trusts the compiler to be correct, and doesn't track environment dependencies. 有时构建系统(CMake、Cargo,也许还有我不知道的其他系统)会做得更好一些,利用编译器内置的依赖跟踪支持(例如 [`gcc -M`](https://gcc.gnu.org/onlinedocs/gcc/Preprocessor-Options.html#index-M) 或 [`rustc --emit=dep-info`](https://doc.rust-lang.org/rustc/command-line-arguments.html#--emit-specifies-the-types-of-output-files-to-generate)),并自动添加对构建规则本身的依赖。这比什么都不做要好得多,也比手动跟踪依赖关系可靠得多。但它仍然从根本上信任编译器的正确性,并且不跟踪环境依赖关系。 #### ephemeral state / 短暂状态 This kind of build system always does a full build, and lets you modify the environment in arbitrary ways as you do so. This is simple, always correct, and expensive. Some examples: 这种构建系统总是执行完整构建,并允许您在构建过程中以任意方式修改环境。这很简单,总是正确的,而且代价高昂。一些例子: * `make clean && make` (kinda—assuming that `make clean` is reliable, which it often isn't.) `make clean && make` (有点——假设 `make clean` 是可靠的,但它通常不是。) * Github Actions (`step:` rules) * Docker containers (time between startup and shutdown) Docker 容器(启动和关闭之间的时间) * Initramfs (time between initial load and chroot into the full system) Initramfs(初始加载到 chroot 到完整系统之间的时间) * Systemd service startup rules * Most compiler invocations (e.g. `cc file.c -o file`) 大多数编译器调用(例如 `cc file.c -o file`) These are ok if you can afford them. But they are expensive! Most people using Github Actions are only doing so because GHA is a hyperscaler giving away free CI time like there's no tomorrow. I suspect we would see far less wasted CPU-hours if people had to consider the actual costs of using them. 如果你负担得起,这些都没问题。但它们很昂贵!大多数使用 Github Actions 的人这样做只是因为 GHA 是一个超大规模提供商,像不要钱一样提供免费的 CI 时间。我怀疑如果人们必须考虑使用它们的实际成本,我们将会看到更少的 CPU 小时被浪费。 #### hermetic builds / 封闭式构建 This is the kind of phrase that's well-known to people who work on build systems and basically unheard of outside it, alongside "monadic builds". "hermetic" means that *the only things in your environment are those you have explicitly put there*. This sometimes called "sandboxing", although that has unfortunate connotations about security that don't always apply here. Some examples of this: 这个词对于从事构建系统的人来说很熟悉,但在圈外基本上闻所未闻,就像“单体构建”一样。“封闭”意味着*环境中唯一的东西就是你明确放进去的那些*。这有时被称为“沙盒”,尽管这带有不幸的安全含义,在这里并不总是适用。这方面的一些例子: * Dockerfiles (more generally, OCI images) Dockerfiles(更一般地,OCI 镜像) * nix * Bazel / Buck2 * ["Bazel Remote Execution Protocol"](https://github.com/bazelbuild/remote-apis) (not actually tied to Bazel), which lets you run an arbitrary set of build commands on a remote worker [“Bazel 远程执行协议”](https://github.com/bazelbuild/remote-apis)(实际上与 Bazel 无关),它允许你在远程工作机上运行任意一组构建命令 This has a lot of benefits! It statically guarantees that you *cannot* forget any of your inputs; it is 100% reliable, assuming no issues with the network or with the implementing tool 🙃; and it gives you very very granular insight into what your dependencies actually are. Some things you can *do* with a hermetic build system: 这有很多好处!它静态地保证你*不可能*忘记任何输入;它 100% 可靠,假设网络或实现工具没有问题 🙃;它让你对你的依赖关系有非常非常精细的了解。你可以用一个封闭的构建系统*做*的一些事情: * On a change to some source code, rerun only the affected tests. You know statically which those are, because the build tool forced you to write out the dependency edges. 当某些源代码发生更改时,只重新运行受影响的测试。你知道静态地哪些测试受影响,因为构建工具强制你写出了依赖关系。 * Remote caching. If you have the same environment everywhere, you can upload your cache to the cloud, and download and reuse it again on another machine. You can do this in CI—but you can also do it locally! The time for a """full build""" can be almost instantaneous because when a new engineer gets onboarded they can immediately reuse everyone else's build cache. 远程缓存。如果你在任何地方都有相同的环境,你可以将你的缓存上传到云端,并在另一台机器上下载并重用它。你可以在 CI 中这样做——但你也可以在本地这样做!一个“完整构建”的时间几乎可以瞬间完成,因为当一个新工程师入职时,他们可以立即重用其他所有人的构建缓存。 The main downside is that you have to actually specify all those dependencies (if you don't, you get a hard error instead of an unsound build graph, which is the main difference between hermetic systems and "not my problem"). Bazel and Buck2 give you starlark, so you have a ~real[^10] language in which to do it, but it's still a ton of work. Both have an enormous "prelude" module that just defines where you get a compiler toolchain from[^12]. 主要缺点是,你必须实际指定所有这些依赖项(如果你不指定,你会得到一个硬性错误,而不是一个不健全的构建图,这是封闭式系统和“不是我的问题”之间的主要区别)。Bazel 和 Buck2 给了你 starlark,所以你有一种~真正的[^10]语言来做这件事,但这仍然是大量的工作。两者都有一个巨大的“前奏”模块,它只定义了你从哪里获得一个编译器工具链[^12]。 Nix can be thought of as taking this "prelude" idea all the way, by expanding the "prelude" (nixpkgs) to "everything that's ever been packaged for NixOS". When you write `import <nixpkgs>`, your nix build is logically in the same build graph as the nixpkgs monorepo; it just happens to have an enormous remote cache already pre-built. Nix 可以被认为是将这个“前奏”思想发挥到了极致,它将“前奏”(nixpkgs)扩展为“为 NixOS 打包过的所有东西”。当你写 `import <nixpkgs>` 时,你的 nix 构建在逻辑上与 nixpkgs 单体仓库处于同一个构建图中;只是它碰巧已经预先构建了一个巨大的远程缓存。 > Bazel and Buck2 don’t have anything like nixpkgs, which is the main reason that using them requires a full time dedicated build engineer: that engineer has to keep writing build rules from scratch any time you add an external dependency. They also have to package any language toolchains that aren’t in the prelude. > Bazel 和 Buck2 没有任何类似 nixpkgs 的东西,这是使用它们需要一个全职专职构建工程师的主要原因:那个工程师每次添加外部依赖时都必须从头开始编写构建规则。他们还必须打包任何不在前奏中的语言工具链。 Nix has one more interesting property, which is that all its packages compose. You can install two different versions of the same package and that's fine because they use different [store](https://nix.dev/manual/nix/2.24/store/) paths. They fit together like lesbians' fingers interlock. Nix 还有一个有趣的特性,那就是它的所有包都是可组合的。你可以安装同一个包的两个不同版本,这没问题,因为它们使用不同的[存储](https://nix.dev/manual/nix/2.24/store/)路径。它们就像女同性恋者的手指一样紧密地交织在一起。 Compare this to docker, which does *not* compose[^11]. In docker, there is no way to say "Inherit the build environment from multiple different source images". The closest you can get is a "multi-stage build", where you explicitly copy over individual files from an earlier image to a later image. It can't blindly copy over all the files because some of them might want to end up at the same path, and touching fingers would be gay. 与此相比,docker 是*不可*组合的[^11]。在 docker 中,没有办法说“从多个不同的源镜像继承构建环境”。你所能做的最接近的事情是“多阶段构建”,即你明确地将单个文件从一个较早的镜像复制到一个较晚的镜像。它不能盲目地复制所有文件,因为其中一些文件可能想最终放在同一个路径上,而触摸手指会是同性恋行为。 #### tracing / 追踪 The last kind I'm aware of, and the rarest I've seen, is *tracing* build systems. These have the same goal as hermetic build systems: they still want 100% of your dependencies to be specified. But they go about it in a different way. Rather than sandboxing your code and only allowing access to the dependencies you specify, they *instrument* your code, tracing its file accesses, and record the dependencies of each build step. Some examples: 我所知道的最后一种,也是我见过的最稀有的一种,是*追踪*构建系统。它们的目标与封闭式构建系统相同:它们仍然希望 100% 的依赖项都被指定。但它们的实现方式不同。它们不是沙盒化你的代码,只允许访问你指定的依赖项,而是*检测*你的代码,追踪它的文件访问,并记录每个构建步骤的依赖项。一些例子: * [Tup](https://gittup.org/tup/ex_a_first_tupfile.html#:~:text=a%20program%20grows) * [Bear](https://github.com/rizsotto/Bear) * [Riker](https://github.com/curtsinger-lab/riker) * [Ekam](https://github.com/capnproto/ekam/) * [The rust compiler, actually](https://rustc-dev-guide.rust-lang.org/query.html) [实际上是 rust 编译器](https://rustc-dev-guide.rust-lang.org/query.html) * "A build system with orthogonal persistence" ([previously](https://jyn.dev/complected-and-orthogonal-persistence/#how-far-can-we-take-this); [previously](https://jade.fyi/blog/the-postmodern-build-system/#make-an-existing-architecture-and-os-deterministic); [previously](https://nlnet.nl/project/Ripple/)) “具有正交持久性的构建系统” ([之前](https://jyn.dev/complected-and-orthogonal-persistence/#how-far-can-we-take-this); [之前](https://jade.fyi/blog/the-postmodern-build-system/#make-an-existing-architecture-and-os-deterministic); [之前](https://nlnet.nl/project/Ripple/)) The advantage of these is that you get all the benefits of a hermetic build system without any of the cost of having to write out your dependencies. 这些方法的优点是,你可以获得封闭式构建系统的所有好处,而无需付出写出依赖关系的成本。 The first main disadvantage is that they require the kernel to support syscall tracing, which essentially means they only work on Linux. I have Ideas™ for how to get this working on macOS without disabling SIP, but they're still incomplete and not fully general; I may write a follow-up post about that. I don't yet have ideas for how this could work on Windows, but [it seems possible](https://stackoverflow.com/questions/864839/monitoring-certain-system-calls-done-by-a-process-in-windows). 第一个主要缺点是它们要求内核支持系统调用跟踪,这基本上意味着它们只能在 Linux 上工作。我有关于如何在不禁用 SIP 的情况下在 macOS 上实现这一点的想法™,但它们仍然不完整且不完全通用;我可能会就此写一篇后续文章。我还没有关于这如何在 Windows 上工作的想法,但[这似乎是可能的](https://stackoverflow.com/questions/864839/monitoring-certain-system-calls-done-by-a-process-in-windows)。 The second main disadvantage is that not knowing the graph up front causes many issues for the build system. In particular: 第二个主要缺点是,预先不知道图会导致构建系统出现许多问题。特别是: * If you change the graph, it doesn't find out until the next time it reruns a build. This can lead to degenerate cases where the same rule has to be run multiple times until it doesn't access any new inputs. 如果你改变了图,它直到下一次重新运行构建时才会发现。这可能导致退化的情况,即同一个规则必须多次运行,直到它不再访问任何新的输入。 * If you don't cache the graph, you have that problem on *every edge in the graph*. [This is the problem Ekam has](https://github.com/capnproto/ekam/issues/63), and makes it very slow to run full builds. Its solution is to run in "watch" mode, where it caches the graph in-memory instead of on-disk. 如果你不缓存图,你就会在*图的每一条边*上遇到这个问题。[这就是 Ekam 的问题所在](https://github.com/capnproto/ekam/issues/63),这使得运行完整构建非常慢。它的解决方案是在“监视”模式下运行,在这种模式下,它将图缓存在内存中而不是磁盘上。 * If you do cache the graph, you can only do so for so long before it becomes prohibitively expensive to do that for all possible executions. For "normal" codebases this isn't a problem, but if you're Google or Facebook, this is actually a practical concern. I think it is still possible to do this with a tracing build system (by having your cache points look a lot more like many Bazel BUILD files than a single top-level Ninja file), but no one has ever tried it at that scale. 如果你确实缓存了图,你也只能这样做一段时间,之后为所有可能的执行都这样做就会变得非常昂贵。对于“正常”的代码库来说,这不是问题,但如果你是谷歌或 Facebook,这实际上是一个实际问题。我认为用一个跟踪构建系统仍然可以做到这一点(通过让你的缓存点看起来更像许多 Bazel BUILD 文件,而不是一个单一的顶级 Ninja 文件),但从来没有人尝试过那么大的规模。 * If the same file can come from many possible places, due to multiple search paths (e.g. a `<system>` include header in C, or any import really in a JVM language), then you have a very rough time specifying what your dependencies actually are. The best ninja can do is say “depend on the whole directory containing that file”, which sucks because it rebuilds *whenever* that directory changes, not just when your new file is added. It’s possible to work around this with a (theoretical) serialization format other than Ninja, but regardless, you’re adding lots of file `stat()`s to your hot path. 如果同一个文件可以来自许多可能的地方,由于多个搜索路径(例如 C 中的 `<system>` 包含头文件,或者 JVM 语言中的任何导入),那么你在指定你的依赖项实际上是什么时会非常困难。ninja 能做的最好的事情是说“依赖包含该文件的整个目录”,这很糟糕,因为它*每当*该目录发生变化时都会重新构建,而不仅仅是当你的新文件被添加时。使用一种(理论上的)不同于 Ninja 的序列化格式来解决这个问题是可能的,但无论如何,你都在你的热路径上添加了大量的 `stat()` 文件。 * The build system does not know which dependencies are direct (specified by you, the owner of the module being compiled) and which are transient (specified by the modules you depend on). This makes error reporting worse, and generally lets you do fewer kinds of queries on the graph. 构建系统不知道哪些依赖是直接的(由你,也就是被编译模块的所有者指定),哪些是传递的(由你所依赖的模块指定)。这使得错误报告更糟,并且通常让你能在图上做的查询种类更少。 * My friend Alexis Hunt, a build system expert, says "there are deeper pathologies down that route of madness". So. That's concerning. 我的朋友 Alexis Hunt,一位构建系统专家,说“那条疯狂的道路上还有更深层次的病态”。所以。这很令人担忧。 I have been convinced that tracing is useful as a tool to *generate* your build graph, but not as a tool actually used when executing it. Compare also [gazelle](https://github.com/bazel-contrib/bazel-gazelle), which is something like that for Bazel, but based on parsing source files rather than tracking syscalls. 我已经被说服,跟踪作为一种*生成*你的构建图的工具是有用的,但不是作为实际执行它时使用的工具。还可以比较一下 [gazelle](https://github.com/bazel-contrib/bazel-gazelle),它对于 Bazel 来说有点像那样,但它是基于解析源文件而不是跟踪系统调用。 Combining paradigms in this way also make it possible to verify your hermetic builds in ways that are hard to do with mere sandboxing. For example, a tracing build system can catch missing dependencies: 以这种方式组合范例也使得以仅靠沙盒难以做到的方式来验证你的封闭式构建成为可能。例如,一个跟踪构建系统可以捕获缺失的依赖项: * emitting untracked *outputs* 发出未跟踪的*输出* * overwriting source files (!), 覆盖源文件(!), * using an input file that was registered for a different rule 使用为不同规则注册的输入文件 and it can also detect non-reproducible builds: 它还可以检测非可重现的构建: * reading the current time, or absolute path to the current directory 读取当前时间,或当前目录的绝对路径 * iterating all files in a directory (this is non-deterministic) 迭代目录中的所有文件(这是不确定的) * machine and kernel-level sources of randomness. 机器和内核级别的随机源。 ## future work / 未来的工作 There's more to talk about here—how build systems affect the dynamics between upstream maintainers and distro packagers; how .a files are [bad file formats](https://medium.com/@eyal.itkin/the-a-file-is-a-relic-why-static-archives-were-a-bad-idea-all-along-8cd1cf6310c5); how [mtime comparisons are generally bad](https://apenwarr.ca/log/20181113); how configuration options make the tradeoffs much more complicated; how FUSE can let a build system integrate with a VCS to avoid downloading unnecessary files into a shallow checkout; but this post is quite long enough already. 这里还有更多可以谈论的话题——构建系统如何影响上游维护者和发行版打包者之间的动态;.a 文件如何是[糟糕的文件格式](https://medium.com/@eyal.itkin/the-a-file-is-a-relic-why-static-archives-were-a-bad-idea-all-along-8cd1cf6310c5);[mtime 比较通常是不好的](https://apenwarr.ca/log/20181113);配置选项如何使权衡变得更加复杂;FUSE 如何让构建系统与 VCS 集成以避免将不必要的文件下载到浅层检出中;但这篇文章已经足够长了。 ## takeaways / 要点 * Most build systems do not prioritize correctness. 大多数构建系统不优先考虑正确性。 * Prioritizing correctness comes with severe, hard to avoid tradeoffs. 优先考虑正确性会带来严重、难以避免的权衡。 * Tracing build systems show the potential to avoid some of those tradeoffs, but are highly platform specific and come with tradeoffs of their own at large enough scale. Combining a tracing build system with a hermetic build system seems like the best of both worlds. 跟踪构建系统显示出避免其中一些权衡的潜力,但它们高度依赖于平台,并且在足够大的规模下会带来自身的权衡。将跟踪构建系统与封闭式构建系统相结合似乎是两全其美的方法。 * Writing build rules in a "normal" (but constrained) programming language, then serializing them to a build graph, has surprisingly few tradeoffs. I'm not sure why more build systems don't do this. 用一种“正常的”(但受限的)编程语言编写构建规则,然后将它们序列化为构建图,出乎意料地几乎没有什么权衡。我不确定为什么没有更多的构建系统这样做。 --- ## bibliography / 参考文献 * [Alan Dipert, Micha Niskin, Joshua Smith, “Boot: build tooling for Clojure”](https://boot-clj.github.io/) * [Alexis Hunt, Ola Rozenfield, and Adrian Ludwin, “bazelbuild/remote-apis: An API for caching and execution of actions on a remote system.”](https://github.com/bazelbuild/remote-apis) * [Andrew Kelley, “zig cc: a Powerful Drop-In Replacement for GCC/Clang”](https://andrewkelley.me/post/zig-cc-powerful-drop-in-replacement-gcc-clang.html) * [Andrew Thompson, “Packagers don’t know best”](https://vagabond.github.io/rants/2013/06/21/z_packagers-dont-know-best) * [Andrey Mokhov et. al., “Non-recursive Make Considered Harmful”](https://www.microsoft.com/en-us/research/wp-content/uploads/2016/03/hadrian.pdf) * [apenwarr, “mtime comparison considered harmful”](https://apenwarr.ca/log/20181113) * [Apple Inc., “Statically linked binaries on Mac OS X”](https://developer.apple.com/library/archive/qa/qa1118/_index.html) * [Aria Desires, “C Isn’t A Language Anymore”](https://faultlore.com/blah/c-isnt-a-language/) * [Charlie Curtsinger and Daniel W. Barowy, “curtsinger-lab/riker: Always-Correct and Fast Incremental Builds from Simple Specifications”](https://github.com/curtsinger-lab/riker) * [Chris Hopman and Neil Mitchell, “Build faster with Buck2: Our open source build system”](https://engineering.fb.com/2023/04/06/open-source/buck2-open-source-large-scale-build-system/) * [Debian, “Software Packaging”](https://wiki.debian.org/SoftwarePackaging) * [Debian, “Static Linking”](https://wiki.debian.org/StaticLinking) * [Dolstra, E., & The CppNix contributors., “Nix Store”](https://nix.dev/manual/nix/2.24/store/) * [Eyal Itkin, “The .a File is a Relic: Why Static Archives Were a Bad Idea All Along”](https://medium.com/@eyal.itkin/the-a-file-is-a-relic-why-static-archives-were-a-bad-idea-all-along-8cd1cf6310c5) * [Felix Klock and Mark Rousskov on behalf of the Rust compiler team, “Announcing Rust 1.52.1”](https://blog.rust-lang.org/2021/05/10/Rust-1.52.1/) * [Free Software Foundation, Inc., “GNU make”](https://docs.jade.fyi/gnu/make.html) * [GitHub, Inc., “actions/cache: Cache dependencies and build outputs in GitHub Actions”](https://github.com/actions/cache) * [Google Inc., “bazel-contrib/bazel-gazelle: a Bazel build file generator for Bazel projects”](https://github.com/bazel-contrib/bazel-gazelle) * [Google LLC, “Jujutsu docs”](https://jj-vcs.github.io/jj/latest/config/) * [Jack Lloyd and Steven Fackler, “rust-openssl”](https://docs.rs/openssl/latest/openssl/) * [Jack O’Connor, “Safety and Soundness in Rust”](https://jacko.io/safety_and_soundness.html) * [Jade Lovelace, “The postmodern build system”](https://jade.fyi/blog/the-postmodern-build-system/) * [Julia Evans, “ninja: a simple way to do builds”](https://jvns.ca/blog/2020/10/26/ninja--a-simple-way-to-do-builds/) * [jyn, “Complected and Orthogonal Persistence”](https://jyn.dev/complected-and-orthogonal-persistence/) * [jyn, “Constrained Languages are Easier to Optimize”](https://jyn.dev/constrained-languages-are-easier-to-optimize/) * [jyn, “i think i have identified what i dislike about ansible”](https://tech.lgbt/@jyn/112617315565817787) * [Kenton Varda, “Ekam Build System”](https://github.com/capnproto/ekam/) * [László Nagy, “rizsotto/Bear: a tool that generates a compilation database for clang tooling”](https://github.com/rizsotto/Bear) * [Laurent Le Brun, “Starlark Programming Language”](https://starlark-lang.org/) * [Mateusz “j00ru” Jurczyk, “Windows X86-64 System Call Table (XP/2003/Vista/7/8/10/11 and Server)”](https://j00ru.vexillium.org/syscalls/nt/64/) * [Michał Górny, “The modern packager’s security nightmare”](https://blogs.gentoo.org/mgorny/2021/02/19/the-modern-packagers-security-nightmare/) * [Mike Shal, “A First Tupfile”](https://gittup.org/tup/ex_a_first_tupfile.html) * [Mike Shal, “Build System Rules and Algorithms”](https://gittup.org/tup/build_system_rules_and_algorithms.pdf) * [Mike Shal, “tup”](https://gittup.org/tup/manual.html) * [Nico Weber, “Ninja, a small build system with a focus on speed”](https://ninja-build.org/) * [NLnet, “Ripple”](https://nlnet.nl/project/Ripple/) * [OpenJS Foundation, “Grunt: The JavaScript Task Runner”](https://gruntjs.com/sample-gruntfile) * [“Preprocessor Options (Using the GNU Compiler Collection (GCC))”](https://gcc.gnu.org/onlinedocs/gcc/Preprocessor-Options.html) * [Randall Munroe, “xkcd: Average Familiarity”](https://xkcd.com/2501/) * [Richard M. Stallman and the GCC Developer Community, “Invoking GCC”](https://gcc.gnu.org/onlinedocs/gcc-15.2.0/gcc.pdf) * [Rich Hickey, “Clojure - Vars and the Global Environment”](https://clojure.org/reference/vars) * [Stack Exchange, “What are the advantages of requiring forward declaration of methods/fields like C/C++ does?”](https://langdev.stackexchange.com/questions/153/what-are-the-advantages-of-requiring-forward-declaration-of-methods-fields-like/) * [Stack Overflow, “Monitoring certain system calls done by a process in Windows”](https://stackoverflow.com/questions/864839/monitoring-certain-system-calls-done-by-a-process-in-windows) * [System Calls Manual, “dlopen(3)”](https://man7.org/linux/man-pages/man3/dlopen.3.html) * [System Manager’s Manual, “ld.so(8)”](https://linux.die.net/man/8/ld.so) * [The Apache Groovy project, “The Apache Groovy™ programming language”](https://groovy-lang.org/) * [The Chromium Authors, “gn”](https://gn.googlesource.com/gn/) * [Theo de Raadt, “Removing syscall(2) from libc and kernel”](https://marc.info/?l=openbsd-tech&m=169841790407370&w=2) * [The Rust Project Contributors, “Bootstrapping the compiler”](https://rustc-dev-guide.rust-lang.org/building/bootstrapping/intro.html) * [The Rust Project Contributors, “Link using the linker directly”](https://github.com/rust-lang/rust/issues/11937) * [The Rust Project Contributors, “Rustdoc overview - Multiple runs, same output directory”](https://rustc-dev-guide.rust-lang.org/rustdoc.html) * [The Rust Project Contributors, “The Cargo Book”](https://doc.rust-lang.org/cargo/) * [The Rust Project Contributors, “Command-line Arguments - The rustc book”](https://doc.rust-lang.org/rustc/command-line-arguments.html) * [The Rust Project Contributors, “Queries: demand-driven compilation”](https://rustc-dev-guide.rust-lang.org/query.html) * [The Rust Project Contributors, “What Bootstrapping does”](https://rustc-dev-guide.rust-lang.org/building/bootstrapping/what-bootstrapping-does.html) * [Thomas Pöchtrager, “MacOS Cross-Toolchain for Linux and *BSD”](https://github.com/tpoechtrager/osxcross?tab=readme-ov-file) * [“What is a compiler toolchain? - Stack Overflow”](https://stackoverflow.com/a/69006179) * [Wikipedia, “Dynamic dispatch”](https://en.wikipedia.org/wiki/Dynamic_dispatch) * [Wikipedia, “Late binding”](https://en.wikipedia.org/wiki/Late_binding) * [Wikipedia, “Name binding”](https://en.wikipedia.org/wiki/Name_binding) * [william woodruff, “Weird architectures weren’t supported to begin with”](https://blog.yossarian.net/2021/02/28/Weird-architectures-werent-supported-to-begin-with) * [Zig contributors, “Zig Build System“](https://ziglang.org/learn/build-system/) --- <br> [^14]: the uncommon case mostly looks like [incremental bugs in rustc itself](https://blog.rust-lang.org/2021/05/10/Rust-1.52.1/), or issues around rerunning build scripts. <br> 罕见的情况大多是 [rustc 本身的增量编译错误](https://blog.rust-lang.org/2021/05/10/Rust-1.52.1/),或者是重新运行构建脚本的问题。 [^1]: see [this stackexchange post](https://langdev.stackexchange.com/questions/153/what-are-the-advantages-of-requiring-forward-declaration-of-methods-fields-like/) for more discussion about the tradeoffs between forward declarations and requiring full access to the source. <br> 有关前向声明和要求完全访问源代码之间的权衡的更多讨论,请参阅[此 stackexchange 帖子](https://langdev.stackexchange.com/questions/153/what-are-the-advantages-of-requiring-forward-declaration-of-methods-fields-like/)。 [^2]: even [Rust depends on crt1.o when linking](https://github.com/rust-lang/rust/issues/11937)! <br> 甚至 [Rust 在链接时也依赖于 crt1.o](https://github.com/rust-lang/rust/issues/11937)! [^3]: early binding is called "static linking". <br> 早期绑定称为“静态链接”。 [^4]: actually, Zig solved this in [the funniest way possible](https://andrewkelley.me/post/zig-cc-powerful-drop-in-replacement-gcc-clang.html), by bundling a C toolchain with their Zig compiler. This is a legitimately quite impressive feat. If there's any Zig contributors reading—props to you, you did a great job. <br> 实际上,Zig 以一种[最有趣的方式](https://andrewkelley.me/post/zig-cc-powerful-drop-in-replacement-gcc-clang.html)解决了这个问题,即将其 Zig 编译器与 C 工具链捆绑在一起。这确实是一项令人印象深刻的壮举。如果有任何 Zig 贡献者正在阅读本文——向你们致敬,你们做得很好。 [^6]: almost every build system does this, so I don't even feel compelled to name names. <br> 几乎每个构建系统都这样做,所以我甚至觉得没有必要点名。 [^7]: Starlark is *not* tied to hermetic build systems. The fact that the only common uses of it are in hermetic build systems is unfortunate. <br> Starlark 并*不*与封闭式构建系统绑定。它仅有的常见用途都在封闭式构建系统中,这很不幸。 [^8]: H.T. [Julia Evans](https://jvns.ca/blog/2020/10/26/ninja--a-simple-way-to-do-builds/) [^9]: actually variables are more general than this, but for $in and $out this is true. <br> 实际上变量比这更通用,但对于 $in 和 $out 来说是这样。 [^10]: another example is "rebuilding build.ninja when the build graph changes". it's more common than you think because the language is so limited that it's easier to rerun the configure script than to try and fit the dependency info into the graph. <br> 另一个例子是“在构建图更改时重建 build.ninja”。这比你想象的要常见,因为该语言非常有限,重新运行配置脚本比尝试将依赖信息放入图中更容易。 [^11]: not actually turing-complete <br> 实际上不是图灵完备的 [^12]: I have been informed that the open-source version of Bazel is not actually hermetic-by-default inside of its prelude, and just uses system libraries. This is quite unfortunate; with this method of using Bazel you are getting a lot of the downsides and little of the upside. Most people I know using it are doing so in the hermetic mode. <br> 我被告知,开源版本的 Bazel 在其前奏中实际上并不是默认封闭的,它只是使用系统库。这非常不幸;使用这种方法使用 Bazel,你既得到了很多缺点,又几乎没有得到好处。我认识的大多数人都是在封闭模式下使用它的。 [^13]: there's something *called* `docker compose`, but it composes containers, not images. <br> 有一个东西*叫做* `docker compose`,但它组合的是容器,而不是镜像 网闻录 构建系统的权衡