diff --git a/.anvil.lock b/.anvil.lock index 78aebece..911b3eb7 100644 --- a/.anvil.lock +++ b/.anvil.lock @@ -1,7 +1,7 @@ version = 1 tool = "anvil" tool_version = "0.7.0" -catalog_checksum = "sha256:7ab5d33f1eb49c1f7982d6623079488505a838d163955afc0f7fff782b0a4cf5" +catalog_checksum = "sha256:545245661e6d0b3edb1411ec48d0c797f5ec9eda4cf248f11828b0cb51a40f19" [[file]] path = ".anvil/container/Dockerfile.dockerignore" @@ -109,7 +109,7 @@ checksum = "sha256:7e67d90a9d8bd8cd5c8adb68baf41fdd4625bff4d425b5bf18b75baee6289 [[file]] path = "justfiles/anvil/checks/fmt.just" -checksum = "sha256:0434e1a8720453a4bc635ea56af1d19875ac79e6fdd76349052cd57e70392eb8" +checksum = "sha256:4f258afb70f8186c270f28958b2376cb13199dd5b2897588312d841fbffe88be" [[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 7af41063..ccb2a4f4 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/justfile.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/justfile.rs @@ -787,7 +787,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 ") @@ -799,7 +813,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 48291c5e..65dd44f5 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. [script("pwsh", "-NoProfile")] @@ -23,7 +32,8 @@ anvil-fmt: 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}' --check + $env:RUSTUP_TOOLCHAIN = '{{ rust_nightly }}' + cargo each --workspace --keep-going '--' cargo fmt --manifest-path '{manifest}' --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 7b1f8220..7c7dc70e 100644 --- a/crates/cargo-anvil/tests/recipe_contracts.rs +++ b/crates/cargo-anvil/tests/recipe_contracts.rs @@ -908,8 +908,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; } @@ -923,20 +932,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}" - ); - assert!(!commands.contains("fmt --all")); + invocations.contains("each --workspace --keep-going -- cargo 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] diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap index 7bc3e039..0650fd0a 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap @@ -2263,6 +2263,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. [script("pwsh", "-NoProfile")] @@ -2270,7 +2279,8 @@ anvil-fmt: 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}' --check + $env:RUSTUP_TOOLCHAIN = '{{ rust_nightly }}' + cargo each --workspace --keep-going '--' cargo fmt --manifest-path '{manifest}' --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 ade9fb80..a1c894a7 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap @@ -2351,6 +2351,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. [script("pwsh", "-NoProfile")] @@ -2358,7 +2367,8 @@ anvil-fmt: 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}' --check + $env:RUSTUP_TOOLCHAIN = '{{ rust_nightly }}' + cargo each --workspace --keep-going '--' cargo fmt --manifest-path '{manifest}' --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 df2d8cc6..4f5826d4 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap @@ -1084,6 +1084,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. [script("pwsh", "-NoProfile")] @@ -1091,7 +1100,8 @@ anvil-fmt: 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}' --check + $env:RUSTUP_TOOLCHAIN = '{{ rust_nightly }}' + cargo each --workspace --keep-going '--' cargo fmt --manifest-path '{manifest}' --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 48291c5e..65dd44f5 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. [script("pwsh", "-NoProfile")] @@ -23,7 +32,8 @@ anvil-fmt: 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}' --check + $env:RUSTUP_TOOLCHAIN = '{{ rust_nightly }}' + cargo each --workspace --keep-going '--' cargo fmt --manifest-path '{manifest}' --check if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Per-check setup + validate-prereqs