chore(oci-build): replace bunyip-api stub-cache hacks with cargo-chef (BUNYIP-389) #383

Merged
nrupard merged 1 commit from chore/BUNYIP-389-cargo-chef-build into main 2026-07-21 19:42:55 +02:00
Owner

Problem

The bunyip-api image build cached dependencies with a hand-rolled stub build: stub sources, rm globs that delete the stub artifacts (one of which, .fingerprint/bunyip-*, misses the underscore lib crates), a find -exec touch mtime hack to force the real rebuild, and a belt-and-suspenders 5 MB "stub build leaked" guard. The guard existed only because the process is non-deterministic (v0.2.0 shipped a 557 KB stub, BUNYIP-58), and even on a passing run the real-sources build redundantly recompiled roughly forty dependency crates the cache layer had already built. Fragile and messy: a random-failure surface baked into every build.

Change

Replace it with cargo-chef, the standard deterministic Rust-in-Docker dependency cache:

  • chef base: rust-builder-musl plus cargo install cargo-chef --locked.
  • planner: cargo chef prepare into recipe.json.
  • builder: cargo chef cook --release --bin bunyip-api --bin bunyip-e2e-bootstrap (dependencies only, scoped to the api binaries, cached unless the recipe changes), then COPY . . and cargo build.

No stub sources, no rm globs, no find -exec touch, and no size guard: cargo-chef stubs the workspace internally and deterministically, so no stub can leak and the guard is unnecessary. The export and runtime stages are unchanged; bunyip-web's Dockerfile was already a clean single build and is untouched. The dunite secret / git-config step is preserved for authed mirrors.

Verified

docker build --target builder builds a real bunyip-api (64 MB) and bunyip-e2e-bootstrap (8.7 MB); the final build compiles only the four workspace crates with no dependency recompile. The cook layer is cached unless the dependency graph changes, so a source-only rebuild is just the roughly four-minute final build.

Follow-up (separate)

check.yml's docker step passes no build cache (no --cache-from / --cache-to), unlike build-api.yml. Adding a buildkit cache there would let the cook layer persist across runs so most check runs hit the fast path instead of a cold cook.

## Problem The bunyip-api image build cached dependencies with a hand-rolled stub build: stub sources, `rm` globs that delete the stub artifacts (one of which, `.fingerprint/bunyip-*`, misses the underscore lib crates), a `find -exec touch` mtime hack to force the real rebuild, and a belt-and-suspenders 5 MB "stub build leaked" guard. The guard existed only because the process is non-deterministic (v0.2.0 shipped a 557 KB stub, BUNYIP-58), and even on a passing run the real-sources build redundantly recompiled roughly forty dependency crates the cache layer had already built. Fragile and messy: a random-failure surface baked into every build. ## Change Replace it with cargo-chef, the standard deterministic Rust-in-Docker dependency cache: - `chef` base: rust-builder-musl plus `cargo install cargo-chef --locked`. - `planner`: `cargo chef prepare` into `recipe.json`. - `builder`: `cargo chef cook --release --bin bunyip-api --bin bunyip-e2e-bootstrap` (dependencies only, scoped to the api binaries, cached unless the recipe changes), then `COPY . .` and `cargo build`. No stub sources, no `rm` globs, no `find -exec touch`, and no size guard: cargo-chef stubs the workspace internally and deterministically, so no stub can leak and the guard is unnecessary. The export and runtime stages are unchanged; bunyip-web's Dockerfile was already a clean single build and is untouched. The dunite secret / git-config step is preserved for authed mirrors. ## Verified `docker build --target builder` builds a real `bunyip-api` (64 MB) and `bunyip-e2e-bootstrap` (8.7 MB); the final build compiles only the four workspace crates with no dependency recompile. The cook layer is cached unless the dependency graph changes, so a source-only rebuild is just the roughly four-minute final build. ## Follow-up (separate) check.yml's docker step passes no build cache (no `--cache-from` / `--cache-to`), unlike build-api.yml. Adding a buildkit cache there would let the cook layer persist across runs so most check runs hit the fast path instead of a cold cook.
chore(oci-build): replace bunyip-api stub-cache hacks with cargo-chef (BUNYIP-389)
All checks were successful
E2E / Playwright against deployment (pull_request) Successful in 35s
Check / fmt + clippy + build + tests (pull_request) Successful in 13m23s
Create release / Create release from merged PR (pull_request) Has been skipped
8a66354c57
The bunyip-api image build cached dependencies with a hand-rolled stub build: it stubbed every crate's source, built the stub, deleted the stub artifacts with globs (one of which, .fingerprint/bunyip-*, missed the underscore lib crates), force-rebuilt the real binary by touching every .rs, and finally a belt-and-suspenders RUN guard failed the build if bunyip-api came out under 5 MB ("stub build leaked"). The guard existed only because the mtime/cache dance was non-deterministic (v0.2.0 shipped a 557 KB stub, BUNYIP-58), and even on a passing run the real-sources build redundantly recompiled roughly forty dependency crates the cache layer had already built. The process was messy and fragile.

Replace it with cargo-chef, the standard deterministic Rust-in-Docker dependency cache: a chef base (rust-builder-musl plus cargo install cargo-chef --locked), a planner stage (cargo chef prepare into recipe.json), and a builder stage that cooks the dependency set then builds the workspace. cook is scoped to the two api binaries (cargo chef cook --release --bin bunyip-api --bin bunyip-e2e-bootstrap) so it builds the final build's dependency set rather than the whole workspace, skipping bunyip-web's deps and the resolver-2 feature-variant duplicates the api image does not need. The dunite secret / git-config step is preserved for authed mirrors.

Timing (local, at the CARGO_BUILD_JOBS=2 default; CI passes the real nproc/2): the cook layer is cached unless the dependency graph changes, so a source-only rebuild is just the final build (about 4 minutes, only the four workspace crates, no dependency recompile), versus the old approach which redundantly recompiled dependencies on every build. A full cold cook is the tradeoff for determinism.

No stub sources, no rm globs, no find -exec touch, and no size guard: cargo-chef stubs the workspace internally and deterministically, so no stub can leak and the guard is unnecessary. The export and runtime stages are unchanged (binary stays at /build/target/release/bunyip-api). bunyip-web's Dockerfile was already a clean single build and is untouched.

Verified: docker build --target builder produces a real bunyip-api (64 MB) and bunyip-e2e-bootstrap (8.7 MB), and the final build compiles only the four workspace crates with no dependency recompile.

#BUNYIP-389

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
nrupard deleted branch chore/BUNYIP-389-cargo-chef-build 2026-07-21 19:42:56 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/bunyip!383
No description provided.