From b44fae70ff1412ad4774414414f51b1c4b15879b Mon Sep 17 00:00:00 2001 From: Vaiz <4908982+Vaiz@users.noreply.github.com> Date: Fri, 4 Sep 2026 08:46:53 +0100 Subject: [PATCH 1/3] fix(cargo-anvil): make anvil-fmt actually run the pinned nightly rustfmt `cargo fmt` only dispatches: cargo-fmt runs `rustfmt` as a child process through the rustup shim, and that shim reads `RUSTUP_TOOLCHAIN`, which rustup exports into every child of a `+`-selected cargo and does not rewrite when a nested cargo carries its own `+`. So the inner `+{{ rust_nightly }}` selected a nightly cargo, but the `rustfmt` underneath it still resolved through the outer stable selection. Stable rustfmt does not fail on unstable options -- it downgrades each to a warning and exits 0, so every unstable `rustfmt.toml` rule silently stopped being enforced while the check kept passing. Set `RUSTUP_TOOLCHAIN` for the recipe instead, which covers every descendant. The wrapper loses its stable pin deliberately: that pin is the thing that leaks, and `cargo each` only enumerates members, it compiles nothing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .anvil.lock | 2 +- .../src/anvil/artifacts/justfile.rs | 19 ++++++- .../templates/justfiles/anvil/checks/fmt.just | 12 +++- crates/cargo-anvil/tests/recipe_contracts.rs | 56 ++++++++++++++++--- .../snapshots/snapshots__ado_backend.snap | 12 +++- .../snapshots/snapshots__github_backend.snap | 12 +++- .../snapshots/snapshots__local_only.snap | 12 +++- justfiles/anvil/checks/fmt.just | 12 +++- 8 files changed, 121 insertions(+), 16 deletions(-) diff --git a/.anvil.lock b/.anvil.lock index 19659c75..08e66b67 100644 --- a/.anvil.lock +++ b/.anvil.lock @@ -117,7 +117,7 @@ checksum = "sha256:7e67d90a9d8bd8cd5c8adb68baf41fdd4625bff4d425b5bf18b75baee6289 [[file]] path = "justfiles/anvil/checks/fmt.just" -checksum = "sha256:692e388c44f3c0de2c6bff873ea3440ee25cd6e56b48e0424ebf9c9b1bda4e66" +checksum = "sha256:3e833fb76d4ad23508721927c2e26c95b7d6a9b80df9f09338c32a85614cf62f" [[file]] path = "justfiles/anvil/checks/license-headers.just" diff --git a/crates/cargo-anvil/src/anvil/artifacts/justfile.rs b/crates/cargo-anvil/src/anvil/artifacts/justfile.rs index b789b36b..caabbf9c 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/justfile.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/justfile.rs @@ -796,7 +796,21 @@ mod tests { fn checks_do_not_invoke_an_implicit_default_cargo() { for (path, body) in CHECK_FILES { let caches_stable_args = body.contains("$stableArgs = {{_anvil_stable_toolchain_args}}"); - for line in body.lines().map(str::trim) { + // A recipe may pin RUSTUP_TOOLCHAIN for its own process instead of + // writing `+toolchain` at each site. That is the stronger selection: + // the rustup shim consults it for every descendant, which is what a + // dispatcher like `cargo fmt` needs. + let mut pins_toolchain_environment = false; + for raw in body.lines() { + let line = raw.trim(); + // An unindented line starts a new recipe, so one recipe's pin + // cannot license an unpinned cargo in the next. + if !raw.starts_with([' ', '\t']) && !line.is_empty() { + pins_toolchain_environment = false; + } + if line.starts_with("$env:RUSTUP_TOOLCHAIN = '{{") { + pins_toolchain_environment = true; + } let invokes_cargo = line.starts_with("cargo ") || line.starts_with("& cargo ") || line.contains("= cargo ") @@ -808,7 +822,8 @@ mod tests { .map(|(_, arguments)| arguments.trim_start()) .expect("invokes_cargo patterns always include 'cargo '"); assert!( - toolchain.starts_with("'+") + pins_toolchain_environment + || toolchain.starts_with("'+") || toolchain.starts_with("\"+") || toolchain.starts_with("{{_anvil_stable_toolchain_args}}") || (caches_stable_args && toolchain.starts_with("@stableArgs")), diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/fmt.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/fmt.just index 638def38..c99de54b 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/fmt.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/fmt.just @@ -16,6 +16,15 @@ # Iterate workspace members rather than every local path dependency that # `cargo fmt --all` discovers. `--keep-going` reports formatting failures from # every member while retaining a bounded rustfmt command line per invocation. +# +# Select the nightly through RUSTUP_TOOLCHAIN, not `+toolchain`. `cargo fmt` +# only dispatches: cargo-fmt runs `rustfmt` as a child process through the +# rustup shim, which reads RUSTUP_TOOLCHAIN and inherits it from any outer +# `+`-selected cargo. So an inner `cargo +nightly fmt` still ran STABLE +# rustfmt, which downgrades every unstable option above to a warning and exits +# 0 -- a green check enforcing nothing. The `cargo each` wrapper is left +# unpinned for the same reason: that pin is what leaks, and the wrapper only +# enumerates members, it compiles nothing. # Check Rust source formatting. [arg("fix", long, value="true")] @@ -24,7 +33,8 @@ anvil-fmt fix="false": anvil-fmt-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include modified) if ($include -eq '--skip') { Write-Host 'anvil-fmt: no modified packages; skipping'; exit 0 } - cargo {{_anvil_stable_toolchain_args}} each --workspace --keep-going '--' cargo '+{{ rust_nightly }}' fmt --manifest-path '{manifest}' {{ if fix == "true" { "" } else { "--check" } }} + $env:RUSTUP_TOOLCHAIN = '{{ rust_nightly }}' + cargo each --workspace --keep-going '--' cargo fmt --manifest-path '{manifest}' {{ if fix == "true" { "" } else { "--check" } }} if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Per-check setup + validate-prereqs diff --git a/crates/cargo-anvil/tests/recipe_contracts.rs b/crates/cargo-anvil/tests/recipe_contracts.rs index e226f7f4..3b5ebc6f 100644 --- a/crates/cargo-anvil/tests/recipe_contracts.rs +++ b/crates/cargo-anvil/tests/recipe_contracts.rs @@ -1130,8 +1130,17 @@ fn public_api_checks_fail_when_metadata_discovery_fails() { } } +/// `cargo fmt` only dispatches: cargo-fmt runs `rustfmt` as a child process through +/// the rustup shim, which reads `RUSTUP_TOOLCHAIN` and inherits it from any outer +/// `+`-selected cargo. So the recipe pins that variable rather than writing +/// `+toolchain`, and has to hold against a hostile ambient selection. #[test] -fn fmt_delegates_workspace_iteration_to_cargo_each() { +fn fmt_runs_the_pinned_nightly_over_workspace_members() { + assert!( + !FMT.contains("_anvil_stable_toolchain_args"), + "a stable-pinned outer cargo exports its selection into cargo-each's children, \ + which is exactly what reaches rustfmt" + ); if !tools_available() { return; } @@ -1145,20 +1154,51 @@ fn fmt_delegates_workspace_iteration_to_cargo_each() { "anvil-impact", ], ); - let log = tmp.path().join("cargo.log"); - let output = run_just(tmp.path(), &["anvil-fmt"], &[("FAKE_CARGO_LOG", log.as_os_str())]); + let args_log = tmp.path().join("cargo-args.log"); + let toolchain_log = tmp.path().join("cargo-toolchain.log"); + let output = run_just( + tmp.path(), + &["anvil-fmt"], + &[ + ("FAKE_CARGO_LOG", args_log.as_os_str()), + ("FAKE_CARGO_TOOLCHAIN_LOG", toolchain_log.as_os_str()), + ("RUSTUP_TOOLCHAIN", OsStr::new("test-stable")), + ], + ); assert!( output.status.success(), "per-package formatting failed\nstdout:\n{}\nstderr:\n{}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr) ); - let commands = fs::read_to_string(&log).unwrap(); + let invocations = fs::read_to_string(&args_log).unwrap(); assert!( - commands.contains("each --workspace --keep-going -- cargo +nightly-test fmt --manifest-path {manifest} --check"), - "unexpected cargo invocation: {commands}" + invocations.contains("each --workspace --keep-going -- cargo fmt --manifest-path {manifest} --check"), + "unexpected cargo invocation: {invocations}" ); - assert!(!commands.contains("fmt --all")); + assert!(!invocations.contains("fmt --all")); + // The fake cargo appends one line to each log per invocation, so the logs + // are positionally aligned and the formatting command can be identified by + // its arguments. + let selections = fs::read_to_string(&toolchain_log).unwrap(); + let formatting: Vec<_> = invocations + .lines() + .zip(selections.lines()) + .filter(|(arguments, _)| arguments.contains("fmt")) + .collect(); + assert!( + !formatting.is_empty(), + "the fmt recipe must reach cargo so its toolchain selection is observable:\n{invocations}" + ); + for (arguments, selection) in formatting { + assert_eq!( + selection.trim(), + "nightly-test", + "`cargo {arguments}` inherited '{selection}' instead of the pinned nightly, so stable \ + rustfmt would downgrade every unstable rustfmt.toml option to a warning and the check \ + would pass without enforcing any of them" + ); + } } #[test] @@ -1180,7 +1220,7 @@ fn fmt_fix_removes_the_check_flag() { let output = run_just(tmp.path(), &["anvil-fmt", "--fix"], &[("FAKE_CARGO_LOG", log.as_os_str())]); assert!(output.status.success()); let commands = fs::read_to_string(log).unwrap(); - assert!(commands.contains("each --workspace --keep-going -- cargo +nightly-test fmt --manifest-path {manifest}")); + assert!(commands.contains("each --workspace --keep-going -- cargo fmt --manifest-path {manifest}")); assert!(!commands.contains("--check")); } diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap index d8c7f9b2..f3ddda6c 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap @@ -2604,6 +2604,15 @@ anvil-external-types-validate-prereqs: anvil-toolchain-nightly-external-types-va # Iterate workspace members rather than every local path dependency that # `cargo fmt --all` discovers. `--keep-going` reports formatting failures from # every member while retaining a bounded rustfmt command line per invocation. +# +# Select the nightly through RUSTUP_TOOLCHAIN, not `+toolchain`. `cargo fmt` +# only dispatches: cargo-fmt runs `rustfmt` as a child process through the +# rustup shim, which reads RUSTUP_TOOLCHAIN and inherits it from any outer +# `+`-selected cargo. So an inner `cargo +nightly fmt` still ran STABLE +# rustfmt, which downgrades every unstable option above to a warning and exits +# 0 -- a green check enforcing nothing. The `cargo each` wrapper is left +# unpinned for the same reason: that pin is what leaks, and the wrapper only +# enumerates members, it compiles nothing. # Check Rust source formatting. [arg("fix", long, value="true")] @@ -2612,7 +2621,8 @@ anvil-fmt fix="false": anvil-fmt-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include modified) if ($include -eq '--skip') { Write-Host 'anvil-fmt: no modified packages; skipping'; exit 0 } - cargo {{_anvil_stable_toolchain_args}} each --workspace --keep-going '--' cargo '+{{ rust_nightly }}' fmt --manifest-path '{manifest}' {{ if fix == "true" { "" } else { "--check" } }} + $env:RUSTUP_TOOLCHAIN = '{{ rust_nightly }}' + cargo each --workspace --keep-going '--' cargo fmt --manifest-path '{manifest}' {{ if fix == "true" { "" } else { "--check" } }} if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Per-check setup + validate-prereqs diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap index a4801877..41aedc51 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap @@ -2746,6 +2746,15 @@ anvil-external-types-validate-prereqs: anvil-toolchain-nightly-external-types-va # Iterate workspace members rather than every local path dependency that # `cargo fmt --all` discovers. `--keep-going` reports formatting failures from # every member while retaining a bounded rustfmt command line per invocation. +# +# Select the nightly through RUSTUP_TOOLCHAIN, not `+toolchain`. `cargo fmt` +# only dispatches: cargo-fmt runs `rustfmt` as a child process through the +# rustup shim, which reads RUSTUP_TOOLCHAIN and inherits it from any outer +# `+`-selected cargo. So an inner `cargo +nightly fmt` still ran STABLE +# rustfmt, which downgrades every unstable option above to a warning and exits +# 0 -- a green check enforcing nothing. The `cargo each` wrapper is left +# unpinned for the same reason: that pin is what leaks, and the wrapper only +# enumerates members, it compiles nothing. # Check Rust source formatting. [arg("fix", long, value="true")] @@ -2754,7 +2763,8 @@ anvil-fmt fix="false": anvil-fmt-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include modified) if ($include -eq '--skip') { Write-Host 'anvil-fmt: no modified packages; skipping'; exit 0 } - cargo {{_anvil_stable_toolchain_args}} each --workspace --keep-going '--' cargo '+{{ rust_nightly }}' fmt --manifest-path '{manifest}' {{ if fix == "true" { "" } else { "--check" } }} + $env:RUSTUP_TOOLCHAIN = '{{ rust_nightly }}' + cargo each --workspace --keep-going '--' cargo fmt --manifest-path '{manifest}' {{ if fix == "true" { "" } else { "--check" } }} if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Per-check setup + validate-prereqs diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap index 97aafea5..59cbcb76 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap @@ -1430,6 +1430,15 @@ anvil-external-types-validate-prereqs: anvil-toolchain-nightly-external-types-va # Iterate workspace members rather than every local path dependency that # `cargo fmt --all` discovers. `--keep-going` reports formatting failures from # every member while retaining a bounded rustfmt command line per invocation. +# +# Select the nightly through RUSTUP_TOOLCHAIN, not `+toolchain`. `cargo fmt` +# only dispatches: cargo-fmt runs `rustfmt` as a child process through the +# rustup shim, which reads RUSTUP_TOOLCHAIN and inherits it from any outer +# `+`-selected cargo. So an inner `cargo +nightly fmt` still ran STABLE +# rustfmt, which downgrades every unstable option above to a warning and exits +# 0 -- a green check enforcing nothing. The `cargo each` wrapper is left +# unpinned for the same reason: that pin is what leaks, and the wrapper only +# enumerates members, it compiles nothing. # Check Rust source formatting. [arg("fix", long, value="true")] @@ -1438,7 +1447,8 @@ anvil-fmt fix="false": anvil-fmt-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include modified) if ($include -eq '--skip') { Write-Host 'anvil-fmt: no modified packages; skipping'; exit 0 } - cargo {{_anvil_stable_toolchain_args}} each --workspace --keep-going '--' cargo '+{{ rust_nightly }}' fmt --manifest-path '{manifest}' {{ if fix == "true" { "" } else { "--check" } }} + $env:RUSTUP_TOOLCHAIN = '{{ rust_nightly }}' + cargo each --workspace --keep-going '--' cargo fmt --manifest-path '{manifest}' {{ if fix == "true" { "" } else { "--check" } }} if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Per-check setup + validate-prereqs diff --git a/justfiles/anvil/checks/fmt.just b/justfiles/anvil/checks/fmt.just index 638def38..c99de54b 100644 --- a/justfiles/anvil/checks/fmt.just +++ b/justfiles/anvil/checks/fmt.just @@ -16,6 +16,15 @@ # Iterate workspace members rather than every local path dependency that # `cargo fmt --all` discovers. `--keep-going` reports formatting failures from # every member while retaining a bounded rustfmt command line per invocation. +# +# Select the nightly through RUSTUP_TOOLCHAIN, not `+toolchain`. `cargo fmt` +# only dispatches: cargo-fmt runs `rustfmt` as a child process through the +# rustup shim, which reads RUSTUP_TOOLCHAIN and inherits it from any outer +# `+`-selected cargo. So an inner `cargo +nightly fmt` still ran STABLE +# rustfmt, which downgrades every unstable option above to a warning and exits +# 0 -- a green check enforcing nothing. The `cargo each` wrapper is left +# unpinned for the same reason: that pin is what leaks, and the wrapper only +# enumerates members, it compiles nothing. # Check Rust source formatting. [arg("fix", long, value="true")] @@ -24,7 +33,8 @@ anvil-fmt fix="false": anvil-fmt-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include modified) if ($include -eq '--skip') { Write-Host 'anvil-fmt: no modified packages; skipping'; exit 0 } - cargo {{_anvil_stable_toolchain_args}} each --workspace --keep-going '--' cargo '+{{ rust_nightly }}' fmt --manifest-path '{manifest}' {{ if fix == "true" { "" } else { "--check" } }} + $env:RUSTUP_TOOLCHAIN = '{{ rust_nightly }}' + cargo each --workspace --keep-going '--' cargo fmt --manifest-path '{manifest}' {{ if fix == "true" { "" } else { "--check" } }} if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Per-check setup + validate-prereqs From 88b084be156cd9ec06247987076d15ec4fb23571 Mon Sep 17 00:00:00 2001 From: Vaiz <4908982+Vaiz@users.noreply.github.com> Date: Mon, 7 Sep 2026 13:05:26 +0100 Subject: [PATCH 2/3] fix(cargo-anvil): pin both formatting Cargo invocations to nightly Apply Martin's proposal instead of assigning RUSTUP_TOOLCHAIN in the recipe. Preserve --fix, assert both explicit pins in the recipe contracts, remove the now-unused environment-pin exception, and regenerate the checked-in artifacts and snapshots. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .anvil.lock | 2 +- .../src/anvil/artifacts/justfile.rs | 19 ++-------- .../templates/justfiles/anvil/checks/fmt.just | 13 ++----- crates/cargo-anvil/tests/recipe_contracts.rs | 35 ++----------------- .../snapshots/snapshots__ado_backend.snap | 13 ++----- .../snapshots/snapshots__github_backend.snap | 13 ++----- .../snapshots/snapshots__local_only.snap | 13 ++----- justfiles/anvil/checks/fmt.just | 13 ++----- 8 files changed, 21 insertions(+), 100 deletions(-) diff --git a/.anvil.lock b/.anvil.lock index 08e66b67..fcb012b3 100644 --- a/.anvil.lock +++ b/.anvil.lock @@ -117,7 +117,7 @@ checksum = "sha256:7e67d90a9d8bd8cd5c8adb68baf41fdd4625bff4d425b5bf18b75baee6289 [[file]] path = "justfiles/anvil/checks/fmt.just" -checksum = "sha256:3e833fb76d4ad23508721927c2e26c95b7d6a9b80df9f09338c32a85614cf62f" +checksum = "sha256:01bb1913af9d8096290a09e51cb1c06726988484a2d72f54709bc26c8712dd3e" [[file]] path = "justfiles/anvil/checks/license-headers.just" diff --git a/crates/cargo-anvil/src/anvil/artifacts/justfile.rs b/crates/cargo-anvil/src/anvil/artifacts/justfile.rs index caabbf9c..b789b36b 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/justfile.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/justfile.rs @@ -796,21 +796,7 @@ mod tests { fn checks_do_not_invoke_an_implicit_default_cargo() { for (path, body) in CHECK_FILES { let caches_stable_args = body.contains("$stableArgs = {{_anvil_stable_toolchain_args}}"); - // A recipe may pin RUSTUP_TOOLCHAIN for its own process instead of - // writing `+toolchain` at each site. That is the stronger selection: - // the rustup shim consults it for every descendant, which is what a - // dispatcher like `cargo fmt` needs. - let mut pins_toolchain_environment = false; - for raw in body.lines() { - let line = raw.trim(); - // An unindented line starts a new recipe, so one recipe's pin - // cannot license an unpinned cargo in the next. - if !raw.starts_with([' ', '\t']) && !line.is_empty() { - pins_toolchain_environment = false; - } - if line.starts_with("$env:RUSTUP_TOOLCHAIN = '{{") { - pins_toolchain_environment = true; - } + for line in body.lines().map(str::trim) { let invokes_cargo = line.starts_with("cargo ") || line.starts_with("& cargo ") || line.contains("= cargo ") @@ -822,8 +808,7 @@ mod tests { .map(|(_, arguments)| arguments.trim_start()) .expect("invokes_cargo patterns always include 'cargo '"); assert!( - pins_toolchain_environment - || toolchain.starts_with("'+") + toolchain.starts_with("'+") || toolchain.starts_with("\"+") || toolchain.starts_with("{{_anvil_stable_toolchain_args}}") || (caches_stable_args && toolchain.starts_with("@stableArgs")), diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/fmt.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/fmt.just index c99de54b..299cb33a 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/fmt.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/fmt.just @@ -17,14 +17,8 @@ # `cargo fmt --all` discovers. `--keep-going` reports formatting failures from # every member while retaining a bounded rustfmt command line per invocation. # -# Select the nightly through RUSTUP_TOOLCHAIN, not `+toolchain`. `cargo fmt` -# only dispatches: cargo-fmt runs `rustfmt` as a child process through the -# rustup shim, which reads RUSTUP_TOOLCHAIN and inherits it from any outer -# `+`-selected cargo. So an inner `cargo +nightly fmt` still ran STABLE -# rustfmt, which downgrades every unstable option above to a warning and exits -# 0 -- a green check enforcing nothing. The `cargo each` wrapper is left -# unpinned for the same reason: that pin is what leaks, and the wrapper only -# enumerates members, it compiles nothing. +# Pin both the cargo-each wrapper and cargo-fmt to the same nightly so the +# formatting command does not mix stable and nightly toolchains. # Check Rust source formatting. [arg("fix", long, value="true")] @@ -33,8 +27,7 @@ anvil-fmt fix="false": anvil-fmt-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include modified) if ($include -eq '--skip') { Write-Host 'anvil-fmt: no modified packages; skipping'; exit 0 } - $env:RUSTUP_TOOLCHAIN = '{{ rust_nightly }}' - cargo each --workspace --keep-going '--' cargo fmt --manifest-path '{manifest}' {{ if fix == "true" { "" } else { "--check" } }} + cargo '+{{ rust_nightly }}' each --workspace --keep-going '--' cargo '+{{ rust_nightly }}' fmt --manifest-path '{manifest}' {{ if fix == "true" { "" } else { "--check" } }} if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Per-check setup + validate-prereqs diff --git a/crates/cargo-anvil/tests/recipe_contracts.rs b/crates/cargo-anvil/tests/recipe_contracts.rs index 3b5ebc6f..e7fdae37 100644 --- a/crates/cargo-anvil/tests/recipe_contracts.rs +++ b/crates/cargo-anvil/tests/recipe_contracts.rs @@ -1130,16 +1130,11 @@ fn public_api_checks_fail_when_metadata_discovery_fails() { } } -/// `cargo fmt` only dispatches: cargo-fmt runs `rustfmt` as a child process through -/// the rustup shim, which reads `RUSTUP_TOOLCHAIN` and inherits it from any outer -/// `+`-selected cargo. So the recipe pins that variable rather than writing -/// `+toolchain`, and has to hold against a hostile ambient selection. #[test] fn fmt_runs_the_pinned_nightly_over_workspace_members() { assert!( !FMT.contains("_anvil_stable_toolchain_args"), - "a stable-pinned outer cargo exports its selection into cargo-each's children, \ - which is exactly what reaches rustfmt" + "formatting must pin both Cargo invocations to nightly" ); if !tools_available() { return; @@ -1155,13 +1150,11 @@ fn fmt_runs_the_pinned_nightly_over_workspace_members() { ], ); let args_log = tmp.path().join("cargo-args.log"); - let toolchain_log = tmp.path().join("cargo-toolchain.log"); let output = run_just( tmp.path(), &["anvil-fmt"], &[ ("FAKE_CARGO_LOG", args_log.as_os_str()), - ("FAKE_CARGO_TOOLCHAIN_LOG", toolchain_log.as_os_str()), ("RUSTUP_TOOLCHAIN", OsStr::new("test-stable")), ], ); @@ -1173,32 +1166,10 @@ fn fmt_runs_the_pinned_nightly_over_workspace_members() { ); let invocations = fs::read_to_string(&args_log).unwrap(); assert!( - invocations.contains("each --workspace --keep-going -- cargo fmt --manifest-path {manifest} --check"), + invocations.contains("+nightly-test each --workspace --keep-going -- cargo +nightly-test fmt --manifest-path {manifest} --check"), "unexpected cargo invocation: {invocations}" ); assert!(!invocations.contains("fmt --all")); - // The fake cargo appends one line to each log per invocation, so the logs - // are positionally aligned and the formatting command can be identified by - // its arguments. - let selections = fs::read_to_string(&toolchain_log).unwrap(); - let formatting: Vec<_> = invocations - .lines() - .zip(selections.lines()) - .filter(|(arguments, _)| arguments.contains("fmt")) - .collect(); - assert!( - !formatting.is_empty(), - "the fmt recipe must reach cargo so its toolchain selection is observable:\n{invocations}" - ); - for (arguments, selection) in formatting { - assert_eq!( - selection.trim(), - "nightly-test", - "`cargo {arguments}` inherited '{selection}' instead of the pinned nightly, so stable \ - rustfmt would downgrade every unstable rustfmt.toml option to a warning and the check \ - would pass without enforcing any of them" - ); - } } #[test] @@ -1220,7 +1191,7 @@ fn fmt_fix_removes_the_check_flag() { let output = run_just(tmp.path(), &["anvil-fmt", "--fix"], &[("FAKE_CARGO_LOG", log.as_os_str())]); assert!(output.status.success()); let commands = fs::read_to_string(log).unwrap(); - assert!(commands.contains("each --workspace --keep-going -- cargo fmt --manifest-path {manifest}")); + assert!(commands.contains("+nightly-test each --workspace --keep-going -- cargo +nightly-test fmt --manifest-path {manifest}")); assert!(!commands.contains("--check")); } diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap index f3ddda6c..63893683 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap @@ -2605,14 +2605,8 @@ anvil-external-types-validate-prereqs: anvil-toolchain-nightly-external-types-va # `cargo fmt --all` discovers. `--keep-going` reports formatting failures from # every member while retaining a bounded rustfmt command line per invocation. # -# Select the nightly through RUSTUP_TOOLCHAIN, not `+toolchain`. `cargo fmt` -# only dispatches: cargo-fmt runs `rustfmt` as a child process through the -# rustup shim, which reads RUSTUP_TOOLCHAIN and inherits it from any outer -# `+`-selected cargo. So an inner `cargo +nightly fmt` still ran STABLE -# rustfmt, which downgrades every unstable option above to a warning and exits -# 0 -- a green check enforcing nothing. The `cargo each` wrapper is left -# unpinned for the same reason: that pin is what leaks, and the wrapper only -# enumerates members, it compiles nothing. +# Pin both the cargo-each wrapper and cargo-fmt to the same nightly so the +# formatting command does not mix stable and nightly toolchains. # Check Rust source formatting. [arg("fix", long, value="true")] @@ -2621,8 +2615,7 @@ anvil-fmt fix="false": anvil-fmt-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include modified) if ($include -eq '--skip') { Write-Host 'anvil-fmt: no modified packages; skipping'; exit 0 } - $env:RUSTUP_TOOLCHAIN = '{{ rust_nightly }}' - cargo each --workspace --keep-going '--' cargo fmt --manifest-path '{manifest}' {{ if fix == "true" { "" } else { "--check" } }} + cargo '+{{ rust_nightly }}' each --workspace --keep-going '--' cargo '+{{ rust_nightly }}' fmt --manifest-path '{manifest}' {{ if fix == "true" { "" } else { "--check" } }} if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Per-check setup + validate-prereqs diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap index 41aedc51..6472f56d 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap @@ -2747,14 +2747,8 @@ anvil-external-types-validate-prereqs: anvil-toolchain-nightly-external-types-va # `cargo fmt --all` discovers. `--keep-going` reports formatting failures from # every member while retaining a bounded rustfmt command line per invocation. # -# Select the nightly through RUSTUP_TOOLCHAIN, not `+toolchain`. `cargo fmt` -# only dispatches: cargo-fmt runs `rustfmt` as a child process through the -# rustup shim, which reads RUSTUP_TOOLCHAIN and inherits it from any outer -# `+`-selected cargo. So an inner `cargo +nightly fmt` still ran STABLE -# rustfmt, which downgrades every unstable option above to a warning and exits -# 0 -- a green check enforcing nothing. The `cargo each` wrapper is left -# unpinned for the same reason: that pin is what leaks, and the wrapper only -# enumerates members, it compiles nothing. +# Pin both the cargo-each wrapper and cargo-fmt to the same nightly so the +# formatting command does not mix stable and nightly toolchains. # Check Rust source formatting. [arg("fix", long, value="true")] @@ -2763,8 +2757,7 @@ anvil-fmt fix="false": anvil-fmt-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include modified) if ($include -eq '--skip') { Write-Host 'anvil-fmt: no modified packages; skipping'; exit 0 } - $env:RUSTUP_TOOLCHAIN = '{{ rust_nightly }}' - cargo each --workspace --keep-going '--' cargo fmt --manifest-path '{manifest}' {{ if fix == "true" { "" } else { "--check" } }} + cargo '+{{ rust_nightly }}' each --workspace --keep-going '--' cargo '+{{ rust_nightly }}' fmt --manifest-path '{manifest}' {{ if fix == "true" { "" } else { "--check" } }} if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Per-check setup + validate-prereqs diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap index 59cbcb76..1be7acf1 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap @@ -1431,14 +1431,8 @@ anvil-external-types-validate-prereqs: anvil-toolchain-nightly-external-types-va # `cargo fmt --all` discovers. `--keep-going` reports formatting failures from # every member while retaining a bounded rustfmt command line per invocation. # -# Select the nightly through RUSTUP_TOOLCHAIN, not `+toolchain`. `cargo fmt` -# only dispatches: cargo-fmt runs `rustfmt` as a child process through the -# rustup shim, which reads RUSTUP_TOOLCHAIN and inherits it from any outer -# `+`-selected cargo. So an inner `cargo +nightly fmt` still ran STABLE -# rustfmt, which downgrades every unstable option above to a warning and exits -# 0 -- a green check enforcing nothing. The `cargo each` wrapper is left -# unpinned for the same reason: that pin is what leaks, and the wrapper only -# enumerates members, it compiles nothing. +# Pin both the cargo-each wrapper and cargo-fmt to the same nightly so the +# formatting command does not mix stable and nightly toolchains. # Check Rust source formatting. [arg("fix", long, value="true")] @@ -1447,8 +1441,7 @@ anvil-fmt fix="false": anvil-fmt-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include modified) if ($include -eq '--skip') { Write-Host 'anvil-fmt: no modified packages; skipping'; exit 0 } - $env:RUSTUP_TOOLCHAIN = '{{ rust_nightly }}' - cargo each --workspace --keep-going '--' cargo fmt --manifest-path '{manifest}' {{ if fix == "true" { "" } else { "--check" } }} + cargo '+{{ rust_nightly }}' each --workspace --keep-going '--' cargo '+{{ rust_nightly }}' fmt --manifest-path '{manifest}' {{ if fix == "true" { "" } else { "--check" } }} if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Per-check setup + validate-prereqs diff --git a/justfiles/anvil/checks/fmt.just b/justfiles/anvil/checks/fmt.just index c99de54b..299cb33a 100644 --- a/justfiles/anvil/checks/fmt.just +++ b/justfiles/anvil/checks/fmt.just @@ -17,14 +17,8 @@ # `cargo fmt --all` discovers. `--keep-going` reports formatting failures from # every member while retaining a bounded rustfmt command line per invocation. # -# Select the nightly through RUSTUP_TOOLCHAIN, not `+toolchain`. `cargo fmt` -# only dispatches: cargo-fmt runs `rustfmt` as a child process through the -# rustup shim, which reads RUSTUP_TOOLCHAIN and inherits it from any outer -# `+`-selected cargo. So an inner `cargo +nightly fmt` still ran STABLE -# rustfmt, which downgrades every unstable option above to a warning and exits -# 0 -- a green check enforcing nothing. The `cargo each` wrapper is left -# unpinned for the same reason: that pin is what leaks, and the wrapper only -# enumerates members, it compiles nothing. +# Pin both the cargo-each wrapper and cargo-fmt to the same nightly so the +# formatting command does not mix stable and nightly toolchains. # Check Rust source formatting. [arg("fix", long, value="true")] @@ -33,8 +27,7 @@ anvil-fmt fix="false": anvil-fmt-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include modified) if ($include -eq '--skip') { Write-Host 'anvil-fmt: no modified packages; skipping'; exit 0 } - $env:RUSTUP_TOOLCHAIN = '{{ rust_nightly }}' - cargo each --workspace --keep-going '--' cargo fmt --manifest-path '{manifest}' {{ if fix == "true" { "" } else { "--check" } }} + cargo '+{{ rust_nightly }}' each --workspace --keep-going '--' cargo '+{{ rust_nightly }}' fmt --manifest-path '{manifest}' {{ if fix == "true" { "" } else { "--check" } }} if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Per-check setup + validate-prereqs From 91443c181cff67d7e79f2de356817bf7e53f86a1 Mon Sep 17 00:00:00 2001 From: Vaiz <4908982+Vaiz@users.noreply.github.com> Date: Mon, 7 Sep 2026 15:05:25 +0100 Subject: [PATCH 3/3] fix(cargo-anvil): restamp the anvil catalog checksum after rebase Rebasing onto main picked up template changes (msrv-test.just, tools.just and the GitHub workflow templates), so the catalog checksum recorded in .anvil.lock no longer matched either side of the merge. Regenerated with `cargo run -p cargo-anvil -- anvil`; `--dry-run` is now clean and no generated artifact changed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .anvil.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.anvil.lock b/.anvil.lock index fcb012b3..7938d604 100644 --- a/.anvil.lock +++ b/.anvil.lock @@ -1,7 +1,7 @@ version = 1 tool = "anvil" tool_version = "0.8.0" -catalog_checksum = "sha256:25beddc77b7a0f7c6d7569bd7d252f63770603600e5f3a2701eb0c5adf8ca44f" +catalog_checksum = "sha256:eee33a59e533a09ea83e2b99509e8f248e98894b88795c83f93b6cdbddf88c3e" [[file]] path = ".anvil/container/Dockerfile.dockerignore"