Skip to main content

Crate epicenter_cli

Crate epicenter_cli 

Source
Expand description

Core library behind the epicenter CLI — the state layer for Cargo builds.

§epicenter — the state layer for Cargo builds

Requires Cargo with build.build-dir support — Rust 1.91+.

Rust has two main problems: slow compilation times and big target directories. This usually means that every Cargo project compiles its dependencies into its own target/ directory: ten projects = ten copies of the same rlibs, consuming gigabytes of storage. So epicenter tries to solve this problem with a simple idea: why not keep one shared version of compiled dependencies?

It manages that state as three tiers, one tool:

  • store — a durable vault of unit groups addressed by Cargo’s own fingerprint hashes (…/epicenter/store/…); survives cargo clean, feeds cold starts and parallel jobs.
  • build — the shared cache every project compiles into (…/epicenter/build/…): dependency rlibs, incremental state, build-script outputs — the heavy 95% — compiled once and reused everywhere.
  • work — disposable per-job build dirs seeded from the vault (…/epicenter/work/<id>/), where CI jobs and agents run fully parallel: nothing shared but the read-only vault.

Around those tiers epicenter owns the lifecycle: configure (setup, doctor — cross-OS config truth), observe (status, who, why, vault, verify), reclaim (prune for age-based housekeeping, tiered evict for deliberate removals with honest freed-vs-released accounting), persist (harvest, restore) and execute (exec). Cargo stays Cargo — no shim, no daemon — and a rustc-wrapper (kache or sccache) remains an optional tier.

One configuration for Linux, WSL, macOS and native Windows: the platform code is std-only and cfg-gated, and the canonical template line is identical everywhere. (Linux/WSL and Windows are verified live; Apple and BSD targets are compile-verified — see Cross-OS notes for the first-run checklist.)

epicenter isn’t a Cargo wrapper — it leans on Cargo’s own fingerprint engine, which decides freshness and therefore recompilation. A unit restored from the vault is Fresh to Cargo in milliseconds, with zero rustc processes spawned.

§Benchmarks

These benchmarks are done against 5 projects with 37 identical dependencies each.

Five cold builds: 71.3 s → 15.3 s (4.7× faster). Disk: 3.65 GiB → 0.91 GiB (−75%). A cold per-job work/ dir seeded from the vault: 0.33 s.

All five projects declare the same platform stack. This is the monorepo / “one platform crate” case. Every non-local Cargo unit is byte-for-byte identical across projects, so epicenter can compile it once and the other projects reuse it.

§What is measured

baselineepicenter
configno build-dir keyepicenter setup → [build] build-dir = '{cargo-cache-home}/epicenter/build'
where dependencies compile<project>/target/ (one copy each)one shared build dir
measurementwall clock around cargo build --locked --offlinesame
reset before every runrm -rf every target/rm -rf every target/ + the whole epicenter/ cache

In addition it measures the vault tier (epicenter harvest + epicenter exec), where a cold per-job build dir is seeded from the durable store instead of recompiling.

Cumulative build time

Disk usage

Epicenter shared projects save

Build time per project

Cold start in a per-job work dir

§Quickstart

cargo install epicenter-cli
epicenter setup                  # write the shared-cache config (backs up files it touches)
cargo build                      # any project of yours — intermediates now land in the shared cache
epicenter status                 # watch it fill up
epicenter doctor                 # verify everything is healthy

§Three workflows, one tool

  1. Daily development (zero extra steps) — run epicenter setup once. Every project on the machine immediately shares compiled dependencies.
  2. Survive cargo clean and cold starts (the vault) — epicenter harvest snapshots built units into the read-only vault; epicenter restore links them back so Cargo skips compilation completely.
  3. Parallel agents & CI jobs (zero lock waits) — epicenter exec --id agent-1 --target-dir -- cargo test seeds a private per-job build dir from the vault in milliseconds. Ten agents can build the same repo at the same time without waiting on Cargo’s build-dir lock or duplicating rlibs on disk.

§Commands

commandwhat it does
epicenter setupwrite the shared-cache config; backups any file it modifies (*.epicenter-bak)
epicenter setup --cache-root <path>use a different cache directory instead of the default
epicenter setup --use-defaultremove a custom root, back to the canonical default
epicenter setup --with-wrapper kachealso configure kache (or sccache) as rustc-wrapper
epicenter setup --without-wrapperremove a previously configured wrapper
epicenter statussizes, per-directory breakdown, largest build units
epicenter who [--unit <q>]unit forensics: per-package blocks, toolchain generations, stale flags
epicenter why <unit>one unit in depth: hashes, features, deps, artifact inventory, FRESH/DIRTY verdict
epicenter evict --unit <q>remove units deliberately (store + views): dry run by default, --yes to delete
epicenter evict --unit <q> --tier storetrim the vault only — no --force needed, the vault is yours to trim
epicenter evict --unit <q> --tier viewunlink the working copies only (build dir + job dirs); vault untouched
epicenter evict --work <job|build> --crate <name>surgical cache-bust: unlink one crate from ONE view; the vault is never touched
epicenter evict --stale / --husks / --older-than 90d / --rustc-key <h>bulk selectors, by default across the store and every known view
epicenter evict --work-dir <job>remove ONE job dir now — explicit choices bypass the GC window (dry run default; a live build’s dir is refused)
epicenter evict --work-dirs [--older-than 7d]collect stale per-job working dirs; a dir a live build has locked is never touched
epicenter prune [--older-than 30d]age-based housekeeping of the shared build dir (policy GC, default 30d). Dry run by default — pass --yes to delete
epicenter doctorhealth check with plain-language verdicts: config, wrapper, filesystem, toolchain, mtime ordering
epicenter vaultshow the vault: generations (toolchain + flags), sizes, links into the build view
epicenter harvest [--unit <q>] [--dry-run]store built unit groups from the shared build dir into the vault
epicenter restore [--into <dir>] [--unit <q>] [--dry-run]seed a working dir from the vault (default: the shared build dir) — deps/ rlibs and .so hard-linked; .rmeta + fingerprint state copied
epicenter verifyaudit the vault: file sizes, canonical stamps, hash contents, dependency ordering — and name the job dirs under work/
epicenter exec [--harvest] [--rm] [--reuse] [--target-dir [DIR]] [--id <id>] [--cwd <dir>] -- <cmd>run a command in a fresh per-job build dir seeded from the vault (CI/agents)

§prune vs evict — housekeeping vs deliberate removal

Both delete things, both show a dry run first, and neither ever runs on its own. The difference is why and where they act:

epicenter pruneepicenter evict
criterionage — a policy (--older-than, default 30d)identity — a selector (--unit, --stale, --rustc-key, --husks) or an explicit target (a job dir)
scopethe shared build dir only (one view)the vault and every known view by default (--tier all); one tier on request
spacefrees bytes no other link holds; vault copies keep theirswith --tier all, removes every link it knows — the report separates actually freed from links released
typical usescheduled housekeeping: “keep the cache bounded”“get rid of this”: a package, a stale toolchain generation, husk units, a finished job dir
never doestouch the vault or job dirs; take a selectorsweep purely by age (except --work-dirs, the job-dir GC); remove the shared build dir itself

Rules of thumb:

  • the cache grew over months and you just want it smaller → prune
  • a specific crate misbehaves, a toolchain generation went stale, an agent’s job dir is done → evict
  • you want the space back, not just an unlink from the working view → evict --tier all (or --tier store to trim the vault alone)

Both report what they would do first; nothing is deleted without --yes. (What “tiers” mean in practice — and how the numbers stay honest — is in Tiers, and honest numbers below.)

§Configuration — one file owns the decision

The shared-cache decision lives in $CARGO_HOME/config.toml (~/.cargo/config.toml) as the [build] build-dir key:

[build]
build-dir = '{cargo-cache-home}/epicenter/build'

epicenter setup writes/manages that key for you (and keeps a *.epicenter-bak backup of the file before touching it). Custom roots go to the same place: epicenter setup --cache-root ~/bigdisk/rust-cache replaces the key’s value with your path. There is no separate epicenter state file — nothing can drift out of sync, and epicenter doctor verifies the whole arrangement. (Older versions used ~/.epicenter/config.toml; it is no longer read and is safe to delete — doctor reminds you if it is still around.)

{cargo-cache-home} is expanded by cargo at every build — one line is correct on Linux, WSL and native Windows; nothing is path-resolved-and-stored.

§Changing the build dir

epicenter setup --cache-root /mnt/big/cache        # literal path
epicenter setup --cache-root '~/rust-cache'        # home-relative
epicenter setup --cache-root '{cargo-cache-home}/n1' # template power-users
epicenter setup --use-default                      # back to default

Priority is explicit: CLI flag > the value already in ~/.cargo/config.toml > the canonical default template. After every change, epicenter doctor re-verifies that the cargo config and the cache directory agree.

§How resolution works (and why it is safe)

  • configs always contain the template, never an expanded path — cargo re-expands {cargo-cache-home} on the platform it builds for, so one file serves WSL and Windows;
  • commands that touch the filesystem (status/prune) ask cargo itself (cargo metadata → build_directory) for the effective directory, then normalize separators locally; the expansion is never written back into any config;
  • paths with backslash separators are the one known hazard in a shared-cross-OS setup — the canonical form therefore uses forward slashes, which are valid separators on Windows too.

§The vault — surviving cargo clean and cold starts

Cargo’s own fingerprints make the shared build dir fast while it is warm — but a wipe (cargo clean, a fresh checkout, a new container) throws that state away. The vault makes the state durable:

{cargo-cache-home}/epicenter/
  store/<parity-key>/          the vault: one generation per toolchain + flag set
    manifest.json              file list, sizes, canonical stamps
    units/<pkg>-<hash>/        one unit group: fingerprint files + artifacts
  build/…                      the shared build dir — a disposable view of the store
epicenter harvest              # after a build: store every unit group in the vault
epicenter vault                # what the store holds, per generation
epicenter verify               # audit: sizes, stamps, hash contents
cargo clean                    # the view is gone — the store is not
epicenter restore              # (or --into <dir>): seed the view from the vault
cargo build                    # Cargo sees fresh fingerprints: zero recompiles

Measured under WSL on ext4: wiping one unit and restoring it takes milliseconds, after which cargo build -v reports Fresh … in 0.01s with zero rustc processes. Restore hard-links deps/ link outputs — a thousand jobs share one copy of every rlib — and copies .rmeta plus the fingerprint state and build-script trees, because rustc and cargo rewrite those in place (a shared inode would either break a rebuild or corrupt the store). Each unit group carries one canonical timestamp: the moment its .rmeta landed, so nothing is ever older than what it compiled against — epicenter verify audits that order. Everything is fail-open: a partial vault just means Cargo recompiles what is missing, never a broken build.

Generations are keyed by <rustc-release>-<commit>-<host>-<flag-digest>: a toolchain upgrade opens a new generation instead of invalidating one, and epicenter vault marks which generation is active. (Cross-arch sharing is not supported — one vault per platform.)

§One line for CI jobs and agents

exec mints a fresh build dir per job, seeds it from the vault, and runs your command in it — no shim, no daemon, no config change:

epicenter exec -- cargo test                    # warm start, nothing kept
epicenter exec --harvest -- cargo build         # …and store the units this job produced
epicenter exec --rm --id nightly -- cargo test  # delete the job dir after the run

For long-lived agents, a job dir is a home:

epicenter exec --id agent-7 --target-dir -- cargo build           # per-job finals dir
epicenter exec --id agent-7 --reuse --target-dir -- cargo test    # continue in it

--reuse continues in an existing job dir instead of refusing (restore is idempotent; a dir a live build holds is still refused). --target-dir pins the child’s CARGO_TARGET_DIR — bare it is <work dir>/target, so finals live and die with the job; with a value it selects that path instead. With both pinned per job, two builds of the same project share nothing but the read-only vault: no lock contention anywhere — the fully parallel case.

