Skip to content

Feature request: allow source-keyed target cache when caching workspace crates #348

Description

@garysassano

Summary

rust-cache works very well for dependency caching, but I ran into a repeated rebuild case when using cache-workspace-crates: true for a Rust workspace with multiple binary targets.

The short version: the target cache key appears to intentionally ignore workspace source contents. That is normally sensible, but when workspace crate artifacts are cached, an exact cache hit can restore stale workspace artifacts and then skip saving the rebuilt target state. The next run restores the same stale target cache again, so the same workspace crates rebuild repeatedly.

It would be useful to have an option to make the target cache key include a user-provided source fingerprint, while keeping Cargo home/dependency cache behavior unchanged.

Use Case

I have a Rust workspace with multiple binary targets built in a CI matrix. Each matrix job uses a separate CARGO_TARGET_DIR.

The goal is for unchanged matrix jobs to be true Cargo no-ops across repeated CI runs, including local workspace crates.

The workflow shape is roughly:

- uses: actions/checkout@v6

- uses: Swatinem/rust-cache@v2
  with:
    workspaces: ./app -> ../../target-for-one-binary
    cache-targets: true
    cache-all-crates: true
    cache-workspace-crates: true
    shared-key: one-binary-target

- run: cargo build --release --bin one-binary
  env:
    CARGO_TARGET_DIR: /tmp/target-for-one-binary

Observed Behavior

Some matrix jobs repeatedly rebuilt local workspace crates even though the cache restore was a full match and checkout had preserved source mtimes.

The repeated rebuilds were most visible for workspace crates that have build scripts or generated code, for example:

  • A parser crate whose build.rs generates parser code.
  • A generated-data crate whose build.rs reads a JSON file and writes generated Rust into OUT_DIR.
  • Binary crates depending on those local generated-code crates.

I added explicit build-script invalidation hints such as:

println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed=path/to/input-file");

That was good for correctness, but it did not solve the repeated CI rebuild by itself.

What Appears To Be Happening

From reading the action behavior and CI logs, the issue appears to be:

  1. rust-cache restores an exact cache key.
  2. Cargo detects some workspace artifacts are stale and rebuilds them.
  3. Because the restored key was an exact hit, rust-cache post step reports Cache up-to-date.
  4. The rebuilt workspace target state is not saved under a new key.
  5. The next run restores the same stale target state and rebuilds the same workspace crates again.

This makes sense for dependency-only caching, but it is surprising when cache-workspace-crates: true is enabled and the desired behavior is to preserve workspace crate artifacts too.

Workaround That Fixed It

I split the cache responsibilities:

- uses: Swatinem/rust-cache@v2
  with:
    workspaces: ./app -> ../../target-for-one-binary
    cache-targets: false
    cache-all-crates: true
    shared-key: one-binary-cargo-home

- name: Compute source key
  id: source-key
  run: |
    hash="$({ git rev-parse HEAD:app; git ls-files -s app; } | sha256sum | cut -d ' ' -f1)"
    echo "hash=$hash" >> "$GITHUB_OUTPUT"

- uses: actions/cache@v5
  with:
    path: /tmp/target-for-one-binary
    key: target-v1-${{ runner.os }}-${{ runner.arch }}-one-binary-${{ steps.source-key.outputs.hash }}
    restore-keys: |
      target-v1-${{ runner.os }}-${{ runner.arch }}-one-binary-

- run: cargo build --release --bin one-binary
  env:
    CARGO_TARGET_DIR: /tmp/target-for-one-binary

The important detail was ordering: the target cache must be restored after rust-cache. If the target cache is restored before rust-cache, target cleanup can remove the workspace artifacts needed for no-op behavior.

Concrete Result

Before the workaround:

  • Most matrix jobs were fast, but three jobs repeatedly rebuilt local workspace crates despite full cache hits.
  • The slow jobs were around 50 to 65 seconds.
  • The repeated rebuilds included generated-code workspace crates and downstream binaries.

After the workaround:

  • The same previously slow jobs had exact target-cache hits.
  • Cargo produced no Compiling lines for those jobs.
  • Their cargo build phase finished in approximately 0.24 to 0.31 seconds.
  • The whole matrix settled into a consistent 34 to 37 second range, dominated by runner/tool setup and artifact upload rather than Rust compilation.

Proposed Feature

It would be useful if rust-cache could support source-keyed target caching directly, while preserving its current Cargo home/dependency cache behavior.

Possible APIs:

with:
  cache-targets: true
  cache-workspace-crates: true
  target-key: ${{ steps.source-key.outputs.hash }}

or:

with:
  cache-targets: true
  cache-workspace-crates: true
  target-key-files: |
    app/**

or a more explicit split:

with:
  cache-cargo-home: true
  cache-targets: true
  cache-workspace-crates: true
  target-shared-key: one-binary
  target-key: ${{ steps.source-key.outputs.hash }}

Desired behavior:

  • Cargo home cache can keep using the current Rust/environment/lockfile/manifest key strategy.
  • Target cache can include a user-provided source fingerprint.
  • On source changes, restore from the previous target cache via restore prefix, then save the rebuilt target under the new source-keyed target cache.
  • On repeated same-source runs, restore exact target cache and no-op.

Why This Would Help

Today, users who need this behavior have to compose rust-cache with a separate actions/cache target step and be careful about ordering. It works, but the interaction is subtle:

  • rust-cache must have cache-targets: false.
  • The full target cache must restore after rust-cache.
  • The target cache key needs to include source state.
  • The target cache still needs restore-prefix behavior so changed source can reuse previous dependency artifacts.

Having this built into rust-cache would make workspace-crate target caching much easier to configure correctly.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions