Rust项目公开征测:Cargo 构建目录新布局方案

4 阅读7分钟

Rust 官方团队正在邀请社区测试一项酝酿已久的 Cargo 变更:全新的构建目录布局(Build Dir Layout v2)

这件事看上去只是目录结构的调整,但它是 Cargo 未来几个重要特性的地基,也可能悄悄影响你现有的构建脚本、CI 流程,乃至你依赖的某些测试库。了解它,比等到正式上线后踩坑要好得多。


为什么要改目录结构

在谈新布局之前,先搞清楚现在的布局有什么问题。

打开一个 Rust 项目的 target/debug/(或 build-dir/debug/),你会看到这样的结构:

debug/
├── .fingerprint/       # 构建缓存追踪,所有包混在一起
├── build/              # 所有包的 build script 产物混在一起
├── deps/               # 所有包的中间编译产物混在一起
├── examples/
└── incremental/

这是一种"按内容类型分类"的组织方式——把所有包的 fingerprint 文件堆在一个 .fingerprint/ 目录里,把所有包的编译产物堆在 deps/ 里。

这种方式带来了几个长期未能解决的问题:

问题一:无法做跨工作区缓存。 共享缓存需要知道哪些产物属于哪个构建单元,而现在一个 deps/ 里几十个包的产物全部混在一起,根本无法做到独立追踪。

问题二:过期产物无法自动清理。 你换了一个依赖版本,旧版本的中间产物就这样永远堆在 deps/ 里,target/ 目录越来越大,只能靠 cargo clean 一刀切。

问题三:文件锁粒度太粗。 现在 cargo buildrust-analyzer 会互相等待同一把锁,因为它们操作的是同一个目录下的混合产物,锁不能更细。

问题四:Windows 上 deps/ 污染 PATH。 构建过程中 deps/ 目录会被加入 PATH,里面混杂着所有包的中间产物,会造成意外的路径污染。

问题五:中间产物文件名冲突。 两个包恰好产生同名的中间文件,就会互相覆盖,这是一个隐性的正确性问题。


新布局长什么样

新布局的核心思想只有一句话:按包名分层,每个构建单元自成一体。

同样是两个包 libbin,新布局变成这样:

