-
作者:Ed Page(Rust 官方团队)
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 build 和 rust-analyzer 会互相等待同一把锁,因为它们操作的是同一个目录下的混合产物,锁不能更细。
问题四:Windows 上 deps/ 污染 PATH。 构建过程中 deps/ 目录会被加入 PATH,里面混杂着所有包的中间产物,会造成意外的路径污染。
问题五:中间产物文件名冲突。 两个包恰好产生同名的中间文件,就会互相覆盖,这是一个隐性的正确性问题。
新布局长什么样
新布局的核心思想只有一句话:按包名分层,每个构建单元自成一体。
同样是两个包 lib 和 bin,新布局变成这样:
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 test、cargo build 和 rust-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 跑一遍,早发现早适配,比等到稳定版本上线后再处理要从容得多。
参考资料:
- 原文:blog.rust-lang.org/2026/03/13/…
- Cargo 构建缓存文档:doc.rust-lang.org/cargo/refer…
- 追踪 Issue:github.com/rust-lang/c…
- 跨工作区缓存提案:github.com/rust-lang/c…