Skip to content

fix: Improve Nix build & CI time - #657

Draft
samuelburnham wants to merge 5 commits into
mainfrom
ci/split-nix-devshell
Draft

samuelburnham wants to merge 5 commits into
mainfrom
ci/split-nix-devshell

Conversation

@samuelburnham

Copy link
Copy Markdown
Member
  • Splits the nix-test and nix-devshell builds back into parallel jobs since unifying them with sticky disks didn't actually save much time. The dev shell does need the nix dev shell packages from the store but cachix downloads are not the bottleneck
  • Enables a common feature set for all Nix package outputs that matches those defined in the crane-built cargoArtifacts (parallel, net) so that dependencies don't have to be recompiled for different packages which is slow in Nix. test-ffi is a local-only feature so it doesn't add significant cost to recompile

The dev-shell step compiles the Lean tree and the Rust workspace in the
checkout, into the sticky disk's .lake and target directories, and never
reads the derivations the sandboxed steps produce. Running it after them
only serialized 6 to 11 minutes of work behind 9 to 21 minutes of Nix
builds: on a kernel-touching PR the job took 33 minutes, and the step did
the same amount of work it did when it was a separate job on a cold
runner. The only shared state was the toolchain closure in /nix/store,
which a fresh runner fetches from Cachix in under a minute.

The sticky disk, sccache, and the RUSTFLAGS pin move with the step,
since only the dev shell's cargo uses them. The merge-queue gate waits
on both jobs.
Cargo compiles a dependency once per unified feature set. The `net`
feature pulls in tokio and iroh, which enable extra features on serde,
libc, once_cell, and other base crates, so a build without `net` sees
different shapes for those crates than the deps-only derivation built
with `parallel,test-ffi,net`, discards them, and recompiles the 57
crates above them, Plonky3 and multi-stark included. `test-ffi`
declares no dependencies and changes nothing.

Give the library and test builds `net` on non-macOS, matching the
lakefile's `ix_rs_net`. The release and CLI builds then request the
same features and collapse into one derivation, so a cache miss pays
for one fewer workspace compile; the test build and nextest reuse the
prebuilt dependencies like clippy already did.

Measured locally with every cache forced to miss, each derivation
built alone: the three package builds took 119 s, 110 s and 111 s
before (two of them recompiling 57 dependencies) and the remaining
two take 110 s each after, recompiling none. Wall time per build is
unchanged because the workspace crates are the critical path; the
saving is the removed build plus the CPU the recompiles consumed.
nextest passes the same 1641 tests under both feature sets.
The executable derivations copied their whole Lake tree into the
output: two copies of each 350 MB static library, the Lean library's
own archive, C and IR intermediates, and a second copy of the binary.
The CLI output came to 2.5 GB and IxTests to 3.8 GB, and most of each
derivation's time went into writing that out and running patchelf over
hundreds of non-ELF files, not into Lake.

Nothing continues a build from an executable, so install only what
the wrapper's LEAN_PATH serves at runtime: the binaries and the olean
parts. The `.olean.server` and `.olean.private` parts stay because the
toolchain's loader reads all three unconditionally for a module
compiled in `module` mode, which is 107 of the CLI's 108 own modules
and 99 of the test modules, and the tests import those through
`get_env!`; removing them fails the import with "failed to open file".
IR files are optional at load time and only serve the interpreter for
declarations without native code, which these binaries all have.

Measured locally, each derivation built alone:

  CLI       150 s, 2.5 GB  ->   65 s, 653 MB
  IxTests   168 s, 3.8 GB  ->   85 s, 1.1 GB

Lake's own build phase is unchanged; the difference is the install and
fixup. The Lean suite passes all 3461 tests.
lean4-nix now installs executables wrapped for standalone use, with
the module files their imports read at runtime under `lib/lean`, so the
hand-rolled olean install and `wrapBin` go. The outputs are
self-contained: they no longer put the 1.2 GB `Ix` tree and every
dependency tree on `LEAN_PATH`, so `nix run .#ix` fetches the toolchain
and one 1.1 GB package instead of a 6 GB closure. The IR files stay
out, as before, since every module these binaries can import is linked
into them.

The `Ix` library, which the executables continue from, is stored as a
zstd archive: 270 MB instead of 1.2 GB. Packing and unpacking cost the
same as copying the tree; the derivation is about 30 s faster only
because Nix has one 270 MB file rather than 1.2 GB across 3400 files to
fix up, scan and hash, and that smaller output is also what Cachix
stores and every job downloads.

The lean4-nix input points at the `install-modes` branch, which adds
`installBin`, `binFiles` and `artifactsFormat`, until that branch is on
`dev`; the URL then goes back to the default branch and the lock is
bumped again.
@samuelburnham
samuelburnham force-pushed the ci/split-nix-devshell branch from d7564a4 to 9b62a57 Compare October 2, 2026 16:05
lean4-nix now installs only the olean parts beside a binary unless a
package asks for the IR files too, which is the set ix had been
selecting by hand; the executable derivations are unchanged by
dropping the override. The lock moves to the lean4-nix commit that
changes the default.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant