
开发者让多个 Git worktree 共用 Rust 构建目录,导致代码串用
来源标题:Rust and Git worktrees: share one Cargo build
多个 Git worktree 各自重复编译相同的 Rust 依赖,作者将它们的构建目录指向同一位置以节省空间,结果出现一个 worktree 在无报错的情况下运行了另一个 worktree 的代码。约十个 worktree 两天的构建目录从 11 GB 膨胀到约 80 GB,单个构建占 5 到 15 GB。作者后来改用编译器缓存 kache 来共享构建。
以下正文由 AI 翻译自 Hacker News 最新(原文版权归原站所有),可能存在偏差,重要内容请以原文为准。
所有文章 2026年11月13日 · 9分钟阅读 · Nathan Broadbent
每个git worktree都会为相同的Rust依赖构建各自的副本。让它们都指向同一个构建目录看起来是解决办法,直到某个worktree在毫无报错的情况下运行了另一个worktree的代码。
2026年11月14日更新:我们已用kache替换了这套方案。kache是一个编译器缓存,可在稳定版Cargo上共享相同的构建,占用更少磁盘且构建更快:Sharing Rust builds across git worktrees with kache。
我同时运行多个编码代理,每个代理都在自己的git worktree中工作:同一仓库的独立检出,位于各自的分支上,这样代理们就不会被彼此的改动绊倒。这对Ruby和JavaScript效果很好,对Rust则差一些。
我们的AWS Lambda worker是用Rust编写的,每个涉及它们的worktree都会在自己的target/文件夹中构建它们。一个worktree的构建占用5到15 GB,其中几乎全是相同的第三方crate,以相同的版本和相同的编译标志被反复编译。两天内,大约十个worktree构建了这些worker,它们的构建文件夹从11 GB涨到了约80 GB。我的笔记本电脑磁盘本来就已经相当满,那时只剩2 GB可用空间。我不得不定期运行Mole(一个很棒的自由磁盘清理工具),外加一个专门的Claude Code会话,每周帮我释放几次磁盘空间。(Rust并不是这里唯一的罪魁祸首。我还需要定期清理Docker容器和镜像、旧的worktree、系统缓存以及许多其他东西。)
解决Cargo磁盘占用问题的一个办法是让每个worktree都指向一个共享的构建文件夹。Cargo确实有这个设置,但当我尝试时,一个worktree会静默地运行另一个worktree的代码,因为Cargo在两个worktree中给我们的crate起了相同的文件名。再加几个Cargo设置就能解决这个问题:每个worktree获得自己的crate构建,而所有worktree共享同一份依赖构建。
2026年11月13日更新:在发现一个关于死锁的未解决Cargo issue后,我们已关闭Cargo的细粒度锁定——这是本文最初推荐的。关于同时构建的一节解释了原因。
sccache是一个编译器缓存,Cargo通过它来运行rustc,下文所有数据中的每次构建都启用了它。它能让新worktree的构建更快,因为依赖从缓存中取出,而不是重新编译。不过,在缓存命中时,sccache会把输出复制到该worktree自己的构建文件夹,所以每个worktree最终仍然拥有每个依赖的独立副本。在一个小型crate的两份副本上做的快速测试中,第二份副本从sccache获取了依赖,其target/仍然达到75 MB,接近第一份副本的90 MB。
sccache也无法缓存你自己的crate。在dev构建中,Cargo会增量编译workspace成员,而sccache会跳过增量构建,以及任何运行链接器的crate(二进制文件和proc macro)。我们仍然将它和共享构建目录一起使用:在我们清空该目录后,依赖可以从sccache的缓存中恢复。
你可以用CARGO_TARGET_DIR共享构建文件夹,或者,自Cargo 1.91起,用build.build-dir设置。我会用build-dir。它只移动中间文件(编译后的crate、fingerprint、增量数据),把最终二进制文件留在每个worktree自己的target/中,这样两个worktree永远不会覆盖彼此的target/debug/my-app。
在这个最小示例中,同一crate的两份副本代表两个worktree。它们只差一行:一个打印“A”,另一个打印“B”。两者都使用同一个共享构建目录:
# .cargo/config.toml in a folder above both copies
[build]
build-dir = "/tmp/shared-build"
$ (cd wtA/app && cargo build -q && ./target/debug/app)
A
$ (cd wtB/app && cargo build -q && ./target/debug/app)
A
Worktree B构建时没有报错,并打印了“A”,因为Cargo给了它worktree A的构建。
这比构建慢要糟糕得多。worktree B中的代理可以修改代码、运行测试、看到测试通过并推送,而没有任何东西告诉它,这些测试运行的是另一个分支的代码。
Cargo用输入内容的哈希来命名每个编译单元:包、其版本、features、profile、编译器版本及其依赖的哈希。这个哈希出现在诸如libmy_app-0d7a8db30f960835.rlib这样的文件名中。当两个构建会产生相同的哈希时,Cargo将它们视为同一个单元。
对于来自路径的包,哈希包含包的路径,但它是相对于项目根目录的,而不是绝对路径。Cargo的源码在SourceId::stable_hash的注释中解释了原因:
/// For path dependencies, we do these to help reproducible builds: /// /// * Remove the workspace prefix, /// so the same source will give the same hash in different locations.
对于可复现构建来说,这是一条好规则:在两个位置检出的相同源码应产生相同的产物。两个worktree是同一仓库在两个位置的检出,但各自的改动不同。Cargo无法区分它们,所以worktree A中的crate和worktree B中的crate得到了相同的哈希。Cargo自2023年起就为此有一个未解决的issue。我检查了我们真实的worktree,其中两个构建了一个同名文件:liblambda_pdf_handler-0d7a8db30f960835.rlib。
在共享构建目录中,这意味着两个worktree共享我们crate的一个fingerprint。Cargo通过将记录的构建与其源文件的修改时间进行比较,来决定一个单元是否是最新的。当worktree B在worktree A之后构建,且B的文件都不比A的构建更新时,Cargo找到一个看起来是最新的fingerprint,并复用A的输出。
第三方crate没有这个问题,而且它们占据了大部分空间。Cargo通过registry和版本识别来自crates.io的crate,所以两个worktree以相同的features构建serde 1.0.228,确实是在构建同一个东西。
(原文较长,此处译出前 6000 字,完整内容请点「打开原文」。)
正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。