Sign in Sign up
1 branch
README.md

cargo shared-target-dir path-dependency collision

#

Reproduction for rust-lang/cargo#12516, plus one finding that is not yet on the issue: the stabilized build.build-dir inherits the bug.

$ ./repro.sh
cargo: cargo 1.97.1 (c980f4866 2026-06-30)

control      a: workspace a linked c::which() = a/c  <-- workspace a's copy
             b: workspace b linked c::which() = b/c  <-- workspace b's copy
             => correct

target-dir   a: workspace a linked c::which() = a/c  <-- workspace a's copy
             b: workspace b linked c::which() = a/c  <-- workspace a's copy
             => *** WRONG: b linked a/c ***

build-dir    a: workspace a linked c::which() = a/c  <-- workspace a's copy
             b: workspace b linked c::which() = a/c  <-- workspace a's copy
             => *** WRONG: b linked a/c ***

Note there is no compile error. b builds successfully and prints a's string. The original issue's repro surfaces as an unsafe fn type error, which makes it look like a diagnostic problem; it isn't. The general case is a binary silently linked against a different crate's code.

Setup

#

Two independent workspaces. Each has a path dependency named c, version 0.1.0, at the relative path c/. The two c crates have different contents. That is the entire setup — there is nothing exotic here.

a/Cargo.toml          b/Cargo.toml
a/src/main.rs         b/src/main.rs
a/c/Cargo.toml        b/c/Cargo.toml
a/c/src/lib.rs        b/c/src/lib.rs     <-- different contents

Reproduced on

#
toolchain shared CARGO_TARGET_DIR shared build.build-dir only
cargo 1.97.1 (2026-06-30) wrong wrong
cargo 1.99.0-nightly (2026-07-29) wrong wrong
./repro.sh            # default toolchain
./repro.sh +nightly   # or any rustup toolchain

What is happening

#

Cargo gives every compilation unit an identity so artifacts can coexist in one directory. That identity is the -C metadata hash: it becomes the -<hash> suffix in libc-<hash>.rlib and the name of the fingerprint directory .fingerprint/c-<hash>/.

The hash is derived from the package id, the profile, the feature set, the rustc version and RUSTFLAGS. The package id contains a SourceId. For a registry dependency the SourceId is the registry URL — globally unique. For a path dependency it is the path, and cargo records that path relative to the workspace root, not as an absolute path (source_id.rs).

That is a deliberate design choice, not an oversight. Relative paths are what make a build path-independent: the same workspace compiled at /home/alice/proj and /build/ci/1234 produces identical artifacts. CI cache restore across differently-named checkout directories depends on it, and so do trim-paths and the eventual per-user artifact cache (#5931).

The consequence is that a/c and b/c — different crates, different contents — produce the same SourceId, so the same package id, so the same -C metadata hash, so the same output filename and the same fingerprint directory.

In a shared directory:

  1. Build a. Cargo compiles a/c, writes libc-<H>.rlib and .fingerprint/c-<H>/.
  2. Build b. Cargo computes the identity of b/c and gets the same <H>. It finds .fingerprint/c-<H>/ already present.
  3. The freshness check compares the recorded dep-info against the files on disk. Nothing in that check distinguishes a/c from b/c — the unit is declared fresh.
  4. Cargo skips compiling b/c and links a/c's rlib into b.

Build order decides the outcome, which is the giveaway when this bites in the wild.

Why build.build-dir does not help

#

build.build-dir was stabilized in cargo 1.94 and splits intermediate artifacts out of target/. A natural assumption is that sharing build-dir while keeping target-dir per-workspace is a safe way to share the expensive part of a build. It is not: deps/ and .fingerprint/ both live in build-dir, so the collision follows build-dir exactly. The build-dir row above is CARGO_BUILD_BUILD_DIR shared with a/target and b/target left separate.

This matters because the 2025h2 build-dir layout rework explicitly names sharing a build cache across workspaces as a motivation and proposes a user-wide cache built on this layout.

Who hits this

#

Anyone with several checkouts of the same workspace and a shared build directory — most commonly git worktree with one worktree per branch, which has become routine with coding agents. Every worktree presents the same workspace-relative path for every workspace member, so the collision rate is 100%, not incidental.

The symptom is usually not this clean. It shows up as a compile error citing a line in your own file for a definition that does not exist in your tree, while git diff is clean — or as a test that passes against code you did not write. Most people who hit it conclude they have a stale build and cargo clean it away, which is why the issue has four reactions after three years.

Workarounds

#

Give each checkout its own target/ — cargo's default. Dependency artifacts compile from global ~/.cargo paths and are identical across checkouts, so they can be hardlinked between sibling checkouts (rsync -a --link-dest) for zero bytes, which recovers most of what a shared directory would have bought.

A RUSTC_WRAPPER cannot fix this: cargo computes unit identity before it invokes rustc, and expects specific filenames back.