Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@ jiff = { version = "0.2", features = ["serde"] }
jwalk = "0.8"
trash = "5"
which = "7"
# Safe syscall wrappers. `geteuid` for temp-file ownership checks, `flock` to
# detect a Cargo build in progress — the `libc` equivalents would require
# `unsafe`, which every crate forbids.
rustix = { version = "1.1", features = ["process", "fs"] }
# rustc's own stable hasher (rust-lang/rustc-stable-hash). Reproduces the
# `"rustc"` value Cargo writes into each fingerprint, so Vacuum can name the
# toolchain that built an artifact without executing any compiler.
rustc-stable-hash = "0.1"

# CLI
clap = { version = "4", features = ["derive", "env", "wrap_help"] }
Expand Down
51 changes: 49 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,58 @@ The Steelbore Standard.

| Category | Examples | Risk |
|----------|----------|------|
| **Dev build artifacts** | Rust `target/`, `node_modules`, `.next`, `dist`, `build`, `__pycache__` | Safe — fully regenerable |
| **Package-manager garbage** | `nix-collect-garbage -d`, unused Flatpak runtimes, cargo registry cache, journald logs | Low — reclaimed by each tool |
| **Dev build artifacts** | Rust `target/`, `node_modules`, `.next`, `dist`, `build`, `__pycache__` — plus a native prune of dead units *inside* a `target/` you want to keep | Safe — fully regenerable |
| **Package-manager garbage** | `nix-collect-garbage -d` (user and system-wide), unused Flatpak runtimes, `journalctl --vacuum-time`, `systemd-tmpfiles --clean`, podman/docker prune | Low — reclaimed by each tool |
| **App / user caches** | `~/.cache`, browser caches, regenerable model blobs | Low |
| **Stale temp files** | Entries under `/tmp`, `/var/tmp`, `$TMPDIR` that are yours and untouched for 7+ days | Low — never touches live session state |
| **Large files** | The biggest files and directories, browsed interactively | Your call |

### About the Cargo prune

Deleting a whole `target/` costs a full rebuild. The prune instead removes only the
units that are already dead, so a project you are still working on keeps its warm
cache. It runs entirely off the filesystem — Vacuum never executes `cargo`,
`rustup`, or `rustc`, which matters on a Nix or Guix system where there may be no
runnable toolchain on `PATH` at all.

Three things are offered per target directory:

- **units from an old toolchain.** Each build unit records the compiler that made
it. Units are grouped by that value and the group with the most recent activity
is the one in use; the rest are left over from a compiler you have since
changed. Where the version can be named it is (`2 units from rustc 1.95.0`).
- **cold units**, not rebuilt in `--stale-days` days (default 30). Freshness comes
from the `invoked.timestamp` file Cargo writes for the purpose — never access
time, which is not updated on `relatime` mounts when Cargo reuses an artifact.
- **the incremental cache**, which is pure rebuild-time state and often the
largest single item in a `target/`.

Guards: a profile whose `.cargo-lock` is held by a running build is skipped
entirely; the unhashed final binaries are never touched; a unit whose fingerprint
cannot be read is kept, never swept; and sizes count hardlinked inodes once, so
the reported figure is what you actually get back.

These candidates overlap the whole-directory one for the same target — take one or
the other. Use `--cleaner cargo-prune` to select only the prune:

```sh
vacuum list --cleaner cargo-prune
vacuum clean --cleaner cargo-prune --apply
```

### About the temp-file cleaner

It is the safe replacement for `sudo rm -r /tmp/*`, which destroys other users'
files and the live state of running processes. An entry is offered only when it is
not a symlink, is owned by you, has gone untouched for seven days, and is not
session state (`.X11-unix`, `systemd-private-*`, `.Trash-*`, and friends). Root-owned
leftovers are left to `sudo systemd-tmpfiles --clean`, which Vacuum prints for you.

These candidates are **purged rather than trashed**, even without `--purge`: the trash
for a path under `/tmp` is `/tmp/.Trash-$uid`, on the same filesystem, so trashing
would reclaim nothing at exactly the moment you need the space. Vacuum says so in
its output rather than doing it quietly, and `--apply` is still required.

## Safety first

- **Dry-run by default.** Nothing is deleted until you pass `--apply`.
Expand Down
6 changes: 5 additions & 1 deletion crates/vacuum-cleaners/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,17 @@ rust-version.workspace = true
license.workspace = true
repository.workspace = true
homepage.workspace = true
description = "The Vacuum cleaner catalog: build artifacts, package GC, caches, large files."
description = "The Vacuum cleaner catalog: build artifacts, package GC, caches, temp files, large files."
keywords.workspace = true
categories.workspace = true

[dependencies]
vacuum-core.workspace = true
which.workspace = true
rustix.workspace = true
rustc-stable-hash.workspace = true
serde.workspace = true
serde_json.workspace = true

[dev-dependencies]
tempfile.workspace = true
Expand Down
11 changes: 4 additions & 7 deletions crates/vacuum-cleaners/src/build_artifacts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ impl Cleaner for BuildArtifacts {
detail: Some("regenerable build output".to_owned()),
bytes,
regenerable: true,
trash_ok: true,
risk: Risk::Safe,
target: Target::Path { path },
});
Expand Down Expand Up @@ -97,14 +98,12 @@ mod tests {
fs::write(proj.join("Cargo.toml"), b"[package]").unwrap();
fs::write(proj.join("target/artifact"), vec![0_u8; 4096]).unwrap();

let ctx = ScanContext {
roots: vec![tmp.path().to_path_buf()],
};
let ctx = ScanContext::new(vec![tmp.path().to_path_buf()]);
let found = BuildArtifacts.scan(&ctx).unwrap();
assert_eq!(found.len(), 1);
match &found[0].target {
Target::Path { path } => assert_eq!(path.file_name().unwrap(), "target"),
Target::Command { .. } => panic!("expected a path target"),
other => panic!("expected a path target, got {other:?}"),
}
}

Expand All @@ -114,9 +113,7 @@ mod tests {
fs::create_dir_all(tmp.path().join("misc/target")).unwrap();
fs::write(tmp.path().join("misc/target/data"), vec![0_u8; 4096]).unwrap();

let ctx = ScanContext {
roots: vec![tmp.path().to_path_buf()],
};
let ctx = ScanContext::new(vec![tmp.path().to_path_buf()]);
assert!(BuildArtifacts.scan(&ctx).unwrap().is_empty());
}
}
5 changes: 2 additions & 3 deletions crates/vacuum-cleaners/src/caches.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ impl Caches {
detail: Some("cache (regenerated on demand)".to_owned()),
bytes,
regenerable: true,
trash_ok: true,
risk: Risk::Safe,
target: Target::Path { path },
});
Expand All @@ -98,9 +99,7 @@ mod tests {
fs::create_dir_all(&cache).unwrap();
fs::write(cache.join("blob"), vec![0_u8; 2048]).unwrap();

let ctx = ScanContext {
roots: vec![tmp.path().to_path_buf()],
};
let ctx = ScanContext::new(vec![tmp.path().to_path_buf()]);
let found = Caches.scan(&ctx).unwrap();
assert_eq!(found.len(), 1);
assert_eq!(found[0].bytes, 2048);
Expand Down
Loading
Loading