build-dir/
└── debug/
    ├── .cargo-lock
    ├── build/
    │   ├── bin/                          # 按包名分目录
    │   │   ├── [BUILD_SCRIPT_BIN_HASH]/
    │   │   │   ├── fingerprint/*
    │   │   │   └── out/*
    │   │   ├── [BUILD_SCRIPT_RUN_HASH]/
    │   │   │   ├── fingerprint/*
    │   │   │   ├── out/*
    │   │   │   └── run/*
    │   │   └── [HASH]/
    │   │       ├── fingerprint/*
    │   │       └── out/*
    │   └── lib/                          # 按包名分目录
    │       ├── [BUILD_SCRIPT_BIN_HASH]/
    │       │   ├── fingerprint/*
    │       │   └── out/*
    │       ├── [BUILD_SCRIPT_RUN_HASH]/
    │       │   ├── fingerprint/*
    │       │   ├── out/*
    │       │   └── run/*
    │       └── [HASH]/
    │           ├── fingerprint/*
    │           └── out/*
    └── incremental/

与旧布局的对比,用一张表来说明:

对比维度旧布局新布局
组织方式按内容类型(fingerprint/build/deps)按包名 + 构建单元哈希
产物隔离所有包混在同一目录每个包独立子目录
fingerprint 位置统一在 .fingerprint/随各构建单元就近存放
清理粒度只能整体 clean可以按包、按单元精确清理
文件锁粒度整个 target 目录可细化到每个构建单元

旧布局中,.fingerprint/bin-[HASH]deps/bin-[HASH] 是分散在两个地方的,属于同一个构建单元的信息被物理上拆开了。新布局把它们全部收拢在 build/bin/[HASH]/ 这一棵子树下,一个构建单元的所有信息都在自己的目录里,完全自包含。


这个改动解锁了什么

这次目录重构是一块基石,它让下面这些长期搁置的功能变得可行:

跨工作区缓存共享(cross-workspace caching)

这是最重要的动机。当每个构建单元都有自己独立的目录时,Cargo 才能把这些目录当作可移植的缓存单元,在不同工作区之间共享。对于大型 monorepo 或多项目组合来说,这意味着编译时间的大幅缩短。

过期构建产物的自动清理

旧布局下,Cargo 很难判断一个构建单元的产物是否还有用,因为相关文件散落在多个目录。新布局下,一个构建单元的全部产物就在一个目录里,过期了直接删掉那个目录即可,实现常数级的磁盘占用。

更细粒度的文件锁

cargo testcargo buildrust-analyzer 现在会争抢同一把粗粒度的锁。新布局可以将锁的粒度细化到每个构建单元目录,让它们真正并行工作,互不阻塞。

顺带修复的问题

在推进这个方案的过程中,团队还发现它意外地改善了一些历史问题:deps/ 里中间产物的积累导致构建性能下降的问题、Windows 上 PATH 污染的问题、中间产物文件名冲突的问题,都随之得到缓解。


谁可能受到影响

final artifacts(最终产物,即 target-dir 下的内容)不会变化,也就是说你用 target/release/my-binary 这样的路径访问最终可执行文件,不受影响。

受影响的是那些依赖了 build-dir 内部布局的工具或脚本,例如:

  • 从测试二进制路径推导可执行文件路径的代码:应改用 CARGO_BIN_EXE_* 环境变量;
  • 构建脚本(build script)从自身路径或 OUT_DIR 推导 target-dir 位置:需要适配新的目录结构;
  • 从 rustc 产出物路径查找用户请求的工件:同样需要适配。

目前已知受影响的库状态(截至博客发布时):

库名状态
assert_cmd已修复
snapbox已修复
executable-path已修复
trycmd已修复
cli_test_dir待修复(Issue #65)
compiletest_rs待修复(Issue #309)
term-transcript待修复(Issue #269)
test_bin待修复(Issue #13)

如何参与测试

需要 nightly 2026-03-10 及以上版本,在命令行加上 -Zbuild-dir-new-layout 标志:

cargo test -Zbuild-dir-new-layout
cargo build -Zbuild-dir-new-layout

把你平时跑的测试、发布流程、CI 脚本都过一遍。如果遇到失败,也可以先用以下方式单独验证是否与构建目录分离本身(而非新布局)相关:

CARGO_BUILD_BUILD_DIR=build cargo test

发现问题后,可以:

  • 修复本地代码中的硬编码路径;
  • 向受影响的上游库提 Issue,并在 Cargo 的追踪 Issue #15010 留下记录;
  • 直接在追踪 Issue 上提供反馈。

后续规划

这次布局变更之后,Cargo 团队还有几个方向在路上:

  • 缩短构建路径长度,降低 Windows 用户遇到路径过长错误的风险;
  • 将产物从 --profile--target 子目录中移出,让更多产物可以跨配置共享;
  • 推动更多工具解耦,减少对 build-dir 内部布局的隐式依赖。

部分变更目前被文件锁的改进工作所阻塞,而文件锁的改进又依赖本次布局变更先落地,所以这是一个需要按顺序推进的工程。


写在最后

Cargo 的构建目录布局是个典型的"用久了就不敢动"的地方。大量工具在文档未声明支持的情况下,依赖了 target-dir 内部的路径约定,导致每一次改动都牵一发动全身。

这次 Rust 团队选择在正式发布前广泛征集测试反馈,正是希望在改动落地之前,把受影响的工具逐一梳理清楚,给上游项目足够的时间适配。

如果你有使用 CI 系统缓存 target 目录、或者依赖测试辅助库来定位编译产物,建议现在就用 nightly 跑一遍,早发现早适配,比等到稳定版本上线后再处理要从容得多。


参考资料: