A RUSTC_WRAPPER that provides cross-worktree Rust compilation caching on top of sccache.
sccache's SCCACHE_BASEDIRS does not normalise paths for Rust compilations (only C/C++ preprocessor output). When building from different git worktrees, every rustc invocation contains absolute paths that differ between worktrees (e.g. --out-dir, --extern, -L), so sccache always cache-misses across trees — even for identical source code.
sccache-wrapper interposes a normalising cache layer between cargo and sccache:
- Parse rustc arguments minimally to extract crate name, output dir, emit flags, and source file.
- Filter workspace-dependent arguments from the cache key:
-C extra-filename=,-C incremental=,--out-dir,-L,--externpaths (only the crate name is kept), and display-only flags (--diagnostic-width,--color).-C metadata=is kept because it distinguishes target vs host compilations. - Normalise remaining arguments by replacing the workspace root with a fixed placeholder (
@@WORKSPACE@@). - Hash the normalised arguments together with the source tree content and compiler identity to produce a workspace-independent cache key.
- On HIT: restore cached artifacts to
--out-dirvia hardlinks (zero-copy, no disk duplication), mapping filenames from the storedextra-filenameto the current one. Replay captured stdout/stderr and rustc's recorded exit status. - On MISS: acquire a per-key
flock(singleflight — concurrent builds of the same key wait rather than compile redundantly), delegate to sccache (which delegates to rustc), capture the outputs, and store them in the cache. Only successful builds (exit status 0) are stored — negative caching is intentionally off because stderr is not yet path-normalised. --testinvocations are cached too: rustc emits a test-harness binary (<name><ef>, nolibprefix;.exeon Windows) instead oflib*.rlib/lib*.rmetaeven when--crate-type libis also passed, and the wrapper recognises this and stores/restores the binary directly. Hardlink restoration preserves the executable bit.- Non-cacheable invocations (e.g.
--print,-vV, proc-macro/binary crate types) are passed through to sccache viaexec()with zero overhead. - Incremental stripping:
-C incremental=…is removed from the arguments passed to sccache/rustc, since incremental compilation is incompatible with sccache's deterministic-output requirement.
| Variable | Default | Description |
|---|---|---|
WB_WORKSPACE_ROOT |
git rev-parse --show-toplevel |
Workspace root for path normalisation. Set this to avoid a git subprocess per rustc invocation. |
WB_RUSTC_CACHE_DIR |
$TMPDIR/winterbaume-rustc-cache |
Cache directory. Use a project-local path in sandboxed environments. |
WB_RUSTC_CACHE |
(enabled) | Set to 0 to disable cross-worktree caching entirely. |
WB_RUSTC_CACHE_DEBUG |
(off) | Set to any value to log HIT/MISS/waiting messages and cache-key args to stderr. |
WB_SCCACHE_WRAPPER_SCOREBOARD |
(off) | Directory to write per-crate scoreboard files into. When set, every cache-miss invocation registers itself so external observers can see which compilations are running, who is contending for them, and how long each one has been (or was) running. See Scoreboard. |
When using mise, all variables are set automatically via mise.toml.
# Build the wrapper (must unset RUSTC_WRAPPER to avoid circular invocation)
RUSTC_WRAPPER= cargo build -p sccache-wrapper
# Set environment
export RUSTC_WRAPPER="$(realpath ./target/debug/sccache-wrapper)"
export WB_WORKSPACE_ROOT="$PWD"
export WB_RUSTC_CACHE_DIR="$PWD/.agents-workspace/tmp/winterbaume-rustc-cache"
# Build normally — the wrapper is transparent
cargo build -p winterbaume-server --all-featuresDump all cache entries with their stored command lines:
./target/debug/sccache-wrapper --dump-cacheEach entry shows:
- received — the original command line cargo passed to the wrapper
- emitted — the command line actually sent downstream to sccache (with
-C incremental=…stripped) - files — the cached artifacts
Example output:
--- bytes (fd8ac43c4611) ef=-09b1a263836cddec ---
received: rustc --crate-name bytes --edition=2021 …
emitted: rustc --crate-name bytes --edition=2021 …
files: bytes-09b1a263836cddec.d, libbytes-09b1a263836cddec.rmeta, libbytes-09b1a263836cddec.rlib
To see live HIT/MISS messages during a build, set WB_RUSTC_CACHE_DEBUG=1.
When WB_SCCACHE_WRAPPER_SCOREBOARD is set to a writable directory, each cache-miss invocation drops a per-crate JSON file under that directory:
$WB_SCCACHE_WRAPPER_SCOREBOARD/
<crate>-<short_key>.json # one file per cache key
<crate>-<short_key>.json.lock # fslock sibling for serialised RMW
Each file records:
state—buildingwhile a leader is compiling,readyonce it has finished.started_at,last_update,completed_at— Unix timestamps; subtract to get build duration (live or final).command_line— the full rustc argv as cargo passed it.contenders— every wrapper process currently registered for this cache key, with itspid, role (buildingfor the singleflight leader,waitingfor followers),joined_at/last_seenheartbeats, the inferredsessionid (extracted fromCARGO_TARGET_DIR'starget-<session>suffix when the agent cargo wrapper is in use), and aprocess_treechain (caller → cargo → sccache-wrapper).
A background heartbeat thread refreshes last_seen every couple of seconds while the build runs, so external observers can distinguish live builds from abandoned ones. Stale contenders (no heartbeat for ~30s) and ready entries older than five minutes are pruned opportunistically by the next session that touches the file.
Dump a human-readable view of the directory:
WB_SCCACHE_WRAPPER_SCOREBOARD=/path/to/dir ./target/debug/sccache-wrapper --show-scoreboardExample output:
--- aws_config (e1731f628173) ready 6s (done) ---
cmd: /Users/foo/.rustup/toolchains/.../bin/rustc --crate-name aws_config …
contenders: (none)
--- tokio (56f0412baa90) building 4s (live) ---
cmd: /Users/foo/.rustup/toolchains/.../bin/rustc --crate-name tokio …
contenders:
[B] pid=62043 session=claude-60027 sccache-wrapper(62043) → cargo(61174) → zsh(61172) → claude(49917) (last_seen 1s ago)
[W] pid=62120 session=codex-58002 sccache-wrapper(62120) → cargo(60900) → zsh(60898) → codex(57001) (last_seen 2s ago)
$WB_RUSTC_CACHE_DIR/
<first-2-hex>/<sha256>/
manifest # extra-filename + line-oriented file list
args_received # original rustc command line (normalised)
args_emitted # command line sent to sccache (normalised)
stdout # captured stdout (paths use @@WORKSPACE@@ placeholder)
stderr # captured stderr
libfoo-abc123.rlib # hardlinked artifact
libfoo-abc123.rmeta
foo-abc123.d # dep-info with normalised paths
locks/<first-2-hex>/<sha256>.lock # flock files for singleflight
Cargo's metadata hashes (the -abc123 suffix in libfoo-abc123.rlib) are stable across worktrees at the same commit. The only difference in rustc invocations is the absolute workspace path prefix and some display-only flags. By normalising/filtering these before hashing, the wrapper produces identical cache keys regardless of which worktree the build runs in.
When cargo compiles a crate twice in one build (target context + host context for proc-macro support), the two invocations receive different -C metadata= values. Because the wrapper keeps metadata in the cache key, these produce distinct cache entries and do not interfere with each other.
Artifacts are shared via filesystem hardlinks: the cache and target/ directories share the same inodes, so no data is duplicated on disk. The singleflight lock ensures that concurrent builds of the same cache key (e.g. two worktrees compiling bytes simultaneously) do not duplicate work — the first acquires the lock and compiles, the second waits and then reads from cache.