Build scripts
shippy caches the runs of build scripts (build.rs) like compilations, and the box runs them in its sandbox so one person's run can be everyone's. Read this when a cold build is slow even with every crate restored, or when you need to decide which of your own packages' scripts may be shared.
Why it matters
A cold build with every compilation restored can still be minutes of build scripts: C and C++ compiled by cc, cmake and make (rocksdb, jemalloc, aws-lc), and code generated into OUT_DIR.
On a small machine those scripts are most of a cold build. A client of a warm box that compiled rocksdb and jemalloc itself took 332 s. The same build with their runs restored took 136 s.
The runs of downloaded packages' scripts are cached with no extra switch. Without a remote they are kept in this machine's store, for a second checkout or a cleaned target directory. With one they are shared:
shippy setup --remote http://box:7447 --token-file token.txt
shippy build # dependency build scripts are restored from the box when it has themWhat a run's key covers
A run is one execution of a build script. Like a compilation, it has a recipe and a key. The key covers what cargo's own rules say a run depends on:
| What | Examples |
|---|---|
| The script's executable | by content |
| The package directory | every file by content, which are executable, its links |
| What dependencies' scripts export | files in the directories DEP_* variables name |
What cargo and cc give it | TARGET, PROFILE, OPT_LEVEL, CARGO_FEATURE_*, CARGO_CFG_*, CC, CFLAGS, SOURCE_DATE_EPOCH |
| The Rust compiler | rustc -vV |
| The C toolchain | first line of cc --version and c++ --version, and the C library's version |
| What the run itself named | variables from rerun-if-env-changed, files from rerun-if-changed outside what the rows above hold |
The first six rows are known before the script runs. They form the pre-key, so a package is looked up without running anything. The last row is learned from a run and picks among runs of one pre-key.
PATH and HOME are each machine's own. A script that names one depends on its machine, and its run is not shared. NUM_JOBS and the jobserver are in no key.
The result is OUT_DIR packed as one tree, plus what the script printed, which is replayed to cargo.
Which scripts are cached
SHIPPY_SCRIPTS=deps shippy buildSHIPPY_SCRIPTS | Whose runs are cached |
|---|---|
0 | Nobody's. Cargo runs every script, as without shippy. |
deps | Packages from a registry or a git repository. The default, with a remote or without one. |
1 | Every package's. |
In shippy.toml, build_scripts = true under [cache] is 1 and false is 0.
Remote execution changes who runs a script nobody has run yet, not whose runs are cached:
| The machine | The box has a public run | The box has none |
|---|---|---|
| Uses the cache only | restored | runs here, uploaded as a claim |
| Has remote execution on | restored | the box runs it on request |
Shared and local runs
A run is shared (other machines may get it) or local (kept in this machine's store only). The two never have one key.
| The script of | Its run |
|---|---|
| A registry or git dependency | shared: the package directory is the same for everyone |
| A workspace package | local, unless named in share_scripts |
A package in local_scripts | local |
| A path dependency outside the workspace | local |
A shared run becomes local for this machine when the recipe cannot cover what it reads, for example:
- the script asks about its git repository (
.git/,rev-parse); - the package holds a link that leads out of it;
- the package is behind a link, such as
~/.cargo/registryon another disk; - the package and its exports exceed
SHIPPY_SCRIPT_MAX_BYTES(1 GiB) orSHIPPY_SCRIPT_MAX_FILES(50,000); - the run names
PATHorHOME, or arerun-if-changedpath the inputs do not hold.
To see what happened to each script, look at <store>/sessions/<session>.scripts (one line per script: package, shared or local, restored, run-on-the-box or ran-here, and why), or set SHIPPY_DEBUG=1.
How a shared run becomes public
Only the box's own run is ever public. There are two ways to get there.
- A claim. The script ran on a client and the run is uploaded with the files the box lacks. The box runs the recipe in its sandbox and compares. When they agree, the box's run is published, never the claim's bytes. When they differ, or the script fails in the sandbox, nothing is published and the reason is recorded.
- A request. With remote execution on, a missed shared run goes to
POST /v1/exec. The box runs the script, publishes the run, and sends back the packedOUT_DIR, which is restored like a hit. If anything goes wrong, the script runs here.
Either way a run is published under the variables its script named with rerun-if-env-changed and the workspace files it named with rerun-if-changed, and nothing else its sender declared.
The claim path is the safer of the two: the box compares two runs before publishing. A request has no run to compare with.
When a claim and the sandbox "agree"
Two honest runs on two machines differ: compiled code holds the directory it was compiled in, build logs hold times and host names. So the comparison checks that:
- the directives are the same set, warnings aside;
- every file that is not compiled code (objects, archives, shared objects) and not build-system bookkeeping (
CMakeFiles,config.log,Makefile,*.d) has the same content in both, and neither run has a file the other lacks.
Generated source is what this protects.
What the sandbox holds
No network, the system call filter, the limits. Inside: the script's executable and the package directory (read-only), the declared files of other packages and the target directory, an empty OUT_DIR, the run's own /tmp as HOME, the toolchain whose rustc -vV matches the recipe, the system's C toolchain, and the recipe's variables. Nothing else. A script that needs what its key does not name fails there, and runs on each machine instead.
What a published run may tell cargo
A restored run's directives act on the machine that restores it, so a run is published only when every directive means the same on any machine:
| Directive | Shared when |
|---|---|
rustc-link-search | the directory is in OUT_DIR or the package |
rustc-link-lib | the library is in such a directory, or comes with the C toolchain: c, m, dl, rt, pthread, util, resolv, gcc, gcc_s, atomic, stdc++, c++, c++abi; for an Apple target also System, objc, iconv and the frameworks CoreFoundation, CoreServices, Foundation, IOKit, Security, SystemConfiguration |
rustc-env, rustc-cfg, rustc-link-arg* | it names no absolute path outside the build's directories, also not as //usr/lib, as a path of the toolchain's directory, or as one that climbs out of the package and OUT_DIR with ..; and a linker argument names no library but such a one |
rerun-if-env-changed | the variable is not PATH or HOME |
rustc-flags, error | never |
warning | always |
A script that links a system library (rustc-link-lib=ssl after a pkg-config probe) is the machine's own. Such scripts are usually quick and run on each machine, in every new target directory: the same rule decides whether a machine restores a run it made itself. One difference: a machine's own run may name a file outside the build with rerun-if-changed, as bindgen names the system headers it read. The run's key holds that file's content, so the run is restored while the header is the same and the script runs again once it has changed.
What is never shared
- A workspace package's script, unless named in
share_scripts. - The script of a git dependency that is one package of a larger repository, unless a trusted principal ran it. It can read the repository around its package, which no key covers and the sandbox does not hold.
- A script that asks about its git repository (
vergen,built,shadow-rs,git describe). The sandbox has no repository. - A script whose run names its machine: a system library, an absolute path,
PATH. - A script whose claimed run differs from the sandbox's (it stamps the time, or reads a variable it does not name).
- A script that needs the network or a tool the box lacks.
Settings
On the machine that builds:
| Setting | Default | What it does |
|---|---|---|
SHIPPY_SCRIPTS, or build_scripts under [cache] | deps | Whose runs are cached (above). |
share_scripts = ["a"] under [cache] (SHIPPY_SCRIPTS_SHARE=a) | none | Workspace packages whose scripts read only their own package and what they declare. |
local_scripts = ["c"] under [cache] (SHIPPY_SCRIPTS_LOCAL=c) | none | Packages whose scripts always run here. |
SHIPPY_SCRIPT_MAX_BYTES, SHIPPY_SCRIPT_MAX_FILES | 1 GiB, 50,000 | The most a shared run declares. |
SHIPPY_SCRIPT_TIMEOUT | 1800 | Seconds a run on the box may take, waiting included, before the script runs here. |
On the box (in the [env] table of shippyd.toml):
| Variable | Default | What it does |
|---|---|---|
SHIPPYD_SCRIPTS | 1 | 0 runs no build scripts: claims stay claims, requests are refused. |
SHIPPYD_SCRIPT_TIMEOUT | 1800 | Seconds one script may run. |
SHIPPYD_SCRIPT_JOBS | half of shippyd's CPUs, 2 to 32 | NUM_JOBS for the script, and its CPUs when runs have cgroups. |
SHIPPYD_SCRIPT_OUTPUT_BYTES | 8 GiB | What one script may leave in OUT_DIR. |
SHIPPYD_SCRIPT_OUTPUT_FILES | 200,000 | How many files and directories. |
Go deeper
- docs/build-scripts.md
- docs/operations.md: Build scripts, for tuning
SHIPPYD_SCRIPT_JOBSon a busy box - docs/remote-exec.md