rust-lang / rust-lang/rust-analyzer
flycheck's `cargo check` bypasses the lockfile redirection the build-script check receives, rewriting the project's Cargo.lock
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 16.9k
- Forks
- 2.2k
- Avg merge
- 1d 12h
- Merged PRs (30d)
- 72
Description
Opening a project in rust-analyzer rewrites a tracked Cargo.lock when a resolve has work to do — for example when a path dependency on disk is newer than what the lockfile records.
rust-analyzer already protects against this: during workspace load it runs cargo check for build scripts with CARGO_RESOLVER_LOCKFILE_PATH pointing at a temp copy, so that resolve cannot touch the real lockfile. The flycheck cargo check does not get that treatment, and its resolve writes the project's lockfile directly.
So the two cargo check invocations in a single session disagree about whether the lockfile is writable.
Reproduction
Four files, no network, path dependency only.
mkdir -p /tmp/ra-minrepro/{dep/src,app/src}
cd /tmp/ra-minrepro
cat > dep/Cargo.toml <<'EOF'
[package]
name = "minrepro-dep"
version = "0.1.0"
edition = "2021"
EOF
echo 'pub fn hello() -> &'"'"'static str { "hi" }' > dep/src/lib.rs
cat > app/Cargo.toml <<'EOF'
[package]
name = "minrepro-app"
version = "0.1.0"
edition = "2021"
[dependencies]
minrepro-dep = { path = "../dep" }
EOF
echo 'pub fn greet() -> &'"'"'static str { minrepro_dep::hello() }' > app/src/lib.rs
# generate the lockfile against dep 0.1.0, then bump the dep
( cd app && cargo generate-lockfile )
sed -i 's/^version = "0.1.0"/version = "0.1.1"/' dep/Cargo.toml
sha256sum app/Cargo.lock # record this
Now open /tmp/ra-minrepro/app in an editor with rust-analyzer, or drive the server over stdio with initialize + initialized against rootUri: file:///tmp/ra-minrepro/app and wait for workspace load.
sha256sum app/Cargo.lock # changed
grep -A1 'name = "minrepro-dep"' app/Cargo.lock # now 0.1.1
Observed here: fdff43a8… → ab606929…, minrepro-dep 0.1.0 → 0.1.1. No file was opened and nothing was saved; workspace load alone is enough.
Mechanism
Both cargo check invocations from one session, captured with a logging cargo shim recording argv and environment per invocation:
check #1 — build scripts, protected
check --quiet --workspace --message-format=json --manifest-path <root>/Cargo.toml \
-Zlockfile-path --keep-going --compile-time-deps --all-targets -Zunstable-options
CARGO_RESOLVER_LOCKFILE_PATH=/tmp/rust-analyzer3dbd91-1/Cargo.lock
__CARGO_TEST_CHANNEL_OVERRIDE_DO_NOT_USE_THIS=nightly
check #2 — flycheck, writes the lockfile
check --workspace --message-format=json --manifest-path <root>/Cargo.toml \
--keep-going --all-targets
(neither variable set)
The redirection is the environment variable, not the flag. Running check #2's argv verbatim:
- without those variables → the source lockfile is rewritten (
fdff43a8…→ab606929…) - with
CARGO_RESOLVER_LOCKFILE_PATHand the channel override exported, nothing else changed → the source lockfile is untouched and the redirect target receives the write instead
Same resolution, different destination, one variable.
Note that -Zlockfile-path in check #1's argv is inert: cargo 1.97 stabilized it, and cargo emits flag -Z lockfile-path has been stabilized in the 1.97 release, and is no longer necessary. It names no path. We initially believed the flag was the discriminator; the A/B above disproved that.
Two things a reader may reasonably ask
Should flycheck use a redirected lockfile at all? The precedent is already in-tree — the build-script check resolves against a redirected lockfile today. This asks for consistency with existing behaviour rather than new policy.
Does redirecting change diagnostics? Not in the case we measured. Running check #2's argv against the same tree with and without CARGO_RESOLVER_LOCKFILE_PATH set, on a version of the repro that emits diagnostics (an unused variable, so the comparison is non-empty and could fail), both runs emitted the same two compiler-message entries with byte-identical rendered output. The redirect changes where the resolution is written, not what resolves.
Environment and limits
rust-analyzer 0.3.2963-standalone
cargo 1.98.0 (797e8a9bc 2026-08-05) (Arch Linux rust 1:1.98.0-1.1)
rustc 1.98.0 (88d9e12ae 2026-08-18)
CachyOS (Arch-derived), kernel 7.2.0-1-cachyos
Stating the limits of what we verified, since some of this reads stronger than it is:
- The rust-analyzer behaviour was observed on one host and one rust-analyzer build (0.3.2963). Four instruments were used — two independently written LSP clients, a logging cargo shim, and a
/procpoller — and the behaviour was checked twice by a second party, on a separately written LSP client. First, from a prose description of the setup only, building an equivalent tree with a different layout and different file contents: same behaviour, different hashes — which rules out a reproduction that only works for its author's exact files. Second, by running the block above verbatim: the resulting lockfile hashes were identical to the ones recorded here (fdff43a8…→ab606929…), so the reproduction is deterministic as published. Neither establishes environment independence for the rust-analyzer half. - The cargo-side write was reproduced on a second machine: Ubuntu 24.04.4, glibc 2.39, cargo 1.94.1 (Homebrew) — a different distro, libc, cargo version and cargo packaging. Same transition. Note 1.94.1 predates the 1.97 stabilization of
-Zlockfile-path, so the write behaviour is not tied to that stabilization. - No machine on which we ran a rust-analyzer session had
rust-srcinstalled, so rust-analyzer emittedcan't load standard library from sysrootduring those runs. Both checks fired and the write happened regardless. The second machine, where the cargo-side write reproduced, did haverust-srcpresent — so sysroot availability does not prevent the write on the cargo side. We could not test a rust-analyzer session withrust-srcpresent.
Why it matters
The write is invisible to the caller. A diagnostic action with read-only intent mutates a tracked file, and the trigger — a path dependency on a sibling checkout that has moved ahead of the committed lockfile — arises in any workflow where two related crates are developed side by side. Editors that load a workspace on open will do this without user action.
Related: #19011, #19729, #15165 and PR #20018, which introduced the lockfile copy for cargo metadata. This report is about the flycheck cargo check path, which that work does not cover.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start at rust-analyzer's flycheck cargo check path and compare it with the existing build-script check that sets CARGO_RESOLVER_LOCKFILE_PATH. Trace how each invocation builds its environment, then run the four-file path-dependency reproduction and verify workspace loading leaves app/Cargo.lock unchanged while diagnostics remain equivalent.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- devtools
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 72/100