Skip to content
shippy

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

PartWhat it isWhere it runs
shippyThe front end you type instead of cargo. It starts cargo, fetches results ahead of time and uploads what was built.Your machine
shippy-rustcThe compiler wrapper. Cargo calls it for every crate instead of rustc. Same binary as shippy.Your machine, once per crate
shippydThe 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_DIR and OUT_DIR.
  • Every compiled file it can read, by digest: its dependencies' .rlib and .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

pre-key
known before rustc runs
Hash of the toolchain, arguments, environment and compiled inputs.
manifest digest
known after one run
Hash of the source files and env! variables, with their content.
action key
when a manifest matches
Hash of the pre-key and the manifest. This is the result's address.

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.)

Loading diagram...

One crate, start to finish

1 · describe
Write the recipe
Placeholders for paths, digests for files. Fast, because digests are cached on the files.
2 · local
Look in ~/.cache/shippy
Most rebuilds after a branch switch or an undo end here.
3 · remote
Ask the box
Usually already downloaded by the prefetch before cargo started.
4 · miss
Run rustc
Messages stream to cargo line by line, so dependents start early.
5 · keep
Store and upload
A hard link into the store, then an upload off cargo's critical path.

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 stat and getxattr calls, not gigabytes of hashing.
  • Storing costs a link, not a copy. After a miss, the new .rlib and .rmeta enter the store as hard links to the files rustc just wrote.
  • Restoring links a library and copies the rest. A restored crate's .rlib is the store's own file under a second name: rustc replaces that file and never writes into it. Its .rmeta is 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.

Go deeper