Skip to content
shippy

Paths and the fixed place

If compiler output depends on where your checkout lives, two people never share a result. /home/alice/tempo and /home/bob/src/tempo would build different files from the same code. shippy takes the location out in three ways: remapping (always on), cargo's home at a fixed place (for procedural macros, on wherever the machine allows it) and the fixed place for everything (opt in).

Remapping: always on

The wrapper tells rustc to record three directories under virtual names:

Real directoryRecorded as
The workspace/shippy/ws
The target directory/shippy/target
Cargo's home (~/.cargo)/shippy/cargo

It does this with --remap-path-prefix. Cargo passes your own sources as relative paths, so diagnostics for your code look the same as always. Paths into dependencies show the virtual root. Dep-info files keep real paths, so they are stored with placeholders and filled in again on restore.

One of these directories may be a link, or have one in it, such as ~/.cargo/registry on another disk. Crates compiled from behind the link are recorded under the same names and shared like any other. The build scripts of packages behind the link run on your machine.

Crates that read their own path

Some crates read env!("CARGO_MANIFEST_DIR"), usually to find test fixtures or to include! a generated file. Such a crate may embed the real path. So after compiling one, the wrapper searches its outputs for each root's real path:

  • Path found: the result is pinned to that root and reused only where the root is the same.
  • Path not found: it doesn't matter, and the result is shared normally.

Cargo's home at a fixed place: on by default, for procedural macros

Your dependencies live in cargo's home, and its path is another for every user: /home/alice/.cargo, /home/bob/.cargo, a CI runner's. A dependency that holds its own directory would be pinned to one user's home, and every crate compiled against it would differ for every user. On tempo that is one procedural macro and the 33 crates above it.

So shippy runs a build in a private mount namespace where cargo's home is also at /shippy/at/cargo, and tells the compiler of every procedural macro cargo downloaded that name. A macro that holds its directory holds the same one for everybody, and the second user of a box restores it, and the crates expanded with it.

Nothing else moves:

  • Libraries and programs hold what cargo gives them. Only procedural macros get the fixed name: their code runs inside builds and nowhere else. A library that reads a file of its own directory when your program runs finds it, started through shippy or not. Such a library is compiled once for each cargo home, as without shippy.
  • Your checkout and target directory are where they are. Messages, debug info and panics name the same paths as in any build.
  • Cargo is told nothing. It keeps your real cargo home, so its fingerprints are the ones plain cargo writes.
  • No bubblewrap. The namespace is shippy's own work. Where a user cannot make one (no unprivileged user namespaces, a container, macOS), the build is the build it was, and a macro that holds its directory stays pinned to your cargo home. shippy doctor says which it is.

Plain cargo and your editor

cargo and rust-analyzer can use the target directory shippy built in. Everything is there for them, except a macro that holds /shippy/at/cargo: that path is nowhere outside a build, so cargo compiles the macro again for your own cargo home, and the crates above it. Nothing fails, and nothing goes back and forth: the next shippy build keeps cargo's macro and compiles the crates above it once more.

After that, those crates are tied to your cargo home in that target directory, and no longer restored from what others built. To keep sharing them, give the editor a target directory of its own (rust-analyzer.cargo.targetDir).

Things to know

  • What the build runs is in a user namespace: sudo and other programs that take privileges from their file (ping, snaps, rootless containers) have none there. Builds as root get no namespace unless you ask with SHIPPY_FIXED=home.
  • A macro that writes its own directory into the code it generates leaves /shippy/at/cargo in the crate that uses it. shippy says so when it compiles such a crate, and keeps it on your machine. SHIPPY_FIXED=0 builds it as cargo does.
  • SHIPPY_FIXED=0, or fixed_paths = false, builds with every directory where it is.

The fixed place: opt in

Remapping covers what rustc writes. It can't reach three things:

  • Cargo hashes the absolute path of a path dependency outside the cargo workspace into -C metadata of that crate and every crate above it. On pyramid that is all 21 workspace crates, which then share nothing between two checkouts.
  • Build scripts write absolute paths into what they generate.
  • A crate of your checkout that really embeds its own directory embeds the real one.

The fixed place fixes all three by making every path the same on every machine:

# shippy.toml
[cache]
fixed_paths = true

or SHIPPY_FIXED=1. The front end then runs cargo in a private mount namespace where:

Loading diagram...

It uses bubblewrap without privileges. The three directories are bind mounts, the rest of the file system is as it is, and nothing is copied. rustc is still told to record the virtual roots, so keys from both modes agree wherever remapping alone was enough: a store filled in one mode serves the other.

Things to know

  • It needs bubblewrap and unprivileged user namespaces. Where they are missing, shippy says so and builds as it does without the setting.
  • Plain cargo on the same target directory rebuilds everything, because cargo's fingerprints now name /shippy paths. Give each its own target directory.

Go deeper