Build work, accounted for.
A Rust-first build system with a machine-wide, content-addressed artifact cache
and explicit input tracking. It reads your existing Cargo.toml and Cargo.lock.
Quick start · How it works · Architecture · Protocol · Limitations · Roadmap
Mitts plans the workspace itself and invokes rustc directly. Every
compile action gets a key derived from all of its inputs. The output is stored
once in a machine-wide immutable store and restored into every workspace that
needs it, across projects, worktrees and branches.
If an input cannot be accounted for, the action is rebuilt, not guessed.
Cargo's build cache lives in each workspace's target/ directory. Anything
that creates a new target/ starts from zero:
- a second checkout or
git worktreeof the same repository, - switching to a branch that changes
Cargo.lock, then switching back, - another project that depends on the same crates with the same features,
- parallel coding agents, each working in its own worktree.
The same serde, syn or tokio gets compiled again and again, and every copy
takes disk space.
The usual workarounds have sharp edges:
- A shared
CARGO_TARGET_DIRserializes builds behind one lock, and projects with different feature sets overwrite each other's artifacts. - A compiler wrapper such as sccache sees one
rustccall at a time. Its key depends on absolute paths unless they are normalized, and crates that invoke the linker (bin,proc-macro,dylib) are not cached. Cargo still plans the build and runs every build script.
Mitts takes a different route: it replaces Cargo's executor, not the compiler. It sees the whole unit graph, so it can key each unit by its dependencies' keys, cache linking units and build script runs, and let several builds share one store safely.
- Reuse across workspaces. One store per machine. A dependency compiled for one checkout is restored into the next one with the same inputs.
- Complete keys. Toolchain, target, profile, flags, features, source contents, dependency keys and tracked environment. Unknown inputs mean a rebuild.
- Cargo manifests stay the contract. No new manifest, no second lockfile. This repository's build, test, lint, coverage and release-source workflows run through Mitts; they do not invoke the Cargo CLI. External Cargo interoperability remains available to consumers.
- Linking units and build scripts are cached. Binaries, tests, proc macros
and build-script executions are keyed and stored, not only
rlibs. - Concurrent builds cooperate. Per-key locks: two terminals or worktrees that need the same dependency compile it once. One jobserver bounds parallelism for all Mitts processes on the machine.
- Safe sharing. Store objects are immutable: published atomically, stored
read-only and restored by reflink or copy.
MITTS_VERIFY_RESTORE=1re-hashes every object before it is restored (the default trusts the store, which saves one read of every restored byte). Where the filesystem cannot reflink, a restore hardlinks the read-only store object intotarget/instead of copying its bytes: the shared inode stays0o444(0o555for executables), the target index rejects a link that gained write bits, and Mitts only ever replaces workspace files by rename. Never a writable link to a store object.MITTS_STORE_HARDLINKS=0keeps private copies for workspaces that other tools write into in place. - Zero-copy workspace staging. Intra-workspace library aliases, secondary binaries,
and atomic unit staging use intra-workspace hardlinks and rename promotion without
copying bytes. When
MITTS_CACHE_DIRand the workspacetarget/reside on the same filesystem volume, staging promotions operate without copying bytes; across separate mounts, they transparently fall back to safe copying. - A no-op fast path. An unchanged workspace skips planning and keying.
- One CLI.
build,run,check,clippy,fmt,test(with doctests),coverage,explain,fetch,gc,clean.
Install the latest signed native release:
curl -fsSL https://mittsbuild.com/install.sh | shTo build the source checkout, first install the latest released Mitts, then use Mitts to build the checkout:
git clone https://github.com/alex09x/mitts
cd mitts
mitts build --release
MITTS_JOBS=2 mitts build # limit concurrent native build units
install -m755 target/release/mitts "$HOME/.cargo/bin/mitts"Then run it inside any Cargo workspace:
mitts build # compile into target/debug/
mitts build --release
mitts run # build and run a binary
mitts test # unit, integration and doc tests
mitts test --tests -p foo # Cargo-style target selection
mitts test -c # every test in its own process, all the cores (-c=8: 8): docs/test-concurrency.md
mitts test --all-targets --no-run # compile tests, examples and benchmarks
mitts bench # build benchmark targets (bench profile) and run them with --bench
mitts bench --bench speed -- --save-baseline main
# The cache stays under MITTS_CACHE_MAX_BYTES (default 100G) by itself: docs/cache-limits.md
mitts llvm-cov --ignore-filename-regex 'tests/' --fail-under-lines 80
mitts check | clippy | fmt | coverage
mitts explain # the plan, units and why each is cached or not
mitts fetch # prefetch dependencies into the source store
mitts package # create and verify a source archive
mitts publish --dry-run # prepare publication without uploading
mitts gc # prune the shared store within quotas
mitts clean # remove this workspace's Mitts artifactsFor Git projects, packaging currently requires a clean repository with the package at its root.
Manifest include patterns can select ignored files; nested workspace packages
and symlinked directories are not yet supported. mitts publish targets crates.io and reads
the token from CARGO_REGISTRY_TOKEN.
Mitts writes build outputs to Cargo's usual target/debug/ and target/release/
directories. CARGO_TARGET_DIR and --target-dir select a different output root.
Its shared cache is separate
(--cache-dir, else $MITTS_CACHE_DIR, else $XDG_CACHE_HOME/mitts, else
~/.cache/mitts). The artifact store, action cache, locks, jobserver, source
store, git mirrors and index cache all live under that one root.
Git dependencies, public or private, are cloned by Mitts itself with the system
git, so your SSH keys, agent, credential helpers and GIT_SSH_COMMAND apply
exactly as on the command line; prompts are disabled, so a missing credential
fails instead of hanging. Nothing goes through Cargo. A checkout Cargo already
made under ~/.cargo/git/checkouts is reused instead of cloned again.
Every other way Cargo gets a crate works the same way, from the same config:
- Alternative registries:
[registries.<name>] index = "sparse+https://…"andregistry = "<name>"on the dependency. Tokens come from Cargo's credential providers, in Cargo's order:cargo:token(the environment andcredentials.toml),cargo:macos-keychain,cargo:libsecret,cargo:token-from-stdout <command>, and plugins such ascargo-credential-1passwordthrough the credential provider protocol, configured by[registry] global-credential-providers,[registries.<name>] credential-providerand[credential-alias]. A registry whoseconfig.jsonsaysauth-requiredfails with a clear message when no provider has a token. Git-index registries are refused: use the sparse protocol. - Mirrors:
[source.crates-io] replace-with = "<name>"with[source.<name>] registry = "sparse+https://…". Bytes come from the mirror,Cargo.lockkeeps naming crates.io, exactly as Cargo writes it. - Vendored sources:
[source.vendored-sources] directory = "vendor"fromcargo vendor. Packages are checked against their.cargo-checksum.jsonand copied into the store; nothing touches the network, git packages included. Without a lockfile the vendor directory serves as the index. [patch]:[patch.crates-io] foo = { path = … }or{ git = … }replaces the registry package; without a lockfile the replacement is resolved and pinned, and it has to satisfy the requirement, as with Cargo.
[env] in a Cargo config is applied to rustc, build scripts and the programs
mitts run and mitts test start, with Cargo's force and relative rules;
the config files are inputs, so a changed value rebuilds what read it.
Without a Cargo.lock, Mitts resolves one the way Cargo does: highest
non-yanked version per requirement, features flowing along the edges, an
optional dependency included only when a feature turns it on, one version per
compatibility range. The result is a lockfile Cargo accepts as its own.
Cargo.toml + Cargo.lock
│
▼
resolve ─── reuse Cargo.lock, or resolve against the crates.io sparse index
│
▼
fetch ───── download .crate archives in parallel, verify, unpack once per machine;
│ clone git dependencies with the system git at the locked commit
│
▼
plan ────── compilation units: lib · bin · test · build script · proc macro
│ with features, targets and dependency edges (Cargo's resolver rules)
▼
key ─────── SHA-256 over toolchain, target, profile, flags, features,
│ source contents, dependency keys and tracked environment
▼
execute ─── parallel DAG of direct rustc calls, pipelined on .rmeta
hit → restore from the store (reflink, read-only link or copy)
miss → lock the key, compile, publish atomically to the store
A unit's key includes its dependencies' keys, not their output bytes, so a
change anywhere below a unit changes its key. Build scripts run in a staging
directory; their directives (rustc-cfg, rustc-link-*, rustc-env, …) enter
the key of the unit they configure, and their rerun-if-* declarations decide
when a cached run can be reused.
After a successful build Mitts records a snapshot of the workspace. If nothing in it changed, the next build skips straight to the result. Any mismatch falls back to the full keyed build, so the fast path never skips work the protocol would do.
The full protocol — key derivation, locking, build scripts, the fast path, store layout and garbage collection — is in docs/architecture.md.
Mitts is a language-neutral core with language adapters on top. The core knows nothing about Cargo; the Rust adapter turns a Cargo workspace into keyed actions.
┌──────────────────────────────────────────────┐
mitts CLI ───▶ │ Rust adapter │
(src/cli) │ manifest · resolver · fetcher · features │
│ planner · keys · DAG runner · build scripts │
├──────────────────────────────────────────────┤
│ C/C++ adapter (experimental, Mitts.toml) │
└──────────────────────┬───────────────────────┘
│ actions + keys
┌──────────────────────▼───────────────────────┐
│ Core │
│ content-addressed store · action cache │
│ key locks · jobserver · fast path · GC │
└──────────────────────────────────────────────┘
| Path | What lives there |
|---|---|
src/cli/ |
Command handlers: build, test, clippy, explain, … |
src/adapter/rust/ |
Cargo semantics: manifests, lockfile, sparse index, feature resolution, unit planning, action keys, the DAG runner, build scripts, doctests |
src/adapter/c/ |
Experimental C/C++ adapter on the same store |
src/core/ |
Language-neutral store (cas.rs), action cache, locks, jobserver, fast path (build_lock*.rs), GC, reflinks |
docs/ |
Protocol, design decisions, scenarios |
tests/ |
Integration tests, including Cargo-parity and cache-invalidation tests |
scripts/ |
Benchmark and audit harness |
fixtures/ |
Minimal Rust and C projects used by tests |
A cache hit requires every input below to match. If Mitts cannot see an input, it does not reuse the result.
| Input | Tracked as |
|---|---|
| Toolchain | rustc -vV output |
| Target and profile | target triple, profile settings, RUSTFLAGS and Cargo config rustflags |
| Features | the unit's resolved feature set (resolver 1 or 2, as Cargo would) |
| Sources | content hashes of the package files and every file rustc reports reading (include_str!, include_bytes!, modules) |
| Dependencies | the action keys of all dependency units |
| Environment | Relevant CARGO_* and RUST_* settings, and variables read through env! / option_env!, with <unset> recorded explicitly |
| Cargo configuration | the .cargo/config(.toml) files Cargo would read |
| Build scripts | their directives, OUT_DIR, and the content of rerun-if-changed paths and values of rerun-if-env-changed variables |
Configured rustc-wrapper and rustc-workspace-wrapper programs are invoked for
compilation. Because an arbitrary wrapper can depend on untracked inputs, Mitts
disables action-cache and build-script-cache reuse while either wrapper is active.
This can make those builds slower.
Some proc macros read files from your package while they expand: sqlx checks
queries against .sqlx/, askama compiles templates/, rust-embed and
include_dir embed whole directories. rustc only learns about such reads when
the macro registers them, so declare these crates in the root Cargo.toml:
[workspace.metadata.mitts] # [package.metadata.mitts] in a single crate
impure-proc-macros = ["sqlx", "askama", "rust-embed"]Crates that depend on a listed macro are keyed on every file in their package
and on their absolute path: an edited template rebuilds them, and another
checkout never reuses them. Every other macro is treated as pure, and its
dependents are shared across worktrees. Name the crate you depend on; the
macro crate it pulls in (sqlx-macros) works too.
A qualitative comparison of how each tool reuses work. It is not a performance claim.
| Cargo | Cargo + sccache | Mitts | |
|---|---|---|---|
Reads Cargo.toml / Cargo.lock as is |
yes | yes | yes |
| Reuse across workspaces and worktrees | no (per target/) |
yes, per rustc call |
yes, per unit |
| Key depends on the checkout path | — | yes, unless normalized | no for registry crates; for workspace crates only when not eligible |
Caches bin, test and proc-macro units |
— | no | yes |
| Caches build-script runs | no | no | yes |
| Concurrent builds of one dependency | wait on the target/ lock |
both may compile on a miss | one compiles, the other reuses |
| Remote / team cache | no | yes (S3, GCS, Redis, …) | planned |
| Windows | yes | yes | no |
Bazel and Buck2 solve a larger problem — hermetic, multi-language builds with
remote execution — but require their own build files instead of Cargo.toml.
Mitts is pre-release. Know these before you rely on it:
- Linux and macOS only. Locks and the jobserver are POSIX.
- Host target only. Cross-compilation is not supported yet.
- No remote cache yet. The store is per machine.
- Sparse registries only. A registry served as a git index (
registry+…other than crates.io, or[source] registry = "https://…"withoutsparse+) andlocal-registrysources are refused with advice. - Credentials are read, never written.
cargo loginstays Cargo's job;cargo:wincredis Windows-only and skipped. - Some units are never cached: crates of type
cdylib,dyliborstaticlib, and crates whoseenv!reads cannot be tracked. They always build; they are reported bymitts explain. - Workspace crates across checkouts. Your own crates are reused across
worktrees only when their key can be made path-independent (no
CARGO_MANIFEST_DIRreads, no build script, no declared impure proc macro). Otherwise the key includes the absolute path. On macOS the linker's debug stabs are written relative to the workspace (-oso_prefix), so linked binaries are shared too; start the debugger in the workspace root for it to find the object files. - Macros that read your files are yours to declare. A proc macro is treated as pure unless the project lists it (see above). A listed macro's dependents follow every file in their package; an unlisted macro that reads a file without registering it with rustc can produce a stale result, exactly as with Cargo.
- Build scripts are not sandboxed. A script that reads files or variables it
did not declare with
rerun-if-*can produce a stale result — exactly as with Cargo. - Unsupported Cargo features fail loudly. For example
--profile, globs with wildcards in intermediate directories inworkspace.members. Mitts reports them instead of guessing. - Same-filesystem zero-copy. Zero-copy unit promotion from staging requires
MITTS_CACHE_DIRand the workspacetarget/directory to reside on the same filesystem mount. Crossing filesystem boundaries safely falls back to standard copy.
Don't use Mitts yet if you need Windows, cross-compilation, a team cache, or a stable release. Cargo, optionally with sccache, is the safe choice there.
Performance numbers are published only with a reproducible methodology: fixed hardware, cold, warm and changed-input runs, several worktrees, the stronger of plain Cargo and sccache as the baseline, and parity checks on diagnostics, exit codes and test results. The study is in progress (ROADMAP M8). Until it is published, Mitts makes no speedup or disk-saving claims.
| Area | State |
|---|---|
Cargo.toml / Cargo.lock reading, sparse-index resolution |
done |
| Feature resolution (resolver 1 and 2) | done |
| Parallel fetcher and global source store | done |
Native DAG runner, pipelined rustc, jobserver |
done |
| Content-addressed store and action cache | done |
| Build script execution and caching | done |
| No-op fast path | done |
check, clippy, fmt, test (incl. doctests), coverage |
done |
| Conservative GC with quotas | done |
| C/C++ adapter on the same store | experimental |
| Path-independent keys for workspace crates | partial |
| Reproducible benchmark study | in progress |
| Remote / team cache | planned |
| Windows, cross-compilation | not supported |
Milestones and evidence: ROADMAP.md.
Does Mitts replace Cargo? Mitts reads the existing Cargo manifests and lockfiles and provides the Rust build/test workflow for this project. Its own CI and release workflows use Mitts without invoking Cargo. Cargo remains available to external users who need it.
Can I use it in CI? Yes. This repository's CI installs the latest stable Mitts, then uses it for native builds, tests, coverage, formatting and Clippy. The shared artifact store is local to each runner; a cross-machine remote cache is not available yet.
Will it fight with rust-analyzer or existing Cargo outputs?
Mitts uses the same default output paths as Cargo. Its shared artifact store stays
separate, and mitts clean removes only indexed Mitts outputs that still match
their recorded content; it preserves unrelated or replaced Cargo files.
If Cargo holds a profile's build-directory lock, cleanup is skipped until that
build finishes.
Where is the cache and how do I clear it?
--cache-dir, else $MITTS_CACHE_DIR, $XDG_CACHE_HOME/mitts or
~/.cache/mitts; everything Mitts caches lives under that one root. Use
mitts gc to prune within quotas; deleting the directory is also safe when no
build is running.
Why was a unit not cached?
Run mitts explain. It lists every unit and the reason it is or is not
eligible for the cache.
- Keep build and cache orchestration language-neutral. Language semantics live in adapters; Rust is the first one.
- A cache hit needs complete, tracked inputs. Unknown inputs are treated as changed.
- Shared artifacts are immutable and verified before use. Mutable state stays in each workspace. No hardlinks to writable files, no timestamp tricks.
- Cleanup never touches artifacts of an active build.
Contributor and agent rules: AGENTS.md. Architecture decisions: docs/decisions.
Alexander Panasenko · alex@prod.codes · mittsbuild.com
Dual-licensed under MIT OR Apache-2.0, at your option.