The command’s exit code becomes epicenter’s exit code; its stdout and stderr pass through untouched (epicenter’s own frame lines go to stderr). Every job owns its build dir, so parallel jobs never wait on Cargo’s per-build-dir lock, and job dirs are disposable — they live under {cargo-cache-home}/epicenter/work/<id>/.

Job dirs accumulate by design (one per job). epicenter evict --work-dirs collects the ones untouched for a week (customizable with --older-than, dry-run by default, --yes to delete) and reports what was actually freed — bytes still hard-linked from the vault or a wrapper’s store are released, not freed, and are reported separately. A dir a live build has locked is never touched. And a dir you no longer need goes right now: epicenter evict --work-dir <job> --yes — the age window is a GC policy for bulk collection, not a gate on explicit choices.

§Tiers, and honest numbers

The vault is the user’s storage: trimming it never breaks a build — the only cost is the next compile. evict therefore treats the tiers explicitly:

  • default --tier all — the store and every known view (build dir + job dirs). Use it to actually reclaim space; the dry run already tells you the truth: nominal bytes, actually freed (the last hard link goes with it), and links released (the vault or a wrapper store still holds those bytes).
  • --tier store — the vault alone; no --force needed for the current generation, because views keep working from their own links.
  • --tier view / evict --work <job> --crate <name> — cache-bust a chosen crate in one view (a patched dependency in an agent job, a flaky crate in CI). A freshness operation, never a space claim; the vault is untouched and a view a live build has locked refuses to be touched.

§Cross-OS notes

  • macOS / BSD: nothing platform-specific to configure — same template, same commands. First-run checks worth doing on real hardware: the Cargo lock guard (exec --reuse must refuse while a build runs in the job dir, and evict --work-dirs must skip it), hard links plus the copy+stamp fallback across APFS volumes, and epicenter doctor. Stamps are whole-second, so HFS+’ 1-second mtime granularity is safe.
  • WSL users: projects on /mnt/c are fine, but keep the cache on the Linux side (the default does) — 10–30× faster I/O. doctor warns if the cache ever drifts onto a Windows-mounted drive.
  • Never put an expanded Windows path (C:\…) into ~/.cargo/config.toml: WSL cargo reads that same file as an ancestor config and breaks. doctor catches this.

§With kache or sccache (optional)

One line: epicenter setup --with-wrapper kache. The wrapper tier adds compile-result caching across machines (CI/remote) and languages beyond Rust. epicenter stays the cache-state authority either way: config health, pruning, ownership, diagnostics.

§License

Licensed under the GNU General Public License v3.0 or later (GPL-3.0-or-later). See the LICENSE file in the repository.

Modules§

config
Editing Cargo’s global config.toml — the single source of truth.
doctor
epicenter doctor: one check per known failure mode of a shared build-dir setup. Each check emits one line with a severity icon; the checks are grouped into sections for printing.
evict
epicenter evict — deliberate, user-invoked removal of units. Eviction is never automatic: the caller must select units explicitly.
exec
epicenter exec — run one command against a fresh per-job build dir seeded from the vault.
fingerprint
Read-only parser for cargo’s .fingerprint/ state: parses each unit directory into a Unit (kind, hashes, fingerprint JSON, dep-info) and attaches the artifacts that belong to it.
paths
Where things live: $HOME, $CARGO_HOME, and the shared cache root.
prune
Garbage collection for the shared cache.
status
Read-only inspection of the shared cache: total size, per-directory buckets, oldest entry, and the largest build units.
util
Small formatting/parsing helpers shared by the other modules.
vault
The vault: a durable store of Cargo unit groups, addressed by Cargo’s own fingerprint hashes.
who
epicenter who / epicenter why — unit ownership forensics over the shared build dir. Ground truth source: .fingerprint/ unit dirs (via the crate::fingerprint reader), artifacts attached by suffix, staleness reasoned with the mtime rule.