Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Add `--external-only` flag to `cargo chef prepare` to strip all `path = "..."` dependency entries from the recipe (including intra-workspace path deps and any other local path deps), enabling a stable Docker cache layer for third-party dependencies that is not invalidated by workspace-internal changes ([#359](https://github.com/LukeMathWalker/cargo-chef/issues/359))

## [0.1.77](https://github.com/LukeMathWalker/cargo-chef/compare/v0.1.76...v0.1.77) - 2026-03-03

### Fixed
Expand All @@ -18,7 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

- Minimize generated recipe to increase cache hit ratio when `cargo chef prepare` is invoked with a `--bin` option (by [@preiter93](https://github.com/preiter93))
- Publish a prebuilt `cargo-chef` Docker image for every upstream Rust tag.
- Publish a prebuilt `cargo-chef` Docker image for every upstream Rust tag.
- Broaden the set of supported architectures for Docker images to include `i386` and `arm32v7`

### Other
Expand Down
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ assert_cmd = "2"
assert_fs = "1.1.3"
predicates = "3"
rstest = "0.26"
toml = { version = "1.1", features = ["preserve_order"] }

# The profile that 'dist' will build with
[profile.dist]
Expand Down
1 change: 1 addition & 0 deletions examples/workspace-split-cache/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/target
129 changes: 129 additions & 0 deletions examples/workspace-split-cache/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions examples/workspace-split-cache/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[workspace]
members = ["app", "logic", "model"]
resolver = "2"
121 changes: 121 additions & 0 deletions examples/workspace-split-cache/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# syntax=docker/dockerfile:1
#
# Example: Split Docker cache layers for third-party vs workspace-internal deps.
#
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ THE PROBLEM │
# │ │
# │ In a large Cargo workspace the standard cargo-chef pattern has a │
# │ disappointing property: the cook layer (which pre-compiles dependencies) │
# │ is invalidated whenever *any* workspace manifest changes — adding a new │
# │ internal crate, splitting one, adding a new binary target, etc. — even │
# │ when no external (third-party) dependency changed at all. │
# │ │
# │ The root cause is that recipe.json captures both external deps AND the │
# │ intra-workspace path dependencies. Any structural change to the workspace │
# │ changes the recipe, which busts the cook cache, which forces a full │
# │ recompile of *all* third-party crates from source. │
# │ │
# │ In a workspace with many internal packages (e.g. 26 in torrust-tracker), │
# │ this means the cache is effectively always cold: every feature branch or │
# │ PR that touches any manifest invalidates hundreds of MB of compiled crates. │
# └─────────────────────────────────────────────────────────────────────────────┘
#
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ THE SOLUTION │
# │ │
# │ Produce two recipes from the same workspace: │
# │ │
# │ recipe-thirdparty.json — all path deps stripped (external deps only). │
# │ • Changes only when an external dependency is added/removed/updated. │
# │ • The cook_thirdparty layer built from it is a durable cache of │
# │ compiled third-party crates that survives any workspace-internal │
# │ refactor. │
# │ │
# │ recipe.json — the standard full recipe (external + internal structure). │
# │ • Changes when any manifest changes (normal cargo-chef behaviour). │
# │ • The cook layer built on top of cook_thirdparty only needs to │
# │ (re)compile the lightweight workspace stubs; all third-party │
# │ artifacts are already present from the layer below. │
# │ │
# │ Stage graph: │
# │ │
# │ chef ──► planner │
# │ │ │
# │ ├── recipe-thirdparty.json ──► cook_thirdparty (stable) │
# │ │ │ │
# │ └── recipe.json ──────────────────► cook (fast) │
# │ │ │
# │ real source ──────► builder │
# └─────────────────────────────────────────────────────────────────────────────┘
#
# Build:
# docker build -t workspace-split-cache .
#
# Note: this Dockerfile uses the pre-built lukemathwalker/cargo-chef image.
# Adjust the tag to pin a specific cargo-chef + Rust version pair.

FROM lukemathwalker/cargo-chef:latest-rust-1 AS chef
WORKDIR /app

# strip_path_deps.py uses only the Python standard library (no third-party
# packages needed), but python3 itself may not be present on the slim Rust image.
# NOTE: This Dockerfile uses the Python workaround for illustration purposes.
# For the native solution (no Python required), see Dockerfile.native.
RUN apt-get update \
&& apt-get install -y --no-install-recommends python3 \
&& rm -rf /var/lib/apt/lists/*

# ── planner ───────────────────────────────────────────────────────────────────
# Copies the full source tree and generates both recipe files.
# Nothing inside this stage changes the compiled output — it is purely
# analytical (cargo chef prepare only reads manifests, never compiles).
FROM chef AS planner
COPY . .

# Standard full recipe: captures external deps AND workspace-internal structure.
RUN cargo chef prepare --recipe-path recipe.json

# Thirdparty-only recipe: same as above but with all `path = "..."` deps stripped
# from every manifest and all local package entries removed from the lock file.
# This file is stable across any pure workspace-internal change.
RUN python3 strip_path_deps.py recipe.json recipe-thirdparty.json

# ── cook_thirdparty ───────────────────────────────────────────────────────────
# Compiles ONLY the external (third-party) crates.
#
# Cache behaviour:
# • Re-runs when recipe-thirdparty.json changes, i.e. when an external
# dependency is added, removed, or updated.
# • Stays cached for any pure workspace-internal change: new crates, renamed
# crates, restructured path deps, new binary targets, etc.
FROM chef AS cook_thirdparty
COPY --from=planner /app/recipe-thirdparty.json recipe.json
RUN cargo chef cook --release --recipe-path recipe.json

# ── cook ──────────────────────────────────────────────────────────────────────
# Inherits all third-party artifacts from cook_thirdparty.
# Runs cargo chef cook with the full recipe to build the workspace stubs.
# Cargo reuses every external crate already compiled in cook_thirdparty and
# only (re)compiles the lightweight workspace-member stub files.
#
# Cache behaviour:
# • Re-runs when recipe.json changes (any manifest change).
# • But third-party compilation is skipped — only stubs are (re)built, which
# is fast even for large workspaces.
FROM cook_thirdparty AS cook
COPY --from=planner /app/recipe.json recipe.json
RUN cargo chef cook --release --recipe-path recipe.json

# ── builder ───────────────────────────────────────────────────────────────────
# Copies real application source over the skeleton and compiles the actual app.
# Third-party artifacts are reused from cook; only workspace crates are rebuilt.
FROM cook AS builder
COPY . .
RUN cargo build --release --bin app

# ── runtime ───────────────────────────────────────────────────────────────────
FROM debian:bookworm-slim AS runtime
WORKDIR /app
COPY --from=builder /app/target/release/app /usr/local/bin/
ENTRYPOINT ["/usr/local/bin/app"]
67 changes: 67 additions & 0 deletions examples/workspace-split-cache/Dockerfile.native
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# syntax=docker/dockerfile:1
#
# Example: Split Docker cache layers using the native --external-only flag.
#
# This Dockerfile demonstrates the same two-layer cook pattern as Dockerfile
# but uses the native `cargo chef prepare --external-only` flag instead of the
# Python post-processing script.
#
# Requires a locally built cargo-chef binary that includes the --external-only
# flag (not yet released upstream — built from this repo).
#
# Build (two steps):
#
# Step 1 — build cargo-chef from the repo root and tag it:
# docker build -t cargo-chef-local ../../
#
# Step 2 — build this example passing the local image as a named build context:
# docker build \
# --build-context cargo-chef-local=docker-image://cargo-chef-local:latest \
# -f Dockerfile.native \
# -t workspace-split-cache-native \
# .
#
# Once --external-only is released on crates.io the COPY --from=cargo-chef-local
# line below can be replaced with:
# RUN cargo install --locked cargo-chef

FROM rust:1 AS chef
WORKDIR /app
# Install the locally patched cargo-chef binary injected via --build-context.
# See the build instructions at the top of this file.
COPY --from=cargo-chef-local /usr/local/bin/cargo-chef /usr/local/bin/cargo-chef

# ── planner ───────────────────────────────────────────────────────────────────
FROM chef AS planner
COPY . .

# Standard full recipe (external deps + workspace-internal structure).
RUN cargo chef prepare --recipe-path recipe.json

# Third-party-only recipe: path deps and local lock entries stripped natively.
# This is the clean version — no Python script needed.
RUN cargo chef prepare --external-only --recipe-path recipe-thirdparty.json

# ── cook_thirdparty ───────────────────────────────────────────────────────────
# Compiles ONLY external (crates.io) dependencies.
# Invalidated only when an external dep actually changes.
FROM chef AS cook_thirdparty
COPY --from=planner /app/recipe-thirdparty.json recipe.json
RUN cargo chef cook --release --recipe-path recipe.json

# ── cook ──────────────────────────────────────────────────────────────────────
# Inherits third-party artifacts; recompiles only workspace stubs.
FROM cook_thirdparty AS cook
COPY --from=planner /app/recipe.json recipe.json
RUN cargo chef cook --release --recipe-path recipe.json

# ── builder ───────────────────────────────────────────────────────────────────
FROM cook AS builder
COPY . .
RUN cargo build --release --bin app

# ── runtime ───────────────────────────────────────────────────────────────────
FROM debian:bookworm-slim AS runtime
WORKDIR /app
COPY --from=builder /app/target/release/app /usr/local/bin/
ENTRYPOINT ["/usr/local/bin/app"]
Loading