README.md raw
cargo-grunk
Manage cargo’s disk usage, both in $CARGO_HOME and in project target
directories.
Grunk deletes only what can be regenerated. Sources are never touched.
cargo grunk status # what could be reclaimed, and where it went
cargo grunk clean # delete it
cargo grunk clean --dry-run # say what a clean would delete, and stop
cargo grunk list # the projects grunk manages, and how it found them
cargo grunk scan <dirs...> # search for projects, and manage what turns up
cargo grunk add <path> # manage one project
cargo grunk remove <path> # stop managing it
What it deletes
| Category | What it is | Restored by |
|---|---|---|
target | A project’s build output | Rebuilding from local sources |
registry-src | Sources unpacked from a .crate | Re-extracting a local archive |
registry-crate | A downloaded .crate tarball | Re-downloading |
git-checkout | A working tree of a git dependency | Re-extracting from a local clone |
git-db | A bare clone of a git dependency | Re-downloading |
The registry index is never deleted: cargo refreshes it on demand and it buys almost nothing.
--category limits any command to one kind, and is repeatable.
How it ages things
Nothing is deleted until it has gone unused for longer than the age threshold.
For the cargo home, “unused” comes from cargo’s own .global-cache database,
which records when each cached artifact was last used. File timestamps would
be wrong here: the mtime of an unpacked source directory says when it was
extracted, which can be months before the last build that depended on it.
Cargo 1.76 and newer maintain this database; without it, grunk will say so and
leave the cargo home alone.
For target directories there is no such record, so the age is the newest mtime in the tree — the moment of the last build.
A bare git clone is treated as being as recent as its liveliest checkout, since deleting the clone takes its checkouts with it.
Why clean is quick and status is not
Finding a target directory’s age means one stat per file, and a large target
runs to six figures of them. That is the entire cost of running grunk.
But deciding a target is too new to touch does not need the whole walk — it
needs one file newer than the cutoff, and a target being actively built is full
of them. So clean stops at the first one. The walk only runs to the end when
nothing is newer, which is exactly when the target is about to be deleted and
its size is worth having anyway. Cheap when the answer is “leave it”, thorough
when it is “delete it”.
That is why clean reports what it keeps as a count rather than a size: it
never measured them. status does measure everything, and pays for it — it is
the command for a person who wants the numbers, not the one for a boot script.
Nothing here is cached between runs, and nothing infers a tree’s age from its directory’s mtime. A directory’s mtime does not change when a file inside it is rewritten in place, nor when anything in a subdirectory changes at all — so a freshly rebuilt target would look untouched, and get deleted.
How it finds projects
Nothing in $CARGO_HOME records where your projects are, so grunk manages a
project only if it has been told to. There are two ways it gets told, and
cargo grunk list shows which applies to each.
crates-toml— path installs recorded in$CARGO_HOME/.crates.toml. Free, needs no configuration, and works the moment grunk is installed. It only sees packages installed withcargo install --path, which is typically a minority of your projects.explicit— the config’sprojectslist, maintained byadd,removeandscan. Everything else.
Neither goes looking. That is deliberate: searching a source tree for cargo manifests means walking every file in it — on a large tree, a million of them, to find a few dozen — and that is far too much work to repeat on every run of a command you want in a boot script.
So searching is a thing you run, not a thing that runs:
cargo grunk scan ~/source ~/work # find projects, and manage what turns up
cargo grunk scan --dry-run ~/src # just say what it found
scan walks the directories given looking for Cargo.toml, stopping the moment
it finds one on any path — a workspace root sits above its members, so there is
nothing deeper worth looking at. It skips anything already managed, reports each
project with the size of its build output, and writes the rest into projects.
Because it looks for manifests rather than build output, it finds projects that
have never been built too. Run it when you have new projects worth managing.
Nothing else ever walks.
Cargo is asked where each project’s target directory actually is, so workspace
members resolve to the one target directory they share, and a CARGO_TARGET_DIR
or build.target-dir override is respected.
A directory is only treated as build output once cargo has actually built into
it — proven by cargo’s cache tag or, for targets built before cargo began
tagging them (pre-1.42), by the .rustc_info.json cargo leaves at the root. A
target directory cargo never wrote to, holding hand-made data, is left alone.
Configuration
~/.config/grunk.toml, or wherever $XDG_CONFIG_HOME points. Every key is
optional, and the file need not exist — without it, grunk manages the projects
.crates.toml knows about and the cargo home.
# How long data must go unused before `clean` will delete it.
# Any humantime duration: "3d", "12h", "1h 30m". Defaults to "3d".
min_age = "3d"
# The projects grunk manages. `cargo grunk scan`, `add` and `remove` maintain
# this list, and preserve the rest of the file as they do. Projects that
# .crates.toml already records are managed without appearing here.
projects = ["~/work/circuit/frontend"]
# Thresholds for particular categories, overriding min_age.
[min_age_overrides]
registry-crate = "30d" # re-downloading costs a network trip; keep it longer
target = "1d" # rebuilding is cheap and these are the big ones
--min-age on the command line overrides every configured threshold:
cargo grunk clean --min-age 0s # take everything, regardless of age
Concurrency
Grunk takes the same lock cargo takes before touching the cargo home, so a
concurrent cargo build cannot be downloading a crate into a directory being
deleted. Files are deleted before their tracking rows, so an interruption leaves
cargo refetching something it already has rather than orphaning bytes it has
forgotten about.
Licence
Public domain, under the Unlicense.