The big picture
shippy answers one question for every crate in your build: have I seen a compilation with exactly these inputs before? If yes, it restores the output. If no, it runs the real rustc and remembers the answer. This page explains how it asks that question quickly and safely.
Three parts
| Part | What it is | Where it runs |
|---|---|---|
shippy | The front end you type instead of cargo. It starts cargo, fetches results ahead of time and uploads what was built. | Your machine |
shippy-rustc | The compiler wrapper. Cargo calls it for every crate instead of rustc. Same binary as shippy. | Your machine, once per crate |
shippyd | The server, called the box. It keeps a shared store, rebuilds untrusted uploads in a sandbox, and can compile for you. | A shared machine |
A recipe describes one compilation
Before deciding anything, the wrapper writes down a recipe: a description of one rustc call by content, not by where files live.
- The toolchain: the exact text of
rustc -vV. - The arguments, with machine-specific directories swapped for placeholders like
<<WS>>(workspace) and<<TARGET>>(target directory). - The environment a crate can read without the compiler noticing, such as
CARGO_PKG_*,CARGO_MANIFEST_DIRandOUT_DIR. - Every compiled file it can read, by digest: its dependencies'
.rliband.rmeta, proc macros, native libraries. - Its manifest: the source files it opened and the
env!variables it read. rustc reports these after it runs. For what is linked on this machine, also the files the linker read outside the build, which the linker's command line shows: libraries, the files a linker script names in place of one, and files such as a version script.
Because paths are replaced by placeholders, two people with checkouts in different directories write the same recipe for the same crate.
Three keys come out of it
Why two steps? A crate's source files are only known after rustc has read them. So shippy uses the pre-key to find the manifests that earlier compilations left, checks each one against your files and environment as they are now, and one that matches gives the action key. The result under that key is taken when its own dep-info names no variable that has another value on your machine. No compiler runs just to find out what a crate reads. (ccache's direct mode and Shake's constructive traces work the same way.)
One crate, start to finish
Why it is fast
- Digests live on the files. A file's BLAKE3 digest is cached in an extended attribute, guarded by its size and modification time. Describing a crate with 800 dependencies is
statandgetxattrcalls, not gigabytes of hashing. - Storing costs a link, not a copy. After a miss, the new
.rliband.rmetaenter the store as hard links to the files rustc just wrote. - Restoring links a library and copies the rest. A restored crate's
.rlibis the store's own file under a second name: rustc replaces that file and never writes into it. Its.rmetais a copy, as new as the restore, which is the file cargo dates the crate by. Executables, shared objects and build scripts' output directories are a reflink where the filesystem supports it, and an in-kernel copy elsewhere. This is why a store and target directory in memory (tmpfs) or on btrfs/XFS are faster than ext4. - Cargo starts a wrapper without copying itself. Cargo forks for every compiler it starts, to keep its jobserver's pipe open in it. shippy makes that jobserver as a fifo and gives it to cargo by name, so cargo starts its compilers with
posix_spawn. In a build that restores everything, the forks were most of what cargo did. - Messages stream. On a miss, rustc's messages go to cargo line by line, so cargo starts dependents as soon as metadata is written.
- Colour is not in the key. rustc always produces coloured diagnostics and the wrapper strips them when cargo wants plain ones, so a result built in CI serves a terminal and the reverse.
Rules the wrapper never breaks
- A key covers everything the compilation can read. When in doubt, shippy doesn't cache that compilation.
- No absolute path of your machine reaches a key, a stored dep-info file or a stored diagnostic.
- Whatever a server answers is checked before it is used.
What it can't do yet
After a real edit to a function body, every crate that depends on the edited one still recompiles. rustc writes a new crate hash into the edited crate's metadata on every change, and dependents embed it. Skipping them needs compiler support; it is on the roadmap. Until then, tuned settings are what speed up the edit loop.
Next
- Trust: how untrusted machines can add to a shared cache safely.
- Paths: how two checkouts in different directories share results.
- What is cached: libraries, executables and build scripts.