target 目录越来越大,Rust 项目该怎么清理

Rust 项目一多,磁盘空间常常先被 target 目录吃掉。每个工作区的 target 里,既有最后要运行的程序,也有依赖编译结果、增量编译数据和构建脚本产生的中间文件。它们都在增长,但生命周期并不一样。

整目录删除当然最快,代价是最终产物也会一起消失。只想清掉缓存,又得挨个项目判断哪些目录能删。这个两难,是我写 cargoc 的原因。

cargoc 把 Cargo 的中间构建文件放到专用目录,按工作区记录和清理空闲缓存;最终产物仍留在 Cargo 原来的 target-dir。它解决的是"哪些中间文件可以清、什么时候清"。文件是否变小,取决于后续有没有删除空闲缓存;编译仍由 Cargo 完成。

[target 里到底该删什么](#target 里到底该删什么)

Cargo 现在可以把两类产物分开:target-dir 保存面向使用者的最终产物,build-dir 保存构建过程中的中间文件。两者默认仍可能落在工作区的 target 目录里,所以日常使用时看起来像一整块缓存。

这一区分很重要。最终程序、库或安装结果通常需要继续使用;中间文件则可以在合适的时候重新生成。直接运行 cargo clean 会交给 Cargo 自己处理清理范围,不能把它当成"只删除中间文件"的命令。

Cargo 1.91 的构建缓存文档说明了这两个目录的关系:https://doc.rust-lang.org/1.91.0/cargo/reference/build-cache.html。

cargoc 的做法是:为每个受管理的工作区分配一个专用的 build-dir,让真实 Cargo 继续完成编译。target-dir 的既有配置不变,清理器也不连接最终产物目录。

目录关系示意,不是磁盘占用实测。图中省略了命令接管、异常和进程管理分支。交互版及源码定位:https://assets.bigfisharc.cn/images/cargoc-social/layout-zh-ff0d1a14e5.html。

缓存根目录下,每个工作区使用自己的路径哈希目录。统一管理不等于让多个项目共用一份可写缓存,也不等于跨项目依赖去重。安装 cargoc 后,旧 target 里的文件也不会自动搬走;旧目录没有立刻变小是正常的。

正在编译的缓存,不能只按时间删除

"很久没修改"只能帮助排序,不能证明目录现在没人使用。一个构建可能已经启动,只是还没有产生新的文件。

一次受管理构建里有三层保护:

  • 当前工作区持有租约,自动清理会显式保护它;
  • 清理候选时,还要取得候选工作区的租约和扫描到的 Cargo 原生锁;
  • 包装器停止构建时,只关闭本次调用拥有的进程组或 Windows Job,不按进程名杀掉机器上的其他 Cargo。

因此,清理器遇到锁忙的缓存会跳过。它宁愿暂时达不到空间目标,也不会为了凑数字强删正在使用的目录。

构建退出时,顺序也不能反过来:包装器先关闭自己的进程树,再更新最后使用时间并释放租约,最后才执行结束后的清理。这样清理器不会在子进程仍可能访问缓存时,把它当成空闲目录。

代码路径也对应这三个职责。runner::run 定位工作区、注入 build-dir 并管理一次 Cargo 调用。Store::lease 和 Store::collect 负责缓存身份、租约与候选清理。OwnedChild 负责本次进程组或 Windows Job 的生命周期。

源码中的 构建生命周期 和 候选缓存清理 可以对照阅读。链接固定到当前项目提交,避免以后文件变化后读者找不到对应位置。

预算是软限制,不是硬配额

cargoc 的默认配置给受管理的中间缓存设置了 80 GiB 软预算。超过预算,或者缓存所在卷的剩余空间低于期望值时,自动清理会从较久没有通过 cargoc 使用的工作区开始处理。默认超过 7 天未使用的缓存,也可能因过期被清理。

压力清理的低水位比例默认是 0.8,所以容量目标可以按配置推算为:

复制代码
80 GiB × 0.8 = 64 GiB

64 GiB 是清理策略的目标,不是"已经释放了 16 GiB"。清理按整个工作区的候选缓存进行,也不会精准停在这个数字。活跃缓存受保护、候选缓存不足,或缓存卷与最终产物不在同一块盘时,结果都可能不同。

默认还要求缓存卷保留 20 GiB 空间;运行期间低于 1 GiB 紧急阈值时,cargoc 会停止本次调用拥有的 Cargo 进程树。这两项检查只保护缓存卷和当前调用,不是整台机器的硬盘配额。最终产物、Cargo 下载的依赖数据、旧 target 和其他软件的占用,都不计入这 80 GiB。

先用显式命令试一遍

当前仓库源码版本是 0.1.4。预编译包和安装方式以项目的 Releases 页面及 README 为准:https://github.com/bigfish1913/cargoc/releases。

受管理构建需要 Cargo 1.91+ 。项目声明的 Rust 1.88 是编译 cargoc 自身的最低版本,不能拿来判断被包装的 Cargo 是否支持 build-dir。

安装后可以先检查版本并初始化 cargoc 自己的配置:

复制代码
cargo --version
cargoc --version
cargoc init

init 默认创建 $CARGO_HOME/cargoc.toml;没有设置 CARGO_HOME 时,通常是 ~/.cargo/cargoc.toml。它与 Cargo 的 config.toml 分开,不会把构建目录配置写进 Cargo 的全局配置。

如果指定缓存位置,要使用新的专用目录:

复制代码
cargoc init --cache-dir /path/to/cargoc-cache

不要把已有的非空目录、源码、笔记或最终产物目录直接指定为缓存根。程序会拒绝接管没有所有权标记的非空目录。

进入一个 Rust 工作区后,先从显式调用开始:

复制代码
cargoc build
cargoc status
cargoc clean --dry-run

status 用来确认受管理目录、逻辑占用和缓存卷剩余空间。clean --dry-run 只报告空闲受管理缓存的拟删除结果。removed_bytes 是预演数字,不是已经回收的磁盘空间。确认范围后,再决定是否执行 cargoc clean。

空间不足时,先看阈值和缓存卷

一次实际演示中,Windows 上的缓存卷空闲空间低于默认的 20 GiB,cargoc 没有启动 Cargo,而是返回:

复制代码
cargoc: Insufficient free disk after safe cleanup; refusing to start Cargo

这类提示先用 cargoc status 和 cargoc config 检查缓存位置、空闲空间和阈值。可以释放空间,或把专用缓存放到更充足的卷;不要为了让命令继续就直接降低日常保护值。

现有演示使用 Cargo 1.99.1、本地 cargoc 0.1.2 和独立配置。演示配置把 min_free_gib 临时调为 2,并保留紧急阈值 1,离线构建才成功。下面的图是实际输出的排版摘录,不是终端窗口截图,也不用于证明空间收益。

图中条件是 Windows / Cargo 1.99.1 / cargoc 0.1.2;[demo config] 表示独立配置。removed_bytes 是预演的拟删除量,不代表本次已经释放的空间。

[想继续输入 cargo,可以另外启用接管](#想继续输入 cargo,可以另外启用接管)

只安装 cargoc 不会改变普通 cargo 命令。希望继续输入 cargo build、cargo test 时,可以启用它提供的 PATH 入口接管:

复制代码
cargoc setup --enable
export PATH="$HOME/.cargoc/bin:$PATH"
command -v cargo
cargo --version
cargoc setup --status

Unix 下 setup 不会替你修改 shell 启动文件。需要把 export 放进对应的 shell 配置。Windows 会写入用户级 PATH;启用后要重新打开终端,再用 Get-Command cargo 检查入口。

这个入口是用户目录中的 shim/代理,不替换 Rustup 的真实 Cargo。代理会转发参数和退出码。IDE 或脚本如果写死真实 Cargo 的绝对路径,或者使用 rustup run <toolchain> cargo ...,仍可能绕过接管。

不想继续接管时,执行:

复制代码
cargoc setup --disable

它只移除 cargoc 自己的入口,不删除 Rustup 的 Cargo,也不会自动删除已经生成的受管理缓存。

[cargoc clean 和 cargo clean 不是一回事](#cargoc clean 和 cargo clean 不是一回事)

cargoc clean 清理的是空闲的受管理中间缓存,而且即使没有设置自动清理,也会处理这批缓存。最终产物在它的删除范围之外。

如果执行:

复制代码
cargoc exec clean

这条命令会转发给 Cargo 原生的 clean,可能删除 Cargo 目标目录中的最终产物。exec 只是解决命令名冲突,不是关闭缓存管理的开关。

同样,非托管的旧 target 或旧版 build-dir 不会被 cargoc 自动接管。需要清理它们时,先停止相关构建和 IDE,再确认原来的 target-dir、最终产物和恢复副本。之后由原所有者单独处理。cargoc 提供的 audit 只读盘点旧目录,不会替你删除。

它适合解决哪一种问题

如果你只是偶尔清理一个 Rust 项目,手动处理可能已经够用。cargoc 适合的是多个工作区长期积累中间构建文件,希望把这批可重建数据集中到一个有归属、可观察、可预演的缓存根里。

它不会带来跨项目依赖复用,也不承诺编译速度提升。空间回收来自删除空闲中间缓存,代价是之后可能重编。它也不负责最终产物、Cargo 下载缓存或整台机器的磁盘配额。

项目地址:https://github.com/bigfish1913/cargoc。反馈时请带上操作系统、Cargo/cargoc 版本、调用方式,以及脱敏后的配置和错误输出;这些信息有助于区分目录、版本和入口接管问题。


原文链接:https://blog.bigfisharc.cn/blog/rust-disk-cargoc/