From 62fc4b28405a23c8c8022bc59eb05a3116cf2bae Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Thu, 27 Aug 2026 13:21:35 +0200 Subject: [PATCH 01/32] Make Anvil stable toolchains deterministic Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .anvil.lock | 20 +- .anvil/container/Containerfile | 11 +- .anvil/container/Containerfile.dockerignore | 3 + .anvil/container/image-id.ps1 | 10 +- .anvil/container/image-id.sh | 13 +- .anvil/resolve-stable-toolchain.ps1 | 156 +++++++++++ .github/actions/anvil-setup/action.yml | 31 ++- crates/cargo-anvil/README.md | 5 +- crates/cargo-anvil/docs/design/README.md | 25 +- crates/cargo-anvil/docs/design/ado.md | 33 ++- crates/cargo-anvil/docs/design/containers.md | 22 +- crates/cargo-anvil/docs/design/github.md | 31 ++- crates/cargo-anvil/docs/design/local.md | 51 ++-- .../src/anvil/artifacts/container.rs | 15 +- .../src/anvil/artifacts/justfile.rs | 140 ++++++++++ crates/cargo-anvil/src/anvil/artifacts/mod.rs | 2 + crates/cargo-anvil/src/lib.rs | 3 +- .../cargo-anvil/templates/ado/steps/setup.yml | 13 + .../templates/anvil/container/Containerfile | 11 +- .../container/Containerfile.dockerignore | 3 + .../templates/anvil/container/image-id.ps1 | 10 +- .../templates/anvil/container/image-id.sh | 13 +- .../anvil/resolve-stable-toolchain.ps1 | 156 +++++++++++ .../templates/github/setup-action.yml | 31 ++- .../templates/justfiles/anvil/tools.just | 4 + .../templates/justfiles/anvil/versions.just | 17 ++ .../tests/container_customization.rs | 2 + crates/cargo-anvil/tests/container_upgrade.rs | 6 + .../snapshots/snapshots__ado_backend.snap | 229 ++++++++++++++-- .../snapshots/snapshots__github_backend.snap | 247 +++++++++++++++--- .../snapshots/snapshots__local_only.snap | 216 +++++++++++++-- justfiles/anvil/tools.just | 4 + justfiles/anvil/versions.just | 17 ++ 33 files changed, 1350 insertions(+), 200 deletions(-) create mode 100644 .anvil/resolve-stable-toolchain.ps1 create mode 100644 crates/cargo-anvil/templates/anvil/resolve-stable-toolchain.ps1 diff --git a/.anvil.lock b/.anvil.lock index e3aa0ecd6..4c251b937 100644 --- a/.anvil.lock +++ b/.anvil.lock @@ -1,15 +1,15 @@ version = 1 tool = "anvil" tool_version = "0.5.0" -catalog_checksum = "sha256:64436c7a20508d0413fa5659e80cd5afe3d272096be6e892c46c33b169af5d0c" +catalog_checksum = "sha256:8f085fd850e403e724c6b1b20b788056d3624fede21b7d21dd2d9f896a6f8fbc" [[file]] path = ".anvil/container/Containerfile" -checksum = "sha256:4c343282b4e773978ff29b7d39e3a0955975bf64ad3e63afe1aa53e51be42916" +checksum = "sha256:ea2c9447d21b8ab851bca2c5f2b2b32223782dfd8e44de26d2e63b79e4212530" [[file]] path = ".anvil/container/Containerfile.dockerignore" -checksum = "sha256:359c2bd70d599ff1e5674eae2af5bb35f344f78e642da7024cfdc6efc5d345da" +checksum = "sha256:4751a0669d2caa0c96a763167d69f6a9fd9ae4cf9f8d3e915f286be4b8323866" [[file]] path = ".anvil/container/README.md" @@ -21,11 +21,11 @@ checksum = "sha256:09576bca317f5a413572f6626fc052214f885589f0547ed0b0c044b92cc40 [[file]] path = ".anvil/container/image-id.ps1" -checksum = "sha256:8cdd2dd9cdfb037d768802e4dda7d1156f626dc711663e6bc39fbb86d1ce2e04" +checksum = "sha256:dc8a2f709a966fced778ea7223bd48ddac5b8ec425c9cf452952f0c8eff5fc98" [[file]] path = ".anvil/container/image-id.sh" -checksum = "sha256:b526a643e42902dd1e8acff65529fb63741760f72c14b123e9747394158faf53" +checksum = "sha256:8cb83009675be4181bf4dd919b077f9ea60651ac7b4f2009b783c308bd812a9b" [[file]] path = ".anvil/container/run-in-container.ps1" @@ -35,6 +35,10 @@ checksum = "sha256:34da3c0d0defc55523d0b46722d2f3c49796875f01d277790075af8efabac path = ".anvil/container/run-in-container.sh" checksum = "sha256:bf4a21ac097d34b6b4b3a4a507455dda96052980228b6145ce6d33c8e4674317" +[[file]] +path = ".anvil/resolve-stable-toolchain.ps1" +checksum = "sha256:149bf7057f1d88b802a8f3a2f22cafb0c5d336bb560ed6f35735c78fcc23bce9" + [[file]] path = ".github/actions/anvil-impact/action.yml" checksum = "sha256:ac509263f374f4b41270f92969e77b3352f815609e518ac8d6cd3d1a126649cc" @@ -49,7 +53,7 @@ checksum = "sha256:b6ea795dbdc88145a7cc02199134e95cd8baa052f95a8913308630bbd3928 [[file]] path = ".github/actions/anvil-setup/action.yml" -checksum = "sha256:07a77c9984b68a587fe3abdacc19f3ac3b9d3a5f973571ce9a351d6d4866dcc5" +checksum = "sha256:4e28dcbd0cfc90336e9fed2a500e11975d3c2bf9fbb3a9dbc53e96d1e0164953" [[file]] path = ".github/actions/anvil-setup/just-problem-matcher.json" @@ -249,11 +253,11 @@ checksum = "sha256:713c5a2ae28b6b5aa20dd38226278b3b7f71bbc5e6b84a16eaf5244713f27 [[file]] path = "justfiles/anvil/tools.just" -checksum = "sha256:a1e44ca16f172b487afa3997f102512733d3b65a4418cf894cbd749a3abc17dc" +checksum = "sha256:adfce0506e21386dbbe510fb80fdf1ee8e5a06a56e38d585a19f88c1213d05a5" [[file]] path = "justfiles/anvil/versions.just" -checksum = "sha256:acbea93d5117db747537f4f7b9a5eb90b7d3e0dd3e8684cc0e4dc1dcb15ac93e" +checksum = "sha256:47e0853a4905b1e2735551ff6188cc8b959fcff93d5c79fbc6fc58b8c4f15f95" [[region]] host = ".delta.toml" diff --git a/.anvil/container/Containerfile b/.anvil/container/Containerfile index fa68b18f1..dc4a709ef 100644 --- a/.anvil/container/Containerfile +++ b/.anvil/container/Containerfile @@ -49,15 +49,14 @@ RUN curl --proto '=https' --tlsv1.2 -fsSLo /tmp/rustup-init \ WORKDIR /opt/anvil COPY . . -RUN test -f rust-toolchain.toml || { \ - echo "anvil-container requires rust-toolchain.toml" >&2; \ - exit 1; \ - } RUN --mount=type=cache,id=anvil-cargo-registry,target=/usr/local/cargo/registry \ --mount=type=cache,id=anvil-cargo-git,target=/usr/local/cargo/git \ --mount=type=cache,id=anvil-cargo-target,target=/tmp/anvil-target \ - printf "anvil_runner := \"native\"\nimport 'justfiles/anvil/mod.just'\n" > Justfile \ - && CARGO_TARGET_DIR=/tmp/anvil-target just anvil-setup + pwsh -NoProfile -File .anvil/resolve-stable-toolchain.ps1 -InstallIfMissing \ + && toolchain="$(pwsh -NoProfile -File .anvil/resolve-stable-toolchain.ps1 -ForEnvironment)" \ + && printf "anvil_runner := \"native\"\nimport 'justfiles/anvil/mod.just'\n" > Justfile \ + && RUSTUP_TOOLCHAIN="$toolchain" ANVIL_STABLE_TOOLCHAIN_SOURCE=container \ + CARGO_TARGET_DIR=/tmp/anvil-target just anvil-setup COPY .anvil/container/entrypoint.sh /usr/local/bin/anvil-container-entrypoint RUN chmod 755 /usr/local/bin/anvil-container-entrypoint diff --git a/.anvil/container/Containerfile.dockerignore b/.anvil/container/Containerfile.dockerignore index 6566e6571..6aca85683 100644 --- a/.anvil/container/Containerfile.dockerignore +++ b/.anvil/container/Containerfile.dockerignore @@ -16,10 +16,13 @@ # contract in the allow-list too, at every depth, because a deeper candidate # always has an ancestor of exactly that shape. ** +!Cargo.toml +!rust-toolchain !rust-toolchain.toml !justfiles/anvil/*.just !justfiles/anvil/checks/*.just !justfiles/anvil/groups/*.just +!.anvil/resolve-stable-toolchain.ps1 !.anvil/container/* .anvil/container/*/* .anvil/container/customize.sh diff --git a/.anvil/container/image-id.ps1 b/.anvil/container/image-id.ps1 index dfc6a53f3..c95dd2c70 100644 --- a/.anvil/container/image-id.ps1 +++ b/.anvil/container/image-id.ps1 @@ -14,11 +14,13 @@ if ($LASTEXITCODE -ne 0 -or -not $repoRoot) { } $inputs = @( - 'rust-toolchain.toml' + 'Cargo.toml', + '.anvil/resolve-stable-toolchain.ps1' ) -$toolchainPath = Join-Path $repoRoot 'rust-toolchain.toml' -if (-not (Test-Path -LiteralPath $toolchainPath -PathType Leaf)) { - throw 'anvil-container requires a repository-owned rust-toolchain.toml.' +foreach ($toolchainFile in @('rust-toolchain', 'rust-toolchain.toml')) { + if (Test-Path -LiteralPath (Join-Path $repoRoot $toolchainFile) -PathType Leaf) { + $inputs += $toolchainFile + } } $containerPath = Join-Path $repoRoot '.anvil/container' $containerRecipe = 'justfiles/anvil/container.just' diff --git a/.anvil/container/image-id.sh b/.anvil/container/image-id.sh index e0ed81051..f399e349b 100644 --- a/.anvil/container/image-id.sh +++ b/.anvil/container/image-id.sh @@ -7,12 +7,6 @@ if ! repo_root="$(git rev-parse --show-toplevel 2>/dev/null)"; then exit 1 fi -toolchain_path="$repo_root/rust-toolchain.toml" -if [[ ! -f "$toolchain_path" ]]; then - echo 'anvil-container requires a repository-owned rust-toolchain.toml.' >&2 - exit 1 -fi - container_dir="$repo_root/.anvil/container" container_recipe="justfiles/anvil/container.just" default_base_image="$(sed -n 's/^ARG BASE_IMAGE=//p' "$container_dir/Containerfile" | head -n 1)" @@ -25,7 +19,12 @@ if [[ ! "$base_image" =~ @sha256:[0-9a-fA-F]{64}$ ]]; then echo 'anvil-container: ANVIL_CONTAINER_BASE_IMAGE must be pinned by sha256 digest (image@sha256:<64 hex characters>).' >&2 exit 1 fi -inputs=(rust-toolchain.toml) +inputs=(Cargo.toml .anvil/resolve-stable-toolchain.ps1) +for toolchain_file in rust-toolchain rust-toolchain.toml; do + if [[ -f "$repo_root/$toolchain_file" ]]; then + inputs+=("$toolchain_file") + fi +done while IFS= read -r path; do relative="${path#"$repo_root"/}" # The container entry recipe drives execution on the host; it is not diff --git a/.anvil/resolve-stable-toolchain.ps1 b/.anvil/resolve-stable-toolchain.ps1 new file mode 100644 index 000000000..e0351d6b0 --- /dev/null +++ b/.anvil/resolve-stable-toolchain.ps1 @@ -0,0 +1,156 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# GENERATED BY cargo-anvil. DO NOT EDIT DIRECTLY. + +param( + [switch] $ValidateWorkspaceMsrv, + [switch] $InstallIfMissing, + [switch] $ForEnvironment +) + +$ErrorActionPreference = 'Stop' +Set-StrictMode -Version 3 + +$repoRoot = Split-Path -Parent $PSScriptRoot + +function Get-SelectingToolchainFile { + foreach ($name in @('rust-toolchain', 'rust-toolchain.toml')) { + $path = Join-Path $repoRoot $name + if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { + continue + } + + $content = Get-Content -LiteralPath $path -Raw + if ($name -eq 'rust-toolchain' -and $content.TrimStart() -notmatch '^\[') { + return [pscustomobject]@{ Path = $path; Value = $content.Trim(); Source = 'file'; Installable = $false } + } + + $inToolchain = $false + foreach ($line in $content -split '\r?\n') { + if ($line -match '^\s*\[\s*toolchain\s*\]\s*(?:#.*)?$') { + $inToolchain = $true + continue + } + if ($line -match '^\s*\[') { + $inToolchain = $false + continue + } + if ($inToolchain -and $line -match '^\s*["'']?(channel|path)["'']?\s*=\s*(["''])([^"'']+)\2\s*(?:#.*)?$') { + $value = $Matches[3] + if ($Matches[1] -eq 'path' -and -not [IO.Path]::IsPathRooted($value)) { + $value = [IO.Path]::GetFullPath((Join-Path $repoRoot $value)) + } + return [pscustomobject]@{ + Path = $path + Value = $value + Source = 'file' + Installable = $false + } + } + } + } + + return $null +} + +function Get-RootMsrv { + $manifestPath = Join-Path $repoRoot 'Cargo.toml' + if (-not (Test-Path -LiteralPath $manifestPath -PathType Leaf)) { + throw "anvil: Cargo.toml not found at repository root '$repoRoot'" + } + + $values = @{} + $section = '' + foreach ($line in (Get-Content -LiteralPath $manifestPath)) { + if ($line -match '^\s*\[\s*([^\]]+)\s*\]\s*(?:#.*)?$') { + $section = $Matches[1].Trim() + continue + } + if ($section -in @('workspace.package', 'package') -and + $line -match '^\s*["'']?rust-version["'']?\s*=\s*(["''])([^"'']+)\1\s*(?:#.*)?$') { + $values[$section] = $Matches[2] + } + } + + if ($values.ContainsKey('workspace.package')) { + return $values['workspace.package'] + } + if ($values.ContainsKey('package')) { + return $values['package'] + } + + throw 'anvil: no selecting rust-toolchain(.toml) and no root [workspace.package] or [package] rust-version; declare an MSRV or select a toolchain explicitly' +} + +function Assert-UniformWorkspaceMsrv { + $manifestPath = Join-Path $repoRoot 'Cargo.toml' + $metadataText = (& cargo metadata --manifest-path $manifestPath --format-version 1 --no-deps 2>&1 | Out-String) + if ($LASTEXITCODE -ne 0) { + throw "anvil: cargo metadata failed while validating workspace MSRVs:`n$metadataText" + } + $metadata = $metadataText | ConvertFrom-Json + $memberIds = [Collections.Generic.HashSet[string]]::new( + [string[]] $metadata.workspace_members, + [StringComparer]::Ordinal + ) + $members = @($metadata.packages | Where-Object { $memberIds.Contains([string] $_.id) }) + $missing = @($members | Where-Object { [string]::IsNullOrWhiteSpace([string] $_.rust_version) } | ForEach-Object name) + if ($missing.Count -gt 0) { + throw "anvil: workspace packages without a resolved rust-version: $($missing -join ', '); declare one or select a toolchain explicitly" + } + + $versions = @($members | ForEach-Object { [string] $_.rust_version } | Sort-Object -Unique) + $rootMsrv = Get-RootMsrv + if ($versions.Count -ne 1 -or $versions[0] -ne $rootMsrv) { + throw "anvil: workspace packages must resolve to the root MSRV $rootMsrv; found $($versions -join ', '); select one catalog toolchain explicitly with rust-toolchain.toml" + } +} + +$fileSelection = Get-SelectingToolchainFile +if ($ValidateWorkspaceMsrv) { + $skipWorkspaceMsrvValidation = if ([string]::IsNullOrWhiteSpace($env:ANVIL_STABLE_TOOLCHAIN_SOURCE)) { + -not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN) + } else { + $env:ANVIL_STABLE_TOOLCHAIN_SOURCE -in @('environment', 'container') + } + if (-not $skipWorkspaceMsrvValidation -and $null -eq $fileSelection) { + Assert-UniformWorkspaceMsrv + } + exit 0 +} + +$selection = if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { + [pscustomobject]@{ Value = $env:RUSTUP_TOOLCHAIN; Source = 'environment'; Installable = $false } +} elseif ($null -ne $fileSelection) { + $fileSelection +} else { + [pscustomobject]@{ Value = Get-RootMsrv; Source = 'msrv'; Installable = $true } +} + +if ($InstallIfMissing) { + if (-not $selection.Installable) { + exit 0 + } + + $installed = rustup toolchain list + if ($LASTEXITCODE -ne 0) { + throw 'anvil: rustup toolchain list failed' + } + if ($installed -match "(?m)^$([regex]::Escape($selection.Value))(?:-|$)") { + exit 0 + } + + Write-Host "anvil: installing stable toolchain '$($selection.Value)'" + rustup toolchain install $selection.Value --profile minimal + if ($LASTEXITCODE -ne 0) { + throw "anvil: failed to install stable toolchain '$($selection.Value)'" + } + exit 0 +} + +if ($ForEnvironment -and $selection.Source -eq 'file') { + '' +} else { + $selection.Value +} +exit 0 diff --git a/.github/actions/anvil-setup/action.yml b/.github/actions/anvil-setup/action.yml index 9b09eca56..ed6fa2446 100644 --- a/.github/actions/anvil-setup/action.yml +++ b/.github/actions/anvil-setup/action.yml @@ -77,6 +77,24 @@ runs: } "Free after cleanup (GiB): $(Get-FreeDiskGiB)" + - name: Resolve stable toolchain + shell: pwsh -NoProfile -Command ". '{0}'" + run: | + $source = if ([string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { 'resolved' } else { 'environment' } + & '.anvil/resolve-stable-toolchain.ps1' -InstallIfMissing + if ($LASTEXITCODE -ne 0) { + throw 'anvil: failed to install stable toolchain' + } + $toolchain = (& '.anvil/resolve-stable-toolchain.ps1' | Out-String).Trim() + if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($toolchain)) { + throw 'anvil: failed to resolve stable toolchain' + } + $environmentValue = (& '.anvil/resolve-stable-toolchain.ps1' -ForEnvironment | Out-String).Trim() + if (-not [string]::IsNullOrWhiteSpace($environmentValue)) { + "RUSTUP_TOOLCHAIN=$environmentValue" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + } + "ANVIL_STABLE_TOOLCHAIN_SOURCE=$source" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + Write-Host "anvil: selected stable toolchain '$toolchain'" - id: rustc-version shell: bash run: echo "version=$(rustc --version | awk '{print $2}')" >> "$GITHUB_OUTPUT" @@ -120,19 +138,6 @@ runs: ~/.cargo/.crates2.json target/ - # rustup auto-installs the toolchain from rust-toolchain.toml on - # first cargo invocation, but it doesn't pull profile/component - # extras. The `anvil-setup` recipe in step "Install anvil - # toolchains + tools" below handles that exhaustively (the - # per-component install recipes in tools.just install - # default-toolchain components and pinned-nightly components for - # miri/careful/etc.) -- we don't add components inline here anymore. - # This step exists only to ensure rustup itself has the default - # toolchain set up so subsequent cargo / just calls work. - - name: Ensure default toolchain is installed - shell: bash - run: rustup show active-toolchain || rustup default stable - # The catalog recipe `anvil-setup` (or `anvil--setup` # when a group is specified via the `group` input) needs `just` to # run. We bootstrap just here (chicken-and-egg), then hand off diff --git a/crates/cargo-anvil/README.md b/crates/cargo-anvil/README.md index 7643b7943..17867d9fb 100644 --- a/crates/cargo-anvil/README.md +++ b/crates/cargo-anvil/README.md @@ -117,7 +117,8 @@ environment without installing those tools directly on the host. * Bash on Linux and WSL; `PowerShell` Core (`pwsh`) and WSL 2 on Windows. * `[script]` support enabled in the root `Justfile` (`set unstable` when required by the installed `just` version). -* A repository-owned `rust-toolchain.toml`. +* A root `Cargo.toml` whose MSRV is defined, unless a repository + `rust-toolchain` or `rust-toolchain.toml` selects the stable toolchain. * On Windows, Docker Engine running in the default WSL distribution: ```powershell @@ -444,7 +445,7 @@ And `docs/verification.md` for the continuous-validation strategy. This crate was developed as part of The Oxidizer Project. Browse this crate's source code. - [__cargo_doc2readme_dependencies_info]: ggGmYW0CYXZlMC43LjJhdIQbFhzZ8rzWNNYbuRaDSGWynFgbH4PMdoT7GNcbVwNPtPjAhvFhYvRhcoQbjXc_4J4IhEMbUEGBObScOREbL4ona_NAG7cbuZfYsFwUELhhZIGDa2NhcmdvLWFudmlsZTAuNS4wa2NhcmdvX2Fudmls + [__cargo_doc2readme_dependencies_info]: ggGmYW0CYXZlMC43LjJhdIQbFhzZ8rzWNNYbuRaDSGWynFgbH4PMdoT7GNcbVwNPtPjAhvFhYvRhcoQbnLd9XGSG1pUbKkS1MKyxFjEbACJD5-7hMiMbhc9RgQO7NuFhZIGDa2NhcmdvLWFudmlsZTAuNS4wa2NhcmdvX2Fudmls [__link0]: https://crates.io/crates/cargo-delta [__link1]: https://crates.io/crates/cargo-spellcheck [__link2]: https://crates.io/crates/cargo-coverage-gate diff --git a/crates/cargo-anvil/docs/design/README.md b/crates/cargo-anvil/docs/design/README.md index 54c4b6531..74f7eff1d 100644 --- a/crates/cargo-anvil/docs/design/README.md +++ b/crates/cargo-anvil/docs/design/README.md @@ -72,12 +72,12 @@ missed, and onboarding new Rust repos requires copying-and-praying. emitted templates contain no references to those harnesses; users wrap anvil's stages template in their compliance-extending pipeline themselves. See [ado.md](./ado.md). - Building a general-purpose cloud workflows compiler/IR. We share **check semantics**, not cloud workflows features. -- Owning `.cargo/config.toml`, `rust-toolchain.toml`, or workspace layout in `Cargo.toml`. -- Installing the Rust toolchain. msrustup owns it on 1ESPT; the runner image owns it on - GitHub-hosted runners; the user owns it locally. The tool validates `rustc` version at - recipe time and produces a clean failure when it doesn't meet the catalog minimum. - Future work: warn (not fail) when the locally-installed toolchain drifts materially - from the version the catalog targets, so local results stay predictive of cloud workflows. +- Owning `.cargo/config.toml`, `rust-toolchain`, `rust-toolchain.toml`, or workspace layout in + `Cargo.toml`. +- Owning how private or path toolchains are provisioned. Anvil deterministically selects an + existing override, a repository toolchain file, or the repository MSRV. GitHub setup and + container construction install a missing public channel through rustup; internal and path + toolchains remain the execution environment's responsibility. - Managing exact tool versions on the user's behalf — we enforce minimums only. See [local.md §3](./local.md#3-tool-versions-and-installation). - Hosting a service. The tool is a CLI binary; updates ship via crates.io. @@ -244,7 +244,7 @@ repo/ ├── rustfmt.toml managed-region: anvil-rustfmt (opt out with empty stub) ├── .delta.toml managed-region: anvil-delta (points to owned cloud config) ├── .gitattributes managed-region: anvil-gitattributes (pins *.rs to LF) -├── rust-toolchain.toml user-authored (read only) +├── rust-toolchain[.toml] optional, user-authored (read only) ├── .cargo/config.toml user-authored (read only) │ ├── .github/ only if --backend github (or autodetected) — see github.md @@ -305,11 +305,12 @@ Detail on each host: - **`.gitattributes`** — managed region pinning `*.rs text eol=lf` so Rust sources keep LF line endings on every platform (rustfmt and other tools assume LF). Created if absent; users add their own attribute rules outside the region. -- **`rust-toolchain.toml`** and **`.cargo/config.toml`** — never touched. Read-only inputs - used by `anvil-tool-rustc-validate-prereqs` to validate the user's `rustc` version - against the catalog minimum. the cloud workflow building blocks do not install Rust; that is the - user's pipeline's job - (msrustup in 1ESPT, rustup on GH runners). +- **`rust-toolchain`**, **`rust-toolchain.toml`**, and **`.cargo/config.toml`** — never + touched. Toolchain files are optional read-only inputs to stable-toolchain selection. + When neither selects a toolchain, Anvil selects the root manifest's + `[workspace.package].rust-version` or `[package].rust-version`. GitHub setup installs a + missing public selection through rustup. ADO pipelines provision their selected internal + toolchain before Anvil runs. The tool's persistent state lives in `.anvil.lock` at the repo root — the sidecar manifest tracking last-rendered checksums per owned file and per managed region. See diff --git a/crates/cargo-anvil/docs/design/ado.md b/crates/cargo-anvil/docs/design/ado.md index df3e7059f..b2bb240b8 100644 --- a/crates/cargo-anvil/docs/design/ado.md +++ b/crates/cargo-anvil/docs/design/ado.md @@ -646,28 +646,33 @@ mechanics are in [local.md §4](./local.md#4-impact-scoping-pass-through-env-var ## 6. Rust toolchain -anvil does not install Rust on ADO. The step templates assume `cargo` is on PATH. The -user's root pipeline (or compliance template) installs Rust before the anvil stages run. +The user's root pipeline or compliance template installs Rust before the Anvil stages +run. The generated setup step then resolves the catalog toolchain and publishes +environment/MSRV selections as `RUSTUP_TOOLCHAIN` for subsequent steps before it +captures `rustc --version` and restores the cache. A selecting toolchain file remains +unset in the environment so rustup processes the complete file. -Why anvil doesn't ship a Rust install step: +Selection follows the shared local contract: an existing `RUSTUP_TOOLCHAIN`, then a +root toolchain file with `channel` or `path`, then the root MSRV. Internal pipelines +set `RUSTUP_TOOLCHAIN=ms-prod-*` so it overrides a public toolchain file or MSRV, +while their existing installer-specific setting continues to tell msrustup which +toolchain to provision. Current msrustup honors the standard override precedence. -- **1ESPT compliance.** Compliance pipelines install Rust via msrustup - (Microsoft-internal). The standard `RustInstaller` ADO task is not used. anvil must - emit nothing that conflicts with that. -- **Toolchain choice is a repo decision.** msrustup channels (`ms-prod-1.93`, etc.) are - repo-policy questions anvil has no business making. +Anvil does not install stable Rust on ADO: + +- **1ESPT compliance.** Compliance pipelines provision Rust through msrustup. +- **Backend mapping is a repository decision.** Mapping a public MSRV to an internal + `ms-prod-*` channel belongs in the root pipeline, not the catalog. In the OSS / non-1ESPT case, the user adds a `RustInstaller@1` task (or a rustup shell script) to their root pipeline before the anvil stages template runs. A typical placement: a setup stage that `dependsOn`s nothing and runs first, followed by the anvil stages. -`anvil-tool-rustc-validate-prereqs` (depended on by every check that needs rustc) -validates the installed `rustc` against the catalog minimum at recipe time; a -below-minimum `rustc` produces a clean failure message. For nightly-requiring -checks (miri, careful, udeps), the matching toolchain-validate-prereqs recipe -fails with a suggestion to ask the team's pipeline owner to add `nightly` to -msrustup. +`anvil-tool-rustc-validate-prereqs` validates the selected compiler and the +uniform-MSRV fallback rule. For nightly-requiring checks, the matching +toolchain-validate-prereqs recipe fails with a suggestion to ask the team's pipeline +owner to add that dated nightly to msrustup. ## 7. Caching diff --git a/crates/cargo-anvil/docs/design/containers.md b/crates/cargo-anvil/docs/design/containers.md index 1499d446e..dbdcd14c8 100644 --- a/crates/cargo-anvil/docs/design/containers.md +++ b/crates/cargo-anvil/docs/design/containers.md @@ -126,8 +126,8 @@ The driver: ## 5. Image construction and identity The public `Containerfile` starts from a pinned public Linux base and installs -`just`, Rustup, and PowerShell. It copies the generated Anvil tree and the -repository-owned `rust-toolchain.toml`, then runs: +`just`, Rustup, and PowerShell. It copies the generated Anvil tree, root +`Cargo.toml`, and optional repository toolchain file, then runs: ```text just anvil-setup @@ -138,7 +138,9 @@ of truth for Rust toolchains and Cargo tools. The local image tag is a SHA-256 hash of build-relevant repository content: -- `rust-toolchain.toml`; +- root `Cargo.toml`; +- an optional `rust-toolchain` or `rust-toolchain.toml`; +- the generated stable-toolchain resolver; - generated `justfiles/anvil/**/*.just` recipes; - the `Containerfile`, `Containerfile.dockerignore`, entrypoint, and other static image inputs. @@ -159,8 +161,15 @@ The next invocation builds that image, while images for older branches remain available. Runtime execution uses `--pull=never` and never substitutes `latest`. -Container execution requires a `rust-toolchain.toml` in the repository root. It -does not choose a default Rust channel when that file is absent. +Container execution uses the same deterministic stable-toolchain selection as +native execution: an existing `RUSTUP_TOOLCHAIN`, a selecting repository +toolchain file, or the root manifest's MSRV. It fails rather than choosing a +runner or image default when none is available. + +The restricted image-construction context does not contain workspace member +manifests, so uniform per-package MSRV validation runs in native and cloud setup +rather than during image construction. Container checks still use the selected +root MSRV; a package that requires a newer compiler fails normally. The public default is digest-pinned Debian Bookworm. A user or automation can select another image compatible with the generated Debian-based @@ -352,7 +361,8 @@ Host requirements: Windows; `wsl -e docker version` must succeed and the driver does not invoke Windows `docker.exe`; - `linux/amd64` execution support; -- a repository-owned `rust-toolchain.toml`. +- a root `Cargo.toml` whose MSRV is defined, unless a repository toolchain file + selects the stable toolchain. Runtime controls: diff --git a/crates/cargo-anvil/docs/design/github.md b/crates/cargo-anvil/docs/design/github.md index 35192935b..58db6e6ef 100644 --- a/crates/cargo-anvil/docs/design/github.md +++ b/crates/cargo-anvil/docs/design/github.md @@ -712,7 +712,8 @@ annotation. The matcher promotes that existing diagnostic without wrapping Just, parsing its output in a custom runner, or repeating group membership in the action. -The action does not install Rust; it expects `cargo` on PATH (see §7). +The action expects the rustup proxies on `PATH` and installs a missing selected public +toolchain (see §7). `anvil-impact` is described in §6 below. Its optional `free-disk-space` input defaults to `false`. When enabled on a @@ -762,19 +763,21 @@ The check → bucket mapping is in ## 7. Rust toolchain -anvil does not install Rust on GitHub. The composite actions assume `cargo` is on PATH. -GH-hosted runners ship with a recent stable Rust and `rustup` pre-installed; if your -`rust-toolchain.toml` pins a different channel, the first `cargo` invocation in a job -triggers `rustup` to download the pinned toolchain. For a published stable channel this -typically takes 10–30 seconds on Linux (somewhat longer on Windows and longer still for -nightly with components). The auto-install runs once per job and is not cached across -jobs by anvil — `~/.rustup` has high invalidation churn and the install cost is small -relative to the cached cargo registry / `target/` paths (§8). Repos that want to skip -even this per-job overhead can add their own toolchain-install step (e.g. -`dtolnay/rust-toolchain@stable`) before the anvil composite action runs. - -On self-hosted runners or pre-baked images without rustup, the user adds a Rust install -step to their root workflow before the `uses:` of the reusable workflow: +The setup action resolves the catalog's stable toolchain before reading `rustc` or +restoring build artifacts. It installs a missing public channel through rustup, exports +environment/MSRV selections through `GITHUB_ENV`, leaves selecting toolchain files for +rustup to process natively, and keys the cache on the resulting compiler version. All +matrix legs therefore use the same repository selection instead of the different stable +versions that runner images may carry. + +Selection follows the shared local contract: an existing `RUSTUP_TOOLCHAIN`, then a +root toolchain file with `channel` or `path`, then the root MSRV. There is no +runner-default fallback. GH-hosted runners provide the rustup proxy used to install a +selected public channel. A repository that supplies a path toolchain must provision +that path before the Anvil action runs. + +On self-hosted runners or pre-baked images without rustup, the user provisions the +selected toolchain before the `uses:` of the reusable workflow: ```yaml jobs: diff --git a/crates/cargo-anvil/docs/design/local.md b/crates/cargo-anvil/docs/design/local.md index 69db4dbdb..fa19c70ee 100644 --- a/crates/cargo-anvil/docs/design/local.md +++ b/crates/cargo-anvil/docs/design/local.md @@ -362,21 +362,42 @@ invocations like `just anvil-miri` fail loudly if a required tool is missing or predates the catalog minimum, with a one-line hint pointing at the matching `anvil-tool--install` recipe. -### 3.5 The Rust toolchain - -`rust-toolchain.toml` is read but never written, and anvil never installs the *project's* -Rust toolchain itself. Per-backend rationale lives in [github.md](./github.md#rust-toolchain) -and [ado.md](./ado.md#rust-toolchain); short version: msrustup owns it on ADO/1ESPT, the -runner image owns it on GH, the user owns it locally. - -`anvil-tool-rustc-validate-prereqs` validates the installed `rustc` against the -catalog's minimum at recipe time; a below-minimum `rustc` produces a clean failure -message naming the version mismatch. Per-check toolchain requirements (e.g. miri, -careful, udeps need nightly) are enforced by the matching -`anvil-toolchain--validate-prereqs` recipe, which suggests the -user-environment-appropriate install command in the failure message -(`rustup install nightly-YYYY-MM-DD` or "ask your team's pipeline owner to add -nightly to msrustup"). +### 3.5 The stable Rust toolchain + +Anvil selects one deterministic toolchain for every check that otherwise uses the +ambient stable toolchain. The selection order is: + +1. `RUSTUP_TOOLCHAIN`, when the caller already set it. This is the standard rustup + override and lets internal pipelines select an installed `ms-prod-*` toolchain. +2. The `channel` or `path` selected by a root `rust-toolchain` or + `rust-toolchain.toml`. The legacy file name wins when both exist, matching rustup. +3. The root package or `[workspace.package]` `rust-version`, treated as the + repository MSRV. + +There is no runner-default fallback. A repository with neither a selecting toolchain +file nor a root MSRV fails with an actionable setup error rather than inheriting a +compiler that can change with the machine image. + +The generated `.anvil/resolve-stable-toolchain.ps1` implements this contract. For +an existing environment override or MSRV fallback, the recipe tree exports the +result as `RUSTUP_TOOLCHAIN`. For a selecting toolchain file it leaves that +variable empty so rustup processes the complete file, including its components, +targets, and profile. `just anvil-*` therefore behaves the same locally and in +cloud workflows without changing the user's global rustup default. Raw `cargo` +commands outside Anvil retain normal rustup behavior. + +When selection falls back to the root MSRV, prerequisite validation reads +`cargo metadata` and requires every workspace package to resolve to that same +`rust_version`. Workspaces with missing or heterogeneous package MSRVs must choose a +single catalog toolchain explicitly with a selecting toolchain file. Anvil does not +build a per-package toolchain matrix for this uncommon case. + +`anvil-tool-rustc-validate-prereqs` verifies the selected compiler and the uniform +MSRV rule. Per-check toolchain requirements (for example, miri, careful, and udeps +need nightly) remain enforced by the matching +`anvil-toolchain--validate-prereqs` recipe. Explicit `cargo +toolchain` +arguments retain rustup's higher precedence, so these checks continue to use the +catalog's dated nightly pins. ### 3.6 Nightly pinning diff --git a/crates/cargo-anvil/src/anvil/artifacts/container.rs b/crates/cargo-anvil/src/anvil/artifacts/container.rs index e68fcdb9f..d01b6990e 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/container.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/container.rs @@ -188,7 +188,8 @@ mod tests { } fn write_image_id_fixture(root: &Path) { - write(&root.join("rust-toolchain.toml"), "channel = \"1.93\"\n"); + write(&root.join("Cargo.toml"), "[workspace.package]\nrust-version = \"1.93\"\n"); + write(&root.join(".anvil/resolve-stable-toolchain.ps1"), "Write-Output '1.93'\n"); write(&root.join("justfiles/anvil/versions.just"), "tool_version := \"1\"\n"); write( &root.join(CONTAINERFILE_PATH), @@ -228,9 +229,10 @@ mod tests { assert!(CONTAINERFILE.contains("just anvil-setup")); assert!(CONTAINERFILE.contains("COPY . .")); assert!(IGNORE.contains("!.anvil/container/*")); + assert!(IGNORE.contains("!.anvil/resolve-stable-toolchain.ps1")); assert!(IGNORE.contains("!justfiles/anvil/checks/*.just")); assert!(CONTAINERFILE.contains("anvil_runner := \\\"native\\\"")); - assert!(CONTAINERFILE.contains("requires rust-toolchain.toml")); + assert!(!CONTAINERFILE.contains("requires rust-toolchain.toml")); assert!(CONTAINERFILE.contains("anvil-container-entrypoint")); } @@ -340,7 +342,11 @@ mod tests { crate::anvil::artifacts::anvil_artifacts() .into_iter() .filter_map(|artifact| match artifact { - Artifact::OwnedFile(spec) if spec.path.starts_with("justfiles/") || spec.path.starts_with(".anvil/container/") => { + Artifact::OwnedFile(spec) + if spec.path.starts_with("justfiles/") + || spec.path.starts_with(".anvil/container/") + || spec.path == ".anvil/resolve-stable-toolchain.ps1" => + { Some(spec.path) } _ => None, @@ -359,6 +365,8 @@ mod tests { let mut included: Vec<&str> = vec![ // Repository-owned rather than catalog-owned, but a required // image input all the same. + "Cargo.toml", + "rust-toolchain", "rust-toolchain.toml", ]; included.extend(catalog_context_inputs()); @@ -395,7 +403,6 @@ mod tests { "justfiles/anvil/container/run-in-container.sh", // Everything else stays out: the working tree is bind-mounted at // run time rather than baked into the image. - "Cargo.toml", "crates/example/src/lib.rs", "justfiles/basic.just", "justfiles/anvil/notes.md", diff --git a/crates/cargo-anvil/src/anvil/artifacts/justfile.rs b/crates/cargo-anvil/src/anvil/artifacts/justfile.rs index 7b8d9d299..765e7283a 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/justfile.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/justfile.rs @@ -27,6 +27,12 @@ const VERSIONS_JUST: &str = include_str!("../../../templates/justfiles/anvil/ver /// Repo-root-relative path of the pinned-versions recipe file. const VERSIONS_JUST_PATH: &str = "justfiles/anvil/versions.just"; +/// Stable-toolchain resolver shared by local recipes and cloud setup. +const STABLE_TOOLCHAIN_RESOLVER: &str = include_str!("../../../templates/anvil/resolve-stable-toolchain.ps1"); + +/// Repo-root-relative path of the stable-toolchain resolver. +const STABLE_TOOLCHAIN_RESOLVER_PATH: &str = ".anvil/resolve-stable-toolchain.ps1"; + /// Contents of `justfiles/anvil/tools.just` baked into the binary. const TOOLS_JUST: &str = include_str!("../../../templates/justfiles/anvil/tools.just"); @@ -186,6 +192,12 @@ pub fn versions() -> Artifact { Artifact::owned_file(VERSIONS_JUST_PATH, VERSIONS_JUST) } +/// `.anvil/resolve-stable-toolchain.ps1` — deterministic stable selection. +#[must_use] +pub fn stable_toolchain_resolver() -> Artifact { + Artifact::owned_file(STABLE_TOOLCHAIN_RESOLVER_PATH, STABLE_TOOLCHAIN_RESOLVER) +} + /// `justfiles/anvil/tools.just` — tool install / prereq recipes. #[must_use] pub fn tools() -> Artifact { @@ -452,6 +464,134 @@ mod tests { } } + #[test] + fn stable_toolchain_resolution_is_exported() { + assert!(VERSIONS_JUST.contains("rust_stable_override := env_var_or_default(")); + assert!(VERSIONS_JUST.contains("shell('pwsh -NoProfile -File .anvil/resolve-stable-toolchain.ps1 -ForEnvironment')")); + assert!(VERSIONS_JUST.contains("export RUSTUP_TOOLCHAIN := rust_stable")); + assert!(VERSIONS_JUST.contains("export ANVIL_STABLE_TOOLCHAIN_SOURCE :=")); + assert!(STABLE_TOOLCHAIN_RESOLVER.contains("RUSTUP_TOOLCHAIN")); + assert!(STABLE_TOOLCHAIN_RESOLVER.contains("rust-version")); + assert!(STABLE_TOOLCHAIN_RESOLVER.contains("ValidateWorkspaceMsrv")); + assert!(STABLE_TOOLCHAIN_RESOLVER.contains("InstallIfMissing")); + } + + mod stable_toolchain_resolver_tests { + use std::fs; + use std::path::Path; + use std::process::{Command, Output}; + + use tempfile::TempDir; + + use super::STABLE_TOOLCHAIN_RESOLVER; + + fn fixture(cargo_toml: &str) -> TempDir { + let temp = TempDir::new().expect("temporary repository must be creatable"); + fs::create_dir(temp.path().join(".anvil")).expect("resolver directory must be creatable"); + fs::write(temp.path().join(".anvil/resolve-stable-toolchain.ps1"), STABLE_TOOLCHAIN_RESOLVER) + .expect("resolver fixture must be writable"); + fs::write(temp.path().join("Cargo.toml"), cargo_toml).expect("manifest fixture must be writable"); + temp + } + + fn run(root: &Path, args: &[&str], rustup_toolchain: Option<&str>) -> Output { + let mut command = Command::new("pwsh"); + command + .args(["-NoProfile", "-File", ".anvil/resolve-stable-toolchain.ps1"]) + .args(args) + .current_dir(root) + .env_remove("ANVIL_STABLE_TOOLCHAIN_SOURCE"); + match rustup_toolchain { + Some(value) => { + command.env("RUSTUP_TOOLCHAIN", value); + } + None => { + command.env_remove("RUSTUP_TOOLCHAIN"); + } + } + command.output().expect("pwsh must be available to test the generated resolver") + } + + fn resolved(root: &Path, rustup_toolchain: Option<&str>) -> String { + let output = run(root, &[], rustup_toolchain); + assert!( + output.status.success(), + "resolver failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + String::from_utf8(output.stdout) + .expect("resolver output must be UTF-8") + .trim() + .to_owned() + } + + #[test] + fn honors_environment_files_and_msrv_in_precedence_order() { + let temp = fixture("[workspace.package]\nrust-version = \"1.93\"\n"); + let root = temp.path(); + assert_eq!(resolved(root, None), "1.93"); + assert_eq!( + String::from_utf8(run(root, &["-ForEnvironment"], None).stdout) + .expect("resolver output must be UTF-8") + .trim(), + "1.93" + ); + + fs::write(root.join("rust-toolchain.toml"), "[toolchain]\ncomponents = [\"clippy\"]\n") + .expect("toolchain fixture must be writable"); + assert_eq!(resolved(root, None), "1.93"); + + fs::write(root.join("rust-toolchain.toml"), "[toolchain]\nchannel = \"1.94\"\n").expect("toolchain fixture must be writable"); + assert_eq!(resolved(root, None), "1.94"); + assert!( + String::from_utf8(run(root, &["-ForEnvironment"], None).stdout) + .expect("resolver output must be UTF-8") + .trim() + .is_empty() + ); + + fs::write(root.join("rust-toolchain"), "1.92\n").expect("legacy toolchain fixture must be writable"); + assert_eq!(resolved(root, None), "1.92"); + assert_eq!(resolved(root, Some("custom-toolchain")), "custom-toolchain"); + assert_eq!( + String::from_utf8(run(root, &["-ForEnvironment"], Some("custom-toolchain")).stdout) + .expect("resolver output must be UTF-8") + .trim(), + "custom-toolchain" + ); + } + + #[test] + fn rejects_missing_and_heterogeneous_workspace_msrvs() { + let missing = fixture("[workspace]\nresolver = \"2\"\n"); + let output = run(missing.path(), &[], None); + assert!(!output.status.success()); + assert!(String::from_utf8_lossy(&output.stderr).contains("no root [workspace.package]")); + + let heterogeneous = + fixture("[workspace]\nresolver = \"2\"\nmembers = [\"a\", \"b\"]\n[workspace.package]\nrust-version = \"1.92\"\n"); + for (name, version) in [("a", "1.92"), ("b", "1.93")] { + let member = heterogeneous.path().join(name); + fs::create_dir(&member).expect("member directory must be creatable"); + fs::write( + member.join("Cargo.toml"), + format!( + "[package]\nname = \"{name}\"\nversion = \"0.1.0\"\nedition = \"2021\"\nrust-version = \"{version}\"\n[lib]\npath = \"lib.rs\"\n" + ), + ) + .expect("member manifest must be writable"); + fs::write(member.join("lib.rs"), "").expect("member source must be writable"); + } + + let output = run(heterogeneous.path(), &["-ValidateWorkspaceMsrv"], None); + assert!(!output.status.success()); + assert!(String::from_utf8_lossy(&output.stderr).contains("must resolve to the root MSRV")); + + let output = run(heterogeneous.path(), &["-ValidateWorkspaceMsrv"], Some("explicit-toolchain")); + assert!(output.status.success(), "explicit override must permit heterogeneous MSRVs"); + } + } + #[test] fn mod_just_imports_siblings_and_defines_alias() { for needle in [ diff --git a/crates/cargo-anvil/src/anvil/artifacts/mod.rs b/crates/cargo-anvil/src/anvil/artifacts/mod.rs index 45d0fad9a..7d6d0e996 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/mod.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/mod.rs @@ -36,6 +36,7 @@ pub(crate) fn anvil_artifacts() -> Vec { justfile::entry(), justfile::tools(), justfile::versions(), + justfile::stable_toolchain_resolver(), justfile::helpers(), justfile::runner(), justfile::tiers(), @@ -81,6 +82,7 @@ mod tests { let singletons = [ justfile::entry(), justfile::versions(), + justfile::stable_toolchain_resolver(), justfile::tools(), justfile::helpers(), justfile::runner(), diff --git a/crates/cargo-anvil/src/lib.rs b/crates/cargo-anvil/src/lib.rs index 696c9309b..16ab49f22 100644 --- a/crates/cargo-anvil/src/lib.rs +++ b/crates/cargo-anvil/src/lib.rs @@ -118,7 +118,8 @@ //! - Bash on Linux and WSL; `PowerShell` Core (`pwsh`) and WSL 2 on Windows. //! - `[script]` support enabled in the root `Justfile` (`set unstable` when //! required by the installed `just` version). -//! - A repository-owned `rust-toolchain.toml`. +//! - A root `Cargo.toml` whose MSRV is defined, unless a repository +//! `rust-toolchain` or `rust-toolchain.toml` selects the stable toolchain. //! - On Windows, Docker Engine running in the default WSL distribution: //! //! ```powershell diff --git a/crates/cargo-anvil/templates/ado/steps/setup.yml b/crates/cargo-anvil/templates/ado/steps/setup.yml index 11327a349..f1c7353be 100644 --- a/crates/cargo-anvil/templates/ado/steps/setup.yml +++ b/crates/cargo-anvil/templates/ado/steps/setup.yml @@ -18,6 +18,19 @@ parameters: type: string default: '' steps: + - pwsh: | + $source = if ([string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { 'resolved' } else { 'environment' } + $toolchain = (& '.anvil/resolve-stable-toolchain.ps1' | Out-String).Trim() + if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($toolchain)) { + throw 'anvil: failed to resolve stable toolchain' + } + $environmentValue = (& '.anvil/resolve-stable-toolchain.ps1' -ForEnvironment | Out-String).Trim() + if (-not [string]::IsNullOrWhiteSpace($environmentValue)) { + Write-Host "##vso[task.setvariable variable=RUSTUP_TOOLCHAIN]$environmentValue" + } + Write-Host "##vso[task.setvariable variable=ANVIL_STABLE_TOOLCHAIN_SOURCE]$source" + Write-Host "anvil: selected stable toolchain '$toolchain'" + displayName: anvil setup (resolve stable toolchain) - bash: echo "##vso[task.setvariable variable=anvil_rustc_version]$(rustc --version | awk '{print $2}')" displayName: anvil setup (capture rustc version) - task: Cache@2 diff --git a/crates/cargo-anvil/templates/anvil/container/Containerfile b/crates/cargo-anvil/templates/anvil/container/Containerfile index fa68b18f1..dc4a709ef 100644 --- a/crates/cargo-anvil/templates/anvil/container/Containerfile +++ b/crates/cargo-anvil/templates/anvil/container/Containerfile @@ -49,15 +49,14 @@ RUN curl --proto '=https' --tlsv1.2 -fsSLo /tmp/rustup-init \ WORKDIR /opt/anvil COPY . . -RUN test -f rust-toolchain.toml || { \ - echo "anvil-container requires rust-toolchain.toml" >&2; \ - exit 1; \ - } RUN --mount=type=cache,id=anvil-cargo-registry,target=/usr/local/cargo/registry \ --mount=type=cache,id=anvil-cargo-git,target=/usr/local/cargo/git \ --mount=type=cache,id=anvil-cargo-target,target=/tmp/anvil-target \ - printf "anvil_runner := \"native\"\nimport 'justfiles/anvil/mod.just'\n" > Justfile \ - && CARGO_TARGET_DIR=/tmp/anvil-target just anvil-setup + pwsh -NoProfile -File .anvil/resolve-stable-toolchain.ps1 -InstallIfMissing \ + && toolchain="$(pwsh -NoProfile -File .anvil/resolve-stable-toolchain.ps1 -ForEnvironment)" \ + && printf "anvil_runner := \"native\"\nimport 'justfiles/anvil/mod.just'\n" > Justfile \ + && RUSTUP_TOOLCHAIN="$toolchain" ANVIL_STABLE_TOOLCHAIN_SOURCE=container \ + CARGO_TARGET_DIR=/tmp/anvil-target just anvil-setup COPY .anvil/container/entrypoint.sh /usr/local/bin/anvil-container-entrypoint RUN chmod 755 /usr/local/bin/anvil-container-entrypoint diff --git a/crates/cargo-anvil/templates/anvil/container/Containerfile.dockerignore b/crates/cargo-anvil/templates/anvil/container/Containerfile.dockerignore index 6566e6571..6aca85683 100644 --- a/crates/cargo-anvil/templates/anvil/container/Containerfile.dockerignore +++ b/crates/cargo-anvil/templates/anvil/container/Containerfile.dockerignore @@ -16,10 +16,13 @@ # contract in the allow-list too, at every depth, because a deeper candidate # always has an ancestor of exactly that shape. ** +!Cargo.toml +!rust-toolchain !rust-toolchain.toml !justfiles/anvil/*.just !justfiles/anvil/checks/*.just !justfiles/anvil/groups/*.just +!.anvil/resolve-stable-toolchain.ps1 !.anvil/container/* .anvil/container/*/* .anvil/container/customize.sh diff --git a/crates/cargo-anvil/templates/anvil/container/image-id.ps1 b/crates/cargo-anvil/templates/anvil/container/image-id.ps1 index dfc6a53f3..c95dd2c70 100644 --- a/crates/cargo-anvil/templates/anvil/container/image-id.ps1 +++ b/crates/cargo-anvil/templates/anvil/container/image-id.ps1 @@ -14,11 +14,13 @@ if ($LASTEXITCODE -ne 0 -or -not $repoRoot) { } $inputs = @( - 'rust-toolchain.toml' + 'Cargo.toml', + '.anvil/resolve-stable-toolchain.ps1' ) -$toolchainPath = Join-Path $repoRoot 'rust-toolchain.toml' -if (-not (Test-Path -LiteralPath $toolchainPath -PathType Leaf)) { - throw 'anvil-container requires a repository-owned rust-toolchain.toml.' +foreach ($toolchainFile in @('rust-toolchain', 'rust-toolchain.toml')) { + if (Test-Path -LiteralPath (Join-Path $repoRoot $toolchainFile) -PathType Leaf) { + $inputs += $toolchainFile + } } $containerPath = Join-Path $repoRoot '.anvil/container' $containerRecipe = 'justfiles/anvil/container.just' diff --git a/crates/cargo-anvil/templates/anvil/container/image-id.sh b/crates/cargo-anvil/templates/anvil/container/image-id.sh index e0ed81051..f399e349b 100644 --- a/crates/cargo-anvil/templates/anvil/container/image-id.sh +++ b/crates/cargo-anvil/templates/anvil/container/image-id.sh @@ -7,12 +7,6 @@ if ! repo_root="$(git rev-parse --show-toplevel 2>/dev/null)"; then exit 1 fi -toolchain_path="$repo_root/rust-toolchain.toml" -if [[ ! -f "$toolchain_path" ]]; then - echo 'anvil-container requires a repository-owned rust-toolchain.toml.' >&2 - exit 1 -fi - container_dir="$repo_root/.anvil/container" container_recipe="justfiles/anvil/container.just" default_base_image="$(sed -n 's/^ARG BASE_IMAGE=//p' "$container_dir/Containerfile" | head -n 1)" @@ -25,7 +19,12 @@ if [[ ! "$base_image" =~ @sha256:[0-9a-fA-F]{64}$ ]]; then echo 'anvil-container: ANVIL_CONTAINER_BASE_IMAGE must be pinned by sha256 digest (image@sha256:<64 hex characters>).' >&2 exit 1 fi -inputs=(rust-toolchain.toml) +inputs=(Cargo.toml .anvil/resolve-stable-toolchain.ps1) +for toolchain_file in rust-toolchain rust-toolchain.toml; do + if [[ -f "$repo_root/$toolchain_file" ]]; then + inputs+=("$toolchain_file") + fi +done while IFS= read -r path; do relative="${path#"$repo_root"/}" # The container entry recipe drives execution on the host; it is not diff --git a/crates/cargo-anvil/templates/anvil/resolve-stable-toolchain.ps1 b/crates/cargo-anvil/templates/anvil/resolve-stable-toolchain.ps1 new file mode 100644 index 000000000..e0351d6b0 --- /dev/null +++ b/crates/cargo-anvil/templates/anvil/resolve-stable-toolchain.ps1 @@ -0,0 +1,156 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# GENERATED BY cargo-anvil. DO NOT EDIT DIRECTLY. + +param( + [switch] $ValidateWorkspaceMsrv, + [switch] $InstallIfMissing, + [switch] $ForEnvironment +) + +$ErrorActionPreference = 'Stop' +Set-StrictMode -Version 3 + +$repoRoot = Split-Path -Parent $PSScriptRoot + +function Get-SelectingToolchainFile { + foreach ($name in @('rust-toolchain', 'rust-toolchain.toml')) { + $path = Join-Path $repoRoot $name + if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { + continue + } + + $content = Get-Content -LiteralPath $path -Raw + if ($name -eq 'rust-toolchain' -and $content.TrimStart() -notmatch '^\[') { + return [pscustomobject]@{ Path = $path; Value = $content.Trim(); Source = 'file'; Installable = $false } + } + + $inToolchain = $false + foreach ($line in $content -split '\r?\n') { + if ($line -match '^\s*\[\s*toolchain\s*\]\s*(?:#.*)?$') { + $inToolchain = $true + continue + } + if ($line -match '^\s*\[') { + $inToolchain = $false + continue + } + if ($inToolchain -and $line -match '^\s*["'']?(channel|path)["'']?\s*=\s*(["''])([^"'']+)\2\s*(?:#.*)?$') { + $value = $Matches[3] + if ($Matches[1] -eq 'path' -and -not [IO.Path]::IsPathRooted($value)) { + $value = [IO.Path]::GetFullPath((Join-Path $repoRoot $value)) + } + return [pscustomobject]@{ + Path = $path + Value = $value + Source = 'file' + Installable = $false + } + } + } + } + + return $null +} + +function Get-RootMsrv { + $manifestPath = Join-Path $repoRoot 'Cargo.toml' + if (-not (Test-Path -LiteralPath $manifestPath -PathType Leaf)) { + throw "anvil: Cargo.toml not found at repository root '$repoRoot'" + } + + $values = @{} + $section = '' + foreach ($line in (Get-Content -LiteralPath $manifestPath)) { + if ($line -match '^\s*\[\s*([^\]]+)\s*\]\s*(?:#.*)?$') { + $section = $Matches[1].Trim() + continue + } + if ($section -in @('workspace.package', 'package') -and + $line -match '^\s*["'']?rust-version["'']?\s*=\s*(["''])([^"'']+)\1\s*(?:#.*)?$') { + $values[$section] = $Matches[2] + } + } + + if ($values.ContainsKey('workspace.package')) { + return $values['workspace.package'] + } + if ($values.ContainsKey('package')) { + return $values['package'] + } + + throw 'anvil: no selecting rust-toolchain(.toml) and no root [workspace.package] or [package] rust-version; declare an MSRV or select a toolchain explicitly' +} + +function Assert-UniformWorkspaceMsrv { + $manifestPath = Join-Path $repoRoot 'Cargo.toml' + $metadataText = (& cargo metadata --manifest-path $manifestPath --format-version 1 --no-deps 2>&1 | Out-String) + if ($LASTEXITCODE -ne 0) { + throw "anvil: cargo metadata failed while validating workspace MSRVs:`n$metadataText" + } + $metadata = $metadataText | ConvertFrom-Json + $memberIds = [Collections.Generic.HashSet[string]]::new( + [string[]] $metadata.workspace_members, + [StringComparer]::Ordinal + ) + $members = @($metadata.packages | Where-Object { $memberIds.Contains([string] $_.id) }) + $missing = @($members | Where-Object { [string]::IsNullOrWhiteSpace([string] $_.rust_version) } | ForEach-Object name) + if ($missing.Count -gt 0) { + throw "anvil: workspace packages without a resolved rust-version: $($missing -join ', '); declare one or select a toolchain explicitly" + } + + $versions = @($members | ForEach-Object { [string] $_.rust_version } | Sort-Object -Unique) + $rootMsrv = Get-RootMsrv + if ($versions.Count -ne 1 -or $versions[0] -ne $rootMsrv) { + throw "anvil: workspace packages must resolve to the root MSRV $rootMsrv; found $($versions -join ', '); select one catalog toolchain explicitly with rust-toolchain.toml" + } +} + +$fileSelection = Get-SelectingToolchainFile +if ($ValidateWorkspaceMsrv) { + $skipWorkspaceMsrvValidation = if ([string]::IsNullOrWhiteSpace($env:ANVIL_STABLE_TOOLCHAIN_SOURCE)) { + -not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN) + } else { + $env:ANVIL_STABLE_TOOLCHAIN_SOURCE -in @('environment', 'container') + } + if (-not $skipWorkspaceMsrvValidation -and $null -eq $fileSelection) { + Assert-UniformWorkspaceMsrv + } + exit 0 +} + +$selection = if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { + [pscustomobject]@{ Value = $env:RUSTUP_TOOLCHAIN; Source = 'environment'; Installable = $false } +} elseif ($null -ne $fileSelection) { + $fileSelection +} else { + [pscustomobject]@{ Value = Get-RootMsrv; Source = 'msrv'; Installable = $true } +} + +if ($InstallIfMissing) { + if (-not $selection.Installable) { + exit 0 + } + + $installed = rustup toolchain list + if ($LASTEXITCODE -ne 0) { + throw 'anvil: rustup toolchain list failed' + } + if ($installed -match "(?m)^$([regex]::Escape($selection.Value))(?:-|$)") { + exit 0 + } + + Write-Host "anvil: installing stable toolchain '$($selection.Value)'" + rustup toolchain install $selection.Value --profile minimal + if ($LASTEXITCODE -ne 0) { + throw "anvil: failed to install stable toolchain '$($selection.Value)'" + } + exit 0 +} + +if ($ForEnvironment -and $selection.Source -eq 'file') { + '' +} else { + $selection.Value +} +exit 0 diff --git a/crates/cargo-anvil/templates/github/setup-action.yml b/crates/cargo-anvil/templates/github/setup-action.yml index 9b09eca56..ed6fa2446 100644 --- a/crates/cargo-anvil/templates/github/setup-action.yml +++ b/crates/cargo-anvil/templates/github/setup-action.yml @@ -77,6 +77,24 @@ runs: } "Free after cleanup (GiB): $(Get-FreeDiskGiB)" + - name: Resolve stable toolchain + shell: pwsh -NoProfile -Command ". '{0}'" + run: | + $source = if ([string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { 'resolved' } else { 'environment' } + & '.anvil/resolve-stable-toolchain.ps1' -InstallIfMissing + if ($LASTEXITCODE -ne 0) { + throw 'anvil: failed to install stable toolchain' + } + $toolchain = (& '.anvil/resolve-stable-toolchain.ps1' | Out-String).Trim() + if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($toolchain)) { + throw 'anvil: failed to resolve stable toolchain' + } + $environmentValue = (& '.anvil/resolve-stable-toolchain.ps1' -ForEnvironment | Out-String).Trim() + if (-not [string]::IsNullOrWhiteSpace($environmentValue)) { + "RUSTUP_TOOLCHAIN=$environmentValue" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + } + "ANVIL_STABLE_TOOLCHAIN_SOURCE=$source" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + Write-Host "anvil: selected stable toolchain '$toolchain'" - id: rustc-version shell: bash run: echo "version=$(rustc --version | awk '{print $2}')" >> "$GITHUB_OUTPUT" @@ -120,19 +138,6 @@ runs: ~/.cargo/.crates2.json target/ - # rustup auto-installs the toolchain from rust-toolchain.toml on - # first cargo invocation, but it doesn't pull profile/component - # extras. The `anvil-setup` recipe in step "Install anvil - # toolchains + tools" below handles that exhaustively (the - # per-component install recipes in tools.just install - # default-toolchain components and pinned-nightly components for - # miri/careful/etc.) -- we don't add components inline here anymore. - # This step exists only to ensure rustup itself has the default - # toolchain set up so subsequent cargo / just calls work. - - name: Ensure default toolchain is installed - shell: bash - run: rustup show active-toolchain || rustup default stable - # The catalog recipe `anvil-setup` (or `anvil--setup` # when a group is specified via the `group` input) needs `just` to # run. We bootstrap just here (chicken-and-egg), then hand off diff --git a/crates/cargo-anvil/templates/justfiles/anvil/tools.just b/crates/cargo-anvil/templates/justfiles/anvil/tools.just index cb6aab145..1de4bcaf5 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/tools.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/tools.just @@ -144,6 +144,10 @@ anvil-tool-rustc-validate-prereqs: Write-Error 'anvil: rustc not found. Install via rustup: https://rustup.rs' exit 1 } + & pwsh -NoProfile -File '.anvil/resolve-stable-toolchain.ps1' -ValidateWorkspaceMsrv + if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE + } # Validate that PowerShell Core is available. [group("anvil-setup")] diff --git a/crates/cargo-anvil/templates/justfiles/anvil/versions.just b/crates/cargo-anvil/templates/justfiles/anvil/versions.just index 564a3b90f..d33e5aa03 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/versions.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/versions.just @@ -30,6 +30,23 @@ # Rustup toolchains # ============================================================================ +# Stable toolchain used by every check without an explicit +toolchain argument. +# The resolver honors RUSTUP_TOOLCHAIN, then a selecting root toolchain file, +# then the root package/workspace MSRV. Environment and MSRV selections are +# exported here; a toolchain-file selection stays empty so rustup honors the +# file's complete targets/components/profile configuration. +rust_stable_override := env_var_or_default('RUSTUP_TOOLCHAIN', '') +rust_stable := if rust_stable_override != '' { + rust_stable_override +} else { + shell('pwsh -NoProfile -File .anvil/resolve-stable-toolchain.ps1 -ForEnvironment') +} +export RUSTUP_TOOLCHAIN := rust_stable +export ANVIL_STABLE_TOOLCHAIN_SOURCE := env_var_or_default( + 'ANVIL_STABLE_TOOLCHAIN_SOURCE', + if rust_stable_override == '' { 'resolved' } else { 'environment' }, +) + # General nightly used by udeps, miri, careful, and any future # nightly-dependent check. Bumped on a regular cadence (monthly is a # reasonable default) when an adopter has time to absorb formatting / diff --git a/crates/cargo-anvil/tests/container_customization.rs b/crates/cargo-anvil/tests/container_customization.rs index 91e18b897..9400b0850 100644 --- a/crates/cargo-anvil/tests/container_customization.rs +++ b/crates/cargo-anvil/tests/container_customization.rs @@ -488,6 +488,7 @@ fn powershell_just_dispatch_treats_interpolated_values_as_data() { let runner_output = Command::new("just") .args(["_anvil-run", "missing", "x') { Write-Output RUNNER_INJECTED } elseif ('a"]) .current_dir(root) + .env("RUSTUP_TOOLCHAIN", "1.93") .output() .expect("just must be available"); assert!(!runner_output.status.success(), "the missing native tier must fail"); @@ -503,6 +504,7 @@ fn powershell_just_dispatch_treats_interpolated_values_as_data() { let recipe_output = Command::new("just") .args(["anvil-container", "x'); Write-Output RECIPE_INJECTED; @('a"]) .current_dir(root) + .env("RUSTUP_TOOLCHAIN", "1.93") .output() .expect("just must be available"); assert!( diff --git a/crates/cargo-anvil/tests/container_upgrade.rs b/crates/cargo-anvil/tests/container_upgrade.rs index 35d400843..933c3e94a 100644 --- a/crates/cargo-anvil/tests/container_upgrade.rs +++ b/crates/cargo-anvil/tests/container_upgrade.rs @@ -94,6 +94,12 @@ fn rewind_to_pre_move_layout(root: &Path) { .unwrap_or_else(|| panic!("{current} must be tracked by the fresh lock")); manifest.files.insert(previous, checksum); } + let resolver = ".anvil/resolve-stable-toolchain.ps1"; + std::fs::remove_file(root.join(resolver)).unwrap(); + manifest + .files + .remove(resolver) + .unwrap_or_else(|| panic!("{resolver} must be tracked by the fresh lock")); // Provenance of the older build. It is recorded, never a gate. manifest.catalog_checksum = Some("sha256:0000000000000000000000000000000000000000000000000000000000000000".to_owned()); manifest.tool_version = Some("0.2.0".to_owned()); diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap index 7d6f37c46..8a62346f4 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap @@ -54,15 +54,14 @@ RUN curl --proto '=https' --tlsv1.2 -fsSLo /tmp/rustup-init \ WORKDIR /opt/anvil COPY . . -RUN test -f rust-toolchain.toml || { \ - echo "anvil-container requires rust-toolchain.toml" >&2; \ - exit 1; \ - } RUN --mount=type=cache,id=anvil-cargo-registry,target=/usr/local/cargo/registry \ --mount=type=cache,id=anvil-cargo-git,target=/usr/local/cargo/git \ --mount=type=cache,id=anvil-cargo-target,target=/tmp/anvil-target \ - printf "anvil_runner := \"native\"\nimport 'justfiles/anvil/mod.just'\n" > Justfile \ - && CARGO_TARGET_DIR=/tmp/anvil-target just anvil-setup + pwsh -NoProfile -File .anvil/resolve-stable-toolchain.ps1 -InstallIfMissing \ + && toolchain="$(pwsh -NoProfile -File .anvil/resolve-stable-toolchain.ps1 -ForEnvironment)" \ + && printf "anvil_runner := \"native\"\nimport 'justfiles/anvil/mod.just'\n" > Justfile \ + && RUSTUP_TOOLCHAIN="$toolchain" ANVIL_STABLE_TOOLCHAIN_SOURCE=container \ + CARGO_TARGET_DIR=/tmp/anvil-target just anvil-setup COPY .anvil/container/entrypoint.sh /usr/local/bin/anvil-container-entrypoint RUN chmod 755 /usr/local/bin/anvil-container-entrypoint @@ -92,10 +91,13 @@ CMD ["bash"] # contract in the allow-list too, at every depth, because a deeper candidate # always has an ancestor of exactly that shape. ** +!Cargo.toml +!rust-toolchain !rust-toolchain.toml !justfiles/anvil/*.just !justfiles/anvil/checks/*.just !justfiles/anvil/groups/*.just +!.anvil/resolve-stable-toolchain.ps1 !.anvil/container/* .anvil/container/*/* .anvil/container/customize.sh @@ -373,11 +375,13 @@ if ($LASTEXITCODE -ne 0 -or -not $repoRoot) { } $inputs = @( - 'rust-toolchain.toml' + 'Cargo.toml', + '.anvil/resolve-stable-toolchain.ps1' ) -$toolchainPath = Join-Path $repoRoot 'rust-toolchain.toml' -if (-not (Test-Path -LiteralPath $toolchainPath -PathType Leaf)) { - throw 'anvil-container requires a repository-owned rust-toolchain.toml.' +foreach ($toolchainFile in @('rust-toolchain', 'rust-toolchain.toml')) { + if (Test-Path -LiteralPath (Join-Path $repoRoot $toolchainFile) -PathType Leaf) { + $inputs += $toolchainFile + } } $containerPath = Join-Path $repoRoot '.anvil/container' $containerRecipe = 'justfiles/anvil/container.just' @@ -444,12 +448,6 @@ if ! repo_root="$(git rev-parse --show-toplevel 2>/dev/null)"; then exit 1 fi -toolchain_path="$repo_root/rust-toolchain.toml" -if [[ ! -f "$toolchain_path" ]]; then - echo 'anvil-container requires a repository-owned rust-toolchain.toml.' >&2 - exit 1 -fi - container_dir="$repo_root/.anvil/container" container_recipe="justfiles/anvil/container.just" default_base_image="$(sed -n 's/^ARG BASE_IMAGE=//p' "$container_dir/Containerfile" | head -n 1)" @@ -462,7 +460,12 @@ if [[ ! "$base_image" =~ @sha256:[0-9a-fA-F]{64}$ ]]; then echo 'anvil-container: ANVIL_CONTAINER_BASE_IMAGE must be pinned by sha256 digest (image@sha256:<64 hex characters>).' >&2 exit 1 fi -inputs=(rust-toolchain.toml) +inputs=(Cargo.toml .anvil/resolve-stable-toolchain.ps1) +for toolchain_file in rust-toolchain rust-toolchain.toml; do + if [[ -f "$repo_root/$toolchain_file" ]]; then + inputs+=("$toolchain_file") + fi +done while IFS= read -r path; do relative="${path#"$repo_root"/}" # The container entry recipe drives execution on the host; it is not @@ -1193,6 +1196,164 @@ if (($# == 0)); then fi docker "${run_args[@]}" "$image" just "$@" +=== .anvil/resolve-stable-toolchain.ps1 === +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# GENERATED BY cargo-anvil. DO NOT EDIT DIRECTLY. + +param( + [switch] $ValidateWorkspaceMsrv, + [switch] $InstallIfMissing, + [switch] $ForEnvironment +) + +$ErrorActionPreference = 'Stop' +Set-StrictMode -Version 3 + +$repoRoot = Split-Path -Parent $PSScriptRoot + +function Get-SelectingToolchainFile { + foreach ($name in @('rust-toolchain', 'rust-toolchain.toml')) { + $path = Join-Path $repoRoot $name + if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { + continue + } + + $content = Get-Content -LiteralPath $path -Raw + if ($name -eq 'rust-toolchain' -and $content.TrimStart() -notmatch '^\[') { + return [pscustomobject]@{ Path = $path; Value = $content.Trim(); Source = 'file'; Installable = $false } + } + + $inToolchain = $false + foreach ($line in $content -split '\r?\n') { + if ($line -match '^\s*\[\s*toolchain\s*\]\s*(?:#.*)?$') { + $inToolchain = $true + continue + } + if ($line -match '^\s*\[') { + $inToolchain = $false + continue + } + if ($inToolchain -and $line -match '^\s*["'']?(channel|path)["'']?\s*=\s*(["''])([^"'']+)\2\s*(?:#.*)?$') { + $value = $Matches[3] + if ($Matches[1] -eq 'path' -and -not [IO.Path]::IsPathRooted($value)) { + $value = [IO.Path]::GetFullPath((Join-Path $repoRoot $value)) + } + return [pscustomobject]@{ + Path = $path + Value = $value + Source = 'file' + Installable = $false + } + } + } + } + + return $null +} + +function Get-RootMsrv { + $manifestPath = Join-Path $repoRoot 'Cargo.toml' + if (-not (Test-Path -LiteralPath $manifestPath -PathType Leaf)) { + throw "anvil: Cargo.toml not found at repository root '$repoRoot'" + } + + $values = @{} + $section = '' + foreach ($line in (Get-Content -LiteralPath $manifestPath)) { + if ($line -match '^\s*\[\s*([^\]]+)\s*\]\s*(?:#.*)?$') { + $section = $Matches[1].Trim() + continue + } + if ($section -in @('workspace.package', 'package') -and + $line -match '^\s*["'']?rust-version["'']?\s*=\s*(["''])([^"'']+)\1\s*(?:#.*)?$') { + $values[$section] = $Matches[2] + } + } + + if ($values.ContainsKey('workspace.package')) { + return $values['workspace.package'] + } + if ($values.ContainsKey('package')) { + return $values['package'] + } + + throw 'anvil: no selecting rust-toolchain(.toml) and no root [workspace.package] or [package] rust-version; declare an MSRV or select a toolchain explicitly' +} + +function Assert-UniformWorkspaceMsrv { + $manifestPath = Join-Path $repoRoot 'Cargo.toml' + $metadataText = (& cargo metadata --manifest-path $manifestPath --format-version 1 --no-deps 2>&1 | Out-String) + if ($LASTEXITCODE -ne 0) { + throw "anvil: cargo metadata failed while validating workspace MSRVs:`n$metadataText" + } + $metadata = $metadataText | ConvertFrom-Json + $memberIds = [Collections.Generic.HashSet[string]]::new( + [string[]] $metadata.workspace_members, + [StringComparer]::Ordinal + ) + $members = @($metadata.packages | Where-Object { $memberIds.Contains([string] $_.id) }) + $missing = @($members | Where-Object { [string]::IsNullOrWhiteSpace([string] $_.rust_version) } | ForEach-Object name) + if ($missing.Count -gt 0) { + throw "anvil: workspace packages without a resolved rust-version: $($missing -join ', '); declare one or select a toolchain explicitly" + } + + $versions = @($members | ForEach-Object { [string] $_.rust_version } | Sort-Object -Unique) + $rootMsrv = Get-RootMsrv + if ($versions.Count -ne 1 -or $versions[0] -ne $rootMsrv) { + throw "anvil: workspace packages must resolve to the root MSRV $rootMsrv; found $($versions -join ', '); select one catalog toolchain explicitly with rust-toolchain.toml" + } +} + +$fileSelection = Get-SelectingToolchainFile +if ($ValidateWorkspaceMsrv) { + $skipWorkspaceMsrvValidation = if ([string]::IsNullOrWhiteSpace($env:ANVIL_STABLE_TOOLCHAIN_SOURCE)) { + -not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN) + } else { + $env:ANVIL_STABLE_TOOLCHAIN_SOURCE -in @('environment', 'container') + } + if (-not $skipWorkspaceMsrvValidation -and $null -eq $fileSelection) { + Assert-UniformWorkspaceMsrv + } + exit 0 +} + +$selection = if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { + [pscustomobject]@{ Value = $env:RUSTUP_TOOLCHAIN; Source = 'environment'; Installable = $false } +} elseif ($null -ne $fileSelection) { + $fileSelection +} else { + [pscustomobject]@{ Value = Get-RootMsrv; Source = 'msrv'; Installable = $true } +} + +if ($InstallIfMissing) { + if (-not $selection.Installable) { + exit 0 + } + + $installed = rustup toolchain list + if ($LASTEXITCODE -ne 0) { + throw 'anvil: rustup toolchain list failed' + } + if ($installed -match "(?m)^$([regex]::Escape($selection.Value))(?:-|$)") { + exit 0 + } + + Write-Host "anvil: installing stable toolchain '$($selection.Value)'" + rustup toolchain install $selection.Value --profile minimal + if ($LASTEXITCODE -ne 0) { + throw "anvil: failed to install stable toolchain '$($selection.Value)'" + } + exit 0 +} + +if ($ForEnvironment -and $selection.Source -eq 'file') { + '' +} else { + $selection.Value +} +exit 0 + === .delta.toml === # >>> anvil-managed: anvil-delta # Changes to these workspace-level inputs conservatively affect every package. @@ -2393,6 +2554,19 @@ parameters: type: string default: '' steps: + - pwsh: | + $source = if ([string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { 'resolved' } else { 'environment' } + $toolchain = (& '.anvil/resolve-stable-toolchain.ps1' | Out-String).Trim() + if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($toolchain)) { + throw 'anvil: failed to resolve stable toolchain' + } + $environmentValue = (& '.anvil/resolve-stable-toolchain.ps1' -ForEnvironment | Out-String).Trim() + if (-not [string]::IsNullOrWhiteSpace($environmentValue)) { + Write-Host "##vso[task.setvariable variable=RUSTUP_TOOLCHAIN]$environmentValue" + } + Write-Host "##vso[task.setvariable variable=ANVIL_STABLE_TOOLCHAIN_SOURCE]$source" + Write-Host "anvil: selected stable toolchain '$toolchain'" + displayName: anvil setup (resolve stable toolchain) - bash: echo "##vso[task.setvariable variable=anvil_rustc_version]$(rustc --version | awk '{print $2}')" displayName: anvil setup (capture rustc version) - task: Cache@2 @@ -5602,6 +5776,10 @@ anvil-tool-rustc-validate-prereqs: Write-Error 'anvil: rustc not found. Install via rustup: https://rustup.rs' exit 1 } + & pwsh -NoProfile -File '.anvil/resolve-stable-toolchain.ps1' -ValidateWorkspaceMsrv + if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE + } # Validate that PowerShell Core is available. [group("anvil-setup")] @@ -6231,6 +6409,23 @@ anvil-tool-cargo-udeps-validate-prereqs: (_check-tool "cargo-udeps" cargo_udeps_ # Rustup toolchains # ============================================================================ +# Stable toolchain used by every check without an explicit +toolchain argument. +# The resolver honors RUSTUP_TOOLCHAIN, then a selecting root toolchain file, +# then the root package/workspace MSRV. Environment and MSRV selections are +# exported here; a toolchain-file selection stays empty so rustup honors the +# file's complete targets/components/profile configuration. +rust_stable_override := env_var_or_default('RUSTUP_TOOLCHAIN', '') +rust_stable := if rust_stable_override != '' { + rust_stable_override +} else { + shell('pwsh -NoProfile -File .anvil/resolve-stable-toolchain.ps1 -ForEnvironment') +} +export RUSTUP_TOOLCHAIN := rust_stable +export ANVIL_STABLE_TOOLCHAIN_SOURCE := env_var_or_default( + 'ANVIL_STABLE_TOOLCHAIN_SOURCE', + if rust_stable_override == '' { 'resolved' } else { 'environment' }, +) + # General nightly used by udeps, miri, careful, and any future # nightly-dependent check. Bumped on a regular cadence (monthly is a # reasonable default) when an adopter has time to absorb formatting / diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap index 0fe700aa5..79ac557a8 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap @@ -54,15 +54,14 @@ RUN curl --proto '=https' --tlsv1.2 -fsSLo /tmp/rustup-init \ WORKDIR /opt/anvil COPY . . -RUN test -f rust-toolchain.toml || { \ - echo "anvil-container requires rust-toolchain.toml" >&2; \ - exit 1; \ - } RUN --mount=type=cache,id=anvil-cargo-registry,target=/usr/local/cargo/registry \ --mount=type=cache,id=anvil-cargo-git,target=/usr/local/cargo/git \ --mount=type=cache,id=anvil-cargo-target,target=/tmp/anvil-target \ - printf "anvil_runner := \"native\"\nimport 'justfiles/anvil/mod.just'\n" > Justfile \ - && CARGO_TARGET_DIR=/tmp/anvil-target just anvil-setup + pwsh -NoProfile -File .anvil/resolve-stable-toolchain.ps1 -InstallIfMissing \ + && toolchain="$(pwsh -NoProfile -File .anvil/resolve-stable-toolchain.ps1 -ForEnvironment)" \ + && printf "anvil_runner := \"native\"\nimport 'justfiles/anvil/mod.just'\n" > Justfile \ + && RUSTUP_TOOLCHAIN="$toolchain" ANVIL_STABLE_TOOLCHAIN_SOURCE=container \ + CARGO_TARGET_DIR=/tmp/anvil-target just anvil-setup COPY .anvil/container/entrypoint.sh /usr/local/bin/anvil-container-entrypoint RUN chmod 755 /usr/local/bin/anvil-container-entrypoint @@ -92,10 +91,13 @@ CMD ["bash"] # contract in the allow-list too, at every depth, because a deeper candidate # always has an ancestor of exactly that shape. ** +!Cargo.toml +!rust-toolchain !rust-toolchain.toml !justfiles/anvil/*.just !justfiles/anvil/checks/*.just !justfiles/anvil/groups/*.just +!.anvil/resolve-stable-toolchain.ps1 !.anvil/container/* .anvil/container/*/* .anvil/container/customize.sh @@ -373,11 +375,13 @@ if ($LASTEXITCODE -ne 0 -or -not $repoRoot) { } $inputs = @( - 'rust-toolchain.toml' + 'Cargo.toml', + '.anvil/resolve-stable-toolchain.ps1' ) -$toolchainPath = Join-Path $repoRoot 'rust-toolchain.toml' -if (-not (Test-Path -LiteralPath $toolchainPath -PathType Leaf)) { - throw 'anvil-container requires a repository-owned rust-toolchain.toml.' +foreach ($toolchainFile in @('rust-toolchain', 'rust-toolchain.toml')) { + if (Test-Path -LiteralPath (Join-Path $repoRoot $toolchainFile) -PathType Leaf) { + $inputs += $toolchainFile + } } $containerPath = Join-Path $repoRoot '.anvil/container' $containerRecipe = 'justfiles/anvil/container.just' @@ -444,12 +448,6 @@ if ! repo_root="$(git rev-parse --show-toplevel 2>/dev/null)"; then exit 1 fi -toolchain_path="$repo_root/rust-toolchain.toml" -if [[ ! -f "$toolchain_path" ]]; then - echo 'anvil-container requires a repository-owned rust-toolchain.toml.' >&2 - exit 1 -fi - container_dir="$repo_root/.anvil/container" container_recipe="justfiles/anvil/container.just" default_base_image="$(sed -n 's/^ARG BASE_IMAGE=//p' "$container_dir/Containerfile" | head -n 1)" @@ -462,7 +460,12 @@ if [[ ! "$base_image" =~ @sha256:[0-9a-fA-F]{64}$ ]]; then echo 'anvil-container: ANVIL_CONTAINER_BASE_IMAGE must be pinned by sha256 digest (image@sha256:<64 hex characters>).' >&2 exit 1 fi -inputs=(rust-toolchain.toml) +inputs=(Cargo.toml .anvil/resolve-stable-toolchain.ps1) +for toolchain_file in rust-toolchain rust-toolchain.toml; do + if [[ -f "$repo_root/$toolchain_file" ]]; then + inputs+=("$toolchain_file") + fi +done while IFS= read -r path; do relative="${path#"$repo_root"/}" # The container entry recipe drives execution on the host; it is not @@ -1193,6 +1196,164 @@ if (($# == 0)); then fi docker "${run_args[@]}" "$image" just "$@" +=== .anvil/resolve-stable-toolchain.ps1 === +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# GENERATED BY cargo-anvil. DO NOT EDIT DIRECTLY. + +param( + [switch] $ValidateWorkspaceMsrv, + [switch] $InstallIfMissing, + [switch] $ForEnvironment +) + +$ErrorActionPreference = 'Stop' +Set-StrictMode -Version 3 + +$repoRoot = Split-Path -Parent $PSScriptRoot + +function Get-SelectingToolchainFile { + foreach ($name in @('rust-toolchain', 'rust-toolchain.toml')) { + $path = Join-Path $repoRoot $name + if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { + continue + } + + $content = Get-Content -LiteralPath $path -Raw + if ($name -eq 'rust-toolchain' -and $content.TrimStart() -notmatch '^\[') { + return [pscustomobject]@{ Path = $path; Value = $content.Trim(); Source = 'file'; Installable = $false } + } + + $inToolchain = $false + foreach ($line in $content -split '\r?\n') { + if ($line -match '^\s*\[\s*toolchain\s*\]\s*(?:#.*)?$') { + $inToolchain = $true + continue + } + if ($line -match '^\s*\[') { + $inToolchain = $false + continue + } + if ($inToolchain -and $line -match '^\s*["'']?(channel|path)["'']?\s*=\s*(["''])([^"'']+)\2\s*(?:#.*)?$') { + $value = $Matches[3] + if ($Matches[1] -eq 'path' -and -not [IO.Path]::IsPathRooted($value)) { + $value = [IO.Path]::GetFullPath((Join-Path $repoRoot $value)) + } + return [pscustomobject]@{ + Path = $path + Value = $value + Source = 'file' + Installable = $false + } + } + } + } + + return $null +} + +function Get-RootMsrv { + $manifestPath = Join-Path $repoRoot 'Cargo.toml' + if (-not (Test-Path -LiteralPath $manifestPath -PathType Leaf)) { + throw "anvil: Cargo.toml not found at repository root '$repoRoot'" + } + + $values = @{} + $section = '' + foreach ($line in (Get-Content -LiteralPath $manifestPath)) { + if ($line -match '^\s*\[\s*([^\]]+)\s*\]\s*(?:#.*)?$') { + $section = $Matches[1].Trim() + continue + } + if ($section -in @('workspace.package', 'package') -and + $line -match '^\s*["'']?rust-version["'']?\s*=\s*(["''])([^"'']+)\1\s*(?:#.*)?$') { + $values[$section] = $Matches[2] + } + } + + if ($values.ContainsKey('workspace.package')) { + return $values['workspace.package'] + } + if ($values.ContainsKey('package')) { + return $values['package'] + } + + throw 'anvil: no selecting rust-toolchain(.toml) and no root [workspace.package] or [package] rust-version; declare an MSRV or select a toolchain explicitly' +} + +function Assert-UniformWorkspaceMsrv { + $manifestPath = Join-Path $repoRoot 'Cargo.toml' + $metadataText = (& cargo metadata --manifest-path $manifestPath --format-version 1 --no-deps 2>&1 | Out-String) + if ($LASTEXITCODE -ne 0) { + throw "anvil: cargo metadata failed while validating workspace MSRVs:`n$metadataText" + } + $metadata = $metadataText | ConvertFrom-Json + $memberIds = [Collections.Generic.HashSet[string]]::new( + [string[]] $metadata.workspace_members, + [StringComparer]::Ordinal + ) + $members = @($metadata.packages | Where-Object { $memberIds.Contains([string] $_.id) }) + $missing = @($members | Where-Object { [string]::IsNullOrWhiteSpace([string] $_.rust_version) } | ForEach-Object name) + if ($missing.Count -gt 0) { + throw "anvil: workspace packages without a resolved rust-version: $($missing -join ', '); declare one or select a toolchain explicitly" + } + + $versions = @($members | ForEach-Object { [string] $_.rust_version } | Sort-Object -Unique) + $rootMsrv = Get-RootMsrv + if ($versions.Count -ne 1 -or $versions[0] -ne $rootMsrv) { + throw "anvil: workspace packages must resolve to the root MSRV $rootMsrv; found $($versions -join ', '); select one catalog toolchain explicitly with rust-toolchain.toml" + } +} + +$fileSelection = Get-SelectingToolchainFile +if ($ValidateWorkspaceMsrv) { + $skipWorkspaceMsrvValidation = if ([string]::IsNullOrWhiteSpace($env:ANVIL_STABLE_TOOLCHAIN_SOURCE)) { + -not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN) + } else { + $env:ANVIL_STABLE_TOOLCHAIN_SOURCE -in @('environment', 'container') + } + if (-not $skipWorkspaceMsrvValidation -and $null -eq $fileSelection) { + Assert-UniformWorkspaceMsrv + } + exit 0 +} + +$selection = if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { + [pscustomobject]@{ Value = $env:RUSTUP_TOOLCHAIN; Source = 'environment'; Installable = $false } +} elseif ($null -ne $fileSelection) { + $fileSelection +} else { + [pscustomobject]@{ Value = Get-RootMsrv; Source = 'msrv'; Installable = $true } +} + +if ($InstallIfMissing) { + if (-not $selection.Installable) { + exit 0 + } + + $installed = rustup toolchain list + if ($LASTEXITCODE -ne 0) { + throw 'anvil: rustup toolchain list failed' + } + if ($installed -match "(?m)^$([regex]::Escape($selection.Value))(?:-|$)") { + exit 0 + } + + Write-Host "anvil: installing stable toolchain '$($selection.Value)'" + rustup toolchain install $selection.Value --profile minimal + if ($LASTEXITCODE -ne 0) { + throw "anvil: failed to install stable toolchain '$($selection.Value)'" + } + exit 0 +} + +if ($ForEnvironment -and $selection.Source -eq 'file') { + '' +} else { + $selection.Value +} +exit 0 + === .delta.toml === # >>> anvil-managed: anvil-delta # Changes to these workspace-level inputs conservatively affect every package. @@ -1673,6 +1834,24 @@ runs: } "Free after cleanup (GiB): $(Get-FreeDiskGiB)" + - name: Resolve stable toolchain + shell: pwsh -NoProfile -Command ". '{0}'" + run: | + $source = if ([string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { 'resolved' } else { 'environment' } + & '.anvil/resolve-stable-toolchain.ps1' -InstallIfMissing + if ($LASTEXITCODE -ne 0) { + throw 'anvil: failed to install stable toolchain' + } + $toolchain = (& '.anvil/resolve-stable-toolchain.ps1' | Out-String).Trim() + if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($toolchain)) { + throw 'anvil: failed to resolve stable toolchain' + } + $environmentValue = (& '.anvil/resolve-stable-toolchain.ps1' -ForEnvironment | Out-String).Trim() + if (-not [string]::IsNullOrWhiteSpace($environmentValue)) { + "RUSTUP_TOOLCHAIN=$environmentValue" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + } + "ANVIL_STABLE_TOOLCHAIN_SOURCE=$source" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + Write-Host "anvil: selected stable toolchain '$toolchain'" - id: rustc-version shell: bash run: echo "version=$(rustc --version | awk '{print $2}')" >> "$GITHUB_OUTPUT" @@ -1716,19 +1895,6 @@ runs: ~/.cargo/.crates2.json target/ - # rustup auto-installs the toolchain from rust-toolchain.toml on - # first cargo invocation, but it doesn't pull profile/component - # extras. The `anvil-setup` recipe in step "Install anvil - # toolchains + tools" below handles that exhaustively (the - # per-component install recipes in tools.just install - # default-toolchain components and pinned-nightly components for - # miri/careful/etc.) -- we don't add components inline here anymore. - # This step exists only to ensure rustup itself has the default - # toolchain set up so subsequent cargo / just calls work. - - name: Ensure default toolchain is installed - shell: bash - run: rustup show active-toolchain || rustup default stable - # The catalog recipe `anvil-setup` (or `anvil--setup` # when a group is specified via the `group` input) needs `just` to # run. We bootstrap just here (chicken-and-egg), then hand off @@ -5501,6 +5667,10 @@ anvil-tool-rustc-validate-prereqs: Write-Error 'anvil: rustc not found. Install via rustup: https://rustup.rs' exit 1 } + & pwsh -NoProfile -File '.anvil/resolve-stable-toolchain.ps1' -ValidateWorkspaceMsrv + if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE + } # Validate that PowerShell Core is available. [group("anvil-setup")] @@ -6130,6 +6300,23 @@ anvil-tool-cargo-udeps-validate-prereqs: (_check-tool "cargo-udeps" cargo_udeps_ # Rustup toolchains # ============================================================================ +# Stable toolchain used by every check without an explicit +toolchain argument. +# The resolver honors RUSTUP_TOOLCHAIN, then a selecting root toolchain file, +# then the root package/workspace MSRV. Environment and MSRV selections are +# exported here; a toolchain-file selection stays empty so rustup honors the +# file's complete targets/components/profile configuration. +rust_stable_override := env_var_or_default('RUSTUP_TOOLCHAIN', '') +rust_stable := if rust_stable_override != '' { + rust_stable_override +} else { + shell('pwsh -NoProfile -File .anvil/resolve-stable-toolchain.ps1 -ForEnvironment') +} +export RUSTUP_TOOLCHAIN := rust_stable +export ANVIL_STABLE_TOOLCHAIN_SOURCE := env_var_or_default( + 'ANVIL_STABLE_TOOLCHAIN_SOURCE', + if rust_stable_override == '' { 'resolved' } else { 'environment' }, +) + # General nightly used by udeps, miri, careful, and any future # nightly-dependent check. Bumped on a regular cadence (monthly is a # reasonable default) when an adopter has time to absorb formatting / diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap index 078a7888d..807f625c3 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap @@ -54,15 +54,14 @@ RUN curl --proto '=https' --tlsv1.2 -fsSLo /tmp/rustup-init \ WORKDIR /opt/anvil COPY . . -RUN test -f rust-toolchain.toml || { \ - echo "anvil-container requires rust-toolchain.toml" >&2; \ - exit 1; \ - } RUN --mount=type=cache,id=anvil-cargo-registry,target=/usr/local/cargo/registry \ --mount=type=cache,id=anvil-cargo-git,target=/usr/local/cargo/git \ --mount=type=cache,id=anvil-cargo-target,target=/tmp/anvil-target \ - printf "anvil_runner := \"native\"\nimport 'justfiles/anvil/mod.just'\n" > Justfile \ - && CARGO_TARGET_DIR=/tmp/anvil-target just anvil-setup + pwsh -NoProfile -File .anvil/resolve-stable-toolchain.ps1 -InstallIfMissing \ + && toolchain="$(pwsh -NoProfile -File .anvil/resolve-stable-toolchain.ps1 -ForEnvironment)" \ + && printf "anvil_runner := \"native\"\nimport 'justfiles/anvil/mod.just'\n" > Justfile \ + && RUSTUP_TOOLCHAIN="$toolchain" ANVIL_STABLE_TOOLCHAIN_SOURCE=container \ + CARGO_TARGET_DIR=/tmp/anvil-target just anvil-setup COPY .anvil/container/entrypoint.sh /usr/local/bin/anvil-container-entrypoint RUN chmod 755 /usr/local/bin/anvil-container-entrypoint @@ -92,10 +91,13 @@ CMD ["bash"] # contract in the allow-list too, at every depth, because a deeper candidate # always has an ancestor of exactly that shape. ** +!Cargo.toml +!rust-toolchain !rust-toolchain.toml !justfiles/anvil/*.just !justfiles/anvil/checks/*.just !justfiles/anvil/groups/*.just +!.anvil/resolve-stable-toolchain.ps1 !.anvil/container/* .anvil/container/*/* .anvil/container/customize.sh @@ -373,11 +375,13 @@ if ($LASTEXITCODE -ne 0 -or -not $repoRoot) { } $inputs = @( - 'rust-toolchain.toml' + 'Cargo.toml', + '.anvil/resolve-stable-toolchain.ps1' ) -$toolchainPath = Join-Path $repoRoot 'rust-toolchain.toml' -if (-not (Test-Path -LiteralPath $toolchainPath -PathType Leaf)) { - throw 'anvil-container requires a repository-owned rust-toolchain.toml.' +foreach ($toolchainFile in @('rust-toolchain', 'rust-toolchain.toml')) { + if (Test-Path -LiteralPath (Join-Path $repoRoot $toolchainFile) -PathType Leaf) { + $inputs += $toolchainFile + } } $containerPath = Join-Path $repoRoot '.anvil/container' $containerRecipe = 'justfiles/anvil/container.just' @@ -444,12 +448,6 @@ if ! repo_root="$(git rev-parse --show-toplevel 2>/dev/null)"; then exit 1 fi -toolchain_path="$repo_root/rust-toolchain.toml" -if [[ ! -f "$toolchain_path" ]]; then - echo 'anvil-container requires a repository-owned rust-toolchain.toml.' >&2 - exit 1 -fi - container_dir="$repo_root/.anvil/container" container_recipe="justfiles/anvil/container.just" default_base_image="$(sed -n 's/^ARG BASE_IMAGE=//p' "$container_dir/Containerfile" | head -n 1)" @@ -462,7 +460,12 @@ if [[ ! "$base_image" =~ @sha256:[0-9a-fA-F]{64}$ ]]; then echo 'anvil-container: ANVIL_CONTAINER_BASE_IMAGE must be pinned by sha256 digest (image@sha256:<64 hex characters>).' >&2 exit 1 fi -inputs=(rust-toolchain.toml) +inputs=(Cargo.toml .anvil/resolve-stable-toolchain.ps1) +for toolchain_file in rust-toolchain rust-toolchain.toml; do + if [[ -f "$repo_root/$toolchain_file" ]]; then + inputs+=("$toolchain_file") + fi +done while IFS= read -r path; do relative="${path#"$repo_root"/}" # The container entry recipe drives execution on the host; it is not @@ -1193,6 +1196,164 @@ if (($# == 0)); then fi docker "${run_args[@]}" "$image" just "$@" +=== .anvil/resolve-stable-toolchain.ps1 === +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# GENERATED BY cargo-anvil. DO NOT EDIT DIRECTLY. + +param( + [switch] $ValidateWorkspaceMsrv, + [switch] $InstallIfMissing, + [switch] $ForEnvironment +) + +$ErrorActionPreference = 'Stop' +Set-StrictMode -Version 3 + +$repoRoot = Split-Path -Parent $PSScriptRoot + +function Get-SelectingToolchainFile { + foreach ($name in @('rust-toolchain', 'rust-toolchain.toml')) { + $path = Join-Path $repoRoot $name + if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { + continue + } + + $content = Get-Content -LiteralPath $path -Raw + if ($name -eq 'rust-toolchain' -and $content.TrimStart() -notmatch '^\[') { + return [pscustomobject]@{ Path = $path; Value = $content.Trim(); Source = 'file'; Installable = $false } + } + + $inToolchain = $false + foreach ($line in $content -split '\r?\n') { + if ($line -match '^\s*\[\s*toolchain\s*\]\s*(?:#.*)?$') { + $inToolchain = $true + continue + } + if ($line -match '^\s*\[') { + $inToolchain = $false + continue + } + if ($inToolchain -and $line -match '^\s*["'']?(channel|path)["'']?\s*=\s*(["''])([^"'']+)\2\s*(?:#.*)?$') { + $value = $Matches[3] + if ($Matches[1] -eq 'path' -and -not [IO.Path]::IsPathRooted($value)) { + $value = [IO.Path]::GetFullPath((Join-Path $repoRoot $value)) + } + return [pscustomobject]@{ + Path = $path + Value = $value + Source = 'file' + Installable = $false + } + } + } + } + + return $null +} + +function Get-RootMsrv { + $manifestPath = Join-Path $repoRoot 'Cargo.toml' + if (-not (Test-Path -LiteralPath $manifestPath -PathType Leaf)) { + throw "anvil: Cargo.toml not found at repository root '$repoRoot'" + } + + $values = @{} + $section = '' + foreach ($line in (Get-Content -LiteralPath $manifestPath)) { + if ($line -match '^\s*\[\s*([^\]]+)\s*\]\s*(?:#.*)?$') { + $section = $Matches[1].Trim() + continue + } + if ($section -in @('workspace.package', 'package') -and + $line -match '^\s*["'']?rust-version["'']?\s*=\s*(["''])([^"'']+)\1\s*(?:#.*)?$') { + $values[$section] = $Matches[2] + } + } + + if ($values.ContainsKey('workspace.package')) { + return $values['workspace.package'] + } + if ($values.ContainsKey('package')) { + return $values['package'] + } + + throw 'anvil: no selecting rust-toolchain(.toml) and no root [workspace.package] or [package] rust-version; declare an MSRV or select a toolchain explicitly' +} + +function Assert-UniformWorkspaceMsrv { + $manifestPath = Join-Path $repoRoot 'Cargo.toml' + $metadataText = (& cargo metadata --manifest-path $manifestPath --format-version 1 --no-deps 2>&1 | Out-String) + if ($LASTEXITCODE -ne 0) { + throw "anvil: cargo metadata failed while validating workspace MSRVs:`n$metadataText" + } + $metadata = $metadataText | ConvertFrom-Json + $memberIds = [Collections.Generic.HashSet[string]]::new( + [string[]] $metadata.workspace_members, + [StringComparer]::Ordinal + ) + $members = @($metadata.packages | Where-Object { $memberIds.Contains([string] $_.id) }) + $missing = @($members | Where-Object { [string]::IsNullOrWhiteSpace([string] $_.rust_version) } | ForEach-Object name) + if ($missing.Count -gt 0) { + throw "anvil: workspace packages without a resolved rust-version: $($missing -join ', '); declare one or select a toolchain explicitly" + } + + $versions = @($members | ForEach-Object { [string] $_.rust_version } | Sort-Object -Unique) + $rootMsrv = Get-RootMsrv + if ($versions.Count -ne 1 -or $versions[0] -ne $rootMsrv) { + throw "anvil: workspace packages must resolve to the root MSRV $rootMsrv; found $($versions -join ', '); select one catalog toolchain explicitly with rust-toolchain.toml" + } +} + +$fileSelection = Get-SelectingToolchainFile +if ($ValidateWorkspaceMsrv) { + $skipWorkspaceMsrvValidation = if ([string]::IsNullOrWhiteSpace($env:ANVIL_STABLE_TOOLCHAIN_SOURCE)) { + -not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN) + } else { + $env:ANVIL_STABLE_TOOLCHAIN_SOURCE -in @('environment', 'container') + } + if (-not $skipWorkspaceMsrvValidation -and $null -eq $fileSelection) { + Assert-UniformWorkspaceMsrv + } + exit 0 +} + +$selection = if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { + [pscustomobject]@{ Value = $env:RUSTUP_TOOLCHAIN; Source = 'environment'; Installable = $false } +} elseif ($null -ne $fileSelection) { + $fileSelection +} else { + [pscustomobject]@{ Value = Get-RootMsrv; Source = 'msrv'; Installable = $true } +} + +if ($InstallIfMissing) { + if (-not $selection.Installable) { + exit 0 + } + + $installed = rustup toolchain list + if ($LASTEXITCODE -ne 0) { + throw 'anvil: rustup toolchain list failed' + } + if ($installed -match "(?m)^$([regex]::Escape($selection.Value))(?:-|$)") { + exit 0 + } + + Write-Host "anvil: installing stable toolchain '$($selection.Value)'" + rustup toolchain install $selection.Value --profile minimal + if ($LASTEXITCODE -ne 0) { + throw "anvil: failed to install stable toolchain '$($selection.Value)'" + } + exit 0 +} + +if ($ForEnvironment -and $selection.Source -eq 'file') { + '' +} else { + $selection.Value +} +exit 0 + === .delta.toml === # >>> anvil-managed: anvil-delta # Changes to these workspace-level inputs conservatively affect every package. @@ -4342,6 +4503,10 @@ anvil-tool-rustc-validate-prereqs: Write-Error 'anvil: rustc not found. Install via rustup: https://rustup.rs' exit 1 } + & pwsh -NoProfile -File '.anvil/resolve-stable-toolchain.ps1' -ValidateWorkspaceMsrv + if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE + } # Validate that PowerShell Core is available. [group("anvil-setup")] @@ -4971,6 +5136,23 @@ anvil-tool-cargo-udeps-validate-prereqs: (_check-tool "cargo-udeps" cargo_udeps_ # Rustup toolchains # ============================================================================ +# Stable toolchain used by every check without an explicit +toolchain argument. +# The resolver honors RUSTUP_TOOLCHAIN, then a selecting root toolchain file, +# then the root package/workspace MSRV. Environment and MSRV selections are +# exported here; a toolchain-file selection stays empty so rustup honors the +# file's complete targets/components/profile configuration. +rust_stable_override := env_var_or_default('RUSTUP_TOOLCHAIN', '') +rust_stable := if rust_stable_override != '' { + rust_stable_override +} else { + shell('pwsh -NoProfile -File .anvil/resolve-stable-toolchain.ps1 -ForEnvironment') +} +export RUSTUP_TOOLCHAIN := rust_stable +export ANVIL_STABLE_TOOLCHAIN_SOURCE := env_var_or_default( + 'ANVIL_STABLE_TOOLCHAIN_SOURCE', + if rust_stable_override == '' { 'resolved' } else { 'environment' }, +) + # General nightly used by udeps, miri, careful, and any future # nightly-dependent check. Bumped on a regular cadence (monthly is a # reasonable default) when an adopter has time to absorb formatting / diff --git a/justfiles/anvil/tools.just b/justfiles/anvil/tools.just index cb6aab145..1de4bcaf5 100644 --- a/justfiles/anvil/tools.just +++ b/justfiles/anvil/tools.just @@ -144,6 +144,10 @@ anvil-tool-rustc-validate-prereqs: Write-Error 'anvil: rustc not found. Install via rustup: https://rustup.rs' exit 1 } + & pwsh -NoProfile -File '.anvil/resolve-stable-toolchain.ps1' -ValidateWorkspaceMsrv + if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE + } # Validate that PowerShell Core is available. [group("anvil-setup")] diff --git a/justfiles/anvil/versions.just b/justfiles/anvil/versions.just index 564a3b90f..d33e5aa03 100644 --- a/justfiles/anvil/versions.just +++ b/justfiles/anvil/versions.just @@ -30,6 +30,23 @@ # Rustup toolchains # ============================================================================ +# Stable toolchain used by every check without an explicit +toolchain argument. +# The resolver honors RUSTUP_TOOLCHAIN, then a selecting root toolchain file, +# then the root package/workspace MSRV. Environment and MSRV selections are +# exported here; a toolchain-file selection stays empty so rustup honors the +# file's complete targets/components/profile configuration. +rust_stable_override := env_var_or_default('RUSTUP_TOOLCHAIN', '') +rust_stable := if rust_stable_override != '' { + rust_stable_override +} else { + shell('pwsh -NoProfile -File .anvil/resolve-stable-toolchain.ps1 -ForEnvironment') +} +export RUSTUP_TOOLCHAIN := rust_stable +export ANVIL_STABLE_TOOLCHAIN_SOURCE := env_var_or_default( + 'ANVIL_STABLE_TOOLCHAIN_SOURCE', + if rust_stable_override == '' { 'resolved' } else { 'environment' }, +) + # General nightly used by udeps, miri, careful, and any future # nightly-dependent check. Bumped on a regular cadence (monthly is a # reasonable default) when an adopter has time to absorb formatting / From 901f81d795f839ed20764427e2cec0d1087f456e Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Thu, 27 Aug 2026 13:50:51 +0200 Subject: [PATCH 02/32] feat(cargo-anvil): test PRs against MSRV Add a conditional Linux PR check when a selecting toolchain file differs from the workspace MSRV, with support for mapped internal toolchains. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .anvil.lock | 22 +- .anvil/resolve-stable-toolchain.ps1 | 100 +++++-- .github/actions/anvil-impact/action.yml | 12 + .github/actions/anvil-setup/action.yml | 4 +- .github/workflows/anvil-pr-impl.yml | 17 ++ crates/cargo-anvil/README.md | 5 +- crates/cargo-anvil/docs/design/README.md | 3 +- crates/cargo-anvil/docs/design/ado.md | 15 +- crates/cargo-anvil/docs/design/checks.md | 39 ++- crates/cargo-anvil/docs/design/github.md | 36 ++- crates/cargo-anvil/docs/design/local.md | 16 +- crates/cargo-anvil/src/anvil/artifacts/ado.rs | 9 +- .../cargo-anvil/src/anvil/artifacts/github.rs | 13 +- .../src/anvil/artifacts/justfile.rs | 78 ++++- crates/cargo-anvil/src/lib.rs | 3 +- crates/cargo-anvil/src/run.rs | 1 + .../cargo-anvil/templates/ado/pr-stages.yml | 23 +- .../templates/ado/steps/impact.yml | 8 + .../cargo-anvil/templates/ado/steps/setup.yml | 2 +- .../anvil/resolve-stable-toolchain.ps1 | 100 +++++-- .../templates/github/impact-action.yml | 12 + .../templates/github/pr-impl-workflow.yml | 17 ++ .../templates/github/setup-action.yml | 4 +- .../justfiles/anvil/checks/msrv-test.just | 48 ++++ .../justfiles/anvil/groups/pr-msrv.just | 19 ++ .../justfiles/anvil/groups/pr-slow.just | 6 +- .../templates/justfiles/anvil/mod.just | 2 + .../snapshots/snapshots__ado_backend.snap | 270 ++++++++++++++++-- .../snapshots/snapshots__github_backend.snap | 212 ++++++++++++-- .../snapshots/snapshots__local_only.snap | 179 ++++++++++-- justfiles/anvil/checks/msrv-test.just | 48 ++++ justfiles/anvil/groups/pr-msrv.just | 19 ++ justfiles/anvil/groups/pr-slow.just | 6 +- justfiles/anvil/mod.just | 2 + 34 files changed, 1186 insertions(+), 164 deletions(-) create mode 100644 crates/cargo-anvil/templates/justfiles/anvil/checks/msrv-test.just create mode 100644 crates/cargo-anvil/templates/justfiles/anvil/groups/pr-msrv.just create mode 100644 justfiles/anvil/checks/msrv-test.just create mode 100644 justfiles/anvil/groups/pr-msrv.just diff --git a/.anvil.lock b/.anvil.lock index 4c251b937..c1a878492 100644 --- a/.anvil.lock +++ b/.anvil.lock @@ -1,7 +1,7 @@ version = 1 tool = "anvil" tool_version = "0.5.0" -catalog_checksum = "sha256:8f085fd850e403e724c6b1b20b788056d3624fede21b7d21dd2d9f896a6f8fbc" +catalog_checksum = "sha256:d7d8adc0600a6c6b38c70dfcd3237d76a46fe036f25f42220a30fa5b56159b1a" [[file]] path = ".anvil/container/Containerfile" @@ -37,11 +37,11 @@ checksum = "sha256:bf4a21ac097d34b6b4b3a4a507455dda96052980228b6145ce6d33c8e4674 [[file]] path = ".anvil/resolve-stable-toolchain.ps1" -checksum = "sha256:149bf7057f1d88b802a8f3a2f22cafb0c5d336bb560ed6f35735c78fcc23bce9" +checksum = "sha256:1e2c8efa192d3889d8912fcc8cd90f5623bc736a9f923ac6a9bac7713f7c92d1" [[file]] path = ".github/actions/anvil-impact/action.yml" -checksum = "sha256:ac509263f374f4b41270f92969e77b3352f815609e518ac8d6cd3d1a126649cc" +checksum = "sha256:d905538e5554c2a45e16bfe69ba687e9da6d94fc4f1cfefdfdd9857c9718b4c7" [[file]] path = ".github/actions/anvil-report-status/action.yml" @@ -53,7 +53,7 @@ checksum = "sha256:b6ea795dbdc88145a7cc02199134e95cd8baa052f95a8913308630bbd3928 [[file]] path = ".github/actions/anvil-setup/action.yml" -checksum = "sha256:4e28dcbd0cfc90336e9fed2a500e11975d3c2bf9fbb3a9dbc53e96d1e0164953" +checksum = "sha256:b091fba1ce3ede82593c328fc887789d561bbd6cf4114aca4b6b71a84b4e8cd5" [[file]] path = ".github/actions/anvil-setup/just-problem-matcher.json" @@ -61,7 +61,7 @@ checksum = "sha256:ea44d5e1a2cb1471cf2cef05eceab846d8afa45cf691be800f21fec1218da [[file]] path = ".github/workflows/anvil-pr-impl.yml" -checksum = "sha256:a743000362364b1b6597e970cc51d2664ba240b7753c8380eed88784b03a13a8" +checksum = "sha256:a7f5aaa9629e4a30a5ef4f9fae8f5736f6ee48ead7273caf5cd1467f30072bfe" [[file]] path = ".github/workflows/anvil-pr.yml" @@ -167,6 +167,10 @@ checksum = "sha256:451ecb560e4a13f77d5dfd95810ac2cc24cf5f0df403a589afeda258868fa path = "justfiles/anvil/checks/miri.just" checksum = "sha256:14499aa7e29bc631b1905af5bb9f7d2da2c94646cf6d75f5746f930fa8044fe8" +[[file]] +path = "justfiles/anvil/checks/msrv-test.just" +checksum = "sha256:7fd4a2d9ed342b16831d78b0cefc7bca0acc37c9e936a63ccb39fafa8002f6eb" + [[file]] path = "justfiles/anvil/checks/mutants-diff.just" checksum = "sha256:3bfed6cfa99fe92bc4dcfe01baeaca6344e80cdc69997808ff4cf466a6e968c0" @@ -203,6 +207,10 @@ checksum = "sha256:cbec6450a64f800963ea5ba9d2eedfa4e90d91d441307f18d109629f84912 path = "justfiles/anvil/groups/pr-fast.just" checksum = "sha256:463a188fe046ad8e7bbbc4ce1ac8fab3cfe1381e6c1a180033fcdb466a5ee044" +[[file]] +path = "justfiles/anvil/groups/pr-msrv.just" +checksum = "sha256:72bba15ad27f857d6158151c758c10b6ae0989b70cb63f025060dbbc9eae6e24" + [[file]] path = "justfiles/anvil/groups/pr-mutants.just" checksum = "sha256:db2775585c53737021496bbc3de1f00a3552bf8a3daabeef85afae5ec134d8b9" @@ -213,7 +221,7 @@ checksum = "sha256:ca2fbc515da12744660692e63d37b8288147be6a1b8545f5e53fdc98c33e7 [[file]] path = "justfiles/anvil/groups/pr-slow.just" -checksum = "sha256:8b7c490cb6eb20c31549ef71eb75eac86ae145c8c6380ee1f590ccd81eec48ea" +checksum = "sha256:24d52b06002166a8ba7ebca5a2c5c7c2db5de83867709b10cfbe58d95a39aab6" [[file]] path = "justfiles/anvil/groups/pr-test.just" @@ -241,7 +249,7 @@ checksum = "sha256:cf6b30b8f4fd10eeb5bb5660c8e60512434fd94405a4fd8ea7399ababa089 [[file]] path = "justfiles/anvil/mod.just" -checksum = "sha256:f3bc59e6228858e350d434d0cc76497cde258279772c414feae2b899d5cc5666" +checksum = "sha256:ef99da692981afae10451437a9ba9a75c4fdbce6431b6458900b7891ab1d0149" [[file]] path = "justfiles/anvil/runner.just" diff --git a/.anvil/resolve-stable-toolchain.ps1 b/.anvil/resolve-stable-toolchain.ps1 index e0351d6b0..df8cbfd35 100644 --- a/.anvil/resolve-stable-toolchain.ps1 +++ b/.anvil/resolve-stable-toolchain.ps1 @@ -5,7 +5,9 @@ param( [switch] $ValidateWorkspaceMsrv, [switch] $InstallIfMissing, - [switch] $ForEnvironment + [switch] $ForEnvironment, + [switch] $MsrvCompatibilityToolchain, + [switch] $InstallMsrvCompatibilityIfNeeded ) $ErrorActionPreference = 'Stop' @@ -22,7 +24,13 @@ function Get-SelectingToolchainFile { $content = Get-Content -LiteralPath $path -Raw if ($name -eq 'rust-toolchain' -and $content.TrimStart() -notmatch '^\[') { - return [pscustomobject]@{ Path = $path; Value = $content.Trim(); Source = 'file'; Installable = $false } + return [pscustomobject]@{ + Path = $path + Value = $content.Trim() + Selector = 'channel' + Source = 'file' + Installable = $false + } } $inToolchain = $false @@ -43,6 +51,7 @@ function Get-SelectingToolchainFile { return [pscustomobject]@{ Path = $path Value = $value + Selector = $Matches[1] Source = 'file' Installable = $false } @@ -53,7 +62,7 @@ function Get-SelectingToolchainFile { return $null } -function Get-RootMsrv { +function Get-RootMsrv([switch] $AllowMissing) { $manifestPath = Join-Path $repoRoot 'Cargo.toml' if (-not (Test-Path -LiteralPath $manifestPath -PathType Leaf)) { throw "anvil: Cargo.toml not found at repository root '$repoRoot'" @@ -79,6 +88,9 @@ function Get-RootMsrv { return $values['package'] } + if ($AllowMissing) { + return $null + } throw 'anvil: no selecting rust-toolchain(.toml) and no root [workspace.package] or [package] rust-version; declare an MSRV or select a toolchain explicitly' } @@ -106,6 +118,50 @@ function Assert-UniformWorkspaceMsrv { } } +function Get-MsrvCompatibilitySelection { + if ($null -eq $fileSelection) { + return $null + } + + $msrv = Get-RootMsrv -AllowMissing + if ([string]::IsNullOrWhiteSpace($msrv)) { + return $null + } + $selectedMatch = [regex]::Match($fileSelection.Value, '^(\d+)\.(\d+)(?:\.\d+)?(?:-.+)?$') + $msrvMatch = [regex]::Match($msrv, '^(\d+)\.(\d+)(?:\.\d+)?(?:-.+)?$') + if ($fileSelection.Selector -eq 'channel' -and $selectedMatch.Success -and $msrvMatch.Success) { + if ($selectedMatch.Groups[1].Value -eq $msrvMatch.Groups[1].Value -and + $selectedMatch.Groups[2].Value -eq $msrvMatch.Groups[2].Value) { + return $null + } + } + + if (-not [string]::IsNullOrWhiteSpace($env:ANVIL_MSRV_TOOLCHAIN)) { + return [pscustomobject]@{ Value = $env:ANVIL_MSRV_TOOLCHAIN; Installable = $false; Mapped = $true } + } + return [pscustomobject]@{ Value = $msrv; Installable = $true; Mapped = $false } +} + +function Install-Toolchain([object] $toolchain, [string] $label) { + if (-not $toolchain.Installable) { + return + } + + $installed = rustup toolchain list + if ($LASTEXITCODE -ne 0) { + throw 'anvil: rustup toolchain list failed' + } + if ($installed -match "(?m)^$([regex]::Escape($toolchain.Value))(?:-|$)") { + return + } + + Write-Host "anvil: installing $label toolchain '$($toolchain.Value)'" + rustup toolchain install $toolchain.Value --profile minimal + if ($LASTEXITCODE -ne 0) { + throw "anvil: failed to install $label toolchain '$($toolchain.Value)'" + } +} + $fileSelection = Get-SelectingToolchainFile if ($ValidateWorkspaceMsrv) { $skipWorkspaceMsrvValidation = if ([string]::IsNullOrWhiteSpace($env:ANVIL_STABLE_TOOLCHAIN_SOURCE)) { @@ -119,6 +175,26 @@ if ($ValidateWorkspaceMsrv) { exit 0 } +if ($MsrvCompatibilityToolchain -or $InstallMsrvCompatibilityIfNeeded) { + $compatibilitySelection = Get-MsrvCompatibilitySelection + if ($null -eq $compatibilitySelection) { + exit 0 + } + if ($InstallMsrvCompatibilityIfNeeded) { + if ($compatibilitySelection.Mapped) { + & cargo "+$($compatibilitySelection.Value)" --version *> $null + if ($LASTEXITCODE -ne 0) { + throw "anvil: mapped MSRV toolchain '$($compatibilitySelection.Value)' is unavailable; provision ANVIL_MSRV_TOOLCHAIN or unset it to let Anvil install the declared public MSRV" + } + } else { + Install-Toolchain $compatibilitySelection 'MSRV compatibility' + } + } else { + $compatibilitySelection.Value + } + exit 0 +} + $selection = if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { [pscustomobject]@{ Value = $env:RUSTUP_TOOLCHAIN; Source = 'environment'; Installable = $false } } elseif ($null -ne $fileSelection) { @@ -128,23 +204,7 @@ $selection = if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { } if ($InstallIfMissing) { - if (-not $selection.Installable) { - exit 0 - } - - $installed = rustup toolchain list - if ($LASTEXITCODE -ne 0) { - throw 'anvil: rustup toolchain list failed' - } - if ($installed -match "(?m)^$([regex]::Escape($selection.Value))(?:-|$)") { - exit 0 - } - - Write-Host "anvil: installing stable toolchain '$($selection.Value)'" - rustup toolchain install $selection.Value --profile minimal - if ($LASTEXITCODE -ne 0) { - throw "anvil: failed to install stable toolchain '$($selection.Value)'" - } + Install-Toolchain $selection 'stable' exit 0 } diff --git a/.github/actions/anvil-impact/action.yml b/.github/actions/anvil-impact/action.yml index 705fd40e3..4c1f8b14a 100644 --- a/.github/actions/anvil-impact/action.yml +++ b/.github/actions/anvil-impact/action.yml @@ -19,6 +19,8 @@ description: | (modified ∪ rev-deps). include_required - same shape, for crates in the required set (affected ∪ workspace-internal transitive deps). + msrv_compatibility_required - "true" when a selecting toolchain file + differs from the root MSRV. Recipes in justfiles/anvil/checks/ interpret each variable per their tier: modified-tier recipes (fmt, license-headers, spellcheck, ...) short- @@ -39,6 +41,9 @@ outputs: include_required: description: Pre-formatted --package args for the required tier. value: ${{ steps.compute.outputs.include_required }} + msrv_compatibility_required: + description: Whether a selecting toolchain file differs from the root MSRV. + value: ${{ steps.msrv_compatibility.outputs.required }} runs: using: composite steps: @@ -53,6 +58,13 @@ runs: - name: Install cargo-delta shell: bash run: just anvil-tool-cargo-delta-install binstall + - id: msrv_compatibility + name: Detect MSRV compatibility run + shell: pwsh -NoProfile -Command ". '{0}'" + run: | + $toolchain = (& '.anvil/resolve-stable-toolchain.ps1' -MsrvCompatibilityToolchain | Out-String).Trim() + $required = -not [string]::IsNullOrWhiteSpace($toolchain) + "required=$($required.ToString().ToLowerInvariant())" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append - id: compute name: Compute impact shell: bash diff --git a/.github/actions/anvil-setup/action.yml b/.github/actions/anvil-setup/action.yml index ed6fa2446..6105a4a22 100644 --- a/.github/actions/anvil-setup/action.yml +++ b/.github/actions/anvil-setup/action.yml @@ -118,9 +118,9 @@ runs: # the save -- leaving the cache empty forever. The restore-keys # fall back across jobs so the install work is still shared # across sibling legs on subsequent runs. - key: anvil-v1-${{ runner.os }}-${{ runner.arch }}-rust${{ steps.rustc-version.outputs.version }}-${{ hashFiles('Cargo.lock', '.cargo/config.toml', 'rust-toolchain.toml', 'justfiles/anvil/versions.just') }}-${{ github.job }} + key: anvil-v1-${{ runner.os }}-${{ runner.arch }}-rust${{ steps.rustc-version.outputs.version }}-${{ hashFiles('Cargo.toml', 'Cargo.lock', '.cargo/config.toml', 'rust-toolchain', 'rust-toolchain.toml', 'justfiles/anvil/versions.just') }}-${{ github.job }} restore-keys: | - anvil-v1-${{ runner.os }}-${{ runner.arch }}-rust${{ steps.rustc-version.outputs.version }}-${{ hashFiles('Cargo.lock', '.cargo/config.toml', 'rust-toolchain.toml', 'justfiles/anvil/versions.just') }}- + anvil-v1-${{ runner.os }}-${{ runner.arch }}-rust${{ steps.rustc-version.outputs.version }}-${{ hashFiles('Cargo.toml', 'Cargo.lock', '.cargo/config.toml', 'rust-toolchain', 'rust-toolchain.toml', 'justfiles/anvil/versions.just') }}- anvil-v1-${{ runner.os }}-${{ runner.arch }}-rust${{ steps.rustc-version.outputs.version }}- anvil-v1-${{ runner.os }}-${{ runner.arch }}- # `.crates.toml` and `.crates2.json` track which cargo-installed diff --git a/.github/workflows/anvil-pr-impl.yml b/.github/workflows/anvil-pr-impl.yml index 7ad5cd5d7..58ea25ad4 100644 --- a/.github/workflows/anvil-pr-impl.yml +++ b/.github/workflows/anvil-pr-impl.yml @@ -70,6 +70,7 @@ jobs: include_modified: ${{ steps.delta.outputs.include_modified }} include_affected: ${{ steps.delta.outputs.include_affected }} include_required: ${{ steps.delta.outputs.include_required }} + msrv_compatibility_required: ${{ steps.delta.outputs.msrv_compatibility_required }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -211,6 +212,22 @@ jobs: token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: false + pr-msrv: + name: "Check Group: MSRV Compatibility (linux)" + needs: impact-linux + if: needs.impact-linux.outputs.msrv_compatibility_required == 'true' + runs-on: ${{ inputs.linux_runner }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + lfs: true + - uses: ./.github/actions/anvil-run-group + with: + group: pr-msrv + publish_commit_statuses: ${{ inputs.publish_commit_statuses }} + free-disk-space: true + include_affected: ${{ needs.impact-linux.outputs.include_affected }} + pr-runtime-analysis: name: "Check Group: Runtime Analysis (${{ matrix.os }})" # Stricter-runtime correctness: miri + careful. diff --git a/crates/cargo-anvil/README.md b/crates/cargo-anvil/README.md index 17867d9fb..16129d6a5 100644 --- a/crates/cargo-anvil/README.md +++ b/crates/cargo-anvil/README.md @@ -250,9 +250,10 @@ sub-groups run sequentially within the one job per OS leg): udepsruns twice: with and without --all-targets semver-checkfindings and inconclusive comparisons are advisory (posts a PR comment); Anvil preflight failures remain enforcing external-types - pr-slowpr-testllvm-covdual feature-config; gated by cargo-coverage-gate + pr-slowpr-testllvm-covdual feature-config; gated by cargo-coverage-gate doc-testruns both feature configs examplescompile-only + pr-msrvmsrv-testconditional dual feature-config compatibility test pr-runtime-analysismirilibtest, not nextest carefulself-cleans on a toolchain bump loomopt-in targets only @@ -445,7 +446,7 @@ And `docs/verification.md` for the continuous-validation strategy. This crate was developed as part of The Oxidizer Project. Browse this crate's source code. - [__cargo_doc2readme_dependencies_info]: ggGmYW0CYXZlMC43LjJhdIQbFhzZ8rzWNNYbuRaDSGWynFgbH4PMdoT7GNcbVwNPtPjAhvFhYvRhcoQbnLd9XGSG1pUbKkS1MKyxFjEbACJD5-7hMiMbhc9RgQO7NuFhZIGDa2NhcmdvLWFudmlsZTAuNS4wa2NhcmdvX2Fudmls + [__cargo_doc2readme_dependencies_info]: ggGmYW0CYXZlMC43LjJhdIQbFhzZ8rzWNNYbuRaDSGWynFgbH4PMdoT7GNcbVwNPtPjAhvFhYvRhcoQbywWDnJjuEVQbVf5umUifVEQbY6gqzBFsQbEbp97bK82npRRhZIGDa2NhcmdvLWFudmlsZTAuNS4wa2NhcmdvX2Fudmls [__link0]: https://crates.io/crates/cargo-delta [__link1]: https://crates.io/crates/cargo-spellcheck [__link2]: https://crates.io/crates/cargo-coverage-gate diff --git a/crates/cargo-anvil/docs/design/README.md b/crates/cargo-anvil/docs/design/README.md index 74f7eff1d..8d484bd98 100644 --- a/crates/cargo-anvil/docs/design/README.md +++ b/crates/cargo-anvil/docs/design/README.md @@ -396,7 +396,8 @@ pipeline. | Group | OS / arch scope (default) | Rationale | |-------------------------------------------------------------|----------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------| | `pr-fast`, `scheduled-advisories` | All legs above | Contain compile-sensitive checks (clippy, doc-build, udeps, semver-check, external-types) that only see the host's compiled crate graph -- cfg-gated code is invisible to a single-leg run. Text/metadata checks running redundantly is cheaper than splitting jobs. | -| `pr-test`, `pr-runtime-analysis`, `scheduled-test` | All legs above | Where compile-time and runtime OS / arch bugs actually surface. The three `pr-slow*` groups run as parallel cloud-workflow jobs (split out from a former single `pr-slow`) for shorter wall-clock per leg. | +| `pr-test`, `pr-runtime-analysis`, `scheduled-test` | All legs above | Where compile-time and runtime OS / arch bugs actually surface. The slow PR groups run as parallel cloud-workflow jobs for shorter wall-clock per leg. | +| `pr-msrv` | Linux x86_64 | Compatibility tests under the declared MSRV when a selecting toolchain file uses a different compiler. | | `pr-mutants` | GH: Linux x86_64 + Windows x86_64 + Linux aarch64 (windows-arm self-skips). ADO: Linux x86_64 + Windows x86_64 | Diff-scoped mutation testing. cargo-mutants doesn't build on `aarch64-pc-windows-msvc`; the recipe self-skips so the windows-arm leg is a no-op. | | `scheduled-exhaustive` | Linux x86_64 + Windows x86_64 | Full `cargo-mutants` / `cargo-hack` / `bench`. cargo-mutants doesn't build on `aarch64-pc-windows-msvc`; rather than splitting the matrix to add an ARM-Linux leg for cargo-hack and bench, the whole group is x86-only. Adopters with ARM-specific concerns extend the matrix in their root workflow. | diff --git a/crates/cargo-anvil/docs/design/ado.md b/crates/cargo-anvil/docs/design/ado.md index b2bb240b8..e42a9db48 100644 --- a/crates/cargo-anvil/docs/design/ado.md +++ b/crates/cargo-anvil/docs/design/ado.md @@ -353,6 +353,15 @@ checks actually consume is the catalog's concern, not the wiring layer's. This m moving a check between groups (e.g. `clippy` from `pr-fast` to `scheduled-advisories`) never changes the stages template. +The PR template also contains a Linux-only `pr_msrv` stage. It runs in parallel +with `pr_test`, `pr_runtime_analysis`, and `pr_mutants`, consumes the Linux +affected-package impact set, and invokes `anvil-pr-msrv`. The recipe is a no-op +unless a selecting toolchain file differs from the root MSRV. The Linux impact job +publishes that condition so ADO skips the stage before setup when no run is needed. +Internal pipelines set `ANVIL_MSRV_TOOLCHAIN` to the provisioned `ms-prod-*` +equivalent of that MSRV; public pipelines let Anvil install the declared MSRV +through rustup. + ### 4.1 Per-job wrapper (`steps/job.yml`) — the 1ESPT extensibility point Every job in `pr.yml` and `scheduled.yml` is rendered through a wrapper template at @@ -627,13 +636,15 @@ via `anvil-tool-cargo-delta-install` and runs configured snapshots and `cargo delta impact --format json`. The snapshots use `_anvil-base-ref`; cargo- delta's changed-file detection uses the fixed `[git].remote_branch` policy from the repository's `.delta.toml` when configured. Each tier becomes a pre-built -`--package …` string or `--skip`; the -three results are exported as ADO output variables via +`--package …` string or `--skip`. Impact also detects whether a selecting +toolchain file differs from the root MSRV. The four results are exported as ADO +output variables via `##vso[task.setvariable variable=…;isOutput=true]`: - `compute.include_modified` - `compute.include_affected` - `compute.include_required` +- `compute.msrv_compatibility_required` Downstream jobs reference them via `dependencies.impact.outputs['compute.']` inside the runtime macro `$[ … ]` (rather than the compile-time `${{ … }}` macro) diff --git a/crates/cargo-anvil/docs/design/checks.md b/crates/cargo-anvil/docs/design/checks.md index 69d2be0a1..6427b449c 100644 --- a/crates/cargo-anvil/docs/design/checks.md +++ b/crates/cargo-anvil/docs/design/checks.md @@ -45,6 +45,7 @@ flowchart LR pr --> pr_fast[anvil-pr-fast]:::group pr --> pr_slow[anvil-pr-slow]:::group pr_slow --> pr_test[anvil-pr-test]:::group + pr_slow --> pr_msrv[anvil-pr-msrv]:::group pr_slow --> pr_runtime_analysis[anvil-pr-runtime-analysis]:::group pr_slow --> pr_mutants[anvil-pr-mutants]:::group @@ -73,6 +74,7 @@ flowchart LR pr_test --> llvm_cov[llvm-cov]:::check pr_test --> doc_test[doc-test]:::check pr_test --> examples[examples]:::check + pr_msrv --> msrv_test[msrv-test]:::check pr_runtime_analysis --> miri[miri]:::check pr_runtime_analysis --> careful[careful]:::check @@ -104,18 +106,26 @@ flowchart LR classDef check fill:#f3e8ff,stroke:#6f42c1,stroke-width:1px,font-size:10px; ``` -(Tier nodes are the user-facing entry points; group nodes are the unit of cloud workflows parallelization; check nodes are the individual `anvil-` recipes. `pr-test`, `pr-runtime-analysis`, and `pr-mutants` were split out from a single `pr-slow` group so the three workloads run as parallel cloud-workflow jobs per OS leg rather than sequentially in one job. Locally, `just anvil-pr-slow` is an umbrella recipe that invokes the three sub-recipes in order, and `just anvil` is an alias for `just anvil-pr`.) +(Tier nodes are the user-facing entry points; group nodes are the unit of cloud +workflows parallelization; check nodes are the individual `anvil-` recipes. +The four `pr-slow` workloads run as parallel cloud-workflow jobs/stages rather +than sequentially. Locally, `just anvil-pr-slow` invokes the four sub-recipes in +order, and `just anvil` is an alias for `just anvil-pr`.) -### PR tier (4 groups) +### PR tier (5 groups) | Group | OS scope | Purpose | |--------------------|---------------------------------------|----------------------------------------------------------------------------------------------------------------------| | `pr-fast` | Linux x86_64 + Windows x86_64 + Linux aarch64 + Windows aarch64 (GH) / Linux x86_64 + Windows x86_64 (ADO) | All static analysis: clippy, `udeps`, `semver-check`, `external-types`, plus the text/metadata checks (fmt, license-headers, ...). Cross-OS because clippy, doc-build, udeps, semver-check, and external-types all compile per host target. Text/metadata checks run on every leg too; the redundancy cost is negligible compared to a separate job's setup overhead. | | `pr-test` | Same default as `pr-fast` | Tests + coverage: `llvm-cov` (instrumented `nextest`), `doc-test`, `examples`. Coverage is uploaded once from the canonical x86_64 Linux leg. | +| `pr-msrv` | Linux x86_64 only | Compatibility tests under the declared MSRV when a selecting root toolchain file uses a different compiler. The job is a cheap no-op when no compatibility run is needed. | | `pr-runtime-analysis` | Same default as `pr-fast` | Stricter-runtime correctness: `miri`, `careful`, `loom` (concurrency model checking), `bolero` (short-duration fuzzing smoke). Impact-scoped via `ANVIL_INCLUDE_AFFECTED` so wall-clock is proportional to the PR's blast radius; the cheap checks (loom/bolero) self-skip when no affected crate ships their harness. | | `pr-mutants` | Linux x86_64 + Windows x86_64 + Linux aarch64 (GH) / Linux x86_64 + Windows x86_64 (ADO) | Diff-scoped mutation testing (`mutants --in-diff`). The recipe self-skips on `aarch64-pc-windows-msvc` (cargo-mutants doesn't build there), so the GH windows-arm leg is a no-op rather than a job failure. | -The three `pr-slow*` groups are independent: failures in `pr-test` don't block `pr-runtime-analysis` or `pr-mutants` from running, and overall PR wall-clock is `max(pr-test, pr-runtime-analysis, pr-mutants)` per leg rather than the sum. Locally, `just anvil-pr-slow` is an umbrella recipe that runs all three sub-recipes sequentially so adopters who want "run everything slow" don't have to type three commands. +The four `pr-slow*` groups are independent: failures in `pr-test` don't block +`pr-msrv`, `pr-runtime-analysis`, or `pr-mutants`, and overall PR wall-clock is +their maximum rather than their sum. Locally, `just anvil-pr-slow` is an umbrella +recipe that runs all four sub-recipes sequentially. ### scheduled tier (4 groups) @@ -138,7 +148,7 @@ backend-specific knobs ([github.md §4](./github.md#4-owned-reusable-workflows) the per-leg runner-label inputs and forking the workflow when the matrix shape itself needs to change, [ado.md §4](./ado.md#4-owned-stages-templates) for `linuxPool`/`windowsPool`). -Locally there is no OS matrix; `just anvil-pr-slow` (the umbrella recipe) runs the three sub-recipes in sequence against whatever OS the +Locally there is no OS matrix; `just anvil-pr-slow` (the umbrella recipe) runs the four sub-recipes in sequence against whatever OS the developer is on. See [README.md §8.3](./README.md#83-cross-os-test-matrices) for the overall rationale. @@ -175,10 +185,10 @@ that provided the strongest version of the check. ### `pr-slow` -The PR-tier slow checks are split into three independent cloud-workflow-visible groups — -`pr-test`, `pr-runtime-analysis`, `pr-mutants` — that each run as their own job (GitHub) or +The PR-tier slow checks are split into four independent cloud-workflow-visible groups — +`pr-test`, `pr-msrv`, `pr-runtime-analysis`, `pr-mutants` — that each run as their own job (GitHub) or stage (ADO) in parallel. An umbrella `anvil-pr-slow` recipe is also provided in -`groups.just` for local use; it invokes the three sub-recipes sequentially so +`groups.just` for local use; it invokes the four sub-recipes sequentially so adopters can type one command to run "everything slow" without needing the cloud workflow matrix overhead. @@ -192,6 +202,21 @@ matrix overhead. This is the same set of checks that used to live in the standalone `pr-test` group; merging into `pr-test` removes one cloud-workflow job from the matrix without changing what runs. +#### `pr-msrv` (minimum-version compatibility) + +When a root `rust-toolchain` or `rust-toolchain.toml` selects a compiler whose +numeric major/minor version differs from the root MSRV, Anvil runs affected-package +tests under the MSRV with both all features and default features. Non-numeric +channels such as `stable` and path toolchains conservatively count as different. +A components-only toolchain file does not select a compiler and therefore does not +trigger this check. + +The compatibility run uses a dedicated Linux x86_64 job/stage in parallel with the +other PR groups. Impact preparation publishes whether the run is required, allowing +the cloud workflow to skip the job/stage before setup when it is not. +`ANVIL_MSRV_TOOLCHAIN` may map the public MSRV to an already-provisioned internal +toolchain; when it is unset, setup installs the declared MSRV through rustup. + #### `pr-runtime-analysis` (stricter-runtime correctness) | Check | Invocation | Source | diff --git a/crates/cargo-anvil/docs/design/github.md b/crates/cargo-anvil/docs/design/github.md index 58db6e6ef..27eeb5d19 100644 --- a/crates/cargo-anvil/docs/design/github.md +++ b/crates/cargo-anvil/docs/design/github.md @@ -368,10 +368,21 @@ jobs: include_affected: ${{ needs.impact.outputs.include_affected }} include_required: ${{ needs.impact.outputs.include_required }} - # pr-runtime-analysis (miri + careful) and pr-mutants (mutants) follow the same - # shape; pr-mutants additionally sets `env: BASE_REF` for diff-scoped - # cargo-mutants, and the anvil-mutants-diff recipe self-skips on - # aarch64-pc-windows-msvc (where cargo-mutants doesn't build). + pr-msrv: + name: "Check Group: MSRV Compatibility (linux)" + needs: impact-linux + if: needs.impact-linux.outputs.msrv_compatibility_required == 'true' + runs-on: ${{ inputs.linux_runner }} + steps: + - uses: actions/checkout + - uses: ./.github/actions/anvil-run-group + with: + group: pr-msrv + include_affected: ${{ needs.impact-linux.outputs.include_affected }} + + # pr-runtime-analysis (miri + careful) and pr-mutants (mutants) follow the + # multi-OS shape; pr-mutants additionally sets `env: BASE_REF` for + # diff-scoped cargo-mutants. ``` Every multi-OS job hardcodes its OS axis as an inline YAML array. Per-leg runner @@ -384,6 +395,13 @@ empty matrices that GitHub Actions silently treats as "no legs to run") without meaningfully expanding what adopters could customize — anyone who wants to change the OS axis is almost certainly making other changes too. +The MSRV compatibility group is deliberately a single Linux x86_64 job rather +than another four-leg matrix. Compiler-version compatibility is substantially +less host-specific than the ordinary test suite, and running it in parallel keeps +the extra test pass from extending `pr-test` serially. The Linux impact job exposes +whether a selecting toolchain file differs from the root MSRV, so GitHub skips the +compatibility job before setup when no extra run is needed. + The pr-* jobs gate on the impact jobs *succeeding*: their `needs: [impact-linux, impact-windows]` uses GitHub's default behavior, so if an impact job fails the pr-* jobs are skipped and the run fails at impact (we add no `if: always()` / `if: @@ -752,11 +770,13 @@ Outputs: | `include_modified` | `--package X@ver --package Y@ver …` for cargo-delta's `modified` tier, or `--skip` when empty. | | `include_affected` | Same shape, for the `affected` tier (modified ∪ workspace rev-deps). | | `include_required` | Same shape, for the `required` tier (affected ∪ workspace-internal transitive deps). | +| `msrv_compatibility_required` | `true` when a selecting toolchain file differs from the declared root MSRV; otherwise `false`. | -The wiring never gates jobs on these outputs — every job runs regardless of `--skip` -status. Per-recipe interpretation lives in the recipes themselves (see [local.md §4](./local.md#4-impact-scoping-pass-through-env-vars)). -This is intentional: unscoped checks (`deny`, `audit`, `aprz`, `pr-title`, -`mutants-full`) must run on every PR even when every tier reports `--skip`. +The include-list outputs never gate jobs; recipes interpret their `--skip` +sentinels (see [local.md §4](./local.md#4-impact-scoping-pass-through-env-vars)). +The boolean compatibility output is the sole exception and gates the dedicated +`pr-msrv` job before setup. Unscoped checks (`deny`, `audit`, `aprz`, `pr-title`, +`mutants-full`) still run on every PR even when every tier reports `--skip`. The check → bucket mapping is in [checks.md §5](./checks.md#5-impact-scoping-check--env-var-mapping). diff --git a/crates/cargo-anvil/docs/design/local.md b/crates/cargo-anvil/docs/design/local.md index fa19c70ee..24418b14d 100644 --- a/crates/cargo-anvil/docs/design/local.md +++ b/crates/cargo-anvil/docs/design/local.md @@ -115,10 +115,10 @@ namespaces are kept disjoint by naming choice: no check is named `- any tier × group combination (e.g. the coverage-instrumented test check is named `llvm-cov`, not `test`, so that group names like `anvil-pr-test` unambiguously refer to a group recipe). -The `pr-slow` work is split into three independent cloud-workflow-visible sub-groups -(`pr-test`, `pr-runtime-analysis`, `pr-mutants`) so they run as parallel cloud-workflow jobs/stages. +The `pr-slow` work is split into four independent cloud-workflow-visible sub-groups +(`pr-test`, `pr-msrv`, `pr-runtime-analysis`, `pr-mutants`) so they run as parallel cloud-workflow jobs/stages. A convenience umbrella `anvil-pr-slow` recipe is also provided for local -use; it invokes the three sub-recipes sequentially. `pr-mutants` (mutants) is +use; it invokes the four sub-recipes sequentially. `pr-mutants` (mutants) is diff-scoped against the PR base; `scheduled-exhaustive` runs the full-workspace mutants recipe: @@ -129,8 +129,9 @@ anvil-pr-fast: anvil-fmt anvil-clippy anvil-cargo-sort anvil-license-headers \ anvil-deny anvil-audit anvil-udeps anvil-semver-check \ anvil-external-types anvil-aprz -anvil-pr-slow: anvil-pr-test anvil-pr-runtime-analysis anvil-pr-mutants +anvil-pr-slow: anvil-pr-test anvil-pr-msrv anvil-pr-runtime-analysis anvil-pr-mutants anvil-pr-test: anvil-llvm-cov anvil-doc-test anvil-examples +anvil-pr-msrv: anvil-msrv-test anvil-pr-runtime-analysis: anvil-miri anvil-careful anvil-loom anvil-bolero anvil-pr-mutants: anvil-mutants-diff @@ -392,6 +393,13 @@ When selection falls back to the root MSRV, prerequisite validation reads single catalog toolchain explicitly with a selecting toolchain file. Anvil does not build a per-package toolchain matrix for this uncommon case. +When a selecting root toolchain file uses a different compiler from the root MSRV, +`anvil-msrv-test` runs affected-package tests under the MSRV with all features and +with default features. Numeric channels are compared by major/minor version, so +`1.95` and `1.95.0` match; non-numeric channels and path toolchains conservatively +trigger the compatibility run. `ANVIL_MSRV_TOOLCHAIN` optionally maps the declared +MSRV to a provisioned internal toolchain without changing when the check is required. + `anvil-tool-rustc-validate-prereqs` verifies the selected compiler and the uniform MSRV rule. Per-check toolchain requirements (for example, miri, careful, and udeps need nightly) remain enforced by the matching diff --git a/crates/cargo-anvil/src/anvil/artifacts/ado.rs b/crates/cargo-anvil/src/anvil/artifacts/ado.rs index a62b74595..b21bc3588 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/ado.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/ado.rs @@ -48,6 +48,7 @@ const SCHEDULED_ROOT_PIPELINE: &str = include_str!("../../../templates/ado/sched const GROUPS: &[&str] = &[ "pr-fast", "pr-test", + "pr-msrv", "pr-runtime-analysis", "pr-mutants", "scheduled-test", @@ -155,6 +156,7 @@ pub fn scheduled_root_pipeline() -> Artifact { pub(crate) const GROUP_STEPS: &[(&str, &str)] = &[ ("pr-fast", ".pipelines/anvil/steps/pr-fast.yml"), ("pr-test", ".pipelines/anvil/steps/pr-test.yml"), + ("pr-msrv", ".pipelines/anvil/steps/pr-msrv.yml"), ("pr-runtime-analysis", ".pipelines/anvil/steps/pr-runtime-analysis.yml"), ("pr-mutants", ".pipelines/anvil/steps/pr-mutants.yml"), ("scheduled-test", ".pipelines/anvil/steps/scheduled-test.yml"), @@ -318,6 +320,9 @@ mod tests { // Downstream stages consume the per-OS job outputs from the one stage. assert!(PR_STAGES.contains("stageDependencies.impact.compute_linux.outputs")); assert!(PR_STAGES.contains("stageDependencies.impact.compute_windows.outputs")); + assert!(PR_STAGES.contains("compute.msrv_compatibility_required")); + assert!(PR_STAGES.contains("eq(variables['msrv_compatibility_required'], 'true')")); + assert!(IMPACT_STEP.contains("-MsrvCompatibilityToolchain")); assert!(PR_STAGES.contains("- template: steps/job.yml")); assert!( !PR_STAGES.contains("\n - job: "), @@ -331,8 +336,8 @@ mod tests { // Every pr-* stage depends on the single impact stage. assert_eq!( PR_STAGES.matches("dependsOn: [impact]").count(), - 4, - "each of the four pr-* stages must depend on the single impact stage" + 5, + "each of the five pr-* stages must depend on the single impact stage" ); } diff --git a/crates/cargo-anvil/src/anvil/artifacts/github.rs b/crates/cargo-anvil/src/anvil/artifacts/github.rs index 963df7e58..c67b3c457 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/github.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/github.rs @@ -137,7 +137,7 @@ mod tests { use super::*; - const PR_GROUPS: &[&str] = &["pr-fast", "pr-test", "pr-runtime-analysis", "pr-mutants"]; + const PR_GROUPS: &[&str] = &["pr-fast", "pr-test", "pr-msrv", "pr-runtime-analysis", "pr-mutants"]; const SCHEDULED_GROUPS: &[&str] = &[ "scheduled-test", "scheduled-advisories", @@ -307,6 +307,8 @@ export -f just fn impact_action_uses_group_none_and_installs_only_cargo_delta() { assert!(IMPACT_ACTION.contains("group: none")); assert!(IMPACT_ACTION.contains("anvil-tool-cargo-delta-install")); + assert!(IMPACT_ACTION.contains("steps.msrv_compatibility.outputs.required")); + assert!(IMPACT_ACTION.contains("-MsrvCompatibilityToolchain")); assert!(IMPACT_ACTION.contains("delta_config=\"$(pwd)/.delta.toml\"")); assert!(!IMPACT_ACTION.contains("remote_branch =")); assert_eq!( @@ -345,6 +347,7 @@ export -f just "impact-windows:", "pr-fast:", "pr-test:", + "pr-msrv:", "pr-runtime-analysis:", "pr-mutants:", ] { @@ -357,6 +360,8 @@ export -f just ); } assert!(PR_IMPL_WORKFLOW.contains("needs: [impact-linux, impact-windows]")); + assert!(PR_IMPL_WORKFLOW.contains("msrv_compatibility_required: ${{ steps.delta.outputs.msrv_compatibility_required }}")); + assert!(PR_IMPL_WORKFLOW.contains("if: needs.impact-linux.outputs.msrv_compatibility_required == 'true'")); assert!(PR_IMPL_WORKFLOW.contains("os: [linux, windows, linux-arm, windows-arm]")); assert!(!PR_IMPL_WORKFLOW.contains("fromJSON")); assert!(PR_IMPL_WORKFLOW.contains("PR_TITLE")); @@ -390,7 +395,7 @@ export -f just PR_IMPL_WORKFLOW .matches("publish_commit_statuses: ${{ inputs.publish_commit_statuses }}") .count(), - 4, + PR_GROUPS.len(), "every PR group job must receive the status opt-in" ); assert_eq!( @@ -407,8 +412,8 @@ export -f just ); assert_eq!( PR_IMPL_WORKFLOW.matches("free-disk-space: true").count(), - 1, - "disk cleanup should be enabled for the PR test group" + 2, + "disk cleanup should be enabled for the PR test and MSRV groups" ); } diff --git a/crates/cargo-anvil/src/anvil/artifacts/justfile.rs b/crates/cargo-anvil/src/anvil/artifacts/justfile.rs index 765e7283a..84847048a 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/justfile.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/justfile.rs @@ -128,6 +128,7 @@ const CHECK_FILES: &[(&str, &str)] = split_recipe_files!( "miri-race-coverage", "miri-strict-provenance", "miri-tree-borrows", + "msrv-test", "mutants-diff", "mutants-full", "pr-title", @@ -144,6 +145,7 @@ const GROUP_FILES: &[(&str, &str)] = split_recipe_files!( "groups", [ "pr-fast", + "pr-msrv", "pr-slow", "pr-test", "pr-runtime-analysis", @@ -372,6 +374,7 @@ mod tests { "anvil-pr-fast:", "anvil-pr-slow:", "anvil-pr-test:", + "anvil-pr-msrv:", "anvil-pr-runtime-analysis:", "anvil-pr-mutants:", "anvil-scheduled-test:", @@ -383,12 +386,15 @@ mod tests { for needle in ["anvil-pr-slow1:", "anvil-pr-slow2:", "anvil-pr-slow3:"] { assert!(!groups.contains(needle), "groups tree still contains stale '{needle}'"); } - assert!(groups.contains("anvil-pr-slow: anvil-pr-slow-validate-prereqs anvil-pr-test anvil-pr-runtime-analysis anvil-pr-mutants")); + assert!(groups.contains( + "anvil-pr-slow: anvil-pr-slow-validate-prereqs anvil-pr-test anvil-pr-msrv anvil-pr-runtime-analysis anvil-pr-mutants" + )); // Every group recipe lists its own validate-prereqs aggregate first so // all tool checks run up front (just dedups the per-check ones). for needle in [ "anvil-pr-fast: anvil-pr-fast-validate-prereqs", "anvil-pr-test: anvil-pr-test-validate-prereqs", + "anvil-pr-msrv: anvil-pr-msrv-validate-prereqs", "anvil-pr-runtime-analysis: anvil-pr-runtime-analysis-validate-prereqs", "anvil-pr-mutants: anvil-pr-mutants-validate-prereqs", "anvil-scheduled-test: anvil-scheduled-test-validate-prereqs", @@ -474,6 +480,8 @@ mod tests { assert!(STABLE_TOOLCHAIN_RESOLVER.contains("rust-version")); assert!(STABLE_TOOLCHAIN_RESOLVER.contains("ValidateWorkspaceMsrv")); assert!(STABLE_TOOLCHAIN_RESOLVER.contains("InstallIfMissing")); + assert!(STABLE_TOOLCHAIN_RESOLVER.contains("MsrvCompatibilityToolchain")); + assert!(STABLE_TOOLCHAIN_RESOLVER.contains("InstallMsrvCompatibilityIfNeeded")); } mod stable_toolchain_resolver_tests { @@ -500,7 +508,8 @@ mod tests { .args(["-NoProfile", "-File", ".anvil/resolve-stable-toolchain.ps1"]) .args(args) .current_dir(root) - .env_remove("ANVIL_STABLE_TOOLCHAIN_SOURCE"); + .env_remove("ANVIL_STABLE_TOOLCHAIN_SOURCE") + .env_remove("ANVIL_MSRV_TOOLCHAIN"); match rustup_toolchain { Some(value) => { command.env("RUSTUP_TOOLCHAIN", value); @@ -590,6 +599,71 @@ mod tests { let output = run(heterogeneous.path(), &["-ValidateWorkspaceMsrv"], Some("explicit-toolchain")); assert!(output.status.success(), "explicit override must permit heterogeneous MSRVs"); } + + #[test] + fn requests_compatibility_only_for_a_different_selecting_toolchain() { + let no_msrv = fixture("[workspace]\nresolver = \"2\"\n"); + fs::write(no_msrv.path().join("rust-toolchain.toml"), "[toolchain]\nchannel = \"1.94\"\n") + .expect("toolchain fixture must be writable"); + let output = run(no_msrv.path(), &["-MsrvCompatibilityToolchain"], None); + assert!( + output.status.success(), + "a repository without a declared MSRV must skip compatibility" + ); + assert!( + String::from_utf8(output.stdout) + .expect("resolver output must be UTF-8") + .trim() + .is_empty() + ); + + let temp = fixture("[workspace.package]\nrust-version = \"1.93\"\n"); + let root = temp.path(); + let compatibility = |root: &Path| { + let output = run(root, &["-MsrvCompatibilityToolchain"], None); + assert!( + output.status.success(), + "compatibility resolution failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + String::from_utf8(output.stdout) + .expect("resolver output must be UTF-8") + .trim() + .to_owned() + }; + + assert!(compatibility(root).is_empty()); + fs::write(root.join("rust-toolchain.toml"), "[toolchain]\ncomponents = [\"clippy\"]\n") + .expect("toolchain fixture must be writable"); + assert!(compatibility(root).is_empty()); + + fs::write(root.join("rust-toolchain.toml"), "[toolchain]\nchannel = \"1.93.0\"\n").expect("toolchain fixture must be writable"); + assert!(compatibility(root).is_empty()); + + fs::write(root.join("rust-toolchain.toml"), "[toolchain]\nchannel = \"1.94\"\n").expect("toolchain fixture must be writable"); + assert_eq!(compatibility(root), "1.93"); + let output = Command::new("pwsh") + .args([ + "-NoProfile", + "-File", + ".anvil/resolve-stable-toolchain.ps1", + "-MsrvCompatibilityToolchain", + ]) + .current_dir(root) + .env_remove("RUSTUP_TOOLCHAIN") + .env("ANVIL_MSRV_TOOLCHAIN", "ms-prod-1.93") + .output() + .expect("pwsh must be available to test the generated resolver"); + assert!(output.status.success()); + assert_eq!( + String::from_utf8(output.stdout).expect("resolver output must be UTF-8").trim(), + "ms-prod-1.93" + ); + + fs::write(root.join("rust-toolchain.toml"), "[toolchain]\npath = \"toolchains/custom\"\n") + .expect("toolchain fixture must be writable"); + assert_eq!(compatibility(root), "1.93"); + } } #[test] diff --git a/crates/cargo-anvil/src/lib.rs b/crates/cargo-anvil/src/lib.rs index 16ab49f22..d596bb1ec 100644 --- a/crates/cargo-anvil/src/lib.rs +++ b/crates/cargo-anvil/src/lib.rs @@ -251,9 +251,10 @@ //! udepsruns twice: with and without --all-targets //! semver-checkfindings and inconclusive comparisons are advisory (posts a PR comment); Anvil preflight failures remain enforcing //! external-types -//! pr-slowpr-testllvm-covdual feature-config; gated by cargo-coverage-gate +//! pr-slowpr-testllvm-covdual feature-config; gated by cargo-coverage-gate //! doc-testruns both feature configs //! examplescompile-only +//! pr-msrvmsrv-testconditional dual feature-config compatibility test //! pr-runtime-analysismirilibtest, not nextest //! carefulself-cleans on a toolchain bump //! loomopt-in targets only diff --git a/crates/cargo-anvil/src/run.rs b/crates/cargo-anvil/src/run.rs index cb03b78ef..32bc04365 100644 --- a/crates/cargo-anvil/src/run.rs +++ b/crates/cargo-anvil/src/run.rs @@ -1001,6 +1001,7 @@ mod tests { ".pipelines/anvil/steps/advisory-comments.yml", ".pipelines/anvil/steps/pr-fast.yml", ".pipelines/anvil/steps/pr-test.yml", + ".pipelines/anvil/steps/pr-msrv.yml", ".pipelines/anvil/steps/pr-runtime-analysis.yml", ".pipelines/anvil/steps/pr-mutants.yml", ".pipelines/anvil/steps/scheduled-test.yml", diff --git a/crates/cargo-anvil/templates/ado/pr-stages.yml b/crates/cargo-anvil/templates/ado/pr-stages.yml index 6e1ac2f8c..dbd2588fc 100644 --- a/crates/cargo-anvil/templates/ado/pr-stages.yml +++ b/crates/cargo-anvil/templates/ado/pr-stages.yml @@ -105,12 +105,12 @@ stages: include_affected: $(include_affected_windows) include_required: $(include_required_windows) - # The PR-tier slow checks are split into three independent stages - # (pr_test, pr_runtime_analysis, pr_mutants) so they run in parallel rather + # The PR-tier slow checks are split into four independent stages + # (pr_test, pr_msrv, pr_runtime_analysis, pr_mutants) so they run in parallel rather # than sequentially in one job. Each stage owns its own dependsOn # link to the impact stages and its own variable hookup. ADO has # no hosted ARM agents, so the matrix stays Linux + Windows x86_64 - # across all three stages. + # across the multi-OS stages. - stage: pr_test displayName: anvil pr-test (tests + coverage) @@ -167,6 +167,23 @@ stages: summaryFileLocation: target/coverage/cobertura-*.xml failIfCoverageEmpty: false + - stage: pr_msrv + displayName: anvil pr-msrv (MSRV compatibility) + dependsOn: [impact] + condition: and(succeeded(), eq(variables['msrv_compatibility_required'], 'true')) + variables: + include_affected_linux: $[ stageDependencies.impact.compute_linux.outputs['compute.include_affected'] ] + msrv_compatibility_required: $[ stageDependencies.impact.compute_linux.outputs['compute.msrv_compatibility_required'] ] + jobs: + - template: steps/job.yml + parameters: + name: linux + pool: ${{ parameters.linuxPool }} + steps: + - template: steps/pr-msrv.yml + parameters: + include_affected: $(include_affected_linux) + - stage: pr_runtime_analysis displayName: anvil pr-runtime-analysis (miri + careful) dependsOn: [impact] diff --git a/crates/cargo-anvil/templates/ado/steps/impact.yml b/crates/cargo-anvil/templates/ado/steps/impact.yml index 2331ee9c0..d808571fa 100644 --- a/crates/cargo-anvil/templates/ado/steps/impact.yml +++ b/crates/cargo-anvil/templates/ado/steps/impact.yml @@ -15,6 +15,8 @@ # include_affected - same shape, for the affected set (modified ∪ rev-deps). # include_required - same shape, for the required set # (affected ∪ workspace-internal transitive deps). +# msrv_compatibility_required - "true" when a selecting toolchain file +# differs from the root MSRV. # # Unscoped recipes (deny, audit, aprz, pr-title) ignore all three. steps: @@ -46,6 +48,12 @@ steps: # worktree's rust-toolchain.toml. Capture it here, in the PR checkout, # where the active toolchain is resolved and installed. active_toolchain="$(rustup show active-toolchain | head -n1 | cut -d' ' -f1)" + compatibility_toolchain="$(pwsh -NoProfile -File .anvil/resolve-stable-toolchain.ps1 -MsrvCompatibilityToolchain)" + if [ -n "$compatibility_toolchain" ]; then + echo "##vso[task.setvariable variable=msrv_compatibility_required;isOutput=true]true" + else + echo "##vso[task.setvariable variable=msrv_compatibility_required;isOutput=true]false" + fi # Two snapshots + impact compare (cargo-delta has no --base # shortcut). The baseline runs in a worktree at the merge target # so the checked-out tree is untouched. diff --git a/crates/cargo-anvil/templates/ado/steps/setup.yml b/crates/cargo-anvil/templates/ado/steps/setup.yml index f1c7353be..04b699e93 100644 --- a/crates/cargo-anvil/templates/ado/steps/setup.yml +++ b/crates/cargo-anvil/templates/ado/steps/setup.yml @@ -39,7 +39,7 @@ steps: # lockfile hashes + catalog hash. Including arch keeps any # future ARM pool an adopter adds from colliding with x86_64 on # the same OS namespace. - key: 'anvil-v1 | "$(Agent.OS)" | "$(Agent.OSArchitecture)" | rust$(anvil_rustc_version) | Cargo.lock | .cargo/config.toml | rust-toolchain.toml | justfiles/anvil/versions.just' + key: 'anvil-v1 | "$(Agent.OS)" | "$(Agent.OSArchitecture)" | rust$(anvil_rustc_version) | Cargo.toml | Cargo.lock | .cargo/config.toml | rust-toolchain | rust-toolchain.toml | justfiles/anvil/versions.just' restoreKeys: | anvil-v1 | "$(Agent.OS)" | "$(Agent.OSArchitecture)" | rust$(anvil_rustc_version) anvil-v1 | "$(Agent.OS)" | "$(Agent.OSArchitecture)" diff --git a/crates/cargo-anvil/templates/anvil/resolve-stable-toolchain.ps1 b/crates/cargo-anvil/templates/anvil/resolve-stable-toolchain.ps1 index e0351d6b0..df8cbfd35 100644 --- a/crates/cargo-anvil/templates/anvil/resolve-stable-toolchain.ps1 +++ b/crates/cargo-anvil/templates/anvil/resolve-stable-toolchain.ps1 @@ -5,7 +5,9 @@ param( [switch] $ValidateWorkspaceMsrv, [switch] $InstallIfMissing, - [switch] $ForEnvironment + [switch] $ForEnvironment, + [switch] $MsrvCompatibilityToolchain, + [switch] $InstallMsrvCompatibilityIfNeeded ) $ErrorActionPreference = 'Stop' @@ -22,7 +24,13 @@ function Get-SelectingToolchainFile { $content = Get-Content -LiteralPath $path -Raw if ($name -eq 'rust-toolchain' -and $content.TrimStart() -notmatch '^\[') { - return [pscustomobject]@{ Path = $path; Value = $content.Trim(); Source = 'file'; Installable = $false } + return [pscustomobject]@{ + Path = $path + Value = $content.Trim() + Selector = 'channel' + Source = 'file' + Installable = $false + } } $inToolchain = $false @@ -43,6 +51,7 @@ function Get-SelectingToolchainFile { return [pscustomobject]@{ Path = $path Value = $value + Selector = $Matches[1] Source = 'file' Installable = $false } @@ -53,7 +62,7 @@ function Get-SelectingToolchainFile { return $null } -function Get-RootMsrv { +function Get-RootMsrv([switch] $AllowMissing) { $manifestPath = Join-Path $repoRoot 'Cargo.toml' if (-not (Test-Path -LiteralPath $manifestPath -PathType Leaf)) { throw "anvil: Cargo.toml not found at repository root '$repoRoot'" @@ -79,6 +88,9 @@ function Get-RootMsrv { return $values['package'] } + if ($AllowMissing) { + return $null + } throw 'anvil: no selecting rust-toolchain(.toml) and no root [workspace.package] or [package] rust-version; declare an MSRV or select a toolchain explicitly' } @@ -106,6 +118,50 @@ function Assert-UniformWorkspaceMsrv { } } +function Get-MsrvCompatibilitySelection { + if ($null -eq $fileSelection) { + return $null + } + + $msrv = Get-RootMsrv -AllowMissing + if ([string]::IsNullOrWhiteSpace($msrv)) { + return $null + } + $selectedMatch = [regex]::Match($fileSelection.Value, '^(\d+)\.(\d+)(?:\.\d+)?(?:-.+)?$') + $msrvMatch = [regex]::Match($msrv, '^(\d+)\.(\d+)(?:\.\d+)?(?:-.+)?$') + if ($fileSelection.Selector -eq 'channel' -and $selectedMatch.Success -and $msrvMatch.Success) { + if ($selectedMatch.Groups[1].Value -eq $msrvMatch.Groups[1].Value -and + $selectedMatch.Groups[2].Value -eq $msrvMatch.Groups[2].Value) { + return $null + } + } + + if (-not [string]::IsNullOrWhiteSpace($env:ANVIL_MSRV_TOOLCHAIN)) { + return [pscustomobject]@{ Value = $env:ANVIL_MSRV_TOOLCHAIN; Installable = $false; Mapped = $true } + } + return [pscustomobject]@{ Value = $msrv; Installable = $true; Mapped = $false } +} + +function Install-Toolchain([object] $toolchain, [string] $label) { + if (-not $toolchain.Installable) { + return + } + + $installed = rustup toolchain list + if ($LASTEXITCODE -ne 0) { + throw 'anvil: rustup toolchain list failed' + } + if ($installed -match "(?m)^$([regex]::Escape($toolchain.Value))(?:-|$)") { + return + } + + Write-Host "anvil: installing $label toolchain '$($toolchain.Value)'" + rustup toolchain install $toolchain.Value --profile minimal + if ($LASTEXITCODE -ne 0) { + throw "anvil: failed to install $label toolchain '$($toolchain.Value)'" + } +} + $fileSelection = Get-SelectingToolchainFile if ($ValidateWorkspaceMsrv) { $skipWorkspaceMsrvValidation = if ([string]::IsNullOrWhiteSpace($env:ANVIL_STABLE_TOOLCHAIN_SOURCE)) { @@ -119,6 +175,26 @@ if ($ValidateWorkspaceMsrv) { exit 0 } +if ($MsrvCompatibilityToolchain -or $InstallMsrvCompatibilityIfNeeded) { + $compatibilitySelection = Get-MsrvCompatibilitySelection + if ($null -eq $compatibilitySelection) { + exit 0 + } + if ($InstallMsrvCompatibilityIfNeeded) { + if ($compatibilitySelection.Mapped) { + & cargo "+$($compatibilitySelection.Value)" --version *> $null + if ($LASTEXITCODE -ne 0) { + throw "anvil: mapped MSRV toolchain '$($compatibilitySelection.Value)' is unavailable; provision ANVIL_MSRV_TOOLCHAIN or unset it to let Anvil install the declared public MSRV" + } + } else { + Install-Toolchain $compatibilitySelection 'MSRV compatibility' + } + } else { + $compatibilitySelection.Value + } + exit 0 +} + $selection = if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { [pscustomobject]@{ Value = $env:RUSTUP_TOOLCHAIN; Source = 'environment'; Installable = $false } } elseif ($null -ne $fileSelection) { @@ -128,23 +204,7 @@ $selection = if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { } if ($InstallIfMissing) { - if (-not $selection.Installable) { - exit 0 - } - - $installed = rustup toolchain list - if ($LASTEXITCODE -ne 0) { - throw 'anvil: rustup toolchain list failed' - } - if ($installed -match "(?m)^$([regex]::Escape($selection.Value))(?:-|$)") { - exit 0 - } - - Write-Host "anvil: installing stable toolchain '$($selection.Value)'" - rustup toolchain install $selection.Value --profile minimal - if ($LASTEXITCODE -ne 0) { - throw "anvil: failed to install stable toolchain '$($selection.Value)'" - } + Install-Toolchain $selection 'stable' exit 0 } diff --git a/crates/cargo-anvil/templates/github/impact-action.yml b/crates/cargo-anvil/templates/github/impact-action.yml index 705fd40e3..4c1f8b14a 100644 --- a/crates/cargo-anvil/templates/github/impact-action.yml +++ b/crates/cargo-anvil/templates/github/impact-action.yml @@ -19,6 +19,8 @@ description: | (modified ∪ rev-deps). include_required - same shape, for crates in the required set (affected ∪ workspace-internal transitive deps). + msrv_compatibility_required - "true" when a selecting toolchain file + differs from the root MSRV. Recipes in justfiles/anvil/checks/ interpret each variable per their tier: modified-tier recipes (fmt, license-headers, spellcheck, ...) short- @@ -39,6 +41,9 @@ outputs: include_required: description: Pre-formatted --package args for the required tier. value: ${{ steps.compute.outputs.include_required }} + msrv_compatibility_required: + description: Whether a selecting toolchain file differs from the root MSRV. + value: ${{ steps.msrv_compatibility.outputs.required }} runs: using: composite steps: @@ -53,6 +58,13 @@ runs: - name: Install cargo-delta shell: bash run: just anvil-tool-cargo-delta-install binstall + - id: msrv_compatibility + name: Detect MSRV compatibility run + shell: pwsh -NoProfile -Command ". '{0}'" + run: | + $toolchain = (& '.anvil/resolve-stable-toolchain.ps1' -MsrvCompatibilityToolchain | Out-String).Trim() + $required = -not [string]::IsNullOrWhiteSpace($toolchain) + "required=$($required.ToString().ToLowerInvariant())" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append - id: compute name: Compute impact shell: bash diff --git a/crates/cargo-anvil/templates/github/pr-impl-workflow.yml b/crates/cargo-anvil/templates/github/pr-impl-workflow.yml index 7ad5cd5d7..58ea25ad4 100644 --- a/crates/cargo-anvil/templates/github/pr-impl-workflow.yml +++ b/crates/cargo-anvil/templates/github/pr-impl-workflow.yml @@ -70,6 +70,7 @@ jobs: include_modified: ${{ steps.delta.outputs.include_modified }} include_affected: ${{ steps.delta.outputs.include_affected }} include_required: ${{ steps.delta.outputs.include_required }} + msrv_compatibility_required: ${{ steps.delta.outputs.msrv_compatibility_required }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -211,6 +212,22 @@ jobs: token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: false + pr-msrv: + name: "Check Group: MSRV Compatibility (linux)" + needs: impact-linux + if: needs.impact-linux.outputs.msrv_compatibility_required == 'true' + runs-on: ${{ inputs.linux_runner }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + lfs: true + - uses: ./.github/actions/anvil-run-group + with: + group: pr-msrv + publish_commit_statuses: ${{ inputs.publish_commit_statuses }} + free-disk-space: true + include_affected: ${{ needs.impact-linux.outputs.include_affected }} + pr-runtime-analysis: name: "Check Group: Runtime Analysis (${{ matrix.os }})" # Stricter-runtime correctness: miri + careful. diff --git a/crates/cargo-anvil/templates/github/setup-action.yml b/crates/cargo-anvil/templates/github/setup-action.yml index ed6fa2446..6105a4a22 100644 --- a/crates/cargo-anvil/templates/github/setup-action.yml +++ b/crates/cargo-anvil/templates/github/setup-action.yml @@ -118,9 +118,9 @@ runs: # the save -- leaving the cache empty forever. The restore-keys # fall back across jobs so the install work is still shared # across sibling legs on subsequent runs. - key: anvil-v1-${{ runner.os }}-${{ runner.arch }}-rust${{ steps.rustc-version.outputs.version }}-${{ hashFiles('Cargo.lock', '.cargo/config.toml', 'rust-toolchain.toml', 'justfiles/anvil/versions.just') }}-${{ github.job }} + key: anvil-v1-${{ runner.os }}-${{ runner.arch }}-rust${{ steps.rustc-version.outputs.version }}-${{ hashFiles('Cargo.toml', 'Cargo.lock', '.cargo/config.toml', 'rust-toolchain', 'rust-toolchain.toml', 'justfiles/anvil/versions.just') }}-${{ github.job }} restore-keys: | - anvil-v1-${{ runner.os }}-${{ runner.arch }}-rust${{ steps.rustc-version.outputs.version }}-${{ hashFiles('Cargo.lock', '.cargo/config.toml', 'rust-toolchain.toml', 'justfiles/anvil/versions.just') }}- + anvil-v1-${{ runner.os }}-${{ runner.arch }}-rust${{ steps.rustc-version.outputs.version }}-${{ hashFiles('Cargo.toml', 'Cargo.lock', '.cargo/config.toml', 'rust-toolchain', 'rust-toolchain.toml', 'justfiles/anvil/versions.just') }}- anvil-v1-${{ runner.os }}-${{ runner.arch }}-rust${{ steps.rustc-version.outputs.version }}- anvil-v1-${{ runner.os }}-${{ runner.arch }}- # `.crates.toml` and `.crates2.json` track which cargo-installed diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/msrv-test.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/msrv-test.just new file mode 100644 index 000000000..0e4889236 --- /dev/null +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/msrv-test.just @@ -0,0 +1,48 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# GENERATED BY cargo-anvil. DO NOT EDIT DIRECTLY. +# Update cargo-anvil and regenerate; repository-specific edits stop automatic updates. +# Update behaviour: https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/updates.md + +# See https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/checks.md + +# Run affected-package tests under the declared MSRV when a selecting root +# toolchain file chooses a different compiler. +[script("pwsh", "-NoProfile")] +anvil-msrv-test: anvil-msrv-test-validate-prereqs + $ErrorActionPreference = 'Stop' + if ($env:ANVIL_INCLUDE_AFFECTED -eq '--skip') { exit 0 } + $toolchain = (& '.anvil/resolve-stable-toolchain.ps1' -MsrvCompatibilityToolchain | Out-String).Trim() + if ([string]::IsNullOrWhiteSpace($toolchain)) { + Write-Host 'anvil-msrv-test: selected toolchain matches the MSRV; skipping' + exit 0 + } + $pkg = @(if ($env:ANVIL_INCLUDE_AFFECTED) { -split $env:ANVIL_INCLUDE_AFFECTED } else { '--workspace' }) + & cargo "+$toolchain" test @pkg --all-targets --all-features --locked + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & cargo "+$toolchain" test @pkg --all-targets --locked + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + +# Install the MSRV compatibility toolchain only when this check is required. +[group("anvil-setup")] +[script("pwsh", "-NoProfile")] +anvil-msrv-test-setup installer="install": + & '.anvil/resolve-stable-toolchain.ps1' -InstallMsrvCompatibilityIfNeeded + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + +# Validate the conditional MSRV compatibility prerequisite. +[group("anvil-setup")] +[script("pwsh", "-NoProfile")] +anvil-msrv-test-validate-prereqs: + $toolchain = (& '.anvil/resolve-stable-toolchain.ps1' -MsrvCompatibilityToolchain | Out-String).Trim() + if ([string]::IsNullOrWhiteSpace($toolchain)) { exit 0 } + & cargo "+$toolchain" --version + if ($LASTEXITCODE -ne 0) { + $hint = if ($env:ANVIL_MSRV_TOOLCHAIN) { + 'provision ANVIL_MSRV_TOOLCHAIN or unset it to let Anvil install the declared public MSRV' + } else { + 'run: just anvil-msrv-test-setup' + } + Write-Error "anvil: MSRV compatibility toolchain '$toolchain' is unavailable; $hint" + exit $LASTEXITCODE + } diff --git a/crates/cargo-anvil/templates/justfiles/anvil/groups/pr-msrv.just b/crates/cargo-anvil/templates/justfiles/anvil/groups/pr-msrv.just new file mode 100644 index 000000000..1c25aa3f9 --- /dev/null +++ b/crates/cargo-anvil/templates/justfiles/anvil/groups/pr-msrv.just @@ -0,0 +1,19 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# GENERATED BY cargo-anvil. DO NOT EDIT DIRECTLY. +# Update cargo-anvil and regenerate; repository-specific edits stop automatic updates. +# Update behaviour: https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/updates.md + +# See https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/checks.md + +# Run pull request compatibility tests under the declared MSRV. +[group("anvil")] +anvil-pr-msrv: anvil-pr-msrv-validate-prereqs anvil-msrv-test + +# Install prerequisites for the `anvil-pr-msrv` recipe. +[group("anvil-setup")] +anvil-pr-msrv-setup installer="install": (anvil-msrv-test-setup installer) + +# Validate prerequisites for the `anvil-pr-msrv` recipe. +[group("anvil-setup")] +anvil-pr-msrv-validate-prereqs: anvil-msrv-test-validate-prereqs diff --git a/crates/cargo-anvil/templates/justfiles/anvil/groups/pr-slow.just b/crates/cargo-anvil/templates/justfiles/anvil/groups/pr-slow.just index 75ad63c1d..f063a49ec 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/groups/pr-slow.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/groups/pr-slow.just @@ -8,7 +8,7 @@ # pr-slow is the single PR-tier group for everything that takes more # than ~30s per crate (tests, stricter runtimes, mutation testing). -# It's internally split into three sub-recipes so individual concerns +# It's internally split into four sub-recipes so individual concerns # can be invoked locally without dragging the others along: # # slow1: tests + coverage (replaces the former pr-test group) @@ -26,12 +26,13 @@ # Run the slow pull request checks. [group("anvil")] -anvil-pr-slow: anvil-pr-slow-validate-prereqs anvil-pr-test anvil-pr-runtime-analysis anvil-pr-mutants +anvil-pr-slow: anvil-pr-slow-validate-prereqs anvil-pr-test anvil-pr-msrv anvil-pr-runtime-analysis anvil-pr-mutants # Install prerequisites for the `anvil-pr-slow` recipe. [group("anvil-setup")] anvil-pr-slow-setup installer="install": \ (anvil-pr-test-setup installer) \ + (anvil-pr-msrv-setup installer) \ (anvil-pr-runtime-analysis-setup installer) \ (anvil-pr-mutants-setup installer) @@ -39,5 +40,6 @@ anvil-pr-slow-setup installer="install": \ [group("anvil-setup")] anvil-pr-slow-validate-prereqs: \ anvil-pr-test-validate-prereqs \ + anvil-pr-msrv-validate-prereqs \ anvil-pr-runtime-analysis-validate-prereqs \ anvil-pr-mutants-validate-prereqs diff --git a/crates/cargo-anvil/templates/justfiles/anvil/mod.just b/crates/cargo-anvil/templates/justfiles/anvil/mod.just index 710bead68..456c9e12f 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/mod.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/mod.just @@ -59,6 +59,7 @@ import 'checks/miri.just' import 'checks/miri-race-coverage.just' import 'checks/miri-strict-provenance.just' import 'checks/miri-tree-borrows.just' +import 'checks/msrv-test.just' import 'checks/mutants-diff.just' import 'checks/mutants-full.just' import 'checks/pr-title.just' @@ -68,6 +69,7 @@ import 'checks/spellcheck.just' import 'checks/udeps.just' import 'container.just' import 'groups/pr-fast.just' +import 'groups/pr-msrv.just' import 'groups/pr-slow.just' import 'groups/pr-test.just' import 'groups/pr-runtime-analysis.just' diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap index 8a62346f4..e7b3c68d2 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap @@ -1204,7 +1204,9 @@ docker "${run_args[@]}" "$image" just "$@" param( [switch] $ValidateWorkspaceMsrv, [switch] $InstallIfMissing, - [switch] $ForEnvironment + [switch] $ForEnvironment, + [switch] $MsrvCompatibilityToolchain, + [switch] $InstallMsrvCompatibilityIfNeeded ) $ErrorActionPreference = 'Stop' @@ -1221,7 +1223,13 @@ function Get-SelectingToolchainFile { $content = Get-Content -LiteralPath $path -Raw if ($name -eq 'rust-toolchain' -and $content.TrimStart() -notmatch '^\[') { - return [pscustomobject]@{ Path = $path; Value = $content.Trim(); Source = 'file'; Installable = $false } + return [pscustomobject]@{ + Path = $path + Value = $content.Trim() + Selector = 'channel' + Source = 'file' + Installable = $false + } } $inToolchain = $false @@ -1242,6 +1250,7 @@ function Get-SelectingToolchainFile { return [pscustomobject]@{ Path = $path Value = $value + Selector = $Matches[1] Source = 'file' Installable = $false } @@ -1252,7 +1261,7 @@ function Get-SelectingToolchainFile { return $null } -function Get-RootMsrv { +function Get-RootMsrv([switch] $AllowMissing) { $manifestPath = Join-Path $repoRoot 'Cargo.toml' if (-not (Test-Path -LiteralPath $manifestPath -PathType Leaf)) { throw "anvil: Cargo.toml not found at repository root '$repoRoot'" @@ -1278,6 +1287,9 @@ function Get-RootMsrv { return $values['package'] } + if ($AllowMissing) { + return $null + } throw 'anvil: no selecting rust-toolchain(.toml) and no root [workspace.package] or [package] rust-version; declare an MSRV or select a toolchain explicitly' } @@ -1305,6 +1317,50 @@ function Assert-UniformWorkspaceMsrv { } } +function Get-MsrvCompatibilitySelection { + if ($null -eq $fileSelection) { + return $null + } + + $msrv = Get-RootMsrv -AllowMissing + if ([string]::IsNullOrWhiteSpace($msrv)) { + return $null + } + $selectedMatch = [regex]::Match($fileSelection.Value, '^(\d+)\.(\d+)(?:\.\d+)?(?:-.+)?$') + $msrvMatch = [regex]::Match($msrv, '^(\d+)\.(\d+)(?:\.\d+)?(?:-.+)?$') + if ($fileSelection.Selector -eq 'channel' -and $selectedMatch.Success -and $msrvMatch.Success) { + if ($selectedMatch.Groups[1].Value -eq $msrvMatch.Groups[1].Value -and + $selectedMatch.Groups[2].Value -eq $msrvMatch.Groups[2].Value) { + return $null + } + } + + if (-not [string]::IsNullOrWhiteSpace($env:ANVIL_MSRV_TOOLCHAIN)) { + return [pscustomobject]@{ Value = $env:ANVIL_MSRV_TOOLCHAIN; Installable = $false; Mapped = $true } + } + return [pscustomobject]@{ Value = $msrv; Installable = $true; Mapped = $false } +} + +function Install-Toolchain([object] $toolchain, [string] $label) { + if (-not $toolchain.Installable) { + return + } + + $installed = rustup toolchain list + if ($LASTEXITCODE -ne 0) { + throw 'anvil: rustup toolchain list failed' + } + if ($installed -match "(?m)^$([regex]::Escape($toolchain.Value))(?:-|$)") { + return + } + + Write-Host "anvil: installing $label toolchain '$($toolchain.Value)'" + rustup toolchain install $toolchain.Value --profile minimal + if ($LASTEXITCODE -ne 0) { + throw "anvil: failed to install $label toolchain '$($toolchain.Value)'" + } +} + $fileSelection = Get-SelectingToolchainFile if ($ValidateWorkspaceMsrv) { $skipWorkspaceMsrvValidation = if ([string]::IsNullOrWhiteSpace($env:ANVIL_STABLE_TOOLCHAIN_SOURCE)) { @@ -1318,6 +1374,26 @@ if ($ValidateWorkspaceMsrv) { exit 0 } +if ($MsrvCompatibilityToolchain -or $InstallMsrvCompatibilityIfNeeded) { + $compatibilitySelection = Get-MsrvCompatibilitySelection + if ($null -eq $compatibilitySelection) { + exit 0 + } + if ($InstallMsrvCompatibilityIfNeeded) { + if ($compatibilitySelection.Mapped) { + & cargo "+$($compatibilitySelection.Value)" --version *> $null + if ($LASTEXITCODE -ne 0) { + throw "anvil: mapped MSRV toolchain '$($compatibilitySelection.Value)' is unavailable; provision ANVIL_MSRV_TOOLCHAIN or unset it to let Anvil install the declared public MSRV" + } + } else { + Install-Toolchain $compatibilitySelection 'MSRV compatibility' + } + } else { + $compatibilitySelection.Value + } + exit 0 +} + $selection = if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { [pscustomobject]@{ Value = $env:RUSTUP_TOOLCHAIN; Source = 'environment'; Installable = $false } } elseif ($null -ne $fileSelection) { @@ -1327,23 +1403,7 @@ $selection = if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { } if ($InstallIfMissing) { - if (-not $selection.Installable) { - exit 0 - } - - $installed = rustup toolchain list - if ($LASTEXITCODE -ne 0) { - throw 'anvil: rustup toolchain list failed' - } - if ($installed -match "(?m)^$([regex]::Escape($selection.Value))(?:-|$)") { - exit 0 - } - - Write-Host "anvil: installing stable toolchain '$($selection.Value)'" - rustup toolchain install $selection.Value --profile minimal - if ($LASTEXITCODE -ne 0) { - throw "anvil: failed to install stable toolchain '$($selection.Value)'" - } + Install-Toolchain $selection 'stable' exit 0 } @@ -1579,12 +1639,12 @@ stages: include_affected: $(include_affected_windows) include_required: $(include_required_windows) - # The PR-tier slow checks are split into three independent stages - # (pr_test, pr_runtime_analysis, pr_mutants) so they run in parallel rather + # The PR-tier slow checks are split into four independent stages + # (pr_test, pr_msrv, pr_runtime_analysis, pr_mutants) so they run in parallel rather # than sequentially in one job. Each stage owns its own dependsOn # link to the impact stages and its own variable hookup. ADO has # no hosted ARM agents, so the matrix stays Linux + Windows x86_64 - # across all three stages. + # across the multi-OS stages. - stage: pr_test displayName: anvil pr-test (tests + coverage) @@ -1641,6 +1701,23 @@ stages: summaryFileLocation: target/coverage/cobertura-*.xml failIfCoverageEmpty: false + - stage: pr_msrv + displayName: anvil pr-msrv (MSRV compatibility) + dependsOn: [impact] + condition: and(succeeded(), eq(variables['msrv_compatibility_required'], 'true')) + variables: + include_affected_linux: $[ stageDependencies.impact.compute_linux.outputs['compute.include_affected'] ] + msrv_compatibility_required: $[ stageDependencies.impact.compute_linux.outputs['compute.msrv_compatibility_required'] ] + jobs: + - template: steps/job.yml + parameters: + name: linux + pool: ${{ parameters.linuxPool }} + steps: + - template: steps/pr-msrv.yml + parameters: + include_affected: $(include_affected_linux) + - stage: pr_runtime_analysis displayName: anvil pr-runtime-analysis (miri + careful) dependsOn: [impact] @@ -1933,6 +2010,8 @@ steps: # include_affected - same shape, for the affected set (modified ∪ rev-deps). # include_required - same shape, for the required set # (affected ∪ workspace-internal transitive deps). +# msrv_compatibility_required - "true" when a selecting toolchain file +# differs from the root MSRV. # # Unscoped recipes (deny, audit, aprz, pr-title) ignore all three. steps: @@ -1964,6 +2043,12 @@ steps: # worktree's rust-toolchain.toml. Capture it here, in the PR checkout, # where the active toolchain is resolved and installed. active_toolchain="$(rustup show active-toolchain | head -n1 | cut -d' ' -f1)" + compatibility_toolchain="$(pwsh -NoProfile -File .anvil/resolve-stable-toolchain.ps1 -MsrvCompatibilityToolchain)" + if [ -n "$compatibility_toolchain" ]; then + echo "##vso[task.setvariable variable=msrv_compatibility_required;isOutput=true]true" + else + echo "##vso[task.setvariable variable=msrv_compatibility_required;isOutput=true]false" + fi # Two snapshots + impact compare (cargo-delta has no --base # shortcut). The baseline runs in a worktree at the merge target # so the checked-out tree is untouched. @@ -2127,6 +2212,64 @@ steps: ANVIL_INCLUDE_AFFECTED: ${{ parameters.include_affected }} ANVIL_INCLUDE_REQUIRED: ${{ parameters.include_required }} +=== .pipelines/anvil/steps/pr-msrv.yml === +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# GENERATED BY cargo-anvil. DO NOT EDIT DIRECTLY. +# Update cargo-anvil and regenerate; repository-specific edits stop automatic updates. +# Update behaviour: https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/updates.md +# The token pr-msrv is substituted by cargo-anvil at emit time with +# the concrete check-group name (pr-fast, pr-test, scheduled-runtime-analysis, ...). +parameters: + - name: include_modified + type: string + default: '' + - name: include_affected + type: string + default: '' + - name: include_required + type: string + default: '' +steps: + - template: setup.yml + parameters: + group: pr-msrv + # ADO has no PR-title predefined variable: `System.PullRequest.Title` + # does NOT exist, so `$(System.PullRequest.Title)` would expand to the + # literal unexpanded macro text (non-empty) and make anvil-pr-title + # validate that string. Resolve the real title from the REST API on PR + # builds and publish it as the PR_TITLE pipeline variable. Only + # anvil-pr-title (in pr-fast) consults it; other groups ignore it, but we + # resolve uniformly to keep group.yml the same across groups. Non-PR builds + # skip; a known PR whose metadata cannot be retrieved fails closed. + - pwsh: | + $ErrorActionPreference = 'Stop' + $prId = $env:SYSTEM_PULLREQUEST_PULLREQUESTID + if (-not $prId) { + Write-Host 'anvil: not a PR build; leaving PR_TITLE empty' + Write-Host '##vso[task.setvariable variable=PR_TITLE]' + exit 0 + } + $uri = "$($env:SYSTEM_COLLECTIONURI)$($env:SYSTEM_TEAMPROJECTID)/_apis/git/repositories/$($env:BUILD_REPOSITORY_ID)/pullRequests/${prId}?api-version=7.0" + try { + $resp = Invoke-RestMethod -Uri $uri -Headers @{ Authorization = "Bearer $($env:SYSTEM_ACCESSTOKEN)" } + Write-Host "##vso[task.setvariable variable=PR_TITLE]$($resp.title)" + } catch { + Write-Error "anvil: could not resolve PR title for PR $prId ($_). Ensure the job can access the repository OAuth token." + exit 1 + } + displayName: anvil-pr-msrv (resolve PR title) + env: + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + - bash: just anvil-pr-msrv + displayName: anvil-pr-msrv + env: + # Resolved by the preceding step; empty only outside PR builds. + PR_TITLE: $(PR_TITLE) + ANVIL_INCLUDE_MODIFIED: ${{ parameters.include_modified }} + ANVIL_INCLUDE_AFFECTED: ${{ parameters.include_affected }} + ANVIL_INCLUDE_REQUIRED: ${{ parameters.include_required }} + === .pipelines/anvil/steps/pr-mutants.yml === # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. @@ -2575,7 +2718,7 @@ steps: # lockfile hashes + catalog hash. Including arch keeps any # future ARM pool an adopter adds from colliding with x86_64 on # the same OS namespace. - key: 'anvil-v1 | "$(Agent.OS)" | "$(Agent.OSArchitecture)" | rust$(anvil_rustc_version) | Cargo.lock | .cargo/config.toml | rust-toolchain.toml | justfiles/anvil/versions.just' + key: 'anvil-v1 | "$(Agent.OS)" | "$(Agent.OSArchitecture)" | rust$(anvil_rustc_version) | Cargo.toml | Cargo.lock | .cargo/config.toml | rust-toolchain | rust-toolchain.toml | justfiles/anvil/versions.just' restoreKeys: | anvil-v1 | "$(Agent.OS)" | "$(Agent.OSArchitecture)" | rust$(anvil_rustc_version) anvil-v1 | "$(Agent.OS)" | "$(Agent.OSArchitecture)" @@ -4090,6 +4233,56 @@ anvil-miri-setup installer="install": anvil-component-nightly-miri-install anvil [group("anvil-setup")] anvil-miri-validate-prereqs: anvil-component-nightly-miri-validate-prereqs anvil-component-nightly-rust-src-validate-prereqs +=== justfiles/anvil/checks/msrv-test.just === +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# GENERATED BY cargo-anvil. DO NOT EDIT DIRECTLY. +# Update cargo-anvil and regenerate; repository-specific edits stop automatic updates. +# Update behaviour: https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/updates.md + +# See https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/checks.md + +# Run affected-package tests under the declared MSRV when a selecting root +# toolchain file chooses a different compiler. +[script("pwsh", "-NoProfile")] +anvil-msrv-test: anvil-msrv-test-validate-prereqs + $ErrorActionPreference = 'Stop' + if ($env:ANVIL_INCLUDE_AFFECTED -eq '--skip') { exit 0 } + $toolchain = (& '.anvil/resolve-stable-toolchain.ps1' -MsrvCompatibilityToolchain | Out-String).Trim() + if ([string]::IsNullOrWhiteSpace($toolchain)) { + Write-Host 'anvil-msrv-test: selected toolchain matches the MSRV; skipping' + exit 0 + } + $pkg = @(if ($env:ANVIL_INCLUDE_AFFECTED) { -split $env:ANVIL_INCLUDE_AFFECTED } else { '--workspace' }) + & cargo "+$toolchain" test @pkg --all-targets --all-features --locked + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & cargo "+$toolchain" test @pkg --all-targets --locked + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + +# Install the MSRV compatibility toolchain only when this check is required. +[group("anvil-setup")] +[script("pwsh", "-NoProfile")] +anvil-msrv-test-setup installer="install": + & '.anvil/resolve-stable-toolchain.ps1' -InstallMsrvCompatibilityIfNeeded + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + +# Validate the conditional MSRV compatibility prerequisite. +[group("anvil-setup")] +[script("pwsh", "-NoProfile")] +anvil-msrv-test-validate-prereqs: + $toolchain = (& '.anvil/resolve-stable-toolchain.ps1' -MsrvCompatibilityToolchain | Out-String).Trim() + if ([string]::IsNullOrWhiteSpace($toolchain)) { exit 0 } + & cargo "+$toolchain" --version + if ($LASTEXITCODE -ne 0) { + $hint = if ($env:ANVIL_MSRV_TOOLCHAIN) { + 'provision ANVIL_MSRV_TOOLCHAIN or unset it to let Anvil install the declared public MSRV' + } else { + 'run: just anvil-msrv-test-setup' + } + Write-Error "anvil: MSRV compatibility toolchain '$toolchain' is unavailable; $hint" + exit $LASTEXITCODE + } + === justfiles/anvil/checks/mutants-diff.just === # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. @@ -4843,6 +5036,27 @@ anvil-pr-fast-validate-prereqs: \ anvil-semver-check-validate-prereqs \ anvil-external-types-validate-prereqs +=== justfiles/anvil/groups/pr-msrv.just === +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# GENERATED BY cargo-anvil. DO NOT EDIT DIRECTLY. +# Update cargo-anvil and regenerate; repository-specific edits stop automatic updates. +# Update behaviour: https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/updates.md + +# See https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/checks.md + +# Run pull request compatibility tests under the declared MSRV. +[group("anvil")] +anvil-pr-msrv: anvil-pr-msrv-validate-prereqs anvil-msrv-test + +# Install prerequisites for the `anvil-pr-msrv` recipe. +[group("anvil-setup")] +anvil-pr-msrv-setup installer="install": (anvil-msrv-test-setup installer) + +# Validate prerequisites for the `anvil-pr-msrv` recipe. +[group("anvil-setup")] +anvil-pr-msrv-validate-prereqs: anvil-msrv-test-validate-prereqs + === justfiles/anvil/groups/pr-mutants.just === # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. @@ -4908,7 +5122,7 @@ anvil-pr-runtime-analysis-validate-prereqs: \ # pr-slow is the single PR-tier group for everything that takes more # than ~30s per crate (tests, stricter runtimes, mutation testing). -# It's internally split into three sub-recipes so individual concerns +# It's internally split into four sub-recipes so individual concerns # can be invoked locally without dragging the others along: # # slow1: tests + coverage (replaces the former pr-test group) @@ -4926,12 +5140,13 @@ anvil-pr-runtime-analysis-validate-prereqs: \ # Run the slow pull request checks. [group("anvil")] -anvil-pr-slow: anvil-pr-slow-validate-prereqs anvil-pr-test anvil-pr-runtime-analysis anvil-pr-mutants +anvil-pr-slow: anvil-pr-slow-validate-prereqs anvil-pr-test anvil-pr-msrv anvil-pr-runtime-analysis anvil-pr-mutants # Install prerequisites for the `anvil-pr-slow` recipe. [group("anvil-setup")] anvil-pr-slow-setup installer="install": \ (anvil-pr-test-setup installer) \ + (anvil-pr-msrv-setup installer) \ (anvil-pr-runtime-analysis-setup installer) \ (anvil-pr-mutants-setup installer) @@ -4939,6 +5154,7 @@ anvil-pr-slow-setup installer="install": \ [group("anvil-setup")] anvil-pr-slow-validate-prereqs: \ anvil-pr-test-validate-prereqs \ + anvil-pr-msrv-validate-prereqs \ anvil-pr-runtime-analysis-validate-prereqs \ anvil-pr-mutants-validate-prereqs @@ -5435,6 +5651,7 @@ import 'checks/miri.just' import 'checks/miri-race-coverage.just' import 'checks/miri-strict-provenance.just' import 'checks/miri-tree-borrows.just' +import 'checks/msrv-test.just' import 'checks/mutants-diff.just' import 'checks/mutants-full.just' import 'checks/pr-title.just' @@ -5444,6 +5661,7 @@ import 'checks/spellcheck.just' import 'checks/udeps.just' import 'container.just' import 'groups/pr-fast.just' +import 'groups/pr-msrv.just' import 'groups/pr-slow.just' import 'groups/pr-test.just' import 'groups/pr-runtime-analysis.just' diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap index 79ac557a8..5784c35fb 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap @@ -1204,7 +1204,9 @@ docker "${run_args[@]}" "$image" just "$@" param( [switch] $ValidateWorkspaceMsrv, [switch] $InstallIfMissing, - [switch] $ForEnvironment + [switch] $ForEnvironment, + [switch] $MsrvCompatibilityToolchain, + [switch] $InstallMsrvCompatibilityIfNeeded ) $ErrorActionPreference = 'Stop' @@ -1221,7 +1223,13 @@ function Get-SelectingToolchainFile { $content = Get-Content -LiteralPath $path -Raw if ($name -eq 'rust-toolchain' -and $content.TrimStart() -notmatch '^\[') { - return [pscustomobject]@{ Path = $path; Value = $content.Trim(); Source = 'file'; Installable = $false } + return [pscustomobject]@{ + Path = $path + Value = $content.Trim() + Selector = 'channel' + Source = 'file' + Installable = $false + } } $inToolchain = $false @@ -1242,6 +1250,7 @@ function Get-SelectingToolchainFile { return [pscustomobject]@{ Path = $path Value = $value + Selector = $Matches[1] Source = 'file' Installable = $false } @@ -1252,7 +1261,7 @@ function Get-SelectingToolchainFile { return $null } -function Get-RootMsrv { +function Get-RootMsrv([switch] $AllowMissing) { $manifestPath = Join-Path $repoRoot 'Cargo.toml' if (-not (Test-Path -LiteralPath $manifestPath -PathType Leaf)) { throw "anvil: Cargo.toml not found at repository root '$repoRoot'" @@ -1278,6 +1287,9 @@ function Get-RootMsrv { return $values['package'] } + if ($AllowMissing) { + return $null + } throw 'anvil: no selecting rust-toolchain(.toml) and no root [workspace.package] or [package] rust-version; declare an MSRV or select a toolchain explicitly' } @@ -1305,6 +1317,50 @@ function Assert-UniformWorkspaceMsrv { } } +function Get-MsrvCompatibilitySelection { + if ($null -eq $fileSelection) { + return $null + } + + $msrv = Get-RootMsrv -AllowMissing + if ([string]::IsNullOrWhiteSpace($msrv)) { + return $null + } + $selectedMatch = [regex]::Match($fileSelection.Value, '^(\d+)\.(\d+)(?:\.\d+)?(?:-.+)?$') + $msrvMatch = [regex]::Match($msrv, '^(\d+)\.(\d+)(?:\.\d+)?(?:-.+)?$') + if ($fileSelection.Selector -eq 'channel' -and $selectedMatch.Success -and $msrvMatch.Success) { + if ($selectedMatch.Groups[1].Value -eq $msrvMatch.Groups[1].Value -and + $selectedMatch.Groups[2].Value -eq $msrvMatch.Groups[2].Value) { + return $null + } + } + + if (-not [string]::IsNullOrWhiteSpace($env:ANVIL_MSRV_TOOLCHAIN)) { + return [pscustomobject]@{ Value = $env:ANVIL_MSRV_TOOLCHAIN; Installable = $false; Mapped = $true } + } + return [pscustomobject]@{ Value = $msrv; Installable = $true; Mapped = $false } +} + +function Install-Toolchain([object] $toolchain, [string] $label) { + if (-not $toolchain.Installable) { + return + } + + $installed = rustup toolchain list + if ($LASTEXITCODE -ne 0) { + throw 'anvil: rustup toolchain list failed' + } + if ($installed -match "(?m)^$([regex]::Escape($toolchain.Value))(?:-|$)") { + return + } + + Write-Host "anvil: installing $label toolchain '$($toolchain.Value)'" + rustup toolchain install $toolchain.Value --profile minimal + if ($LASTEXITCODE -ne 0) { + throw "anvil: failed to install $label toolchain '$($toolchain.Value)'" + } +} + $fileSelection = Get-SelectingToolchainFile if ($ValidateWorkspaceMsrv) { $skipWorkspaceMsrvValidation = if ([string]::IsNullOrWhiteSpace($env:ANVIL_STABLE_TOOLCHAIN_SOURCE)) { @@ -1318,6 +1374,26 @@ if ($ValidateWorkspaceMsrv) { exit 0 } +if ($MsrvCompatibilityToolchain -or $InstallMsrvCompatibilityIfNeeded) { + $compatibilitySelection = Get-MsrvCompatibilitySelection + if ($null -eq $compatibilitySelection) { + exit 0 + } + if ($InstallMsrvCompatibilityIfNeeded) { + if ($compatibilitySelection.Mapped) { + & cargo "+$($compatibilitySelection.Value)" --version *> $null + if ($LASTEXITCODE -ne 0) { + throw "anvil: mapped MSRV toolchain '$($compatibilitySelection.Value)' is unavailable; provision ANVIL_MSRV_TOOLCHAIN or unset it to let Anvil install the declared public MSRV" + } + } else { + Install-Toolchain $compatibilitySelection 'MSRV compatibility' + } + } else { + $compatibilitySelection.Value + } + exit 0 +} + $selection = if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { [pscustomobject]@{ Value = $env:RUSTUP_TOOLCHAIN; Source = 'environment'; Installable = $false } } elseif ($null -ne $fileSelection) { @@ -1327,23 +1403,7 @@ $selection = if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { } if ($InstallIfMissing) { - if (-not $selection.Installable) { - exit 0 - } - - $installed = rustup toolchain list - if ($LASTEXITCODE -ne 0) { - throw 'anvil: rustup toolchain list failed' - } - if ($installed -match "(?m)^$([regex]::Escape($selection.Value))(?:-|$)") { - exit 0 - } - - Write-Host "anvil: installing stable toolchain '$($selection.Value)'" - rustup toolchain install $selection.Value --profile minimal - if ($LASTEXITCODE -ne 0) { - throw "anvil: failed to install stable toolchain '$($selection.Value)'" - } + Install-Toolchain $selection 'stable' exit 0 } @@ -1412,6 +1472,8 @@ description: | (modified ∪ rev-deps). include_required - same shape, for crates in the required set (affected ∪ workspace-internal transitive deps). + msrv_compatibility_required - "true" when a selecting toolchain file + differs from the root MSRV. Recipes in justfiles/anvil/checks/ interpret each variable per their tier: modified-tier recipes (fmt, license-headers, spellcheck, ...) short- @@ -1432,6 +1494,9 @@ outputs: include_required: description: Pre-formatted --package args for the required tier. value: ${{ steps.compute.outputs.include_required }} + msrv_compatibility_required: + description: Whether a selecting toolchain file differs from the root MSRV. + value: ${{ steps.msrv_compatibility.outputs.required }} runs: using: composite steps: @@ -1446,6 +1511,13 @@ runs: - name: Install cargo-delta shell: bash run: just anvil-tool-cargo-delta-install binstall + - id: msrv_compatibility + name: Detect MSRV compatibility run + shell: pwsh -NoProfile -Command ". '{0}'" + run: | + $toolchain = (& '.anvil/resolve-stable-toolchain.ps1' -MsrvCompatibilityToolchain | Out-String).Trim() + $required = -not [string]::IsNullOrWhiteSpace($toolchain) + "required=$($required.ToString().ToLowerInvariant())" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append - id: compute name: Compute impact shell: bash @@ -1875,9 +1947,9 @@ runs: # the save -- leaving the cache empty forever. The restore-keys # fall back across jobs so the install work is still shared # across sibling legs on subsequent runs. - key: anvil-v1-${{ runner.os }}-${{ runner.arch }}-rust${{ steps.rustc-version.outputs.version }}-${{ hashFiles('Cargo.lock', '.cargo/config.toml', 'rust-toolchain.toml', 'justfiles/anvil/versions.just') }}-${{ github.job }} + key: anvil-v1-${{ runner.os }}-${{ runner.arch }}-rust${{ steps.rustc-version.outputs.version }}-${{ hashFiles('Cargo.toml', 'Cargo.lock', '.cargo/config.toml', 'rust-toolchain', 'rust-toolchain.toml', 'justfiles/anvil/versions.just') }}-${{ github.job }} restore-keys: | - anvil-v1-${{ runner.os }}-${{ runner.arch }}-rust${{ steps.rustc-version.outputs.version }}-${{ hashFiles('Cargo.lock', '.cargo/config.toml', 'rust-toolchain.toml', 'justfiles/anvil/versions.just') }}- + anvil-v1-${{ runner.os }}-${{ runner.arch }}-rust${{ steps.rustc-version.outputs.version }}-${{ hashFiles('Cargo.toml', 'Cargo.lock', '.cargo/config.toml', 'rust-toolchain', 'rust-toolchain.toml', 'justfiles/anvil/versions.just') }}- anvil-v1-${{ runner.os }}-${{ runner.arch }}-rust${{ steps.rustc-version.outputs.version }}- anvil-v1-${{ runner.os }}-${{ runner.arch }}- # `.crates.toml` and `.crates2.json` track which cargo-installed @@ -2067,6 +2139,7 @@ jobs: include_modified: ${{ steps.delta.outputs.include_modified }} include_affected: ${{ steps.delta.outputs.include_affected }} include_required: ${{ steps.delta.outputs.include_required }} + msrv_compatibility_required: ${{ steps.delta.outputs.msrv_compatibility_required }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -2208,6 +2281,22 @@ jobs: token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: false + pr-msrv: + name: "Check Group: MSRV Compatibility (linux)" + needs: impact-linux + if: needs.impact-linux.outputs.msrv_compatibility_required == 'true' + runs-on: ${{ inputs.linux_runner }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + lfs: true + - uses: ./.github/actions/anvil-run-group + with: + group: pr-msrv + publish_commit_statuses: ${{ inputs.publish_commit_statuses }} + free-disk-space: true + include_affected: ${{ needs.impact-linux.outputs.include_affected }} + pr-runtime-analysis: name: "Check Group: Runtime Analysis (${{ matrix.os }})" # Stricter-runtime correctness: miri + careful. @@ -3981,6 +4070,56 @@ anvil-miri-setup installer="install": anvil-component-nightly-miri-install anvil [group("anvil-setup")] anvil-miri-validate-prereqs: anvil-component-nightly-miri-validate-prereqs anvil-component-nightly-rust-src-validate-prereqs +=== justfiles/anvil/checks/msrv-test.just === +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# GENERATED BY cargo-anvil. DO NOT EDIT DIRECTLY. +# Update cargo-anvil and regenerate; repository-specific edits stop automatic updates. +# Update behaviour: https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/updates.md + +# See https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/checks.md + +# Run affected-package tests under the declared MSRV when a selecting root +# toolchain file chooses a different compiler. +[script("pwsh", "-NoProfile")] +anvil-msrv-test: anvil-msrv-test-validate-prereqs + $ErrorActionPreference = 'Stop' + if ($env:ANVIL_INCLUDE_AFFECTED -eq '--skip') { exit 0 } + $toolchain = (& '.anvil/resolve-stable-toolchain.ps1' -MsrvCompatibilityToolchain | Out-String).Trim() + if ([string]::IsNullOrWhiteSpace($toolchain)) { + Write-Host 'anvil-msrv-test: selected toolchain matches the MSRV; skipping' + exit 0 + } + $pkg = @(if ($env:ANVIL_INCLUDE_AFFECTED) { -split $env:ANVIL_INCLUDE_AFFECTED } else { '--workspace' }) + & cargo "+$toolchain" test @pkg --all-targets --all-features --locked + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & cargo "+$toolchain" test @pkg --all-targets --locked + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + +# Install the MSRV compatibility toolchain only when this check is required. +[group("anvil-setup")] +[script("pwsh", "-NoProfile")] +anvil-msrv-test-setup installer="install": + & '.anvil/resolve-stable-toolchain.ps1' -InstallMsrvCompatibilityIfNeeded + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + +# Validate the conditional MSRV compatibility prerequisite. +[group("anvil-setup")] +[script("pwsh", "-NoProfile")] +anvil-msrv-test-validate-prereqs: + $toolchain = (& '.anvil/resolve-stable-toolchain.ps1' -MsrvCompatibilityToolchain | Out-String).Trim() + if ([string]::IsNullOrWhiteSpace($toolchain)) { exit 0 } + & cargo "+$toolchain" --version + if ($LASTEXITCODE -ne 0) { + $hint = if ($env:ANVIL_MSRV_TOOLCHAIN) { + 'provision ANVIL_MSRV_TOOLCHAIN or unset it to let Anvil install the declared public MSRV' + } else { + 'run: just anvil-msrv-test-setup' + } + Write-Error "anvil: MSRV compatibility toolchain '$toolchain' is unavailable; $hint" + exit $LASTEXITCODE + } + === justfiles/anvil/checks/mutants-diff.just === # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. @@ -4734,6 +4873,27 @@ anvil-pr-fast-validate-prereqs: \ anvil-semver-check-validate-prereqs \ anvil-external-types-validate-prereqs +=== justfiles/anvil/groups/pr-msrv.just === +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# GENERATED BY cargo-anvil. DO NOT EDIT DIRECTLY. +# Update cargo-anvil and regenerate; repository-specific edits stop automatic updates. +# Update behaviour: https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/updates.md + +# See https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/checks.md + +# Run pull request compatibility tests under the declared MSRV. +[group("anvil")] +anvil-pr-msrv: anvil-pr-msrv-validate-prereqs anvil-msrv-test + +# Install prerequisites for the `anvil-pr-msrv` recipe. +[group("anvil-setup")] +anvil-pr-msrv-setup installer="install": (anvil-msrv-test-setup installer) + +# Validate prerequisites for the `anvil-pr-msrv` recipe. +[group("anvil-setup")] +anvil-pr-msrv-validate-prereqs: anvil-msrv-test-validate-prereqs + === justfiles/anvil/groups/pr-mutants.just === # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. @@ -4799,7 +4959,7 @@ anvil-pr-runtime-analysis-validate-prereqs: \ # pr-slow is the single PR-tier group for everything that takes more # than ~30s per crate (tests, stricter runtimes, mutation testing). -# It's internally split into three sub-recipes so individual concerns +# It's internally split into four sub-recipes so individual concerns # can be invoked locally without dragging the others along: # # slow1: tests + coverage (replaces the former pr-test group) @@ -4817,12 +4977,13 @@ anvil-pr-runtime-analysis-validate-prereqs: \ # Run the slow pull request checks. [group("anvil")] -anvil-pr-slow: anvil-pr-slow-validate-prereqs anvil-pr-test anvil-pr-runtime-analysis anvil-pr-mutants +anvil-pr-slow: anvil-pr-slow-validate-prereqs anvil-pr-test anvil-pr-msrv anvil-pr-runtime-analysis anvil-pr-mutants # Install prerequisites for the `anvil-pr-slow` recipe. [group("anvil-setup")] anvil-pr-slow-setup installer="install": \ (anvil-pr-test-setup installer) \ + (anvil-pr-msrv-setup installer) \ (anvil-pr-runtime-analysis-setup installer) \ (anvil-pr-mutants-setup installer) @@ -4830,6 +4991,7 @@ anvil-pr-slow-setup installer="install": \ [group("anvil-setup")] anvil-pr-slow-validate-prereqs: \ anvil-pr-test-validate-prereqs \ + anvil-pr-msrv-validate-prereqs \ anvil-pr-runtime-analysis-validate-prereqs \ anvil-pr-mutants-validate-prereqs @@ -5326,6 +5488,7 @@ import 'checks/miri.just' import 'checks/miri-race-coverage.just' import 'checks/miri-strict-provenance.just' import 'checks/miri-tree-borrows.just' +import 'checks/msrv-test.just' import 'checks/mutants-diff.just' import 'checks/mutants-full.just' import 'checks/pr-title.just' @@ -5335,6 +5498,7 @@ import 'checks/spellcheck.just' import 'checks/udeps.just' import 'container.just' import 'groups/pr-fast.just' +import 'groups/pr-msrv.just' import 'groups/pr-slow.just' import 'groups/pr-test.just' import 'groups/pr-runtime-analysis.just' diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap index 807f625c3..a92f2adb3 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap @@ -1204,7 +1204,9 @@ docker "${run_args[@]}" "$image" just "$@" param( [switch] $ValidateWorkspaceMsrv, [switch] $InstallIfMissing, - [switch] $ForEnvironment + [switch] $ForEnvironment, + [switch] $MsrvCompatibilityToolchain, + [switch] $InstallMsrvCompatibilityIfNeeded ) $ErrorActionPreference = 'Stop' @@ -1221,7 +1223,13 @@ function Get-SelectingToolchainFile { $content = Get-Content -LiteralPath $path -Raw if ($name -eq 'rust-toolchain' -and $content.TrimStart() -notmatch '^\[') { - return [pscustomobject]@{ Path = $path; Value = $content.Trim(); Source = 'file'; Installable = $false } + return [pscustomobject]@{ + Path = $path + Value = $content.Trim() + Selector = 'channel' + Source = 'file' + Installable = $false + } } $inToolchain = $false @@ -1242,6 +1250,7 @@ function Get-SelectingToolchainFile { return [pscustomobject]@{ Path = $path Value = $value + Selector = $Matches[1] Source = 'file' Installable = $false } @@ -1252,7 +1261,7 @@ function Get-SelectingToolchainFile { return $null } -function Get-RootMsrv { +function Get-RootMsrv([switch] $AllowMissing) { $manifestPath = Join-Path $repoRoot 'Cargo.toml' if (-not (Test-Path -LiteralPath $manifestPath -PathType Leaf)) { throw "anvil: Cargo.toml not found at repository root '$repoRoot'" @@ -1278,6 +1287,9 @@ function Get-RootMsrv { return $values['package'] } + if ($AllowMissing) { + return $null + } throw 'anvil: no selecting rust-toolchain(.toml) and no root [workspace.package] or [package] rust-version; declare an MSRV or select a toolchain explicitly' } @@ -1305,6 +1317,50 @@ function Assert-UniformWorkspaceMsrv { } } +function Get-MsrvCompatibilitySelection { + if ($null -eq $fileSelection) { + return $null + } + + $msrv = Get-RootMsrv -AllowMissing + if ([string]::IsNullOrWhiteSpace($msrv)) { + return $null + } + $selectedMatch = [regex]::Match($fileSelection.Value, '^(\d+)\.(\d+)(?:\.\d+)?(?:-.+)?$') + $msrvMatch = [regex]::Match($msrv, '^(\d+)\.(\d+)(?:\.\d+)?(?:-.+)?$') + if ($fileSelection.Selector -eq 'channel' -and $selectedMatch.Success -and $msrvMatch.Success) { + if ($selectedMatch.Groups[1].Value -eq $msrvMatch.Groups[1].Value -and + $selectedMatch.Groups[2].Value -eq $msrvMatch.Groups[2].Value) { + return $null + } + } + + if (-not [string]::IsNullOrWhiteSpace($env:ANVIL_MSRV_TOOLCHAIN)) { + return [pscustomobject]@{ Value = $env:ANVIL_MSRV_TOOLCHAIN; Installable = $false; Mapped = $true } + } + return [pscustomobject]@{ Value = $msrv; Installable = $true; Mapped = $false } +} + +function Install-Toolchain([object] $toolchain, [string] $label) { + if (-not $toolchain.Installable) { + return + } + + $installed = rustup toolchain list + if ($LASTEXITCODE -ne 0) { + throw 'anvil: rustup toolchain list failed' + } + if ($installed -match "(?m)^$([regex]::Escape($toolchain.Value))(?:-|$)") { + return + } + + Write-Host "anvil: installing $label toolchain '$($toolchain.Value)'" + rustup toolchain install $toolchain.Value --profile minimal + if ($LASTEXITCODE -ne 0) { + throw "anvil: failed to install $label toolchain '$($toolchain.Value)'" + } +} + $fileSelection = Get-SelectingToolchainFile if ($ValidateWorkspaceMsrv) { $skipWorkspaceMsrvValidation = if ([string]::IsNullOrWhiteSpace($env:ANVIL_STABLE_TOOLCHAIN_SOURCE)) { @@ -1318,6 +1374,26 @@ if ($ValidateWorkspaceMsrv) { exit 0 } +if ($MsrvCompatibilityToolchain -or $InstallMsrvCompatibilityIfNeeded) { + $compatibilitySelection = Get-MsrvCompatibilitySelection + if ($null -eq $compatibilitySelection) { + exit 0 + } + if ($InstallMsrvCompatibilityIfNeeded) { + if ($compatibilitySelection.Mapped) { + & cargo "+$($compatibilitySelection.Value)" --version *> $null + if ($LASTEXITCODE -ne 0) { + throw "anvil: mapped MSRV toolchain '$($compatibilitySelection.Value)' is unavailable; provision ANVIL_MSRV_TOOLCHAIN or unset it to let Anvil install the declared public MSRV" + } + } else { + Install-Toolchain $compatibilitySelection 'MSRV compatibility' + } + } else { + $compatibilitySelection.Value + } + exit 0 +} + $selection = if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { [pscustomobject]@{ Value = $env:RUSTUP_TOOLCHAIN; Source = 'environment'; Installable = $false } } elseif ($null -ne $fileSelection) { @@ -1327,23 +1403,7 @@ $selection = if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { } if ($InstallIfMissing) { - if (-not $selection.Installable) { - exit 0 - } - - $installed = rustup toolchain list - if ($LASTEXITCODE -ne 0) { - throw 'anvil: rustup toolchain list failed' - } - if ($installed -match "(?m)^$([regex]::Escape($selection.Value))(?:-|$)") { - exit 0 - } - - Write-Host "anvil: installing stable toolchain '$($selection.Value)'" - rustup toolchain install $selection.Value --profile minimal - if ($LASTEXITCODE -ne 0) { - throw "anvil: failed to install stable toolchain '$($selection.Value)'" - } + Install-Toolchain $selection 'stable' exit 0 } @@ -2817,6 +2877,56 @@ anvil-miri-setup installer="install": anvil-component-nightly-miri-install anvil [group("anvil-setup")] anvil-miri-validate-prereqs: anvil-component-nightly-miri-validate-prereqs anvil-component-nightly-rust-src-validate-prereqs +=== justfiles/anvil/checks/msrv-test.just === +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# GENERATED BY cargo-anvil. DO NOT EDIT DIRECTLY. +# Update cargo-anvil and regenerate; repository-specific edits stop automatic updates. +# Update behaviour: https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/updates.md + +# See https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/checks.md + +# Run affected-package tests under the declared MSRV when a selecting root +# toolchain file chooses a different compiler. +[script("pwsh", "-NoProfile")] +anvil-msrv-test: anvil-msrv-test-validate-prereqs + $ErrorActionPreference = 'Stop' + if ($env:ANVIL_INCLUDE_AFFECTED -eq '--skip') { exit 0 } + $toolchain = (& '.anvil/resolve-stable-toolchain.ps1' -MsrvCompatibilityToolchain | Out-String).Trim() + if ([string]::IsNullOrWhiteSpace($toolchain)) { + Write-Host 'anvil-msrv-test: selected toolchain matches the MSRV; skipping' + exit 0 + } + $pkg = @(if ($env:ANVIL_INCLUDE_AFFECTED) { -split $env:ANVIL_INCLUDE_AFFECTED } else { '--workspace' }) + & cargo "+$toolchain" test @pkg --all-targets --all-features --locked + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & cargo "+$toolchain" test @pkg --all-targets --locked + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + +# Install the MSRV compatibility toolchain only when this check is required. +[group("anvil-setup")] +[script("pwsh", "-NoProfile")] +anvil-msrv-test-setup installer="install": + & '.anvil/resolve-stable-toolchain.ps1' -InstallMsrvCompatibilityIfNeeded + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + +# Validate the conditional MSRV compatibility prerequisite. +[group("anvil-setup")] +[script("pwsh", "-NoProfile")] +anvil-msrv-test-validate-prereqs: + $toolchain = (& '.anvil/resolve-stable-toolchain.ps1' -MsrvCompatibilityToolchain | Out-String).Trim() + if ([string]::IsNullOrWhiteSpace($toolchain)) { exit 0 } + & cargo "+$toolchain" --version + if ($LASTEXITCODE -ne 0) { + $hint = if ($env:ANVIL_MSRV_TOOLCHAIN) { + 'provision ANVIL_MSRV_TOOLCHAIN or unset it to let Anvil install the declared public MSRV' + } else { + 'run: just anvil-msrv-test-setup' + } + Write-Error "anvil: MSRV compatibility toolchain '$toolchain' is unavailable; $hint" + exit $LASTEXITCODE + } + === justfiles/anvil/checks/mutants-diff.just === # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. @@ -3570,6 +3680,27 @@ anvil-pr-fast-validate-prereqs: \ anvil-semver-check-validate-prereqs \ anvil-external-types-validate-prereqs +=== justfiles/anvil/groups/pr-msrv.just === +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# GENERATED BY cargo-anvil. DO NOT EDIT DIRECTLY. +# Update cargo-anvil and regenerate; repository-specific edits stop automatic updates. +# Update behaviour: https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/updates.md + +# See https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/checks.md + +# Run pull request compatibility tests under the declared MSRV. +[group("anvil")] +anvil-pr-msrv: anvil-pr-msrv-validate-prereqs anvil-msrv-test + +# Install prerequisites for the `anvil-pr-msrv` recipe. +[group("anvil-setup")] +anvil-pr-msrv-setup installer="install": (anvil-msrv-test-setup installer) + +# Validate prerequisites for the `anvil-pr-msrv` recipe. +[group("anvil-setup")] +anvil-pr-msrv-validate-prereqs: anvil-msrv-test-validate-prereqs + === justfiles/anvil/groups/pr-mutants.just === # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. @@ -3635,7 +3766,7 @@ anvil-pr-runtime-analysis-validate-prereqs: \ # pr-slow is the single PR-tier group for everything that takes more # than ~30s per crate (tests, stricter runtimes, mutation testing). -# It's internally split into three sub-recipes so individual concerns +# It's internally split into four sub-recipes so individual concerns # can be invoked locally without dragging the others along: # # slow1: tests + coverage (replaces the former pr-test group) @@ -3653,12 +3784,13 @@ anvil-pr-runtime-analysis-validate-prereqs: \ # Run the slow pull request checks. [group("anvil")] -anvil-pr-slow: anvil-pr-slow-validate-prereqs anvil-pr-test anvil-pr-runtime-analysis anvil-pr-mutants +anvil-pr-slow: anvil-pr-slow-validate-prereqs anvil-pr-test anvil-pr-msrv anvil-pr-runtime-analysis anvil-pr-mutants # Install prerequisites for the `anvil-pr-slow` recipe. [group("anvil-setup")] anvil-pr-slow-setup installer="install": \ (anvil-pr-test-setup installer) \ + (anvil-pr-msrv-setup installer) \ (anvil-pr-runtime-analysis-setup installer) \ (anvil-pr-mutants-setup installer) @@ -3666,6 +3798,7 @@ anvil-pr-slow-setup installer="install": \ [group("anvil-setup")] anvil-pr-slow-validate-prereqs: \ anvil-pr-test-validate-prereqs \ + anvil-pr-msrv-validate-prereqs \ anvil-pr-runtime-analysis-validate-prereqs \ anvil-pr-mutants-validate-prereqs @@ -4162,6 +4295,7 @@ import 'checks/miri.just' import 'checks/miri-race-coverage.just' import 'checks/miri-strict-provenance.just' import 'checks/miri-tree-borrows.just' +import 'checks/msrv-test.just' import 'checks/mutants-diff.just' import 'checks/mutants-full.just' import 'checks/pr-title.just' @@ -4171,6 +4305,7 @@ import 'checks/spellcheck.just' import 'checks/udeps.just' import 'container.just' import 'groups/pr-fast.just' +import 'groups/pr-msrv.just' import 'groups/pr-slow.just' import 'groups/pr-test.just' import 'groups/pr-runtime-analysis.just' diff --git a/justfiles/anvil/checks/msrv-test.just b/justfiles/anvil/checks/msrv-test.just new file mode 100644 index 000000000..0e4889236 --- /dev/null +++ b/justfiles/anvil/checks/msrv-test.just @@ -0,0 +1,48 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# GENERATED BY cargo-anvil. DO NOT EDIT DIRECTLY. +# Update cargo-anvil and regenerate; repository-specific edits stop automatic updates. +# Update behaviour: https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/updates.md + +# See https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/checks.md + +# Run affected-package tests under the declared MSRV when a selecting root +# toolchain file chooses a different compiler. +[script("pwsh", "-NoProfile")] +anvil-msrv-test: anvil-msrv-test-validate-prereqs + $ErrorActionPreference = 'Stop' + if ($env:ANVIL_INCLUDE_AFFECTED -eq '--skip') { exit 0 } + $toolchain = (& '.anvil/resolve-stable-toolchain.ps1' -MsrvCompatibilityToolchain | Out-String).Trim() + if ([string]::IsNullOrWhiteSpace($toolchain)) { + Write-Host 'anvil-msrv-test: selected toolchain matches the MSRV; skipping' + exit 0 + } + $pkg = @(if ($env:ANVIL_INCLUDE_AFFECTED) { -split $env:ANVIL_INCLUDE_AFFECTED } else { '--workspace' }) + & cargo "+$toolchain" test @pkg --all-targets --all-features --locked + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & cargo "+$toolchain" test @pkg --all-targets --locked + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + +# Install the MSRV compatibility toolchain only when this check is required. +[group("anvil-setup")] +[script("pwsh", "-NoProfile")] +anvil-msrv-test-setup installer="install": + & '.anvil/resolve-stable-toolchain.ps1' -InstallMsrvCompatibilityIfNeeded + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + +# Validate the conditional MSRV compatibility prerequisite. +[group("anvil-setup")] +[script("pwsh", "-NoProfile")] +anvil-msrv-test-validate-prereqs: + $toolchain = (& '.anvil/resolve-stable-toolchain.ps1' -MsrvCompatibilityToolchain | Out-String).Trim() + if ([string]::IsNullOrWhiteSpace($toolchain)) { exit 0 } + & cargo "+$toolchain" --version + if ($LASTEXITCODE -ne 0) { + $hint = if ($env:ANVIL_MSRV_TOOLCHAIN) { + 'provision ANVIL_MSRV_TOOLCHAIN or unset it to let Anvil install the declared public MSRV' + } else { + 'run: just anvil-msrv-test-setup' + } + Write-Error "anvil: MSRV compatibility toolchain '$toolchain' is unavailable; $hint" + exit $LASTEXITCODE + } diff --git a/justfiles/anvil/groups/pr-msrv.just b/justfiles/anvil/groups/pr-msrv.just new file mode 100644 index 000000000..1c25aa3f9 --- /dev/null +++ b/justfiles/anvil/groups/pr-msrv.just @@ -0,0 +1,19 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# GENERATED BY cargo-anvil. DO NOT EDIT DIRECTLY. +# Update cargo-anvil and regenerate; repository-specific edits stop automatic updates. +# Update behaviour: https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/updates.md + +# See https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/checks.md + +# Run pull request compatibility tests under the declared MSRV. +[group("anvil")] +anvil-pr-msrv: anvil-pr-msrv-validate-prereqs anvil-msrv-test + +# Install prerequisites for the `anvil-pr-msrv` recipe. +[group("anvil-setup")] +anvil-pr-msrv-setup installer="install": (anvil-msrv-test-setup installer) + +# Validate prerequisites for the `anvil-pr-msrv` recipe. +[group("anvil-setup")] +anvil-pr-msrv-validate-prereqs: anvil-msrv-test-validate-prereqs diff --git a/justfiles/anvil/groups/pr-slow.just b/justfiles/anvil/groups/pr-slow.just index 75ad63c1d..f063a49ec 100644 --- a/justfiles/anvil/groups/pr-slow.just +++ b/justfiles/anvil/groups/pr-slow.just @@ -8,7 +8,7 @@ # pr-slow is the single PR-tier group for everything that takes more # than ~30s per crate (tests, stricter runtimes, mutation testing). -# It's internally split into three sub-recipes so individual concerns +# It's internally split into four sub-recipes so individual concerns # can be invoked locally without dragging the others along: # # slow1: tests + coverage (replaces the former pr-test group) @@ -26,12 +26,13 @@ # Run the slow pull request checks. [group("anvil")] -anvil-pr-slow: anvil-pr-slow-validate-prereqs anvil-pr-test anvil-pr-runtime-analysis anvil-pr-mutants +anvil-pr-slow: anvil-pr-slow-validate-prereqs anvil-pr-test anvil-pr-msrv anvil-pr-runtime-analysis anvil-pr-mutants # Install prerequisites for the `anvil-pr-slow` recipe. [group("anvil-setup")] anvil-pr-slow-setup installer="install": \ (anvil-pr-test-setup installer) \ + (anvil-pr-msrv-setup installer) \ (anvil-pr-runtime-analysis-setup installer) \ (anvil-pr-mutants-setup installer) @@ -39,5 +40,6 @@ anvil-pr-slow-setup installer="install": \ [group("anvil-setup")] anvil-pr-slow-validate-prereqs: \ anvil-pr-test-validate-prereqs \ + anvil-pr-msrv-validate-prereqs \ anvil-pr-runtime-analysis-validate-prereqs \ anvil-pr-mutants-validate-prereqs diff --git a/justfiles/anvil/mod.just b/justfiles/anvil/mod.just index 710bead68..456c9e12f 100644 --- a/justfiles/anvil/mod.just +++ b/justfiles/anvil/mod.just @@ -59,6 +59,7 @@ import 'checks/miri.just' import 'checks/miri-race-coverage.just' import 'checks/miri-strict-provenance.just' import 'checks/miri-tree-borrows.just' +import 'checks/msrv-test.just' import 'checks/mutants-diff.just' import 'checks/mutants-full.just' import 'checks/pr-title.just' @@ -68,6 +69,7 @@ import 'checks/spellcheck.just' import 'checks/udeps.just' import 'container.just' import 'groups/pr-fast.just' +import 'groups/pr-msrv.just' import 'groups/pr-slow.just' import 'groups/pr-test.just' import 'groups/pr-runtime-analysis.just' From 2c332531afcf1c681fdc604a1fbf34906f171ef4 Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Thu, 27 Aug 2026 18:56:43 +0200 Subject: [PATCH 03/32] feat(cargo-anvil): run MSRV tests across platforms Run affected-package tests under every declared root MSRV across the same GitHub and ADO platform matrices as ordinary PR tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .anvil.lock | 10 +-- .anvil/resolve-stable-toolchain.ps1 | 36 +++----- .github/actions/anvil-impact/action.yml | 15 ++-- .github/workflows/anvil-pr-impl.yml | 19 ++-- crates/cargo-anvil/README.md | 4 +- crates/cargo-anvil/docs/design/README.md | 3 +- crates/cargo-anvil/docs/design/ado.md | 16 ++-- crates/cargo-anvil/docs/design/checks.md | 24 ++--- crates/cargo-anvil/docs/design/github.md | 32 ++++--- crates/cargo-anvil/docs/design/local.md | 11 ++- crates/cargo-anvil/src/anvil/artifacts/ado.rs | 6 +- .../cargo-anvil/src/anvil/artifacts/github.rs | 9 +- .../src/anvil/artifacts/justfile.rs | 33 +++---- crates/cargo-anvil/src/lib.rs | 2 +- .../cargo-anvil/templates/ado/pr-stages.yml | 15 +++- .../templates/ado/steps/impact.yml | 11 ++- .../anvil/resolve-stable-toolchain.ps1 | 36 +++----- .../templates/github/impact-action.yml | 15 ++-- .../templates/github/pr-impl-workflow.yml | 19 ++-- .../justfiles/anvil/checks/msrv-test.just | 17 ++-- .../snapshots/snapshots__ado_backend.snap | 79 ++++++++--------- .../snapshots/snapshots__github_backend.snap | 87 +++++++++---------- .../snapshots/snapshots__local_only.snap | 53 +++++------ justfiles/anvil/checks/msrv-test.just | 17 ++-- 24 files changed, 269 insertions(+), 300 deletions(-) diff --git a/.anvil.lock b/.anvil.lock index c1a878492..c172e2d45 100644 --- a/.anvil.lock +++ b/.anvil.lock @@ -1,7 +1,7 @@ version = 1 tool = "anvil" tool_version = "0.5.0" -catalog_checksum = "sha256:d7d8adc0600a6c6b38c70dfcd3237d76a46fe036f25f42220a30fa5b56159b1a" +catalog_checksum = "sha256:864e2493db93d367febdd04f3c268aab71802f920c3d903a319daefb1f2f5a8f" [[file]] path = ".anvil/container/Containerfile" @@ -37,11 +37,11 @@ checksum = "sha256:bf4a21ac097d34b6b4b3a4a507455dda96052980228b6145ce6d33c8e4674 [[file]] path = ".anvil/resolve-stable-toolchain.ps1" -checksum = "sha256:1e2c8efa192d3889d8912fcc8cd90f5623bc736a9f923ac6a9bac7713f7c92d1" +checksum = "sha256:59f7a3c56be794854553fe5c7dab854b04a5c20b156e52c9d38b3a10386d17a0" [[file]] path = ".github/actions/anvil-impact/action.yml" -checksum = "sha256:d905538e5554c2a45e16bfe69ba687e9da6d94fc4f1cfefdfdd9857c9718b4c7" +checksum = "sha256:416486dcf8326782b8b92f97c7ac9e81ea0d102a7f89c6bcd14269625e99c18a" [[file]] path = ".github/actions/anvil-report-status/action.yml" @@ -61,7 +61,7 @@ checksum = "sha256:ea44d5e1a2cb1471cf2cef05eceab846d8afa45cf691be800f21fec1218da [[file]] path = ".github/workflows/anvil-pr-impl.yml" -checksum = "sha256:a7f5aaa9629e4a30a5ef4f9fae8f5736f6ee48ead7273caf5cd1467f30072bfe" +checksum = "sha256:a4978b2d97727997d343530290496635cb56c1a6d346f6fed1f51b20bcb79ded" [[file]] path = ".github/workflows/anvil-pr.yml" @@ -169,7 +169,7 @@ checksum = "sha256:14499aa7e29bc631b1905af5bb9f7d2da2c94646cf6d75f5746f930fa8044 [[file]] path = "justfiles/anvil/checks/msrv-test.just" -checksum = "sha256:7fd4a2d9ed342b16831d78b0cefc7bca0acc37c9e936a63ccb39fafa8002f6eb" +checksum = "sha256:32ec0ec2317678b73fe3e4e96eea1e82d9a8c6e7f19cac43e981586a87fdadcc" [[file]] path = "justfiles/anvil/checks/mutants-diff.just" diff --git a/.anvil/resolve-stable-toolchain.ps1 b/.anvil/resolve-stable-toolchain.ps1 index df8cbfd35..736f8d084 100644 --- a/.anvil/resolve-stable-toolchain.ps1 +++ b/.anvil/resolve-stable-toolchain.ps1 @@ -6,8 +6,8 @@ param( [switch] $ValidateWorkspaceMsrv, [switch] $InstallIfMissing, [switch] $ForEnvironment, - [switch] $MsrvCompatibilityToolchain, - [switch] $InstallMsrvCompatibilityIfNeeded + [switch] $MsrvToolchain, + [switch] $InstallMsrvIfNeeded ) $ErrorActionPreference = 'Stop' @@ -118,23 +118,11 @@ function Assert-UniformWorkspaceMsrv { } } -function Get-MsrvCompatibilitySelection { - if ($null -eq $fileSelection) { - return $null - } - +function Get-MsrvSelection { $msrv = Get-RootMsrv -AllowMissing if ([string]::IsNullOrWhiteSpace($msrv)) { return $null } - $selectedMatch = [regex]::Match($fileSelection.Value, '^(\d+)\.(\d+)(?:\.\d+)?(?:-.+)?$') - $msrvMatch = [regex]::Match($msrv, '^(\d+)\.(\d+)(?:\.\d+)?(?:-.+)?$') - if ($fileSelection.Selector -eq 'channel' -and $selectedMatch.Success -and $msrvMatch.Success) { - if ($selectedMatch.Groups[1].Value -eq $msrvMatch.Groups[1].Value -and - $selectedMatch.Groups[2].Value -eq $msrvMatch.Groups[2].Value) { - return $null - } - } if (-not [string]::IsNullOrWhiteSpace($env:ANVIL_MSRV_TOOLCHAIN)) { return [pscustomobject]@{ Value = $env:ANVIL_MSRV_TOOLCHAIN; Installable = $false; Mapped = $true } @@ -175,22 +163,22 @@ if ($ValidateWorkspaceMsrv) { exit 0 } -if ($MsrvCompatibilityToolchain -or $InstallMsrvCompatibilityIfNeeded) { - $compatibilitySelection = Get-MsrvCompatibilitySelection - if ($null -eq $compatibilitySelection) { +if ($MsrvToolchain -or $InstallMsrvIfNeeded) { + $msrvSelection = Get-MsrvSelection + if ($null -eq $msrvSelection) { exit 0 } - if ($InstallMsrvCompatibilityIfNeeded) { - if ($compatibilitySelection.Mapped) { - & cargo "+$($compatibilitySelection.Value)" --version *> $null + if ($InstallMsrvIfNeeded) { + if ($msrvSelection.Mapped) { + & cargo "+$($msrvSelection.Value)" --version *> $null if ($LASTEXITCODE -ne 0) { - throw "anvil: mapped MSRV toolchain '$($compatibilitySelection.Value)' is unavailable; provision ANVIL_MSRV_TOOLCHAIN or unset it to let Anvil install the declared public MSRV" + throw "anvil: mapped MSRV toolchain '$($msrvSelection.Value)' is unavailable; provision ANVIL_MSRV_TOOLCHAIN or unset it to let Anvil install the declared public MSRV" } } else { - Install-Toolchain $compatibilitySelection 'MSRV compatibility' + Install-Toolchain $msrvSelection 'MSRV' } } else { - $compatibilitySelection.Value + $msrvSelection.Value } exit 0 } diff --git a/.github/actions/anvil-impact/action.yml b/.github/actions/anvil-impact/action.yml index 4c1f8b14a..37610ebb7 100644 --- a/.github/actions/anvil-impact/action.yml +++ b/.github/actions/anvil-impact/action.yml @@ -19,8 +19,7 @@ description: | (modified ∪ rev-deps). include_required - same shape, for crates in the required set (affected ∪ workspace-internal transitive deps). - msrv_compatibility_required - "true" when a selecting toolchain file - differs from the root MSRV. + msrv_test_required - "true" when the root manifest declares an MSRV. Recipes in justfiles/anvil/checks/ interpret each variable per their tier: modified-tier recipes (fmt, license-headers, spellcheck, ...) short- @@ -41,9 +40,9 @@ outputs: include_required: description: Pre-formatted --package args for the required tier. value: ${{ steps.compute.outputs.include_required }} - msrv_compatibility_required: - description: Whether a selecting toolchain file differs from the root MSRV. - value: ${{ steps.msrv_compatibility.outputs.required }} + msrv_test_required: + description: Whether the root manifest declares an MSRV. + value: ${{ steps.msrv_test.outputs.required }} runs: using: composite steps: @@ -58,11 +57,11 @@ runs: - name: Install cargo-delta shell: bash run: just anvil-tool-cargo-delta-install binstall - - id: msrv_compatibility - name: Detect MSRV compatibility run + - id: msrv_test + name: Detect MSRV test run shell: pwsh -NoProfile -Command ". '{0}'" run: | - $toolchain = (& '.anvil/resolve-stable-toolchain.ps1' -MsrvCompatibilityToolchain | Out-String).Trim() + $toolchain = (& '.anvil/resolve-stable-toolchain.ps1' -MsrvToolchain | Out-String).Trim() $required = -not [string]::IsNullOrWhiteSpace($toolchain) "required=$($required.ToString().ToLowerInvariant())" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append - id: compute diff --git a/.github/workflows/anvil-pr-impl.yml b/.github/workflows/anvil-pr-impl.yml index 58ea25ad4..102256f6f 100644 --- a/.github/workflows/anvil-pr-impl.yml +++ b/.github/workflows/anvil-pr-impl.yml @@ -70,7 +70,7 @@ jobs: include_modified: ${{ steps.delta.outputs.include_modified }} include_affected: ${{ steps.delta.outputs.include_affected }} include_required: ${{ steps.delta.outputs.include_required }} - msrv_compatibility_required: ${{ steps.delta.outputs.msrv_compatibility_required }} + msrv_test_required: ${{ steps.delta.outputs.msrv_test_required }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -213,10 +213,17 @@ jobs: fail_ci_if_error: false pr-msrv: - name: "Check Group: MSRV Compatibility (linux)" - needs: impact-linux - if: needs.impact-linux.outputs.msrv_compatibility_required == 'true' - runs-on: ${{ inputs.linux_runner }} + name: "Check Group: MSRV Tests (${{ matrix.os }})" + needs: [impact-linux, impact-windows] + if: needs.impact-linux.outputs.msrv_test_required == 'true' + strategy: + fail-fast: false + matrix: + os: [linux, windows, linux-arm, windows-arm] + runs-on: ${{ matrix.os == 'linux' && inputs.linux_runner + || matrix.os == 'windows' && inputs.windows_runner + || matrix.os == 'linux-arm' && inputs.linux_arm_runner + || inputs.windows_arm_runner }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -226,7 +233,7 @@ jobs: group: pr-msrv publish_commit_statuses: ${{ inputs.publish_commit_statuses }} free-disk-space: true - include_affected: ${{ needs.impact-linux.outputs.include_affected }} + include_affected: ${{ startsWith(matrix.os, 'linux') && needs.impact-linux.outputs.include_affected || needs.impact-windows.outputs.include_affected }} pr-runtime-analysis: name: "Check Group: Runtime Analysis (${{ matrix.os }})" diff --git a/crates/cargo-anvil/README.md b/crates/cargo-anvil/README.md index 16129d6a5..bbd3a8906 100644 --- a/crates/cargo-anvil/README.md +++ b/crates/cargo-anvil/README.md @@ -253,7 +253,7 @@ sub-groups run sequentially within the one job per OS leg): pr-slowpr-testllvm-covdual feature-config; gated by cargo-coverage-gate doc-testruns both feature configs examplescompile-only - pr-msrvmsrv-testconditional dual feature-config compatibility test + pr-msrvmsrv-testdual feature-config tests under the declared MSRV pr-runtime-analysismirilibtest, not nextest carefulself-cleans on a toolchain bump loomopt-in targets only @@ -446,7 +446,7 @@ And `docs/verification.md` for the continuous-validation strategy. This crate was developed as part of The Oxidizer Project. Browse this crate's source code. - [__cargo_doc2readme_dependencies_info]: ggGmYW0CYXZlMC43LjJhdIQbFhzZ8rzWNNYbuRaDSGWynFgbH4PMdoT7GNcbVwNPtPjAhvFhYvRhcoQbywWDnJjuEVQbVf5umUifVEQbY6gqzBFsQbEbp97bK82npRRhZIGDa2NhcmdvLWFudmlsZTAuNS4wa2NhcmdvX2Fudmls + [__cargo_doc2readme_dependencies_info]: ggGmYW0CYXZlMC43LjJhdIQbFhzZ8rzWNNYbuRaDSGWynFgbH4PMdoT7GNcbVwNPtPjAhvFhYvRhcoQbhItkGau5TkAbZDeEp4HNV44bz2WM-DxXj1wbNcIWG-Q_tAZhZIGDa2NhcmdvLWFudmlsZTAuNS4wa2NhcmdvX2Fudmls [__link0]: https://crates.io/crates/cargo-delta [__link1]: https://crates.io/crates/cargo-spellcheck [__link2]: https://crates.io/crates/cargo-coverage-gate diff --git a/crates/cargo-anvil/docs/design/README.md b/crates/cargo-anvil/docs/design/README.md index 8d484bd98..f6741ae56 100644 --- a/crates/cargo-anvil/docs/design/README.md +++ b/crates/cargo-anvil/docs/design/README.md @@ -396,8 +396,7 @@ pipeline. | Group | OS / arch scope (default) | Rationale | |-------------------------------------------------------------|----------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------| | `pr-fast`, `scheduled-advisories` | All legs above | Contain compile-sensitive checks (clippy, doc-build, udeps, semver-check, external-types) that only see the host's compiled crate graph -- cfg-gated code is invisible to a single-leg run. Text/metadata checks running redundantly is cheaper than splitting jobs. | -| `pr-test`, `pr-runtime-analysis`, `scheduled-test` | All legs above | Where compile-time and runtime OS / arch bugs actually surface. The slow PR groups run as parallel cloud-workflow jobs for shorter wall-clock per leg. | -| `pr-msrv` | Linux x86_64 | Compatibility tests under the declared MSRV when a selecting toolchain file uses a different compiler. | +| `pr-test`, `pr-msrv`, `pr-runtime-analysis`, `scheduled-test` | All legs above | Where compile-time and runtime OS / arch bugs actually surface. `pr-msrv` runs the affected test suite under the minimum supported compiler. The slow PR groups run as parallel cloud-workflow jobs for shorter wall-clock per leg. | | `pr-mutants` | GH: Linux x86_64 + Windows x86_64 + Linux aarch64 (windows-arm self-skips). ADO: Linux x86_64 + Windows x86_64 | Diff-scoped mutation testing. cargo-mutants doesn't build on `aarch64-pc-windows-msvc`; the recipe self-skips so the windows-arm leg is a no-op. | | `scheduled-exhaustive` | Linux x86_64 + Windows x86_64 | Full `cargo-mutants` / `cargo-hack` / `bench`. cargo-mutants doesn't build on `aarch64-pc-windows-msvc`; rather than splitting the matrix to add an ARM-Linux leg for cargo-hack and bench, the whole group is x86-only. Adopters with ARM-specific concerns extend the matrix in their root workflow. | diff --git a/crates/cargo-anvil/docs/design/ado.md b/crates/cargo-anvil/docs/design/ado.md index e42a9db48..854798523 100644 --- a/crates/cargo-anvil/docs/design/ado.md +++ b/crates/cargo-anvil/docs/design/ado.md @@ -353,11 +353,11 @@ checks actually consume is the catalog's concern, not the wiring layer's. This m moving a check between groups (e.g. `clippy` from `pr-fast` to `scheduled-advisories`) never changes the stages template. -The PR template also contains a Linux-only `pr_msrv` stage. It runs in parallel -with `pr_test`, `pr_runtime_analysis`, and `pr_mutants`, consumes the Linux -affected-package impact set, and invokes `anvil-pr-msrv`. The recipe is a no-op -unless a selecting toolchain file differs from the root MSRV. The Linux impact job -publishes that condition so ADO skips the stage before setup when no run is needed. +The PR template also contains a `pr_msrv` stage. It runs in parallel with +`pr_test`, `pr_runtime_analysis`, and `pr_mutants`, uses the same Linux and Windows +x86_64 jobs and per-OS affected-package impact sets as `pr_test`, and invokes +`anvil-pr-msrv`. The Linux impact job publishes whether the root manifest declares +an MSRV so ADO skips the stage before setup when no run is needed. Internal pipelines set `ANVIL_MSRV_TOOLCHAIN` to the provisioned `ms-prod-*` equivalent of that MSRV; public pipelines let Anvil install the declared MSRV through rustup. @@ -636,15 +636,15 @@ via `anvil-tool-cargo-delta-install` and runs configured snapshots and `cargo delta impact --format json`. The snapshots use `_anvil-base-ref`; cargo- delta's changed-file detection uses the fixed `[git].remote_branch` policy from the repository's `.delta.toml` when configured. Each tier becomes a pre-built -`--package …` string or `--skip`. Impact also detects whether a selecting -toolchain file differs from the root MSRV. The four results are exported as ADO +`--package …` string or `--skip`. Impact also detects whether the root manifest +declares an MSRV. The four results are exported as ADO output variables via `##vso[task.setvariable variable=…;isOutput=true]`: - `compute.include_modified` - `compute.include_affected` - `compute.include_required` -- `compute.msrv_compatibility_required` +- `compute.msrv_test_required` Downstream jobs reference them via `dependencies.impact.outputs['compute.']` inside the runtime macro `$[ … ]` (rather than the compile-time `${{ … }}` macro) diff --git a/crates/cargo-anvil/docs/design/checks.md b/crates/cargo-anvil/docs/design/checks.md index 6427b449c..9ae4df668 100644 --- a/crates/cargo-anvil/docs/design/checks.md +++ b/crates/cargo-anvil/docs/design/checks.md @@ -118,7 +118,7 @@ order, and `just anvil` is an alias for `just anvil-pr`.) |--------------------|---------------------------------------|----------------------------------------------------------------------------------------------------------------------| | `pr-fast` | Linux x86_64 + Windows x86_64 + Linux aarch64 + Windows aarch64 (GH) / Linux x86_64 + Windows x86_64 (ADO) | All static analysis: clippy, `udeps`, `semver-check`, `external-types`, plus the text/metadata checks (fmt, license-headers, ...). Cross-OS because clippy, doc-build, udeps, semver-check, and external-types all compile per host target. Text/metadata checks run on every leg too; the redundancy cost is negligible compared to a separate job's setup overhead. | | `pr-test` | Same default as `pr-fast` | Tests + coverage: `llvm-cov` (instrumented `nextest`), `doc-test`, `examples`. Coverage is uploaded once from the canonical x86_64 Linux leg. | -| `pr-msrv` | Linux x86_64 only | Compatibility tests under the declared MSRV when a selecting root toolchain file uses a different compiler. The job is a cheap no-op when no compatibility run is needed. | +| `pr-msrv` | Same default as `pr-test` | Affected-package unit and integration tests under the declared MSRV, with all features and default features. The group is skipped before setup when no root MSRV is declared. | | `pr-runtime-analysis` | Same default as `pr-fast` | Stricter-runtime correctness: `miri`, `careful`, `loom` (concurrency model checking), `bolero` (short-duration fuzzing smoke). Impact-scoped via `ANVIL_INCLUDE_AFFECTED` so wall-clock is proportional to the PR's blast radius; the cheap checks (loom/bolero) self-skip when no affected crate ships their harness. | | `pr-mutants` | Linux x86_64 + Windows x86_64 + Linux aarch64 (GH) / Linux x86_64 + Windows x86_64 (ADO) | Diff-scoped mutation testing (`mutants --in-diff`). The recipe self-skips on `aarch64-pc-windows-msvc` (cargo-mutants doesn't build there), so the GH windows-arm leg is a no-op rather than a job failure. | @@ -202,18 +202,20 @@ matrix overhead. This is the same set of checks that used to live in the standalone `pr-test` group; merging into `pr-test` removes one cloud-workflow job from the matrix without changing what runs. -#### `pr-msrv` (minimum-version compatibility) +#### `pr-msrv` (minimum-version tests) -When a root `rust-toolchain` or `rust-toolchain.toml` selects a compiler whose -numeric major/minor version differs from the root MSRV, Anvil runs affected-package -tests under the MSRV with both all features and default features. Non-numeric -channels such as `stable` and path toolchains conservatively count as different. -A components-only toolchain file does not select a compiler and therefore does not -trigger this check. +When the root manifest declares an MSRV, Anvil runs affected-package unit and +integration tests under that compiler with both all features and default features. +This is the ordinary test-suite execution at the minimum supported compiler; +`pr-test` runs the same affected suite through coverage instrumentation on the +catalog nightly. A selecting toolchain file does not suppress the MSRV run, even +when it selects the same compiler, because the MSRV group is the authoritative +minimum-version test result. -The compatibility run uses a dedicated Linux x86_64 job/stage in parallel with the -other PR groups. Impact preparation publishes whether the run is required, allowing -the cloud workflow to skip the job/stage before setup when it is not. +The group uses the same OS/architecture matrix and per-OS impact sets as `pr-test` +so cfg-gated targets and dependencies are exercised under the MSRV. It runs in +parallel with the other PR groups. Impact preparation publishes whether a root MSRV +exists, allowing the cloud workflow to skip the group before setup when it does not. `ANVIL_MSRV_TOOLCHAIN` may map the public MSRV to an already-provisioned internal toolchain; when it is unset, setup installs the declared MSRV through rustup. diff --git a/crates/cargo-anvil/docs/design/github.md b/crates/cargo-anvil/docs/design/github.md index 27eeb5d19..db9e0049f 100644 --- a/crates/cargo-anvil/docs/design/github.md +++ b/crates/cargo-anvil/docs/design/github.md @@ -369,16 +369,20 @@ jobs: include_required: ${{ needs.impact.outputs.include_required }} pr-msrv: - name: "Check Group: MSRV Compatibility (linux)" - needs: impact-linux - if: needs.impact-linux.outputs.msrv_compatibility_required == 'true' - runs-on: ${{ inputs.linux_runner }} + name: "Check Group: MSRV Tests (${{ matrix.os }})" + needs: [impact-linux, impact-windows] + if: needs.impact-linux.outputs.msrv_test_required == 'true' + strategy: + fail-fast: false + matrix: + os: [linux, windows, linux-arm, windows-arm] + runs-on: # selected from matrix.os and the runner-label inputs steps: - uses: actions/checkout - uses: ./.github/actions/anvil-run-group with: group: pr-msrv - include_affected: ${{ needs.impact-linux.outputs.include_affected }} + include_affected: # selected from the matching OS-family impact output # pr-runtime-analysis (miri + careful) and pr-mutants (mutants) follow the # multi-OS shape; pr-mutants additionally sets `env: BASE_REF` for @@ -395,12 +399,12 @@ empty matrices that GitHub Actions silently treats as "no legs to run") without meaningfully expanding what adopters could customize — anyone who wants to change the OS axis is almost certainly making other changes too. -The MSRV compatibility group is deliberately a single Linux x86_64 job rather -than another four-leg matrix. Compiler-version compatibility is substantially -less host-specific than the ordinary test suite, and running it in parallel keeps -the extra test pass from extending `pr-test` serially. The Linux impact job exposes -whether a selecting toolchain file differs from the root MSRV, so GitHub skips the -compatibility job before setup when no extra run is needed. +The MSRV group uses the same four-leg matrix as `pr-test`. Minimum-version breaks +can be confined to cfg-gated OS or architecture code, so a single Linux execution +would not establish the supported configuration contract. Running the group in +parallel keeps the extra test pass from extending `pr-test` serially. The Linux +impact job exposes whether the root manifest declares an MSRV, so GitHub skips the +matrix before setup when no minimum version is declared. The pr-* jobs gate on the impact jobs *succeeding*: their `needs: [impact-linux, impact-windows]` uses GitHub's default behavior, so if an impact job fails the pr-* @@ -770,12 +774,12 @@ Outputs: | `include_modified` | `--package X@ver --package Y@ver …` for cargo-delta's `modified` tier, or `--skip` when empty. | | `include_affected` | Same shape, for the `affected` tier (modified ∪ workspace rev-deps). | | `include_required` | Same shape, for the `required` tier (affected ∪ workspace-internal transitive deps). | -| `msrv_compatibility_required` | `true` when a selecting toolchain file differs from the declared root MSRV; otherwise `false`. | +| `msrv_test_required` | `true` when the root manifest declares an MSRV; otherwise `false`. | The include-list outputs never gate jobs; recipes interpret their `--skip` sentinels (see [local.md §4](./local.md#4-impact-scoping-pass-through-env-vars)). -The boolean compatibility output is the sole exception and gates the dedicated -`pr-msrv` job before setup. Unscoped checks (`deny`, `audit`, `aprz`, `pr-title`, +The boolean MSRV output is the sole exception and gates the `pr-msrv` matrix +before setup. Unscoped checks (`deny`, `audit`, `aprz`, `pr-title`, `mutants-full`) still run on every PR even when every tier reports `--skip`. The check → bucket mapping is in diff --git a/crates/cargo-anvil/docs/design/local.md b/crates/cargo-anvil/docs/design/local.md index 24418b14d..6062c97bc 100644 --- a/crates/cargo-anvil/docs/design/local.md +++ b/crates/cargo-anvil/docs/design/local.md @@ -393,12 +393,11 @@ When selection falls back to the root MSRV, prerequisite validation reads single catalog toolchain explicitly with a selecting toolchain file. Anvil does not build a per-package toolchain matrix for this uncommon case. -When a selecting root toolchain file uses a different compiler from the root MSRV, -`anvil-msrv-test` runs affected-package tests under the MSRV with all features and -with default features. Numeric channels are compared by major/minor version, so -`1.95` and `1.95.0` match; non-numeric channels and path toolchains conservatively -trigger the compatibility run. `ANVIL_MSRV_TOOLCHAIN` optionally maps the declared -MSRV to a provisioned internal toolchain without changing when the check is required. +When the root manifest declares an MSRV, `anvil-msrv-test` runs affected-package +unit and integration tests under that compiler with all features and with default +features. A selecting toolchain file does not suppress this minimum-version run. +`ANVIL_MSRV_TOOLCHAIN` optionally maps the declared MSRV to a provisioned internal +toolchain. Without a root MSRV the recipe is a no-op. `anvil-tool-rustc-validate-prereqs` verifies the selected compiler and the uniform MSRV rule. Per-check toolchain requirements (for example, miri, careful, and udeps diff --git a/crates/cargo-anvil/src/anvil/artifacts/ado.rs b/crates/cargo-anvil/src/anvil/artifacts/ado.rs index b21bc3588..df48f063b 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/ado.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/ado.rs @@ -320,9 +320,9 @@ mod tests { // Downstream stages consume the per-OS job outputs from the one stage. assert!(PR_STAGES.contains("stageDependencies.impact.compute_linux.outputs")); assert!(PR_STAGES.contains("stageDependencies.impact.compute_windows.outputs")); - assert!(PR_STAGES.contains("compute.msrv_compatibility_required")); - assert!(PR_STAGES.contains("eq(variables['msrv_compatibility_required'], 'true')")); - assert!(IMPACT_STEP.contains("-MsrvCompatibilityToolchain")); + assert!(PR_STAGES.contains("compute.msrv_test_required")); + assert!(PR_STAGES.contains("eq(variables['msrv_test_required'], 'true')")); + assert!(IMPACT_STEP.contains("-MsrvToolchain")); assert!(PR_STAGES.contains("- template: steps/job.yml")); assert!( !PR_STAGES.contains("\n - job: "), diff --git a/crates/cargo-anvil/src/anvil/artifacts/github.rs b/crates/cargo-anvil/src/anvil/artifacts/github.rs index c67b3c457..449591e1a 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/github.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/github.rs @@ -307,8 +307,8 @@ export -f just fn impact_action_uses_group_none_and_installs_only_cargo_delta() { assert!(IMPACT_ACTION.contains("group: none")); assert!(IMPACT_ACTION.contains("anvil-tool-cargo-delta-install")); - assert!(IMPACT_ACTION.contains("steps.msrv_compatibility.outputs.required")); - assert!(IMPACT_ACTION.contains("-MsrvCompatibilityToolchain")); + assert!(IMPACT_ACTION.contains("steps.msrv_test.outputs.required")); + assert!(IMPACT_ACTION.contains("-MsrvToolchain")); assert!(IMPACT_ACTION.contains("delta_config=\"$(pwd)/.delta.toml\"")); assert!(!IMPACT_ACTION.contains("remote_branch =")); assert_eq!( @@ -360,8 +360,9 @@ export -f just ); } assert!(PR_IMPL_WORKFLOW.contains("needs: [impact-linux, impact-windows]")); - assert!(PR_IMPL_WORKFLOW.contains("msrv_compatibility_required: ${{ steps.delta.outputs.msrv_compatibility_required }}")); - assert!(PR_IMPL_WORKFLOW.contains("if: needs.impact-linux.outputs.msrv_compatibility_required == 'true'")); + assert!(PR_IMPL_WORKFLOW.contains("msrv_test_required: ${{ steps.delta.outputs.msrv_test_required }}")); + assert!(PR_IMPL_WORKFLOW.contains("if: needs.impact-linux.outputs.msrv_test_required == 'true'")); + assert!(PR_IMPL_WORKFLOW.contains("Check Group: MSRV Tests (${{ matrix.os }})")); assert!(PR_IMPL_WORKFLOW.contains("os: [linux, windows, linux-arm, windows-arm]")); assert!(!PR_IMPL_WORKFLOW.contains("fromJSON")); assert!(PR_IMPL_WORKFLOW.contains("PR_TITLE")); diff --git a/crates/cargo-anvil/src/anvil/artifacts/justfile.rs b/crates/cargo-anvil/src/anvil/artifacts/justfile.rs index 84847048a..a99d2d0dd 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/justfile.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/justfile.rs @@ -480,8 +480,8 @@ mod tests { assert!(STABLE_TOOLCHAIN_RESOLVER.contains("rust-version")); assert!(STABLE_TOOLCHAIN_RESOLVER.contains("ValidateWorkspaceMsrv")); assert!(STABLE_TOOLCHAIN_RESOLVER.contains("InstallIfMissing")); - assert!(STABLE_TOOLCHAIN_RESOLVER.contains("MsrvCompatibilityToolchain")); - assert!(STABLE_TOOLCHAIN_RESOLVER.contains("InstallMsrvCompatibilityIfNeeded")); + assert!(STABLE_TOOLCHAIN_RESOLVER.contains("MsrvToolchain")); + assert!(STABLE_TOOLCHAIN_RESOLVER.contains("InstallMsrvIfNeeded")); } mod stable_toolchain_resolver_tests { @@ -601,14 +601,14 @@ mod tests { } #[test] - fn requests_compatibility_only_for_a_different_selecting_toolchain() { + fn resolves_msrv_whenever_the_root_declares_one() { let no_msrv = fixture("[workspace]\nresolver = \"2\"\n"); fs::write(no_msrv.path().join("rust-toolchain.toml"), "[toolchain]\nchannel = \"1.94\"\n") .expect("toolchain fixture must be writable"); - let output = run(no_msrv.path(), &["-MsrvCompatibilityToolchain"], None); + let output = run(no_msrv.path(), &["-MsrvToolchain"], None); assert!( output.status.success(), - "a repository without a declared MSRV must skip compatibility" + "a repository without a declared MSRV must skip the MSRV test" ); assert!( String::from_utf8(output.stdout) @@ -619,11 +619,11 @@ mod tests { let temp = fixture("[workspace.package]\nrust-version = \"1.93\"\n"); let root = temp.path(); - let compatibility = |root: &Path| { - let output = run(root, &["-MsrvCompatibilityToolchain"], None); + let resolve_msrv = |root: &Path| { + let output = run(root, &["-MsrvToolchain"], None); assert!( output.status.success(), - "compatibility resolution failed: {}", + "MSRV resolution failed: {}", String::from_utf8_lossy(&output.stderr) ); String::from_utf8(output.stdout) @@ -632,23 +632,18 @@ mod tests { .to_owned() }; - assert!(compatibility(root).is_empty()); + assert_eq!(resolve_msrv(root), "1.93"); fs::write(root.join("rust-toolchain.toml"), "[toolchain]\ncomponents = [\"clippy\"]\n") .expect("toolchain fixture must be writable"); - assert!(compatibility(root).is_empty()); + assert_eq!(resolve_msrv(root), "1.93"); fs::write(root.join("rust-toolchain.toml"), "[toolchain]\nchannel = \"1.93.0\"\n").expect("toolchain fixture must be writable"); - assert!(compatibility(root).is_empty()); + assert_eq!(resolve_msrv(root), "1.93"); fs::write(root.join("rust-toolchain.toml"), "[toolchain]\nchannel = \"1.94\"\n").expect("toolchain fixture must be writable"); - assert_eq!(compatibility(root), "1.93"); + assert_eq!(resolve_msrv(root), "1.93"); let output = Command::new("pwsh") - .args([ - "-NoProfile", - "-File", - ".anvil/resolve-stable-toolchain.ps1", - "-MsrvCompatibilityToolchain", - ]) + .args(["-NoProfile", "-File", ".anvil/resolve-stable-toolchain.ps1", "-MsrvToolchain"]) .current_dir(root) .env_remove("RUSTUP_TOOLCHAIN") .env("ANVIL_MSRV_TOOLCHAIN", "ms-prod-1.93") @@ -662,7 +657,7 @@ mod tests { fs::write(root.join("rust-toolchain.toml"), "[toolchain]\npath = \"toolchains/custom\"\n") .expect("toolchain fixture must be writable"); - assert_eq!(compatibility(root), "1.93"); + assert_eq!(resolve_msrv(root), "1.93"); } } diff --git a/crates/cargo-anvil/src/lib.rs b/crates/cargo-anvil/src/lib.rs index d596bb1ec..de8af6ad7 100644 --- a/crates/cargo-anvil/src/lib.rs +++ b/crates/cargo-anvil/src/lib.rs @@ -254,7 +254,7 @@ //! pr-slowpr-testllvm-covdual feature-config; gated by cargo-coverage-gate //! doc-testruns both feature configs //! examplescompile-only -//! pr-msrvmsrv-testconditional dual feature-config compatibility test +//! pr-msrvmsrv-testdual feature-config tests under the declared MSRV //! pr-runtime-analysismirilibtest, not nextest //! carefulself-cleans on a toolchain bump //! loomopt-in targets only diff --git a/crates/cargo-anvil/templates/ado/pr-stages.yml b/crates/cargo-anvil/templates/ado/pr-stages.yml index dbd2588fc..a5ecf90f4 100644 --- a/crates/cargo-anvil/templates/ado/pr-stages.yml +++ b/crates/cargo-anvil/templates/ado/pr-stages.yml @@ -168,12 +168,13 @@ stages: failIfCoverageEmpty: false - stage: pr_msrv - displayName: anvil pr-msrv (MSRV compatibility) + displayName: anvil pr-msrv (MSRV tests) dependsOn: [impact] - condition: and(succeeded(), eq(variables['msrv_compatibility_required'], 'true')) + condition: and(succeeded(), eq(variables['msrv_test_required'], 'true')) variables: include_affected_linux: $[ stageDependencies.impact.compute_linux.outputs['compute.include_affected'] ] - msrv_compatibility_required: $[ stageDependencies.impact.compute_linux.outputs['compute.msrv_compatibility_required'] ] + include_affected_windows: $[ stageDependencies.impact.compute_windows.outputs['compute.include_affected'] ] + msrv_test_required: $[ stageDependencies.impact.compute_linux.outputs['compute.msrv_test_required'] ] jobs: - template: steps/job.yml parameters: @@ -183,6 +184,14 @@ stages: - template: steps/pr-msrv.yml parameters: include_affected: $(include_affected_linux) + - template: steps/job.yml + parameters: + name: windows + pool: ${{ parameters.windowsPool }} + steps: + - template: steps/pr-msrv.yml + parameters: + include_affected: $(include_affected_windows) - stage: pr_runtime_analysis displayName: anvil pr-runtime-analysis (miri + careful) diff --git a/crates/cargo-anvil/templates/ado/steps/impact.yml b/crates/cargo-anvil/templates/ado/steps/impact.yml index d808571fa..f0c34ea78 100644 --- a/crates/cargo-anvil/templates/ado/steps/impact.yml +++ b/crates/cargo-anvil/templates/ado/steps/impact.yml @@ -15,8 +15,7 @@ # include_affected - same shape, for the affected set (modified ∪ rev-deps). # include_required - same shape, for the required set # (affected ∪ workspace-internal transitive deps). -# msrv_compatibility_required - "true" when a selecting toolchain file -# differs from the root MSRV. +# msrv_test_required - "true" when the root manifest declares an MSRV. # # Unscoped recipes (deny, audit, aprz, pr-title) ignore all three. steps: @@ -48,11 +47,11 @@ steps: # worktree's rust-toolchain.toml. Capture it here, in the PR checkout, # where the active toolchain is resolved and installed. active_toolchain="$(rustup show active-toolchain | head -n1 | cut -d' ' -f1)" - compatibility_toolchain="$(pwsh -NoProfile -File .anvil/resolve-stable-toolchain.ps1 -MsrvCompatibilityToolchain)" - if [ -n "$compatibility_toolchain" ]; then - echo "##vso[task.setvariable variable=msrv_compatibility_required;isOutput=true]true" + msrv_toolchain="$(pwsh -NoProfile -File .anvil/resolve-stable-toolchain.ps1 -MsrvToolchain)" + if [ -n "$msrv_toolchain" ]; then + echo "##vso[task.setvariable variable=msrv_test_required;isOutput=true]true" else - echo "##vso[task.setvariable variable=msrv_compatibility_required;isOutput=true]false" + echo "##vso[task.setvariable variable=msrv_test_required;isOutput=true]false" fi # Two snapshots + impact compare (cargo-delta has no --base # shortcut). The baseline runs in a worktree at the merge target diff --git a/crates/cargo-anvil/templates/anvil/resolve-stable-toolchain.ps1 b/crates/cargo-anvil/templates/anvil/resolve-stable-toolchain.ps1 index df8cbfd35..736f8d084 100644 --- a/crates/cargo-anvil/templates/anvil/resolve-stable-toolchain.ps1 +++ b/crates/cargo-anvil/templates/anvil/resolve-stable-toolchain.ps1 @@ -6,8 +6,8 @@ param( [switch] $ValidateWorkspaceMsrv, [switch] $InstallIfMissing, [switch] $ForEnvironment, - [switch] $MsrvCompatibilityToolchain, - [switch] $InstallMsrvCompatibilityIfNeeded + [switch] $MsrvToolchain, + [switch] $InstallMsrvIfNeeded ) $ErrorActionPreference = 'Stop' @@ -118,23 +118,11 @@ function Assert-UniformWorkspaceMsrv { } } -function Get-MsrvCompatibilitySelection { - if ($null -eq $fileSelection) { - return $null - } - +function Get-MsrvSelection { $msrv = Get-RootMsrv -AllowMissing if ([string]::IsNullOrWhiteSpace($msrv)) { return $null } - $selectedMatch = [regex]::Match($fileSelection.Value, '^(\d+)\.(\d+)(?:\.\d+)?(?:-.+)?$') - $msrvMatch = [regex]::Match($msrv, '^(\d+)\.(\d+)(?:\.\d+)?(?:-.+)?$') - if ($fileSelection.Selector -eq 'channel' -and $selectedMatch.Success -and $msrvMatch.Success) { - if ($selectedMatch.Groups[1].Value -eq $msrvMatch.Groups[1].Value -and - $selectedMatch.Groups[2].Value -eq $msrvMatch.Groups[2].Value) { - return $null - } - } if (-not [string]::IsNullOrWhiteSpace($env:ANVIL_MSRV_TOOLCHAIN)) { return [pscustomobject]@{ Value = $env:ANVIL_MSRV_TOOLCHAIN; Installable = $false; Mapped = $true } @@ -175,22 +163,22 @@ if ($ValidateWorkspaceMsrv) { exit 0 } -if ($MsrvCompatibilityToolchain -or $InstallMsrvCompatibilityIfNeeded) { - $compatibilitySelection = Get-MsrvCompatibilitySelection - if ($null -eq $compatibilitySelection) { +if ($MsrvToolchain -or $InstallMsrvIfNeeded) { + $msrvSelection = Get-MsrvSelection + if ($null -eq $msrvSelection) { exit 0 } - if ($InstallMsrvCompatibilityIfNeeded) { - if ($compatibilitySelection.Mapped) { - & cargo "+$($compatibilitySelection.Value)" --version *> $null + if ($InstallMsrvIfNeeded) { + if ($msrvSelection.Mapped) { + & cargo "+$($msrvSelection.Value)" --version *> $null if ($LASTEXITCODE -ne 0) { - throw "anvil: mapped MSRV toolchain '$($compatibilitySelection.Value)' is unavailable; provision ANVIL_MSRV_TOOLCHAIN or unset it to let Anvil install the declared public MSRV" + throw "anvil: mapped MSRV toolchain '$($msrvSelection.Value)' is unavailable; provision ANVIL_MSRV_TOOLCHAIN or unset it to let Anvil install the declared public MSRV" } } else { - Install-Toolchain $compatibilitySelection 'MSRV compatibility' + Install-Toolchain $msrvSelection 'MSRV' } } else { - $compatibilitySelection.Value + $msrvSelection.Value } exit 0 } diff --git a/crates/cargo-anvil/templates/github/impact-action.yml b/crates/cargo-anvil/templates/github/impact-action.yml index 4c1f8b14a..37610ebb7 100644 --- a/crates/cargo-anvil/templates/github/impact-action.yml +++ b/crates/cargo-anvil/templates/github/impact-action.yml @@ -19,8 +19,7 @@ description: | (modified ∪ rev-deps). include_required - same shape, for crates in the required set (affected ∪ workspace-internal transitive deps). - msrv_compatibility_required - "true" when a selecting toolchain file - differs from the root MSRV. + msrv_test_required - "true" when the root manifest declares an MSRV. Recipes in justfiles/anvil/checks/ interpret each variable per their tier: modified-tier recipes (fmt, license-headers, spellcheck, ...) short- @@ -41,9 +40,9 @@ outputs: include_required: description: Pre-formatted --package args for the required tier. value: ${{ steps.compute.outputs.include_required }} - msrv_compatibility_required: - description: Whether a selecting toolchain file differs from the root MSRV. - value: ${{ steps.msrv_compatibility.outputs.required }} + msrv_test_required: + description: Whether the root manifest declares an MSRV. + value: ${{ steps.msrv_test.outputs.required }} runs: using: composite steps: @@ -58,11 +57,11 @@ runs: - name: Install cargo-delta shell: bash run: just anvil-tool-cargo-delta-install binstall - - id: msrv_compatibility - name: Detect MSRV compatibility run + - id: msrv_test + name: Detect MSRV test run shell: pwsh -NoProfile -Command ". '{0}'" run: | - $toolchain = (& '.anvil/resolve-stable-toolchain.ps1' -MsrvCompatibilityToolchain | Out-String).Trim() + $toolchain = (& '.anvil/resolve-stable-toolchain.ps1' -MsrvToolchain | Out-String).Trim() $required = -not [string]::IsNullOrWhiteSpace($toolchain) "required=$($required.ToString().ToLowerInvariant())" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append - id: compute diff --git a/crates/cargo-anvil/templates/github/pr-impl-workflow.yml b/crates/cargo-anvil/templates/github/pr-impl-workflow.yml index 58ea25ad4..102256f6f 100644 --- a/crates/cargo-anvil/templates/github/pr-impl-workflow.yml +++ b/crates/cargo-anvil/templates/github/pr-impl-workflow.yml @@ -70,7 +70,7 @@ jobs: include_modified: ${{ steps.delta.outputs.include_modified }} include_affected: ${{ steps.delta.outputs.include_affected }} include_required: ${{ steps.delta.outputs.include_required }} - msrv_compatibility_required: ${{ steps.delta.outputs.msrv_compatibility_required }} + msrv_test_required: ${{ steps.delta.outputs.msrv_test_required }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -213,10 +213,17 @@ jobs: fail_ci_if_error: false pr-msrv: - name: "Check Group: MSRV Compatibility (linux)" - needs: impact-linux - if: needs.impact-linux.outputs.msrv_compatibility_required == 'true' - runs-on: ${{ inputs.linux_runner }} + name: "Check Group: MSRV Tests (${{ matrix.os }})" + needs: [impact-linux, impact-windows] + if: needs.impact-linux.outputs.msrv_test_required == 'true' + strategy: + fail-fast: false + matrix: + os: [linux, windows, linux-arm, windows-arm] + runs-on: ${{ matrix.os == 'linux' && inputs.linux_runner + || matrix.os == 'windows' && inputs.windows_runner + || matrix.os == 'linux-arm' && inputs.linux_arm_runner + || inputs.windows_arm_runner }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -226,7 +233,7 @@ jobs: group: pr-msrv publish_commit_statuses: ${{ inputs.publish_commit_statuses }} free-disk-space: true - include_affected: ${{ needs.impact-linux.outputs.include_affected }} + include_affected: ${{ startsWith(matrix.os, 'linux') && needs.impact-linux.outputs.include_affected || needs.impact-windows.outputs.include_affected }} pr-runtime-analysis: name: "Check Group: Runtime Analysis (${{ matrix.os }})" diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/msrv-test.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/msrv-test.just index 0e4889236..491701f50 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/msrv-test.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/msrv-test.just @@ -6,15 +6,14 @@ # See https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/checks.md -# Run affected-package tests under the declared MSRV when a selecting root -# toolchain file chooses a different compiler. +# Run affected-package tests under the declared MSRV. [script("pwsh", "-NoProfile")] anvil-msrv-test: anvil-msrv-test-validate-prereqs $ErrorActionPreference = 'Stop' if ($env:ANVIL_INCLUDE_AFFECTED -eq '--skip') { exit 0 } - $toolchain = (& '.anvil/resolve-stable-toolchain.ps1' -MsrvCompatibilityToolchain | Out-String).Trim() + $toolchain = (& '.anvil/resolve-stable-toolchain.ps1' -MsrvToolchain | Out-String).Trim() if ([string]::IsNullOrWhiteSpace($toolchain)) { - Write-Host 'anvil-msrv-test: selected toolchain matches the MSRV; skipping' + Write-Host 'anvil-msrv-test: no root MSRV declared; skipping' exit 0 } $pkg = @(if ($env:ANVIL_INCLUDE_AFFECTED) { -split $env:ANVIL_INCLUDE_AFFECTED } else { '--workspace' }) @@ -23,18 +22,18 @@ anvil-msrv-test: anvil-msrv-test-validate-prereqs & cargo "+$toolchain" test @pkg --all-targets --locked if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } -# Install the MSRV compatibility toolchain only when this check is required. +# Install the declared MSRV toolchain only when this check is required. [group("anvil-setup")] [script("pwsh", "-NoProfile")] anvil-msrv-test-setup installer="install": - & '.anvil/resolve-stable-toolchain.ps1' -InstallMsrvCompatibilityIfNeeded + & '.anvil/resolve-stable-toolchain.ps1' -InstallMsrvIfNeeded if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } -# Validate the conditional MSRV compatibility prerequisite. +# Validate the MSRV prerequisite. [group("anvil-setup")] [script("pwsh", "-NoProfile")] anvil-msrv-test-validate-prereqs: - $toolchain = (& '.anvil/resolve-stable-toolchain.ps1' -MsrvCompatibilityToolchain | Out-String).Trim() + $toolchain = (& '.anvil/resolve-stable-toolchain.ps1' -MsrvToolchain | Out-String).Trim() if ([string]::IsNullOrWhiteSpace($toolchain)) { exit 0 } & cargo "+$toolchain" --version if ($LASTEXITCODE -ne 0) { @@ -43,6 +42,6 @@ anvil-msrv-test-validate-prereqs: } else { 'run: just anvil-msrv-test-setup' } - Write-Error "anvil: MSRV compatibility toolchain '$toolchain' is unavailable; $hint" + Write-Error "anvil: MSRV toolchain '$toolchain' is unavailable; $hint" exit $LASTEXITCODE } diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap index e7b3c68d2..f3159bc0e 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap @@ -1205,8 +1205,8 @@ param( [switch] $ValidateWorkspaceMsrv, [switch] $InstallIfMissing, [switch] $ForEnvironment, - [switch] $MsrvCompatibilityToolchain, - [switch] $InstallMsrvCompatibilityIfNeeded + [switch] $MsrvToolchain, + [switch] $InstallMsrvIfNeeded ) $ErrorActionPreference = 'Stop' @@ -1317,23 +1317,11 @@ function Assert-UniformWorkspaceMsrv { } } -function Get-MsrvCompatibilitySelection { - if ($null -eq $fileSelection) { - return $null - } - +function Get-MsrvSelection { $msrv = Get-RootMsrv -AllowMissing if ([string]::IsNullOrWhiteSpace($msrv)) { return $null } - $selectedMatch = [regex]::Match($fileSelection.Value, '^(\d+)\.(\d+)(?:\.\d+)?(?:-.+)?$') - $msrvMatch = [regex]::Match($msrv, '^(\d+)\.(\d+)(?:\.\d+)?(?:-.+)?$') - if ($fileSelection.Selector -eq 'channel' -and $selectedMatch.Success -and $msrvMatch.Success) { - if ($selectedMatch.Groups[1].Value -eq $msrvMatch.Groups[1].Value -and - $selectedMatch.Groups[2].Value -eq $msrvMatch.Groups[2].Value) { - return $null - } - } if (-not [string]::IsNullOrWhiteSpace($env:ANVIL_MSRV_TOOLCHAIN)) { return [pscustomobject]@{ Value = $env:ANVIL_MSRV_TOOLCHAIN; Installable = $false; Mapped = $true } @@ -1374,22 +1362,22 @@ if ($ValidateWorkspaceMsrv) { exit 0 } -if ($MsrvCompatibilityToolchain -or $InstallMsrvCompatibilityIfNeeded) { - $compatibilitySelection = Get-MsrvCompatibilitySelection - if ($null -eq $compatibilitySelection) { +if ($MsrvToolchain -or $InstallMsrvIfNeeded) { + $msrvSelection = Get-MsrvSelection + if ($null -eq $msrvSelection) { exit 0 } - if ($InstallMsrvCompatibilityIfNeeded) { - if ($compatibilitySelection.Mapped) { - & cargo "+$($compatibilitySelection.Value)" --version *> $null + if ($InstallMsrvIfNeeded) { + if ($msrvSelection.Mapped) { + & cargo "+$($msrvSelection.Value)" --version *> $null if ($LASTEXITCODE -ne 0) { - throw "anvil: mapped MSRV toolchain '$($compatibilitySelection.Value)' is unavailable; provision ANVIL_MSRV_TOOLCHAIN or unset it to let Anvil install the declared public MSRV" + throw "anvil: mapped MSRV toolchain '$($msrvSelection.Value)' is unavailable; provision ANVIL_MSRV_TOOLCHAIN or unset it to let Anvil install the declared public MSRV" } } else { - Install-Toolchain $compatibilitySelection 'MSRV compatibility' + Install-Toolchain $msrvSelection 'MSRV' } } else { - $compatibilitySelection.Value + $msrvSelection.Value } exit 0 } @@ -1702,12 +1690,13 @@ stages: failIfCoverageEmpty: false - stage: pr_msrv - displayName: anvil pr-msrv (MSRV compatibility) + displayName: anvil pr-msrv (MSRV tests) dependsOn: [impact] - condition: and(succeeded(), eq(variables['msrv_compatibility_required'], 'true')) + condition: and(succeeded(), eq(variables['msrv_test_required'], 'true')) variables: include_affected_linux: $[ stageDependencies.impact.compute_linux.outputs['compute.include_affected'] ] - msrv_compatibility_required: $[ stageDependencies.impact.compute_linux.outputs['compute.msrv_compatibility_required'] ] + include_affected_windows: $[ stageDependencies.impact.compute_windows.outputs['compute.include_affected'] ] + msrv_test_required: $[ stageDependencies.impact.compute_linux.outputs['compute.msrv_test_required'] ] jobs: - template: steps/job.yml parameters: @@ -1717,6 +1706,14 @@ stages: - template: steps/pr-msrv.yml parameters: include_affected: $(include_affected_linux) + - template: steps/job.yml + parameters: + name: windows + pool: ${{ parameters.windowsPool }} + steps: + - template: steps/pr-msrv.yml + parameters: + include_affected: $(include_affected_windows) - stage: pr_runtime_analysis displayName: anvil pr-runtime-analysis (miri + careful) @@ -2010,8 +2007,7 @@ steps: # include_affected - same shape, for the affected set (modified ∪ rev-deps). # include_required - same shape, for the required set # (affected ∪ workspace-internal transitive deps). -# msrv_compatibility_required - "true" when a selecting toolchain file -# differs from the root MSRV. +# msrv_test_required - "true" when the root manifest declares an MSRV. # # Unscoped recipes (deny, audit, aprz, pr-title) ignore all three. steps: @@ -2043,11 +2039,11 @@ steps: # worktree's rust-toolchain.toml. Capture it here, in the PR checkout, # where the active toolchain is resolved and installed. active_toolchain="$(rustup show active-toolchain | head -n1 | cut -d' ' -f1)" - compatibility_toolchain="$(pwsh -NoProfile -File .anvil/resolve-stable-toolchain.ps1 -MsrvCompatibilityToolchain)" - if [ -n "$compatibility_toolchain" ]; then - echo "##vso[task.setvariable variable=msrv_compatibility_required;isOutput=true]true" + msrv_toolchain="$(pwsh -NoProfile -File .anvil/resolve-stable-toolchain.ps1 -MsrvToolchain)" + if [ -n "$msrv_toolchain" ]; then + echo "##vso[task.setvariable variable=msrv_test_required;isOutput=true]true" else - echo "##vso[task.setvariable variable=msrv_compatibility_required;isOutput=true]false" + echo "##vso[task.setvariable variable=msrv_test_required;isOutput=true]false" fi # Two snapshots + impact compare (cargo-delta has no --base # shortcut). The baseline runs in a worktree at the merge target @@ -4242,15 +4238,14 @@ anvil-miri-validate-prereqs: anvil-component-nightly-miri-validate-prereqs anvil # See https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/checks.md -# Run affected-package tests under the declared MSRV when a selecting root -# toolchain file chooses a different compiler. +# Run affected-package tests under the declared MSRV. [script("pwsh", "-NoProfile")] anvil-msrv-test: anvil-msrv-test-validate-prereqs $ErrorActionPreference = 'Stop' if ($env:ANVIL_INCLUDE_AFFECTED -eq '--skip') { exit 0 } - $toolchain = (& '.anvil/resolve-stable-toolchain.ps1' -MsrvCompatibilityToolchain | Out-String).Trim() + $toolchain = (& '.anvil/resolve-stable-toolchain.ps1' -MsrvToolchain | Out-String).Trim() if ([string]::IsNullOrWhiteSpace($toolchain)) { - Write-Host 'anvil-msrv-test: selected toolchain matches the MSRV; skipping' + Write-Host 'anvil-msrv-test: no root MSRV declared; skipping' exit 0 } $pkg = @(if ($env:ANVIL_INCLUDE_AFFECTED) { -split $env:ANVIL_INCLUDE_AFFECTED } else { '--workspace' }) @@ -4259,18 +4254,18 @@ anvil-msrv-test: anvil-msrv-test-validate-prereqs & cargo "+$toolchain" test @pkg --all-targets --locked if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } -# Install the MSRV compatibility toolchain only when this check is required. +# Install the declared MSRV toolchain only when this check is required. [group("anvil-setup")] [script("pwsh", "-NoProfile")] anvil-msrv-test-setup installer="install": - & '.anvil/resolve-stable-toolchain.ps1' -InstallMsrvCompatibilityIfNeeded + & '.anvil/resolve-stable-toolchain.ps1' -InstallMsrvIfNeeded if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } -# Validate the conditional MSRV compatibility prerequisite. +# Validate the MSRV prerequisite. [group("anvil-setup")] [script("pwsh", "-NoProfile")] anvil-msrv-test-validate-prereqs: - $toolchain = (& '.anvil/resolve-stable-toolchain.ps1' -MsrvCompatibilityToolchain | Out-String).Trim() + $toolchain = (& '.anvil/resolve-stable-toolchain.ps1' -MsrvToolchain | Out-String).Trim() if ([string]::IsNullOrWhiteSpace($toolchain)) { exit 0 } & cargo "+$toolchain" --version if ($LASTEXITCODE -ne 0) { @@ -4279,7 +4274,7 @@ anvil-msrv-test-validate-prereqs: } else { 'run: just anvil-msrv-test-setup' } - Write-Error "anvil: MSRV compatibility toolchain '$toolchain' is unavailable; $hint" + Write-Error "anvil: MSRV toolchain '$toolchain' is unavailable; $hint" exit $LASTEXITCODE } diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap index 5784c35fb..682e42120 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap @@ -1205,8 +1205,8 @@ param( [switch] $ValidateWorkspaceMsrv, [switch] $InstallIfMissing, [switch] $ForEnvironment, - [switch] $MsrvCompatibilityToolchain, - [switch] $InstallMsrvCompatibilityIfNeeded + [switch] $MsrvToolchain, + [switch] $InstallMsrvIfNeeded ) $ErrorActionPreference = 'Stop' @@ -1317,23 +1317,11 @@ function Assert-UniformWorkspaceMsrv { } } -function Get-MsrvCompatibilitySelection { - if ($null -eq $fileSelection) { - return $null - } - +function Get-MsrvSelection { $msrv = Get-RootMsrv -AllowMissing if ([string]::IsNullOrWhiteSpace($msrv)) { return $null } - $selectedMatch = [regex]::Match($fileSelection.Value, '^(\d+)\.(\d+)(?:\.\d+)?(?:-.+)?$') - $msrvMatch = [regex]::Match($msrv, '^(\d+)\.(\d+)(?:\.\d+)?(?:-.+)?$') - if ($fileSelection.Selector -eq 'channel' -and $selectedMatch.Success -and $msrvMatch.Success) { - if ($selectedMatch.Groups[1].Value -eq $msrvMatch.Groups[1].Value -and - $selectedMatch.Groups[2].Value -eq $msrvMatch.Groups[2].Value) { - return $null - } - } if (-not [string]::IsNullOrWhiteSpace($env:ANVIL_MSRV_TOOLCHAIN)) { return [pscustomobject]@{ Value = $env:ANVIL_MSRV_TOOLCHAIN; Installable = $false; Mapped = $true } @@ -1374,22 +1362,22 @@ if ($ValidateWorkspaceMsrv) { exit 0 } -if ($MsrvCompatibilityToolchain -or $InstallMsrvCompatibilityIfNeeded) { - $compatibilitySelection = Get-MsrvCompatibilitySelection - if ($null -eq $compatibilitySelection) { +if ($MsrvToolchain -or $InstallMsrvIfNeeded) { + $msrvSelection = Get-MsrvSelection + if ($null -eq $msrvSelection) { exit 0 } - if ($InstallMsrvCompatibilityIfNeeded) { - if ($compatibilitySelection.Mapped) { - & cargo "+$($compatibilitySelection.Value)" --version *> $null + if ($InstallMsrvIfNeeded) { + if ($msrvSelection.Mapped) { + & cargo "+$($msrvSelection.Value)" --version *> $null if ($LASTEXITCODE -ne 0) { - throw "anvil: mapped MSRV toolchain '$($compatibilitySelection.Value)' is unavailable; provision ANVIL_MSRV_TOOLCHAIN or unset it to let Anvil install the declared public MSRV" + throw "anvil: mapped MSRV toolchain '$($msrvSelection.Value)' is unavailable; provision ANVIL_MSRV_TOOLCHAIN or unset it to let Anvil install the declared public MSRV" } } else { - Install-Toolchain $compatibilitySelection 'MSRV compatibility' + Install-Toolchain $msrvSelection 'MSRV' } } else { - $compatibilitySelection.Value + $msrvSelection.Value } exit 0 } @@ -1472,8 +1460,7 @@ description: | (modified ∪ rev-deps). include_required - same shape, for crates in the required set (affected ∪ workspace-internal transitive deps). - msrv_compatibility_required - "true" when a selecting toolchain file - differs from the root MSRV. + msrv_test_required - "true" when the root manifest declares an MSRV. Recipes in justfiles/anvil/checks/ interpret each variable per their tier: modified-tier recipes (fmt, license-headers, spellcheck, ...) short- @@ -1494,9 +1481,9 @@ outputs: include_required: description: Pre-formatted --package args for the required tier. value: ${{ steps.compute.outputs.include_required }} - msrv_compatibility_required: - description: Whether a selecting toolchain file differs from the root MSRV. - value: ${{ steps.msrv_compatibility.outputs.required }} + msrv_test_required: + description: Whether the root manifest declares an MSRV. + value: ${{ steps.msrv_test.outputs.required }} runs: using: composite steps: @@ -1511,11 +1498,11 @@ runs: - name: Install cargo-delta shell: bash run: just anvil-tool-cargo-delta-install binstall - - id: msrv_compatibility - name: Detect MSRV compatibility run + - id: msrv_test + name: Detect MSRV test run shell: pwsh -NoProfile -Command ". '{0}'" run: | - $toolchain = (& '.anvil/resolve-stable-toolchain.ps1' -MsrvCompatibilityToolchain | Out-String).Trim() + $toolchain = (& '.anvil/resolve-stable-toolchain.ps1' -MsrvToolchain | Out-String).Trim() $required = -not [string]::IsNullOrWhiteSpace($toolchain) "required=$($required.ToString().ToLowerInvariant())" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append - id: compute @@ -2139,7 +2126,7 @@ jobs: include_modified: ${{ steps.delta.outputs.include_modified }} include_affected: ${{ steps.delta.outputs.include_affected }} include_required: ${{ steps.delta.outputs.include_required }} - msrv_compatibility_required: ${{ steps.delta.outputs.msrv_compatibility_required }} + msrv_test_required: ${{ steps.delta.outputs.msrv_test_required }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -2282,10 +2269,17 @@ jobs: fail_ci_if_error: false pr-msrv: - name: "Check Group: MSRV Compatibility (linux)" - needs: impact-linux - if: needs.impact-linux.outputs.msrv_compatibility_required == 'true' - runs-on: ${{ inputs.linux_runner }} + name: "Check Group: MSRV Tests (${{ matrix.os }})" + needs: [impact-linux, impact-windows] + if: needs.impact-linux.outputs.msrv_test_required == 'true' + strategy: + fail-fast: false + matrix: + os: [linux, windows, linux-arm, windows-arm] + runs-on: ${{ matrix.os == 'linux' && inputs.linux_runner + || matrix.os == 'windows' && inputs.windows_runner + || matrix.os == 'linux-arm' && inputs.linux_arm_runner + || inputs.windows_arm_runner }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -2295,7 +2289,7 @@ jobs: group: pr-msrv publish_commit_statuses: ${{ inputs.publish_commit_statuses }} free-disk-space: true - include_affected: ${{ needs.impact-linux.outputs.include_affected }} + include_affected: ${{ startsWith(matrix.os, 'linux') && needs.impact-linux.outputs.include_affected || needs.impact-windows.outputs.include_affected }} pr-runtime-analysis: name: "Check Group: Runtime Analysis (${{ matrix.os }})" @@ -4079,15 +4073,14 @@ anvil-miri-validate-prereqs: anvil-component-nightly-miri-validate-prereqs anvil # See https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/checks.md -# Run affected-package tests under the declared MSRV when a selecting root -# toolchain file chooses a different compiler. +# Run affected-package tests under the declared MSRV. [script("pwsh", "-NoProfile")] anvil-msrv-test: anvil-msrv-test-validate-prereqs $ErrorActionPreference = 'Stop' if ($env:ANVIL_INCLUDE_AFFECTED -eq '--skip') { exit 0 } - $toolchain = (& '.anvil/resolve-stable-toolchain.ps1' -MsrvCompatibilityToolchain | Out-String).Trim() + $toolchain = (& '.anvil/resolve-stable-toolchain.ps1' -MsrvToolchain | Out-String).Trim() if ([string]::IsNullOrWhiteSpace($toolchain)) { - Write-Host 'anvil-msrv-test: selected toolchain matches the MSRV; skipping' + Write-Host 'anvil-msrv-test: no root MSRV declared; skipping' exit 0 } $pkg = @(if ($env:ANVIL_INCLUDE_AFFECTED) { -split $env:ANVIL_INCLUDE_AFFECTED } else { '--workspace' }) @@ -4096,18 +4089,18 @@ anvil-msrv-test: anvil-msrv-test-validate-prereqs & cargo "+$toolchain" test @pkg --all-targets --locked if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } -# Install the MSRV compatibility toolchain only when this check is required. +# Install the declared MSRV toolchain only when this check is required. [group("anvil-setup")] [script("pwsh", "-NoProfile")] anvil-msrv-test-setup installer="install": - & '.anvil/resolve-stable-toolchain.ps1' -InstallMsrvCompatibilityIfNeeded + & '.anvil/resolve-stable-toolchain.ps1' -InstallMsrvIfNeeded if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } -# Validate the conditional MSRV compatibility prerequisite. +# Validate the MSRV prerequisite. [group("anvil-setup")] [script("pwsh", "-NoProfile")] anvil-msrv-test-validate-prereqs: - $toolchain = (& '.anvil/resolve-stable-toolchain.ps1' -MsrvCompatibilityToolchain | Out-String).Trim() + $toolchain = (& '.anvil/resolve-stable-toolchain.ps1' -MsrvToolchain | Out-String).Trim() if ([string]::IsNullOrWhiteSpace($toolchain)) { exit 0 } & cargo "+$toolchain" --version if ($LASTEXITCODE -ne 0) { @@ -4116,7 +4109,7 @@ anvil-msrv-test-validate-prereqs: } else { 'run: just anvil-msrv-test-setup' } - Write-Error "anvil: MSRV compatibility toolchain '$toolchain' is unavailable; $hint" + Write-Error "anvil: MSRV toolchain '$toolchain' is unavailable; $hint" exit $LASTEXITCODE } diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap index a92f2adb3..4a2b39809 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap @@ -1205,8 +1205,8 @@ param( [switch] $ValidateWorkspaceMsrv, [switch] $InstallIfMissing, [switch] $ForEnvironment, - [switch] $MsrvCompatibilityToolchain, - [switch] $InstallMsrvCompatibilityIfNeeded + [switch] $MsrvToolchain, + [switch] $InstallMsrvIfNeeded ) $ErrorActionPreference = 'Stop' @@ -1317,23 +1317,11 @@ function Assert-UniformWorkspaceMsrv { } } -function Get-MsrvCompatibilitySelection { - if ($null -eq $fileSelection) { - return $null - } - +function Get-MsrvSelection { $msrv = Get-RootMsrv -AllowMissing if ([string]::IsNullOrWhiteSpace($msrv)) { return $null } - $selectedMatch = [regex]::Match($fileSelection.Value, '^(\d+)\.(\d+)(?:\.\d+)?(?:-.+)?$') - $msrvMatch = [regex]::Match($msrv, '^(\d+)\.(\d+)(?:\.\d+)?(?:-.+)?$') - if ($fileSelection.Selector -eq 'channel' -and $selectedMatch.Success -and $msrvMatch.Success) { - if ($selectedMatch.Groups[1].Value -eq $msrvMatch.Groups[1].Value -and - $selectedMatch.Groups[2].Value -eq $msrvMatch.Groups[2].Value) { - return $null - } - } if (-not [string]::IsNullOrWhiteSpace($env:ANVIL_MSRV_TOOLCHAIN)) { return [pscustomobject]@{ Value = $env:ANVIL_MSRV_TOOLCHAIN; Installable = $false; Mapped = $true } @@ -1374,22 +1362,22 @@ if ($ValidateWorkspaceMsrv) { exit 0 } -if ($MsrvCompatibilityToolchain -or $InstallMsrvCompatibilityIfNeeded) { - $compatibilitySelection = Get-MsrvCompatibilitySelection - if ($null -eq $compatibilitySelection) { +if ($MsrvToolchain -or $InstallMsrvIfNeeded) { + $msrvSelection = Get-MsrvSelection + if ($null -eq $msrvSelection) { exit 0 } - if ($InstallMsrvCompatibilityIfNeeded) { - if ($compatibilitySelection.Mapped) { - & cargo "+$($compatibilitySelection.Value)" --version *> $null + if ($InstallMsrvIfNeeded) { + if ($msrvSelection.Mapped) { + & cargo "+$($msrvSelection.Value)" --version *> $null if ($LASTEXITCODE -ne 0) { - throw "anvil: mapped MSRV toolchain '$($compatibilitySelection.Value)' is unavailable; provision ANVIL_MSRV_TOOLCHAIN or unset it to let Anvil install the declared public MSRV" + throw "anvil: mapped MSRV toolchain '$($msrvSelection.Value)' is unavailable; provision ANVIL_MSRV_TOOLCHAIN or unset it to let Anvil install the declared public MSRV" } } else { - Install-Toolchain $compatibilitySelection 'MSRV compatibility' + Install-Toolchain $msrvSelection 'MSRV' } } else { - $compatibilitySelection.Value + $msrvSelection.Value } exit 0 } @@ -2886,15 +2874,14 @@ anvil-miri-validate-prereqs: anvil-component-nightly-miri-validate-prereqs anvil # See https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/checks.md -# Run affected-package tests under the declared MSRV when a selecting root -# toolchain file chooses a different compiler. +# Run affected-package tests under the declared MSRV. [script("pwsh", "-NoProfile")] anvil-msrv-test: anvil-msrv-test-validate-prereqs $ErrorActionPreference = 'Stop' if ($env:ANVIL_INCLUDE_AFFECTED -eq '--skip') { exit 0 } - $toolchain = (& '.anvil/resolve-stable-toolchain.ps1' -MsrvCompatibilityToolchain | Out-String).Trim() + $toolchain = (& '.anvil/resolve-stable-toolchain.ps1' -MsrvToolchain | Out-String).Trim() if ([string]::IsNullOrWhiteSpace($toolchain)) { - Write-Host 'anvil-msrv-test: selected toolchain matches the MSRV; skipping' + Write-Host 'anvil-msrv-test: no root MSRV declared; skipping' exit 0 } $pkg = @(if ($env:ANVIL_INCLUDE_AFFECTED) { -split $env:ANVIL_INCLUDE_AFFECTED } else { '--workspace' }) @@ -2903,18 +2890,18 @@ anvil-msrv-test: anvil-msrv-test-validate-prereqs & cargo "+$toolchain" test @pkg --all-targets --locked if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } -# Install the MSRV compatibility toolchain only when this check is required. +# Install the declared MSRV toolchain only when this check is required. [group("anvil-setup")] [script("pwsh", "-NoProfile")] anvil-msrv-test-setup installer="install": - & '.anvil/resolve-stable-toolchain.ps1' -InstallMsrvCompatibilityIfNeeded + & '.anvil/resolve-stable-toolchain.ps1' -InstallMsrvIfNeeded if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } -# Validate the conditional MSRV compatibility prerequisite. +# Validate the MSRV prerequisite. [group("anvil-setup")] [script("pwsh", "-NoProfile")] anvil-msrv-test-validate-prereqs: - $toolchain = (& '.anvil/resolve-stable-toolchain.ps1' -MsrvCompatibilityToolchain | Out-String).Trim() + $toolchain = (& '.anvil/resolve-stable-toolchain.ps1' -MsrvToolchain | Out-String).Trim() if ([string]::IsNullOrWhiteSpace($toolchain)) { exit 0 } & cargo "+$toolchain" --version if ($LASTEXITCODE -ne 0) { @@ -2923,7 +2910,7 @@ anvil-msrv-test-validate-prereqs: } else { 'run: just anvil-msrv-test-setup' } - Write-Error "anvil: MSRV compatibility toolchain '$toolchain' is unavailable; $hint" + Write-Error "anvil: MSRV toolchain '$toolchain' is unavailable; $hint" exit $LASTEXITCODE } diff --git a/justfiles/anvil/checks/msrv-test.just b/justfiles/anvil/checks/msrv-test.just index 0e4889236..491701f50 100644 --- a/justfiles/anvil/checks/msrv-test.just +++ b/justfiles/anvil/checks/msrv-test.just @@ -6,15 +6,14 @@ # See https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/checks.md -# Run affected-package tests under the declared MSRV when a selecting root -# toolchain file chooses a different compiler. +# Run affected-package tests under the declared MSRV. [script("pwsh", "-NoProfile")] anvil-msrv-test: anvil-msrv-test-validate-prereqs $ErrorActionPreference = 'Stop' if ($env:ANVIL_INCLUDE_AFFECTED -eq '--skip') { exit 0 } - $toolchain = (& '.anvil/resolve-stable-toolchain.ps1' -MsrvCompatibilityToolchain | Out-String).Trim() + $toolchain = (& '.anvil/resolve-stable-toolchain.ps1' -MsrvToolchain | Out-String).Trim() if ([string]::IsNullOrWhiteSpace($toolchain)) { - Write-Host 'anvil-msrv-test: selected toolchain matches the MSRV; skipping' + Write-Host 'anvil-msrv-test: no root MSRV declared; skipping' exit 0 } $pkg = @(if ($env:ANVIL_INCLUDE_AFFECTED) { -split $env:ANVIL_INCLUDE_AFFECTED } else { '--workspace' }) @@ -23,18 +22,18 @@ anvil-msrv-test: anvil-msrv-test-validate-prereqs & cargo "+$toolchain" test @pkg --all-targets --locked if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } -# Install the MSRV compatibility toolchain only when this check is required. +# Install the declared MSRV toolchain only when this check is required. [group("anvil-setup")] [script("pwsh", "-NoProfile")] anvil-msrv-test-setup installer="install": - & '.anvil/resolve-stable-toolchain.ps1' -InstallMsrvCompatibilityIfNeeded + & '.anvil/resolve-stable-toolchain.ps1' -InstallMsrvIfNeeded if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } -# Validate the conditional MSRV compatibility prerequisite. +# Validate the MSRV prerequisite. [group("anvil-setup")] [script("pwsh", "-NoProfile")] anvil-msrv-test-validate-prereqs: - $toolchain = (& '.anvil/resolve-stable-toolchain.ps1' -MsrvCompatibilityToolchain | Out-String).Trim() + $toolchain = (& '.anvil/resolve-stable-toolchain.ps1' -MsrvToolchain | Out-String).Trim() if ([string]::IsNullOrWhiteSpace($toolchain)) { exit 0 } & cargo "+$toolchain" --version if ($LASTEXITCODE -ne 0) { @@ -43,6 +42,6 @@ anvil-msrv-test-validate-prereqs: } else { 'run: just anvil-msrv-test-setup' } - Write-Error "anvil: MSRV compatibility toolchain '$toolchain' is unavailable; $hint" + Write-Error "anvil: MSRV toolchain '$toolchain' is unavailable; $hint" exit $LASTEXITCODE } From 68f9ddc265527f6b7c8a3f4e2101d30b270ca50c Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Thu, 27 Aug 2026 19:30:59 +0200 Subject: [PATCH 04/32] test(cargo-anvil): accept PowerShell error streams Validate resolver diagnostics across stdout and stderr so the regression tests behave consistently across host PowerShell implementations. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/anvil/artifacts/justfile.rs | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/crates/cargo-anvil/src/anvil/artifacts/justfile.rs b/crates/cargo-anvil/src/anvil/artifacts/justfile.rs index a99d2d0dd..bf9f40913 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/justfile.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/justfile.rs @@ -534,6 +534,14 @@ mod tests { .to_owned() } + fn diagnostic(output: &Output) -> String { + format!( + "{}{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ) + } + #[test] fn honors_environment_files_and_msrv_in_precedence_order() { let temp = fixture("[workspace.package]\nrust-version = \"1.93\"\n"); @@ -575,7 +583,11 @@ mod tests { let missing = fixture("[workspace]\nresolver = \"2\"\n"); let output = run(missing.path(), &[], None); assert!(!output.status.success()); - assert!(String::from_utf8_lossy(&output.stderr).contains("no root [workspace.package]")); + assert!( + diagnostic(&output).contains("no root [workspace.package]"), + "unexpected resolver diagnostic: {}", + diagnostic(&output) + ); let heterogeneous = fixture("[workspace]\nresolver = \"2\"\nmembers = [\"a\", \"b\"]\n[workspace.package]\nrust-version = \"1.92\"\n"); @@ -594,7 +606,11 @@ mod tests { let output = run(heterogeneous.path(), &["-ValidateWorkspaceMsrv"], None); assert!(!output.status.success()); - assert!(String::from_utf8_lossy(&output.stderr).contains("must resolve to the root MSRV")); + assert!( + diagnostic(&output).contains("must resolve to the root MSRV"), + "unexpected resolver diagnostic: {}", + diagnostic(&output) + ); let output = run(heterogeneous.path(), &["-ValidateWorkspaceMsrv"], Some("explicit-toolchain")); assert!(output.status.success(), "explicit override must permit heterogeneous MSRVs"); From 51b4a53fa80a0836f489a494d51aaba0ab95e66a Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Thu, 27 Aug 2026 20:02:18 +0200 Subject: [PATCH 05/32] test(cargo-anvil): normalize resolver diagnostics Collapse PowerShell line wrapping before asserting resolver errors so the tests are portable across host terminal widths. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../cargo-anvil/src/anvil/artifacts/justfile.rs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/crates/cargo-anvil/src/anvil/artifacts/justfile.rs b/crates/cargo-anvil/src/anvil/artifacts/justfile.rs index bf9f40913..4b2560129 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/justfile.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/justfile.rs @@ -534,12 +534,15 @@ mod tests { .to_owned() } - fn diagnostic(output: &Output) -> String { + fn normalized_diagnostic(output: &Output) -> String { format!( "{}{}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr) ) + .split_whitespace() + .collect::>() + .join(" ") } #[test] @@ -583,10 +586,10 @@ mod tests { let missing = fixture("[workspace]\nresolver = \"2\"\n"); let output = run(missing.path(), &[], None); assert!(!output.status.success()); + let diagnostic = normalized_diagnostic(&output); assert!( - diagnostic(&output).contains("no root [workspace.package]"), - "unexpected resolver diagnostic: {}", - diagnostic(&output) + diagnostic.contains("no root [workspace.package]"), + "unexpected resolver diagnostic: {diagnostic}" ); let heterogeneous = @@ -606,10 +609,10 @@ mod tests { let output = run(heterogeneous.path(), &["-ValidateWorkspaceMsrv"], None); assert!(!output.status.success()); + let diagnostic = normalized_diagnostic(&output); assert!( - diagnostic(&output).contains("must resolve to the root MSRV"), - "unexpected resolver diagnostic: {}", - diagnostic(&output) + diagnostic.contains("must resolve to the root MSRV"), + "unexpected resolver diagnostic: {diagnostic}" ); let output = run(heterogeneous.path(), &["-ValidateWorkspaceMsrv"], Some("explicit-toolchain")); From b22d8f0909ea367830ba631260840c4606828d64 Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Thu, 27 Aug 2026 20:38:32 +0200 Subject: [PATCH 06/32] test(cargo-anvil): ignore PowerShell line markers Assert stable resolver diagnostic fragments independently so PowerShell continuation markers do not make the tests host-dependent. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/cargo-anvil/src/anvil/artifacts/justfile.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/cargo-anvil/src/anvil/artifacts/justfile.rs b/crates/cargo-anvil/src/anvil/artifacts/justfile.rs index 4b2560129..3b807b0fa 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/justfile.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/justfile.rs @@ -588,7 +588,7 @@ mod tests { assert!(!output.status.success()); let diagnostic = normalized_diagnostic(&output); assert!( - diagnostic.contains("no root [workspace.package]"), + diagnostic.contains("no root") && diagnostic.contains("[workspace.package]"), "unexpected resolver diagnostic: {diagnostic}" ); @@ -611,7 +611,7 @@ mod tests { assert!(!output.status.success()); let diagnostic = normalized_diagnostic(&output); assert!( - diagnostic.contains("must resolve to the root MSRV"), + diagnostic.contains("must resolve to the root") && diagnostic.contains("MSRV"), "unexpected resolver diagnostic: {diagnostic}" ); From 03db9568f75535da6cc2d0f3e4455f7481a7d3fa Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Fri, 28 Aug 2026 11:41:17 +0200 Subject: [PATCH 07/32] test(cargo-anvil): isolate impact fixtures from CI toolchain state Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/cargo-anvil/tests/impact.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/cargo-anvil/tests/impact.rs b/crates/cargo-anvil/tests/impact.rs index 0d267dd79..d32471a33 100644 --- a/crates/cargo-anvil/tests/impact.rs +++ b/crates/cargo-anvil/tests/impact.rs @@ -245,6 +245,10 @@ fn workspace() -> TempDir { /// `GITHUB_BASE_REF=main`); inherited, `_anvil-base-ref` would resolve /// `origin/main`, which does not exist in the temp repo (whose base is /// `origin/master`), and the snapshot would fail base-ref resolution. +/// - `ANVIL_STABLE_TOOLCHAIN_SOURCE`: cloud setup sets this for the outer +/// repository; inherited, resolver validation would treat the fixture's +/// toolchain as its own resolved selection and invoke intentionally fake +/// `cargo metadata` shims used by impact tests. /// /// Tests that need a specific mode or base set the var back on the returned /// Command. @@ -256,6 +260,7 @@ fn just_cmd(root: &Path, args: &[&str]) -> Command { .env_remove("BASE_REF") .env_remove("GITHUB_BASE_REF") .env_remove("SYSTEM_PULLREQUEST_TARGETBRANCH") + .env_remove("ANVIL_STABLE_TOOLCHAIN_SOURCE") .current_dir(root); cmd } From 25628f27cd55da5148ff3ea6f01c2147af08ee28 Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Fri, 28 Aug 2026 11:50:49 +0200 Subject: [PATCH 08/32] fix(cargo-anvil): complete deterministic toolchain adoption Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .anvil.lock | 8 +-- .anvil/resolve-stable-toolchain.ps1 | 12 +++- crates/cargo-anvil/README.md | 6 +- crates/cargo-anvil/docs/design/README.md | 5 +- crates/cargo-anvil/docs/design/ado.md | 21 ++++++- crates/cargo-anvil/docs/design/checks.md | 12 ++-- crates/cargo-anvil/docs/design/github.md | 5 ++ crates/cargo-anvil/docs/design/local.md | 48 +++++++++----- .../src/anvil/artifacts/justfile.rs | 26 +++++++- crates/cargo-anvil/src/anvil/artifacts/mod.rs | 1 + crates/cargo-anvil/src/lib.rs | 4 +- .../anvil/resolve-stable-toolchain.ps1 | 12 +++- .../justfiles/anvil/groups/pr-msrv.just | 8 ++- .../justfiles/anvil/groups/pr-slow.just | 22 +++---- crates/cargo-anvil/tests/impact.rs | 63 +++++++++++++++++++ crates/cargo-anvil/tests/schemas.rs | 1 + .../snapshots/snapshots__ado_backend.snap | 42 ++++++++----- .../snapshots/snapshots__github_backend.snap | 42 ++++++++----- .../snapshots/snapshots__local_only.snap | 42 ++++++++----- justfiles/anvil/groups/pr-msrv.just | 8 ++- justfiles/anvil/groups/pr-slow.just | 22 +++---- 21 files changed, 297 insertions(+), 113 deletions(-) diff --git a/.anvil.lock b/.anvil.lock index e8b36e85b..c6f298237 100644 --- a/.anvil.lock +++ b/.anvil.lock @@ -1,7 +1,7 @@ version = 1 tool = "anvil" tool_version = "0.5.0" -catalog_checksum = "sha256:759dc289290a5dafba0ab616bb90f97e6e2957fc856faac7a43cd3af7270a8c9" +catalog_checksum = "sha256:4734440a19e16d719afbd7a3f9a5686990a62df293c254e20afb3108eb205a82" [[file]] path = ".anvil/container/Containerfile" @@ -37,7 +37,7 @@ checksum = "sha256:0f4fcf0fef43e4736260af771cae15b7eb8afc382e5290fabddc52894b091 [[file]] path = ".anvil/resolve-stable-toolchain.ps1" -checksum = "sha256:cdfef61746d5b02c33aed33acbca9cd78cc0c62319deb4768ecae8c438d2eeea" +checksum = "sha256:c6dc26c63300378994b9c375ff1b026323b7f5706e8b65ee68b77f4cd670e4b8" [[file]] path = ".github/actions/anvil-impact/action.yml" @@ -209,7 +209,7 @@ checksum = "sha256:29a219b7d8b2eaa605b0444838f05ffc7538ab8bccc43d1613c555f7562a7 [[file]] path = "justfiles/anvil/groups/pr-msrv.just" -checksum = "sha256:72bba15ad27f857d6158151c758c10b6ae0989b70cb63f025060dbbc9eae6e24" +checksum = "sha256:5c862a1c7775742377e75d6848650070d11e9ce3facd9ab304b03dbca1c4b1d2" [[file]] path = "justfiles/anvil/groups/pr-mutants.just" @@ -221,7 +221,7 @@ checksum = "sha256:9a94b6ae2d212f91d324e401ebdb2238805f3450d95d4d45df791d7f1679a [[file]] path = "justfiles/anvil/groups/pr-slow.just" -checksum = "sha256:24d52b06002166a8ba7ebca5a2c5c7c2db5de83867709b10cfbe58d95a39aab6" +checksum = "sha256:5d8bb4b73ce14ec76477f90b754c47904054e1497471a21d3d84b0ee6a4676d0" [[file]] path = "justfiles/anvil/groups/pr-test.just" diff --git a/.anvil/resolve-stable-toolchain.ps1 b/.anvil/resolve-stable-toolchain.ps1 index 09ced2d82..a2c3fe460 100644 --- a/.anvil/resolve-stable-toolchain.ps1 +++ b/.anvil/resolve-stable-toolchain.ps1 @@ -27,13 +27,21 @@ function Get-RootToolchainFilePath { return $null } +function Read-RootToolchainFile([string] $path) { + $content = Get-Content -LiteralPath $path -Raw + if ([string]::IsNullOrWhiteSpace($content)) { + throw "anvil: root toolchain file '$path' is empty; remove it or declare a toolchain configuration" + } + return $content +} + function Get-SelectingToolchainFile { $path = Get-RootToolchainFilePath if ($null -eq $path) { return $null } - $content = Get-Content -LiteralPath $path -Raw + $content = Read-RootToolchainFile $path if ((Split-Path -Leaf $path) -eq 'rust-toolchain' -and $content.TrimStart() -notmatch '^\[') { return [pscustomobject]@{ Path = $path @@ -77,7 +85,7 @@ function Get-ToolchainFileOptions { return $null } - $content = Get-Content -LiteralPath $path -Raw + $content = Read-RootToolchainFile $path $section = [regex]::Match( $content, '(?ms)^\s*\[\s*toolchain\s*\]\s*(?:#.*)?$(.*?)(?=^\s*\[|\z)' diff --git a/crates/cargo-anvil/README.md b/crates/cargo-anvil/README.md index bff74e6d0..05a93117f 100644 --- a/crates/cargo-anvil/README.md +++ b/crates/cargo-anvil/README.md @@ -230,8 +230,8 @@ the tables below map each check to the group that runs it, link each check to its tool’s documentation, and note anything anvil-specific. **PR tier** (`anvil-pr`) — runs on every pull request, impact-scoped -both locally and in cloud workflows. Two jobs: `pr-fast`, and `pr-slow` (whose three -sub-groups run sequentially within the one job per OS leg): +both locally and in cloud workflows. `pr-fast` is one job, while the four +`pr-slow` sub-groups run as independent parallel jobs per OS leg: @@ -447,7 +447,7 @@ And `docs/verification.md` for the continuous-validation strategy. This crate was developed as part of The Oxidizer Project. Browse this crate's source code. - [__cargo_doc2readme_dependencies_info]: ggGmYW0CYXZlMC43LjJhdIQbFhzZ8rzWNNYbuRaDSGWynFgbH4PMdoT7GNcbVwNPtPjAhvFhYvRhcoQb-MEFS3pGGcQbqNVu-9TrDKobUimRF_G2c6kbXioUYVt3GTlhZIGDa2NhcmdvLWFudmlsZTAuNS4wa2NhcmdvX2Fudmls + [__cargo_doc2readme_dependencies_info]: ggGmYW0CYXZlMC43LjJhdIQbFhzZ8rzWNNYbuRaDSGWynFgbH4PMdoT7GNcbVwNPtPjAhvFhYvRhcoQbe0CEzvQJlR4bRkLTRoqTA-4bEZiGbyy-O1EbNOkZ-Q2xT4dhZIGDa2NhcmdvLWFudmlsZTAuNS4wa2NhcmdvX2Fudmls [__link0]: https://crates.io/crates/cargo-delta [__link1]: https://crates.io/crates/cargo-spellcheck [__link2]: https://crates.io/crates/cargo-coverage-gate diff --git a/crates/cargo-anvil/docs/design/README.md b/crates/cargo-anvil/docs/design/README.md index 43b0af6b5..e034b62c6 100644 --- a/crates/cargo-anvil/docs/design/README.md +++ b/crates/cargo-anvil/docs/design/README.md @@ -237,7 +237,8 @@ repo/ ├── .anvil.lock sidecar manifest tracking last-rendered checksums (see updates.md) ├── Justfile managed-region: anvil-imports ├── justfiles/anvil/ owned (see local.md) -├── .anvil/container/ owned, only with the container backend (see local.md, containers.md) +├── .anvil/resolve-stable-toolchain.ps1 owned, unconditional bootstrap (see local.md) +├── .anvil/container/ owned, optional container assets (see containers.md) ├── Cargo.toml managed-region: anvil-workspace-lints (or anvil-lints in single-crate) ├── crates//Cargo.toml managed-region: anvil-lints (one per workspace member) ├── deny.toml managed-regions: anvil-deny-{advisories,licenses,bans,sources} @@ -268,6 +269,8 @@ repo/ Detail on each host: - **`Justfile` and `justfiles/anvil/*.just`** — see [local.md](./local.md). +- **`.anvil/resolve-stable-toolchain.ps1`** — the unconditional generated + bootstrap shared by parse-time recipe variables and cloud/container setup. - **`.anvil/container/`** — the non-recipe container assets (Containerfile, drivers, image-ID helpers, README) emitted only when the catalog includes the optional container backend. `justfiles/` holds `.just` recipes and nothing diff --git a/crates/cargo-anvil/docs/design/ado.md b/crates/cargo-anvil/docs/design/ado.md index b60b03162..af236beda 100644 --- a/crates/cargo-anvil/docs/design/ado.md +++ b/crates/cargo-anvil/docs/design/ado.md @@ -50,16 +50,19 @@ flowchart LR impact_s["stage: impact_linux + stage: impact_windows
(2 stages;
outputs consumed by every group below)"]:::stage pr_fast_s["stage: pr_fast
linux + windows jobs"]:::stage pr_test_s["stage: pr_test
linux + windows jobs"]:::stage + pr_msrv_s["stage: pr_msrv
linux + windows jobs"]:::stage pr_runtime_analysis_s["stage: pr_runtime_analysis
linux + windows jobs"]:::stage pr_mutants_s["stage: pr_mutants
linux + windows jobs"]:::stage impact_step[".pipelines/anvil/
steps/impact.yml"]:::step impact_setup[".pipelines/anvil/
steps/setup.yml"]:::step fast_setup[".pipelines/anvil/
steps/setup.yml"]:::step test_setup[".pipelines/anvil/
steps/setup.yml"]:::step + msrv_setup[".pipelines/anvil/
steps/setup.yml"]:::step runtime_setup[".pipelines/anvil/
steps/setup.yml"]:::step mutants_setup[".pipelines/anvil/
steps/setup.yml"]:::step fast_step[".pipelines/anvil/
steps/pr-fast.yml"]:::step test_step[".pipelines/anvil/
steps/pr-test.yml"]:::step + msrv_step[".pipelines/anvil/
steps/pr-msrv.yml"]:::step runtime_step[".pipelines/anvil/
steps/pr-runtime-analysis.yml"]:::step mutants_step[".pipelines/anvil/
steps/pr-mutants.yml"]:::step publish_coverage["PublishCodeCoverageResults@2"]:::external @@ -68,7 +71,9 @@ flowchart LR impact_just["just anvil-impact"]:::recipe impact_setup_just["just anvil-setup"]:::recipe test_just["just anvil-pr-test"]:::recipe + msrv_just["just anvil-pr-msrv"]:::recipe test_setup_just["just anvil-setup"]:::recipe + msrv_setup_just["just anvil-setup"]:::recipe runtime_just["just anvil-pr-runtime-analysis"]:::recipe runtime_setup_just["just anvil-setup"]:::recipe mutants_just["just anvil-pr-mutants"]:::recipe @@ -79,12 +84,14 @@ flowchart LR pr_stages --> impact_s pr_stages --> pr_fast_s pr_stages --> pr_test_s + pr_stages --> pr_msrv_s pr_stages --> pr_runtime_analysis_s pr_stages --> pr_mutants_s impact_s ==> impact_step pr_fast_s ==> fast_step pr_test_s ==> test_step + pr_msrv_s ==> msrv_step pr_test_s ==> publish_coverage pr_runtime_analysis_s ==> runtime_step pr_mutants_s ==> mutants_step @@ -95,6 +102,8 @@ flowchart LR fast_step ==> fast_just test_step ==> test_setup test_step ==> test_just + msrv_step ==> msrv_setup + msrv_step ==> msrv_just runtime_step ==> runtime_setup runtime_step ==> runtime_just mutants_step ==> mutants_setup @@ -103,6 +112,7 @@ flowchart LR impact_setup ==> impact_setup_just fast_setup ==> fast_setup_just test_setup ==> test_setup_just + msrv_setup ==> msrv_setup_just runtime_setup ==> runtime_setup_just mutants_setup ==> mutants_setup_just @@ -115,7 +125,10 @@ flowchart LR classDef recipe fill:#f3e8ff,stroke:#6f42c1,stroke-width:1px; ``` -(Every job in `pr_fast`, `pr_test`, `pr_runtime_analysis`, and `pr_mutants` is rendered through the per-job wrapper at `steps/job.yml`; that uniform indirection is elided from the diagram. See §4.1 for the wrapper's role as a 1ESPT extensibility point.) +(Every job in `pr_fast`, `pr_test`, `pr_msrv`, `pr_runtime_analysis`, and +`pr_mutants` is rendered through the per-job wrapper at `steps/job.yml`; that +uniform indirection is elided from the diagram. See §4.1 for the wrapper's role +as a 1ESPT extensibility point.) The scheduled pipeline (same colour key): @@ -210,6 +223,7 @@ Note the ADO topology differs from GitHub Actions in two places: │ `templateContext:` etc.) ├── pr-fast.yml owned (one step template per group) ├── pr-test.yml owned + ├── pr-msrv.yml owned ├── pr-runtime-analysis.yml owned ├── pr-mutants.yml owned ├── scheduled-test.yml owned @@ -356,8 +370,9 @@ wiring layer's. This means moving a check between groups (e.g. `clippy` from `pr The PR template also contains a `pr_msrv` stage. It runs in parallel with `pr_test`, `pr_runtime_analysis`, and `pr_mutants`, uses the same Linux and Windows x86_64 jobs and per-OS affected-package impact sets as `pr_test`, and invokes -`anvil-pr-msrv`. The Linux impact job publishes whether the root manifest declares -an MSRV so ADO skips the stage before setup when no run is needed. +`anvil-pr-msrv`. The stage consumes the per-OS impact artifact like the other PR +groups; when the root manifest declares no MSRV, the recipe exits successfully +without running tests. Internal pipelines set `ANVIL_MSRV_TOOLCHAIN` to the provisioned `ms-prod-*` equivalent of that MSRV; public pipelines let Anvil install the declared MSRV through rustup. diff --git a/crates/cargo-anvil/docs/design/checks.md b/crates/cargo-anvil/docs/design/checks.md index 50dd19f67..719cc9c69 100644 --- a/crates/cargo-anvil/docs/design/checks.md +++ b/crates/cargo-anvil/docs/design/checks.md @@ -208,14 +208,16 @@ When the root manifest declares an MSRV, Anvil runs affected-package unit and integration tests under that compiler with both all features and default features. This is the ordinary test-suite execution at the minimum supported compiler; `pr-test` runs the same affected suite through coverage instrumentation on the -catalog nightly. A selecting toolchain file does not suppress the MSRV run, even -when it selects the same compiler, because the MSRV group is the authoritative -minimum-version test result. +catalog nightly. Other checks that use the selected stable compiler do not execute +this unit/integration suite, so an MSRV fallback does not make `pr-msrv` a duplicate. +A selecting toolchain file does not suppress the MSRV run, even when it selects the +same compiler, because the MSRV group is the authoritative minimum-version test +result. The group uses the same OS/architecture matrix and per-OS impact sets as `pr-test` so cfg-gated targets and dependencies are exercised under the MSRV. It runs in -parallel with the other PR groups. Impact preparation publishes whether a root MSRV -exists, allowing the cloud workflow to skip the group before setup when it does not. +parallel with the other PR groups. When no root MSRV exists, the recipe exits +successfully without running tests. `ANVIL_MSRV_TOOLCHAIN` may map the public MSRV to an already-provisioned internal toolchain; when it is unset, setup installs the declared MSRV through rustup. diff --git a/crates/cargo-anvil/docs/design/github.md b/crates/cargo-anvil/docs/design/github.md index 9594b5ea0..ba1a4120b 100644 --- a/crates/cargo-anvil/docs/design/github.md +++ b/crates/cargo-anvil/docs/design/github.md @@ -59,6 +59,7 @@ flowchart LR impact["impact-linux + impact-windows
(2 jobs;
outputs consumed by every group below)"]:::job pr_fast_job["pr-fast
matrix: linux, windows,
linux-arm, windows-arm"]:::job pr_test_job["pr-test
matrix: linux, windows,
linux-arm, windows-arm"]:::job + pr_msrv_job["pr-msrv
matrix: linux, windows,
linux-arm, windows-arm"]:::job pr_runtime_analysis_job["pr-runtime-analysis
matrix: linux, windows,
linux-arm, windows-arm"]:::job pr_mutants_job["pr-mutants
matrix: linux, windows,
linux-arm, windows-arm"]:::job impact_act[".github/actions/
anvil-impact"]:::action @@ -68,6 +69,7 @@ flowchart LR impact_just["just anvil-impact"]:::recipe fast_just["just anvil-pr-fast"]:::recipe test_just["just anvil-pr-test"]:::recipe + msrv_just["just anvil-pr-msrv"]:::recipe runtime_just["just anvil-pr-runtime-analysis"]:::recipe mutants_just["just anvil-pr-mutants"]:::recipe setup_just["just anvil-<group>-setup"]:::recipe @@ -77,12 +79,14 @@ flowchart LR pr_impl --> impact pr_impl --> pr_fast_job pr_impl --> pr_test_job + pr_impl --> pr_msrv_job pr_impl --> pr_runtime_analysis_job pr_impl --> pr_mutants_job impact ==> impact_act pr_fast_job ==> run_group_act pr_test_job ==> run_group_act + pr_msrv_job ==> run_group_act pr_test_job ==> codecov_act pr_runtime_analysis_job ==> run_group_act pr_mutants_job ==> run_group_act @@ -92,6 +96,7 @@ flowchart LR run_group_act ==> setup_act run_group_act ==> fast_just run_group_act ==> test_just + run_group_act ==> msrv_just run_group_act ==> runtime_just run_group_act ==> mutants_just setup_act ==> setup_just diff --git a/crates/cargo-anvil/docs/design/local.md b/crates/cargo-anvil/docs/design/local.md index f5e15d791..e402fa060 100644 --- a/crates/cargo-anvil/docs/design/local.md +++ b/crates/cargo-anvil/docs/design/local.md @@ -43,9 +43,9 @@ repo/ │ ├── groups/ one file per group: groups/.just holds the │ │ `anvil-` recipe plus its `*-setup` / │ │ `*-validate-prereqs` (anvil-pr-fast, anvil-pr-test, -│ │ anvil-pr-runtime-analysis, anvil-pr-mutants, +│ │ anvil-pr-msrv, anvil-pr-runtime-analysis, anvil-pr-mutants, │ │ anvil-scheduled-test, …). `anvil-pr-slow` is a -│ │ convenience umbrella over the three pr-slow sub-groups. +│ │ convenience umbrella over the four pr-slow sub-groups. │ ├── container.just optional container entry recipe (`anvil-container`). │ ├── tiers.just tier aggregators (anvil-pr, anvil-scheduled, anvil-full). │ ├── tools.just tool/component/toolchain install + validate-prereqs recipes, @@ -55,24 +55,28 @@ repo/ │ as plain just variables (rust_nightly, cargo_nextest_version, …). │ Read by recipes via `{{ var }}` interpolation. See §3. │ -└── .anvil/container/ optional non-recipe container assets - ├── Containerfile - ├── Containerfile.dockerignore - ├── README.md - ├── entrypoint.sh - ├── image-id.ps1 - ├── image-id.sh - ├── run-in-container.ps1 - └── run-in-container.sh +└── .anvil/ + ├── resolve-stable-toolchain.ps1 unconditional toolchain bootstrap + └── container/ optional non-recipe container assets + ├── Containerfile + ├── Containerfile.dockerignore + ├── README.md + ├── entrypoint.sh + ├── image-id.ps1 + ├── image-id.sh + ├── run-in-container.ps1 + └── run-in-container.sh ``` The Justfile region is the only file anvil adds to that the user co-owns, and it's -a single `import` line. Generated recipes live inside `justfiles/anvil/`; optional -non-recipe container assets live inside `.anvil/container/`. Generated files in -both directories are tool-owned (tracked by full-file checksum in the sidecar -manifest). If the user wants to add project-specific recipes, they add them to -the top-level `Justfile` outside the managed region, or to their own additional -imported `.just` files. The alias `anvil := anvil-pr` lives in `mod.just`, not in +a single `import` line. Generated recipes live inside `justfiles/anvil/`; the +unconditional stable-toolchain bootstrap lives at +`.anvil/resolve-stable-toolchain.ps1`; optional non-recipe container assets live +inside `.anvil/container/`. Generated files in these directories are tool-owned +(tracked by full-file checksum in the sidecar manifest). If the user wants to add +project-specific recipes, they add them to the top-level `Justfile` outside the +managed region, or to their own additional imported `.just` files. The alias +`anvil := anvil-pr` lives in `mod.just`, not in the user's `Justfile`, so renaming or retargeting the alias is a template update with no managed-region churn. @@ -387,6 +391,16 @@ There is no runner-default fallback. A repository with neither a selecting toolc file nor a root MSRV fails with an actionable setup error rather than inheriting a compiler that can change with the machine image. +This is a breaking migration for adopters that previously relied on the runner's +ambient stable compiler. Before regenerating, a repository with one compatibility +floor should declare root `[workspace.package].rust-version` (or package +`rust-version`) and have every workspace member inherit or declare that same value. +A repository with missing or heterogeneous package MSRVs must instead add a root +toolchain file with `channel` or `path` to select the single compiler used by catalog +checks. Internal builds may set `RUSTUP_TOOLCHAIN` to a provisioned toolchain rather +than adding a public compiler pin. An empty root toolchain file is invalid and produces +an actionable error. + The generated `.anvil/resolve-stable-toolchain.ps1` implements this contract. For an existing environment override or MSRV fallback, the recipe tree exports the result as `RUSTUP_TOOLCHAIN`. For a selecting toolchain file it leaves that diff --git a/crates/cargo-anvil/src/anvil/artifacts/justfile.rs b/crates/cargo-anvil/src/anvil/artifacts/justfile.rs index 02f560433..a6643e5be 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/justfile.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/justfile.rs @@ -636,12 +636,13 @@ mod tests { // when it (re)computes the impact set. The group's setup + // validate-prereqs must therefore install / verify cargo-delta, so a // missing tool fails fast at setup rather than mid-run. (pr-slow is an - // umbrella and inherits this via pr-test / pr-runtime-analysis / - // pr-mutants.) Scheduled groups force ANVIL_IMPACT=off and never + // umbrella and inherits this via pr-test / pr-msrv / + // pr-runtime-analysis / pr-mutants.) Scheduled groups force + // ANVIL_IMPACT=off and never // recompute the impact set, so they deliberately do NOT depend on // cargo-delta. let groups = all_group_bodies(); - for g in ["pr-fast", "pr-test", "pr-runtime-analysis", "pr-mutants"] { + for g in ["pr-fast", "pr-test", "pr-msrv", "pr-runtime-analysis", "pr-mutants"] { assert!( groups.contains(&format!( "anvil-{g}-setup installer=\"install\": \\\n (anvil-tool-cargo-delta-install installer)" @@ -862,6 +863,25 @@ mod tests { ); } + #[test] + fn rejects_empty_legacy_toolchain_files_with_an_actionable_error() { + for contents in ["", " \r\n\t"] { + let temp = fixture("[workspace.package]\nrust-version = \"1.93\"\n"); + fs::write(temp.path().join("rust-toolchain"), contents).expect("toolchain fixture must be writable"); + + let output = run(temp.path(), &[], None); + assert!(!output.status.success(), "empty toolchain file must fail resolution"); + let diagnostic = normalized_diagnostic(&output); + assert!( + diagnostic.contains("root toolchain file") + && diagnostic.contains("is empty") + && diagnostic.contains("remove it") + && diagnostic.contains("declare a toolchain configuration"), + "unexpected resolver diagnostic: {diagnostic}" + ); + } + } + #[test] fn reads_multiline_toolchain_file_install_options() { let temp = fixture("[workspace.package]\nrust-version = \"1.93\"\n"); diff --git a/crates/cargo-anvil/src/anvil/artifacts/mod.rs b/crates/cargo-anvil/src/anvil/artifacts/mod.rs index 30fc0bb12..6b73917d9 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/mod.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/mod.rs @@ -102,6 +102,7 @@ mod tests { #[test] fn impact_mode_classifies_pr_and_scheduled_groups() { assert_eq!(impact_mode("pr-fast"), "consume"); + assert_eq!(impact_mode("pr-msrv"), "consume"); assert_eq!(impact_mode("pr-mutants"), "consume"); assert_eq!(impact_mode("scheduled-test"), "off"); assert_eq!(impact_mode("scheduled-exhaustive"), "off"); diff --git a/crates/cargo-anvil/src/lib.rs b/crates/cargo-anvil/src/lib.rs index ea86bad17..a8853f070 100644 --- a/crates/cargo-anvil/src/lib.rs +++ b/crates/cargo-anvil/src/lib.rs @@ -231,8 +231,8 @@ //! check to its tool's documentation, and note anything anvil-specific. //! //! **PR tier** (`anvil-pr`) — runs on every pull request, impact-scoped -//! both locally and in cloud workflows. Two jobs: `pr-fast`, and `pr-slow` (whose three -//! sub-groups run sequentially within the one job per OS leg): +//! both locally and in cloud workflows. `pr-fast` is one job, while the four +//! `pr-slow` sub-groups run as independent parallel jobs per OS leg: //! //!
JobSub-groupCheckNotes
//! diff --git a/crates/cargo-anvil/templates/anvil/resolve-stable-toolchain.ps1 b/crates/cargo-anvil/templates/anvil/resolve-stable-toolchain.ps1 index 09ced2d82..a2c3fe460 100644 --- a/crates/cargo-anvil/templates/anvil/resolve-stable-toolchain.ps1 +++ b/crates/cargo-anvil/templates/anvil/resolve-stable-toolchain.ps1 @@ -27,13 +27,21 @@ function Get-RootToolchainFilePath { return $null } +function Read-RootToolchainFile([string] $path) { + $content = Get-Content -LiteralPath $path -Raw + if ([string]::IsNullOrWhiteSpace($content)) { + throw "anvil: root toolchain file '$path' is empty; remove it or declare a toolchain configuration" + } + return $content +} + function Get-SelectingToolchainFile { $path = Get-RootToolchainFilePath if ($null -eq $path) { return $null } - $content = Get-Content -LiteralPath $path -Raw + $content = Read-RootToolchainFile $path if ((Split-Path -Leaf $path) -eq 'rust-toolchain' -and $content.TrimStart() -notmatch '^\[') { return [pscustomobject]@{ Path = $path @@ -77,7 +85,7 @@ function Get-ToolchainFileOptions { return $null } - $content = Get-Content -LiteralPath $path -Raw + $content = Read-RootToolchainFile $path $section = [regex]::Match( $content, '(?ms)^\s*\[\s*toolchain\s*\]\s*(?:#.*)?$(.*?)(?=^\s*\[|\z)' diff --git a/crates/cargo-anvil/templates/justfiles/anvil/groups/pr-msrv.just b/crates/cargo-anvil/templates/justfiles/anvil/groups/pr-msrv.just index 1c25aa3f9..d35d01ac8 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/groups/pr-msrv.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/groups/pr-msrv.just @@ -12,8 +12,12 @@ anvil-pr-msrv: anvil-pr-msrv-validate-prereqs anvil-msrv-test # Install prerequisites for the `anvil-pr-msrv` recipe. [group("anvil-setup")] -anvil-pr-msrv-setup installer="install": (anvil-msrv-test-setup installer) +anvil-pr-msrv-setup installer="install": \ + (anvil-tool-cargo-delta-install installer) \ + (anvil-msrv-test-setup installer) # Validate prerequisites for the `anvil-pr-msrv` recipe. [group("anvil-setup")] -anvil-pr-msrv-validate-prereqs: anvil-msrv-test-validate-prereqs +anvil-pr-msrv-validate-prereqs: \ + anvil-tool-cargo-delta-validate-prereqs \ + anvil-msrv-test-validate-prereqs diff --git a/crates/cargo-anvil/templates/justfiles/anvil/groups/pr-slow.just b/crates/cargo-anvil/templates/justfiles/anvil/groups/pr-slow.just index f063a49ec..3ac2c01f5 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/groups/pr-slow.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/groups/pr-slow.just @@ -6,21 +6,21 @@ # See https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/checks.md -# pr-slow is the single PR-tier group for everything that takes more -# than ~30s per crate (tests, stricter runtimes, mutation testing). -# It's internally split into four sub-recipes so individual concerns -# can be invoked locally without dragging the others along: +# pr-slow is the local umbrella for everything that takes more than ~30s +# per crate (tests, MSRV tests, stricter runtimes, mutation testing). It's +# split into four sub-recipes so individual concerns can be invoked locally +# without dragging the others along: # -# slow1: tests + coverage (replaces the former pr-test group) -# slow2: stricter-runtime correctness (miri, careful) -# slow3: mutation testing +# pr-test: tests + coverage +# pr-msrv: unit and integration tests under the declared MSRV +# pr-runtime-analysis: stricter-runtime correctness (miri, careful) +# pr-mutants: mutation testing # -# Cloud workflows run pr-slow as ONE job per OS leg -- the sub-recipes run -# sequentially within. This trades per-leg wall-clock for fewer -# orchestration jobs and a flatter PR check graph. Individual sub- -# recipes are runnable on their own locally: +# Cloud workflows run the four sub-groups as independent parallel jobs. +# The umbrella runs them sequentially only when invoked locally: # # $ just anvil-pr-test # tests + coverage only +# $ just anvil-pr-msrv # MSRV unit and integration tests only # $ just anvil-pr-runtime-analysis # miri + careful only # $ just anvil-pr-mutants # mutants only diff --git a/crates/cargo-anvil/tests/impact.rs b/crates/cargo-anvil/tests/impact.rs index d32471a33..9cec814d8 100644 --- a/crates/cargo-anvil/tests/impact.rs +++ b/crates/cargo-anvil/tests/impact.rs @@ -980,6 +980,69 @@ fn scoped_check_consumes_cached_package_list_and_skips_on_sentinel() { ); } +#[test] +fn msrv_test_uses_affected_packages_for_both_feature_modes_and_skips_without_msrv() { + if !tools_available() { + return; + } + let tmp = workspace(); + let root = tmp.path(); + run_impact(root); + let affected = fs::read_to_string(root.join("target/anvil/impact/include_affected.txt")) + .unwrap() + .trim() + .to_owned(); + + let bin = root.join(".fakebin"); + let log = root.join("cargo-argv.log"); + fake_cargo(&bin, &log); + let path = path_with_prefix(&bin); + let run_msrv = || { + just_cmd(root, &["anvil-msrv-test"]) + .env("ANVIL_IMPACT", "consume") + .env("ANVIL_STABLE_TOOLCHAIN_SOURCE", "environment") + .env("RUSTUP_TOOLCHAIN", "test-stable") + .env("PATH", &path) + .output() + .unwrap() + }; + + let output = run_msrv(); + let combined = format!( + "{}{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + assert!(output.status.success(), "anvil-msrv-test failed:\n{combined}"); + let argv = fs::read_to_string(&log).unwrap(); + for expected in [ + format!("+1.97 test {affected} --all-targets --all-features --locked"), + format!("+1.97 test {affected} --all-targets --locked"), + ] { + assert!(argv.contains(&expected), "missing MSRV invocation '{expected}' in:\n{argv}"); + } + + let manifest = fs::read_to_string(root.join("Cargo.toml")).unwrap(); + fs::write( + root.join("Cargo.toml"), + manifest.replace("\n[workspace.package]\nrust-version = \"1.97\"\n", "\n"), + ) + .unwrap(); + fs::write(&log, "").unwrap(); + + let skipped = run_msrv(); + let skip_combined = format!( + "{}{}", + String::from_utf8_lossy(&skipped.stdout), + String::from_utf8_lossy(&skipped.stderr) + ); + assert!(skipped.status.success(), "no-MSRV invocation failed:\n{skip_combined}"); + assert!( + fs::read_to_string(&log).unwrap().is_empty(), + "no-MSRV invocation must skip before calling cargo" + ); +} + /// Write a fake `cargo` into `dir` that answers `cargo metadata …` by printing /// the contents of `metadata_json` and captures every other invocation's argv /// (one per line) to `log`, exiting 0. Lets `anvil-loom` be driven with a diff --git a/crates/cargo-anvil/tests/schemas.rs b/crates/cargo-anvil/tests/schemas.rs index 83580c5a3..2cb084555 100644 --- a/crates/cargo-anvil/tests/schemas.rs +++ b/crates/cargo-anvil/tests/schemas.rs @@ -134,6 +134,7 @@ fn just_lists_emitted_recipes() { ), ("anvil-pr", "# Run all pull request checks."), ("anvil-pr-fast", "# Run the fast pull request checks."), + ("anvil-pr-msrv", "# Run pull request compatibility tests under the declared MSRV."), ("anvil-pr-mutants", "# Run the pull request mutation tests."), ("anvil-pr-runtime-analysis", "# Run the pull request runtime analysis."), ("anvil-pr-slow", "# Run the slow pull request checks."), diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap index 953665941..8255a0076 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap @@ -1225,13 +1225,21 @@ function Get-RootToolchainFilePath { return $null } +function Read-RootToolchainFile([string] $path) { + $content = Get-Content -LiteralPath $path -Raw + if ([string]::IsNullOrWhiteSpace($content)) { + throw "anvil: root toolchain file '$path' is empty; remove it or declare a toolchain configuration" + } + return $content +} + function Get-SelectingToolchainFile { $path = Get-RootToolchainFilePath if ($null -eq $path) { return $null } - $content = Get-Content -LiteralPath $path -Raw + $content = Read-RootToolchainFile $path if ((Split-Path -Leaf $path) -eq 'rust-toolchain' -and $content.TrimStart() -notmatch '^\[') { return [pscustomobject]@{ Path = $path @@ -1275,7 +1283,7 @@ function Get-ToolchainFileOptions { return $null } - $content = Get-Content -LiteralPath $path -Raw + $content = Read-RootToolchainFile $path $section = [regex]::Match( $content, '(?ms)^\s*\[\s*toolchain\s*\]\s*(?:#.*)?$(.*?)(?=^\s*\[|\z)' @@ -5134,11 +5142,15 @@ anvil-pr-msrv: anvil-pr-msrv-validate-prereqs anvil-msrv-test # Install prerequisites for the `anvil-pr-msrv` recipe. [group("anvil-setup")] -anvil-pr-msrv-setup installer="install": (anvil-msrv-test-setup installer) +anvil-pr-msrv-setup installer="install": \ + (anvil-tool-cargo-delta-install installer) \ + (anvil-msrv-test-setup installer) # Validate prerequisites for the `anvil-pr-msrv` recipe. [group("anvil-setup")] -anvil-pr-msrv-validate-prereqs: anvil-msrv-test-validate-prereqs +anvil-pr-msrv-validate-prereqs: \ + anvil-tool-cargo-delta-validate-prereqs \ + anvil-msrv-test-validate-prereqs === justfiles/anvil/groups/pr-mutants.just === # Copyright (c) Microsoft Corporation. @@ -5209,21 +5221,21 @@ anvil-pr-runtime-analysis-validate-prereqs: \ # See https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/checks.md -# pr-slow is the single PR-tier group for everything that takes more -# than ~30s per crate (tests, stricter runtimes, mutation testing). -# It's internally split into four sub-recipes so individual concerns -# can be invoked locally without dragging the others along: +# pr-slow is the local umbrella for everything that takes more than ~30s +# per crate (tests, MSRV tests, stricter runtimes, mutation testing). It's +# split into four sub-recipes so individual concerns can be invoked locally +# without dragging the others along: # -# slow1: tests + coverage (replaces the former pr-test group) -# slow2: stricter-runtime correctness (miri, careful) -# slow3: mutation testing +# pr-test: tests + coverage +# pr-msrv: unit and integration tests under the declared MSRV +# pr-runtime-analysis: stricter-runtime correctness (miri, careful) +# pr-mutants: mutation testing # -# Cloud workflows run pr-slow as ONE job per OS leg -- the sub-recipes run -# sequentially within. This trades per-leg wall-clock for fewer -# orchestration jobs and a flatter PR check graph. Individual sub- -# recipes are runnable on their own locally: +# Cloud workflows run the four sub-groups as independent parallel jobs. +# The umbrella runs them sequentially only when invoked locally: # # $ just anvil-pr-test # tests + coverage only +# $ just anvil-pr-msrv # MSRV unit and integration tests only # $ just anvil-pr-runtime-analysis # miri + careful only # $ just anvil-pr-mutants # mutants only diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap index 1e6c5a4c2..4d5172cec 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap @@ -1225,13 +1225,21 @@ function Get-RootToolchainFilePath { return $null } +function Read-RootToolchainFile([string] $path) { + $content = Get-Content -LiteralPath $path -Raw + if ([string]::IsNullOrWhiteSpace($content)) { + throw "anvil: root toolchain file '$path' is empty; remove it or declare a toolchain configuration" + } + return $content +} + function Get-SelectingToolchainFile { $path = Get-RootToolchainFilePath if ($null -eq $path) { return $null } - $content = Get-Content -LiteralPath $path -Raw + $content = Read-RootToolchainFile $path if ((Split-Path -Leaf $path) -eq 'rust-toolchain' -and $content.TrimStart() -notmatch '^\[') { return [pscustomobject]@{ Path = $path @@ -1275,7 +1283,7 @@ function Get-ToolchainFileOptions { return $null } - $content = Get-Content -LiteralPath $path -Raw + $content = Read-RootToolchainFile $path $section = [regex]::Match( $content, '(?ms)^\s*\[\s*toolchain\s*\]\s*(?:#.*)?$(.*?)(?=^\s*\[|\z)' @@ -4939,11 +4947,15 @@ anvil-pr-msrv: anvil-pr-msrv-validate-prereqs anvil-msrv-test # Install prerequisites for the `anvil-pr-msrv` recipe. [group("anvil-setup")] -anvil-pr-msrv-setup installer="install": (anvil-msrv-test-setup installer) +anvil-pr-msrv-setup installer="install": \ + (anvil-tool-cargo-delta-install installer) \ + (anvil-msrv-test-setup installer) # Validate prerequisites for the `anvil-pr-msrv` recipe. [group("anvil-setup")] -anvil-pr-msrv-validate-prereqs: anvil-msrv-test-validate-prereqs +anvil-pr-msrv-validate-prereqs: \ + anvil-tool-cargo-delta-validate-prereqs \ + anvil-msrv-test-validate-prereqs === justfiles/anvil/groups/pr-mutants.just === # Copyright (c) Microsoft Corporation. @@ -5014,21 +5026,21 @@ anvil-pr-runtime-analysis-validate-prereqs: \ # See https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/checks.md -# pr-slow is the single PR-tier group for everything that takes more -# than ~30s per crate (tests, stricter runtimes, mutation testing). -# It's internally split into four sub-recipes so individual concerns -# can be invoked locally without dragging the others along: +# pr-slow is the local umbrella for everything that takes more than ~30s +# per crate (tests, MSRV tests, stricter runtimes, mutation testing). It's +# split into four sub-recipes so individual concerns can be invoked locally +# without dragging the others along: # -# slow1: tests + coverage (replaces the former pr-test group) -# slow2: stricter-runtime correctness (miri, careful) -# slow3: mutation testing +# pr-test: tests + coverage +# pr-msrv: unit and integration tests under the declared MSRV +# pr-runtime-analysis: stricter-runtime correctness (miri, careful) +# pr-mutants: mutation testing # -# Cloud workflows run pr-slow as ONE job per OS leg -- the sub-recipes run -# sequentially within. This trades per-leg wall-clock for fewer -# orchestration jobs and a flatter PR check graph. Individual sub- -# recipes are runnable on their own locally: +# Cloud workflows run the four sub-groups as independent parallel jobs. +# The umbrella runs them sequentially only when invoked locally: # # $ just anvil-pr-test # tests + coverage only +# $ just anvil-pr-msrv # MSRV unit and integration tests only # $ just anvil-pr-runtime-analysis # miri + careful only # $ just anvil-pr-mutants # mutants only diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap index 18412bded..def43d975 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap @@ -1225,13 +1225,21 @@ function Get-RootToolchainFilePath { return $null } +function Read-RootToolchainFile([string] $path) { + $content = Get-Content -LiteralPath $path -Raw + if ([string]::IsNullOrWhiteSpace($content)) { + throw "anvil: root toolchain file '$path' is empty; remove it or declare a toolchain configuration" + } + return $content +} + function Get-SelectingToolchainFile { $path = Get-RootToolchainFilePath if ($null -eq $path) { return $null } - $content = Get-Content -LiteralPath $path -Raw + $content = Read-RootToolchainFile $path if ((Split-Path -Leaf $path) -eq 'rust-toolchain' -and $content.TrimStart() -notmatch '^\[') { return [pscustomobject]@{ Path = $path @@ -1275,7 +1283,7 @@ function Get-ToolchainFileOptions { return $null } - $content = Get-Content -LiteralPath $path -Raw + $content = Read-RootToolchainFile $path $section = [regex]::Match( $content, '(?ms)^\s*\[\s*toolchain\s*\]\s*(?:#.*)?$(.*?)(?=^\s*\[|\z)' @@ -3780,11 +3788,15 @@ anvil-pr-msrv: anvil-pr-msrv-validate-prereqs anvil-msrv-test # Install prerequisites for the `anvil-pr-msrv` recipe. [group("anvil-setup")] -anvil-pr-msrv-setup installer="install": (anvil-msrv-test-setup installer) +anvil-pr-msrv-setup installer="install": \ + (anvil-tool-cargo-delta-install installer) \ + (anvil-msrv-test-setup installer) # Validate prerequisites for the `anvil-pr-msrv` recipe. [group("anvil-setup")] -anvil-pr-msrv-validate-prereqs: anvil-msrv-test-validate-prereqs +anvil-pr-msrv-validate-prereqs: \ + anvil-tool-cargo-delta-validate-prereqs \ + anvil-msrv-test-validate-prereqs === justfiles/anvil/groups/pr-mutants.just === # Copyright (c) Microsoft Corporation. @@ -3855,21 +3867,21 @@ anvil-pr-runtime-analysis-validate-prereqs: \ # See https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/checks.md -# pr-slow is the single PR-tier group for everything that takes more -# than ~30s per crate (tests, stricter runtimes, mutation testing). -# It's internally split into four sub-recipes so individual concerns -# can be invoked locally without dragging the others along: +# pr-slow is the local umbrella for everything that takes more than ~30s +# per crate (tests, MSRV tests, stricter runtimes, mutation testing). It's +# split into four sub-recipes so individual concerns can be invoked locally +# without dragging the others along: # -# slow1: tests + coverage (replaces the former pr-test group) -# slow2: stricter-runtime correctness (miri, careful) -# slow3: mutation testing +# pr-test: tests + coverage +# pr-msrv: unit and integration tests under the declared MSRV +# pr-runtime-analysis: stricter-runtime correctness (miri, careful) +# pr-mutants: mutation testing # -# Cloud workflows run pr-slow as ONE job per OS leg -- the sub-recipes run -# sequentially within. This trades per-leg wall-clock for fewer -# orchestration jobs and a flatter PR check graph. Individual sub- -# recipes are runnable on their own locally: +# Cloud workflows run the four sub-groups as independent parallel jobs. +# The umbrella runs them sequentially only when invoked locally: # # $ just anvil-pr-test # tests + coverage only +# $ just anvil-pr-msrv # MSRV unit and integration tests only # $ just anvil-pr-runtime-analysis # miri + careful only # $ just anvil-pr-mutants # mutants only diff --git a/justfiles/anvil/groups/pr-msrv.just b/justfiles/anvil/groups/pr-msrv.just index 1c25aa3f9..d35d01ac8 100644 --- a/justfiles/anvil/groups/pr-msrv.just +++ b/justfiles/anvil/groups/pr-msrv.just @@ -12,8 +12,12 @@ anvil-pr-msrv: anvil-pr-msrv-validate-prereqs anvil-msrv-test # Install prerequisites for the `anvil-pr-msrv` recipe. [group("anvil-setup")] -anvil-pr-msrv-setup installer="install": (anvil-msrv-test-setup installer) +anvil-pr-msrv-setup installer="install": \ + (anvil-tool-cargo-delta-install installer) \ + (anvil-msrv-test-setup installer) # Validate prerequisites for the `anvil-pr-msrv` recipe. [group("anvil-setup")] -anvil-pr-msrv-validate-prereqs: anvil-msrv-test-validate-prereqs +anvil-pr-msrv-validate-prereqs: \ + anvil-tool-cargo-delta-validate-prereqs \ + anvil-msrv-test-validate-prereqs diff --git a/justfiles/anvil/groups/pr-slow.just b/justfiles/anvil/groups/pr-slow.just index f063a49ec..3ac2c01f5 100644 --- a/justfiles/anvil/groups/pr-slow.just +++ b/justfiles/anvil/groups/pr-slow.just @@ -6,21 +6,21 @@ # See https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/checks.md -# pr-slow is the single PR-tier group for everything that takes more -# than ~30s per crate (tests, stricter runtimes, mutation testing). -# It's internally split into four sub-recipes so individual concerns -# can be invoked locally without dragging the others along: +# pr-slow is the local umbrella for everything that takes more than ~30s +# per crate (tests, MSRV tests, stricter runtimes, mutation testing). It's +# split into four sub-recipes so individual concerns can be invoked locally +# without dragging the others along: # -# slow1: tests + coverage (replaces the former pr-test group) -# slow2: stricter-runtime correctness (miri, careful) -# slow3: mutation testing +# pr-test: tests + coverage +# pr-msrv: unit and integration tests under the declared MSRV +# pr-runtime-analysis: stricter-runtime correctness (miri, careful) +# pr-mutants: mutation testing # -# Cloud workflows run pr-slow as ONE job per OS leg -- the sub-recipes run -# sequentially within. This trades per-leg wall-clock for fewer -# orchestration jobs and a flatter PR check graph. Individual sub- -# recipes are runnable on their own locally: +# Cloud workflows run the four sub-groups as independent parallel jobs. +# The umbrella runs them sequentially only when invoked locally: # # $ just anvil-pr-test # tests + coverage only +# $ just anvil-pr-msrv # MSRV unit and integration tests only # $ just anvil-pr-runtime-analysis # miri + careful only # $ just anvil-pr-mutants # mutants only From dca6da0bab6d16df4c5703ffb54a29b004eb2501 Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Fri, 28 Aug 2026 12:05:17 +0200 Subject: [PATCH 09/32] test(cargo-anvil): quarantine resolver subprocess test from miri Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/cargo-anvil/src/anvil/artifacts/justfile.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/cargo-anvil/src/anvil/artifacts/justfile.rs b/crates/cargo-anvil/src/anvil/artifacts/justfile.rs index a6643e5be..fd9e15e4f 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/justfile.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/justfile.rs @@ -913,6 +913,7 @@ mod tests { } #[test] + #[cfg_attr(miri, ignore = "uses filesystem and subprocesses; miri isolation forbids them")] fn applies_toolchain_file_options_to_an_msrv_fallback() { let temp = fixture("[workspace.package]\nrust-version = \"1.93\"\n"); fs::write( From 03797004bda9f506a22d3843dfeb5b958115b88d Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Fri, 28 Aug 2026 12:22:55 +0200 Subject: [PATCH 10/32] test(cargo-anvil): exclude resolver process tests from miri Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/cargo-anvil/src/anvil/artifacts/justfile.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/cargo-anvil/src/anvil/artifacts/justfile.rs b/crates/cargo-anvil/src/anvil/artifacts/justfile.rs index fd9e15e4f..2719db5b9 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/justfile.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/justfile.rs @@ -766,6 +766,7 @@ mod tests { assert!(STABLE_TOOLCHAIN_RESOLVER.contains("rustup target add --toolchain")); } + #[cfg(not(miri))] mod stable_toolchain_resolver_tests { use std::path::Path; use std::process::{Command, Output}; @@ -913,7 +914,6 @@ mod tests { } #[test] - #[cfg_attr(miri, ignore = "uses filesystem and subprocesses; miri isolation forbids them")] fn applies_toolchain_file_options_to_an_msrv_fallback() { let temp = fixture("[workspace.package]\nrust-version = \"1.93\"\n"); fs::write( From af8cec2f30e83d086dd27f662af414d157a2b2a3 Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Fri, 28 Aug 2026 13:23:59 +0200 Subject: [PATCH 11/32] Scope stable toolchains to Anvil commands Remove the globally exported resolved toolchain and route stable commands through private Just helpers. Keep explicit nightly and MSRV selection at each command site, and preserve current-checkout toolchain selection for impact baselines. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .anvil.lock | 62 ++--- .anvil/container/Containerfile | 8 +- .anvil/resolve-stable-toolchain.ps1 | 6 +- .github/actions/anvil-setup/action.yml | 16 +- crates/cargo-anvil/docs/design/README.md | 7 +- crates/cargo-anvil/docs/design/ado.md | 9 +- crates/cargo-anvil/docs/design/containers.md | 4 +- crates/cargo-anvil/docs/design/github.md | 12 +- crates/cargo-anvil/docs/design/local.md | 29 ++- .../src/anvil/artifacts/justfile.rs | 35 ++- .../cargo-anvil/templates/ado/steps/setup.yml | 14 +- .../templates/anvil/container/Containerfile | 8 +- .../anvil/resolve-stable-toolchain.ps1 | 6 +- .../templates/github/setup-action.yml | 16 +- .../justfiles/anvil/checks/aprz.just | 2 +- .../justfiles/anvil/checks/audit.just | 2 +- .../justfiles/anvil/checks/bench.just | 2 +- .../justfiles/anvil/checks/bolero.just | 2 +- .../justfiles/anvil/checks/careful.just | 2 +- .../justfiles/anvil/checks/cargo-hack.just | 2 +- .../justfiles/anvil/checks/cargo-sort.just | 2 +- .../justfiles/anvil/checks/clippy.just | 2 +- .../justfiles/anvil/checks/deny.just | 2 +- .../justfiles/anvil/checks/doc-build.just | 2 +- .../justfiles/anvil/checks/doc-test.just | 4 +- .../anvil/checks/ensure-no-cyclic-deps.just | 2 +- .../checks/ensure-no-default-features.just | 2 +- .../justfiles/anvil/checks/examples.just | 2 +- .../anvil/checks/external-types.just | 2 +- .../anvil/checks/license-headers.just | 2 +- .../justfiles/anvil/checks/llvm-cov.just | 4 +- .../justfiles/anvil/checks/loom.just | 4 +- .../justfiles/anvil/checks/msrv-test.just | 6 +- .../justfiles/anvil/checks/mutants-diff.just | 2 +- .../justfiles/anvil/checks/mutants-full.just | 2 +- .../justfiles/anvil/checks/readme-check.just | 4 +- .../justfiles/anvil/checks/semver-check.just | 4 +- .../justfiles/anvil/checks/spellcheck.just | 4 +- .../templates/justfiles/anvil/impact.just | 46 ++-- .../templates/justfiles/anvil/tools.just | 55 ++++- .../templates/justfiles/anvil/versions.just | 21 +- crates/cargo-anvil/tests/impact.rs | 67 +++--- crates/cargo-anvil/tests/recipe_contracts.rs | 12 +- .../snapshots/snapshots__ado_backend.snap | 214 +++++++++-------- .../snapshots/snapshots__github_backend.snap | 216 ++++++++++-------- .../snapshots/snapshots__local_only.snap | 200 ++++++++-------- justfiles/anvil/checks/aprz.just | 2 +- justfiles/anvil/checks/audit.just | 2 +- justfiles/anvil/checks/bench.just | 2 +- justfiles/anvil/checks/bolero.just | 2 +- justfiles/anvil/checks/careful.just | 2 +- justfiles/anvil/checks/cargo-hack.just | 2 +- justfiles/anvil/checks/cargo-sort.just | 2 +- justfiles/anvil/checks/clippy.just | 2 +- justfiles/anvil/checks/deny.just | 2 +- justfiles/anvil/checks/doc-build.just | 2 +- justfiles/anvil/checks/doc-test.just | 4 +- .../anvil/checks/ensure-no-cyclic-deps.just | 2 +- .../checks/ensure-no-default-features.just | 2 +- justfiles/anvil/checks/examples.just | 2 +- justfiles/anvil/checks/external-types.just | 2 +- justfiles/anvil/checks/license-headers.just | 2 +- justfiles/anvil/checks/llvm-cov.just | 4 +- justfiles/anvil/checks/loom.just | 4 +- justfiles/anvil/checks/msrv-test.just | 6 +- justfiles/anvil/checks/mutants-diff.just | 2 +- justfiles/anvil/checks/mutants-full.just | 2 +- justfiles/anvil/checks/readme-check.just | 4 +- justfiles/anvil/checks/semver-check.just | 4 +- justfiles/anvil/checks/spellcheck.just | 4 +- justfiles/anvil/impact.just | 46 ++-- justfiles/anvil/tools.just | 55 ++++- justfiles/anvil/versions.just | 21 +- 73 files changed, 715 insertions(+), 598 deletions(-) diff --git a/.anvil.lock b/.anvil.lock index c6f298237..3160a4b41 100644 --- a/.anvil.lock +++ b/.anvil.lock @@ -1,11 +1,11 @@ version = 1 tool = "anvil" tool_version = "0.5.0" -catalog_checksum = "sha256:4734440a19e16d719afbd7a3f9a5686990a62df293c254e20afb3108eb205a82" +catalog_checksum = "sha256:969758e637017f6cd43c97177d8cb14547afa16cd2f10250469205def7351b54" [[file]] path = ".anvil/container/Containerfile" -checksum = "sha256:ea2c9447d21b8ab851bca2c5f2b2b32223782dfd8e44de26d2e63b79e4212530" +checksum = "sha256:ddd1cd7500a32b3a0bd93a5a60e96674fe4ba667d3ceb44f5207c447a0907c77" [[file]] path = ".anvil/container/Containerfile.dockerignore" @@ -37,7 +37,7 @@ checksum = "sha256:0f4fcf0fef43e4736260af771cae15b7eb8afc382e5290fabddc52894b091 [[file]] path = ".anvil/resolve-stable-toolchain.ps1" -checksum = "sha256:c6dc26c63300378994b9c375ff1b026323b7f5706e8b65ee68b77f4cd670e4b8" +checksum = "sha256:bf3baa2f3e0c33c913fbea5b6b810523fbc5ce3840849fb833a680fcaab330d7" [[file]] path = ".github/actions/anvil-impact/action.yml" @@ -53,7 +53,7 @@ checksum = "sha256:ff8def6c0786b6e146c4b633dfe38cb9b8ede398345516cca32bbcd558608 [[file]] path = ".github/actions/anvil-setup/action.yml" -checksum = "sha256:583a09c3a3b687f8dd40face8b0353e9e03a3fe5686a53916ac1ae44b06b5b2a" +checksum = "sha256:9d0f8a0c5599c2bf35abbc90deb73d7a0ed34c336e6f9effce8a914e3e21c3f4" [[file]] path = ".github/actions/anvil-setup/just-problem-matcher.json" @@ -77,63 +77,63 @@ checksum = "sha256:d4d3bd645a5586e9a1cc3a5fc27e38c93e59b1e29f83ede0ccd610273eec0 [[file]] path = "justfiles/anvil/checks/aprz.just" -checksum = "sha256:62d8334b55b0ba3b221550037dec987d9749e773f40df6c0cd18f08164c095bf" +checksum = "sha256:197541fc6c1dd712e452d17f7dfdfb2d8a405fffb79180b5f8773934ea9b80eb" [[file]] path = "justfiles/anvil/checks/audit.just" -checksum = "sha256:54abf96a320bb4b35a3c0ddf2f30b0f4a30e0673e482ca3a71242fa383536415" +checksum = "sha256:0143075c7f29bf035a42053f66f579b96e75e73b8bf1dfbcd27418ab4926e3ed" [[file]] path = "justfiles/anvil/checks/bench.just" -checksum = "sha256:50f04b4ea6c99df8ad7434d320f34dccdb6db6a77b83e3090e35de0ca3a15f83" +checksum = "sha256:021063c304a691bf0515a80ee63d6a075184ba59ba3bdaef0da63e283996f552" [[file]] path = "justfiles/anvil/checks/bolero.just" -checksum = "sha256:003032f39c781851b880c1d7e63b93573f5252cca9f785b98b04dfa2e858aff3" +checksum = "sha256:c2e2e9474ed8e9e7c92538eba8d741525006c0e1daa771763d05b8a3f11c15e4" [[file]] path = "justfiles/anvil/checks/careful.just" -checksum = "sha256:bf69fadd48df96f97640ec41eade07eb2b6916800014cb277c86208efc34396b" +checksum = "sha256:ea06cf7ebd7d5955b409db500a9ab193311af584437412d7bf0429fa70e43fac" [[file]] path = "justfiles/anvil/checks/cargo-hack.just" -checksum = "sha256:681c8952690496c7d29c7bf83a5ab99c193088b05a5dc7ea3b3b0ed2400904ea" +checksum = "sha256:8580a74aae3ce80a5d1ee39cd3f22240d361d0070f934b5ce67d758dc5123086" [[file]] path = "justfiles/anvil/checks/cargo-sort.just" -checksum = "sha256:2652ae52153eebe3fdb1dfd553aa65cab84d11054d5314470d55774a92a742b3" +checksum = "sha256:4342abfffa17029ed2543e9781840bbe10c7709d15b17ada04ff1c4ee94c2657" [[file]] path = "justfiles/anvil/checks/clippy.just" -checksum = "sha256:64e8f11ed1a308797e5599117ab6152d0ca3a1dfb55e6a4cfdd42d35de13e1e2" +checksum = "sha256:c06c1f09fceb54bf131ea0b8a18c433417d50de224caaae834bad1dc19f509ae" [[file]] path = "justfiles/anvil/checks/deny.just" -checksum = "sha256:4bb4bc0822612e2d2a43689e3f5c7f1190f59ef3db201fad7f3332725ceaef16" +checksum = "sha256:a5526e7ffc2d6f20508f5f7804f17ed768fefdd1888420705625aa057a288f6f" [[file]] path = "justfiles/anvil/checks/doc-build.just" -checksum = "sha256:57bf3ce93d3e7b947cb203f85b572531b86706ed2437ce3098f086745bf5d111" +checksum = "sha256:67d07c8cf394d2a3ef3fe2043978ec9743d9f757fe7c38cce4335d9a43d49f8c" [[file]] path = "justfiles/anvil/checks/doc-test.just" -checksum = "sha256:e923667a5ceb376232db02ec41824d21f2fccffd041565109f7740c001c1c23d" +checksum = "sha256:baa67d238190acb7f0bdcdf3a46536fb98ecbfc931ea49f88e5d0fa5bee2291a" [[file]] path = "justfiles/anvil/checks/ensure-no-cyclic-deps.just" -checksum = "sha256:7351afd07f020f04fa6c7c82d4740ef32cbb1b648550f0a5ff306af6a5d58588" +checksum = "sha256:289228c8d52900aa24c4e497a697102ea7d596ecac16eb4be477e8661d8e6aa8" [[file]] path = "justfiles/anvil/checks/ensure-no-default-features.just" -checksum = "sha256:25c61d31ea76c5e65b5c3caf744ddc9b49f6f9cdf9717b012d1d82cebfe555b9" +checksum = "sha256:57f00949f8e99e63a92af9fe6692ec42875fbc8e48c5434a462be7d9d993767a" [[file]] path = "justfiles/anvil/checks/examples.just" -checksum = "sha256:0e84a7b43ff07608c624ece03d46ddd58c629ebd5f6a5684f3b43b7190026e0f" +checksum = "sha256:c3c38bb08cbb2a3dedf971bd70aa6b1189ee93f18dc7168983e03767d6f7636e" [[file]] path = "justfiles/anvil/checks/external-types.just" -checksum = "sha256:d003f1d82898706c136f42c3156d5ecc8f288c18f125c964b9dca1d24fa93d67" +checksum = "sha256:2da1e7c8d57932eab5717977f72dc9de6b106604c7e0d37ccb2ac09389182527" [[file]] path = "justfiles/anvil/checks/fmt.just" @@ -141,15 +141,15 @@ checksum = "sha256:967b68ef3ed3e0dcac2a1ea22ef92c66a7db802bda718f6e5ce39f529ed8e [[file]] path = "justfiles/anvil/checks/license-headers.just" -checksum = "sha256:ab9ef6c3b50475ff3a8ab873b319f1ec04c456275d566ce323a76730be3ad0fc" +checksum = "sha256:5aa3b85bb14ce44ae753b078f84d3e9b7d9509e04f68534d7f97c82992eaca4f" [[file]] path = "justfiles/anvil/checks/llvm-cov.just" -checksum = "sha256:689f7d5b1eaea32672f8d0b9fff0994cc03bc56fb8ef282a4286f930fc503725" +checksum = "sha256:cdbbcdb7666c320acbf9bd78df905a62b37785d8cd373d0a7b8838ae29154bc0" [[file]] path = "justfiles/anvil/checks/loom.just" -checksum = "sha256:4bf509bcca3507aafd9848ce19ea3691ab2cd77de05b344e042f3ba2c300e35b" +checksum = "sha256:bd1883c419a0c64390263bbd15468da825be3ac8f9059fc8ae28df29917231e5" [[file]] path = "justfiles/anvil/checks/miri-race-coverage.just" @@ -169,15 +169,15 @@ checksum = "sha256:9e9d0cbfef1e1e1586c2af4e9c387719c2f017ea1203d326baacc3ae25df8 [[file]] path = "justfiles/anvil/checks/msrv-test.just" -checksum = "sha256:8e1d19b26fdd4763b545288913d8cc5ea898937a445bb3a59334b699818548b4" +checksum = "sha256:968a0cf65a319626c3ed61f75e1d363608856664d105850be281e91f8ecf3537" [[file]] path = "justfiles/anvil/checks/mutants-diff.just" -checksum = "sha256:cb9721c1cc94161ea8bc20ccd94209daa23d83d1beda9f3c0969e2031a135022" +checksum = "sha256:66b5f07fa4aa3e09a987f349d8bebc65383c8be86570f30ddb37d29df375f62a" [[file]] path = "justfiles/anvil/checks/mutants-full.just" -checksum = "sha256:9792344e6696f5c63f30fdccb5ef90a3598c56adb7561cec33f074c84ce6f89b" +checksum = "sha256:3b2b6d0ce7058afa8c491be1a57e45fc347b09942c1a4860015616207fef6afc" [[file]] path = "justfiles/anvil/checks/pr-title.just" @@ -185,15 +185,15 @@ checksum = "sha256:28734aa77526ca5c53d89a32c3e20ae6b42d2032c73ab6a1dbc9831f25cb0 [[file]] path = "justfiles/anvil/checks/readme-check.just" -checksum = "sha256:d346399f288570066e53fd123baf05f7d4a57f17da8ee687a6d881204c5bae12" +checksum = "sha256:2002fb394eba9d319778693bcaa6076a23a32cdad924e3099331a28d79092db4" [[file]] path = "justfiles/anvil/checks/semver-check.just" -checksum = "sha256:1b8b67f8c054c8da03d43b746f28c20f5366996e3dea3c21cf25d04a6496a072" +checksum = "sha256:374362645bbed4d21671980168817350911895e33e6d7e84646176b94de7b5fb" [[file]] path = "justfiles/anvil/checks/spellcheck.just" -checksum = "sha256:8f47518b756607402a7b3652762d5d37cff5d91eecd1052053176eedd65a9958" +checksum = "sha256:40536077a6bd133597c2fdf653cd0b0913c20bc9cc988afeacbefadec6ea1477" [[file]] path = "justfiles/anvil/checks/udeps.just" @@ -249,7 +249,7 @@ checksum = "sha256:6b71379310b986e9a44000ba9d072c3f8934397a0f7fb1bf27a31a7742ca7 [[file]] path = "justfiles/anvil/impact.just" -checksum = "sha256:5714a138135154b91b234f6bad06ec3ade0e5780f761d631c1eca92b6010e5ad" +checksum = "sha256:3c632a3fd501e17177d2c934308060b0eeed50e2f19e66c874a2a2df2fe03f99" [[file]] path = "justfiles/anvil/mod.just" @@ -265,11 +265,11 @@ checksum = "sha256:5b2d91569f3fe87cd5f516623a01b5c7ae855ad587870ef9a2ae6d5619dd9 [[file]] path = "justfiles/anvil/tools.just" -checksum = "sha256:adfce0506e21386dbbe510fb80fdf1ee8e5a06a56e38d585a19f88c1213d05a5" +checksum = "sha256:68b1574ba4a0de729913eab3862e51a76b2e4459650ddcd7ea1df05fe9b28ef2" [[file]] path = "justfiles/anvil/versions.just" -checksum = "sha256:c804d6d173ba884812731a73ca585eb904b42e1e6df15647c8b15b0047837cf6" +checksum = "sha256:9257adf073dc2119cf70290c2f98b6c52c3146e63443f5188e42d6743c259287" [[region]] host = ".delta.toml" diff --git a/.anvil/container/Containerfile b/.anvil/container/Containerfile index dc4a709ef..43f3685ac 100644 --- a/.anvil/container/Containerfile +++ b/.anvil/container/Containerfile @@ -52,11 +52,9 @@ COPY . . RUN --mount=type=cache,id=anvil-cargo-registry,target=/usr/local/cargo/registry \ --mount=type=cache,id=anvil-cargo-git,target=/usr/local/cargo/git \ --mount=type=cache,id=anvil-cargo-target,target=/tmp/anvil-target \ - pwsh -NoProfile -File .anvil/resolve-stable-toolchain.ps1 -InstallIfMissing \ - && toolchain="$(pwsh -NoProfile -File .anvil/resolve-stable-toolchain.ps1 -ForEnvironment)" \ - && printf "anvil_runner := \"native\"\nimport 'justfiles/anvil/mod.just'\n" > Justfile \ - && RUSTUP_TOOLCHAIN="$toolchain" ANVIL_STABLE_TOOLCHAIN_SOURCE=container \ - CARGO_TARGET_DIR=/tmp/anvil-target just anvil-setup + printf "anvil_runner := \"native\"\nimport 'justfiles/anvil/mod.just'\n" > Justfile \ + && just _anvil-resolve-stable install \ + && CARGO_TARGET_DIR=/tmp/anvil-target just anvil-setup COPY .anvil/container/entrypoint.sh /usr/local/bin/anvil-container-entrypoint RUN chmod 755 /usr/local/bin/anvil-container-entrypoint diff --git a/.anvil/resolve-stable-toolchain.ps1 b/.anvil/resolve-stable-toolchain.ps1 index a2c3fe460..7c8b1b36c 100644 --- a/.anvil/resolve-stable-toolchain.ps1 +++ b/.anvil/resolve-stable-toolchain.ps1 @@ -244,11 +244,7 @@ if ($EmitToolchainFileOptions) { exit 0 } if ($ValidateWorkspaceMsrv) { - $skipWorkspaceMsrvValidation = if ([string]::IsNullOrWhiteSpace($env:ANVIL_STABLE_TOOLCHAIN_SOURCE)) { - -not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN) - } else { - $env:ANVIL_STABLE_TOOLCHAIN_SOURCE -in @('environment', 'container') - } + $skipWorkspaceMsrvValidation = -not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN) if (-not $skipWorkspaceMsrvValidation -and $null -eq $fileSelection) { Assert-UniformWorkspaceMsrv } diff --git a/.github/actions/anvil-setup/action.yml b/.github/actions/anvil-setup/action.yml index 30f7402ca..051fbc3f9 100644 --- a/.github/actions/anvil-setup/action.yml +++ b/.github/actions/anvil-setup/action.yml @@ -80,24 +80,20 @@ runs: - name: Resolve stable toolchain shell: pwsh -NoProfile -Command ". '{0}'" run: | - $source = if ([string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { 'resolved' } else { 'environment' } - & '.anvil/resolve-stable-toolchain.ps1' -InstallIfMissing + & just _anvil-resolve-stable install if ($LASTEXITCODE -ne 0) { throw 'anvil: failed to install stable toolchain' } - $toolchain = (& '.anvil/resolve-stable-toolchain.ps1' | Out-String).Trim() + $toolchain = (& just _anvil-resolve-stable | Out-String).Trim() if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($toolchain)) { throw 'anvil: failed to resolve stable toolchain' } - $environmentValue = (& '.anvil/resolve-stable-toolchain.ps1' -ForEnvironment | Out-String).Trim() - if (-not [string]::IsNullOrWhiteSpace($environmentValue)) { - "RUSTUP_TOOLCHAIN=$environmentValue" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append - } - "ANVIL_STABLE_TOOLCHAIN_SOURCE=$source" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append Write-Host "anvil: selected stable toolchain '$toolchain'" - id: rustc-version - shell: bash - run: echo "version=$(rustc --version | awk '{print $2}')" >> "$GITHUB_OUTPUT" + shell: pwsh -NoProfile -Command ". '{0}'" + run: | + $version = (& just _anvil-with-stable rustc --version | Select-Object -Last 1).Split()[1] + "version=$version" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append - name: Restore cargo cache id: cargo-cache uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 diff --git a/crates/cargo-anvil/docs/design/README.md b/crates/cargo-anvil/docs/design/README.md index e034b62c6..d8e80d90e 100644 --- a/crates/cargo-anvil/docs/design/README.md +++ b/crates/cargo-anvil/docs/design/README.md @@ -270,7 +270,8 @@ Detail on each host: - **`Justfile` and `justfiles/anvil/*.just`** — see [local.md](./local.md). - **`.anvil/resolve-stable-toolchain.ps1`** — the unconditional generated - bootstrap shared by parse-time recipe variables and cloud/container setup. + implementation behind the private stable-toolchain recipes used by local, + cloud, and container setup. - **`.anvil/container/`** — the non-recipe container assets (Containerfile, drivers, image-ID helpers, README) emitted only when the catalog includes the optional container backend. `justfiles/` holds `.just` recipes and nothing @@ -313,7 +314,9 @@ Detail on each host: When neither selects a toolchain, Anvil selects the root manifest's `[workspace.package].rust-version` or `[package].rust-version`. GitHub setup installs a missing public selection through rustup. ADO pipelines provision their selected internal - toolchain before Anvil runs. + toolchain before Anvil runs. The caller's `RUSTUP_TOOLCHAIN` is an input override only; + generated recipes apply resolved selection per stable command rather than exporting it + globally. The tool's persistent state lives in `.anvil.lock` at the repo root — the sidecar manifest tracking last-rendered checksums per owned file and per managed region. See diff --git a/crates/cargo-anvil/docs/design/ado.md b/crates/cargo-anvil/docs/design/ado.md index af236beda..a7276d248 100644 --- a/crates/cargo-anvil/docs/design/ado.md +++ b/crates/cargo-anvil/docs/design/ado.md @@ -742,10 +742,11 @@ mechanics are in [local.md §4](./local.md#4-impact-scoping-via-the-anvil-impact ## 6. Rust toolchain The user's root pipeline or compliance template installs Rust before the Anvil stages -run. The generated setup step then resolves the catalog toolchain and publishes -environment/MSRV selections as `RUSTUP_TOOLCHAIN` for subsequent steps before it -captures `rustc --version` and restores the cache. A selecting toolchain file remains -unset in the environment so rustup processes the complete file. +run. The generated setup step resolves the catalog toolchain and captures its +`rustc --version` through `_anvil-with-stable` before restoring the cache. It does +not publish a resolved `RUSTUP_TOOLCHAIN` to subsequent steps. Each stable recipe +applies the selection only to its own Cargo or Rust child process, while a selecting +toolchain file remains unoverridden so rustup processes the complete file. Selection follows the shared local contract: an existing `RUSTUP_TOOLCHAIN`, then a root toolchain file with `channel` or `path`, then the root MSRV. Internal pipelines diff --git a/crates/cargo-anvil/docs/design/containers.md b/crates/cargo-anvil/docs/design/containers.md index c2ef6707e..4f3987c32 100644 --- a/crates/cargo-anvil/docs/design/containers.md +++ b/crates/cargo-anvil/docs/design/containers.md @@ -174,7 +174,9 @@ available. Runtime execution uses `--pull=never` and never substitutes Container execution uses the same deterministic stable-toolchain selection as native execution: an existing `RUSTUP_TOOLCHAIN`, a selecting repository toolchain file, or the root manifest's MSRV. It fails rather than choosing a -runner or image default when none is available. +runner or image default when none is available. Image setup and checks invoke +the standard resolver and selected-stable execution recipes; the resolved value +is not exported across unrelated recipe processes. The restricted image-construction context does not contain workspace member manifests, so uniform per-package MSRV validation runs in native and cloud setup diff --git a/crates/cargo-anvil/docs/design/github.md b/crates/cargo-anvil/docs/design/github.md index ba1a4120b..37477ed0f 100644 --- a/crates/cargo-anvil/docs/design/github.md +++ b/crates/cargo-anvil/docs/design/github.md @@ -841,11 +841,13 @@ The check → bucket mapping is in ## 7. Rust toolchain The setup action resolves the catalog's stable toolchain before reading `rustc` or -restoring build artifacts. It installs a missing public channel through rustup, exports -environment/MSRV selections through `GITHUB_ENV`, leaves selecting toolchain files for -rustup to process natively, and keys the cache on the resulting compiler version. All -matrix legs therefore use the same repository selection instead of the different stable -versions that runner images may carry. +restoring build artifacts. It installs a missing public channel through rustup and +captures the selected compiler version through `_anvil-with-stable`, but does not +publish a resolved `RUSTUP_TOOLCHAIN` through `GITHUB_ENV`. Each stable recipe applies +the selection only to its own Cargo or Rust child process; selecting toolchain files +remain unoverridden so rustup processes them natively. All matrix legs therefore use +the same repository selection instead of the different stable versions that runner +images may carry, without changing unrelated recipe environments. Selection follows the shared local contract: an existing `RUSTUP_TOOLCHAIN`, then a root toolchain file with `channel` or `path`, then the root MSRV. There is no diff --git a/crates/cargo-anvil/docs/design/local.md b/crates/cargo-anvil/docs/design/local.md index e402fa060..7b6ca823e 100644 --- a/crates/cargo-anvil/docs/design/local.md +++ b/crates/cargo-anvil/docs/design/local.md @@ -401,13 +401,17 @@ checks. Internal builds may set `RUSTUP_TOOLCHAIN` to a provisioned toolchain ra than adding a public compiler pin. An empty root toolchain file is invalid and produces an actionable error. -The generated `.anvil/resolve-stable-toolchain.ps1` implements this contract. For -an existing environment override or MSRV fallback, the recipe tree exports the -result as `RUSTUP_TOOLCHAIN`. For a selecting toolchain file it leaves that -variable empty so rustup processes the complete file, including its components, -targets, and profile. `just anvil-*` therefore behaves the same locally and in -cloud workflows without changing the user's global rustup default. Raw `cargo` -commands outside Anvil retain normal rustup behavior. +The generated `.anvil/resolve-stable-toolchain.ps1` implements the parsing and +installation logic behind the private `_anvil-resolve-stable` recipe. +`RUSTUP_TOOLCHAIN` is an input to selection, never a globally exported output. +Each stable check invokes its Cargo or Rust command through +`_anvil-with-stable`, which applies an environment/MSRV selection only to that +child process. For a selecting toolchain file the helper removes the override +for that child so rustup processes the complete file, including its components, +targets, and profile. Nightly and MSRV checks continue to pass explicit +`+toolchain` arguments. The compiler intent is therefore visible at each command +site, nested Just invocations cannot recursively demand a global exported value, +and unrelated recipes and raw commands retain the caller's normal environment. An environment override or MSRV fallback normally causes rustup to ignore the repository toolchain file. Anvil therefore reads `profile`, `components`, and @@ -418,11 +422,12 @@ Explicit components and targets are added at setup time. If an option is unavail for the selected compiler, Anvil warns and skips it, matching rustup's handling of unavailable options read directly from a toolchain file. -The resolver is a generated bootstrap artifact rather than a regular Just recipe. -`versions.just` needs the resolved value while Just is parsing imports, and cloud -and container setup need it before the generated recipe catalog can run. Keeping -the parser and installation behavior in one script avoids separate implementations -at those pre-recipe call sites; ordinary validation and checks remain Just recipes. +The resolver script is a generated implementation artifact, but all supported +entry points call it through `_anvil-resolve-stable`. Because selection is no +longer a globally exported Just variable, that standard recipe can be invoked +from setup, checks, or nested Just helpers without a parse-time evaluation cycle. +The single script implementation keeps PowerShell parsing and installation +behavior identical across those recipe callers. When selection falls back to the root MSRV, prerequisite validation reads `cargo metadata` and requires every workspace package to resolve to that same diff --git a/crates/cargo-anvil/src/anvil/artifacts/justfile.rs b/crates/cargo-anvil/src/anvil/artifacts/justfile.rs index 2719db5b9..eb0b393e7 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/justfile.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/justfile.rs @@ -750,11 +750,12 @@ mod tests { } #[test] - fn stable_toolchain_resolution_is_exported() { - assert!(VERSIONS_JUST.contains("rust_stable_override := env_var_or_default(")); - assert!(VERSIONS_JUST.contains("shell('pwsh -NoProfile -File .anvil/resolve-stable-toolchain.ps1 -ForEnvironment #')")); - assert!(VERSIONS_JUST.contains("export RUSTUP_TOOLCHAIN := rust_stable")); - assert!(VERSIONS_JUST.contains("export ANVIL_STABLE_TOOLCHAIN_SOURCE :=")); + fn stable_toolchain_resolution_is_scoped_to_each_command() { + assert!(!VERSIONS_JUST.contains("export RUSTUP_TOOLCHAIN")); + assert!(TOOLS_JUST.contains("_anvil-resolve-stable action=\"resolve\":")); + assert!(TOOLS_JUST.contains("_anvil-with-stable command *args:")); + assert!(TOOLS_JUST.contains("Remove-Item Env:\\RUSTUP_TOOLCHAIN")); + assert!(TOOLS_JUST.contains("$env:RUSTUP_TOOLCHAIN = $toolchain")); assert!(STABLE_TOOLCHAIN_RESOLVER.contains("RUSTUP_TOOLCHAIN")); assert!(STABLE_TOOLCHAIN_RESOLVER.contains("rust-version")); assert!(STABLE_TOOLCHAIN_RESOLVER.contains("ValidateWorkspaceMsrv")); @@ -766,6 +767,29 @@ mod tests { assert!(STABLE_TOOLCHAIN_RESOLVER.contains("rustup target add --toolchain")); } + #[test] + fn checks_do_not_invoke_an_implicit_default_cargo() { + for (path, body) in CHECK_FILES { + for line in body.lines().map(str::trim) { + let invokes_cargo = line.starts_with("cargo ") + || line.starts_with("& cargo ") + || line.contains("= cargo ") + || line.contains("= & cargo ") + || line.contains("(& cargo "); + if invokes_cargo { + let toolchain = line + .split_once("cargo ") + .map(|(_, arguments)| arguments.trim_start()) + .expect("invokes_cargo patterns always include 'cargo '"); + assert!( + toolchain.starts_with("'+") || toolchain.starts_with("\"+"), + "{path} invokes Cargo without an explicit toolchain or _anvil-with-stable: {line}" + ); + } + } + } + } + #[cfg(not(miri))] mod stable_toolchain_resolver_tests { use std::path::Path; @@ -791,7 +815,6 @@ mod tests { .args(["-NoProfile", "-File", ".anvil/resolve-stable-toolchain.ps1"]) .args(args) .current_dir(root) - .env_remove("ANVIL_STABLE_TOOLCHAIN_SOURCE") .env_remove("ANVIL_MSRV_TOOLCHAIN"); match rustup_toolchain { Some(value) => { diff --git a/crates/cargo-anvil/templates/ado/steps/setup.yml b/crates/cargo-anvil/templates/ado/steps/setup.yml index 0e64a72ef..9121d96ba 100644 --- a/crates/cargo-anvil/templates/ado/steps/setup.yml +++ b/crates/cargo-anvil/templates/ado/steps/setup.yml @@ -19,23 +19,19 @@ parameters: default: '' steps: - pwsh: | - & '.anvil/resolve-stable-toolchain.ps1' -ApplyToolchainFileOptions + & just _anvil-resolve-stable apply-file-options if ($LASTEXITCODE -ne 0) { throw "anvil: stable toolchain option setup failed with exit code $LASTEXITCODE" } - $source = if ([string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { 'resolved' } else { 'environment' } - $toolchain = (& '.anvil/resolve-stable-toolchain.ps1' | Out-String).Trim() + $toolchain = (& just _anvil-resolve-stable | Out-String).Trim() if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($toolchain)) { throw 'anvil: failed to resolve stable toolchain' } - $environmentValue = (& '.anvil/resolve-stable-toolchain.ps1' -ForEnvironment | Out-String).Trim() - if (-not [string]::IsNullOrWhiteSpace($environmentValue)) { - Write-Host "##vso[task.setvariable variable=RUSTUP_TOOLCHAIN]$environmentValue" - } - Write-Host "##vso[task.setvariable variable=ANVIL_STABLE_TOOLCHAIN_SOURCE]$source" Write-Host "anvil: selected stable toolchain '$toolchain'" displayName: anvil setup (resolve stable toolchain) - - bash: echo "##vso[task.setvariable variable=anvil_rustc_version]$(rustc --version | awk '{print $2}')" + - pwsh: | + $version = (& just _anvil-with-stable rustc --version | Select-Object -Last 1).Split()[1] + Write-Host "##vso[task.setvariable variable=anvil_rustc_version]$version" displayName: anvil setup (capture rustc version) - task: Cache@2 inputs: diff --git a/crates/cargo-anvil/templates/anvil/container/Containerfile b/crates/cargo-anvil/templates/anvil/container/Containerfile index dc4a709ef..43f3685ac 100644 --- a/crates/cargo-anvil/templates/anvil/container/Containerfile +++ b/crates/cargo-anvil/templates/anvil/container/Containerfile @@ -52,11 +52,9 @@ COPY . . RUN --mount=type=cache,id=anvil-cargo-registry,target=/usr/local/cargo/registry \ --mount=type=cache,id=anvil-cargo-git,target=/usr/local/cargo/git \ --mount=type=cache,id=anvil-cargo-target,target=/tmp/anvil-target \ - pwsh -NoProfile -File .anvil/resolve-stable-toolchain.ps1 -InstallIfMissing \ - && toolchain="$(pwsh -NoProfile -File .anvil/resolve-stable-toolchain.ps1 -ForEnvironment)" \ - && printf "anvil_runner := \"native\"\nimport 'justfiles/anvil/mod.just'\n" > Justfile \ - && RUSTUP_TOOLCHAIN="$toolchain" ANVIL_STABLE_TOOLCHAIN_SOURCE=container \ - CARGO_TARGET_DIR=/tmp/anvil-target just anvil-setup + printf "anvil_runner := \"native\"\nimport 'justfiles/anvil/mod.just'\n" > Justfile \ + && just _anvil-resolve-stable install \ + && CARGO_TARGET_DIR=/tmp/anvil-target just anvil-setup COPY .anvil/container/entrypoint.sh /usr/local/bin/anvil-container-entrypoint RUN chmod 755 /usr/local/bin/anvil-container-entrypoint diff --git a/crates/cargo-anvil/templates/anvil/resolve-stable-toolchain.ps1 b/crates/cargo-anvil/templates/anvil/resolve-stable-toolchain.ps1 index a2c3fe460..7c8b1b36c 100644 --- a/crates/cargo-anvil/templates/anvil/resolve-stable-toolchain.ps1 +++ b/crates/cargo-anvil/templates/anvil/resolve-stable-toolchain.ps1 @@ -244,11 +244,7 @@ if ($EmitToolchainFileOptions) { exit 0 } if ($ValidateWorkspaceMsrv) { - $skipWorkspaceMsrvValidation = if ([string]::IsNullOrWhiteSpace($env:ANVIL_STABLE_TOOLCHAIN_SOURCE)) { - -not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN) - } else { - $env:ANVIL_STABLE_TOOLCHAIN_SOURCE -in @('environment', 'container') - } + $skipWorkspaceMsrvValidation = -not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN) if (-not $skipWorkspaceMsrvValidation -and $null -eq $fileSelection) { Assert-UniformWorkspaceMsrv } diff --git a/crates/cargo-anvil/templates/github/setup-action.yml b/crates/cargo-anvil/templates/github/setup-action.yml index 30f7402ca..051fbc3f9 100644 --- a/crates/cargo-anvil/templates/github/setup-action.yml +++ b/crates/cargo-anvil/templates/github/setup-action.yml @@ -80,24 +80,20 @@ runs: - name: Resolve stable toolchain shell: pwsh -NoProfile -Command ". '{0}'" run: | - $source = if ([string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { 'resolved' } else { 'environment' } - & '.anvil/resolve-stable-toolchain.ps1' -InstallIfMissing + & just _anvil-resolve-stable install if ($LASTEXITCODE -ne 0) { throw 'anvil: failed to install stable toolchain' } - $toolchain = (& '.anvil/resolve-stable-toolchain.ps1' | Out-String).Trim() + $toolchain = (& just _anvil-resolve-stable | Out-String).Trim() if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($toolchain)) { throw 'anvil: failed to resolve stable toolchain' } - $environmentValue = (& '.anvil/resolve-stable-toolchain.ps1' -ForEnvironment | Out-String).Trim() - if (-not [string]::IsNullOrWhiteSpace($environmentValue)) { - "RUSTUP_TOOLCHAIN=$environmentValue" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append - } - "ANVIL_STABLE_TOOLCHAIN_SOURCE=$source" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append Write-Host "anvil: selected stable toolchain '$toolchain'" - id: rustc-version - shell: bash - run: echo "version=$(rustc --version | awk '{print $2}')" >> "$GITHUB_OUTPUT" + shell: pwsh -NoProfile -Command ". '{0}'" + run: | + $version = (& just _anvil-with-stable rustc --version | Select-Object -Last 1).Split()[1] + "version=$version" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append - name: Restore cargo cache id: cargo-cache uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/aprz.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/aprz.just index 1627c3a59..897ec37c1 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/aprz.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/aprz.just @@ -44,7 +44,7 @@ anvil-aprz: anvil-aprz-validate-prereqs Write-Warning 'To fix: run `gh auth login` (recommended), or set $env:GITHUB_TOKEN to a GitHub token, then re-run.' } } - cargo aprz deps --error-if-high-risk --console appraisal + & "{{ just_executable() }}" _anvil-with-stable cargo aprz deps --error-if-high-risk --console appraisal if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-aprz` recipe. diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/audit.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/audit.just index 20de20468..d0aef3197 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/audit.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/audit.just @@ -12,7 +12,7 @@ [script("pwsh", "-NoProfile")] anvil-audit: anvil-audit-validate-prereqs $ErrorActionPreference = 'Stop' - & cargo audit + & "{{ just_executable() }}" _anvil-with-stable cargo audit if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-audit` recipe. diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/bench.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/bench.just index 2f4d42f74..7f8ca006f 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/bench.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/bench.just @@ -14,7 +14,7 @@ anvil-bench: anvil-bench-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include affected) if ($include -eq '--skip') { Write-Host 'anvil-bench: no affected packages; skipping'; exit 0 } - & cargo bench @(if ($include) { -split $include } else { '--workspace' }) --all-features --no-run + & "{{ just_executable() }}" _anvil-with-stable cargo bench @(if ($include) { -split $include } else { '--workspace' }) --all-features --no-run if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # bench uses the cargo built-in in compile-only mode; no extra tool install diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/bolero.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/bolero.just index 37f7d21ba..306bb9eea 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/bolero.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/bolero.just @@ -54,7 +54,7 @@ anvil-bolero: anvil-bolero-validate-prereqs anvil-impact $packageNames.Add(($pkgArgs[++$i] -split '@', 2)[0]) } } else { - $metadataJson = & cargo metadata --no-deps --format-version 1 + $metadataJson = & "{{ just_executable() }}" _anvil-with-stable cargo metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-bolero: cargo metadata failed' exit $LASTEXITCODE diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/careful.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/careful.just index be9871403..c937a5dc2 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/careful.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/careful.just @@ -59,7 +59,7 @@ anvil-careful: anvil-careful-validate-prereqs anvil-impact Write-Host " reason : the nightly toolchain and/or resolved cargo-careful executable changed" Write-Host " now : $idLabel" Write-Host " identity: $($prev.Substring(0, [Math]::Min(12, $prev.Length)))... -> $($idHash.Substring(0, 12))..." - cargo clean + & cargo '+{{ rust_nightly }}' clean if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } } # Persist the current identity (also seeds the marker on first run, when diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/cargo-hack.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/cargo-hack.just index ea604935a..c42680435 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/cargo-hack.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/cargo-hack.just @@ -16,7 +16,7 @@ anvil-cargo-hack: anvil-cargo-hack-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include required) if ($include -eq '--skip') { Write-Host 'anvil-cargo-hack: no affected packages; skipping'; exit 0 } - & cargo hack @(if ($include) { -split $include } else { '--workspace' }) --feature-powerset --depth 2 check + & "{{ just_executable() }}" _anvil-with-stable cargo hack @(if ($include) { -split $include } else { '--workspace' }) --feature-powerset --depth 2 check if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-cargo-hack` recipe. diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/cargo-sort.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/cargo-sort.just index a0af1ccb3..8de8ad7a9 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/cargo-sort.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/cargo-sort.just @@ -25,7 +25,7 @@ anvil-cargo-sort: anvil-cargo-sort-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include modified) if ($include -eq '--skip') { Write-Host 'anvil-cargo-sort: no modified packages; skipping'; exit 0 } - cargo sort --workspace --grouped --check --check-format + & "{{ just_executable() }}" _anvil-with-stable cargo sort --workspace --grouped --check --check-format if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-cargo-sort` recipe. diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/clippy.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/clippy.just index c9167c91c..5b9a8eb57 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/clippy.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/clippy.just @@ -19,7 +19,7 @@ anvil-clippy: anvil-clippy-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include affected) if ($include -eq '--skip') { Write-Host 'anvil-clippy: no affected packages; skipping'; exit 0 } - & cargo clippy @(if ($include) { -split $include } else { '--workspace' }) --all-targets --all-features --locked "--" '-D' 'warnings' + & "{{ just_executable() }}" _anvil-with-stable cargo clippy @(if ($include) { -split $include } else { '--workspace' }) --all-targets --all-features --locked "--" '-D' 'warnings' if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-clippy` recipe. diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/deny.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/deny.just index c52b613a4..17d098305 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/deny.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/deny.just @@ -14,7 +14,7 @@ [script("pwsh", "-NoProfile")] anvil-deny: anvil-deny-validate-prereqs $ErrorActionPreference = 'Stop' - & cargo deny check + & "{{ just_executable() }}" _anvil-with-stable cargo deny check if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-deny` recipe. diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/doc-build.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/doc-build.just index 9250d41af..b29336163 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/doc-build.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/doc-build.just @@ -17,7 +17,7 @@ anvil-doc-build: anvil-doc-build-validate-prereqs anvil-impact $include = (& "{{ just_executable() }}" _anvil-impact-include required) if ($include -eq '--skip') { Write-Host 'anvil-doc-build: no affected packages; skipping'; exit 0 } $env:RUSTDOCFLAGS = '-D warnings' - & cargo doc @(if ($include) { -split $include } else { '--workspace' }) --all-features --no-deps + & "{{ just_executable() }}" _anvil-with-stable cargo doc @(if ($include) { -split $include } else { '--workspace' }) --all-features --no-deps if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # doc-build, examples and doc-test are pure cargo built-ins; the rust diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/doc-test.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/doc-test.just index 1954ae4a7..1480f48d5 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/doc-test.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/doc-test.just @@ -20,9 +20,9 @@ anvil-doc-test: anvil-doc-test-validate-prereqs anvil-impact $include = (& "{{ just_executable() }}" _anvil-impact-include affected) if ($include -eq '--skip') { Write-Host 'anvil-doc-test: no affected packages; skipping'; exit 0 } $pkg = @(if ($include) { -split $include } else { '--workspace' }) - & cargo test --doc @pkg --all-features --locked + & "{{ just_executable() }}" _anvil-with-stable cargo test --doc @pkg --all-features --locked if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - & cargo test --doc @pkg --locked + & "{{ just_executable() }}" _anvil-with-stable cargo test --doc @pkg --locked if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-doc-test` recipe. diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/ensure-no-cyclic-deps.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/ensure-no-cyclic-deps.just index 48ef3e7c0..e74beab7c 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/ensure-no-cyclic-deps.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/ensure-no-cyclic-deps.just @@ -14,7 +14,7 @@ anvil-ensure-no-cyclic-deps: anvil-ensure-no-cyclic-deps-validate-prereqs anvil- $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include modified) if ($include -eq '--skip') { Write-Host 'anvil-ensure-no-cyclic-deps: no modified packages; skipping'; exit 0 } - cargo ensure-no-cyclic-deps + & "{{ just_executable() }}" _anvil-with-stable cargo ensure-no-cyclic-deps if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-ensure-no-cyclic-deps` recipe. diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/ensure-no-default-features.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/ensure-no-default-features.just index c56682046..9def94fa3 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/ensure-no-default-features.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/ensure-no-default-features.just @@ -14,7 +14,7 @@ anvil-ensure-no-default-features: anvil-ensure-no-default-features-validate-prer $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include modified) if ($include -eq '--skip') { Write-Host 'anvil-ensure-no-default-features: no modified packages; skipping'; exit 0 } - cargo ensure-no-default-features + & "{{ just_executable() }}" _anvil-with-stable cargo ensure-no-default-features if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-ensure-no-default-features` recipe. diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/examples.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/examples.just index 698b11157..47ab115f9 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/examples.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/examples.just @@ -14,7 +14,7 @@ anvil-examples: anvil-examples-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include affected) if ($include -eq '--skip') { Write-Host 'anvil-examples: no affected packages; skipping'; exit 0 } - & cargo build @(if ($include) { -split $include } else { '--workspace' }) --examples --all-features --locked + & "{{ just_executable() }}" _anvil-with-stable cargo build @(if ($include) { -split $include } else { '--workspace' }) --examples --all-features --locked if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-examples` recipe. diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/external-types.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/external-types.just index 33c0593d2..87287a42a 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/external-types.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/external-types.just @@ -31,7 +31,7 @@ anvil-external-types: anvil-external-types-validate-prereqs anvil-impact $pkg = @(if ($include) { -split $include } else { '--workspace' }) # Build pkg-name -> manifest-path map, restricted to library crates. $libPkgs = @{} - $metadataJson = & cargo metadata --no-deps --format-version 1 + $metadataJson = & "{{ just_executable() }}" _anvil-with-stable cargo metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-external-types: cargo metadata failed' exit $LASTEXITCODE diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/license-headers.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/license-headers.just index 989f57b4b..7f489f48a 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/license-headers.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/license-headers.just @@ -14,7 +14,7 @@ anvil-license-headers: anvil-license-headers-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include modified) if ($include -eq '--skip') { Write-Host 'anvil-license-headers: no modified packages; skipping'; exit 0 } - cargo heather + & "{{ just_executable() }}" _anvil-with-stable cargo heather if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-license-headers` recipe. diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/llvm-cov.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/llvm-cov.just index ab39d6717..fbace94e0 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/llvm-cov.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/llvm-cov.just @@ -37,7 +37,7 @@ anvil-llvm-cov: anvil-llvm-cov-validate-prereqs anvil-impact # never opts a changed package out of tests. $testOnlyPkg = [System.Collections.Generic.List[string]]::new() if ($pkg -contains '--package') { - $metadataJson = & cargo metadata --no-deps --format-version 1 + $metadataJson = & "{{ just_executable() }}" _anvil-with-stable cargo metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-llvm-cov: cargo metadata failed' exit $LASTEXITCODE @@ -168,7 +168,7 @@ anvil-llvm-cov: anvil-llvm-cov-validate-prereqs anvil-impact } $gateArgs.Add($pkg[$i]) } - cargo coverage-gate @gateArgs --lcov target/coverage/lcov-all-features.info --lcov target/coverage/lcov-no-default.info + & "{{ just_executable() }}" _anvil-with-stable cargo coverage-gate @gateArgs --lcov target/coverage/lcov-all-features.info --lcov target/coverage/lcov-no-default.info if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # --- pr-test members (shared with scheduled-test) --- diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/loom.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/loom.just index 0f88aba6b..4cc8441b3 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/loom.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/loom.just @@ -55,7 +55,7 @@ anvil-loom: anvil-loom-validate-prereqs anvil-impact Write-Host 'anvil-loom: no affected packages; skipping' exit 0 } - $meta = cargo metadata --no-deps --format-version 1 | ConvertFrom-Json + $meta = & "{{ just_executable() }}" _anvil-with-stable cargo metadata --no-deps --format-version 1 | ConvertFrom-Json if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Optional impact-scoping: recover bare package names from the @@ -108,7 +108,7 @@ anvil-loom: anvil-loom-validate-prereqs anvil-impact $env:RUSTFLAGS = "--cfg loom $($env:RUSTFLAGS)".Trim() foreach ($lt in $loomTargets) { Write-Host "anvil-loom: $($lt.pkg) :: $($lt.target)" - & cargo test -p $lt.pkg --release --all-features --locked --test $lt.target -- --test-threads=1 + & "{{ just_executable() }}" _anvil-with-stable cargo test -p $lt.pkg --release --all-features --locked --test $lt.target -- --test-threads=1 if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } } diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/msrv-test.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/msrv-test.just index 6e1ca1ddc..362e470c8 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/msrv-test.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/msrv-test.just @@ -12,7 +12,7 @@ anvil-msrv-test: anvil-msrv-test-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include affected) if ($include -eq '--skip') { Write-Host 'anvil-msrv-test: no affected packages; skipping'; exit 0 } - $toolchain = (& '.anvil/resolve-stable-toolchain.ps1' -MsrvToolchain | Out-String).Trim() + $toolchain = (& "{{ just_executable() }}" _anvil-resolve-stable msrv | Out-String).Trim() if ([string]::IsNullOrWhiteSpace($toolchain)) { Write-Host 'anvil-msrv-test: no root MSRV declared; skipping' exit 0 @@ -27,14 +27,14 @@ anvil-msrv-test: anvil-msrv-test-validate-prereqs anvil-impact [group("anvil-setup")] [script("pwsh", "-NoProfile")] anvil-msrv-test-setup installer="install": - & '.anvil/resolve-stable-toolchain.ps1' -InstallMsrvIfNeeded + & "{{ just_executable() }}" _anvil-resolve-stable install-msrv if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Validate the MSRV prerequisite. [group("anvil-setup")] [script("pwsh", "-NoProfile")] anvil-msrv-test-validate-prereqs: - $toolchain = (& '.anvil/resolve-stable-toolchain.ps1' -MsrvToolchain | Out-String).Trim() + $toolchain = (& "{{ just_executable() }}" _anvil-resolve-stable msrv | Out-String).Trim() if ([string]::IsNullOrWhiteSpace($toolchain)) { exit 0 } & cargo "+$toolchain" --version if ($LASTEXITCODE -ne 0) { diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/mutants-diff.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/mutants-diff.just index 25c98ba9a..e3c3c4234 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/mutants-diff.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/mutants-diff.just @@ -43,7 +43,7 @@ anvil-mutants-diff: anvil-mutants-diff-validate-prereqs anvil-impact $diff_path = Join-Path $tmp_dir 'anvil-mutants-diff.diff' git diff "$base..HEAD" --output=$diff_path if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - cargo mutants --in-diff $diff_path --no-shuffle --jobs 0 + & "{{ just_executable() }}" _anvil-with-stable cargo mutants --in-diff $diff_path --no-shuffle --jobs 0 if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # --- pr-mutants members --- diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/mutants-full.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/mutants-full.just index a914f1eb6..4009e7577 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/mutants-full.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/mutants-full.just @@ -17,7 +17,7 @@ anvil-mutants-full: anvil-mutants-full-validate-prereqs Write-Host 'anvil-mutants-full: aarch64-pc-windows-msvc -- cargo-mutants does not build here (winapi); skipping' exit 0 } - & cargo mutants --workspace --no-shuffle --jobs 0 + & "{{ just_executable() }}" _anvil-with-stable cargo mutants --workspace --no-shuffle --jobs 0 if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # --- scheduled-exhaustive members (mutants-full reuses cargo-mutants; diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/readme-check.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/readme-check.just index 1d258e25b..615fa4430 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/readme-check.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/readme-check.just @@ -47,7 +47,7 @@ anvil-readme-check: anvil-readme-check-validate-prereqs # Unscoped: a change to the workspace-level README template is a repo-level # input cargo-delta cannot map to a package, so this check always runs the # full library set rather than an impact subset. - $metadataJson = & cargo metadata --no-deps --format-version 1 + $metadataJson = & "{{ just_executable() }}" _anvil-with-stable cargo metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-readme-check: cargo metadata failed' exit $LASTEXITCODE @@ -89,7 +89,7 @@ anvil-readme-check: anvil-readme-check-validate-prereqs } $args = @('doc2readme', '--check') if ($relTemplate) { $args += @('--template', $relTemplate) } - & cargo @args + & "{{ just_executable() }}" _anvil-with-stable cargo @args if ($LASTEXITCODE -ne 0) { $hadFailure = $true } } finally { Pop-Location diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/semver-check.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/semver-check.just index 4f7112b6e..7cbac0ac6 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/semver-check.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/semver-check.just @@ -65,7 +65,7 @@ anvil-semver-check: anvil-semver-check-validate-prereqs anvil-impact # we got here via impact-scoping (cloud workflows) or full-workspace # fallback (local). $libPkgs = @{} - $metadataJson = & cargo metadata --no-deps --format-version 1 + $metadataJson = & "{{ just_executable() }}" _anvil-with-stable cargo metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-semver-check: cargo metadata failed' exit $LASTEXITCODE @@ -142,7 +142,7 @@ anvil-semver-check: anvil-semver-check-validate-prereqs anvil-impact } Write-Host "anvil-semver-check: $p (baseline $base)" - $outputLines = @(& cargo semver-checks --package $p --baseline-rev $base 2>&1) + $outputLines = @(& "{{ just_executable() }}" _anvil-with-stable cargo semver-checks --package $p --baseline-rev $base 2>&1) $semverExit = $LASTEXITCODE $output = ($outputLines | Out-String) if ($semverExit -eq 100) { diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/spellcheck.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/spellcheck.just index 15e734b47..805d43cc0 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/spellcheck.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/spellcheck.just @@ -42,9 +42,9 @@ anvil-spellcheck: anvil-spellcheck-validate-prereqs # ignores user-curated dictionaries (`extra_dictionaries`, custom # hunspell langs, etc.). if (Test-Path 'spellcheck.toml') { - cargo spellcheck --cfg spellcheck.toml check --code 1 + & "{{ just_executable() }}" _anvil-with-stable cargo spellcheck --cfg spellcheck.toml check --code 1 } else { - cargo spellcheck check --code 1 + & "{{ just_executable() }}" _anvil-with-stable cargo spellcheck check --code 1 } if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } diff --git a/crates/cargo-anvil/templates/justfiles/anvil/impact.just b/crates/cargo-anvil/templates/justfiles/anvil/impact.just index 42d745026..56223c8aa 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/impact.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/impact.just @@ -235,19 +235,25 @@ _anvil-impact-snapshot: # The baseline worktree is checked out at the merge target, whose # rust-toolchain.toml may pin a toolchain that is NOT installed on this # machine (any PR that bumps rust-toolchain.toml hits this). The - # snapshot is metadata-only (file presence + content hashes), so run it - # under the *active* toolchain via RUSTUP_TOOLCHAIN -- resolved here, in - # the current checkout, where it is installed -- which overrides the - # worktree's rust-toolchain.toml. Best-effort: skip if rustup is absent. - $activeToolchain = $null - # `rustup` may be absent (e.g. ADO msrustup provides `cargo` directly - # without a `rustup` command). Under `$ErrorActionPreference = 'Stop'` - # a bare `rustup` call would raise a terminating command-not-found - # error before the `$LASTEXITCODE` guard runs, so probe for the command - # first and leave `$activeToolchain` null when it is unavailable. - if (Get-Command rustup -ErrorAction SilentlyContinue) { - $rt = (rustup show active-toolchain 2>$null) - if (($LASTEXITCODE -eq 0) -and $rt) { $activeToolchain = (($rt | Select-Object -First 1) -split '\s+')[0] } + # snapshot is metadata-only (file presence + content hashes), so use + # the selected compiler from the current checkout to override any + # different toolchain file in the baseline worktree. Environment/MSRV + # selections are already valid rustup overrides. For a selecting + # toolchain file, ask rustup for the concrete active identifier so + # `path` selections are not mistaken for named toolchains. + $baselineToolchain = (& "{{ just_executable() }}" _anvil-resolve-stable for-environment | Out-String).Trim() + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + if ([string]::IsNullOrWhiteSpace($baselineToolchain)) { + if (-not (Get-Command rustup -ErrorAction SilentlyContinue)) { + Write-Error 'anvil-impact: rustup is required to resolve the active repository toolchain for the baseline snapshot' + exit 1 + } + $active = (rustup show active-toolchain 2>$null) + if (($LASTEXITCODE -ne 0) -or -not $active) { + Write-Error 'anvil-impact: failed to resolve the active repository toolchain for the baseline snapshot' + exit 1 + } + $baselineToolchain = (($active | Select-Object -First 1) -split '\s+')[0] } # Temp-root precedence follows the runner-managed scratch dir on each @@ -280,14 +286,12 @@ _anvil-impact-snapshot: Set-Content -LiteralPath $baselineKeyFile -Value $baselineKey -Encoding UTF8 -NoNewline } else { Push-Location $wt - # Preserve any caller-provided RUSTUP_TOOLCHAIN: we override it - # only for this baseline snapshot, then restore the caller's - # value (or remove it if we introduced it) so the current - # snapshot below runs under the same toolchain the caller chose. + # Override only this baseline subprocess scope, then restore the + # caller's input override before returning to the current tree. $hadToolchain = Test-Path Env:\RUSTUP_TOOLCHAIN $priorToolchain = if ($hadToolchain) { $env:RUSTUP_TOOLCHAIN } else { $null } try { - if ($activeToolchain) { $env:RUSTUP_TOOLCHAIN = $activeToolchain } + $env:RUSTUP_TOOLCHAIN = $baselineToolchain $baseSnap = cargo delta @deltaArgs snapshot if ($LASTEXITCODE -ne 0) { throw 'cargo delta snapshot (baseline) failed' } } finally { @@ -314,7 +318,7 @@ _anvil-impact-snapshot: Write-Host 'anvil-impact: current snapshot up to date' } else { Write-Host 'anvil-impact: snapshotting working tree' - $curSnap = cargo delta @deltaArgs snapshot + $curSnap = & "{{ just_executable() }}" _anvil-with-stable cargo delta @deltaArgs snapshot if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-impact: cargo delta snapshot (current) failed'; exit 1 } Set-Content -LiteralPath $currentJson -Value $curSnap -Encoding UTF8 Set-Content -LiteralPath $currentKeyFile -Value $currentState -Encoding UTF8 -NoNewline @@ -432,7 +436,7 @@ anvil-impact: _anvil-impact-snapshot # early), so default to an empty object so impact.json always exists -- # the freshness check above keys on its presence, and a missing file # would force a recompute on every invocation. - $impactOut = (cargo delta @deltaArgs impact --baseline $baselineJson --current $currentJson --format json | Out-String) + $impactOut = (& "{{ just_executable() }}" _anvil-with-stable cargo delta @deltaArgs impact --baseline $baselineJson --current $currentJson --format json | Out-String) if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-impact: cargo delta impact failed'; exit 1 } if ([string]::IsNullOrWhiteSpace($impactOut)) { $impactOut = '{}' } Set-Content -LiteralPath $impactJson -Value $impactOut.Trim() -Encoding UTF8 @@ -509,7 +513,7 @@ _anvil-impact-format tier impactJson: # nested workspaces cargo-delta can instead emit a manifest directory # leaf; accept any reported identifier only when every matching namespace # resolves to the *same* workspace package. - $metadataJson = & cargo metadata --no-deps --format-version 1 + $metadataJson = & "{{ just_executable() }}" _anvil-with-stable cargo metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-impact: cargo metadata failed' exit $LASTEXITCODE diff --git a/crates/cargo-anvil/templates/justfiles/anvil/tools.just b/crates/cargo-anvil/templates/justfiles/anvil/tools.just index 1de4bcaf5..54ef872d2 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/tools.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/tools.just @@ -144,7 +144,7 @@ anvil-tool-rustc-validate-prereqs: Write-Error 'anvil: rustc not found. Install via rustup: https://rustup.rs' exit 1 } - & pwsh -NoProfile -File '.anvil/resolve-stable-toolchain.ps1' -ValidateWorkspaceMsrv + & "{{ just_executable() }}" _anvil-resolve-stable validate-workspace-msrv if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } @@ -173,6 +173,41 @@ anvil-tool-pwsh-validate-prereqs: # Private helpers (install/check primitives) # ============================================================================ +# Resolve or prepare the selected stable compiler without globally exporting it. +[script("pwsh", "-NoProfile")] +_anvil-resolve-stable action="resolve": + $ErrorActionPreference = 'Stop' + $resolverArgs = @(switch ('{{action}}') { + 'resolve' { @() } + 'for-environment' { @('-ForEnvironment') } + 'install' { @('-InstallIfMissing') } + 'apply-file-options' { @('-ApplyToolchainFileOptions') } + 'validate-workspace-msrv' { @('-ValidateWorkspaceMsrv') } + 'msrv' { @('-MsrvToolchain') } + 'install-msrv' { @('-InstallMsrvIfNeeded') } + default { + Write-Error "_anvil-resolve-stable: unknown action '{{action}}'" + exit 2 + } + }) + & pwsh -NoProfile -File '.anvil/resolve-stable-toolchain.ps1' @resolverArgs + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + +# Run one command under the selected stable compiler. A repository toolchain +# file yields no override so rustup processes its complete configuration. +[script("pwsh", "-NoProfile")] +_anvil-with-stable command *args: + $ErrorActionPreference = 'Stop' + $toolchain = (& "{{ just_executable() }}" _anvil-resolve-stable for-environment | Out-String).Trim() + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + if ([string]::IsNullOrWhiteSpace($toolchain)) { + Remove-Item Env:\RUSTUP_TOOLCHAIN -ErrorAction SilentlyContinue + } else { + $env:RUSTUP_TOOLCHAIN = $toolchain + } + & '{{command}}' {{args}} + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + # _install-tool: install a cargo subcommand at the catalog version, # or no-op if it is already installed at or above that version. The # `installer` parameter selects between: @@ -203,7 +238,7 @@ _install-tool name version installer source_prereq="": # cached binaries and fail with "binary already exists in destination". $installed = $null $pattern = '^' + [regex]::Escape($name) + ' v(\S+):' - foreach ($line in (cargo install --list 2>$null)) { + foreach ($line in (& "{{ just_executable() }}" _anvil-with-stable cargo install --list 2>$null)) { if ($line -match $pattern) { $installed = $Matches[1]; break } } if ($installed) { @@ -221,7 +256,7 @@ _install-tool name version installer source_prereq="": if ($installer -eq 'binstall') { if (-not (Get-Command cargo-binstall -ErrorAction SilentlyContinue)) { Write-Host ' Bootstrapping cargo-binstall' - cargo install --locked cargo-binstall + & "{{ just_executable() }}" _anvil-with-stable cargo install --locked cargo-binstall if ($LASTEXITCODE -ne 0) { Write-Error 'cargo-binstall bootstrap failed' exit $LASTEXITCODE @@ -235,7 +270,7 @@ _install-tool name version installer source_prereq="": $binstallArgs += @('--disable-strategies', 'compile') } $binstallArgs += @($name, '--version', "=$version") - cargo @binstallArgs + & "{{ just_executable() }}" _anvil-with-stable cargo @binstallArgs if ($LASTEXITCODE -eq 0) { exit 0 } Write-Host ' binstall failed; falling back to cargo install' -ForegroundColor Yellow } @@ -243,7 +278,7 @@ _install-tool name version installer source_prereq="": & "{{just_executable()}}" $sourcePrereq if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } } - cargo install --locked $name --version "=$version" + & "{{ just_executable() }}" _anvil-with-stable cargo install --locked $name --version "=$version" if ($LASTEXITCODE -ne 0) { Write-Error "$name install FAILED" exit $LASTEXITCODE @@ -259,7 +294,7 @@ _check-tool name version: $installed = $null $pattern = '^' + [regex]::Escape($name) + ' v(\S+):' - foreach ($line in (cargo install --list 2>$null)) { + foreach ($line in (& "{{ just_executable() }}" _anvil-with-stable cargo install --list 2>$null)) { if ($line -match $pattern) { $installed = $Matches[1]; break } } if (-not $installed) { @@ -340,14 +375,14 @@ _install-component toolchain component: } if ($null -ne $subcommand) { if ($toolchain -eq 'default') { - & cargo $subcommand --version *> $null + & "{{ just_executable() }}" _anvil-with-stable cargo $subcommand --version *> $null } else { & cargo "+$toolchain" $subcommand --version *> $null } $present = ($LASTEXITCODE -eq 0) } else { $sysroot = if ($toolchain -eq 'default') { - & rustc --print sysroot 2>$null + & "{{ just_executable() }}" _anvil-with-stable rustc --print sysroot 2>$null } else { & rustc "+$toolchain" --print sysroot 2>$null } @@ -408,14 +443,14 @@ _check-component toolchain component: } if ($null -ne $subcommand) { if ($toolchain -eq 'default') { - & cargo $subcommand --version *> $null + & "{{ just_executable() }}" _anvil-with-stable cargo $subcommand --version *> $null } else { & cargo "+$toolchain" $subcommand --version *> $null } $present = ($LASTEXITCODE -eq 0) } else { $sysroot = if ($toolchain -eq 'default') { - & rustc --print sysroot 2>$null + & "{{ just_executable() }}" _anvil-with-stable rustc --print sysroot 2>$null } else { & rustc "+$toolchain" --print sysroot 2>$null } diff --git a/crates/cargo-anvil/templates/justfiles/anvil/versions.just b/crates/cargo-anvil/templates/justfiles/anvil/versions.just index 888224ea1..e2441b62a 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/versions.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/versions.just @@ -30,24 +30,9 @@ # Rustup toolchains # ============================================================================ -# Stable toolchain used by every check without an explicit +toolchain argument. -# The resolver honors RUSTUP_TOOLCHAIN, then a selecting root toolchain file, -# then the root package/workspace MSRV. Environment and MSRV selections are -# exported here; a toolchain-file selection stays empty so rustup honors the -# file's complete targets/components/profile configuration. -rust_stable_override := env_var_or_default('RUSTUP_TOOLCHAIN', '') -# The resolver command ends in a comment so PowerShell's commonly configured -# -Command shell ignores Just's argv[0] copy instead of joining it back in. -rust_stable := if rust_stable_override != '' { - rust_stable_override -} else { - shell('pwsh -NoProfile -File .anvil/resolve-stable-toolchain.ps1 -ForEnvironment #') -} -export RUSTUP_TOOLCHAIN := rust_stable -export ANVIL_STABLE_TOOLCHAIN_SOURCE := env_var_or_default( - 'ANVIL_STABLE_TOOLCHAIN_SOURCE', - if rust_stable_override == '' { 'resolved' } else { 'environment' }, -) +# Stable checks select their compiler through `_anvil-with-stable` at the +# individual command site. `RUSTUP_TOOLCHAIN` is an input override only; Anvil +# never exports a resolved value globally into unrelated recipes or helpers. # General nightly used by udeps, miri, careful, and any future # nightly-dependent check. Bumped on a regular cadence (monthly is a diff --git a/crates/cargo-anvil/tests/impact.rs b/crates/cargo-anvil/tests/impact.rs index 9cec814d8..40505993f 100644 --- a/crates/cargo-anvil/tests/impact.rs +++ b/crates/cargo-anvil/tests/impact.rs @@ -245,10 +245,6 @@ fn workspace() -> TempDir { /// `GITHUB_BASE_REF=main`); inherited, `_anvil-base-ref` would resolve /// `origin/main`, which does not exist in the temp repo (whose base is /// `origin/master`), and the snapshot would fail base-ref resolution. -/// - `ANVIL_STABLE_TOOLCHAIN_SOURCE`: cloud setup sets this for the outer -/// repository; inherited, resolver validation would treat the fixture's -/// toolchain as its own resolved selection and invoke intentionally fake -/// `cargo metadata` shims used by impact tests. /// /// Tests that need a specific mode or base set the var back on the returned /// Command. @@ -260,7 +256,6 @@ fn just_cmd(root: &Path, args: &[&str]) -> Command { .env_remove("BASE_REF") .env_remove("GITHUB_BASE_REF") .env_remove("SYSTEM_PULLREQUEST_TARGETBRANCH") - .env_remove("ANVIL_STABLE_TOOLCHAIN_SOURCE") .current_dir(root); cmd } @@ -1000,7 +995,6 @@ fn msrv_test_uses_affected_packages_for_both_feature_modes_and_skips_without_msr let run_msrv = || { just_cmd(root, &["anvil-msrv-test"]) .env("ANVIL_IMPACT", "consume") - .env("ANVIL_STABLE_TOOLCHAIN_SOURCE", "environment") .env("RUSTUP_TOOLCHAIN", "test-stable") .env("PATH", &path) .output() @@ -1386,18 +1380,20 @@ fn impact_format_maps_proc_macro_target_name_to_its_package() { } /// Drive `just _anvil-impact-snapshot` under a `cargo` shim that records the -/// `RUSTUP_TOOLCHAIN` in effect at each `cargo delta snapshot` and a `rustup` -/// shim that reports a fixed active toolchain, returning the two logged values -/// in order: `[baseline, current]`. `caller_toolchain` is the value the caller -/// exports (or `None` to leave it unset). -fn snapshot_toolchain_probe(caller_toolchain: Option<&str>) -> Vec { +/// `RUSTUP_TOOLCHAIN` in effect at each `cargo delta snapshot`, returning the +/// two logged values in order: `[baseline, current]`. `caller_toolchain` is the +/// input override the caller exports (or `None` to use repository selection). +fn snapshot_toolchain_probe(caller_toolchain: Option<&str>, toolchain_file: bool) -> Vec { let tmp = workspace(); let root = tmp.path(); + if toolchain_file { + write(&root.join("rust-toolchain.toml"), "[toolchain]\npath = \"toolchains/local\"\n"); + git(root, &["add", "rust-toolchain.toml"]); + git(root, &["commit", "-q", "-m", "select local toolchain"]); + } // cargo shim: log the active RUSTUP_TOOLCHAIN on each `delta snapshot` // (emitting `{}` as the snapshot), satisfy the cargo-delta prereq probe - // (`cargo install --list`), and no-op everything else. rustup shim: report - // a fixed active toolchain, distinct from any caller value, so the baseline - // override is unambiguous. + // (`cargo install --list`), and no-op everything else. let shim = ShimBin::new(&[ ( "cargo.ps1", @@ -1416,7 +1412,7 @@ fn snapshot_toolchain_probe(caller_toolchain: Option<&str>) -> Vec { ( "rustup.ps1", "if (($args -contains 'show') -and ($args -contains 'active-toolchain')) {\n\ - \x20 Write-Output 'anvil-active-toolchain'\n\ + \x20 Write-Output 'file-active-toolchain (overridden by rust-toolchain.toml)'\n\ \x20 exit 0\n\ }\n\ exit 0\n", @@ -1440,34 +1436,39 @@ fn snapshot_toolchain_probe(caller_toolchain: Option<&str>) -> Vec { } #[test] -fn impact_snapshot_runs_baseline_under_active_toolchain_then_restores_caller_value() { +fn impact_snapshot_uses_current_checkout_toolchain_for_both_trees() { if !core_tools_available() { return; } // The baseline snapshot is taken inside a worktree checked out at the merge // target, whose rust-toolchain.toml may pin a toolchain that is NOT - // installed here (any PR that bumps rust-toolchain.toml). The recipe runs - // that snapshot under the *active* toolchain via RUSTUP_TOOLCHAIN, then must - // put the caller's value back so the *current* snapshot runs under the - // toolchain Anvil selected -- restoring either the caller override or the - // deterministic MSRV fallback. A regression that forgets to restore would - // leak the baseline override into the current snapshot. - - // Case A: the caller pins RUSTUP_TOOLCHAIN. Baseline runs under the active - // toolchain; the current snapshot must see the caller's pin restored. - let with_caller = snapshot_toolchain_probe(Some("caller-pin-toolchain")); + // installed here (any PR that bumps rust-toolchain.toml). Both snapshots + // must therefore use the compiler selected from the current checkout so + // cargo-delta compares metadata produced under one compiler policy. + + // Case A: the caller override is the selected compiler for both trees. + let with_caller = snapshot_toolchain_probe(Some("caller-pin-toolchain"), false); assert_eq!( with_caller, - vec!["anvil-active-toolchain".to_owned(), "caller-pin-toolchain".to_owned()], - "baseline must snapshot under the active toolchain, then the caller's RUSTUP_TOOLCHAIN must be restored for the current snapshot" + vec!["caller-pin-toolchain".to_owned(), "caller-pin-toolchain".to_owned()], + "baseline and current snapshots must both use the caller's selected compiler" ); - // Case B: the caller sets nothing. Anvil selects the workspace MSRV, and - // the recipe must restore that value after the temporary baseline override. - let without_caller = snapshot_toolchain_probe(None); + // Case B: without an override, the workspace MSRV selects both snapshots. + let without_caller = snapshot_toolchain_probe(None, false); assert_eq!( without_caller, - vec!["anvil-active-toolchain".to_owned(), "1.97".to_owned()], - "with no caller RUSTUP_TOOLCHAIN, the recipe must restore Anvil's selected MSRV after the baseline override" + vec!["1.97".to_owned(), "1.97".to_owned()], + "baseline and current snapshots must both use Anvil's selected MSRV" + ); + + // Case C: a toolchain file is selected natively in the current checkout. + // The baseline needs rustup's concrete active identifier because the file + // may contain a path that cannot be assigned directly to RUSTUP_TOOLCHAIN. + let with_file = snapshot_toolchain_probe(None, true); + assert_eq!( + with_file, + vec!["file-active-toolchain".to_owned(), "".to_owned()], + "baseline must use the current file's concrete active toolchain while the current snapshot lets rustup process that file natively" ); } diff --git a/crates/cargo-anvil/tests/recipe_contracts.rs b/crates/cargo-anvil/tests/recipe_contracts.rs index 311903589..5b7ea9c10 100644 --- a/crates/cargo-anvil/tests/recipe_contracts.rs +++ b/crates/cargo-anvil/tests/recipe_contracts.rs @@ -25,6 +25,7 @@ const SEMVER: &str = include_str!("../templates/justfiles/anvil/checks/semver-ch const EXTERNAL_TYPES: &str = include_str!("../templates/justfiles/anvil/checks/external-types.just"); const TOOLS: &str = include_str!("../templates/justfiles/anvil/tools.just"); const VERSIONS: &str = include_str!("../templates/justfiles/anvil/versions.just"); +const STABLE_TOOLCHAIN_RESOLVER: &str = include_str!("../templates/anvil/resolve-stable-toolchain.ps1"); const FAKE_CARGO_PS1: &str = r#" $joined = $args -join ' ' if ($env:FAKE_CARGO_LOG) { @@ -147,6 +148,14 @@ fn fixture(imports: &[(&str, &str)], dependency_recipes: &[&str]) -> TempDir { write(&tmp.path().join(name), contents); writeln!(justfile, "import '{name}'").unwrap(); } + if !imports.iter().any(|(name, _)| *name == "tools.just") { + justfile.push_str( + "\n[script(\"pwsh\", \"-NoProfile\")]\n\ + _anvil-with-stable command *args:\n\ + \x20 & '{{command}}' {{args}}\n\ + \x20 if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }\n", + ); + } justfile.push('\n'); for recipe in dependency_recipes { justfile.push_str(recipe); @@ -155,8 +164,9 @@ fn fixture(imports: &[(&str, &str)], dependency_recipes: &[&str]) -> TempDir { write(&tmp.path().join("Justfile"), &justfile); write( &tmp.path().join("Cargo.toml"), - "[package]\nname = \"fixture\"\nversion = \"0.1.0\"\n", + "[package]\nname = \"fixture\"\nversion = \"0.1.0\"\nrust-version = \"1.97\"\n", ); + write(&tmp.path().join(".anvil/resolve-stable-toolchain.ps1"), STABLE_TOOLCHAIN_RESOLVER); let bin = tmp.path().join("fake-bin"); fs::create_dir_all(&bin).unwrap(); diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap index 8255a0076..fedacb966 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap @@ -57,11 +57,9 @@ COPY . . RUN --mount=type=cache,id=anvil-cargo-registry,target=/usr/local/cargo/registry \ --mount=type=cache,id=anvil-cargo-git,target=/usr/local/cargo/git \ --mount=type=cache,id=anvil-cargo-target,target=/tmp/anvil-target \ - pwsh -NoProfile -File .anvil/resolve-stable-toolchain.ps1 -InstallIfMissing \ - && toolchain="$(pwsh -NoProfile -File .anvil/resolve-stable-toolchain.ps1 -ForEnvironment)" \ - && printf "anvil_runner := \"native\"\nimport 'justfiles/anvil/mod.just'\n" > Justfile \ - && RUSTUP_TOOLCHAIN="$toolchain" ANVIL_STABLE_TOOLCHAIN_SOURCE=container \ - CARGO_TARGET_DIR=/tmp/anvil-target just anvil-setup + printf "anvil_runner := \"native\"\nimport 'justfiles/anvil/mod.just'\n" > Justfile \ + && just _anvil-resolve-stable install \ + && CARGO_TARGET_DIR=/tmp/anvil-target just anvil-setup COPY .anvil/container/entrypoint.sh /usr/local/bin/anvil-container-entrypoint RUN chmod 755 /usr/local/bin/anvil-container-entrypoint @@ -1442,11 +1440,7 @@ if ($EmitToolchainFileOptions) { exit 0 } if ($ValidateWorkspaceMsrv) { - $skipWorkspaceMsrvValidation = if ([string]::IsNullOrWhiteSpace($env:ANVIL_STABLE_TOOLCHAIN_SOURCE)) { - -not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN) - } else { - $env:ANVIL_STABLE_TOOLCHAIN_SOURCE -in @('environment', 'container') - } + $skipWorkspaceMsrvValidation = -not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN) if (-not $skipWorkspaceMsrvValidation -and $null -eq $fileSelection) { Assert-UniformWorkspaceMsrv } @@ -2776,23 +2770,19 @@ parameters: default: '' steps: - pwsh: | - & '.anvil/resolve-stable-toolchain.ps1' -ApplyToolchainFileOptions + & just _anvil-resolve-stable apply-file-options if ($LASTEXITCODE -ne 0) { throw "anvil: stable toolchain option setup failed with exit code $LASTEXITCODE" } - $source = if ([string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { 'resolved' } else { 'environment' } - $toolchain = (& '.anvil/resolve-stable-toolchain.ps1' | Out-String).Trim() + $toolchain = (& just _anvil-resolve-stable | Out-String).Trim() if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($toolchain)) { throw 'anvil: failed to resolve stable toolchain' } - $environmentValue = (& '.anvil/resolve-stable-toolchain.ps1' -ForEnvironment | Out-String).Trim() - if (-not [string]::IsNullOrWhiteSpace($environmentValue)) { - Write-Host "##vso[task.setvariable variable=RUSTUP_TOOLCHAIN]$environmentValue" - } - Write-Host "##vso[task.setvariable variable=ANVIL_STABLE_TOOLCHAIN_SOURCE]$source" Write-Host "anvil: selected stable toolchain '$toolchain'" displayName: anvil setup (resolve stable toolchain) - - bash: echo "##vso[task.setvariable variable=anvil_rustc_version]$(rustc --version | awk '{print $2}')" + - pwsh: | + $version = (& just _anvil-with-stable rustc --version | Select-Object -Last 1).Split()[1] + Write-Host "##vso[task.setvariable variable=anvil_rustc_version]$version" displayName: anvil setup (capture rustc version) - task: Cache@2 inputs: @@ -3124,7 +3114,7 @@ anvil-aprz: anvil-aprz-validate-prereqs Write-Warning 'To fix: run `gh auth login` (recommended), or set $env:GITHUB_TOKEN to a GitHub token, then re-run.' } } - cargo aprz deps --error-if-high-risk --console appraisal + & "{{ just_executable() }}" _anvil-with-stable cargo aprz deps --error-if-high-risk --console appraisal if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-aprz` recipe. @@ -3150,7 +3140,7 @@ anvil-aprz-validate-prereqs: anvil-tool-cargo-aprz-validate-prereqs [script("pwsh", "-NoProfile")] anvil-audit: anvil-audit-validate-prereqs $ErrorActionPreference = 'Stop' - & cargo audit + & "{{ just_executable() }}" _anvil-with-stable cargo audit if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-audit` recipe. @@ -3178,7 +3168,7 @@ anvil-bench: anvil-bench-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include affected) if ($include -eq '--skip') { Write-Host 'anvil-bench: no affected packages; skipping'; exit 0 } - & cargo bench @(if ($include) { -split $include } else { '--workspace' }) --all-features --no-run + & "{{ just_executable() }}" _anvil-with-stable cargo bench @(if ($include) { -split $include } else { '--workspace' }) --all-features --no-run if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # bench uses the cargo built-in in compile-only mode; no extra tool install @@ -3249,7 +3239,7 @@ anvil-bolero: anvil-bolero-validate-prereqs anvil-impact $packageNames.Add(($pkgArgs[++$i] -split '@', 2)[0]) } } else { - $metadataJson = & cargo metadata --no-deps --format-version 1 + $metadataJson = & "{{ just_executable() }}" _anvil-with-stable cargo metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-bolero: cargo metadata failed' exit $LASTEXITCODE @@ -3376,7 +3366,7 @@ anvil-careful: anvil-careful-validate-prereqs anvil-impact Write-Host " reason : the nightly toolchain and/or resolved cargo-careful executable changed" Write-Host " now : $idLabel" Write-Host " identity: $($prev.Substring(0, [Math]::Min(12, $prev.Length)))... -> $($idHash.Substring(0, 12))..." - cargo clean + & cargo '+{{ rust_nightly }}' clean if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } } # Persist the current identity (also seeds the marker on first run, when @@ -3413,7 +3403,7 @@ anvil-cargo-hack: anvil-cargo-hack-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include required) if ($include -eq '--skip') { Write-Host 'anvil-cargo-hack: no affected packages; skipping'; exit 0 } - & cargo hack @(if ($include) { -split $include } else { '--workspace' }) --feature-powerset --depth 2 check + & "{{ just_executable() }}" _anvil-with-stable cargo hack @(if ($include) { -split $include } else { '--workspace' }) --feature-powerset --depth 2 check if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-cargo-hack` recipe. @@ -3452,7 +3442,7 @@ anvil-cargo-sort: anvil-cargo-sort-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include modified) if ($include -eq '--skip') { Write-Host 'anvil-cargo-sort: no modified packages; skipping'; exit 0 } - cargo sort --workspace --grouped --check --check-format + & "{{ just_executable() }}" _anvil-with-stable cargo sort --workspace --grouped --check --check-format if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-cargo-sort` recipe. @@ -3485,7 +3475,7 @@ anvil-clippy: anvil-clippy-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include affected) if ($include -eq '--skip') { Write-Host 'anvil-clippy: no affected packages; skipping'; exit 0 } - & cargo clippy @(if ($include) { -split $include } else { '--workspace' }) --all-targets --all-features --locked "--" '-D' 'warnings' + & "{{ just_executable() }}" _anvil-with-stable cargo clippy @(if ($include) { -split $include } else { '--workspace' }) --all-targets --all-features --locked "--" '-D' 'warnings' if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-clippy` recipe. @@ -3513,7 +3503,7 @@ anvil-clippy-validate-prereqs: anvil-component-default-clippy-validate-prereqs [script("pwsh", "-NoProfile")] anvil-deny: anvil-deny-validate-prereqs $ErrorActionPreference = 'Stop' - & cargo deny check + & "{{ just_executable() }}" _anvil-with-stable cargo deny check if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-deny` recipe. @@ -3544,7 +3534,7 @@ anvil-doc-build: anvil-doc-build-validate-prereqs anvil-impact $include = (& "{{ just_executable() }}" _anvil-impact-include required) if ($include -eq '--skip') { Write-Host 'anvil-doc-build: no affected packages; skipping'; exit 0 } $env:RUSTDOCFLAGS = '-D warnings' - & cargo doc @(if ($include) { -split $include } else { '--workspace' }) --all-features --no-deps + & "{{ just_executable() }}" _anvil-with-stable cargo doc @(if ($include) { -split $include } else { '--workspace' }) --all-features --no-deps if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # doc-build, examples and doc-test are pure cargo built-ins; the rust @@ -3582,9 +3572,9 @@ anvil-doc-test: anvil-doc-test-validate-prereqs anvil-impact $include = (& "{{ just_executable() }}" _anvil-impact-include affected) if ($include -eq '--skip') { Write-Host 'anvil-doc-test: no affected packages; skipping'; exit 0 } $pkg = @(if ($include) { -split $include } else { '--workspace' }) - & cargo test --doc @pkg --all-features --locked + & "{{ just_executable() }}" _anvil-with-stable cargo test --doc @pkg --all-features --locked if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - & cargo test --doc @pkg --locked + & "{{ just_executable() }}" _anvil-with-stable cargo test --doc @pkg --locked if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-doc-test` recipe. @@ -3612,7 +3602,7 @@ anvil-ensure-no-cyclic-deps: anvil-ensure-no-cyclic-deps-validate-prereqs anvil- $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include modified) if ($include -eq '--skip') { Write-Host 'anvil-ensure-no-cyclic-deps: no modified packages; skipping'; exit 0 } - cargo ensure-no-cyclic-deps + & "{{ just_executable() }}" _anvil-with-stable cargo ensure-no-cyclic-deps if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-ensure-no-cyclic-deps` recipe. @@ -3640,7 +3630,7 @@ anvil-ensure-no-default-features: anvil-ensure-no-default-features-validate-prer $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include modified) if ($include -eq '--skip') { Write-Host 'anvil-ensure-no-default-features: no modified packages; skipping'; exit 0 } - cargo ensure-no-default-features + & "{{ just_executable() }}" _anvil-with-stable cargo ensure-no-default-features if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-ensure-no-default-features` recipe. @@ -3668,7 +3658,7 @@ anvil-examples: anvil-examples-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include affected) if ($include -eq '--skip') { Write-Host 'anvil-examples: no affected packages; skipping'; exit 0 } - & cargo build @(if ($include) { -split $include } else { '--workspace' }) --examples --all-features --locked + & "{{ just_executable() }}" _anvil-with-stable cargo build @(if ($include) { -split $include } else { '--workspace' }) --examples --all-features --locked if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-examples` recipe. @@ -3713,7 +3703,7 @@ anvil-external-types: anvil-external-types-validate-prereqs anvil-impact $pkg = @(if ($include) { -split $include } else { '--workspace' }) # Build pkg-name -> manifest-path map, restricted to library crates. $libPkgs = @{} - $metadataJson = & cargo metadata --no-deps --format-version 1 + $metadataJson = & "{{ just_executable() }}" _anvil-with-stable cargo metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-external-types: cargo metadata failed' exit $LASTEXITCODE @@ -3841,7 +3831,7 @@ anvil-license-headers: anvil-license-headers-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include modified) if ($include -eq '--skip') { Write-Host 'anvil-license-headers: no modified packages; skipping'; exit 0 } - cargo heather + & "{{ just_executable() }}" _anvil-with-stable cargo heather if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-license-headers` recipe. @@ -3892,7 +3882,7 @@ anvil-llvm-cov: anvil-llvm-cov-validate-prereqs anvil-impact # never opts a changed package out of tests. $testOnlyPkg = [System.Collections.Generic.List[string]]::new() if ($pkg -contains '--package') { - $metadataJson = & cargo metadata --no-deps --format-version 1 + $metadataJson = & "{{ just_executable() }}" _anvil-with-stable cargo metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-llvm-cov: cargo metadata failed' exit $LASTEXITCODE @@ -4023,7 +4013,7 @@ anvil-llvm-cov: anvil-llvm-cov-validate-prereqs anvil-impact } $gateArgs.Add($pkg[$i]) } - cargo coverage-gate @gateArgs --lcov target/coverage/lcov-all-features.info --lcov target/coverage/lcov-no-default.info + & "{{ just_executable() }}" _anvil-with-stable cargo coverage-gate @gateArgs --lcov target/coverage/lcov-all-features.info --lcov target/coverage/lcov-no-default.info if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # --- pr-test members (shared with scheduled-test) --- @@ -4094,7 +4084,7 @@ anvil-loom: anvil-loom-validate-prereqs anvil-impact Write-Host 'anvil-loom: no affected packages; skipping' exit 0 } - $meta = cargo metadata --no-deps --format-version 1 | ConvertFrom-Json + $meta = & "{{ just_executable() }}" _anvil-with-stable cargo metadata --no-deps --format-version 1 | ConvertFrom-Json if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Optional impact-scoping: recover bare package names from the @@ -4147,7 +4137,7 @@ anvil-loom: anvil-loom-validate-prereqs anvil-impact $env:RUSTFLAGS = "--cfg loom $($env:RUSTFLAGS)".Trim() foreach ($lt in $loomTargets) { Write-Host "anvil-loom: $($lt.pkg) :: $($lt.target)" - & cargo test -p $lt.pkg --release --all-features --locked --test $lt.target -- --test-threads=1 + & "{{ just_executable() }}" _anvil-with-stable cargo test -p $lt.pkg --release --all-features --locked --test $lt.target -- --test-threads=1 if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } } @@ -4354,7 +4344,7 @@ anvil-msrv-test: anvil-msrv-test-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include affected) if ($include -eq '--skip') { Write-Host 'anvil-msrv-test: no affected packages; skipping'; exit 0 } - $toolchain = (& '.anvil/resolve-stable-toolchain.ps1' -MsrvToolchain | Out-String).Trim() + $toolchain = (& "{{ just_executable() }}" _anvil-resolve-stable msrv | Out-String).Trim() if ([string]::IsNullOrWhiteSpace($toolchain)) { Write-Host 'anvil-msrv-test: no root MSRV declared; skipping' exit 0 @@ -4369,14 +4359,14 @@ anvil-msrv-test: anvil-msrv-test-validate-prereqs anvil-impact [group("anvil-setup")] [script("pwsh", "-NoProfile")] anvil-msrv-test-setup installer="install": - & '.anvil/resolve-stable-toolchain.ps1' -InstallMsrvIfNeeded + & "{{ just_executable() }}" _anvil-resolve-stable install-msrv if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Validate the MSRV prerequisite. [group("anvil-setup")] [script("pwsh", "-NoProfile")] anvil-msrv-test-validate-prereqs: - $toolchain = (& '.anvil/resolve-stable-toolchain.ps1' -MsrvToolchain | Out-String).Trim() + $toolchain = (& "{{ just_executable() }}" _anvil-resolve-stable msrv | Out-String).Trim() if ([string]::IsNullOrWhiteSpace($toolchain)) { exit 0 } & cargo "+$toolchain" --version if ($LASTEXITCODE -ne 0) { @@ -4435,7 +4425,7 @@ anvil-mutants-diff: anvil-mutants-diff-validate-prereqs anvil-impact $diff_path = Join-Path $tmp_dir 'anvil-mutants-diff.diff' git diff "$base..HEAD" --output=$diff_path if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - cargo mutants --in-diff $diff_path --no-shuffle --jobs 0 + & "{{ just_executable() }}" _anvil-with-stable cargo mutants --in-diff $diff_path --no-shuffle --jobs 0 if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # --- pr-mutants members --- @@ -4468,7 +4458,7 @@ anvil-mutants-full: anvil-mutants-full-validate-prereqs Write-Host 'anvil-mutants-full: aarch64-pc-windows-msvc -- cargo-mutants does not build here (winapi); skipping' exit 0 } - & cargo mutants --workspace --no-shuffle --jobs 0 + & "{{ just_executable() }}" _anvil-with-stable cargo mutants --workspace --no-shuffle --jobs 0 if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # --- scheduled-exhaustive members (mutants-full reuses cargo-mutants; @@ -4616,7 +4606,7 @@ anvil-readme-check: anvil-readme-check-validate-prereqs # Unscoped: a change to the workspace-level README template is a repo-level # input cargo-delta cannot map to a package, so this check always runs the # full library set rather than an impact subset. - $metadataJson = & cargo metadata --no-deps --format-version 1 + $metadataJson = & "{{ just_executable() }}" _anvil-with-stable cargo metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-readme-check: cargo metadata failed' exit $LASTEXITCODE @@ -4658,7 +4648,7 @@ anvil-readme-check: anvil-readme-check-validate-prereqs } $args = @('doc2readme', '--check') if ($relTemplate) { $args += @('--template', $relTemplate) } - & cargo @args + & "{{ just_executable() }}" _anvil-with-stable cargo @args if ($LASTEXITCODE -ne 0) { $hadFailure = $true } } finally { Pop-Location @@ -4742,7 +4732,7 @@ anvil-semver-check: anvil-semver-check-validate-prereqs anvil-impact # we got here via impact-scoping (cloud workflows) or full-workspace # fallback (local). $libPkgs = @{} - $metadataJson = & cargo metadata --no-deps --format-version 1 + $metadataJson = & "{{ just_executable() }}" _anvil-with-stable cargo metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-semver-check: cargo metadata failed' exit $LASTEXITCODE @@ -4819,7 +4809,7 @@ anvil-semver-check: anvil-semver-check-validate-prereqs anvil-impact } Write-Host "anvil-semver-check: $p (baseline $base)" - $outputLines = @(& cargo semver-checks --package $p --baseline-rev $base 2>&1) + $outputLines = @(& "{{ just_executable() }}" _anvil-with-stable cargo semver-checks --package $p --baseline-rev $base 2>&1) $semverExit = $LASTEXITCODE $output = ($outputLines | Out-String) if ($semverExit -eq 100) { @@ -4958,9 +4948,9 @@ anvil-spellcheck: anvil-spellcheck-validate-prereqs # ignores user-curated dictionaries (`extra_dictionaries`, custom # hunspell langs, etc.). if (Test-Path 'spellcheck.toml') { - cargo spellcheck --cfg spellcheck.toml check --code 1 + & "{{ just_executable() }}" _anvil-with-stable cargo spellcheck --cfg spellcheck.toml check --code 1 } else { - cargo spellcheck check --code 1 + & "{{ just_executable() }}" _anvil-with-stable cargo spellcheck check --code 1 } if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } @@ -5821,19 +5811,25 @@ _anvil-impact-snapshot: # The baseline worktree is checked out at the merge target, whose # rust-toolchain.toml may pin a toolchain that is NOT installed on this # machine (any PR that bumps rust-toolchain.toml hits this). The - # snapshot is metadata-only (file presence + content hashes), so run it - # under the *active* toolchain via RUSTUP_TOOLCHAIN -- resolved here, in - # the current checkout, where it is installed -- which overrides the - # worktree's rust-toolchain.toml. Best-effort: skip if rustup is absent. - $activeToolchain = $null - # `rustup` may be absent (e.g. ADO msrustup provides `cargo` directly - # without a `rustup` command). Under `$ErrorActionPreference = 'Stop'` - # a bare `rustup` call would raise a terminating command-not-found - # error before the `$LASTEXITCODE` guard runs, so probe for the command - # first and leave `$activeToolchain` null when it is unavailable. - if (Get-Command rustup -ErrorAction SilentlyContinue) { - $rt = (rustup show active-toolchain 2>$null) - if (($LASTEXITCODE -eq 0) -and $rt) { $activeToolchain = (($rt | Select-Object -First 1) -split '\s+')[0] } + # snapshot is metadata-only (file presence + content hashes), so use + # the selected compiler from the current checkout to override any + # different toolchain file in the baseline worktree. Environment/MSRV + # selections are already valid rustup overrides. For a selecting + # toolchain file, ask rustup for the concrete active identifier so + # `path` selections are not mistaken for named toolchains. + $baselineToolchain = (& "{{ just_executable() }}" _anvil-resolve-stable for-environment | Out-String).Trim() + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + if ([string]::IsNullOrWhiteSpace($baselineToolchain)) { + if (-not (Get-Command rustup -ErrorAction SilentlyContinue)) { + Write-Error 'anvil-impact: rustup is required to resolve the active repository toolchain for the baseline snapshot' + exit 1 + } + $active = (rustup show active-toolchain 2>$null) + if (($LASTEXITCODE -ne 0) -or -not $active) { + Write-Error 'anvil-impact: failed to resolve the active repository toolchain for the baseline snapshot' + exit 1 + } + $baselineToolchain = (($active | Select-Object -First 1) -split '\s+')[0] } # Temp-root precedence follows the runner-managed scratch dir on each @@ -5866,14 +5862,12 @@ _anvil-impact-snapshot: Set-Content -LiteralPath $baselineKeyFile -Value $baselineKey -Encoding UTF8 -NoNewline } else { Push-Location $wt - # Preserve any caller-provided RUSTUP_TOOLCHAIN: we override it - # only for this baseline snapshot, then restore the caller's - # value (or remove it if we introduced it) so the current - # snapshot below runs under the same toolchain the caller chose. + # Override only this baseline subprocess scope, then restore the + # caller's input override before returning to the current tree. $hadToolchain = Test-Path Env:\RUSTUP_TOOLCHAIN $priorToolchain = if ($hadToolchain) { $env:RUSTUP_TOOLCHAIN } else { $null } try { - if ($activeToolchain) { $env:RUSTUP_TOOLCHAIN = $activeToolchain } + $env:RUSTUP_TOOLCHAIN = $baselineToolchain $baseSnap = cargo delta @deltaArgs snapshot if ($LASTEXITCODE -ne 0) { throw 'cargo delta snapshot (baseline) failed' } } finally { @@ -5900,7 +5894,7 @@ _anvil-impact-snapshot: Write-Host 'anvil-impact: current snapshot up to date' } else { Write-Host 'anvil-impact: snapshotting working tree' - $curSnap = cargo delta @deltaArgs snapshot + $curSnap = & "{{ just_executable() }}" _anvil-with-stable cargo delta @deltaArgs snapshot if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-impact: cargo delta snapshot (current) failed'; exit 1 } Set-Content -LiteralPath $currentJson -Value $curSnap -Encoding UTF8 Set-Content -LiteralPath $currentKeyFile -Value $currentState -Encoding UTF8 -NoNewline @@ -6018,7 +6012,7 @@ anvil-impact: _anvil-impact-snapshot # early), so default to an empty object so impact.json always exists -- # the freshness check above keys on its presence, and a missing file # would force a recompute on every invocation. - $impactOut = (cargo delta @deltaArgs impact --baseline $baselineJson --current $currentJson --format json | Out-String) + $impactOut = (& "{{ just_executable() }}" _anvil-with-stable cargo delta @deltaArgs impact --baseline $baselineJson --current $currentJson --format json | Out-String) if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-impact: cargo delta impact failed'; exit 1 } if ([string]::IsNullOrWhiteSpace($impactOut)) { $impactOut = '{}' } Set-Content -LiteralPath $impactJson -Value $impactOut.Trim() -Encoding UTF8 @@ -6095,7 +6089,7 @@ _anvil-impact-format tier impactJson: # nested workspaces cargo-delta can instead emit a manifest directory # leaf; accept any reported identifier only when every matching namespace # resolves to the *same* workspace package. - $metadataJson = & cargo metadata --no-deps --format-version 1 + $metadataJson = & "{{ just_executable() }}" _anvil-with-stable cargo metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-impact: cargo metadata failed' exit $LASTEXITCODE @@ -6644,7 +6638,7 @@ anvil-tool-rustc-validate-prereqs: Write-Error 'anvil: rustc not found. Install via rustup: https://rustup.rs' exit 1 } - & pwsh -NoProfile -File '.anvil/resolve-stable-toolchain.ps1' -ValidateWorkspaceMsrv + & "{{ just_executable() }}" _anvil-resolve-stable validate-workspace-msrv if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } @@ -6673,6 +6667,41 @@ anvil-tool-pwsh-validate-prereqs: # Private helpers (install/check primitives) # ============================================================================ +# Resolve or prepare the selected stable compiler without globally exporting it. +[script("pwsh", "-NoProfile")] +_anvil-resolve-stable action="resolve": + $ErrorActionPreference = 'Stop' + $resolverArgs = @(switch ('{{action}}') { + 'resolve' { @() } + 'for-environment' { @('-ForEnvironment') } + 'install' { @('-InstallIfMissing') } + 'apply-file-options' { @('-ApplyToolchainFileOptions') } + 'validate-workspace-msrv' { @('-ValidateWorkspaceMsrv') } + 'msrv' { @('-MsrvToolchain') } + 'install-msrv' { @('-InstallMsrvIfNeeded') } + default { + Write-Error "_anvil-resolve-stable: unknown action '{{action}}'" + exit 2 + } + }) + & pwsh -NoProfile -File '.anvil/resolve-stable-toolchain.ps1' @resolverArgs + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + +# Run one command under the selected stable compiler. A repository toolchain +# file yields no override so rustup processes its complete configuration. +[script("pwsh", "-NoProfile")] +_anvil-with-stable command *args: + $ErrorActionPreference = 'Stop' + $toolchain = (& "{{ just_executable() }}" _anvil-resolve-stable for-environment | Out-String).Trim() + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + if ([string]::IsNullOrWhiteSpace($toolchain)) { + Remove-Item Env:\RUSTUP_TOOLCHAIN -ErrorAction SilentlyContinue + } else { + $env:RUSTUP_TOOLCHAIN = $toolchain + } + & '{{command}}' {{args}} + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + # _install-tool: install a cargo subcommand at the catalog version, # or no-op if it is already installed at or above that version. The # `installer` parameter selects between: @@ -6703,7 +6732,7 @@ _install-tool name version installer source_prereq="": # cached binaries and fail with "binary already exists in destination". $installed = $null $pattern = '^' + [regex]::Escape($name) + ' v(\S+):' - foreach ($line in (cargo install --list 2>$null)) { + foreach ($line in (& "{{ just_executable() }}" _anvil-with-stable cargo install --list 2>$null)) { if ($line -match $pattern) { $installed = $Matches[1]; break } } if ($installed) { @@ -6721,7 +6750,7 @@ _install-tool name version installer source_prereq="": if ($installer -eq 'binstall') { if (-not (Get-Command cargo-binstall -ErrorAction SilentlyContinue)) { Write-Host ' Bootstrapping cargo-binstall' - cargo install --locked cargo-binstall + & "{{ just_executable() }}" _anvil-with-stable cargo install --locked cargo-binstall if ($LASTEXITCODE -ne 0) { Write-Error 'cargo-binstall bootstrap failed' exit $LASTEXITCODE @@ -6735,7 +6764,7 @@ _install-tool name version installer source_prereq="": $binstallArgs += @('--disable-strategies', 'compile') } $binstallArgs += @($name, '--version', "=$version") - cargo @binstallArgs + & "{{ just_executable() }}" _anvil-with-stable cargo @binstallArgs if ($LASTEXITCODE -eq 0) { exit 0 } Write-Host ' binstall failed; falling back to cargo install' -ForegroundColor Yellow } @@ -6743,7 +6772,7 @@ _install-tool name version installer source_prereq="": & "{{just_executable()}}" $sourcePrereq if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } } - cargo install --locked $name --version "=$version" + & "{{ just_executable() }}" _anvil-with-stable cargo install --locked $name --version "=$version" if ($LASTEXITCODE -ne 0) { Write-Error "$name install FAILED" exit $LASTEXITCODE @@ -6759,7 +6788,7 @@ _check-tool name version: $installed = $null $pattern = '^' + [regex]::Escape($name) + ' v(\S+):' - foreach ($line in (cargo install --list 2>$null)) { + foreach ($line in (& "{{ just_executable() }}" _anvil-with-stable cargo install --list 2>$null)) { if ($line -match $pattern) { $installed = $Matches[1]; break } } if (-not $installed) { @@ -6840,14 +6869,14 @@ _install-component toolchain component: } if ($null -ne $subcommand) { if ($toolchain -eq 'default') { - & cargo $subcommand --version *> $null + & "{{ just_executable() }}" _anvil-with-stable cargo $subcommand --version *> $null } else { & cargo "+$toolchain" $subcommand --version *> $null } $present = ($LASTEXITCODE -eq 0) } else { $sysroot = if ($toolchain -eq 'default') { - & rustc --print sysroot 2>$null + & "{{ just_executable() }}" _anvil-with-stable rustc --print sysroot 2>$null } else { & rustc "+$toolchain" --print sysroot 2>$null } @@ -6908,14 +6937,14 @@ _check-component toolchain component: } if ($null -ne $subcommand) { if ($toolchain -eq 'default') { - & cargo $subcommand --version *> $null + & "{{ just_executable() }}" _anvil-with-stable cargo $subcommand --version *> $null } else { & cargo "+$toolchain" $subcommand --version *> $null } $present = ($LASTEXITCODE -eq 0) } else { $sysroot = if ($toolchain -eq 'default') { - & rustc --print sysroot 2>$null + & "{{ just_executable() }}" _anvil-with-stable rustc --print sysroot 2>$null } else { & rustc "+$toolchain" --print sysroot 2>$null } @@ -7277,24 +7306,9 @@ anvil-tool-cargo-udeps-validate-prereqs: (_check-tool "cargo-udeps" cargo_udeps_ # Rustup toolchains # ============================================================================ -# Stable toolchain used by every check without an explicit +toolchain argument. -# The resolver honors RUSTUP_TOOLCHAIN, then a selecting root toolchain file, -# then the root package/workspace MSRV. Environment and MSRV selections are -# exported here; a toolchain-file selection stays empty so rustup honors the -# file's complete targets/components/profile configuration. -rust_stable_override := env_var_or_default('RUSTUP_TOOLCHAIN', '') -# The resolver command ends in a comment so PowerShell's commonly configured -# -Command shell ignores Just's argv[0] copy instead of joining it back in. -rust_stable := if rust_stable_override != '' { - rust_stable_override -} else { - shell('pwsh -NoProfile -File .anvil/resolve-stable-toolchain.ps1 -ForEnvironment #') -} -export RUSTUP_TOOLCHAIN := rust_stable -export ANVIL_STABLE_TOOLCHAIN_SOURCE := env_var_or_default( - 'ANVIL_STABLE_TOOLCHAIN_SOURCE', - if rust_stable_override == '' { 'resolved' } else { 'environment' }, -) +# Stable checks select their compiler through `_anvil-with-stable` at the +# individual command site. `RUSTUP_TOOLCHAIN` is an input override only; Anvil +# never exports a resolved value globally into unrelated recipes or helpers. # General nightly used by udeps, miri, careful, and any future # nightly-dependent check. Bumped on a regular cadence (monthly is a diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap index 4d5172cec..bb2ef7d31 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap @@ -57,11 +57,9 @@ COPY . . RUN --mount=type=cache,id=anvil-cargo-registry,target=/usr/local/cargo/registry \ --mount=type=cache,id=anvil-cargo-git,target=/usr/local/cargo/git \ --mount=type=cache,id=anvil-cargo-target,target=/tmp/anvil-target \ - pwsh -NoProfile -File .anvil/resolve-stable-toolchain.ps1 -InstallIfMissing \ - && toolchain="$(pwsh -NoProfile -File .anvil/resolve-stable-toolchain.ps1 -ForEnvironment)" \ - && printf "anvil_runner := \"native\"\nimport 'justfiles/anvil/mod.just'\n" > Justfile \ - && RUSTUP_TOOLCHAIN="$toolchain" ANVIL_STABLE_TOOLCHAIN_SOURCE=container \ - CARGO_TARGET_DIR=/tmp/anvil-target just anvil-setup + printf "anvil_runner := \"native\"\nimport 'justfiles/anvil/mod.just'\n" > Justfile \ + && just _anvil-resolve-stable install \ + && CARGO_TARGET_DIR=/tmp/anvil-target just anvil-setup COPY .anvil/container/entrypoint.sh /usr/local/bin/anvil-container-entrypoint RUN chmod 755 /usr/local/bin/anvil-container-entrypoint @@ -1442,11 +1440,7 @@ if ($EmitToolchainFileOptions) { exit 0 } if ($ValidateWorkspaceMsrv) { - $skipWorkspaceMsrvValidation = if ([string]::IsNullOrWhiteSpace($env:ANVIL_STABLE_TOOLCHAIN_SOURCE)) { - -not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN) - } else { - $env:ANVIL_STABLE_TOOLCHAIN_SOURCE -in @('environment', 'container') - } + $skipWorkspaceMsrvValidation = -not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN) if (-not $skipWorkspaceMsrvValidation -and $null -eq $fileSelection) { Assert-UniformWorkspaceMsrv } @@ -1914,24 +1908,20 @@ runs: - name: Resolve stable toolchain shell: pwsh -NoProfile -Command ". '{0}'" run: | - $source = if ([string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { 'resolved' } else { 'environment' } - & '.anvil/resolve-stable-toolchain.ps1' -InstallIfMissing + & just _anvil-resolve-stable install if ($LASTEXITCODE -ne 0) { throw 'anvil: failed to install stable toolchain' } - $toolchain = (& '.anvil/resolve-stable-toolchain.ps1' | Out-String).Trim() + $toolchain = (& just _anvil-resolve-stable | Out-String).Trim() if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($toolchain)) { throw 'anvil: failed to resolve stable toolchain' } - $environmentValue = (& '.anvil/resolve-stable-toolchain.ps1' -ForEnvironment | Out-String).Trim() - if (-not [string]::IsNullOrWhiteSpace($environmentValue)) { - "RUSTUP_TOOLCHAIN=$environmentValue" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append - } - "ANVIL_STABLE_TOOLCHAIN_SOURCE=$source" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append Write-Host "anvil: selected stable toolchain '$toolchain'" - id: rustc-version - shell: bash - run: echo "version=$(rustc --version | awk '{print $2}')" >> "$GITHUB_OUTPUT" + shell: pwsh -NoProfile -Command ". '{0}'" + run: | + $version = (& just _anvil-with-stable rustc --version | Select-Object -Last 1).Split()[1] + "version=$version" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append - name: Restore cargo cache id: cargo-cache uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 @@ -2929,7 +2919,7 @@ anvil-aprz: anvil-aprz-validate-prereqs Write-Warning 'To fix: run `gh auth login` (recommended), or set $env:GITHUB_TOKEN to a GitHub token, then re-run.' } } - cargo aprz deps --error-if-high-risk --console appraisal + & "{{ just_executable() }}" _anvil-with-stable cargo aprz deps --error-if-high-risk --console appraisal if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-aprz` recipe. @@ -2955,7 +2945,7 @@ anvil-aprz-validate-prereqs: anvil-tool-cargo-aprz-validate-prereqs [script("pwsh", "-NoProfile")] anvil-audit: anvil-audit-validate-prereqs $ErrorActionPreference = 'Stop' - & cargo audit + & "{{ just_executable() }}" _anvil-with-stable cargo audit if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-audit` recipe. @@ -2983,7 +2973,7 @@ anvil-bench: anvil-bench-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include affected) if ($include -eq '--skip') { Write-Host 'anvil-bench: no affected packages; skipping'; exit 0 } - & cargo bench @(if ($include) { -split $include } else { '--workspace' }) --all-features --no-run + & "{{ just_executable() }}" _anvil-with-stable cargo bench @(if ($include) { -split $include } else { '--workspace' }) --all-features --no-run if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # bench uses the cargo built-in in compile-only mode; no extra tool install @@ -3054,7 +3044,7 @@ anvil-bolero: anvil-bolero-validate-prereqs anvil-impact $packageNames.Add(($pkgArgs[++$i] -split '@', 2)[0]) } } else { - $metadataJson = & cargo metadata --no-deps --format-version 1 + $metadataJson = & "{{ just_executable() }}" _anvil-with-stable cargo metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-bolero: cargo metadata failed' exit $LASTEXITCODE @@ -3181,7 +3171,7 @@ anvil-careful: anvil-careful-validate-prereqs anvil-impact Write-Host " reason : the nightly toolchain and/or resolved cargo-careful executable changed" Write-Host " now : $idLabel" Write-Host " identity: $($prev.Substring(0, [Math]::Min(12, $prev.Length)))... -> $($idHash.Substring(0, 12))..." - cargo clean + & cargo '+{{ rust_nightly }}' clean if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } } # Persist the current identity (also seeds the marker on first run, when @@ -3218,7 +3208,7 @@ anvil-cargo-hack: anvil-cargo-hack-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include required) if ($include -eq '--skip') { Write-Host 'anvil-cargo-hack: no affected packages; skipping'; exit 0 } - & cargo hack @(if ($include) { -split $include } else { '--workspace' }) --feature-powerset --depth 2 check + & "{{ just_executable() }}" _anvil-with-stable cargo hack @(if ($include) { -split $include } else { '--workspace' }) --feature-powerset --depth 2 check if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-cargo-hack` recipe. @@ -3257,7 +3247,7 @@ anvil-cargo-sort: anvil-cargo-sort-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include modified) if ($include -eq '--skip') { Write-Host 'anvil-cargo-sort: no modified packages; skipping'; exit 0 } - cargo sort --workspace --grouped --check --check-format + & "{{ just_executable() }}" _anvil-with-stable cargo sort --workspace --grouped --check --check-format if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-cargo-sort` recipe. @@ -3290,7 +3280,7 @@ anvil-clippy: anvil-clippy-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include affected) if ($include -eq '--skip') { Write-Host 'anvil-clippy: no affected packages; skipping'; exit 0 } - & cargo clippy @(if ($include) { -split $include } else { '--workspace' }) --all-targets --all-features --locked "--" '-D' 'warnings' + & "{{ just_executable() }}" _anvil-with-stable cargo clippy @(if ($include) { -split $include } else { '--workspace' }) --all-targets --all-features --locked "--" '-D' 'warnings' if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-clippy` recipe. @@ -3318,7 +3308,7 @@ anvil-clippy-validate-prereqs: anvil-component-default-clippy-validate-prereqs [script("pwsh", "-NoProfile")] anvil-deny: anvil-deny-validate-prereqs $ErrorActionPreference = 'Stop' - & cargo deny check + & "{{ just_executable() }}" _anvil-with-stable cargo deny check if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-deny` recipe. @@ -3349,7 +3339,7 @@ anvil-doc-build: anvil-doc-build-validate-prereqs anvil-impact $include = (& "{{ just_executable() }}" _anvil-impact-include required) if ($include -eq '--skip') { Write-Host 'anvil-doc-build: no affected packages; skipping'; exit 0 } $env:RUSTDOCFLAGS = '-D warnings' - & cargo doc @(if ($include) { -split $include } else { '--workspace' }) --all-features --no-deps + & "{{ just_executable() }}" _anvil-with-stable cargo doc @(if ($include) { -split $include } else { '--workspace' }) --all-features --no-deps if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # doc-build, examples and doc-test are pure cargo built-ins; the rust @@ -3387,9 +3377,9 @@ anvil-doc-test: anvil-doc-test-validate-prereqs anvil-impact $include = (& "{{ just_executable() }}" _anvil-impact-include affected) if ($include -eq '--skip') { Write-Host 'anvil-doc-test: no affected packages; skipping'; exit 0 } $pkg = @(if ($include) { -split $include } else { '--workspace' }) - & cargo test --doc @pkg --all-features --locked + & "{{ just_executable() }}" _anvil-with-stable cargo test --doc @pkg --all-features --locked if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - & cargo test --doc @pkg --locked + & "{{ just_executable() }}" _anvil-with-stable cargo test --doc @pkg --locked if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-doc-test` recipe. @@ -3417,7 +3407,7 @@ anvil-ensure-no-cyclic-deps: anvil-ensure-no-cyclic-deps-validate-prereqs anvil- $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include modified) if ($include -eq '--skip') { Write-Host 'anvil-ensure-no-cyclic-deps: no modified packages; skipping'; exit 0 } - cargo ensure-no-cyclic-deps + & "{{ just_executable() }}" _anvil-with-stable cargo ensure-no-cyclic-deps if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-ensure-no-cyclic-deps` recipe. @@ -3445,7 +3435,7 @@ anvil-ensure-no-default-features: anvil-ensure-no-default-features-validate-prer $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include modified) if ($include -eq '--skip') { Write-Host 'anvil-ensure-no-default-features: no modified packages; skipping'; exit 0 } - cargo ensure-no-default-features + & "{{ just_executable() }}" _anvil-with-stable cargo ensure-no-default-features if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-ensure-no-default-features` recipe. @@ -3473,7 +3463,7 @@ anvil-examples: anvil-examples-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include affected) if ($include -eq '--skip') { Write-Host 'anvil-examples: no affected packages; skipping'; exit 0 } - & cargo build @(if ($include) { -split $include } else { '--workspace' }) --examples --all-features --locked + & "{{ just_executable() }}" _anvil-with-stable cargo build @(if ($include) { -split $include } else { '--workspace' }) --examples --all-features --locked if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-examples` recipe. @@ -3518,7 +3508,7 @@ anvil-external-types: anvil-external-types-validate-prereqs anvil-impact $pkg = @(if ($include) { -split $include } else { '--workspace' }) # Build pkg-name -> manifest-path map, restricted to library crates. $libPkgs = @{} - $metadataJson = & cargo metadata --no-deps --format-version 1 + $metadataJson = & "{{ just_executable() }}" _anvil-with-stable cargo metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-external-types: cargo metadata failed' exit $LASTEXITCODE @@ -3646,7 +3636,7 @@ anvil-license-headers: anvil-license-headers-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include modified) if ($include -eq '--skip') { Write-Host 'anvil-license-headers: no modified packages; skipping'; exit 0 } - cargo heather + & "{{ just_executable() }}" _anvil-with-stable cargo heather if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-license-headers` recipe. @@ -3697,7 +3687,7 @@ anvil-llvm-cov: anvil-llvm-cov-validate-prereqs anvil-impact # never opts a changed package out of tests. $testOnlyPkg = [System.Collections.Generic.List[string]]::new() if ($pkg -contains '--package') { - $metadataJson = & cargo metadata --no-deps --format-version 1 + $metadataJson = & "{{ just_executable() }}" _anvil-with-stable cargo metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-llvm-cov: cargo metadata failed' exit $LASTEXITCODE @@ -3828,7 +3818,7 @@ anvil-llvm-cov: anvil-llvm-cov-validate-prereqs anvil-impact } $gateArgs.Add($pkg[$i]) } - cargo coverage-gate @gateArgs --lcov target/coverage/lcov-all-features.info --lcov target/coverage/lcov-no-default.info + & "{{ just_executable() }}" _anvil-with-stable cargo coverage-gate @gateArgs --lcov target/coverage/lcov-all-features.info --lcov target/coverage/lcov-no-default.info if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # --- pr-test members (shared with scheduled-test) --- @@ -3899,7 +3889,7 @@ anvil-loom: anvil-loom-validate-prereqs anvil-impact Write-Host 'anvil-loom: no affected packages; skipping' exit 0 } - $meta = cargo metadata --no-deps --format-version 1 | ConvertFrom-Json + $meta = & "{{ just_executable() }}" _anvil-with-stable cargo metadata --no-deps --format-version 1 | ConvertFrom-Json if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Optional impact-scoping: recover bare package names from the @@ -3952,7 +3942,7 @@ anvil-loom: anvil-loom-validate-prereqs anvil-impact $env:RUSTFLAGS = "--cfg loom $($env:RUSTFLAGS)".Trim() foreach ($lt in $loomTargets) { Write-Host "anvil-loom: $($lt.pkg) :: $($lt.target)" - & cargo test -p $lt.pkg --release --all-features --locked --test $lt.target -- --test-threads=1 + & "{{ just_executable() }}" _anvil-with-stable cargo test -p $lt.pkg --release --all-features --locked --test $lt.target -- --test-threads=1 if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } } @@ -4159,7 +4149,7 @@ anvil-msrv-test: anvil-msrv-test-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include affected) if ($include -eq '--skip') { Write-Host 'anvil-msrv-test: no affected packages; skipping'; exit 0 } - $toolchain = (& '.anvil/resolve-stable-toolchain.ps1' -MsrvToolchain | Out-String).Trim() + $toolchain = (& "{{ just_executable() }}" _anvil-resolve-stable msrv | Out-String).Trim() if ([string]::IsNullOrWhiteSpace($toolchain)) { Write-Host 'anvil-msrv-test: no root MSRV declared; skipping' exit 0 @@ -4174,14 +4164,14 @@ anvil-msrv-test: anvil-msrv-test-validate-prereqs anvil-impact [group("anvil-setup")] [script("pwsh", "-NoProfile")] anvil-msrv-test-setup installer="install": - & '.anvil/resolve-stable-toolchain.ps1' -InstallMsrvIfNeeded + & "{{ just_executable() }}" _anvil-resolve-stable install-msrv if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Validate the MSRV prerequisite. [group("anvil-setup")] [script("pwsh", "-NoProfile")] anvil-msrv-test-validate-prereqs: - $toolchain = (& '.anvil/resolve-stable-toolchain.ps1' -MsrvToolchain | Out-String).Trim() + $toolchain = (& "{{ just_executable() }}" _anvil-resolve-stable msrv | Out-String).Trim() if ([string]::IsNullOrWhiteSpace($toolchain)) { exit 0 } & cargo "+$toolchain" --version if ($LASTEXITCODE -ne 0) { @@ -4240,7 +4230,7 @@ anvil-mutants-diff: anvil-mutants-diff-validate-prereqs anvil-impact $diff_path = Join-Path $tmp_dir 'anvil-mutants-diff.diff' git diff "$base..HEAD" --output=$diff_path if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - cargo mutants --in-diff $diff_path --no-shuffle --jobs 0 + & "{{ just_executable() }}" _anvil-with-stable cargo mutants --in-diff $diff_path --no-shuffle --jobs 0 if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # --- pr-mutants members --- @@ -4273,7 +4263,7 @@ anvil-mutants-full: anvil-mutants-full-validate-prereqs Write-Host 'anvil-mutants-full: aarch64-pc-windows-msvc -- cargo-mutants does not build here (winapi); skipping' exit 0 } - & cargo mutants --workspace --no-shuffle --jobs 0 + & "{{ just_executable() }}" _anvil-with-stable cargo mutants --workspace --no-shuffle --jobs 0 if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # --- scheduled-exhaustive members (mutants-full reuses cargo-mutants; @@ -4421,7 +4411,7 @@ anvil-readme-check: anvil-readme-check-validate-prereqs # Unscoped: a change to the workspace-level README template is a repo-level # input cargo-delta cannot map to a package, so this check always runs the # full library set rather than an impact subset. - $metadataJson = & cargo metadata --no-deps --format-version 1 + $metadataJson = & "{{ just_executable() }}" _anvil-with-stable cargo metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-readme-check: cargo metadata failed' exit $LASTEXITCODE @@ -4463,7 +4453,7 @@ anvil-readme-check: anvil-readme-check-validate-prereqs } $args = @('doc2readme', '--check') if ($relTemplate) { $args += @('--template', $relTemplate) } - & cargo @args + & "{{ just_executable() }}" _anvil-with-stable cargo @args if ($LASTEXITCODE -ne 0) { $hadFailure = $true } } finally { Pop-Location @@ -4547,7 +4537,7 @@ anvil-semver-check: anvil-semver-check-validate-prereqs anvil-impact # we got here via impact-scoping (cloud workflows) or full-workspace # fallback (local). $libPkgs = @{} - $metadataJson = & cargo metadata --no-deps --format-version 1 + $metadataJson = & "{{ just_executable() }}" _anvil-with-stable cargo metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-semver-check: cargo metadata failed' exit $LASTEXITCODE @@ -4624,7 +4614,7 @@ anvil-semver-check: anvil-semver-check-validate-prereqs anvil-impact } Write-Host "anvil-semver-check: $p (baseline $base)" - $outputLines = @(& cargo semver-checks --package $p --baseline-rev $base 2>&1) + $outputLines = @(& "{{ just_executable() }}" _anvil-with-stable cargo semver-checks --package $p --baseline-rev $base 2>&1) $semverExit = $LASTEXITCODE $output = ($outputLines | Out-String) if ($semverExit -eq 100) { @@ -4763,9 +4753,9 @@ anvil-spellcheck: anvil-spellcheck-validate-prereqs # ignores user-curated dictionaries (`extra_dictionaries`, custom # hunspell langs, etc.). if (Test-Path 'spellcheck.toml') { - cargo spellcheck --cfg spellcheck.toml check --code 1 + & "{{ just_executable() }}" _anvil-with-stable cargo spellcheck --cfg spellcheck.toml check --code 1 } else { - cargo spellcheck check --code 1 + & "{{ just_executable() }}" _anvil-with-stable cargo spellcheck check --code 1 } if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } @@ -5626,19 +5616,25 @@ _anvil-impact-snapshot: # The baseline worktree is checked out at the merge target, whose # rust-toolchain.toml may pin a toolchain that is NOT installed on this # machine (any PR that bumps rust-toolchain.toml hits this). The - # snapshot is metadata-only (file presence + content hashes), so run it - # under the *active* toolchain via RUSTUP_TOOLCHAIN -- resolved here, in - # the current checkout, where it is installed -- which overrides the - # worktree's rust-toolchain.toml. Best-effort: skip if rustup is absent. - $activeToolchain = $null - # `rustup` may be absent (e.g. ADO msrustup provides `cargo` directly - # without a `rustup` command). Under `$ErrorActionPreference = 'Stop'` - # a bare `rustup` call would raise a terminating command-not-found - # error before the `$LASTEXITCODE` guard runs, so probe for the command - # first and leave `$activeToolchain` null when it is unavailable. - if (Get-Command rustup -ErrorAction SilentlyContinue) { - $rt = (rustup show active-toolchain 2>$null) - if (($LASTEXITCODE -eq 0) -and $rt) { $activeToolchain = (($rt | Select-Object -First 1) -split '\s+')[0] } + # snapshot is metadata-only (file presence + content hashes), so use + # the selected compiler from the current checkout to override any + # different toolchain file in the baseline worktree. Environment/MSRV + # selections are already valid rustup overrides. For a selecting + # toolchain file, ask rustup for the concrete active identifier so + # `path` selections are not mistaken for named toolchains. + $baselineToolchain = (& "{{ just_executable() }}" _anvil-resolve-stable for-environment | Out-String).Trim() + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + if ([string]::IsNullOrWhiteSpace($baselineToolchain)) { + if (-not (Get-Command rustup -ErrorAction SilentlyContinue)) { + Write-Error 'anvil-impact: rustup is required to resolve the active repository toolchain for the baseline snapshot' + exit 1 + } + $active = (rustup show active-toolchain 2>$null) + if (($LASTEXITCODE -ne 0) -or -not $active) { + Write-Error 'anvil-impact: failed to resolve the active repository toolchain for the baseline snapshot' + exit 1 + } + $baselineToolchain = (($active | Select-Object -First 1) -split '\s+')[0] } # Temp-root precedence follows the runner-managed scratch dir on each @@ -5671,14 +5667,12 @@ _anvil-impact-snapshot: Set-Content -LiteralPath $baselineKeyFile -Value $baselineKey -Encoding UTF8 -NoNewline } else { Push-Location $wt - # Preserve any caller-provided RUSTUP_TOOLCHAIN: we override it - # only for this baseline snapshot, then restore the caller's - # value (or remove it if we introduced it) so the current - # snapshot below runs under the same toolchain the caller chose. + # Override only this baseline subprocess scope, then restore the + # caller's input override before returning to the current tree. $hadToolchain = Test-Path Env:\RUSTUP_TOOLCHAIN $priorToolchain = if ($hadToolchain) { $env:RUSTUP_TOOLCHAIN } else { $null } try { - if ($activeToolchain) { $env:RUSTUP_TOOLCHAIN = $activeToolchain } + $env:RUSTUP_TOOLCHAIN = $baselineToolchain $baseSnap = cargo delta @deltaArgs snapshot if ($LASTEXITCODE -ne 0) { throw 'cargo delta snapshot (baseline) failed' } } finally { @@ -5705,7 +5699,7 @@ _anvil-impact-snapshot: Write-Host 'anvil-impact: current snapshot up to date' } else { Write-Host 'anvil-impact: snapshotting working tree' - $curSnap = cargo delta @deltaArgs snapshot + $curSnap = & "{{ just_executable() }}" _anvil-with-stable cargo delta @deltaArgs snapshot if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-impact: cargo delta snapshot (current) failed'; exit 1 } Set-Content -LiteralPath $currentJson -Value $curSnap -Encoding UTF8 Set-Content -LiteralPath $currentKeyFile -Value $currentState -Encoding UTF8 -NoNewline @@ -5823,7 +5817,7 @@ anvil-impact: _anvil-impact-snapshot # early), so default to an empty object so impact.json always exists -- # the freshness check above keys on its presence, and a missing file # would force a recompute on every invocation. - $impactOut = (cargo delta @deltaArgs impact --baseline $baselineJson --current $currentJson --format json | Out-String) + $impactOut = (& "{{ just_executable() }}" _anvil-with-stable cargo delta @deltaArgs impact --baseline $baselineJson --current $currentJson --format json | Out-String) if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-impact: cargo delta impact failed'; exit 1 } if ([string]::IsNullOrWhiteSpace($impactOut)) { $impactOut = '{}' } Set-Content -LiteralPath $impactJson -Value $impactOut.Trim() -Encoding UTF8 @@ -5900,7 +5894,7 @@ _anvil-impact-format tier impactJson: # nested workspaces cargo-delta can instead emit a manifest directory # leaf; accept any reported identifier only when every matching namespace # resolves to the *same* workspace package. - $metadataJson = & cargo metadata --no-deps --format-version 1 + $metadataJson = & "{{ just_executable() }}" _anvil-with-stable cargo metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-impact: cargo metadata failed' exit $LASTEXITCODE @@ -6449,7 +6443,7 @@ anvil-tool-rustc-validate-prereqs: Write-Error 'anvil: rustc not found. Install via rustup: https://rustup.rs' exit 1 } - & pwsh -NoProfile -File '.anvil/resolve-stable-toolchain.ps1' -ValidateWorkspaceMsrv + & "{{ just_executable() }}" _anvil-resolve-stable validate-workspace-msrv if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } @@ -6478,6 +6472,41 @@ anvil-tool-pwsh-validate-prereqs: # Private helpers (install/check primitives) # ============================================================================ +# Resolve or prepare the selected stable compiler without globally exporting it. +[script("pwsh", "-NoProfile")] +_anvil-resolve-stable action="resolve": + $ErrorActionPreference = 'Stop' + $resolverArgs = @(switch ('{{action}}') { + 'resolve' { @() } + 'for-environment' { @('-ForEnvironment') } + 'install' { @('-InstallIfMissing') } + 'apply-file-options' { @('-ApplyToolchainFileOptions') } + 'validate-workspace-msrv' { @('-ValidateWorkspaceMsrv') } + 'msrv' { @('-MsrvToolchain') } + 'install-msrv' { @('-InstallMsrvIfNeeded') } + default { + Write-Error "_anvil-resolve-stable: unknown action '{{action}}'" + exit 2 + } + }) + & pwsh -NoProfile -File '.anvil/resolve-stable-toolchain.ps1' @resolverArgs + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + +# Run one command under the selected stable compiler. A repository toolchain +# file yields no override so rustup processes its complete configuration. +[script("pwsh", "-NoProfile")] +_anvil-with-stable command *args: + $ErrorActionPreference = 'Stop' + $toolchain = (& "{{ just_executable() }}" _anvil-resolve-stable for-environment | Out-String).Trim() + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + if ([string]::IsNullOrWhiteSpace($toolchain)) { + Remove-Item Env:\RUSTUP_TOOLCHAIN -ErrorAction SilentlyContinue + } else { + $env:RUSTUP_TOOLCHAIN = $toolchain + } + & '{{command}}' {{args}} + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + # _install-tool: install a cargo subcommand at the catalog version, # or no-op if it is already installed at or above that version. The # `installer` parameter selects between: @@ -6508,7 +6537,7 @@ _install-tool name version installer source_prereq="": # cached binaries and fail with "binary already exists in destination". $installed = $null $pattern = '^' + [regex]::Escape($name) + ' v(\S+):' - foreach ($line in (cargo install --list 2>$null)) { + foreach ($line in (& "{{ just_executable() }}" _anvil-with-stable cargo install --list 2>$null)) { if ($line -match $pattern) { $installed = $Matches[1]; break } } if ($installed) { @@ -6526,7 +6555,7 @@ _install-tool name version installer source_prereq="": if ($installer -eq 'binstall') { if (-not (Get-Command cargo-binstall -ErrorAction SilentlyContinue)) { Write-Host ' Bootstrapping cargo-binstall' - cargo install --locked cargo-binstall + & "{{ just_executable() }}" _anvil-with-stable cargo install --locked cargo-binstall if ($LASTEXITCODE -ne 0) { Write-Error 'cargo-binstall bootstrap failed' exit $LASTEXITCODE @@ -6540,7 +6569,7 @@ _install-tool name version installer source_prereq="": $binstallArgs += @('--disable-strategies', 'compile') } $binstallArgs += @($name, '--version', "=$version") - cargo @binstallArgs + & "{{ just_executable() }}" _anvil-with-stable cargo @binstallArgs if ($LASTEXITCODE -eq 0) { exit 0 } Write-Host ' binstall failed; falling back to cargo install' -ForegroundColor Yellow } @@ -6548,7 +6577,7 @@ _install-tool name version installer source_prereq="": & "{{just_executable()}}" $sourcePrereq if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } } - cargo install --locked $name --version "=$version" + & "{{ just_executable() }}" _anvil-with-stable cargo install --locked $name --version "=$version" if ($LASTEXITCODE -ne 0) { Write-Error "$name install FAILED" exit $LASTEXITCODE @@ -6564,7 +6593,7 @@ _check-tool name version: $installed = $null $pattern = '^' + [regex]::Escape($name) + ' v(\S+):' - foreach ($line in (cargo install --list 2>$null)) { + foreach ($line in (& "{{ just_executable() }}" _anvil-with-stable cargo install --list 2>$null)) { if ($line -match $pattern) { $installed = $Matches[1]; break } } if (-not $installed) { @@ -6645,14 +6674,14 @@ _install-component toolchain component: } if ($null -ne $subcommand) { if ($toolchain -eq 'default') { - & cargo $subcommand --version *> $null + & "{{ just_executable() }}" _anvil-with-stable cargo $subcommand --version *> $null } else { & cargo "+$toolchain" $subcommand --version *> $null } $present = ($LASTEXITCODE -eq 0) } else { $sysroot = if ($toolchain -eq 'default') { - & rustc --print sysroot 2>$null + & "{{ just_executable() }}" _anvil-with-stable rustc --print sysroot 2>$null } else { & rustc "+$toolchain" --print sysroot 2>$null } @@ -6713,14 +6742,14 @@ _check-component toolchain component: } if ($null -ne $subcommand) { if ($toolchain -eq 'default') { - & cargo $subcommand --version *> $null + & "{{ just_executable() }}" _anvil-with-stable cargo $subcommand --version *> $null } else { & cargo "+$toolchain" $subcommand --version *> $null } $present = ($LASTEXITCODE -eq 0) } else { $sysroot = if ($toolchain -eq 'default') { - & rustc --print sysroot 2>$null + & "{{ just_executable() }}" _anvil-with-stable rustc --print sysroot 2>$null } else { & rustc "+$toolchain" --print sysroot 2>$null } @@ -7082,24 +7111,9 @@ anvil-tool-cargo-udeps-validate-prereqs: (_check-tool "cargo-udeps" cargo_udeps_ # Rustup toolchains # ============================================================================ -# Stable toolchain used by every check without an explicit +toolchain argument. -# The resolver honors RUSTUP_TOOLCHAIN, then a selecting root toolchain file, -# then the root package/workspace MSRV. Environment and MSRV selections are -# exported here; a toolchain-file selection stays empty so rustup honors the -# file's complete targets/components/profile configuration. -rust_stable_override := env_var_or_default('RUSTUP_TOOLCHAIN', '') -# The resolver command ends in a comment so PowerShell's commonly configured -# -Command shell ignores Just's argv[0] copy instead of joining it back in. -rust_stable := if rust_stable_override != '' { - rust_stable_override -} else { - shell('pwsh -NoProfile -File .anvil/resolve-stable-toolchain.ps1 -ForEnvironment #') -} -export RUSTUP_TOOLCHAIN := rust_stable -export ANVIL_STABLE_TOOLCHAIN_SOURCE := env_var_or_default( - 'ANVIL_STABLE_TOOLCHAIN_SOURCE', - if rust_stable_override == '' { 'resolved' } else { 'environment' }, -) +# Stable checks select their compiler through `_anvil-with-stable` at the +# individual command site. `RUSTUP_TOOLCHAIN` is an input override only; Anvil +# never exports a resolved value globally into unrelated recipes or helpers. # General nightly used by udeps, miri, careful, and any future # nightly-dependent check. Bumped on a regular cadence (monthly is a diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap index def43d975..32a30aa62 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap @@ -57,11 +57,9 @@ COPY . . RUN --mount=type=cache,id=anvil-cargo-registry,target=/usr/local/cargo/registry \ --mount=type=cache,id=anvil-cargo-git,target=/usr/local/cargo/git \ --mount=type=cache,id=anvil-cargo-target,target=/tmp/anvil-target \ - pwsh -NoProfile -File .anvil/resolve-stable-toolchain.ps1 -InstallIfMissing \ - && toolchain="$(pwsh -NoProfile -File .anvil/resolve-stable-toolchain.ps1 -ForEnvironment)" \ - && printf "anvil_runner := \"native\"\nimport 'justfiles/anvil/mod.just'\n" > Justfile \ - && RUSTUP_TOOLCHAIN="$toolchain" ANVIL_STABLE_TOOLCHAIN_SOURCE=container \ - CARGO_TARGET_DIR=/tmp/anvil-target just anvil-setup + printf "anvil_runner := \"native\"\nimport 'justfiles/anvil/mod.just'\n" > Justfile \ + && just _anvil-resolve-stable install \ + && CARGO_TARGET_DIR=/tmp/anvil-target just anvil-setup COPY .anvil/container/entrypoint.sh /usr/local/bin/anvil-container-entrypoint RUN chmod 755 /usr/local/bin/anvil-container-entrypoint @@ -1442,11 +1440,7 @@ if ($EmitToolchainFileOptions) { exit 0 } if ($ValidateWorkspaceMsrv) { - $skipWorkspaceMsrvValidation = if ([string]::IsNullOrWhiteSpace($env:ANVIL_STABLE_TOOLCHAIN_SOURCE)) { - -not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN) - } else { - $env:ANVIL_STABLE_TOOLCHAIN_SOURCE -in @('environment', 'container') - } + $skipWorkspaceMsrvValidation = -not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN) if (-not $skipWorkspaceMsrvValidation -and $null -eq $fileSelection) { Assert-UniformWorkspaceMsrv } @@ -1770,7 +1764,7 @@ anvil-aprz: anvil-aprz-validate-prereqs Write-Warning 'To fix: run `gh auth login` (recommended), or set $env:GITHUB_TOKEN to a GitHub token, then re-run.' } } - cargo aprz deps --error-if-high-risk --console appraisal + & "{{ just_executable() }}" _anvil-with-stable cargo aprz deps --error-if-high-risk --console appraisal if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-aprz` recipe. @@ -1796,7 +1790,7 @@ anvil-aprz-validate-prereqs: anvil-tool-cargo-aprz-validate-prereqs [script("pwsh", "-NoProfile")] anvil-audit: anvil-audit-validate-prereqs $ErrorActionPreference = 'Stop' - & cargo audit + & "{{ just_executable() }}" _anvil-with-stable cargo audit if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-audit` recipe. @@ -1824,7 +1818,7 @@ anvil-bench: anvil-bench-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include affected) if ($include -eq '--skip') { Write-Host 'anvil-bench: no affected packages; skipping'; exit 0 } - & cargo bench @(if ($include) { -split $include } else { '--workspace' }) --all-features --no-run + & "{{ just_executable() }}" _anvil-with-stable cargo bench @(if ($include) { -split $include } else { '--workspace' }) --all-features --no-run if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # bench uses the cargo built-in in compile-only mode; no extra tool install @@ -1895,7 +1889,7 @@ anvil-bolero: anvil-bolero-validate-prereqs anvil-impact $packageNames.Add(($pkgArgs[++$i] -split '@', 2)[0]) } } else { - $metadataJson = & cargo metadata --no-deps --format-version 1 + $metadataJson = & "{{ just_executable() }}" _anvil-with-stable cargo metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-bolero: cargo metadata failed' exit $LASTEXITCODE @@ -2022,7 +2016,7 @@ anvil-careful: anvil-careful-validate-prereqs anvil-impact Write-Host " reason : the nightly toolchain and/or resolved cargo-careful executable changed" Write-Host " now : $idLabel" Write-Host " identity: $($prev.Substring(0, [Math]::Min(12, $prev.Length)))... -> $($idHash.Substring(0, 12))..." - cargo clean + & cargo '+{{ rust_nightly }}' clean if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } } # Persist the current identity (also seeds the marker on first run, when @@ -2059,7 +2053,7 @@ anvil-cargo-hack: anvil-cargo-hack-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include required) if ($include -eq '--skip') { Write-Host 'anvil-cargo-hack: no affected packages; skipping'; exit 0 } - & cargo hack @(if ($include) { -split $include } else { '--workspace' }) --feature-powerset --depth 2 check + & "{{ just_executable() }}" _anvil-with-stable cargo hack @(if ($include) { -split $include } else { '--workspace' }) --feature-powerset --depth 2 check if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-cargo-hack` recipe. @@ -2098,7 +2092,7 @@ anvil-cargo-sort: anvil-cargo-sort-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include modified) if ($include -eq '--skip') { Write-Host 'anvil-cargo-sort: no modified packages; skipping'; exit 0 } - cargo sort --workspace --grouped --check --check-format + & "{{ just_executable() }}" _anvil-with-stable cargo sort --workspace --grouped --check --check-format if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-cargo-sort` recipe. @@ -2131,7 +2125,7 @@ anvil-clippy: anvil-clippy-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include affected) if ($include -eq '--skip') { Write-Host 'anvil-clippy: no affected packages; skipping'; exit 0 } - & cargo clippy @(if ($include) { -split $include } else { '--workspace' }) --all-targets --all-features --locked "--" '-D' 'warnings' + & "{{ just_executable() }}" _anvil-with-stable cargo clippy @(if ($include) { -split $include } else { '--workspace' }) --all-targets --all-features --locked "--" '-D' 'warnings' if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-clippy` recipe. @@ -2159,7 +2153,7 @@ anvil-clippy-validate-prereqs: anvil-component-default-clippy-validate-prereqs [script("pwsh", "-NoProfile")] anvil-deny: anvil-deny-validate-prereqs $ErrorActionPreference = 'Stop' - & cargo deny check + & "{{ just_executable() }}" _anvil-with-stable cargo deny check if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-deny` recipe. @@ -2190,7 +2184,7 @@ anvil-doc-build: anvil-doc-build-validate-prereqs anvil-impact $include = (& "{{ just_executable() }}" _anvil-impact-include required) if ($include -eq '--skip') { Write-Host 'anvil-doc-build: no affected packages; skipping'; exit 0 } $env:RUSTDOCFLAGS = '-D warnings' - & cargo doc @(if ($include) { -split $include } else { '--workspace' }) --all-features --no-deps + & "{{ just_executable() }}" _anvil-with-stable cargo doc @(if ($include) { -split $include } else { '--workspace' }) --all-features --no-deps if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # doc-build, examples and doc-test are pure cargo built-ins; the rust @@ -2228,9 +2222,9 @@ anvil-doc-test: anvil-doc-test-validate-prereqs anvil-impact $include = (& "{{ just_executable() }}" _anvil-impact-include affected) if ($include -eq '--skip') { Write-Host 'anvil-doc-test: no affected packages; skipping'; exit 0 } $pkg = @(if ($include) { -split $include } else { '--workspace' }) - & cargo test --doc @pkg --all-features --locked + & "{{ just_executable() }}" _anvil-with-stable cargo test --doc @pkg --all-features --locked if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - & cargo test --doc @pkg --locked + & "{{ just_executable() }}" _anvil-with-stable cargo test --doc @pkg --locked if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-doc-test` recipe. @@ -2258,7 +2252,7 @@ anvil-ensure-no-cyclic-deps: anvil-ensure-no-cyclic-deps-validate-prereqs anvil- $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include modified) if ($include -eq '--skip') { Write-Host 'anvil-ensure-no-cyclic-deps: no modified packages; skipping'; exit 0 } - cargo ensure-no-cyclic-deps + & "{{ just_executable() }}" _anvil-with-stable cargo ensure-no-cyclic-deps if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-ensure-no-cyclic-deps` recipe. @@ -2286,7 +2280,7 @@ anvil-ensure-no-default-features: anvil-ensure-no-default-features-validate-prer $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include modified) if ($include -eq '--skip') { Write-Host 'anvil-ensure-no-default-features: no modified packages; skipping'; exit 0 } - cargo ensure-no-default-features + & "{{ just_executable() }}" _anvil-with-stable cargo ensure-no-default-features if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-ensure-no-default-features` recipe. @@ -2314,7 +2308,7 @@ anvil-examples: anvil-examples-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include affected) if ($include -eq '--skip') { Write-Host 'anvil-examples: no affected packages; skipping'; exit 0 } - & cargo build @(if ($include) { -split $include } else { '--workspace' }) --examples --all-features --locked + & "{{ just_executable() }}" _anvil-with-stable cargo build @(if ($include) { -split $include } else { '--workspace' }) --examples --all-features --locked if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-examples` recipe. @@ -2359,7 +2353,7 @@ anvil-external-types: anvil-external-types-validate-prereqs anvil-impact $pkg = @(if ($include) { -split $include } else { '--workspace' }) # Build pkg-name -> manifest-path map, restricted to library crates. $libPkgs = @{} - $metadataJson = & cargo metadata --no-deps --format-version 1 + $metadataJson = & "{{ just_executable() }}" _anvil-with-stable cargo metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-external-types: cargo metadata failed' exit $LASTEXITCODE @@ -2487,7 +2481,7 @@ anvil-license-headers: anvil-license-headers-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include modified) if ($include -eq '--skip') { Write-Host 'anvil-license-headers: no modified packages; skipping'; exit 0 } - cargo heather + & "{{ just_executable() }}" _anvil-with-stable cargo heather if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-license-headers` recipe. @@ -2538,7 +2532,7 @@ anvil-llvm-cov: anvil-llvm-cov-validate-prereqs anvil-impact # never opts a changed package out of tests. $testOnlyPkg = [System.Collections.Generic.List[string]]::new() if ($pkg -contains '--package') { - $metadataJson = & cargo metadata --no-deps --format-version 1 + $metadataJson = & "{{ just_executable() }}" _anvil-with-stable cargo metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-llvm-cov: cargo metadata failed' exit $LASTEXITCODE @@ -2669,7 +2663,7 @@ anvil-llvm-cov: anvil-llvm-cov-validate-prereqs anvil-impact } $gateArgs.Add($pkg[$i]) } - cargo coverage-gate @gateArgs --lcov target/coverage/lcov-all-features.info --lcov target/coverage/lcov-no-default.info + & "{{ just_executable() }}" _anvil-with-stable cargo coverage-gate @gateArgs --lcov target/coverage/lcov-all-features.info --lcov target/coverage/lcov-no-default.info if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # --- pr-test members (shared with scheduled-test) --- @@ -2740,7 +2734,7 @@ anvil-loom: anvil-loom-validate-prereqs anvil-impact Write-Host 'anvil-loom: no affected packages; skipping' exit 0 } - $meta = cargo metadata --no-deps --format-version 1 | ConvertFrom-Json + $meta = & "{{ just_executable() }}" _anvil-with-stable cargo metadata --no-deps --format-version 1 | ConvertFrom-Json if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Optional impact-scoping: recover bare package names from the @@ -2793,7 +2787,7 @@ anvil-loom: anvil-loom-validate-prereqs anvil-impact $env:RUSTFLAGS = "--cfg loom $($env:RUSTFLAGS)".Trim() foreach ($lt in $loomTargets) { Write-Host "anvil-loom: $($lt.pkg) :: $($lt.target)" - & cargo test -p $lt.pkg --release --all-features --locked --test $lt.target -- --test-threads=1 + & "{{ just_executable() }}" _anvil-with-stable cargo test -p $lt.pkg --release --all-features --locked --test $lt.target -- --test-threads=1 if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } } @@ -3000,7 +2994,7 @@ anvil-msrv-test: anvil-msrv-test-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include affected) if ($include -eq '--skip') { Write-Host 'anvil-msrv-test: no affected packages; skipping'; exit 0 } - $toolchain = (& '.anvil/resolve-stable-toolchain.ps1' -MsrvToolchain | Out-String).Trim() + $toolchain = (& "{{ just_executable() }}" _anvil-resolve-stable msrv | Out-String).Trim() if ([string]::IsNullOrWhiteSpace($toolchain)) { Write-Host 'anvil-msrv-test: no root MSRV declared; skipping' exit 0 @@ -3015,14 +3009,14 @@ anvil-msrv-test: anvil-msrv-test-validate-prereqs anvil-impact [group("anvil-setup")] [script("pwsh", "-NoProfile")] anvil-msrv-test-setup installer="install": - & '.anvil/resolve-stable-toolchain.ps1' -InstallMsrvIfNeeded + & "{{ just_executable() }}" _anvil-resolve-stable install-msrv if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Validate the MSRV prerequisite. [group("anvil-setup")] [script("pwsh", "-NoProfile")] anvil-msrv-test-validate-prereqs: - $toolchain = (& '.anvil/resolve-stable-toolchain.ps1' -MsrvToolchain | Out-String).Trim() + $toolchain = (& "{{ just_executable() }}" _anvil-resolve-stable msrv | Out-String).Trim() if ([string]::IsNullOrWhiteSpace($toolchain)) { exit 0 } & cargo "+$toolchain" --version if ($LASTEXITCODE -ne 0) { @@ -3081,7 +3075,7 @@ anvil-mutants-diff: anvil-mutants-diff-validate-prereqs anvil-impact $diff_path = Join-Path $tmp_dir 'anvil-mutants-diff.diff' git diff "$base..HEAD" --output=$diff_path if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - cargo mutants --in-diff $diff_path --no-shuffle --jobs 0 + & "{{ just_executable() }}" _anvil-with-stable cargo mutants --in-diff $diff_path --no-shuffle --jobs 0 if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # --- pr-mutants members --- @@ -3114,7 +3108,7 @@ anvil-mutants-full: anvil-mutants-full-validate-prereqs Write-Host 'anvil-mutants-full: aarch64-pc-windows-msvc -- cargo-mutants does not build here (winapi); skipping' exit 0 } - & cargo mutants --workspace --no-shuffle --jobs 0 + & "{{ just_executable() }}" _anvil-with-stable cargo mutants --workspace --no-shuffle --jobs 0 if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # --- scheduled-exhaustive members (mutants-full reuses cargo-mutants; @@ -3262,7 +3256,7 @@ anvil-readme-check: anvil-readme-check-validate-prereqs # Unscoped: a change to the workspace-level README template is a repo-level # input cargo-delta cannot map to a package, so this check always runs the # full library set rather than an impact subset. - $metadataJson = & cargo metadata --no-deps --format-version 1 + $metadataJson = & "{{ just_executable() }}" _anvil-with-stable cargo metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-readme-check: cargo metadata failed' exit $LASTEXITCODE @@ -3304,7 +3298,7 @@ anvil-readme-check: anvil-readme-check-validate-prereqs } $args = @('doc2readme', '--check') if ($relTemplate) { $args += @('--template', $relTemplate) } - & cargo @args + & "{{ just_executable() }}" _anvil-with-stable cargo @args if ($LASTEXITCODE -ne 0) { $hadFailure = $true } } finally { Pop-Location @@ -3388,7 +3382,7 @@ anvil-semver-check: anvil-semver-check-validate-prereqs anvil-impact # we got here via impact-scoping (cloud workflows) or full-workspace # fallback (local). $libPkgs = @{} - $metadataJson = & cargo metadata --no-deps --format-version 1 + $metadataJson = & "{{ just_executable() }}" _anvil-with-stable cargo metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-semver-check: cargo metadata failed' exit $LASTEXITCODE @@ -3465,7 +3459,7 @@ anvil-semver-check: anvil-semver-check-validate-prereqs anvil-impact } Write-Host "anvil-semver-check: $p (baseline $base)" - $outputLines = @(& cargo semver-checks --package $p --baseline-rev $base 2>&1) + $outputLines = @(& "{{ just_executable() }}" _anvil-with-stable cargo semver-checks --package $p --baseline-rev $base 2>&1) $semverExit = $LASTEXITCODE $output = ($outputLines | Out-String) if ($semverExit -eq 100) { @@ -3604,9 +3598,9 @@ anvil-spellcheck: anvil-spellcheck-validate-prereqs # ignores user-curated dictionaries (`extra_dictionaries`, custom # hunspell langs, etc.). if (Test-Path 'spellcheck.toml') { - cargo spellcheck --cfg spellcheck.toml check --code 1 + & "{{ just_executable() }}" _anvil-with-stable cargo spellcheck --cfg spellcheck.toml check --code 1 } else { - cargo spellcheck check --code 1 + & "{{ just_executable() }}" _anvil-with-stable cargo spellcheck check --code 1 } if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } @@ -4467,19 +4461,25 @@ _anvil-impact-snapshot: # The baseline worktree is checked out at the merge target, whose # rust-toolchain.toml may pin a toolchain that is NOT installed on this # machine (any PR that bumps rust-toolchain.toml hits this). The - # snapshot is metadata-only (file presence + content hashes), so run it - # under the *active* toolchain via RUSTUP_TOOLCHAIN -- resolved here, in - # the current checkout, where it is installed -- which overrides the - # worktree's rust-toolchain.toml. Best-effort: skip if rustup is absent. - $activeToolchain = $null - # `rustup` may be absent (e.g. ADO msrustup provides `cargo` directly - # without a `rustup` command). Under `$ErrorActionPreference = 'Stop'` - # a bare `rustup` call would raise a terminating command-not-found - # error before the `$LASTEXITCODE` guard runs, so probe for the command - # first and leave `$activeToolchain` null when it is unavailable. - if (Get-Command rustup -ErrorAction SilentlyContinue) { - $rt = (rustup show active-toolchain 2>$null) - if (($LASTEXITCODE -eq 0) -and $rt) { $activeToolchain = (($rt | Select-Object -First 1) -split '\s+')[0] } + # snapshot is metadata-only (file presence + content hashes), so use + # the selected compiler from the current checkout to override any + # different toolchain file in the baseline worktree. Environment/MSRV + # selections are already valid rustup overrides. For a selecting + # toolchain file, ask rustup for the concrete active identifier so + # `path` selections are not mistaken for named toolchains. + $baselineToolchain = (& "{{ just_executable() }}" _anvil-resolve-stable for-environment | Out-String).Trim() + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + if ([string]::IsNullOrWhiteSpace($baselineToolchain)) { + if (-not (Get-Command rustup -ErrorAction SilentlyContinue)) { + Write-Error 'anvil-impact: rustup is required to resolve the active repository toolchain for the baseline snapshot' + exit 1 + } + $active = (rustup show active-toolchain 2>$null) + if (($LASTEXITCODE -ne 0) -or -not $active) { + Write-Error 'anvil-impact: failed to resolve the active repository toolchain for the baseline snapshot' + exit 1 + } + $baselineToolchain = (($active | Select-Object -First 1) -split '\s+')[0] } # Temp-root precedence follows the runner-managed scratch dir on each @@ -4512,14 +4512,12 @@ _anvil-impact-snapshot: Set-Content -LiteralPath $baselineKeyFile -Value $baselineKey -Encoding UTF8 -NoNewline } else { Push-Location $wt - # Preserve any caller-provided RUSTUP_TOOLCHAIN: we override it - # only for this baseline snapshot, then restore the caller's - # value (or remove it if we introduced it) so the current - # snapshot below runs under the same toolchain the caller chose. + # Override only this baseline subprocess scope, then restore the + # caller's input override before returning to the current tree. $hadToolchain = Test-Path Env:\RUSTUP_TOOLCHAIN $priorToolchain = if ($hadToolchain) { $env:RUSTUP_TOOLCHAIN } else { $null } try { - if ($activeToolchain) { $env:RUSTUP_TOOLCHAIN = $activeToolchain } + $env:RUSTUP_TOOLCHAIN = $baselineToolchain $baseSnap = cargo delta @deltaArgs snapshot if ($LASTEXITCODE -ne 0) { throw 'cargo delta snapshot (baseline) failed' } } finally { @@ -4546,7 +4544,7 @@ _anvil-impact-snapshot: Write-Host 'anvil-impact: current snapshot up to date' } else { Write-Host 'anvil-impact: snapshotting working tree' - $curSnap = cargo delta @deltaArgs snapshot + $curSnap = & "{{ just_executable() }}" _anvil-with-stable cargo delta @deltaArgs snapshot if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-impact: cargo delta snapshot (current) failed'; exit 1 } Set-Content -LiteralPath $currentJson -Value $curSnap -Encoding UTF8 Set-Content -LiteralPath $currentKeyFile -Value $currentState -Encoding UTF8 -NoNewline @@ -4664,7 +4662,7 @@ anvil-impact: _anvil-impact-snapshot # early), so default to an empty object so impact.json always exists -- # the freshness check above keys on its presence, and a missing file # would force a recompute on every invocation. - $impactOut = (cargo delta @deltaArgs impact --baseline $baselineJson --current $currentJson --format json | Out-String) + $impactOut = (& "{{ just_executable() }}" _anvil-with-stable cargo delta @deltaArgs impact --baseline $baselineJson --current $currentJson --format json | Out-String) if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-impact: cargo delta impact failed'; exit 1 } if ([string]::IsNullOrWhiteSpace($impactOut)) { $impactOut = '{}' } Set-Content -LiteralPath $impactJson -Value $impactOut.Trim() -Encoding UTF8 @@ -4741,7 +4739,7 @@ _anvil-impact-format tier impactJson: # nested workspaces cargo-delta can instead emit a manifest directory # leaf; accept any reported identifier only when every matching namespace # resolves to the *same* workspace package. - $metadataJson = & cargo metadata --no-deps --format-version 1 + $metadataJson = & "{{ just_executable() }}" _anvil-with-stable cargo metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-impact: cargo metadata failed' exit $LASTEXITCODE @@ -5290,7 +5288,7 @@ anvil-tool-rustc-validate-prereqs: Write-Error 'anvil: rustc not found. Install via rustup: https://rustup.rs' exit 1 } - & pwsh -NoProfile -File '.anvil/resolve-stable-toolchain.ps1' -ValidateWorkspaceMsrv + & "{{ just_executable() }}" _anvil-resolve-stable validate-workspace-msrv if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } @@ -5319,6 +5317,41 @@ anvil-tool-pwsh-validate-prereqs: # Private helpers (install/check primitives) # ============================================================================ +# Resolve or prepare the selected stable compiler without globally exporting it. +[script("pwsh", "-NoProfile")] +_anvil-resolve-stable action="resolve": + $ErrorActionPreference = 'Stop' + $resolverArgs = @(switch ('{{action}}') { + 'resolve' { @() } + 'for-environment' { @('-ForEnvironment') } + 'install' { @('-InstallIfMissing') } + 'apply-file-options' { @('-ApplyToolchainFileOptions') } + 'validate-workspace-msrv' { @('-ValidateWorkspaceMsrv') } + 'msrv' { @('-MsrvToolchain') } + 'install-msrv' { @('-InstallMsrvIfNeeded') } + default { + Write-Error "_anvil-resolve-stable: unknown action '{{action}}'" + exit 2 + } + }) + & pwsh -NoProfile -File '.anvil/resolve-stable-toolchain.ps1' @resolverArgs + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + +# Run one command under the selected stable compiler. A repository toolchain +# file yields no override so rustup processes its complete configuration. +[script("pwsh", "-NoProfile")] +_anvil-with-stable command *args: + $ErrorActionPreference = 'Stop' + $toolchain = (& "{{ just_executable() }}" _anvil-resolve-stable for-environment | Out-String).Trim() + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + if ([string]::IsNullOrWhiteSpace($toolchain)) { + Remove-Item Env:\RUSTUP_TOOLCHAIN -ErrorAction SilentlyContinue + } else { + $env:RUSTUP_TOOLCHAIN = $toolchain + } + & '{{command}}' {{args}} + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + # _install-tool: install a cargo subcommand at the catalog version, # or no-op if it is already installed at or above that version. The # `installer` parameter selects between: @@ -5349,7 +5382,7 @@ _install-tool name version installer source_prereq="": # cached binaries and fail with "binary already exists in destination". $installed = $null $pattern = '^' + [regex]::Escape($name) + ' v(\S+):' - foreach ($line in (cargo install --list 2>$null)) { + foreach ($line in (& "{{ just_executable() }}" _anvil-with-stable cargo install --list 2>$null)) { if ($line -match $pattern) { $installed = $Matches[1]; break } } if ($installed) { @@ -5367,7 +5400,7 @@ _install-tool name version installer source_prereq="": if ($installer -eq 'binstall') { if (-not (Get-Command cargo-binstall -ErrorAction SilentlyContinue)) { Write-Host ' Bootstrapping cargo-binstall' - cargo install --locked cargo-binstall + & "{{ just_executable() }}" _anvil-with-stable cargo install --locked cargo-binstall if ($LASTEXITCODE -ne 0) { Write-Error 'cargo-binstall bootstrap failed' exit $LASTEXITCODE @@ -5381,7 +5414,7 @@ _install-tool name version installer source_prereq="": $binstallArgs += @('--disable-strategies', 'compile') } $binstallArgs += @($name, '--version', "=$version") - cargo @binstallArgs + & "{{ just_executable() }}" _anvil-with-stable cargo @binstallArgs if ($LASTEXITCODE -eq 0) { exit 0 } Write-Host ' binstall failed; falling back to cargo install' -ForegroundColor Yellow } @@ -5389,7 +5422,7 @@ _install-tool name version installer source_prereq="": & "{{just_executable()}}" $sourcePrereq if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } } - cargo install --locked $name --version "=$version" + & "{{ just_executable() }}" _anvil-with-stable cargo install --locked $name --version "=$version" if ($LASTEXITCODE -ne 0) { Write-Error "$name install FAILED" exit $LASTEXITCODE @@ -5405,7 +5438,7 @@ _check-tool name version: $installed = $null $pattern = '^' + [regex]::Escape($name) + ' v(\S+):' - foreach ($line in (cargo install --list 2>$null)) { + foreach ($line in (& "{{ just_executable() }}" _anvil-with-stable cargo install --list 2>$null)) { if ($line -match $pattern) { $installed = $Matches[1]; break } } if (-not $installed) { @@ -5486,14 +5519,14 @@ _install-component toolchain component: } if ($null -ne $subcommand) { if ($toolchain -eq 'default') { - & cargo $subcommand --version *> $null + & "{{ just_executable() }}" _anvil-with-stable cargo $subcommand --version *> $null } else { & cargo "+$toolchain" $subcommand --version *> $null } $present = ($LASTEXITCODE -eq 0) } else { $sysroot = if ($toolchain -eq 'default') { - & rustc --print sysroot 2>$null + & "{{ just_executable() }}" _anvil-with-stable rustc --print sysroot 2>$null } else { & rustc "+$toolchain" --print sysroot 2>$null } @@ -5554,14 +5587,14 @@ _check-component toolchain component: } if ($null -ne $subcommand) { if ($toolchain -eq 'default') { - & cargo $subcommand --version *> $null + & "{{ just_executable() }}" _anvil-with-stable cargo $subcommand --version *> $null } else { & cargo "+$toolchain" $subcommand --version *> $null } $present = ($LASTEXITCODE -eq 0) } else { $sysroot = if ($toolchain -eq 'default') { - & rustc --print sysroot 2>$null + & "{{ just_executable() }}" _anvil-with-stable rustc --print sysroot 2>$null } else { & rustc "+$toolchain" --print sysroot 2>$null } @@ -5923,24 +5956,9 @@ anvil-tool-cargo-udeps-validate-prereqs: (_check-tool "cargo-udeps" cargo_udeps_ # Rustup toolchains # ============================================================================ -# Stable toolchain used by every check without an explicit +toolchain argument. -# The resolver honors RUSTUP_TOOLCHAIN, then a selecting root toolchain file, -# then the root package/workspace MSRV. Environment and MSRV selections are -# exported here; a toolchain-file selection stays empty so rustup honors the -# file's complete targets/components/profile configuration. -rust_stable_override := env_var_or_default('RUSTUP_TOOLCHAIN', '') -# The resolver command ends in a comment so PowerShell's commonly configured -# -Command shell ignores Just's argv[0] copy instead of joining it back in. -rust_stable := if rust_stable_override != '' { - rust_stable_override -} else { - shell('pwsh -NoProfile -File .anvil/resolve-stable-toolchain.ps1 -ForEnvironment #') -} -export RUSTUP_TOOLCHAIN := rust_stable -export ANVIL_STABLE_TOOLCHAIN_SOURCE := env_var_or_default( - 'ANVIL_STABLE_TOOLCHAIN_SOURCE', - if rust_stable_override == '' { 'resolved' } else { 'environment' }, -) +# Stable checks select their compiler through `_anvil-with-stable` at the +# individual command site. `RUSTUP_TOOLCHAIN` is an input override only; Anvil +# never exports a resolved value globally into unrelated recipes or helpers. # General nightly used by udeps, miri, careful, and any future # nightly-dependent check. Bumped on a regular cadence (monthly is a diff --git a/justfiles/anvil/checks/aprz.just b/justfiles/anvil/checks/aprz.just index 1627c3a59..897ec37c1 100644 --- a/justfiles/anvil/checks/aprz.just +++ b/justfiles/anvil/checks/aprz.just @@ -44,7 +44,7 @@ anvil-aprz: anvil-aprz-validate-prereqs Write-Warning 'To fix: run `gh auth login` (recommended), or set $env:GITHUB_TOKEN to a GitHub token, then re-run.' } } - cargo aprz deps --error-if-high-risk --console appraisal + & "{{ just_executable() }}" _anvil-with-stable cargo aprz deps --error-if-high-risk --console appraisal if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-aprz` recipe. diff --git a/justfiles/anvil/checks/audit.just b/justfiles/anvil/checks/audit.just index 20de20468..d0aef3197 100644 --- a/justfiles/anvil/checks/audit.just +++ b/justfiles/anvil/checks/audit.just @@ -12,7 +12,7 @@ [script("pwsh", "-NoProfile")] anvil-audit: anvil-audit-validate-prereqs $ErrorActionPreference = 'Stop' - & cargo audit + & "{{ just_executable() }}" _anvil-with-stable cargo audit if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-audit` recipe. diff --git a/justfiles/anvil/checks/bench.just b/justfiles/anvil/checks/bench.just index 2f4d42f74..7f8ca006f 100644 --- a/justfiles/anvil/checks/bench.just +++ b/justfiles/anvil/checks/bench.just @@ -14,7 +14,7 @@ anvil-bench: anvil-bench-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include affected) if ($include -eq '--skip') { Write-Host 'anvil-bench: no affected packages; skipping'; exit 0 } - & cargo bench @(if ($include) { -split $include } else { '--workspace' }) --all-features --no-run + & "{{ just_executable() }}" _anvil-with-stable cargo bench @(if ($include) { -split $include } else { '--workspace' }) --all-features --no-run if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # bench uses the cargo built-in in compile-only mode; no extra tool install diff --git a/justfiles/anvil/checks/bolero.just b/justfiles/anvil/checks/bolero.just index 37f7d21ba..306bb9eea 100644 --- a/justfiles/anvil/checks/bolero.just +++ b/justfiles/anvil/checks/bolero.just @@ -54,7 +54,7 @@ anvil-bolero: anvil-bolero-validate-prereqs anvil-impact $packageNames.Add(($pkgArgs[++$i] -split '@', 2)[0]) } } else { - $metadataJson = & cargo metadata --no-deps --format-version 1 + $metadataJson = & "{{ just_executable() }}" _anvil-with-stable cargo metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-bolero: cargo metadata failed' exit $LASTEXITCODE diff --git a/justfiles/anvil/checks/careful.just b/justfiles/anvil/checks/careful.just index be9871403..c937a5dc2 100644 --- a/justfiles/anvil/checks/careful.just +++ b/justfiles/anvil/checks/careful.just @@ -59,7 +59,7 @@ anvil-careful: anvil-careful-validate-prereqs anvil-impact Write-Host " reason : the nightly toolchain and/or resolved cargo-careful executable changed" Write-Host " now : $idLabel" Write-Host " identity: $($prev.Substring(0, [Math]::Min(12, $prev.Length)))... -> $($idHash.Substring(0, 12))..." - cargo clean + & cargo '+{{ rust_nightly }}' clean if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } } # Persist the current identity (also seeds the marker on first run, when diff --git a/justfiles/anvil/checks/cargo-hack.just b/justfiles/anvil/checks/cargo-hack.just index ea604935a..c42680435 100644 --- a/justfiles/anvil/checks/cargo-hack.just +++ b/justfiles/anvil/checks/cargo-hack.just @@ -16,7 +16,7 @@ anvil-cargo-hack: anvil-cargo-hack-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include required) if ($include -eq '--skip') { Write-Host 'anvil-cargo-hack: no affected packages; skipping'; exit 0 } - & cargo hack @(if ($include) { -split $include } else { '--workspace' }) --feature-powerset --depth 2 check + & "{{ just_executable() }}" _anvil-with-stable cargo hack @(if ($include) { -split $include } else { '--workspace' }) --feature-powerset --depth 2 check if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-cargo-hack` recipe. diff --git a/justfiles/anvil/checks/cargo-sort.just b/justfiles/anvil/checks/cargo-sort.just index a0af1ccb3..8de8ad7a9 100644 --- a/justfiles/anvil/checks/cargo-sort.just +++ b/justfiles/anvil/checks/cargo-sort.just @@ -25,7 +25,7 @@ anvil-cargo-sort: anvil-cargo-sort-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include modified) if ($include -eq '--skip') { Write-Host 'anvil-cargo-sort: no modified packages; skipping'; exit 0 } - cargo sort --workspace --grouped --check --check-format + & "{{ just_executable() }}" _anvil-with-stable cargo sort --workspace --grouped --check --check-format if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-cargo-sort` recipe. diff --git a/justfiles/anvil/checks/clippy.just b/justfiles/anvil/checks/clippy.just index c9167c91c..5b9a8eb57 100644 --- a/justfiles/anvil/checks/clippy.just +++ b/justfiles/anvil/checks/clippy.just @@ -19,7 +19,7 @@ anvil-clippy: anvil-clippy-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include affected) if ($include -eq '--skip') { Write-Host 'anvil-clippy: no affected packages; skipping'; exit 0 } - & cargo clippy @(if ($include) { -split $include } else { '--workspace' }) --all-targets --all-features --locked "--" '-D' 'warnings' + & "{{ just_executable() }}" _anvil-with-stable cargo clippy @(if ($include) { -split $include } else { '--workspace' }) --all-targets --all-features --locked "--" '-D' 'warnings' if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-clippy` recipe. diff --git a/justfiles/anvil/checks/deny.just b/justfiles/anvil/checks/deny.just index c52b613a4..17d098305 100644 --- a/justfiles/anvil/checks/deny.just +++ b/justfiles/anvil/checks/deny.just @@ -14,7 +14,7 @@ [script("pwsh", "-NoProfile")] anvil-deny: anvil-deny-validate-prereqs $ErrorActionPreference = 'Stop' - & cargo deny check + & "{{ just_executable() }}" _anvil-with-stable cargo deny check if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-deny` recipe. diff --git a/justfiles/anvil/checks/doc-build.just b/justfiles/anvil/checks/doc-build.just index 9250d41af..b29336163 100644 --- a/justfiles/anvil/checks/doc-build.just +++ b/justfiles/anvil/checks/doc-build.just @@ -17,7 +17,7 @@ anvil-doc-build: anvil-doc-build-validate-prereqs anvil-impact $include = (& "{{ just_executable() }}" _anvil-impact-include required) if ($include -eq '--skip') { Write-Host 'anvil-doc-build: no affected packages; skipping'; exit 0 } $env:RUSTDOCFLAGS = '-D warnings' - & cargo doc @(if ($include) { -split $include } else { '--workspace' }) --all-features --no-deps + & "{{ just_executable() }}" _anvil-with-stable cargo doc @(if ($include) { -split $include } else { '--workspace' }) --all-features --no-deps if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # doc-build, examples and doc-test are pure cargo built-ins; the rust diff --git a/justfiles/anvil/checks/doc-test.just b/justfiles/anvil/checks/doc-test.just index 1954ae4a7..1480f48d5 100644 --- a/justfiles/anvil/checks/doc-test.just +++ b/justfiles/anvil/checks/doc-test.just @@ -20,9 +20,9 @@ anvil-doc-test: anvil-doc-test-validate-prereqs anvil-impact $include = (& "{{ just_executable() }}" _anvil-impact-include affected) if ($include -eq '--skip') { Write-Host 'anvil-doc-test: no affected packages; skipping'; exit 0 } $pkg = @(if ($include) { -split $include } else { '--workspace' }) - & cargo test --doc @pkg --all-features --locked + & "{{ just_executable() }}" _anvil-with-stable cargo test --doc @pkg --all-features --locked if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - & cargo test --doc @pkg --locked + & "{{ just_executable() }}" _anvil-with-stable cargo test --doc @pkg --locked if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-doc-test` recipe. diff --git a/justfiles/anvil/checks/ensure-no-cyclic-deps.just b/justfiles/anvil/checks/ensure-no-cyclic-deps.just index 48ef3e7c0..e74beab7c 100644 --- a/justfiles/anvil/checks/ensure-no-cyclic-deps.just +++ b/justfiles/anvil/checks/ensure-no-cyclic-deps.just @@ -14,7 +14,7 @@ anvil-ensure-no-cyclic-deps: anvil-ensure-no-cyclic-deps-validate-prereqs anvil- $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include modified) if ($include -eq '--skip') { Write-Host 'anvil-ensure-no-cyclic-deps: no modified packages; skipping'; exit 0 } - cargo ensure-no-cyclic-deps + & "{{ just_executable() }}" _anvil-with-stable cargo ensure-no-cyclic-deps if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-ensure-no-cyclic-deps` recipe. diff --git a/justfiles/anvil/checks/ensure-no-default-features.just b/justfiles/anvil/checks/ensure-no-default-features.just index c56682046..9def94fa3 100644 --- a/justfiles/anvil/checks/ensure-no-default-features.just +++ b/justfiles/anvil/checks/ensure-no-default-features.just @@ -14,7 +14,7 @@ anvil-ensure-no-default-features: anvil-ensure-no-default-features-validate-prer $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include modified) if ($include -eq '--skip') { Write-Host 'anvil-ensure-no-default-features: no modified packages; skipping'; exit 0 } - cargo ensure-no-default-features + & "{{ just_executable() }}" _anvil-with-stable cargo ensure-no-default-features if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-ensure-no-default-features` recipe. diff --git a/justfiles/anvil/checks/examples.just b/justfiles/anvil/checks/examples.just index 698b11157..47ab115f9 100644 --- a/justfiles/anvil/checks/examples.just +++ b/justfiles/anvil/checks/examples.just @@ -14,7 +14,7 @@ anvil-examples: anvil-examples-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include affected) if ($include -eq '--skip') { Write-Host 'anvil-examples: no affected packages; skipping'; exit 0 } - & cargo build @(if ($include) { -split $include } else { '--workspace' }) --examples --all-features --locked + & "{{ just_executable() }}" _anvil-with-stable cargo build @(if ($include) { -split $include } else { '--workspace' }) --examples --all-features --locked if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-examples` recipe. diff --git a/justfiles/anvil/checks/external-types.just b/justfiles/anvil/checks/external-types.just index 33c0593d2..87287a42a 100644 --- a/justfiles/anvil/checks/external-types.just +++ b/justfiles/anvil/checks/external-types.just @@ -31,7 +31,7 @@ anvil-external-types: anvil-external-types-validate-prereqs anvil-impact $pkg = @(if ($include) { -split $include } else { '--workspace' }) # Build pkg-name -> manifest-path map, restricted to library crates. $libPkgs = @{} - $metadataJson = & cargo metadata --no-deps --format-version 1 + $metadataJson = & "{{ just_executable() }}" _anvil-with-stable cargo metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-external-types: cargo metadata failed' exit $LASTEXITCODE diff --git a/justfiles/anvil/checks/license-headers.just b/justfiles/anvil/checks/license-headers.just index 989f57b4b..7f489f48a 100644 --- a/justfiles/anvil/checks/license-headers.just +++ b/justfiles/anvil/checks/license-headers.just @@ -14,7 +14,7 @@ anvil-license-headers: anvil-license-headers-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include modified) if ($include -eq '--skip') { Write-Host 'anvil-license-headers: no modified packages; skipping'; exit 0 } - cargo heather + & "{{ just_executable() }}" _anvil-with-stable cargo heather if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-license-headers` recipe. diff --git a/justfiles/anvil/checks/llvm-cov.just b/justfiles/anvil/checks/llvm-cov.just index ab39d6717..fbace94e0 100644 --- a/justfiles/anvil/checks/llvm-cov.just +++ b/justfiles/anvil/checks/llvm-cov.just @@ -37,7 +37,7 @@ anvil-llvm-cov: anvil-llvm-cov-validate-prereqs anvil-impact # never opts a changed package out of tests. $testOnlyPkg = [System.Collections.Generic.List[string]]::new() if ($pkg -contains '--package') { - $metadataJson = & cargo metadata --no-deps --format-version 1 + $metadataJson = & "{{ just_executable() }}" _anvil-with-stable cargo metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-llvm-cov: cargo metadata failed' exit $LASTEXITCODE @@ -168,7 +168,7 @@ anvil-llvm-cov: anvil-llvm-cov-validate-prereqs anvil-impact } $gateArgs.Add($pkg[$i]) } - cargo coverage-gate @gateArgs --lcov target/coverage/lcov-all-features.info --lcov target/coverage/lcov-no-default.info + & "{{ just_executable() }}" _anvil-with-stable cargo coverage-gate @gateArgs --lcov target/coverage/lcov-all-features.info --lcov target/coverage/lcov-no-default.info if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # --- pr-test members (shared with scheduled-test) --- diff --git a/justfiles/anvil/checks/loom.just b/justfiles/anvil/checks/loom.just index 0f88aba6b..4cc8441b3 100644 --- a/justfiles/anvil/checks/loom.just +++ b/justfiles/anvil/checks/loom.just @@ -55,7 +55,7 @@ anvil-loom: anvil-loom-validate-prereqs anvil-impact Write-Host 'anvil-loom: no affected packages; skipping' exit 0 } - $meta = cargo metadata --no-deps --format-version 1 | ConvertFrom-Json + $meta = & "{{ just_executable() }}" _anvil-with-stable cargo metadata --no-deps --format-version 1 | ConvertFrom-Json if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Optional impact-scoping: recover bare package names from the @@ -108,7 +108,7 @@ anvil-loom: anvil-loom-validate-prereqs anvil-impact $env:RUSTFLAGS = "--cfg loom $($env:RUSTFLAGS)".Trim() foreach ($lt in $loomTargets) { Write-Host "anvil-loom: $($lt.pkg) :: $($lt.target)" - & cargo test -p $lt.pkg --release --all-features --locked --test $lt.target -- --test-threads=1 + & "{{ just_executable() }}" _anvil-with-stable cargo test -p $lt.pkg --release --all-features --locked --test $lt.target -- --test-threads=1 if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } } diff --git a/justfiles/anvil/checks/msrv-test.just b/justfiles/anvil/checks/msrv-test.just index 6e1ca1ddc..362e470c8 100644 --- a/justfiles/anvil/checks/msrv-test.just +++ b/justfiles/anvil/checks/msrv-test.just @@ -12,7 +12,7 @@ anvil-msrv-test: anvil-msrv-test-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include affected) if ($include -eq '--skip') { Write-Host 'anvil-msrv-test: no affected packages; skipping'; exit 0 } - $toolchain = (& '.anvil/resolve-stable-toolchain.ps1' -MsrvToolchain | Out-String).Trim() + $toolchain = (& "{{ just_executable() }}" _anvil-resolve-stable msrv | Out-String).Trim() if ([string]::IsNullOrWhiteSpace($toolchain)) { Write-Host 'anvil-msrv-test: no root MSRV declared; skipping' exit 0 @@ -27,14 +27,14 @@ anvil-msrv-test: anvil-msrv-test-validate-prereqs anvil-impact [group("anvil-setup")] [script("pwsh", "-NoProfile")] anvil-msrv-test-setup installer="install": - & '.anvil/resolve-stable-toolchain.ps1' -InstallMsrvIfNeeded + & "{{ just_executable() }}" _anvil-resolve-stable install-msrv if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Validate the MSRV prerequisite. [group("anvil-setup")] [script("pwsh", "-NoProfile")] anvil-msrv-test-validate-prereqs: - $toolchain = (& '.anvil/resolve-stable-toolchain.ps1' -MsrvToolchain | Out-String).Trim() + $toolchain = (& "{{ just_executable() }}" _anvil-resolve-stable msrv | Out-String).Trim() if ([string]::IsNullOrWhiteSpace($toolchain)) { exit 0 } & cargo "+$toolchain" --version if ($LASTEXITCODE -ne 0) { diff --git a/justfiles/anvil/checks/mutants-diff.just b/justfiles/anvil/checks/mutants-diff.just index 25c98ba9a..e3c3c4234 100644 --- a/justfiles/anvil/checks/mutants-diff.just +++ b/justfiles/anvil/checks/mutants-diff.just @@ -43,7 +43,7 @@ anvil-mutants-diff: anvil-mutants-diff-validate-prereqs anvil-impact $diff_path = Join-Path $tmp_dir 'anvil-mutants-diff.diff' git diff "$base..HEAD" --output=$diff_path if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - cargo mutants --in-diff $diff_path --no-shuffle --jobs 0 + & "{{ just_executable() }}" _anvil-with-stable cargo mutants --in-diff $diff_path --no-shuffle --jobs 0 if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # --- pr-mutants members --- diff --git a/justfiles/anvil/checks/mutants-full.just b/justfiles/anvil/checks/mutants-full.just index a914f1eb6..4009e7577 100644 --- a/justfiles/anvil/checks/mutants-full.just +++ b/justfiles/anvil/checks/mutants-full.just @@ -17,7 +17,7 @@ anvil-mutants-full: anvil-mutants-full-validate-prereqs Write-Host 'anvil-mutants-full: aarch64-pc-windows-msvc -- cargo-mutants does not build here (winapi); skipping' exit 0 } - & cargo mutants --workspace --no-shuffle --jobs 0 + & "{{ just_executable() }}" _anvil-with-stable cargo mutants --workspace --no-shuffle --jobs 0 if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # --- scheduled-exhaustive members (mutants-full reuses cargo-mutants; diff --git a/justfiles/anvil/checks/readme-check.just b/justfiles/anvil/checks/readme-check.just index 1d258e25b..615fa4430 100644 --- a/justfiles/anvil/checks/readme-check.just +++ b/justfiles/anvil/checks/readme-check.just @@ -47,7 +47,7 @@ anvil-readme-check: anvil-readme-check-validate-prereqs # Unscoped: a change to the workspace-level README template is a repo-level # input cargo-delta cannot map to a package, so this check always runs the # full library set rather than an impact subset. - $metadataJson = & cargo metadata --no-deps --format-version 1 + $metadataJson = & "{{ just_executable() }}" _anvil-with-stable cargo metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-readme-check: cargo metadata failed' exit $LASTEXITCODE @@ -89,7 +89,7 @@ anvil-readme-check: anvil-readme-check-validate-prereqs } $args = @('doc2readme', '--check') if ($relTemplate) { $args += @('--template', $relTemplate) } - & cargo @args + & "{{ just_executable() }}" _anvil-with-stable cargo @args if ($LASTEXITCODE -ne 0) { $hadFailure = $true } } finally { Pop-Location diff --git a/justfiles/anvil/checks/semver-check.just b/justfiles/anvil/checks/semver-check.just index 4f7112b6e..7cbac0ac6 100644 --- a/justfiles/anvil/checks/semver-check.just +++ b/justfiles/anvil/checks/semver-check.just @@ -65,7 +65,7 @@ anvil-semver-check: anvil-semver-check-validate-prereqs anvil-impact # we got here via impact-scoping (cloud workflows) or full-workspace # fallback (local). $libPkgs = @{} - $metadataJson = & cargo metadata --no-deps --format-version 1 + $metadataJson = & "{{ just_executable() }}" _anvil-with-stable cargo metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-semver-check: cargo metadata failed' exit $LASTEXITCODE @@ -142,7 +142,7 @@ anvil-semver-check: anvil-semver-check-validate-prereqs anvil-impact } Write-Host "anvil-semver-check: $p (baseline $base)" - $outputLines = @(& cargo semver-checks --package $p --baseline-rev $base 2>&1) + $outputLines = @(& "{{ just_executable() }}" _anvil-with-stable cargo semver-checks --package $p --baseline-rev $base 2>&1) $semverExit = $LASTEXITCODE $output = ($outputLines | Out-String) if ($semverExit -eq 100) { diff --git a/justfiles/anvil/checks/spellcheck.just b/justfiles/anvil/checks/spellcheck.just index 15e734b47..805d43cc0 100644 --- a/justfiles/anvil/checks/spellcheck.just +++ b/justfiles/anvil/checks/spellcheck.just @@ -42,9 +42,9 @@ anvil-spellcheck: anvil-spellcheck-validate-prereqs # ignores user-curated dictionaries (`extra_dictionaries`, custom # hunspell langs, etc.). if (Test-Path 'spellcheck.toml') { - cargo spellcheck --cfg spellcheck.toml check --code 1 + & "{{ just_executable() }}" _anvil-with-stable cargo spellcheck --cfg spellcheck.toml check --code 1 } else { - cargo spellcheck check --code 1 + & "{{ just_executable() }}" _anvil-with-stable cargo spellcheck check --code 1 } if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } diff --git a/justfiles/anvil/impact.just b/justfiles/anvil/impact.just index 42d745026..56223c8aa 100644 --- a/justfiles/anvil/impact.just +++ b/justfiles/anvil/impact.just @@ -235,19 +235,25 @@ _anvil-impact-snapshot: # The baseline worktree is checked out at the merge target, whose # rust-toolchain.toml may pin a toolchain that is NOT installed on this # machine (any PR that bumps rust-toolchain.toml hits this). The - # snapshot is metadata-only (file presence + content hashes), so run it - # under the *active* toolchain via RUSTUP_TOOLCHAIN -- resolved here, in - # the current checkout, where it is installed -- which overrides the - # worktree's rust-toolchain.toml. Best-effort: skip if rustup is absent. - $activeToolchain = $null - # `rustup` may be absent (e.g. ADO msrustup provides `cargo` directly - # without a `rustup` command). Under `$ErrorActionPreference = 'Stop'` - # a bare `rustup` call would raise a terminating command-not-found - # error before the `$LASTEXITCODE` guard runs, so probe for the command - # first and leave `$activeToolchain` null when it is unavailable. - if (Get-Command rustup -ErrorAction SilentlyContinue) { - $rt = (rustup show active-toolchain 2>$null) - if (($LASTEXITCODE -eq 0) -and $rt) { $activeToolchain = (($rt | Select-Object -First 1) -split '\s+')[0] } + # snapshot is metadata-only (file presence + content hashes), so use + # the selected compiler from the current checkout to override any + # different toolchain file in the baseline worktree. Environment/MSRV + # selections are already valid rustup overrides. For a selecting + # toolchain file, ask rustup for the concrete active identifier so + # `path` selections are not mistaken for named toolchains. + $baselineToolchain = (& "{{ just_executable() }}" _anvil-resolve-stable for-environment | Out-String).Trim() + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + if ([string]::IsNullOrWhiteSpace($baselineToolchain)) { + if (-not (Get-Command rustup -ErrorAction SilentlyContinue)) { + Write-Error 'anvil-impact: rustup is required to resolve the active repository toolchain for the baseline snapshot' + exit 1 + } + $active = (rustup show active-toolchain 2>$null) + if (($LASTEXITCODE -ne 0) -or -not $active) { + Write-Error 'anvil-impact: failed to resolve the active repository toolchain for the baseline snapshot' + exit 1 + } + $baselineToolchain = (($active | Select-Object -First 1) -split '\s+')[0] } # Temp-root precedence follows the runner-managed scratch dir on each @@ -280,14 +286,12 @@ _anvil-impact-snapshot: Set-Content -LiteralPath $baselineKeyFile -Value $baselineKey -Encoding UTF8 -NoNewline } else { Push-Location $wt - # Preserve any caller-provided RUSTUP_TOOLCHAIN: we override it - # only for this baseline snapshot, then restore the caller's - # value (or remove it if we introduced it) so the current - # snapshot below runs under the same toolchain the caller chose. + # Override only this baseline subprocess scope, then restore the + # caller's input override before returning to the current tree. $hadToolchain = Test-Path Env:\RUSTUP_TOOLCHAIN $priorToolchain = if ($hadToolchain) { $env:RUSTUP_TOOLCHAIN } else { $null } try { - if ($activeToolchain) { $env:RUSTUP_TOOLCHAIN = $activeToolchain } + $env:RUSTUP_TOOLCHAIN = $baselineToolchain $baseSnap = cargo delta @deltaArgs snapshot if ($LASTEXITCODE -ne 0) { throw 'cargo delta snapshot (baseline) failed' } } finally { @@ -314,7 +318,7 @@ _anvil-impact-snapshot: Write-Host 'anvil-impact: current snapshot up to date' } else { Write-Host 'anvil-impact: snapshotting working tree' - $curSnap = cargo delta @deltaArgs snapshot + $curSnap = & "{{ just_executable() }}" _anvil-with-stable cargo delta @deltaArgs snapshot if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-impact: cargo delta snapshot (current) failed'; exit 1 } Set-Content -LiteralPath $currentJson -Value $curSnap -Encoding UTF8 Set-Content -LiteralPath $currentKeyFile -Value $currentState -Encoding UTF8 -NoNewline @@ -432,7 +436,7 @@ anvil-impact: _anvil-impact-snapshot # early), so default to an empty object so impact.json always exists -- # the freshness check above keys on its presence, and a missing file # would force a recompute on every invocation. - $impactOut = (cargo delta @deltaArgs impact --baseline $baselineJson --current $currentJson --format json | Out-String) + $impactOut = (& "{{ just_executable() }}" _anvil-with-stable cargo delta @deltaArgs impact --baseline $baselineJson --current $currentJson --format json | Out-String) if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-impact: cargo delta impact failed'; exit 1 } if ([string]::IsNullOrWhiteSpace($impactOut)) { $impactOut = '{}' } Set-Content -LiteralPath $impactJson -Value $impactOut.Trim() -Encoding UTF8 @@ -509,7 +513,7 @@ _anvil-impact-format tier impactJson: # nested workspaces cargo-delta can instead emit a manifest directory # leaf; accept any reported identifier only when every matching namespace # resolves to the *same* workspace package. - $metadataJson = & cargo metadata --no-deps --format-version 1 + $metadataJson = & "{{ just_executable() }}" _anvil-with-stable cargo metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-impact: cargo metadata failed' exit $LASTEXITCODE diff --git a/justfiles/anvil/tools.just b/justfiles/anvil/tools.just index 1de4bcaf5..54ef872d2 100644 --- a/justfiles/anvil/tools.just +++ b/justfiles/anvil/tools.just @@ -144,7 +144,7 @@ anvil-tool-rustc-validate-prereqs: Write-Error 'anvil: rustc not found. Install via rustup: https://rustup.rs' exit 1 } - & pwsh -NoProfile -File '.anvil/resolve-stable-toolchain.ps1' -ValidateWorkspaceMsrv + & "{{ just_executable() }}" _anvil-resolve-stable validate-workspace-msrv if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } @@ -173,6 +173,41 @@ anvil-tool-pwsh-validate-prereqs: # Private helpers (install/check primitives) # ============================================================================ +# Resolve or prepare the selected stable compiler without globally exporting it. +[script("pwsh", "-NoProfile")] +_anvil-resolve-stable action="resolve": + $ErrorActionPreference = 'Stop' + $resolverArgs = @(switch ('{{action}}') { + 'resolve' { @() } + 'for-environment' { @('-ForEnvironment') } + 'install' { @('-InstallIfMissing') } + 'apply-file-options' { @('-ApplyToolchainFileOptions') } + 'validate-workspace-msrv' { @('-ValidateWorkspaceMsrv') } + 'msrv' { @('-MsrvToolchain') } + 'install-msrv' { @('-InstallMsrvIfNeeded') } + default { + Write-Error "_anvil-resolve-stable: unknown action '{{action}}'" + exit 2 + } + }) + & pwsh -NoProfile -File '.anvil/resolve-stable-toolchain.ps1' @resolverArgs + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + +# Run one command under the selected stable compiler. A repository toolchain +# file yields no override so rustup processes its complete configuration. +[script("pwsh", "-NoProfile")] +_anvil-with-stable command *args: + $ErrorActionPreference = 'Stop' + $toolchain = (& "{{ just_executable() }}" _anvil-resolve-stable for-environment | Out-String).Trim() + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + if ([string]::IsNullOrWhiteSpace($toolchain)) { + Remove-Item Env:\RUSTUP_TOOLCHAIN -ErrorAction SilentlyContinue + } else { + $env:RUSTUP_TOOLCHAIN = $toolchain + } + & '{{command}}' {{args}} + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + # _install-tool: install a cargo subcommand at the catalog version, # or no-op if it is already installed at or above that version. The # `installer` parameter selects between: @@ -203,7 +238,7 @@ _install-tool name version installer source_prereq="": # cached binaries and fail with "binary already exists in destination". $installed = $null $pattern = '^' + [regex]::Escape($name) + ' v(\S+):' - foreach ($line in (cargo install --list 2>$null)) { + foreach ($line in (& "{{ just_executable() }}" _anvil-with-stable cargo install --list 2>$null)) { if ($line -match $pattern) { $installed = $Matches[1]; break } } if ($installed) { @@ -221,7 +256,7 @@ _install-tool name version installer source_prereq="": if ($installer -eq 'binstall') { if (-not (Get-Command cargo-binstall -ErrorAction SilentlyContinue)) { Write-Host ' Bootstrapping cargo-binstall' - cargo install --locked cargo-binstall + & "{{ just_executable() }}" _anvil-with-stable cargo install --locked cargo-binstall if ($LASTEXITCODE -ne 0) { Write-Error 'cargo-binstall bootstrap failed' exit $LASTEXITCODE @@ -235,7 +270,7 @@ _install-tool name version installer source_prereq="": $binstallArgs += @('--disable-strategies', 'compile') } $binstallArgs += @($name, '--version', "=$version") - cargo @binstallArgs + & "{{ just_executable() }}" _anvil-with-stable cargo @binstallArgs if ($LASTEXITCODE -eq 0) { exit 0 } Write-Host ' binstall failed; falling back to cargo install' -ForegroundColor Yellow } @@ -243,7 +278,7 @@ _install-tool name version installer source_prereq="": & "{{just_executable()}}" $sourcePrereq if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } } - cargo install --locked $name --version "=$version" + & "{{ just_executable() }}" _anvil-with-stable cargo install --locked $name --version "=$version" if ($LASTEXITCODE -ne 0) { Write-Error "$name install FAILED" exit $LASTEXITCODE @@ -259,7 +294,7 @@ _check-tool name version: $installed = $null $pattern = '^' + [regex]::Escape($name) + ' v(\S+):' - foreach ($line in (cargo install --list 2>$null)) { + foreach ($line in (& "{{ just_executable() }}" _anvil-with-stable cargo install --list 2>$null)) { if ($line -match $pattern) { $installed = $Matches[1]; break } } if (-not $installed) { @@ -340,14 +375,14 @@ _install-component toolchain component: } if ($null -ne $subcommand) { if ($toolchain -eq 'default') { - & cargo $subcommand --version *> $null + & "{{ just_executable() }}" _anvil-with-stable cargo $subcommand --version *> $null } else { & cargo "+$toolchain" $subcommand --version *> $null } $present = ($LASTEXITCODE -eq 0) } else { $sysroot = if ($toolchain -eq 'default') { - & rustc --print sysroot 2>$null + & "{{ just_executable() }}" _anvil-with-stable rustc --print sysroot 2>$null } else { & rustc "+$toolchain" --print sysroot 2>$null } @@ -408,14 +443,14 @@ _check-component toolchain component: } if ($null -ne $subcommand) { if ($toolchain -eq 'default') { - & cargo $subcommand --version *> $null + & "{{ just_executable() }}" _anvil-with-stable cargo $subcommand --version *> $null } else { & cargo "+$toolchain" $subcommand --version *> $null } $present = ($LASTEXITCODE -eq 0) } else { $sysroot = if ($toolchain -eq 'default') { - & rustc --print sysroot 2>$null + & "{{ just_executable() }}" _anvil-with-stable rustc --print sysroot 2>$null } else { & rustc "+$toolchain" --print sysroot 2>$null } diff --git a/justfiles/anvil/versions.just b/justfiles/anvil/versions.just index 888224ea1..e2441b62a 100644 --- a/justfiles/anvil/versions.just +++ b/justfiles/anvil/versions.just @@ -30,24 +30,9 @@ # Rustup toolchains # ============================================================================ -# Stable toolchain used by every check without an explicit +toolchain argument. -# The resolver honors RUSTUP_TOOLCHAIN, then a selecting root toolchain file, -# then the root package/workspace MSRV. Environment and MSRV selections are -# exported here; a toolchain-file selection stays empty so rustup honors the -# file's complete targets/components/profile configuration. -rust_stable_override := env_var_or_default('RUSTUP_TOOLCHAIN', '') -# The resolver command ends in a comment so PowerShell's commonly configured -# -Command shell ignores Just's argv[0] copy instead of joining it back in. -rust_stable := if rust_stable_override != '' { - rust_stable_override -} else { - shell('pwsh -NoProfile -File .anvil/resolve-stable-toolchain.ps1 -ForEnvironment #') -} -export RUSTUP_TOOLCHAIN := rust_stable -export ANVIL_STABLE_TOOLCHAIN_SOURCE := env_var_or_default( - 'ANVIL_STABLE_TOOLCHAIN_SOURCE', - if rust_stable_override == '' { 'resolved' } else { 'environment' }, -) +# Stable checks select their compiler through `_anvil-with-stable` at the +# individual command site. `RUSTUP_TOOLCHAIN` is an input override only; Anvil +# never exports a resolved value globally into unrelated recipes or helpers. # General nightly used by udeps, miri, careful, and any future # nightly-dependent check. Bumped on a regular cadence (monthly is a From ce7b878fb7553bf057c5145cb7161b207be2ebc9 Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Fri, 28 Aug 2026 13:30:25 +0200 Subject: [PATCH 12/32] Harden MSRV toolchain validation Reject incomplete internal mappings, accept member MSRVs satisfied by the root floor, and cover public and mapped MSRV provisioning branches. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .anvil.lock | 4 +- .anvil/resolve-stable-toolchain.ps1 | 24 ++- crates/cargo-anvil/docs/design/ado.md | 10 +- crates/cargo-anvil/docs/design/checks.md | 8 +- crates/cargo-anvil/docs/design/containers.md | 6 +- crates/cargo-anvil/docs/design/local.md | 30 ++-- .../src/anvil/artifacts/justfile.rs | 150 +++++++++++++++++- .../anvil/resolve-stable-toolchain.ps1 | 24 ++- .../snapshots/snapshots__ado_backend.snap | 24 ++- .../snapshots/snapshots__github_backend.snap | 24 ++- .../snapshots/snapshots__local_only.snap | 24 ++- 11 files changed, 272 insertions(+), 56 deletions(-) diff --git a/.anvil.lock b/.anvil.lock index 3160a4b41..b83469fe3 100644 --- a/.anvil.lock +++ b/.anvil.lock @@ -1,7 +1,7 @@ version = 1 tool = "anvil" tool_version = "0.5.0" -catalog_checksum = "sha256:969758e637017f6cd43c97177d8cb14547afa16cd2f10250469205def7351b54" +catalog_checksum = "sha256:2316debcdff5f1d470a6d10141decd0640af3860f3aa6dc9247a7036faa8f029" [[file]] path = ".anvil/container/Containerfile" @@ -37,7 +37,7 @@ checksum = "sha256:0f4fcf0fef43e4736260af771cae15b7eb8afc382e5290fabddc52894b091 [[file]] path = ".anvil/resolve-stable-toolchain.ps1" -checksum = "sha256:bf3baa2f3e0c33c913fbea5b6b810523fbc5ce3840849fb833a680fcaab330d7" +checksum = "sha256:bb2a90e425585813e4693a8c41fe6bb2705232dc1306d9cce421af85012fd4e4" [[file]] path = ".github/actions/anvil-impact/action.yml" diff --git a/.anvil/resolve-stable-toolchain.ps1 b/.anvil/resolve-stable-toolchain.ps1 index 7c8b1b36c..e38de2673 100644 --- a/.anvil/resolve-stable-toolchain.ps1 +++ b/.anvil/resolve-stable-toolchain.ps1 @@ -154,7 +154,7 @@ function Get-RootMsrv([switch] $AllowMissing) { throw 'anvil: no selecting rust-toolchain(.toml) and no root [workspace.package] or [package] rust-version; declare an MSRV or select a toolchain explicitly' } -function Assert-UniformWorkspaceMsrv { +function Assert-WorkspaceMsrvCompatibility { $manifestPath = Join-Path $repoRoot 'Cargo.toml' $metadataText = (& cargo metadata --manifest-path $manifestPath --format-version 1 --no-deps 2>&1 | Out-String) if ($LASTEXITCODE -ne 0) { @@ -171,10 +171,22 @@ function Assert-UniformWorkspaceMsrv { throw "anvil: workspace packages without a resolved rust-version: $($missing -join ', '); declare one or select a toolchain explicitly" } - $versions = @($members | ForEach-Object { [string] $_.rust_version } | Sort-Object -Unique) $rootMsrv = Get-RootMsrv - if ($versions.Count -ne 1 -or $versions[0] -ne $rootMsrv) { - throw "anvil: workspace packages must resolve to the root MSRV $rootMsrv; found $($versions -join ', '); select one catalog toolchain explicitly with rust-toolchain.toml" + $newer = @( + $members | + Where-Object { [version] ([string] $_.rust_version) -gt [version] $rootMsrv } | + ForEach-Object { "$($_.name) ($($_.rust_version))" } + ) + if ($newer.Count -gt 0) { + throw "anvil: workspace packages require a compiler newer than the root MSRV $rootMsrv`: $($newer -join ', '); raise the root MSRV or select one catalog toolchain explicitly with rust-toolchain.toml" + } +} + +function Assert-CompleteInternalToolchainConfiguration { + if (-not [string]::IsNullOrWhiteSpace($env:ANVIL_MSRV_TOOLCHAIN) -and + [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN) -and + $null -eq $fileSelection) { + throw 'anvil: ANVIL_MSRV_TOOLCHAIN is set without RUSTUP_TOOLCHAIN and no repository toolchain file selects stable checks; set RUSTUP_TOOLCHAIN to the provisioned stable toolchain or unset ANVIL_MSRV_TOOLCHAIN' } } @@ -244,9 +256,10 @@ if ($EmitToolchainFileOptions) { exit 0 } if ($ValidateWorkspaceMsrv) { + Assert-CompleteInternalToolchainConfiguration $skipWorkspaceMsrvValidation = -not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN) if (-not $skipWorkspaceMsrvValidation -and $null -eq $fileSelection) { - Assert-UniformWorkspaceMsrv + Assert-WorkspaceMsrvCompatibility } exit 0 } @@ -270,6 +283,7 @@ if ($MsrvToolchain -or $InstallMsrvIfNeeded) { exit 0 } +Assert-CompleteInternalToolchainConfiguration $selection = if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { [pscustomobject]@{ Value = $env:RUSTUP_TOOLCHAIN; Source = 'environment'; Installable = $false } } elseif ($null -ne $fileSelection) { diff --git a/crates/cargo-anvil/docs/design/ado.md b/crates/cargo-anvil/docs/design/ado.md index a7276d248..c52a0e3a7 100644 --- a/crates/cargo-anvil/docs/design/ado.md +++ b/crates/cargo-anvil/docs/design/ado.md @@ -373,9 +373,11 @@ x86_64 jobs and per-OS affected-package impact sets as `pr_test`, and invokes `anvil-pr-msrv`. The stage consumes the per-OS impact artifact like the other PR groups; when the root manifest declares no MSRV, the recipe exits successfully without running tests. -Internal pipelines set `ANVIL_MSRV_TOOLCHAIN` to the provisioned `ms-prod-*` -equivalent of that MSRV; public pipelines let Anvil install the declared MSRV -through rustup. +Internal pipelines set `RUSTUP_TOOLCHAIN` to their provisioned stable compiler +and `ANVIL_MSRV_TOOLCHAIN` to the provisioned `ms-prod-*` equivalent of the +declared MSRV. Supplying only the MSRV mapping is rejected because stable checks +would otherwise fall through to a public channel. Public pipelines leave both +unset and let Anvil install the declared MSRV through rustup. ### 4.1 Per-job wrapper (`steps/job.yml`) — the 1ESPT extensibility point @@ -769,7 +771,7 @@ placement: a setup stage that `dependsOn`s nothing and runs first, followed by t stages. `anvil-tool-rustc-validate-prereqs` validates the selected compiler and the -uniform-MSRV fallback rule. For nightly-requiring checks, the matching +workspace MSRV compatibility rule. For nightly-requiring checks, the matching toolchain-validate-prereqs recipe fails with a suggestion to ask the team's pipeline owner to add that dated nightly to msrustup. diff --git a/crates/cargo-anvil/docs/design/checks.md b/crates/cargo-anvil/docs/design/checks.md index 719cc9c69..9a2eabf3f 100644 --- a/crates/cargo-anvil/docs/design/checks.md +++ b/crates/cargo-anvil/docs/design/checks.md @@ -218,8 +218,12 @@ The group uses the same OS/architecture matrix and per-OS impact sets as `pr-tes so cfg-gated targets and dependencies are exercised under the MSRV. It runs in parallel with the other PR groups. When no root MSRV exists, the recipe exits successfully without running tests. -`ANVIL_MSRV_TOOLCHAIN` may map the public MSRV to an already-provisioned internal -toolchain; when it is unset, setup installs the declared MSRV through rustup. +`ANVIL_MSRV_TOOLCHAIN` may map the public MSRV to an already-provisioned +internal toolchain. Internal environments pair it with `RUSTUP_TOOLCHAIN` (or a +selecting repository toolchain file) for stable checks; a mapping by itself is +rejected rather than allowing stable checks to fall through to a public +channel. When the mapping is unset, setup installs the declared MSRV through +rustup. #### `pr-runtime-analysis` (stricter-runtime correctness) diff --git a/crates/cargo-anvil/docs/design/containers.md b/crates/cargo-anvil/docs/design/containers.md index 4f3987c32..bd4cde058 100644 --- a/crates/cargo-anvil/docs/design/containers.md +++ b/crates/cargo-anvil/docs/design/containers.md @@ -179,9 +179,9 @@ the standard resolver and selected-stable execution recipes; the resolved value is not exported across unrelated recipe processes. The restricted image-construction context does not contain workspace member -manifests, so uniform per-package MSRV validation runs in native and cloud setup -rather than during image construction. Container checks still use the selected -root MSRV; a package that requires a newer compiler fails normally. +manifests, so per-package MSRV compatibility validation runs in native and +cloud setup rather than during image construction. Container checks still use +the selected root MSRV; a package that requires a newer compiler fails normally. The public default is digest-pinned Debian Bookworm. A user or automation can select another image compatible with the generated Debian-based diff --git a/crates/cargo-anvil/docs/design/local.md b/crates/cargo-anvil/docs/design/local.md index 7b6ca823e..b62c6e419 100644 --- a/crates/cargo-anvil/docs/design/local.md +++ b/crates/cargo-anvil/docs/design/local.md @@ -394,12 +394,15 @@ compiler that can change with the machine image. This is a breaking migration for adopters that previously relied on the runner's ambient stable compiler. Before regenerating, a repository with one compatibility floor should declare root `[workspace.package].rust-version` (or package -`rust-version`) and have every workspace member inherit or declare that same value. -A repository with missing or heterogeneous package MSRVs must instead add a root -toolchain file with `channel` or `path` to select the single compiler used by catalog -checks. Internal builds may set `RUSTUP_TOOLCHAIN` to a provisioned toolchain rather -than adding a public compiler pin. An empty root toolchain file is invalid and produces -an actionable error. +`rust-version`) and have every workspace member inherit it or declare an equal +or lower minimum. A repository with missing package MSRVs, or a member minimum +newer than the root, must instead correct the root floor or add a root toolchain +file with `channel` or `path` to select the single compiler used by catalog +checks. Internal builds may set `RUSTUP_TOOLCHAIN` to a provisioned toolchain +rather than adding a public compiler pin. Setting `ANVIL_MSRV_TOOLCHAIN` +without that stable override (and without a selecting repository toolchain +file) is rejected as incomplete internal configuration. An empty root +toolchain file is invalid and produces an actionable error. The generated `.anvil/resolve-stable-toolchain.ps1` implements the parsing and installation logic behind the private `_anvil-resolve-stable` recipe. @@ -430,10 +433,12 @@ The single script implementation keeps PowerShell parsing and installation behavior identical across those recipe callers. When selection falls back to the root MSRV, prerequisite validation reads -`cargo metadata` and requires every workspace package to resolve to that same -`rust_version`. Workspaces with missing or heterogeneous package MSRVs must choose a -single catalog toolchain explicitly with a selecting toolchain file. Anvil does not -build a per-package toolchain matrix for this uncommon case. +`cargo metadata` and requires every workspace package to resolve a +`rust_version` no newer than the root floor. Lower member minima are valid +because the root compiler satisfies them. Workspaces with missing package +MSRVs or member minima above the root must correct the declarations or choose a +single catalog toolchain explicitly with a selecting toolchain file. Anvil does +not build a per-package toolchain matrix for this uncommon case. When the root manifest declares an MSRV, `anvil-msrv-test` runs affected-package unit and integration tests under that compiler with all features and with default @@ -441,8 +446,9 @@ features. A selecting toolchain file does not suppress this minimum-version run. `ANVIL_MSRV_TOOLCHAIN` optionally maps the declared MSRV to a provisioned internal toolchain. Without a root MSRV the recipe is a no-op. -`anvil-tool-rustc-validate-prereqs` verifies the selected compiler and the uniform -MSRV rule. Per-check toolchain requirements (for example, miri, careful, and udeps +`anvil-tool-rustc-validate-prereqs` verifies the selected compiler and the +workspace MSRV compatibility rule. Per-check toolchain requirements (for +example, miri, careful, and udeps need nightly) remain enforced by the matching `anvil-toolchain--validate-prereqs` recipe. Explicit `cargo +toolchain` arguments retain rustup's higher precedence, so these checks continue to use the diff --git a/crates/cargo-anvil/src/anvil/artifacts/justfile.rs b/crates/cargo-anvil/src/anvil/artifacts/justfile.rs index eb0b393e7..319e91e26 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/justfile.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/justfile.rs @@ -991,7 +991,7 @@ mod tests { } #[test] - fn rejects_missing_and_heterogeneous_workspace_msrvs() { + fn rejects_missing_or_too_new_workspace_msrvs() { let missing = fixture("[workspace]\nresolver = \"2\"\n"); let output = run(missing.path(), &[], None); assert!(!output.status.success()); @@ -1001,10 +1001,10 @@ mod tests { "unexpected resolver diagnostic: {diagnostic}" ); - let heterogeneous = + let workspace = fixture("[workspace]\nresolver = \"2\"\nmembers = [\"a\", \"b\"]\n[workspace.package]\nrust-version = \"1.92\"\n"); - for (name, version) in [("a", "1.92"), ("b", "1.93")] { - let member = heterogeneous.path().join(name); + for (name, version) in [("a", "1.91"), ("b", "1.93")] { + let member = workspace.path().join(name); fs::create_dir(&member).expect("member directory must be creatable"); fs::write( member.join("Cargo.toml"), @@ -1016,16 +1016,150 @@ mod tests { fs::write(member.join("lib.rs"), "").expect("member source must be writable"); } - let output = run(heterogeneous.path(), &["-ValidateWorkspaceMsrv"], None); + let output = run(workspace.path(), &["-ValidateWorkspaceMsrv"], None); assert!(!output.status.success()); let diagnostic = normalized_diagnostic(&output); assert!( - diagnostic.contains("must resolve to the root") && diagnostic.contains("MSRV"), + diagnostic.contains("newer than the root MSRV") && diagnostic.contains("b (1.93)") && !diagnostic.contains("a (1.91)"), "unexpected resolver diagnostic: {diagnostic}" ); - let output = run(heterogeneous.path(), &["-ValidateWorkspaceMsrv"], Some("explicit-toolchain")); - assert!(output.status.success(), "explicit override must permit heterogeneous MSRVs"); + fs::write( + workspace.path().join("b/Cargo.toml"), + "[package]\nname = \"b\"\nversion = \"0.1.0\"\nedition = \"2021\"\nrust-version = \"1.92\"\n[lib]\npath = \"lib.rs\"\n", + ) + .expect("member manifest must be writable"); + let output = run(workspace.path(), &["-ValidateWorkspaceMsrv"], None); + assert!( + output.status.success(), + "member MSRVs at or below the root must be compatible: {}", + normalized_diagnostic(&output) + ); + + let output = run(workspace.path(), &["-ValidateWorkspaceMsrv"], Some("explicit-toolchain")); + assert!(output.status.success(), "explicit override must permit differing MSRVs"); + } + + #[test] + fn rejects_an_unpaired_internal_msrv_mapping_for_stable_selection() { + let temp = fixture("[workspace.package]\nrust-version = \"1.93\"\n"); + let run_with_mapping = |root: &Path| { + Command::new("pwsh") + .args(["-NoProfile", "-File", ".anvil/resolve-stable-toolchain.ps1"]) + .current_dir(root) + .env_remove("RUSTUP_TOOLCHAIN") + .env("ANVIL_MSRV_TOOLCHAIN", "ms-prod-1.93") + .output() + .expect("pwsh must be available to test the generated resolver") + }; + + let output = run_with_mapping(temp.path()); + assert!(!output.status.success(), "an unpaired internal MSRV mapping must fail"); + let diagnostic = normalized_diagnostic(&output); + assert!( + diagnostic.contains("ANVIL_MSRV_TOOLCHAIN") + && diagnostic.contains("without RUSTUP_TOOLCHAIN") + && diagnostic.contains("unset ANVIL_MSRV_TOOLCHAIN"), + "unexpected resolver diagnostic: {diagnostic}" + ); + + fs::write(temp.path().join("rust-toolchain.toml"), "[toolchain]\nchannel = \"1.94\"\n") + .expect("toolchain fixture must be writable"); + let output = run_with_mapping(temp.path()); + assert!( + output.status.success(), + "a repository-selected stable toolchain completes the configuration: {}", + normalized_diagnostic(&output) + ); + } + + #[test] + fn provisions_public_and_mapped_msrv_toolchains() { + let run_install = |root: &Path, mapped: Option<&str>, installed: &str, cargo_exit: i32| { + let shim = TempDir::new().expect("toolchain shim directory must be creatable"); + let rustup_log = shim.path().join("rustup.log"); + let cargo_log = shim.path().join("cargo.log"); + fs::write( + shim.path().join("rustup.ps1"), + format!( + "Add-Content -LiteralPath $env:ANVIL_RUSTUP_LOG -Value ($args -join ' ')\n\ + if (($args -contains 'toolchain') -and ($args -contains 'list')) {{ Write-Output '{installed}' }}\n\ + exit 0\n" + ), + ) + .expect("rustup shim must be writable"); + fs::write( + shim.path().join("cargo.ps1"), + format!( + "Add-Content -LiteralPath $env:ANVIL_CARGO_LOG -Value ($args -join ' ')\n\ + exit {cargo_exit}\n" + ), + ) + .expect("cargo shim must be writable"); + let mut paths = vec![shim.path().to_path_buf()]; + paths.extend(env::split_paths(&env::var_os("PATH").unwrap_or_default())); + let mut command = Command::new("pwsh"); + command + .args(["-NoProfile", "-File", ".anvil/resolve-stable-toolchain.ps1", "-InstallMsrvIfNeeded"]) + .current_dir(root) + .env_remove("RUSTUP_TOOLCHAIN") + .env_remove("ANVIL_MSRV_TOOLCHAIN") + .env("ANVIL_RUSTUP_LOG", &rustup_log) + .env("ANVIL_CARGO_LOG", &cargo_log) + .env("PATH", env::join_paths(paths).expect("shim PATH must be valid")); + if let Some(value) = mapped { + command.env("ANVIL_MSRV_TOOLCHAIN", value); + } + let output = command.output().expect("pwsh must be available to test MSRV provisioning"); + let rustup_calls = fs::read_to_string(rustup_log).unwrap_or_default(); + let cargo_calls = fs::read_to_string(cargo_log).unwrap_or_default(); + (output, rustup_calls, cargo_calls) + }; + + let temp = fixture("[workspace.package]\nrust-version = \"1.93\"\n"); + fs::write( + temp.path().join("rust-toolchain.toml"), + "[toolchain]\ncomponents = [\"clippy\"]\ntargets = [\"wasm32-unknown-unknown\"]\n", + ) + .expect("toolchain fixture must be writable"); + + let (output, rustup_calls, cargo_calls) = run_install(temp.path(), None, "1.93-x86_64-pc-windows-msvc", 0); + assert!( + output.status.success(), + "installed public MSRV provisioning failed: {}", + normalized_diagnostic(&output) + ); + assert!(rustup_calls.contains("toolchain list")); + assert!( + !rustup_calls.contains("toolchain install"), + "installed MSRV must not be reinstalled" + ); + assert!(rustup_calls.contains("component add --toolchain 1.93 clippy")); + assert!(rustup_calls.contains("target add --toolchain 1.93 wasm32-unknown-unknown")); + assert!(cargo_calls.is_empty(), "public MSRV availability is checked through rustup"); + + let (output, rustup_calls, cargo_calls) = run_install(temp.path(), Some("ms-prod-1.93"), "", 0); + assert!( + output.status.success(), + "mapped MSRV provisioning failed: {}", + normalized_diagnostic(&output) + ); + assert!(cargo_calls.contains("+ms-prod-1.93 --version")); + assert!(!rustup_calls.contains("toolchain list")); + assert!(!rustup_calls.contains("toolchain install")); + assert!(rustup_calls.contains("component add --toolchain ms-prod-1.93 clippy")); + assert!(rustup_calls.contains("target add --toolchain ms-prod-1.93 wasm32-unknown-unknown")); + + let (output, _, cargo_calls) = run_install(temp.path(), Some("ms-prod-1.93"), "", 9); + assert!(!output.status.success(), "an unavailable mapped MSRV must fail"); + assert!(cargo_calls.contains("+ms-prod-1.93 --version")); + let diagnostic = normalized_diagnostic(&output); + assert!( + diagnostic.contains("mapped MSRV toolchain") + && diagnostic.contains("is unavailable") + && diagnostic.contains("provision ANVIL_MSRV_TOOLCHAIN"), + "unexpected mapped MSRV diagnostic: {diagnostic}" + ); } #[test] diff --git a/crates/cargo-anvil/templates/anvil/resolve-stable-toolchain.ps1 b/crates/cargo-anvil/templates/anvil/resolve-stable-toolchain.ps1 index 7c8b1b36c..e38de2673 100644 --- a/crates/cargo-anvil/templates/anvil/resolve-stable-toolchain.ps1 +++ b/crates/cargo-anvil/templates/anvil/resolve-stable-toolchain.ps1 @@ -154,7 +154,7 @@ function Get-RootMsrv([switch] $AllowMissing) { throw 'anvil: no selecting rust-toolchain(.toml) and no root [workspace.package] or [package] rust-version; declare an MSRV or select a toolchain explicitly' } -function Assert-UniformWorkspaceMsrv { +function Assert-WorkspaceMsrvCompatibility { $manifestPath = Join-Path $repoRoot 'Cargo.toml' $metadataText = (& cargo metadata --manifest-path $manifestPath --format-version 1 --no-deps 2>&1 | Out-String) if ($LASTEXITCODE -ne 0) { @@ -171,10 +171,22 @@ function Assert-UniformWorkspaceMsrv { throw "anvil: workspace packages without a resolved rust-version: $($missing -join ', '); declare one or select a toolchain explicitly" } - $versions = @($members | ForEach-Object { [string] $_.rust_version } | Sort-Object -Unique) $rootMsrv = Get-RootMsrv - if ($versions.Count -ne 1 -or $versions[0] -ne $rootMsrv) { - throw "anvil: workspace packages must resolve to the root MSRV $rootMsrv; found $($versions -join ', '); select one catalog toolchain explicitly with rust-toolchain.toml" + $newer = @( + $members | + Where-Object { [version] ([string] $_.rust_version) -gt [version] $rootMsrv } | + ForEach-Object { "$($_.name) ($($_.rust_version))" } + ) + if ($newer.Count -gt 0) { + throw "anvil: workspace packages require a compiler newer than the root MSRV $rootMsrv`: $($newer -join ', '); raise the root MSRV or select one catalog toolchain explicitly with rust-toolchain.toml" + } +} + +function Assert-CompleteInternalToolchainConfiguration { + if (-not [string]::IsNullOrWhiteSpace($env:ANVIL_MSRV_TOOLCHAIN) -and + [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN) -and + $null -eq $fileSelection) { + throw 'anvil: ANVIL_MSRV_TOOLCHAIN is set without RUSTUP_TOOLCHAIN and no repository toolchain file selects stable checks; set RUSTUP_TOOLCHAIN to the provisioned stable toolchain or unset ANVIL_MSRV_TOOLCHAIN' } } @@ -244,9 +256,10 @@ if ($EmitToolchainFileOptions) { exit 0 } if ($ValidateWorkspaceMsrv) { + Assert-CompleteInternalToolchainConfiguration $skipWorkspaceMsrvValidation = -not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN) if (-not $skipWorkspaceMsrvValidation -and $null -eq $fileSelection) { - Assert-UniformWorkspaceMsrv + Assert-WorkspaceMsrvCompatibility } exit 0 } @@ -270,6 +283,7 @@ if ($MsrvToolchain -or $InstallMsrvIfNeeded) { exit 0 } +Assert-CompleteInternalToolchainConfiguration $selection = if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { [pscustomobject]@{ Value = $env:RUSTUP_TOOLCHAIN; Source = 'environment'; Installable = $false } } elseif ($null -ne $fileSelection) { diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap index fedacb966..c45d4041e 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap @@ -1350,7 +1350,7 @@ function Get-RootMsrv([switch] $AllowMissing) { throw 'anvil: no selecting rust-toolchain(.toml) and no root [workspace.package] or [package] rust-version; declare an MSRV or select a toolchain explicitly' } -function Assert-UniformWorkspaceMsrv { +function Assert-WorkspaceMsrvCompatibility { $manifestPath = Join-Path $repoRoot 'Cargo.toml' $metadataText = (& cargo metadata --manifest-path $manifestPath --format-version 1 --no-deps 2>&1 | Out-String) if ($LASTEXITCODE -ne 0) { @@ -1367,10 +1367,22 @@ function Assert-UniformWorkspaceMsrv { throw "anvil: workspace packages without a resolved rust-version: $($missing -join ', '); declare one or select a toolchain explicitly" } - $versions = @($members | ForEach-Object { [string] $_.rust_version } | Sort-Object -Unique) $rootMsrv = Get-RootMsrv - if ($versions.Count -ne 1 -or $versions[0] -ne $rootMsrv) { - throw "anvil: workspace packages must resolve to the root MSRV $rootMsrv; found $($versions -join ', '); select one catalog toolchain explicitly with rust-toolchain.toml" + $newer = @( + $members | + Where-Object { [version] ([string] $_.rust_version) -gt [version] $rootMsrv } | + ForEach-Object { "$($_.name) ($($_.rust_version))" } + ) + if ($newer.Count -gt 0) { + throw "anvil: workspace packages require a compiler newer than the root MSRV $rootMsrv`: $($newer -join ', '); raise the root MSRV or select one catalog toolchain explicitly with rust-toolchain.toml" + } +} + +function Assert-CompleteInternalToolchainConfiguration { + if (-not [string]::IsNullOrWhiteSpace($env:ANVIL_MSRV_TOOLCHAIN) -and + [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN) -and + $null -eq $fileSelection) { + throw 'anvil: ANVIL_MSRV_TOOLCHAIN is set without RUSTUP_TOOLCHAIN and no repository toolchain file selects stable checks; set RUSTUP_TOOLCHAIN to the provisioned stable toolchain or unset ANVIL_MSRV_TOOLCHAIN' } } @@ -1440,9 +1452,10 @@ if ($EmitToolchainFileOptions) { exit 0 } if ($ValidateWorkspaceMsrv) { + Assert-CompleteInternalToolchainConfiguration $skipWorkspaceMsrvValidation = -not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN) if (-not $skipWorkspaceMsrvValidation -and $null -eq $fileSelection) { - Assert-UniformWorkspaceMsrv + Assert-WorkspaceMsrvCompatibility } exit 0 } @@ -1466,6 +1479,7 @@ if ($MsrvToolchain -or $InstallMsrvIfNeeded) { exit 0 } +Assert-CompleteInternalToolchainConfiguration $selection = if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { [pscustomobject]@{ Value = $env:RUSTUP_TOOLCHAIN; Source = 'environment'; Installable = $false } } elseif ($null -ne $fileSelection) { diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap index bb2ef7d31..b3fb6f954 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap @@ -1350,7 +1350,7 @@ function Get-RootMsrv([switch] $AllowMissing) { throw 'anvil: no selecting rust-toolchain(.toml) and no root [workspace.package] or [package] rust-version; declare an MSRV or select a toolchain explicitly' } -function Assert-UniformWorkspaceMsrv { +function Assert-WorkspaceMsrvCompatibility { $manifestPath = Join-Path $repoRoot 'Cargo.toml' $metadataText = (& cargo metadata --manifest-path $manifestPath --format-version 1 --no-deps 2>&1 | Out-String) if ($LASTEXITCODE -ne 0) { @@ -1367,10 +1367,22 @@ function Assert-UniformWorkspaceMsrv { throw "anvil: workspace packages without a resolved rust-version: $($missing -join ', '); declare one or select a toolchain explicitly" } - $versions = @($members | ForEach-Object { [string] $_.rust_version } | Sort-Object -Unique) $rootMsrv = Get-RootMsrv - if ($versions.Count -ne 1 -or $versions[0] -ne $rootMsrv) { - throw "anvil: workspace packages must resolve to the root MSRV $rootMsrv; found $($versions -join ', '); select one catalog toolchain explicitly with rust-toolchain.toml" + $newer = @( + $members | + Where-Object { [version] ([string] $_.rust_version) -gt [version] $rootMsrv } | + ForEach-Object { "$($_.name) ($($_.rust_version))" } + ) + if ($newer.Count -gt 0) { + throw "anvil: workspace packages require a compiler newer than the root MSRV $rootMsrv`: $($newer -join ', '); raise the root MSRV or select one catalog toolchain explicitly with rust-toolchain.toml" + } +} + +function Assert-CompleteInternalToolchainConfiguration { + if (-not [string]::IsNullOrWhiteSpace($env:ANVIL_MSRV_TOOLCHAIN) -and + [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN) -and + $null -eq $fileSelection) { + throw 'anvil: ANVIL_MSRV_TOOLCHAIN is set without RUSTUP_TOOLCHAIN and no repository toolchain file selects stable checks; set RUSTUP_TOOLCHAIN to the provisioned stable toolchain or unset ANVIL_MSRV_TOOLCHAIN' } } @@ -1440,9 +1452,10 @@ if ($EmitToolchainFileOptions) { exit 0 } if ($ValidateWorkspaceMsrv) { + Assert-CompleteInternalToolchainConfiguration $skipWorkspaceMsrvValidation = -not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN) if (-not $skipWorkspaceMsrvValidation -and $null -eq $fileSelection) { - Assert-UniformWorkspaceMsrv + Assert-WorkspaceMsrvCompatibility } exit 0 } @@ -1466,6 +1479,7 @@ if ($MsrvToolchain -or $InstallMsrvIfNeeded) { exit 0 } +Assert-CompleteInternalToolchainConfiguration $selection = if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { [pscustomobject]@{ Value = $env:RUSTUP_TOOLCHAIN; Source = 'environment'; Installable = $false } } elseif ($null -ne $fileSelection) { diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap index 32a30aa62..3b0e90e17 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap @@ -1350,7 +1350,7 @@ function Get-RootMsrv([switch] $AllowMissing) { throw 'anvil: no selecting rust-toolchain(.toml) and no root [workspace.package] or [package] rust-version; declare an MSRV or select a toolchain explicitly' } -function Assert-UniformWorkspaceMsrv { +function Assert-WorkspaceMsrvCompatibility { $manifestPath = Join-Path $repoRoot 'Cargo.toml' $metadataText = (& cargo metadata --manifest-path $manifestPath --format-version 1 --no-deps 2>&1 | Out-String) if ($LASTEXITCODE -ne 0) { @@ -1367,10 +1367,22 @@ function Assert-UniformWorkspaceMsrv { throw "anvil: workspace packages without a resolved rust-version: $($missing -join ', '); declare one or select a toolchain explicitly" } - $versions = @($members | ForEach-Object { [string] $_.rust_version } | Sort-Object -Unique) $rootMsrv = Get-RootMsrv - if ($versions.Count -ne 1 -or $versions[0] -ne $rootMsrv) { - throw "anvil: workspace packages must resolve to the root MSRV $rootMsrv; found $($versions -join ', '); select one catalog toolchain explicitly with rust-toolchain.toml" + $newer = @( + $members | + Where-Object { [version] ([string] $_.rust_version) -gt [version] $rootMsrv } | + ForEach-Object { "$($_.name) ($($_.rust_version))" } + ) + if ($newer.Count -gt 0) { + throw "anvil: workspace packages require a compiler newer than the root MSRV $rootMsrv`: $($newer -join ', '); raise the root MSRV or select one catalog toolchain explicitly with rust-toolchain.toml" + } +} + +function Assert-CompleteInternalToolchainConfiguration { + if (-not [string]::IsNullOrWhiteSpace($env:ANVIL_MSRV_TOOLCHAIN) -and + [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN) -and + $null -eq $fileSelection) { + throw 'anvil: ANVIL_MSRV_TOOLCHAIN is set without RUSTUP_TOOLCHAIN and no repository toolchain file selects stable checks; set RUSTUP_TOOLCHAIN to the provisioned stable toolchain or unset ANVIL_MSRV_TOOLCHAIN' } } @@ -1440,9 +1452,10 @@ if ($EmitToolchainFileOptions) { exit 0 } if ($ValidateWorkspaceMsrv) { + Assert-CompleteInternalToolchainConfiguration $skipWorkspaceMsrvValidation = -not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN) if (-not $skipWorkspaceMsrvValidation -and $null -eq $fileSelection) { - Assert-UniformWorkspaceMsrv + Assert-WorkspaceMsrvCompatibility } exit 0 } @@ -1466,6 +1479,7 @@ if ($MsrvToolchain -or $InstallMsrvIfNeeded) { exit 0 } +Assert-CompleteInternalToolchainConfiguration $selection = if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { [pscustomobject]@{ Value = $env:RUSTUP_TOOLCHAIN; Source = 'environment'; Installable = $false } } elseif ($null -ne $fileSelection) { From 7e204b8d4f08502a60caa3cc495aebcd295ac898 Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Fri, 28 Aug 2026 13:33:02 +0200 Subject: [PATCH 13/32] Bootstrap Just before toolchain setup Install the Just bootstrap before invoking private resolver recipes in GitHub Actions, preserving recipe-based selection without assuming Just is preinstalled on hosted runners. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .anvil.lock | 4 +-- .github/actions/anvil-setup/action.yml | 33 ++++++++----------- crates/cargo-anvil/docs/design/github.md | 12 ++++--- .../templates/github/setup-action.yml | 33 ++++++++----------- .../snapshots/snapshots__github_backend.snap | 33 ++++++++----------- 5 files changed, 51 insertions(+), 64 deletions(-) diff --git a/.anvil.lock b/.anvil.lock index b83469fe3..76b935251 100644 --- a/.anvil.lock +++ b/.anvil.lock @@ -1,7 +1,7 @@ version = 1 tool = "anvil" tool_version = "0.5.0" -catalog_checksum = "sha256:2316debcdff5f1d470a6d10141decd0640af3860f3aa6dc9247a7036faa8f029" +catalog_checksum = "sha256:dcc0f0a3090240c891693c71bf9474ff468816c822db3e809547192cc8b74f6a" [[file]] path = ".anvil/container/Containerfile" @@ -53,7 +53,7 @@ checksum = "sha256:ff8def6c0786b6e146c4b633dfe38cb9b8ede398345516cca32bbcd558608 [[file]] path = ".github/actions/anvil-setup/action.yml" -checksum = "sha256:9d0f8a0c5599c2bf35abbc90deb73d7a0ed34c336e6f9effce8a914e3e21c3f4" +checksum = "sha256:445cf5f313a504b2406029fec0d2e07821b2245706e97e1f7e6ff8761f5468bd" [[file]] path = ".github/actions/anvil-setup/just-problem-matcher.json" diff --git a/.github/actions/anvil-setup/action.yml b/.github/actions/anvil-setup/action.yml index 051fbc3f9..20c55562f 100644 --- a/.github/actions/anvil-setup/action.yml +++ b/.github/actions/anvil-setup/action.yml @@ -77,6 +77,18 @@ runs: } "Free after cleanup (GiB): $(Get-FreeDiskGiB)" + # The resolver is a standard Just recipe, so bootstrap Just before any + # selected-toolchain setup. cargo-binstall keeps this cold path fast. + - name: Install cargo-binstall + uses: cargo-bins/cargo-binstall@v1.21.0 # immutable release, the tag cannot be moved + + - name: Install just + shell: bash + run: | + if ! command -v just >/dev/null 2>&1 ; then + cargo binstall --no-confirm --locked just || cargo install --locked just + fi + - name: Resolve stable toolchain shell: pwsh -NoProfile -Command ". '{0}'" run: | @@ -141,27 +153,10 @@ runs: ~/.cargo/.crates2.json # The catalog recipe `anvil-setup` (or `anvil--setup` - # when a group is specified via the `group` input) needs `just` to - # run. We bootstrap just here (chicken-and-egg), then hand off - # everything else to it. The setup recipes are idempotent and + # when a group is specified via the `group` input) uses the Just + # bootstrapped above, then handles everything else. The setup recipes are idempotent and # short-circuit on tools already installed at or above the pinned # version, so re-running on cache-hit runs is cheap. - # Install a prebuilt cargo-binstall binary in seconds. Without this, - # the first `_install-tool` recipe that needs binstall bootstraps it - # via `cargo install --locked cargo-binstall`, which takes ~4 min of - # source compilation on every cold-cache job. The official action - # downloads the release binary from cargo-bins/cargo-binstall, so - # the bootstrap branch in `tools.just` becomes a no-op on GH. - - name: Install cargo-binstall - uses: cargo-bins/cargo-binstall@v1.21.0 # immutable release, the tag cannot be moved - - - name: Install just - shell: bash - run: | - if ! command -v just >/dev/null 2>&1 ; then - cargo binstall --no-confirm --locked just || cargo install --locked just - fi - - name: Install anvil toolchains + tools shell: bash # Carry action data through the environment instead of interpolating it diff --git a/crates/cargo-anvil/docs/design/github.md b/crates/cargo-anvil/docs/design/github.md index 37477ed0f..0659de36a 100644 --- a/crates/cargo-anvil/docs/design/github.md +++ b/crates/cargo-anvil/docs/design/github.md @@ -840,11 +840,13 @@ The check → bucket mapping is in ## 7. Rust toolchain -The setup action resolves the catalog's stable toolchain before reading `rustc` or -restoring build artifacts. It installs a missing public channel through rustup and -captures the selected compiler version through `_anvil-with-stable`, but does not -publish a resolved `RUSTUP_TOOLCHAIN` through `GITHUB_ENV`. Each stable recipe applies -the selection only to its own Cargo or Rust child process; selecting toolchain files +The setup action bootstraps cargo-binstall and Just using the runner environment, +then resolves the catalog's stable toolchain through the standard private Just +recipe before reading `rustc` or restoring build artifacts. It installs a missing +public channel through rustup and captures the selected compiler version through +`_anvil-with-stable`, but does not publish a resolved `RUSTUP_TOOLCHAIN` through +`GITHUB_ENV`. Each stable recipe applies the selection only to its own Cargo or +Rust child process; selecting toolchain files remain unoverridden so rustup processes them natively. All matrix legs therefore use the same repository selection instead of the different stable versions that runner images may carry, without changing unrelated recipe environments. diff --git a/crates/cargo-anvil/templates/github/setup-action.yml b/crates/cargo-anvil/templates/github/setup-action.yml index 051fbc3f9..20c55562f 100644 --- a/crates/cargo-anvil/templates/github/setup-action.yml +++ b/crates/cargo-anvil/templates/github/setup-action.yml @@ -77,6 +77,18 @@ runs: } "Free after cleanup (GiB): $(Get-FreeDiskGiB)" + # The resolver is a standard Just recipe, so bootstrap Just before any + # selected-toolchain setup. cargo-binstall keeps this cold path fast. + - name: Install cargo-binstall + uses: cargo-bins/cargo-binstall@v1.21.0 # immutable release, the tag cannot be moved + + - name: Install just + shell: bash + run: | + if ! command -v just >/dev/null 2>&1 ; then + cargo binstall --no-confirm --locked just || cargo install --locked just + fi + - name: Resolve stable toolchain shell: pwsh -NoProfile -Command ". '{0}'" run: | @@ -141,27 +153,10 @@ runs: ~/.cargo/.crates2.json # The catalog recipe `anvil-setup` (or `anvil--setup` - # when a group is specified via the `group` input) needs `just` to - # run. We bootstrap just here (chicken-and-egg), then hand off - # everything else to it. The setup recipes are idempotent and + # when a group is specified via the `group` input) uses the Just + # bootstrapped above, then handles everything else. The setup recipes are idempotent and # short-circuit on tools already installed at or above the pinned # version, so re-running on cache-hit runs is cheap. - # Install a prebuilt cargo-binstall binary in seconds. Without this, - # the first `_install-tool` recipe that needs binstall bootstraps it - # via `cargo install --locked cargo-binstall`, which takes ~4 min of - # source compilation on every cold-cache job. The official action - # downloads the release binary from cargo-bins/cargo-binstall, so - # the bootstrap branch in `tools.just` becomes a no-op on GH. - - name: Install cargo-binstall - uses: cargo-bins/cargo-binstall@v1.21.0 # immutable release, the tag cannot be moved - - - name: Install just - shell: bash - run: | - if ! command -v just >/dev/null 2>&1 ; then - cargo binstall --no-confirm --locked just || cargo install --locked just - fi - - name: Install anvil toolchains + tools shell: bash # Carry action data through the environment instead of interpolating it diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap index b3fb6f954..11048e117 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap @@ -1919,6 +1919,18 @@ runs: } "Free after cleanup (GiB): $(Get-FreeDiskGiB)" + # The resolver is a standard Just recipe, so bootstrap Just before any + # selected-toolchain setup. cargo-binstall keeps this cold path fast. + - name: Install cargo-binstall + uses: cargo-bins/cargo-binstall@v1.21.0 # immutable release, the tag cannot be moved + + - name: Install just + shell: bash + run: | + if ! command -v just >/dev/null 2>&1 ; then + cargo binstall --no-confirm --locked just || cargo install --locked just + fi + - name: Resolve stable toolchain shell: pwsh -NoProfile -Command ". '{0}'" run: | @@ -1983,27 +1995,10 @@ runs: ~/.cargo/.crates2.json # The catalog recipe `anvil-setup` (or `anvil--setup` - # when a group is specified via the `group` input) needs `just` to - # run. We bootstrap just here (chicken-and-egg), then hand off - # everything else to it. The setup recipes are idempotent and + # when a group is specified via the `group` input) uses the Just + # bootstrapped above, then handles everything else. The setup recipes are idempotent and # short-circuit on tools already installed at or above the pinned # version, so re-running on cache-hit runs is cheap. - # Install a prebuilt cargo-binstall binary in seconds. Without this, - # the first `_install-tool` recipe that needs binstall bootstraps it - # via `cargo install --locked cargo-binstall`, which takes ~4 min of - # source compilation on every cold-cache job. The official action - # downloads the release binary from cargo-bins/cargo-binstall, so - # the bootstrap branch in `tools.just` becomes a no-op on GH. - - name: Install cargo-binstall - uses: cargo-bins/cargo-binstall@v1.21.0 # immutable release, the tag cannot be moved - - - name: Install just - shell: bash - run: | - if ! command -v just >/dev/null 2>&1 ; then - cargo binstall --no-confirm --locked just || cargo install --locked just - fi - - name: Install anvil toolchains + tools shell: bash # Carry action data through the environment instead of interpolating it From 973e08e5fcb4ad493b7bc93b9f6b3aac5758b04c Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Fri, 28 Aug 2026 13:40:07 +0200 Subject: [PATCH 14/32] Make MSRV diagnostic test portable Accept PowerShell's platform-specific diagnostic wrapping while still asserting every actionable part of the mapped-toolchain error. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/cargo-anvil/src/anvil/artifacts/justfile.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/cargo-anvil/src/anvil/artifacts/justfile.rs b/crates/cargo-anvil/src/anvil/artifacts/justfile.rs index 319e91e26..a445082c2 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/justfile.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/justfile.rs @@ -1157,7 +1157,8 @@ mod tests { assert!( diagnostic.contains("mapped MSRV toolchain") && diagnostic.contains("is unavailable") - && diagnostic.contains("provision ANVIL_MSRV_TOOLCHAIN"), + && diagnostic.contains("provision") + && diagnostic.contains("ANVIL_MSRV_TOOLCHAIN"), "unexpected mapped MSRV diagnostic: {diagnostic}" ); } From aa6a450d873a239e8bb48d0410bb47ba79ac3b51 Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Fri, 28 Aug 2026 14:00:16 +0200 Subject: [PATCH 15/32] fix(cargo-anvil): preserve scoped command working directory Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: f7996e6f-5c20-4d7a-b2f7-690af95e557e --- .anvil.lock | 4 +- .../templates/justfiles/anvil/tools.just | 1 + crates/cargo-anvil/tests/recipe_contracts.rs | 48 ++++++++++++++++++- justfiles/anvil/tools.just | 1 + 4 files changed, 51 insertions(+), 3 deletions(-) diff --git a/.anvil.lock b/.anvil.lock index 76b935251..c678bcfe0 100644 --- a/.anvil.lock +++ b/.anvil.lock @@ -1,7 +1,7 @@ version = 1 tool = "anvil" tool_version = "0.5.0" -catalog_checksum = "sha256:dcc0f0a3090240c891693c71bf9474ff468816c822db3e809547192cc8b74f6a" +catalog_checksum = "sha256:6b5852645a9c1ba7433f876456d0f9b3abd56fcd026b638a2e044c6cf5c28e67" [[file]] path = ".anvil/container/Containerfile" @@ -265,7 +265,7 @@ checksum = "sha256:5b2d91569f3fe87cd5f516623a01b5c7ae855ad587870ef9a2ae6d5619dd9 [[file]] path = "justfiles/anvil/tools.just" -checksum = "sha256:68b1574ba4a0de729913eab3862e51a76b2e4459650ddcd7ea1df05fe9b28ef2" +checksum = "sha256:6cfad2ed709ab46ce29666435d7062e638e3deb9e6a000c2a3c877172c8739e9" [[file]] path = "justfiles/anvil/versions.just" diff --git a/crates/cargo-anvil/templates/justfiles/anvil/tools.just b/crates/cargo-anvil/templates/justfiles/anvil/tools.just index 54ef872d2..083a77be3 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/tools.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/tools.just @@ -205,6 +205,7 @@ _anvil-with-stable command *args: } else { $env:RUSTUP_TOOLCHAIN = $toolchain } + Set-Location '{{invocation_directory()}}' & '{{command}}' {{args}} if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } diff --git a/crates/cargo-anvil/tests/recipe_contracts.rs b/crates/cargo-anvil/tests/recipe_contracts.rs index 5b7ea9c10..5d8f288f5 100644 --- a/crates/cargo-anvil/tests/recipe_contracts.rs +++ b/crates/cargo-anvil/tests/recipe_contracts.rs @@ -31,6 +31,9 @@ $joined = $args -join ' ' if ($env:FAKE_CARGO_LOG) { Add-Content -LiteralPath $env:FAKE_CARGO_LOG -Value $joined } +if ($env:FAKE_CARGO_CWD_LOG) { + Add-Content -LiteralPath $env:FAKE_CARGO_CWD_LOG -Value (Get-Location).Path +} if ($args -contains 'metadata') { if ($env:FAKE_METADATA_EXIT) { exit [int]$env:FAKE_METADATA_EXIT } if ($env:FAKE_METADATA_INVALID) { @@ -182,8 +185,16 @@ fn path_with_fake_bin(root: &Path) -> OsString { } fn run_just(root: &Path, arguments: &[&str], environment: &[(&str, &OsStr)]) -> Output { + run_just_from(root, root, arguments, environment) +} + +fn run_just_from(root: &Path, current_dir: &Path, arguments: &[&str], environment: &[(&str, &OsStr)]) -> Output { let mut command = Command::new("just"); - command.args(["--justfile", "Justfile"]).args(arguments).current_dir(root); + command + .arg("--justfile") + .arg(root.join("Justfile")) + .args(arguments) + .current_dir(current_dir); command.env("PATH", path_with_fake_bin(root)); command.env("FAKE_WORKSPACE_ROOT", root); for &(key, value) in environment { @@ -201,6 +212,41 @@ fn assert_failed(output: &Output, context: &str) { ); } +#[test] +fn stable_toolchain_helper_preserves_invocation_directory_and_arguments() { + if !tools_available() { + return; + } + let tmp = fixture(&[("versions.just", VERSIONS), ("tools.just", TOOLS)], &[]); + let crate_dir = tmp.path().join("crates/fixture"); + fs::create_dir_all(&crate_dir).unwrap(); + let args_log = tmp.path().join("cargo-args.log"); + let cwd_log = tmp.path().join("cargo-cwd.log"); + + let output = run_just_from( + tmp.path(), + &crate_dir, + &["_anvil-with-stable", "cargo", "doc2readme", "--check"], + &[ + ("FAKE_CARGO_LOG", args_log.as_os_str()), + ("FAKE_CARGO_CWD_LOG", cwd_log.as_os_str()), + ], + ); + + assert!( + output.status.success(), + "stable helper should preserve command context:\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + assert_eq!(fs::read_to_string(args_log).unwrap().trim(), "doc2readme --check"); + assert_eq!( + Path::new(fs::read_to_string(cwd_log).unwrap().trim()), + crate_dir, + "scoped command must run from the nested Just invocation directory" + ); +} + #[test] fn impact_format_resolves_directory_aliases_and_fails_hard() { if !tools_available() { diff --git a/justfiles/anvil/tools.just b/justfiles/anvil/tools.just index 54ef872d2..083a77be3 100644 --- a/justfiles/anvil/tools.just +++ b/justfiles/anvil/tools.just @@ -205,6 +205,7 @@ _anvil-with-stable command *args: } else { $env:RUSTUP_TOOLCHAIN = $toolchain } + Set-Location '{{invocation_directory()}}' & '{{command}}' {{args}} if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } From ad58520db208bd9669ecc0233325b75c7372594c Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Fri, 28 Aug 2026 14:08:22 +0200 Subject: [PATCH 16/32] fix(cargo-anvil): use native scoped command paths Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: f7996e6f-5c20-4d7a-b2f7-690af95e557e --- .anvil.lock | 4 ++-- crates/cargo-anvil/templates/justfiles/anvil/tools.just | 2 +- .../cargo-anvil/tests/snapshots/snapshots__ado_backend.snap | 1 + .../tests/snapshots/snapshots__github_backend.snap | 1 + crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap | 1 + justfiles/anvil/tools.just | 2 +- 6 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.anvil.lock b/.anvil.lock index c678bcfe0..c843b1df4 100644 --- a/.anvil.lock +++ b/.anvil.lock @@ -1,7 +1,7 @@ version = 1 tool = "anvil" tool_version = "0.5.0" -catalog_checksum = "sha256:6b5852645a9c1ba7433f876456d0f9b3abd56fcd026b638a2e044c6cf5c28e67" +catalog_checksum = "sha256:9c496240081a1a0c65a8dcd4f465766c5267fcf2ca22d63c86fc7a8bd271afa6" [[file]] path = ".anvil/container/Containerfile" @@ -265,7 +265,7 @@ checksum = "sha256:5b2d91569f3fe87cd5f516623a01b5c7ae855ad587870ef9a2ae6d5619dd9 [[file]] path = "justfiles/anvil/tools.just" -checksum = "sha256:6cfad2ed709ab46ce29666435d7062e638e3deb9e6a000c2a3c877172c8739e9" +checksum = "sha256:4fe2cf3e308530208e5326449326a0865a949aa10791180d544848612543eb09" [[file]] path = "justfiles/anvil/versions.just" diff --git a/crates/cargo-anvil/templates/justfiles/anvil/tools.just b/crates/cargo-anvil/templates/justfiles/anvil/tools.just index 083a77be3..218dc28fd 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/tools.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/tools.just @@ -205,7 +205,7 @@ _anvil-with-stable command *args: } else { $env:RUSTUP_TOOLCHAIN = $toolchain } - Set-Location '{{invocation_directory()}}' + Set-Location '{{invocation_directory_native()}}' & '{{command}}' {{args}} if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap index c45d4041e..1f011e5fb 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap @@ -6713,6 +6713,7 @@ _anvil-with-stable command *args: } else { $env:RUSTUP_TOOLCHAIN = $toolchain } + Set-Location '{{invocation_directory_native()}}' & '{{command}}' {{args}} if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap index 11048e117..a3d134bfd 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap @@ -6513,6 +6513,7 @@ _anvil-with-stable command *args: } else { $env:RUSTUP_TOOLCHAIN = $toolchain } + Set-Location '{{invocation_directory_native()}}' & '{{command}}' {{args}} if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap index 3b0e90e17..e98115c21 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap @@ -5363,6 +5363,7 @@ _anvil-with-stable command *args: } else { $env:RUSTUP_TOOLCHAIN = $toolchain } + Set-Location '{{invocation_directory_native()}}' & '{{command}}' {{args}} if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } diff --git a/justfiles/anvil/tools.just b/justfiles/anvil/tools.just index 083a77be3..218dc28fd 100644 --- a/justfiles/anvil/tools.just +++ b/justfiles/anvil/tools.just @@ -205,7 +205,7 @@ _anvil-with-stable command *args: } else { $env:RUSTUP_TOOLCHAIN = $toolchain } - Set-Location '{{invocation_directory()}}' + Set-Location '{{invocation_directory_native()}}' & '{{command}}' {{args}} if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } From b41113cef7540027f4d4847b3a62a6442df15bfa Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Fri, 28 Aug 2026 14:20:03 +0200 Subject: [PATCH 17/32] test(cargo-anvil): compare scoped paths canonically Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: f7996e6f-5c20-4d7a-b2f7-690af95e557e --- crates/cargo-anvil/tests/recipe_contracts.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/cargo-anvil/tests/recipe_contracts.rs b/crates/cargo-anvil/tests/recipe_contracts.rs index 5d8f288f5..3c9773495 100644 --- a/crates/cargo-anvil/tests/recipe_contracts.rs +++ b/crates/cargo-anvil/tests/recipe_contracts.rs @@ -241,8 +241,8 @@ fn stable_toolchain_helper_preserves_invocation_directory_and_arguments() { ); assert_eq!(fs::read_to_string(args_log).unwrap().trim(), "doc2readme --check"); assert_eq!( - Path::new(fs::read_to_string(cwd_log).unwrap().trim()), - crate_dir, + fs::canonicalize(fs::read_to_string(cwd_log).unwrap().trim()).unwrap(), + fs::canonicalize(crate_dir).unwrap(), "scoped command must run from the nested Just invocation directory" ); } From c1fd6f04ad8550a53f675a1d9d771ecb2e4da0a7 Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Fri, 28 Aug 2026 15:30:43 +0200 Subject: [PATCH 18/32] Remove nested stable toolchain wrappers Select stable toolchain arguments lazily in Just so Cargo and Rust commands run directly. Keep setup behavior recipe-owned and remove the generated resolver script. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: f7996e6f-5c20-4d7a-b2f7-690af95e557e --- .anvil.lock | 64 +- .anvil/container/Containerfile.dockerignore | 1 - .anvil/container/image-id.ps1 | 5 +- .anvil/container/image-id.sh | 2 +- .anvil/resolve-stable-toolchain.ps1 | 310 ------- .github/actions/anvil-setup/action.yml | 7 +- crates/cargo-anvil/docs/design/README.md | 4 - crates/cargo-anvil/docs/design/ado.md | 14 +- crates/cargo-anvil/docs/design/checks.md | 6 + crates/cargo-anvil/docs/design/containers.md | 8 +- crates/cargo-anvil/docs/design/github.md | 19 +- crates/cargo-anvil/docs/design/local.md | 71 +- .../src/anvil/artifacts/container.rs | 9 +- .../src/anvil/artifacts/justfile.rs | 172 ++-- crates/cargo-anvil/src/anvil/artifacts/mod.rs | 2 - .../cargo-anvil/templates/ado/steps/setup.yml | 7 +- .../container/Containerfile.dockerignore | 1 - .../templates/anvil/container/image-id.ps1 | 5 +- .../templates/anvil/container/image-id.sh | 2 +- .../anvil/resolve-stable-toolchain.ps1 | 310 ------- .../templates/github/setup-action.yml | 7 +- .../justfiles/anvil/checks/aprz.just | 2 +- .../justfiles/anvil/checks/audit.just | 2 +- .../justfiles/anvil/checks/bench.just | 2 +- .../justfiles/anvil/checks/bolero.just | 2 +- .../justfiles/anvil/checks/cargo-hack.just | 2 +- .../justfiles/anvil/checks/cargo-sort.just | 2 +- .../justfiles/anvil/checks/clippy.just | 2 +- .../justfiles/anvil/checks/deny.just | 2 +- .../justfiles/anvil/checks/doc-build.just | 2 +- .../justfiles/anvil/checks/doc-test.just | 4 +- .../anvil/checks/ensure-no-cyclic-deps.just | 2 +- .../checks/ensure-no-default-features.just | 2 +- .../justfiles/anvil/checks/examples.just | 2 +- .../anvil/checks/external-types.just | 2 +- .../anvil/checks/license-headers.just | 2 +- .../justfiles/anvil/checks/llvm-cov.just | 4 +- .../justfiles/anvil/checks/loom.just | 4 +- .../justfiles/anvil/checks/mutants-diff.just | 2 +- .../justfiles/anvil/checks/mutants-full.just | 2 +- .../justfiles/anvil/checks/readme-check.just | 4 +- .../justfiles/anvil/checks/semver-check.just | 4 +- .../justfiles/anvil/checks/spellcheck.just | 4 +- .../templates/justfiles/anvil/impact.just | 16 +- .../templates/justfiles/anvil/tools.just | 357 +++++++- .../templates/justfiles/anvil/versions.just | 101 ++- crates/cargo-anvil/tests/container_upgrade.rs | 31 +- crates/cargo-anvil/tests/impact.rs | 49 +- crates/cargo-anvil/tests/recipe_contracts.rs | 51 +- .../snapshots/snapshots__ado_backend.snap | 853 ++++++++++-------- .../snapshots/snapshots__github_backend.snap | 853 ++++++++++-------- .../snapshots/snapshots__local_only.snap | 846 +++++++++-------- justfiles/anvil/checks/aprz.just | 2 +- justfiles/anvil/checks/audit.just | 2 +- justfiles/anvil/checks/bench.just | 2 +- justfiles/anvil/checks/bolero.just | 2 +- justfiles/anvil/checks/cargo-hack.just | 2 +- justfiles/anvil/checks/cargo-sort.just | 2 +- justfiles/anvil/checks/clippy.just | 2 +- justfiles/anvil/checks/deny.just | 2 +- justfiles/anvil/checks/doc-build.just | 2 +- justfiles/anvil/checks/doc-test.just | 4 +- .../anvil/checks/ensure-no-cyclic-deps.just | 2 +- .../checks/ensure-no-default-features.just | 2 +- justfiles/anvil/checks/examples.just | 2 +- justfiles/anvil/checks/external-types.just | 2 +- justfiles/anvil/checks/license-headers.just | 2 +- justfiles/anvil/checks/llvm-cov.just | 4 +- justfiles/anvil/checks/loom.just | 4 +- justfiles/anvil/checks/mutants-diff.just | 2 +- justfiles/anvil/checks/mutants-full.just | 2 +- justfiles/anvil/checks/readme-check.just | 4 +- justfiles/anvil/checks/semver-check.just | 4 +- justfiles/anvil/checks/spellcheck.just | 4 +- justfiles/anvil/impact.just | 16 +- justfiles/anvil/tools.just | 357 +++++++- justfiles/anvil/versions.just | 101 ++- 77 files changed, 2585 insertions(+), 2184 deletions(-) delete mode 100644 .anvil/resolve-stable-toolchain.ps1 delete mode 100644 crates/cargo-anvil/templates/anvil/resolve-stable-toolchain.ps1 diff --git a/.anvil.lock b/.anvil.lock index c843b1df4..3704d4038 100644 --- a/.anvil.lock +++ b/.anvil.lock @@ -1,7 +1,7 @@ version = 1 tool = "anvil" tool_version = "0.5.0" -catalog_checksum = "sha256:9c496240081a1a0c65a8dcd4f465766c5267fcf2ca22d63c86fc7a8bd271afa6" +catalog_checksum = "sha256:1d75baef11d88ed6b10b92ace1d4c865a11680544fc295d4d3bd8ad1e5aa5149" [[file]] path = ".anvil/container/Containerfile" @@ -9,7 +9,7 @@ checksum = "sha256:ddd1cd7500a32b3a0bd93a5a60e96674fe4ba667d3ceb44f5207c447a0907 [[file]] path = ".anvil/container/Containerfile.dockerignore" -checksum = "sha256:4751a0669d2caa0c96a763167d69f6a9fd9ae4cf9f8d3e915f286be4b8323866" +checksum = "sha256:260bc557aadb7bec22525d5c98217bed2ebcac0e3252e6697f5d4a3ca629dc34" [[file]] path = ".anvil/container/README.md" @@ -21,11 +21,11 @@ checksum = "sha256:09576bca317f5a413572f6626fc052214f885589f0547ed0b0c044b92cc40 [[file]] path = ".anvil/container/image-id.ps1" -checksum = "sha256:dc8a2f709a966fced778ea7223bd48ddac5b8ec425c9cf452952f0c8eff5fc98" +checksum = "sha256:ce553ba67b21ed670db307f7ac87c95941122ece1c0bd94b1e37b83227d1ace7" [[file]] path = ".anvil/container/image-id.sh" -checksum = "sha256:8cb83009675be4181bf4dd919b077f9ea60651ac7b4f2009b783c308bd812a9b" +checksum = "sha256:f71671ec0f8c2ec2e17328a5aab8172744bed6f0b3a312df2bc2bd60d6460ef6" [[file]] path = ".anvil/container/run-in-container.ps1" @@ -35,10 +35,6 @@ checksum = "sha256:b29d0b7a362d204ce9da0739ae61a480b71239aed52e84c4e015e8ea074e5 path = ".anvil/container/run-in-container.sh" checksum = "sha256:0f4fcf0fef43e4736260af771cae15b7eb8afc382e5290fabddc52894b09139d" -[[file]] -path = ".anvil/resolve-stable-toolchain.ps1" -checksum = "sha256:bb2a90e425585813e4693a8c41fe6bb2705232dc1306d9cce421af85012fd4e4" - [[file]] path = ".github/actions/anvil-impact/action.yml" checksum = "sha256:76be01852dc1f7e01905c4a8ad384ca57eefc15e2ed74c5c00f57dddefae9b6f" @@ -53,7 +49,7 @@ checksum = "sha256:ff8def6c0786b6e146c4b633dfe38cb9b8ede398345516cca32bbcd558608 [[file]] path = ".github/actions/anvil-setup/action.yml" -checksum = "sha256:445cf5f313a504b2406029fec0d2e07821b2245706e97e1f7e6ff8761f5468bd" +checksum = "sha256:367475db03acf48a37c0b205415f5e7120bcb200bba1d27ebb4144cd813d17ae" [[file]] path = ".github/actions/anvil-setup/just-problem-matcher.json" @@ -77,19 +73,19 @@ checksum = "sha256:d4d3bd645a5586e9a1cc3a5fc27e38c93e59b1e29f83ede0ccd610273eec0 [[file]] path = "justfiles/anvil/checks/aprz.just" -checksum = "sha256:197541fc6c1dd712e452d17f7dfdfb2d8a405fffb79180b5f8773934ea9b80eb" +checksum = "sha256:079b0bf8a3e97745d8ac1fd7986056b7787af778ef00e491786572ae5166d810" [[file]] path = "justfiles/anvil/checks/audit.just" -checksum = "sha256:0143075c7f29bf035a42053f66f579b96e75e73b8bf1dfbcd27418ab4926e3ed" +checksum = "sha256:ef33da620683628208b4c5ad51f96f5f6424fe1d56c2d0475487228b1f1aec23" [[file]] path = "justfiles/anvil/checks/bench.just" -checksum = "sha256:021063c304a691bf0515a80ee63d6a075184ba59ba3bdaef0da63e283996f552" +checksum = "sha256:d9f8defd7e08e8988974474ea8761242c572f25f3765da45435c9b62e87d9882" [[file]] path = "justfiles/anvil/checks/bolero.just" -checksum = "sha256:c2e2e9474ed8e9e7c92538eba8d741525006c0e1daa771763d05b8a3f11c15e4" +checksum = "sha256:a67f556d46842487dc0f5851bb5ac634245eab6c2cfac32c6f6f0599b2ddab1d" [[file]] path = "justfiles/anvil/checks/careful.just" @@ -97,43 +93,43 @@ checksum = "sha256:ea06cf7ebd7d5955b409db500a9ab193311af584437412d7bf0429fa70e43 [[file]] path = "justfiles/anvil/checks/cargo-hack.just" -checksum = "sha256:8580a74aae3ce80a5d1ee39cd3f22240d361d0070f934b5ce67d758dc5123086" +checksum = "sha256:4eca78f197c2cf428b693c49cf0af4afb735b9c6caa8c83a71e5f5fd312a9bc1" [[file]] path = "justfiles/anvil/checks/cargo-sort.just" -checksum = "sha256:4342abfffa17029ed2543e9781840bbe10c7709d15b17ada04ff1c4ee94c2657" +checksum = "sha256:ea48b494699b6d6a71acd7070c96e28ab8b8b8ee74049de82afe922540508d75" [[file]] path = "justfiles/anvil/checks/clippy.just" -checksum = "sha256:c06c1f09fceb54bf131ea0b8a18c433417d50de224caaae834bad1dc19f509ae" +checksum = "sha256:945a1bf3bb1372b2bfbbfec00d520a97da56ebc5575b1e6292c421b52cc77243" [[file]] path = "justfiles/anvil/checks/deny.just" -checksum = "sha256:a5526e7ffc2d6f20508f5f7804f17ed768fefdd1888420705625aa057a288f6f" +checksum = "sha256:c5604e8cb0eaa2baf4e2e2c70032d037d21873090375ffcf1ee99983965f6635" [[file]] path = "justfiles/anvil/checks/doc-build.just" -checksum = "sha256:67d07c8cf394d2a3ef3fe2043978ec9743d9f757fe7c38cce4335d9a43d49f8c" +checksum = "sha256:a759a5eee188026e41f411207593293003547e2c17b7ff0227ba2e682211ab78" [[file]] path = "justfiles/anvil/checks/doc-test.just" -checksum = "sha256:baa67d238190acb7f0bdcdf3a46536fb98ecbfc931ea49f88e5d0fa5bee2291a" +checksum = "sha256:582329e6695e675ea7653b46baf3609b525e12db2765e59d12b74cf644b12491" [[file]] path = "justfiles/anvil/checks/ensure-no-cyclic-deps.just" -checksum = "sha256:289228c8d52900aa24c4e497a697102ea7d596ecac16eb4be477e8661d8e6aa8" +checksum = "sha256:2d19930c33a93b49d0c9e9672a31d85e1ad4fb92faecc0fb599803139b11c18f" [[file]] path = "justfiles/anvil/checks/ensure-no-default-features.just" -checksum = "sha256:57f00949f8e99e63a92af9fe6692ec42875fbc8e48c5434a462be7d9d993767a" +checksum = "sha256:38e089afe176aea92c93ac6758154b206b6fc8940931035c0d059449fa824d6c" [[file]] path = "justfiles/anvil/checks/examples.just" -checksum = "sha256:c3c38bb08cbb2a3dedf971bd70aa6b1189ee93f18dc7168983e03767d6f7636e" +checksum = "sha256:3e1b1d5315defdac91bc6ea6495bc015cd0bd4cc468dc317c6ca80464e0d70f0" [[file]] path = "justfiles/anvil/checks/external-types.just" -checksum = "sha256:2da1e7c8d57932eab5717977f72dc9de6b106604c7e0d37ccb2ac09389182527" +checksum = "sha256:7e67d90a9d8bd8cd5c8adb68baf41fdd4625bff4d425b5bf18b75baee62892a8" [[file]] path = "justfiles/anvil/checks/fmt.just" @@ -141,15 +137,15 @@ checksum = "sha256:967b68ef3ed3e0dcac2a1ea22ef92c66a7db802bda718f6e5ce39f529ed8e [[file]] path = "justfiles/anvil/checks/license-headers.just" -checksum = "sha256:5aa3b85bb14ce44ae753b078f84d3e9b7d9509e04f68534d7f97c82992eaca4f" +checksum = "sha256:7aa0560dc9088b33e80cd55aee0a25090cbd3d3610652afd3734d0ca52ee7b47" [[file]] path = "justfiles/anvil/checks/llvm-cov.just" -checksum = "sha256:cdbbcdb7666c320acbf9bd78df905a62b37785d8cd373d0a7b8838ae29154bc0" +checksum = "sha256:cb23283ac3d377b1219c904f4de93b05b437b48c93629fec617307914bba6296" [[file]] path = "justfiles/anvil/checks/loom.just" -checksum = "sha256:bd1883c419a0c64390263bbd15468da825be3ac8f9059fc8ae28df29917231e5" +checksum = "sha256:1e68ac182738e89717c53f3e09993371e1f00baaac7cb7ad8463d79527c8bc4b" [[file]] path = "justfiles/anvil/checks/miri-race-coverage.just" @@ -173,11 +169,11 @@ checksum = "sha256:968a0cf65a319626c3ed61f75e1d363608856664d105850be281e91f8ecf3 [[file]] path = "justfiles/anvil/checks/mutants-diff.just" -checksum = "sha256:66b5f07fa4aa3e09a987f349d8bebc65383c8be86570f30ddb37d29df375f62a" +checksum = "sha256:7b7ae9b1c303d29cb26bc70d362b5c51e3f6fbde01bdbc442da27cf53f315018" [[file]] path = "justfiles/anvil/checks/mutants-full.just" -checksum = "sha256:3b2b6d0ce7058afa8c491be1a57e45fc347b09942c1a4860015616207fef6afc" +checksum = "sha256:604dfb6aa2742695ff638a7d2db6811a1bcbf27da73660027baacc86b2fe1509" [[file]] path = "justfiles/anvil/checks/pr-title.just" @@ -185,15 +181,15 @@ checksum = "sha256:28734aa77526ca5c53d89a32c3e20ae6b42d2032c73ab6a1dbc9831f25cb0 [[file]] path = "justfiles/anvil/checks/readme-check.just" -checksum = "sha256:2002fb394eba9d319778693bcaa6076a23a32cdad924e3099331a28d79092db4" +checksum = "sha256:ce5311a8d091bb8d9cfa631a8d5753546a240d4fca55daab4d1b1bacb7c1f2a9" [[file]] path = "justfiles/anvil/checks/semver-check.just" -checksum = "sha256:374362645bbed4d21671980168817350911895e33e6d7e84646176b94de7b5fb" +checksum = "sha256:7047eb4e98f7f1fddf6a734afd0e8b1e3b4e1747b9992a0cd4c9be5892190f2c" [[file]] path = "justfiles/anvil/checks/spellcheck.just" -checksum = "sha256:40536077a6bd133597c2fdf653cd0b0913c20bc9cc988afeacbefadec6ea1477" +checksum = "sha256:2b2063f7e494dba51f805534bce5235132be1eeabde3a118d42d87ce49d51fd6" [[file]] path = "justfiles/anvil/checks/udeps.just" @@ -249,7 +245,7 @@ checksum = "sha256:6b71379310b986e9a44000ba9d072c3f8934397a0f7fb1bf27a31a7742ca7 [[file]] path = "justfiles/anvil/impact.just" -checksum = "sha256:3c632a3fd501e17177d2c934308060b0eeed50e2f19e66c874a2a2df2fe03f99" +checksum = "sha256:efcee10ddda4d4dd7be46a36187e5bb56c05fb069776fba377a227556bccc2eb" [[file]] path = "justfiles/anvil/mod.just" @@ -265,11 +261,11 @@ checksum = "sha256:5b2d91569f3fe87cd5f516623a01b5c7ae855ad587870ef9a2ae6d5619dd9 [[file]] path = "justfiles/anvil/tools.just" -checksum = "sha256:4fe2cf3e308530208e5326449326a0865a949aa10791180d544848612543eb09" +checksum = "sha256:854a3913c6538d361bc16ff7aa08571678eab00a4773cdd5ba1499b76c018b15" [[file]] path = "justfiles/anvil/versions.just" -checksum = "sha256:9257adf073dc2119cf70290c2f98b6c52c3146e63443f5188e42d6743c259287" +checksum = "sha256:e89afcdbf13139ca0fbd422d934107ce68744bad773cd40d33ea53e2b076f262" [[region]] host = ".delta.toml" diff --git a/.anvil/container/Containerfile.dockerignore b/.anvil/container/Containerfile.dockerignore index 6aca85683..c242cb21a 100644 --- a/.anvil/container/Containerfile.dockerignore +++ b/.anvil/container/Containerfile.dockerignore @@ -22,7 +22,6 @@ !justfiles/anvil/*.just !justfiles/anvil/checks/*.just !justfiles/anvil/groups/*.just -!.anvil/resolve-stable-toolchain.ps1 !.anvil/container/* .anvil/container/*/* .anvil/container/customize.sh diff --git a/.anvil/container/image-id.ps1 b/.anvil/container/image-id.ps1 index c95dd2c70..3ca2e8599 100644 --- a/.anvil/container/image-id.ps1 +++ b/.anvil/container/image-id.ps1 @@ -13,10 +13,7 @@ if ($LASTEXITCODE -ne 0 -or -not $repoRoot) { throw 'anvil-container must run from a Git repository.' } -$inputs = @( - 'Cargo.toml', - '.anvil/resolve-stable-toolchain.ps1' -) +$inputs = @('Cargo.toml') foreach ($toolchainFile in @('rust-toolchain', 'rust-toolchain.toml')) { if (Test-Path -LiteralPath (Join-Path $repoRoot $toolchainFile) -PathType Leaf) { $inputs += $toolchainFile diff --git a/.anvil/container/image-id.sh b/.anvil/container/image-id.sh index f399e349b..2dabc3487 100644 --- a/.anvil/container/image-id.sh +++ b/.anvil/container/image-id.sh @@ -19,7 +19,7 @@ if [[ ! "$base_image" =~ @sha256:[0-9a-fA-F]{64}$ ]]; then echo 'anvil-container: ANVIL_CONTAINER_BASE_IMAGE must be pinned by sha256 digest (image@sha256:<64 hex characters>).' >&2 exit 1 fi -inputs=(Cargo.toml .anvil/resolve-stable-toolchain.ps1) +inputs=(Cargo.toml) for toolchain_file in rust-toolchain rust-toolchain.toml; do if [[ -f "$repo_root/$toolchain_file" ]]; then inputs+=("$toolchain_file") diff --git a/.anvil/resolve-stable-toolchain.ps1 b/.anvil/resolve-stable-toolchain.ps1 deleted file mode 100644 index e38de2673..000000000 --- a/.anvil/resolve-stable-toolchain.ps1 +++ /dev/null @@ -1,310 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# GENERATED BY cargo-anvil. DO NOT EDIT DIRECTLY. - -param( - [switch] $ValidateWorkspaceMsrv, - [switch] $InstallIfMissing, - [switch] $ForEnvironment, - [switch] $MsrvToolchain, - [switch] $InstallMsrvIfNeeded, - [switch] $EmitToolchainFileOptions, - [switch] $ApplyToolchainFileOptions -) - -$ErrorActionPreference = 'Stop' -Set-StrictMode -Version 3 - -$repoRoot = Split-Path -Parent $PSScriptRoot - -function Get-RootToolchainFilePath { - foreach ($name in @('rust-toolchain', 'rust-toolchain.toml')) { - $path = Join-Path $repoRoot $name - if (Test-Path -LiteralPath $path -PathType Leaf) { - return $path - } - } - return $null -} - -function Read-RootToolchainFile([string] $path) { - $content = Get-Content -LiteralPath $path -Raw - if ([string]::IsNullOrWhiteSpace($content)) { - throw "anvil: root toolchain file '$path' is empty; remove it or declare a toolchain configuration" - } - return $content -} - -function Get-SelectingToolchainFile { - $path = Get-RootToolchainFilePath - if ($null -eq $path) { - return $null - } - - $content = Read-RootToolchainFile $path - if ((Split-Path -Leaf $path) -eq 'rust-toolchain' -and $content.TrimStart() -notmatch '^\[') { - return [pscustomobject]@{ - Path = $path - Value = $content.Trim() - Selector = 'channel' - Source = 'file' - Installable = $false - } - } - - $inToolchain = $false - foreach ($line in $content -split '\r?\n') { - if ($line -match '^\s*\[\s*toolchain\s*\]\s*(?:#.*)?$') { - $inToolchain = $true - continue - } - if ($line -match '^\s*\[') { - $inToolchain = $false - continue - } - if ($inToolchain -and $line -match '^\s*["'']?(channel|path)["'']?\s*=\s*(["''])([^"'']+)\2\s*(?:#.*)?$') { - $value = $Matches[3] - if ($Matches[1] -eq 'path' -and -not [IO.Path]::IsPathRooted($value)) { - $value = [IO.Path]::GetFullPath((Join-Path $repoRoot $value)) - } - return [pscustomobject]@{ - Path = $path - Value = $value - Selector = $Matches[1] - Source = 'file' - Installable = $false - } - } - } - return $null -} - -function Get-ToolchainFileOptions { - $path = Get-RootToolchainFilePath - if ($null -eq $path) { - return $null - } - - $content = Read-RootToolchainFile $path - $section = [regex]::Match( - $content, - '(?ms)^\s*\[\s*toolchain\s*\]\s*(?:#.*)?$(.*?)(?=^\s*\[|\z)' - ) - if (-not $section.Success) { - return $null - } - - $body = $section.Groups[1].Value - $profileMatch = [regex]::Match( - $body, - '(?m)^\s*["'']?profile["'']?\s*=\s*(["''])([^"'']+)\1\s*(?:#.*)?$' - ) - $readArray = { - param([string] $name) - $arrayMatch = [regex]::Match( - $body, - "(?ms)^\s*[`"']?$name[`"']?\s*=\s*\[(.*?)\]" - ) - if (-not $arrayMatch.Success) { - return @() - } - return @( - [regex]::Matches($arrayMatch.Groups[1].Value, '(["''])([^"'']+)\1') | - ForEach-Object { $_.Groups[2].Value } - ) - } - - return [pscustomobject]@{ - Path = $path - Profile = if ($profileMatch.Success) { $profileMatch.Groups[2].Value } else { $null } - Components = @(& $readArray 'components') - Targets = @(& $readArray 'targets') - } -} - -function Get-RootMsrv([switch] $AllowMissing) { - $manifestPath = Join-Path $repoRoot 'Cargo.toml' - if (-not (Test-Path -LiteralPath $manifestPath -PathType Leaf)) { - throw "anvil: Cargo.toml not found at repository root '$repoRoot'" - } - - $values = @{} - $section = '' - foreach ($line in (Get-Content -LiteralPath $manifestPath)) { - if ($line -match '^\s*\[\s*([^\]]+)\s*\]\s*(?:#.*)?$') { - $section = $Matches[1].Trim() - continue - } - if ($section -in @('workspace.package', 'package') -and - $line -match '^\s*["'']?rust-version["'']?\s*=\s*(["''])([^"'']+)\1\s*(?:#.*)?$') { - $values[$section] = $Matches[2] - } - } - - if ($values.ContainsKey('workspace.package')) { - return $values['workspace.package'] - } - if ($values.ContainsKey('package')) { - return $values['package'] - } - - if ($AllowMissing) { - return $null - } - throw 'anvil: no selecting rust-toolchain(.toml) and no root [workspace.package] or [package] rust-version; declare an MSRV or select a toolchain explicitly' -} - -function Assert-WorkspaceMsrvCompatibility { - $manifestPath = Join-Path $repoRoot 'Cargo.toml' - $metadataText = (& cargo metadata --manifest-path $manifestPath --format-version 1 --no-deps 2>&1 | Out-String) - if ($LASTEXITCODE -ne 0) { - throw "anvil: cargo metadata failed while validating workspace MSRVs:`n$metadataText" - } - $metadata = $metadataText | ConvertFrom-Json - $memberIds = [Collections.Generic.HashSet[string]]::new( - [string[]] $metadata.workspace_members, - [StringComparer]::Ordinal - ) - $members = @($metadata.packages | Where-Object { $memberIds.Contains([string] $_.id) }) - $missing = @($members | Where-Object { [string]::IsNullOrWhiteSpace([string] $_.rust_version) } | ForEach-Object name) - if ($missing.Count -gt 0) { - throw "anvil: workspace packages without a resolved rust-version: $($missing -join ', '); declare one or select a toolchain explicitly" - } - - $rootMsrv = Get-RootMsrv - $newer = @( - $members | - Where-Object { [version] ([string] $_.rust_version) -gt [version] $rootMsrv } | - ForEach-Object { "$($_.name) ($($_.rust_version))" } - ) - if ($newer.Count -gt 0) { - throw "anvil: workspace packages require a compiler newer than the root MSRV $rootMsrv`: $($newer -join ', '); raise the root MSRV or select one catalog toolchain explicitly with rust-toolchain.toml" - } -} - -function Assert-CompleteInternalToolchainConfiguration { - if (-not [string]::IsNullOrWhiteSpace($env:ANVIL_MSRV_TOOLCHAIN) -and - [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN) -and - $null -eq $fileSelection) { - throw 'anvil: ANVIL_MSRV_TOOLCHAIN is set without RUSTUP_TOOLCHAIN and no repository toolchain file selects stable checks; set RUSTUP_TOOLCHAIN to the provisioned stable toolchain or unset ANVIL_MSRV_TOOLCHAIN' - } -} - -function Get-MsrvSelection { - $msrv = Get-RootMsrv -AllowMissing - if ([string]::IsNullOrWhiteSpace($msrv)) { - return $null - } - - if (-not [string]::IsNullOrWhiteSpace($env:ANVIL_MSRV_TOOLCHAIN)) { - return [pscustomobject]@{ - Value = $env:ANVIL_MSRV_TOOLCHAIN - Source = 'mapped-msrv' - Installable = $false - Mapped = $true - } - } - return [pscustomobject]@{ Value = $msrv; Source = 'msrv'; Installable = $true; Mapped = $false } -} - -function Apply-ToolchainFileOptions([object] $toolchain) { - if ($toolchain.Source -eq 'file' -or $null -eq $toolchainFileOptions) { - return - } - foreach ($component in $toolchainFileOptions.Components) { - rustup component add --toolchain $toolchain.Value $component - if ($LASTEXITCODE -ne 0) { - Write-Warning "anvil: toolchain-file component '$component' is unavailable for '$($toolchain.Value)'; rustup would skip an unavailable file component" - } - } - foreach ($target in $toolchainFileOptions.Targets) { - rustup target add --toolchain $toolchain.Value $target - if ($LASTEXITCODE -ne 0) { - Write-Warning "anvil: toolchain-file target '$target' is unavailable for '$($toolchain.Value)'; rustup would skip an unavailable file target" - } - } -} - -function Install-Toolchain([object] $toolchain, [string] $label) { - if ($toolchain.Installable) { - $installed = rustup toolchain list - if ($LASTEXITCODE -ne 0) { - throw 'anvil: rustup toolchain list failed' - } - if ($installed -notmatch "(?m)^$([regex]::Escape($toolchain.Value))(?:-|$)") { - $profile = if ($null -ne $toolchainFileOptions -and $toolchainFileOptions.Profile) { - $toolchainFileOptions.Profile - } else { - 'minimal' - } - Write-Host "anvil: installing $label toolchain '$($toolchain.Value)'" - rustup toolchain install $toolchain.Value --profile $profile - if ($LASTEXITCODE -ne 0) { - throw "anvil: failed to install $label toolchain '$($toolchain.Value)'" - } - } - } - Apply-ToolchainFileOptions $toolchain -} - -$fileSelection = Get-SelectingToolchainFile -$toolchainFileOptions = Get-ToolchainFileOptions -if ($EmitToolchainFileOptions) { - if ($null -ne $toolchainFileOptions) { - $toolchainFileOptions | ConvertTo-Json -Compress - } - exit 0 -} -if ($ValidateWorkspaceMsrv) { - Assert-CompleteInternalToolchainConfiguration - $skipWorkspaceMsrvValidation = -not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN) - if (-not $skipWorkspaceMsrvValidation -and $null -eq $fileSelection) { - Assert-WorkspaceMsrvCompatibility - } - exit 0 -} - -if ($MsrvToolchain -or $InstallMsrvIfNeeded) { - $msrvSelection = Get-MsrvSelection - if ($null -eq $msrvSelection) { - exit 0 - } - if ($InstallMsrvIfNeeded) { - if ($msrvSelection.Mapped) { - & cargo "+$($msrvSelection.Value)" --version *> $null - if ($LASTEXITCODE -ne 0) { - throw "anvil: mapped MSRV toolchain '$($msrvSelection.Value)' is unavailable; provision ANVIL_MSRV_TOOLCHAIN or unset it to let Anvil install the declared public MSRV" - } - } - Install-Toolchain $msrvSelection 'MSRV' - } else { - $msrvSelection.Value - } - exit 0 -} - -Assert-CompleteInternalToolchainConfiguration -$selection = if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { - [pscustomobject]@{ Value = $env:RUSTUP_TOOLCHAIN; Source = 'environment'; Installable = $false } -} elseif ($null -ne $fileSelection) { - $fileSelection -} else { - [pscustomobject]@{ Value = Get-RootMsrv; Source = 'msrv'; Installable = $true } -} - -if ($ApplyToolchainFileOptions) { - Apply-ToolchainFileOptions $selection - exit 0 -} - -if ($InstallIfMissing) { - Install-Toolchain $selection 'stable' - exit 0 -} - -if ($ForEnvironment -and $selection.Source -eq 'file') { - '' -} else { - $selection.Value -} -exit 0 diff --git a/.github/actions/anvil-setup/action.yml b/.github/actions/anvil-setup/action.yml index 20c55562f..cf3ad0863 100644 --- a/.github/actions/anvil-setup/action.yml +++ b/.github/actions/anvil-setup/action.yml @@ -104,7 +104,12 @@ runs: - id: rustc-version shell: pwsh -NoProfile -Command ". '{0}'" run: | - $version = (& just _anvil-with-stable rustc --version | Select-Object -Last 1).Split()[1] + $toolchain = (& just _anvil-resolve-stable for-environment | Out-String).Trim() + if ($LASTEXITCODE -ne 0) { + throw 'anvil: failed to select stable toolchain arguments' + } + $toolchainArgs = if ([string]::IsNullOrWhiteSpace($toolchain)) { @() } else { @("+$toolchain") } + $version = (& rustc @toolchainArgs --version | Select-Object -Last 1).Split()[1] "version=$version" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append - name: Restore cargo cache id: cargo-cache diff --git a/crates/cargo-anvil/docs/design/README.md b/crates/cargo-anvil/docs/design/README.md index d8e80d90e..7c56dbf5c 100644 --- a/crates/cargo-anvil/docs/design/README.md +++ b/crates/cargo-anvil/docs/design/README.md @@ -237,7 +237,6 @@ repo/ ├── .anvil.lock sidecar manifest tracking last-rendered checksums (see updates.md) ├── Justfile managed-region: anvil-imports ├── justfiles/anvil/ owned (see local.md) -├── .anvil/resolve-stable-toolchain.ps1 owned, unconditional bootstrap (see local.md) ├── .anvil/container/ owned, optional container assets (see containers.md) ├── Cargo.toml managed-region: anvil-workspace-lints (or anvil-lints in single-crate) ├── crates//Cargo.toml managed-region: anvil-lints (one per workspace member) @@ -269,9 +268,6 @@ repo/ Detail on each host: - **`Justfile` and `justfiles/anvil/*.just`** — see [local.md](./local.md). -- **`.anvil/resolve-stable-toolchain.ps1`** — the unconditional generated - implementation behind the private stable-toolchain recipes used by local, - cloud, and container setup. - **`.anvil/container/`** — the non-recipe container assets (Containerfile, drivers, image-ID helpers, README) emitted only when the catalog includes the optional container backend. `justfiles/` holds `.just` recipes and nothing diff --git a/crates/cargo-anvil/docs/design/ado.md b/crates/cargo-anvil/docs/design/ado.md index c52a0e3a7..765bd0d21 100644 --- a/crates/cargo-anvil/docs/design/ado.md +++ b/crates/cargo-anvil/docs/design/ado.md @@ -743,12 +743,14 @@ mechanics are in [local.md §4](./local.md#4-impact-scoping-via-the-anvil-impact ## 6. Rust toolchain -The user's root pipeline or compliance template installs Rust before the Anvil stages -run. The generated setup step resolves the catalog toolchain and captures its -`rustc --version` through `_anvil-with-stable` before restoring the cache. It does -not publish a resolved `RUSTUP_TOOLCHAIN` to subsequent steps. Each stable recipe -applies the selection only to its own Cargo or Rust child process, while a selecting -toolchain file remains unoverridden so rustup processes the complete file. +The user's root pipeline or compliance template installs Rust before the Anvil +stages run. The generated setup step prepares the catalog toolchain and invokes +`rustc --version` directly with either the selected `+toolchain` argument or no +override for a selecting repository file before restoring the cache. It does +not publish a resolved `RUSTUP_TOOLCHAIN` to subsequent steps. Each stable +recipe lazily selects the same optional argument and invokes Cargo or Rust +directly in its current process, while a selecting toolchain file remains +unoverridden so rustup processes the complete file. Selection follows the shared local contract: an existing `RUSTUP_TOOLCHAIN`, then a root toolchain file with `channel` or `path`, then the root MSRV. Internal pipelines diff --git a/crates/cargo-anvil/docs/design/checks.md b/crates/cargo-anvil/docs/design/checks.md index 9a2eabf3f..c0ad44472 100644 --- a/crates/cargo-anvil/docs/design/checks.md +++ b/crates/cargo-anvil/docs/design/checks.md @@ -163,6 +163,12 @@ recipes locally. The cell format is `cargo invocation (short rationale)`. "Source" cites the surveyed repo that provided the strongest version of the check. +Invocations shown without a pinned nightly or MSRV use the selected stable +compiler. Their generated PowerShell recipes invoke Cargo directly with the +lazy optional stable argument: an explicit `+toolchain` for a caller override +or root MSRV, and no argument when a repository toolchain file selects the +compiler. They do not route the main command through a nested Just recipe. + ### `pr-fast` | Check | Invocation | Source | diff --git a/crates/cargo-anvil/docs/design/containers.md b/crates/cargo-anvil/docs/design/containers.md index bd4cde058..2c7a75ec1 100644 --- a/crates/cargo-anvil/docs/design/containers.md +++ b/crates/cargo-anvil/docs/design/containers.md @@ -150,7 +150,6 @@ The local image tag is a SHA-256 hash of build-relevant repository content: - root `Cargo.toml`; - an optional `rust-toolchain` or `rust-toolchain.toml`; -- the generated stable-toolchain resolver; - generated `justfiles/anvil/**/*.just` recipes; - the `Containerfile`, `Containerfile.dockerignore`, entrypoint, and other static image inputs. @@ -174,9 +173,10 @@ available. Runtime execution uses `--pull=never` and never substitutes Container execution uses the same deterministic stable-toolchain selection as native execution: an existing `RUSTUP_TOOLCHAIN`, a selecting repository toolchain file, or the root manifest's MSRV. It fails rather than choosing a -runner or image default when none is available. Image setup and checks invoke -the standard resolver and selected-stable execution recipes; the resolved value -is not exported across unrelated recipe processes. +runner or image default when none is available. Image setup uses the ordinary +toolchain setup recipe. Checks then evaluate the lazy optional `+toolchain` +argument and invoke Cargo or Rust directly in their current recipe process; no +resolved value is exported across unrelated recipe processes. The restricted image-construction context does not contain workspace member manifests, so per-package MSRV compatibility validation runs in native and diff --git a/crates/cargo-anvil/docs/design/github.md b/crates/cargo-anvil/docs/design/github.md index 0659de36a..873353fbb 100644 --- a/crates/cargo-anvil/docs/design/github.md +++ b/crates/cargo-anvil/docs/design/github.md @@ -841,15 +841,16 @@ The check → bucket mapping is in ## 7. Rust toolchain The setup action bootstraps cargo-binstall and Just using the runner environment, -then resolves the catalog's stable toolchain through the standard private Just -recipe before reading `rustc` or restoring build artifacts. It installs a missing -public channel through rustup and captures the selected compiler version through -`_anvil-with-stable`, but does not publish a resolved `RUSTUP_TOOLCHAIN` through -`GITHUB_ENV`. Each stable recipe applies the selection only to its own Cargo or -Rust child process; selecting toolchain files -remain unoverridden so rustup processes them natively. All matrix legs therefore use -the same repository selection instead of the different stable versions that runner -images may carry, without changing unrelated recipe environments. +then prepares the catalog's stable toolchain through the standard private Just +recipe before reading `rustc` or restoring build artifacts. It installs a +missing public channel through rustup and invokes `rustc` directly with either +the selected `+toolchain` argument or no override for a selecting repository +file. It does not publish a resolved `RUSTUP_TOOLCHAIN` through `GITHUB_ENV`. +Each stable recipe lazily selects the same optional argument and invokes Cargo +or Rust directly in its current process; selecting toolchain files remain +unoverridden so rustup processes them natively. All matrix legs therefore use +the same repository selection instead of the different stable versions that +runner images may carry, without changing unrelated recipe environments. Selection follows the shared local contract: an existing `RUSTUP_TOOLCHAIN`, then a root toolchain file with `channel` or `path`, then the root MSRV. There is no diff --git a/crates/cargo-anvil/docs/design/local.md b/crates/cargo-anvil/docs/design/local.md index b62c6e419..eaf3b2c60 100644 --- a/crates/cargo-anvil/docs/design/local.md +++ b/crates/cargo-anvil/docs/design/local.md @@ -51,28 +51,25 @@ repo/ │ ├── tools.just tool/component/toolchain install + validate-prereqs recipes, │ │ plus the cargo-spellcheck source-deps check and │ │ anvil-validate-prereqs. -│ └── versions.just catalog nightly toolchains and cargo-subcommand minimum versions -│ as plain just variables (rust_nightly, cargo_nextest_version, …). -│ Read by recipes via `{{ var }}` interpolation. See §3. +│ └── versions.just catalog nightly toolchains and cargo-subcommand minimum versions, +│ plus the lazy stable-toolchain argument selector, as +│ non-exported just variables. See §3. │ -└── .anvil/ - ├── resolve-stable-toolchain.ps1 unconditional toolchain bootstrap - └── container/ optional non-recipe container assets - ├── Containerfile - ├── Containerfile.dockerignore - ├── README.md - ├── entrypoint.sh - ├── image-id.ps1 - ├── image-id.sh - ├── run-in-container.ps1 - └── run-in-container.sh +└── .anvil/container/ optional non-recipe container assets + ├── Containerfile + ├── Containerfile.dockerignore + ├── README.md + ├── entrypoint.sh + ├── image-id.ps1 + ├── image-id.sh + ├── run-in-container.ps1 + └── run-in-container.sh ``` The Justfile region is the only file anvil adds to that the user co-owns, and it's -a single `import` line. Generated recipes live inside `justfiles/anvil/`; the -unconditional stable-toolchain bootstrap lives at -`.anvil/resolve-stable-toolchain.ps1`; optional non-recipe container assets live -inside `.anvil/container/`. Generated files in these directories are tool-owned +a single `import` line. Generated recipes live inside `justfiles/anvil/`; +optional non-recipe container assets live inside `.anvil/container/`. +Generated files in these directories are tool-owned (tracked by full-file checksum in the sidecar manifest). If the user wants to add project-specific recipes, they add them to the top-level `Justfile` outside the managed region, or to their own additional imported `.just` files. The alias @@ -404,17 +401,23 @@ without that stable override (and without a selecting repository toolchain file) is rejected as incomplete internal configuration. An empty root toolchain file is invalid and produces an actionable error. -The generated `.anvil/resolve-stable-toolchain.ps1` implements the parsing and -installation logic behind the private `_anvil-resolve-stable` recipe. +`versions.just` holds a shared multiline PowerShell selection script and the +lazy, non-exported `_anvil_stable_toolchain_args` value that evaluates it. `RUSTUP_TOOLCHAIN` is an input to selection, never a globally exported output. -Each stable check invokes its Cargo or Rust command through -`_anvil-with-stable`, which applies an environment/MSRV selection only to that -child process. For a selecting toolchain file the helper removes the override -for that child so rustup processes the complete file, including its components, -targets, and profile. Nightly and MSRV checks continue to pass explicit -`+toolchain` arguments. The compiler intent is therefore visible at each command -site, nested Just invocations cannot recursively demand a global exported value, -and unrelated recipes and raw commands retain the caller's normal environment. +The value is a PowerShell argument-array expression: `@()` for a selecting +repository toolchain file, or `@('+toolchain')` for a caller override or MSRV +fallback. Apostrophes in explicit toolchain names are escaped before the +expression is interpolated. + +Each stable recipe executes Cargo or Rust directly in its current PowerShell +recipe process, for example +`& cargo {{_anvil_stable_toolchain_args}} clippy ...`. A selecting toolchain +file therefore reaches native rustup unoverridden, including its components, +targets, profile, and path semantics. Explicit caller/MSRV selections are +visible as Cargo's `+toolchain` argument. Nightly and MSRV-specific checks keep +their own explicit `+toolchain` arguments. No nested Just wrapper sits on the +main Cargo/Rust command path, and unrelated recipes and raw commands retain the +caller's normal environment. An environment override or MSRV fallback normally causes rustup to ignore the repository toolchain file. Anvil therefore reads `profile`, `components`, and @@ -425,12 +428,12 @@ Explicit components and targets are added at setup time. If an option is unavail for the selected compiler, Anvil warns and skips it, matching rustup's handling of unavailable options read directly from a toolchain file. -The resolver script is a generated implementation artifact, but all supported -entry points call it through `_anvil-resolve-stable`. Because selection is no -longer a globally exported Just variable, that standard recipe can be invoked -from setup, checks, or nested Just helpers without a parse-time evaluation cycle. -The single script implementation keeps PowerShell parsing and installation -behavior identical across those recipe callers. +The private `_anvil-resolve-stable` recipe contains the setup implementation +for installing public selections, replaying toolchain-file options, validating +workspace MSRVs, and mapping the MSRV in internal environments. Setup workflows +and container construction may invoke this ordinary recipe. It does not wrap +the Cargo/Rust command that performs a check and does not delegate to a +standalone script. When selection falls back to the root MSRV, prerequisite validation reads `cargo metadata` and requires every workspace package to resolve a diff --git a/crates/cargo-anvil/src/anvil/artifacts/container.rs b/crates/cargo-anvil/src/anvil/artifacts/container.rs index d01b6990e..7cbdcab1e 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/container.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/container.rs @@ -189,7 +189,6 @@ mod tests { fn write_image_id_fixture(root: &Path) { write(&root.join("Cargo.toml"), "[workspace.package]\nrust-version = \"1.93\"\n"); - write(&root.join(".anvil/resolve-stable-toolchain.ps1"), "Write-Output '1.93'\n"); write(&root.join("justfiles/anvil/versions.just"), "tool_version := \"1\"\n"); write( &root.join(CONTAINERFILE_PATH), @@ -229,7 +228,7 @@ mod tests { assert!(CONTAINERFILE.contains("just anvil-setup")); assert!(CONTAINERFILE.contains("COPY . .")); assert!(IGNORE.contains("!.anvil/container/*")); - assert!(IGNORE.contains("!.anvil/resolve-stable-toolchain.ps1")); + assert!(!IGNORE.contains("resolve-stable-toolchain")); assert!(IGNORE.contains("!justfiles/anvil/checks/*.just")); assert!(CONTAINERFILE.contains("anvil_runner := \\\"native\\\"")); assert!(!CONTAINERFILE.contains("requires rust-toolchain.toml")); @@ -342,11 +341,7 @@ mod tests { crate::anvil::artifacts::anvil_artifacts() .into_iter() .filter_map(|artifact| match artifact { - Artifact::OwnedFile(spec) - if spec.path.starts_with("justfiles/") - || spec.path.starts_with(".anvil/container/") - || spec.path == ".anvil/resolve-stable-toolchain.ps1" => - { + Artifact::OwnedFile(spec) if spec.path.starts_with("justfiles/") || spec.path.starts_with(".anvil/container/") => { Some(spec.path) } _ => None, diff --git a/crates/cargo-anvil/src/anvil/artifacts/justfile.rs b/crates/cargo-anvil/src/anvil/artifacts/justfile.rs index a445082c2..0ac0af584 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/justfile.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/justfile.rs @@ -27,12 +27,6 @@ const VERSIONS_JUST: &str = include_str!("../../../templates/justfiles/anvil/ver /// Repo-root-relative path of the pinned-versions recipe file. const VERSIONS_JUST_PATH: &str = "justfiles/anvil/versions.just"; -/// Stable-toolchain resolver shared by local recipes and cloud setup. -const STABLE_TOOLCHAIN_RESOLVER: &str = include_str!("../../../templates/anvil/resolve-stable-toolchain.ps1"); - -/// Repo-root-relative path of the stable-toolchain resolver. -const STABLE_TOOLCHAIN_RESOLVER_PATH: &str = ".anvil/resolve-stable-toolchain.ps1"; - /// Contents of `justfiles/anvil/tools.just` baked into the binary. const TOOLS_JUST: &str = include_str!("../../../templates/justfiles/anvil/tools.just"); @@ -210,12 +204,6 @@ pub fn versions() -> Artifact { Artifact::owned_file(VERSIONS_JUST_PATH, VERSIONS_JUST) } -/// `.anvil/resolve-stable-toolchain.ps1` — deterministic stable selection. -#[must_use] -pub fn stable_toolchain_resolver() -> Artifact { - Artifact::owned_file(STABLE_TOOLCHAIN_RESOLVER_PATH, STABLE_TOOLCHAIN_RESOLVER) -} - /// `justfiles/anvil/tools.just` — tool install / prereq recipes. #[must_use] pub fn tools() -> Artifact { @@ -566,7 +554,7 @@ mod tests { "_anvil-base-ref", "git rev-parse --verify \"$base^{commit}\"", "git cat-file -e $baselineManifest", - "cargo semver-checks --package $p --baseline-rev $base", + "cargo {{_anvil_stable_toolchain_args}} semver-checks --package $p --baseline-rev $base", ] { assert!(body.contains(needle), "semver check template missing '{needle}'"); } @@ -750,21 +738,20 @@ mod tests { } #[test] - fn stable_toolchain_resolution_is_scoped_to_each_command() { + fn stable_toolchain_selection_is_lazy_and_scoped_to_each_command() { assert!(!VERSIONS_JUST.contains("export RUSTUP_TOOLCHAIN")); + assert!(VERSIONS_JUST.contains("_anvil_stable_toolchain_selection_script := '''")); + assert!(VERSIONS_JUST.contains("set lazy")); + assert!(VERSIONS_JUST.contains("_anvil_stable_toolchain_args :=")); + assert!(VERSIONS_JUST.contains("shell(\"& ([scriptblock]::Create('\"")); + assert!(VERSIONS_JUST.contains("quote(_anvil_stable_toolchain_selection_script)")); + assert!(VERSIONS_JUST.contains("Write-Output '@()'")); + assert!(VERSIONS_JUST.contains("\"@('+\"")); assert!(TOOLS_JUST.contains("_anvil-resolve-stable action=\"resolve\":")); - assert!(TOOLS_JUST.contains("_anvil-with-stable command *args:")); - assert!(TOOLS_JUST.contains("Remove-Item Env:\\RUSTUP_TOOLCHAIN")); - assert!(TOOLS_JUST.contains("$env:RUSTUP_TOOLCHAIN = $toolchain")); - assert!(STABLE_TOOLCHAIN_RESOLVER.contains("RUSTUP_TOOLCHAIN")); - assert!(STABLE_TOOLCHAIN_RESOLVER.contains("rust-version")); - assert!(STABLE_TOOLCHAIN_RESOLVER.contains("ValidateWorkspaceMsrv")); - assert!(STABLE_TOOLCHAIN_RESOLVER.contains("InstallIfMissing")); - assert!(STABLE_TOOLCHAIN_RESOLVER.contains("MsrvToolchain")); - assert!(STABLE_TOOLCHAIN_RESOLVER.contains("InstallMsrvIfNeeded")); - assert!(STABLE_TOOLCHAIN_RESOLVER.contains("EmitToolchainFileOptions")); - assert!(STABLE_TOOLCHAIN_RESOLVER.contains("rustup component add --toolchain")); - assert!(STABLE_TOOLCHAIN_RESOLVER.contains("rustup target add --toolchain")); + assert!(!TOOLS_JUST.contains("_anvil-with-stable")); + assert!(TOOLS_JUST.contains("& cargo {{_anvil_stable_toolchain_args}}")); + assert!(TOOLS_JUST.contains("rustup component add --toolchain")); + assert!(TOOLS_JUST.contains("rustup target add --toolchain")); } #[test] @@ -782,8 +769,10 @@ mod tests { .map(|(_, arguments)| arguments.trim_start()) .expect("invokes_cargo patterns always include 'cargo '"); assert!( - toolchain.starts_with("'+") || toolchain.starts_with("\"+"), - "{path} invokes Cargo without an explicit toolchain or _anvil-with-stable: {line}" + toolchain.starts_with("'+") + || toolchain.starts_with("\"+") + || toolchain.starts_with("{{_anvil_stable_toolchain_args}}"), + "{path} invokes Cargo without an explicit toolchain selection: {line}" ); } } @@ -798,24 +787,69 @@ mod tests { use tempfile::TempDir; - use super::STABLE_TOOLCHAIN_RESOLVER; + use super::{TOOLS_JUST, VERSIONS_JUST}; fn fixture(cargo_toml: &str) -> TempDir { let temp = TempDir::new().expect("temporary repository must be creatable"); - fs::create_dir(temp.path().join(".anvil")).expect("resolver directory must be creatable"); - fs::write(temp.path().join(".anvil/resolve-stable-toolchain.ps1"), STABLE_TOOLCHAIN_RESOLVER) - .expect("resolver fixture must be writable"); fs::write(temp.path().join("Cargo.toml"), cargo_toml).expect("manifest fixture must be writable"); + fs::write(temp.path().join("versions.just"), VERSIONS_JUST).expect("versions fixture must be writable"); + fs::write(temp.path().join("tools.just"), TOOLS_JUST).expect("tools fixture must be writable"); + fs::write( + temp.path().join("Justfile"), + concat!( + "set unstable\n", + "set windows-shell := [\"pwsh\", \"-NoProfile\", \"-Command\"]\n", + "import 'versions.just'\n", + "import 'tools.just'\n\n", + "[script(\"pwsh\", \"-NoProfile\")]\n", + "_anvil-test-stable-args:\n", + " Write-Output \"{{_anvil_stable_toolchain_args}}\"\n", + ), + ) + .expect("Justfile fixture must be writable"); temp } - fn run(root: &Path, args: &[&str], rustup_toolchain: Option<&str>) -> Output { - let mut command = Command::new("pwsh"); + fn command(root: &Path) -> Command { + let mut command = Command::new("just"); command - .args(["-NoProfile", "-File", ".anvil/resolve-stable-toolchain.ps1"]) - .args(args) + .args(["--justfile"]) + .arg(root.join("Justfile")) .current_dir(root) - .env_remove("ANVIL_MSRV_TOOLCHAIN"); + .env_remove("ANVIL_MSRV_TOOLCHAIN") + .env_remove("RUSTUP_TOOLCHAIN"); + command + } + + fn run(root: &Path, args: &[&str], rustup_toolchain: Option<&str>) -> Output { + let mut command = command(root); + match args { + [] => { + command.arg("_anvil-test-stable-args"); + } + ["-ForEnvironment"] => { + command.args(["_anvil-resolve-stable", "for-environment"]); + } + ["-InstallIfMissing"] => { + command.args(["_anvil-resolve-stable", "install"]); + } + ["-ApplyToolchainFileOptions"] => { + command.args(["_anvil-resolve-stable", "apply-file-options"]); + } + ["-ValidateWorkspaceMsrv"] => { + command.args(["_anvil-resolve-stable", "validate-workspace-msrv"]); + } + ["-MsrvToolchain"] => { + command.args(["_anvil-resolve-stable", "msrv"]); + } + ["-InstallMsrvIfNeeded"] => { + command.args(["_anvil-resolve-stable", "install-msrv"]); + } + ["-EmitToolchainFileOptions"] => { + command.args(["_anvil-resolve-stable", "emit-file-options"]); + } + other => panic!("unsupported resolver arguments in test: {other:?}"), + } match rustup_toolchain { Some(value) => { command.env("RUSTUP_TOOLCHAIN", value); @@ -824,7 +858,9 @@ mod tests { command.env_remove("RUSTUP_TOOLCHAIN"); } } - command.output().expect("pwsh must be available to test the generated resolver") + command + .output() + .expect("just must be available to test generated toolchain selection") } fn resolved(root: &Path, rustup_toolchain: Option<&str>) -> String { @@ -855,36 +891,19 @@ mod tests { fn honors_environment_files_and_msrv_in_precedence_order() { let temp = fixture("[workspace.package]\nrust-version = \"1.93\"\n"); let root = temp.path(); - assert_eq!(resolved(root, None), "1.93"); - assert_eq!( - String::from_utf8(run(root, &["-ForEnvironment"], None).stdout) - .expect("resolver output must be UTF-8") - .trim(), - "1.93" - ); + assert_eq!(resolved(root, None), "@('+1.93')"); fs::write(root.join("rust-toolchain.toml"), "[toolchain]\ncomponents = [\"clippy\"]\n") .expect("toolchain fixture must be writable"); - assert_eq!(resolved(root, None), "1.93"); + assert_eq!(resolved(root, None), "@('+1.93')"); fs::write(root.join("rust-toolchain.toml"), "[toolchain]\nchannel = \"1.94\"\n").expect("toolchain fixture must be writable"); - assert_eq!(resolved(root, None), "1.94"); - assert!( - String::from_utf8(run(root, &["-ForEnvironment"], None).stdout) - .expect("resolver output must be UTF-8") - .trim() - .is_empty() - ); + assert_eq!(resolved(root, None), "@()"); fs::write(root.join("rust-toolchain"), "1.92\n").expect("legacy toolchain fixture must be writable"); - assert_eq!(resolved(root, None), "1.92"); - assert_eq!(resolved(root, Some("custom-toolchain")), "custom-toolchain"); - assert_eq!( - String::from_utf8(run(root, &["-ForEnvironment"], Some("custom-toolchain")).stdout) - .expect("resolver output must be UTF-8") - .trim(), - "custom-toolchain" - ); + assert_eq!(resolved(root, None), "@()"); + assert_eq!(resolved(root, Some("custom-toolchain")), "@('+custom-toolchain')"); + assert_eq!(resolved(root, Some("team's-toolchain")), "@('+team''s-toolchain')"); } #[test] @@ -966,14 +985,12 @@ mod tests { let mut paths = vec![shim.path().to_path_buf()]; paths.extend(env::split_paths(&env::var_os("PATH").unwrap_or_default())); - let output = Command::new("pwsh") - .args(["-NoProfile", "-File", ".anvil/resolve-stable-toolchain.ps1", "-InstallIfMissing"]) - .current_dir(temp.path()) - .env_remove("RUSTUP_TOOLCHAIN") + let output = command(temp.path()) + .args(["_anvil-resolve-stable", "install"]) .env("ANVIL_TEST_LOG", &log) .env("PATH", env::join_paths(paths).expect("shim PATH must be valid")) .output() - .expect("pwsh must be available to test the generated resolver"); + .expect("just must be available to test the generated resolver recipe"); assert!( output.status.success(), "toolchain option application failed: {}", @@ -1044,13 +1061,11 @@ mod tests { fn rejects_an_unpaired_internal_msrv_mapping_for_stable_selection() { let temp = fixture("[workspace.package]\nrust-version = \"1.93\"\n"); let run_with_mapping = |root: &Path| { - Command::new("pwsh") - .args(["-NoProfile", "-File", ".anvil/resolve-stable-toolchain.ps1"]) - .current_dir(root) - .env_remove("RUSTUP_TOOLCHAIN") + command(root) + .arg("_anvil-test-stable-args") .env("ANVIL_MSRV_TOOLCHAIN", "ms-prod-1.93") .output() - .expect("pwsh must be available to test the generated resolver") + .expect("just must be available to test the generated toolchain selection") }; let output = run_with_mapping(temp.path()); @@ -1098,12 +1113,9 @@ mod tests { .expect("cargo shim must be writable"); let mut paths = vec![shim.path().to_path_buf()]; paths.extend(env::split_paths(&env::var_os("PATH").unwrap_or_default())); - let mut command = Command::new("pwsh"); + let mut command = command(root); command - .args(["-NoProfile", "-File", ".anvil/resolve-stable-toolchain.ps1", "-InstallMsrvIfNeeded"]) - .current_dir(root) - .env_remove("RUSTUP_TOOLCHAIN") - .env_remove("ANVIL_MSRV_TOOLCHAIN") + .args(["_anvil-resolve-stable", "install-msrv"]) .env("ANVIL_RUSTUP_LOG", &rustup_log) .env("ANVIL_CARGO_LOG", &cargo_log) .env("PATH", env::join_paths(paths).expect("shim PATH must be valid")); @@ -1205,13 +1217,11 @@ mod tests { fs::write(root.join("rust-toolchain.toml"), "[toolchain]\nchannel = \"1.94\"\n").expect("toolchain fixture must be writable"); assert_eq!(resolve_msrv(root), "1.93"); - let output = Command::new("pwsh") - .args(["-NoProfile", "-File", ".anvil/resolve-stable-toolchain.ps1", "-MsrvToolchain"]) - .current_dir(root) - .env_remove("RUSTUP_TOOLCHAIN") + let output = command(root) + .args(["_anvil-resolve-stable", "msrv"]) .env("ANVIL_MSRV_TOOLCHAIN", "ms-prod-1.93") .output() - .expect("pwsh must be available to test the generated resolver"); + .expect("just must be available to test the generated resolver recipe"); assert!(output.status.success()); assert_eq!( String::from_utf8(output.stdout).expect("resolver output must be UTF-8").trim(), diff --git a/crates/cargo-anvil/src/anvil/artifacts/mod.rs b/crates/cargo-anvil/src/anvil/artifacts/mod.rs index 6b73917d9..34f6bff1f 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/mod.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/mod.rs @@ -60,7 +60,6 @@ pub(crate) fn anvil_artifacts() -> Vec { justfile::entry(), justfile::tools(), justfile::versions(), - justfile::stable_toolchain_resolver(), justfile::helpers(), justfile::impact(), justfile::runner(), @@ -122,7 +121,6 @@ mod tests { let singletons = [ justfile::entry(), justfile::versions(), - justfile::stable_toolchain_resolver(), justfile::tools(), justfile::helpers(), justfile::impact(), diff --git a/crates/cargo-anvil/templates/ado/steps/setup.yml b/crates/cargo-anvil/templates/ado/steps/setup.yml index 9121d96ba..ad3c6cbcc 100644 --- a/crates/cargo-anvil/templates/ado/steps/setup.yml +++ b/crates/cargo-anvil/templates/ado/steps/setup.yml @@ -30,7 +30,12 @@ steps: Write-Host "anvil: selected stable toolchain '$toolchain'" displayName: anvil setup (resolve stable toolchain) - pwsh: | - $version = (& just _anvil-with-stable rustc --version | Select-Object -Last 1).Split()[1] + $toolchain = (& just _anvil-resolve-stable for-environment | Out-String).Trim() + if ($LASTEXITCODE -ne 0) { + throw 'anvil: failed to select stable toolchain arguments' + } + $toolchainArgs = if ([string]::IsNullOrWhiteSpace($toolchain)) { @() } else { @("+$toolchain") } + $version = (& rustc @toolchainArgs --version | Select-Object -Last 1).Split()[1] Write-Host "##vso[task.setvariable variable=anvil_rustc_version]$version" displayName: anvil setup (capture rustc version) - task: Cache@2 diff --git a/crates/cargo-anvil/templates/anvil/container/Containerfile.dockerignore b/crates/cargo-anvil/templates/anvil/container/Containerfile.dockerignore index 6aca85683..c242cb21a 100644 --- a/crates/cargo-anvil/templates/anvil/container/Containerfile.dockerignore +++ b/crates/cargo-anvil/templates/anvil/container/Containerfile.dockerignore @@ -22,7 +22,6 @@ !justfiles/anvil/*.just !justfiles/anvil/checks/*.just !justfiles/anvil/groups/*.just -!.anvil/resolve-stable-toolchain.ps1 !.anvil/container/* .anvil/container/*/* .anvil/container/customize.sh diff --git a/crates/cargo-anvil/templates/anvil/container/image-id.ps1 b/crates/cargo-anvil/templates/anvil/container/image-id.ps1 index c95dd2c70..3ca2e8599 100644 --- a/crates/cargo-anvil/templates/anvil/container/image-id.ps1 +++ b/crates/cargo-anvil/templates/anvil/container/image-id.ps1 @@ -13,10 +13,7 @@ if ($LASTEXITCODE -ne 0 -or -not $repoRoot) { throw 'anvil-container must run from a Git repository.' } -$inputs = @( - 'Cargo.toml', - '.anvil/resolve-stable-toolchain.ps1' -) +$inputs = @('Cargo.toml') foreach ($toolchainFile in @('rust-toolchain', 'rust-toolchain.toml')) { if (Test-Path -LiteralPath (Join-Path $repoRoot $toolchainFile) -PathType Leaf) { $inputs += $toolchainFile diff --git a/crates/cargo-anvil/templates/anvil/container/image-id.sh b/crates/cargo-anvil/templates/anvil/container/image-id.sh index f399e349b..2dabc3487 100644 --- a/crates/cargo-anvil/templates/anvil/container/image-id.sh +++ b/crates/cargo-anvil/templates/anvil/container/image-id.sh @@ -19,7 +19,7 @@ if [[ ! "$base_image" =~ @sha256:[0-9a-fA-F]{64}$ ]]; then echo 'anvil-container: ANVIL_CONTAINER_BASE_IMAGE must be pinned by sha256 digest (image@sha256:<64 hex characters>).' >&2 exit 1 fi -inputs=(Cargo.toml .anvil/resolve-stable-toolchain.ps1) +inputs=(Cargo.toml) for toolchain_file in rust-toolchain rust-toolchain.toml; do if [[ -f "$repo_root/$toolchain_file" ]]; then inputs+=("$toolchain_file") diff --git a/crates/cargo-anvil/templates/anvil/resolve-stable-toolchain.ps1 b/crates/cargo-anvil/templates/anvil/resolve-stable-toolchain.ps1 deleted file mode 100644 index e38de2673..000000000 --- a/crates/cargo-anvil/templates/anvil/resolve-stable-toolchain.ps1 +++ /dev/null @@ -1,310 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# GENERATED BY cargo-anvil. DO NOT EDIT DIRECTLY. - -param( - [switch] $ValidateWorkspaceMsrv, - [switch] $InstallIfMissing, - [switch] $ForEnvironment, - [switch] $MsrvToolchain, - [switch] $InstallMsrvIfNeeded, - [switch] $EmitToolchainFileOptions, - [switch] $ApplyToolchainFileOptions -) - -$ErrorActionPreference = 'Stop' -Set-StrictMode -Version 3 - -$repoRoot = Split-Path -Parent $PSScriptRoot - -function Get-RootToolchainFilePath { - foreach ($name in @('rust-toolchain', 'rust-toolchain.toml')) { - $path = Join-Path $repoRoot $name - if (Test-Path -LiteralPath $path -PathType Leaf) { - return $path - } - } - return $null -} - -function Read-RootToolchainFile([string] $path) { - $content = Get-Content -LiteralPath $path -Raw - if ([string]::IsNullOrWhiteSpace($content)) { - throw "anvil: root toolchain file '$path' is empty; remove it or declare a toolchain configuration" - } - return $content -} - -function Get-SelectingToolchainFile { - $path = Get-RootToolchainFilePath - if ($null -eq $path) { - return $null - } - - $content = Read-RootToolchainFile $path - if ((Split-Path -Leaf $path) -eq 'rust-toolchain' -and $content.TrimStart() -notmatch '^\[') { - return [pscustomobject]@{ - Path = $path - Value = $content.Trim() - Selector = 'channel' - Source = 'file' - Installable = $false - } - } - - $inToolchain = $false - foreach ($line in $content -split '\r?\n') { - if ($line -match '^\s*\[\s*toolchain\s*\]\s*(?:#.*)?$') { - $inToolchain = $true - continue - } - if ($line -match '^\s*\[') { - $inToolchain = $false - continue - } - if ($inToolchain -and $line -match '^\s*["'']?(channel|path)["'']?\s*=\s*(["''])([^"'']+)\2\s*(?:#.*)?$') { - $value = $Matches[3] - if ($Matches[1] -eq 'path' -and -not [IO.Path]::IsPathRooted($value)) { - $value = [IO.Path]::GetFullPath((Join-Path $repoRoot $value)) - } - return [pscustomobject]@{ - Path = $path - Value = $value - Selector = $Matches[1] - Source = 'file' - Installable = $false - } - } - } - return $null -} - -function Get-ToolchainFileOptions { - $path = Get-RootToolchainFilePath - if ($null -eq $path) { - return $null - } - - $content = Read-RootToolchainFile $path - $section = [regex]::Match( - $content, - '(?ms)^\s*\[\s*toolchain\s*\]\s*(?:#.*)?$(.*?)(?=^\s*\[|\z)' - ) - if (-not $section.Success) { - return $null - } - - $body = $section.Groups[1].Value - $profileMatch = [regex]::Match( - $body, - '(?m)^\s*["'']?profile["'']?\s*=\s*(["''])([^"'']+)\1\s*(?:#.*)?$' - ) - $readArray = { - param([string] $name) - $arrayMatch = [regex]::Match( - $body, - "(?ms)^\s*[`"']?$name[`"']?\s*=\s*\[(.*?)\]" - ) - if (-not $arrayMatch.Success) { - return @() - } - return @( - [regex]::Matches($arrayMatch.Groups[1].Value, '(["''])([^"'']+)\1') | - ForEach-Object { $_.Groups[2].Value } - ) - } - - return [pscustomobject]@{ - Path = $path - Profile = if ($profileMatch.Success) { $profileMatch.Groups[2].Value } else { $null } - Components = @(& $readArray 'components') - Targets = @(& $readArray 'targets') - } -} - -function Get-RootMsrv([switch] $AllowMissing) { - $manifestPath = Join-Path $repoRoot 'Cargo.toml' - if (-not (Test-Path -LiteralPath $manifestPath -PathType Leaf)) { - throw "anvil: Cargo.toml not found at repository root '$repoRoot'" - } - - $values = @{} - $section = '' - foreach ($line in (Get-Content -LiteralPath $manifestPath)) { - if ($line -match '^\s*\[\s*([^\]]+)\s*\]\s*(?:#.*)?$') { - $section = $Matches[1].Trim() - continue - } - if ($section -in @('workspace.package', 'package') -and - $line -match '^\s*["'']?rust-version["'']?\s*=\s*(["''])([^"'']+)\1\s*(?:#.*)?$') { - $values[$section] = $Matches[2] - } - } - - if ($values.ContainsKey('workspace.package')) { - return $values['workspace.package'] - } - if ($values.ContainsKey('package')) { - return $values['package'] - } - - if ($AllowMissing) { - return $null - } - throw 'anvil: no selecting rust-toolchain(.toml) and no root [workspace.package] or [package] rust-version; declare an MSRV or select a toolchain explicitly' -} - -function Assert-WorkspaceMsrvCompatibility { - $manifestPath = Join-Path $repoRoot 'Cargo.toml' - $metadataText = (& cargo metadata --manifest-path $manifestPath --format-version 1 --no-deps 2>&1 | Out-String) - if ($LASTEXITCODE -ne 0) { - throw "anvil: cargo metadata failed while validating workspace MSRVs:`n$metadataText" - } - $metadata = $metadataText | ConvertFrom-Json - $memberIds = [Collections.Generic.HashSet[string]]::new( - [string[]] $metadata.workspace_members, - [StringComparer]::Ordinal - ) - $members = @($metadata.packages | Where-Object { $memberIds.Contains([string] $_.id) }) - $missing = @($members | Where-Object { [string]::IsNullOrWhiteSpace([string] $_.rust_version) } | ForEach-Object name) - if ($missing.Count -gt 0) { - throw "anvil: workspace packages without a resolved rust-version: $($missing -join ', '); declare one or select a toolchain explicitly" - } - - $rootMsrv = Get-RootMsrv - $newer = @( - $members | - Where-Object { [version] ([string] $_.rust_version) -gt [version] $rootMsrv } | - ForEach-Object { "$($_.name) ($($_.rust_version))" } - ) - if ($newer.Count -gt 0) { - throw "anvil: workspace packages require a compiler newer than the root MSRV $rootMsrv`: $($newer -join ', '); raise the root MSRV or select one catalog toolchain explicitly with rust-toolchain.toml" - } -} - -function Assert-CompleteInternalToolchainConfiguration { - if (-not [string]::IsNullOrWhiteSpace($env:ANVIL_MSRV_TOOLCHAIN) -and - [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN) -and - $null -eq $fileSelection) { - throw 'anvil: ANVIL_MSRV_TOOLCHAIN is set without RUSTUP_TOOLCHAIN and no repository toolchain file selects stable checks; set RUSTUP_TOOLCHAIN to the provisioned stable toolchain or unset ANVIL_MSRV_TOOLCHAIN' - } -} - -function Get-MsrvSelection { - $msrv = Get-RootMsrv -AllowMissing - if ([string]::IsNullOrWhiteSpace($msrv)) { - return $null - } - - if (-not [string]::IsNullOrWhiteSpace($env:ANVIL_MSRV_TOOLCHAIN)) { - return [pscustomobject]@{ - Value = $env:ANVIL_MSRV_TOOLCHAIN - Source = 'mapped-msrv' - Installable = $false - Mapped = $true - } - } - return [pscustomobject]@{ Value = $msrv; Source = 'msrv'; Installable = $true; Mapped = $false } -} - -function Apply-ToolchainFileOptions([object] $toolchain) { - if ($toolchain.Source -eq 'file' -or $null -eq $toolchainFileOptions) { - return - } - foreach ($component in $toolchainFileOptions.Components) { - rustup component add --toolchain $toolchain.Value $component - if ($LASTEXITCODE -ne 0) { - Write-Warning "anvil: toolchain-file component '$component' is unavailable for '$($toolchain.Value)'; rustup would skip an unavailable file component" - } - } - foreach ($target in $toolchainFileOptions.Targets) { - rustup target add --toolchain $toolchain.Value $target - if ($LASTEXITCODE -ne 0) { - Write-Warning "anvil: toolchain-file target '$target' is unavailable for '$($toolchain.Value)'; rustup would skip an unavailable file target" - } - } -} - -function Install-Toolchain([object] $toolchain, [string] $label) { - if ($toolchain.Installable) { - $installed = rustup toolchain list - if ($LASTEXITCODE -ne 0) { - throw 'anvil: rustup toolchain list failed' - } - if ($installed -notmatch "(?m)^$([regex]::Escape($toolchain.Value))(?:-|$)") { - $profile = if ($null -ne $toolchainFileOptions -and $toolchainFileOptions.Profile) { - $toolchainFileOptions.Profile - } else { - 'minimal' - } - Write-Host "anvil: installing $label toolchain '$($toolchain.Value)'" - rustup toolchain install $toolchain.Value --profile $profile - if ($LASTEXITCODE -ne 0) { - throw "anvil: failed to install $label toolchain '$($toolchain.Value)'" - } - } - } - Apply-ToolchainFileOptions $toolchain -} - -$fileSelection = Get-SelectingToolchainFile -$toolchainFileOptions = Get-ToolchainFileOptions -if ($EmitToolchainFileOptions) { - if ($null -ne $toolchainFileOptions) { - $toolchainFileOptions | ConvertTo-Json -Compress - } - exit 0 -} -if ($ValidateWorkspaceMsrv) { - Assert-CompleteInternalToolchainConfiguration - $skipWorkspaceMsrvValidation = -not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN) - if (-not $skipWorkspaceMsrvValidation -and $null -eq $fileSelection) { - Assert-WorkspaceMsrvCompatibility - } - exit 0 -} - -if ($MsrvToolchain -or $InstallMsrvIfNeeded) { - $msrvSelection = Get-MsrvSelection - if ($null -eq $msrvSelection) { - exit 0 - } - if ($InstallMsrvIfNeeded) { - if ($msrvSelection.Mapped) { - & cargo "+$($msrvSelection.Value)" --version *> $null - if ($LASTEXITCODE -ne 0) { - throw "anvil: mapped MSRV toolchain '$($msrvSelection.Value)' is unavailable; provision ANVIL_MSRV_TOOLCHAIN or unset it to let Anvil install the declared public MSRV" - } - } - Install-Toolchain $msrvSelection 'MSRV' - } else { - $msrvSelection.Value - } - exit 0 -} - -Assert-CompleteInternalToolchainConfiguration -$selection = if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { - [pscustomobject]@{ Value = $env:RUSTUP_TOOLCHAIN; Source = 'environment'; Installable = $false } -} elseif ($null -ne $fileSelection) { - $fileSelection -} else { - [pscustomobject]@{ Value = Get-RootMsrv; Source = 'msrv'; Installable = $true } -} - -if ($ApplyToolchainFileOptions) { - Apply-ToolchainFileOptions $selection - exit 0 -} - -if ($InstallIfMissing) { - Install-Toolchain $selection 'stable' - exit 0 -} - -if ($ForEnvironment -and $selection.Source -eq 'file') { - '' -} else { - $selection.Value -} -exit 0 diff --git a/crates/cargo-anvil/templates/github/setup-action.yml b/crates/cargo-anvil/templates/github/setup-action.yml index 20c55562f..cf3ad0863 100644 --- a/crates/cargo-anvil/templates/github/setup-action.yml +++ b/crates/cargo-anvil/templates/github/setup-action.yml @@ -104,7 +104,12 @@ runs: - id: rustc-version shell: pwsh -NoProfile -Command ". '{0}'" run: | - $version = (& just _anvil-with-stable rustc --version | Select-Object -Last 1).Split()[1] + $toolchain = (& just _anvil-resolve-stable for-environment | Out-String).Trim() + if ($LASTEXITCODE -ne 0) { + throw 'anvil: failed to select stable toolchain arguments' + } + $toolchainArgs = if ([string]::IsNullOrWhiteSpace($toolchain)) { @() } else { @("+$toolchain") } + $version = (& rustc @toolchainArgs --version | Select-Object -Last 1).Split()[1] "version=$version" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append - name: Restore cargo cache id: cargo-cache diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/aprz.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/aprz.just index 897ec37c1..90c5b428f 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/aprz.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/aprz.just @@ -44,7 +44,7 @@ anvil-aprz: anvil-aprz-validate-prereqs Write-Warning 'To fix: run `gh auth login` (recommended), or set $env:GITHUB_TOKEN to a GitHub token, then re-run.' } } - & "{{ just_executable() }}" _anvil-with-stable cargo aprz deps --error-if-high-risk --console appraisal + & cargo {{_anvil_stable_toolchain_args}} aprz deps --error-if-high-risk --console appraisal if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-aprz` recipe. diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/audit.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/audit.just index d0aef3197..a43c5de01 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/audit.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/audit.just @@ -12,7 +12,7 @@ [script("pwsh", "-NoProfile")] anvil-audit: anvil-audit-validate-prereqs $ErrorActionPreference = 'Stop' - & "{{ just_executable() }}" _anvil-with-stable cargo audit + & cargo {{_anvil_stable_toolchain_args}} audit if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-audit` recipe. diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/bench.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/bench.just index 7f8ca006f..0158612e4 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/bench.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/bench.just @@ -14,7 +14,7 @@ anvil-bench: anvil-bench-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include affected) if ($include -eq '--skip') { Write-Host 'anvil-bench: no affected packages; skipping'; exit 0 } - & "{{ just_executable() }}" _anvil-with-stable cargo bench @(if ($include) { -split $include } else { '--workspace' }) --all-features --no-run + & cargo {{_anvil_stable_toolchain_args}} bench @(if ($include) { -split $include } else { '--workspace' }) --all-features --no-run if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # bench uses the cargo built-in in compile-only mode; no extra tool install diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/bolero.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/bolero.just index 306bb9eea..8ba438b39 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/bolero.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/bolero.just @@ -54,7 +54,7 @@ anvil-bolero: anvil-bolero-validate-prereqs anvil-impact $packageNames.Add(($pkgArgs[++$i] -split '@', 2)[0]) } } else { - $metadataJson = & "{{ just_executable() }}" _anvil-with-stable cargo metadata --no-deps --format-version 1 + $metadataJson = & cargo {{_anvil_stable_toolchain_args}} metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-bolero: cargo metadata failed' exit $LASTEXITCODE diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/cargo-hack.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/cargo-hack.just index c42680435..0377fb4da 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/cargo-hack.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/cargo-hack.just @@ -16,7 +16,7 @@ anvil-cargo-hack: anvil-cargo-hack-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include required) if ($include -eq '--skip') { Write-Host 'anvil-cargo-hack: no affected packages; skipping'; exit 0 } - & "{{ just_executable() }}" _anvil-with-stable cargo hack @(if ($include) { -split $include } else { '--workspace' }) --feature-powerset --depth 2 check + & cargo {{_anvil_stable_toolchain_args}} hack @(if ($include) { -split $include } else { '--workspace' }) --feature-powerset --depth 2 check if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-cargo-hack` recipe. diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/cargo-sort.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/cargo-sort.just index 8de8ad7a9..dcf5d192d 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/cargo-sort.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/cargo-sort.just @@ -25,7 +25,7 @@ anvil-cargo-sort: anvil-cargo-sort-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include modified) if ($include -eq '--skip') { Write-Host 'anvil-cargo-sort: no modified packages; skipping'; exit 0 } - & "{{ just_executable() }}" _anvil-with-stable cargo sort --workspace --grouped --check --check-format + & cargo {{_anvil_stable_toolchain_args}} sort --workspace --grouped --check --check-format if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-cargo-sort` recipe. diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/clippy.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/clippy.just index 5b9a8eb57..a0354f2c4 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/clippy.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/clippy.just @@ -19,7 +19,7 @@ anvil-clippy: anvil-clippy-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include affected) if ($include -eq '--skip') { Write-Host 'anvil-clippy: no affected packages; skipping'; exit 0 } - & "{{ just_executable() }}" _anvil-with-stable cargo clippy @(if ($include) { -split $include } else { '--workspace' }) --all-targets --all-features --locked "--" '-D' 'warnings' + & cargo {{_anvil_stable_toolchain_args}} clippy @(if ($include) { -split $include } else { '--workspace' }) --all-targets --all-features --locked "--" '-D' 'warnings' if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-clippy` recipe. diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/deny.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/deny.just index 17d098305..c798669b5 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/deny.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/deny.just @@ -14,7 +14,7 @@ [script("pwsh", "-NoProfile")] anvil-deny: anvil-deny-validate-prereqs $ErrorActionPreference = 'Stop' - & "{{ just_executable() }}" _anvil-with-stable cargo deny check + & cargo {{_anvil_stable_toolchain_args}} deny check if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-deny` recipe. diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/doc-build.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/doc-build.just index b29336163..c02ad20bc 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/doc-build.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/doc-build.just @@ -17,7 +17,7 @@ anvil-doc-build: anvil-doc-build-validate-prereqs anvil-impact $include = (& "{{ just_executable() }}" _anvil-impact-include required) if ($include -eq '--skip') { Write-Host 'anvil-doc-build: no affected packages; skipping'; exit 0 } $env:RUSTDOCFLAGS = '-D warnings' - & "{{ just_executable() }}" _anvil-with-stable cargo doc @(if ($include) { -split $include } else { '--workspace' }) --all-features --no-deps + & cargo {{_anvil_stable_toolchain_args}} doc @(if ($include) { -split $include } else { '--workspace' }) --all-features --no-deps if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # doc-build, examples and doc-test are pure cargo built-ins; the rust diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/doc-test.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/doc-test.just index 1480f48d5..97f959768 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/doc-test.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/doc-test.just @@ -20,9 +20,9 @@ anvil-doc-test: anvil-doc-test-validate-prereqs anvil-impact $include = (& "{{ just_executable() }}" _anvil-impact-include affected) if ($include -eq '--skip') { Write-Host 'anvil-doc-test: no affected packages; skipping'; exit 0 } $pkg = @(if ($include) { -split $include } else { '--workspace' }) - & "{{ just_executable() }}" _anvil-with-stable cargo test --doc @pkg --all-features --locked + & cargo {{_anvil_stable_toolchain_args}} test --doc @pkg --all-features --locked if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - & "{{ just_executable() }}" _anvil-with-stable cargo test --doc @pkg --locked + & cargo {{_anvil_stable_toolchain_args}} test --doc @pkg --locked if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-doc-test` recipe. diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/ensure-no-cyclic-deps.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/ensure-no-cyclic-deps.just index e74beab7c..57f360170 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/ensure-no-cyclic-deps.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/ensure-no-cyclic-deps.just @@ -14,7 +14,7 @@ anvil-ensure-no-cyclic-deps: anvil-ensure-no-cyclic-deps-validate-prereqs anvil- $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include modified) if ($include -eq '--skip') { Write-Host 'anvil-ensure-no-cyclic-deps: no modified packages; skipping'; exit 0 } - & "{{ just_executable() }}" _anvil-with-stable cargo ensure-no-cyclic-deps + & cargo {{_anvil_stable_toolchain_args}} ensure-no-cyclic-deps if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-ensure-no-cyclic-deps` recipe. diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/ensure-no-default-features.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/ensure-no-default-features.just index 9def94fa3..cee4d24d5 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/ensure-no-default-features.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/ensure-no-default-features.just @@ -14,7 +14,7 @@ anvil-ensure-no-default-features: anvil-ensure-no-default-features-validate-prer $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include modified) if ($include -eq '--skip') { Write-Host 'anvil-ensure-no-default-features: no modified packages; skipping'; exit 0 } - & "{{ just_executable() }}" _anvil-with-stable cargo ensure-no-default-features + & cargo {{_anvil_stable_toolchain_args}} ensure-no-default-features if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-ensure-no-default-features` recipe. diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/examples.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/examples.just index 47ab115f9..de3b11ef5 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/examples.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/examples.just @@ -14,7 +14,7 @@ anvil-examples: anvil-examples-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include affected) if ($include -eq '--skip') { Write-Host 'anvil-examples: no affected packages; skipping'; exit 0 } - & "{{ just_executable() }}" _anvil-with-stable cargo build @(if ($include) { -split $include } else { '--workspace' }) --examples --all-features --locked + & cargo {{_anvil_stable_toolchain_args}} build @(if ($include) { -split $include } else { '--workspace' }) --examples --all-features --locked if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-examples` recipe. diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/external-types.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/external-types.just index 87287a42a..d659a3d4d 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/external-types.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/external-types.just @@ -31,7 +31,7 @@ anvil-external-types: anvil-external-types-validate-prereqs anvil-impact $pkg = @(if ($include) { -split $include } else { '--workspace' }) # Build pkg-name -> manifest-path map, restricted to library crates. $libPkgs = @{} - $metadataJson = & "{{ just_executable() }}" _anvil-with-stable cargo metadata --no-deps --format-version 1 + $metadataJson = & cargo {{_anvil_stable_toolchain_args}} metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-external-types: cargo metadata failed' exit $LASTEXITCODE diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/license-headers.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/license-headers.just index 7f489f48a..fb794c32e 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/license-headers.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/license-headers.just @@ -14,7 +14,7 @@ anvil-license-headers: anvil-license-headers-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include modified) if ($include -eq '--skip') { Write-Host 'anvil-license-headers: no modified packages; skipping'; exit 0 } - & "{{ just_executable() }}" _anvil-with-stable cargo heather + & cargo {{_anvil_stable_toolchain_args}} heather if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-license-headers` recipe. diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/llvm-cov.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/llvm-cov.just index fbace94e0..1096d2743 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/llvm-cov.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/llvm-cov.just @@ -37,7 +37,7 @@ anvil-llvm-cov: anvil-llvm-cov-validate-prereqs anvil-impact # never opts a changed package out of tests. $testOnlyPkg = [System.Collections.Generic.List[string]]::new() if ($pkg -contains '--package') { - $metadataJson = & "{{ just_executable() }}" _anvil-with-stable cargo metadata --no-deps --format-version 1 + $metadataJson = & cargo {{_anvil_stable_toolchain_args}} metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-llvm-cov: cargo metadata failed' exit $LASTEXITCODE @@ -168,7 +168,7 @@ anvil-llvm-cov: anvil-llvm-cov-validate-prereqs anvil-impact } $gateArgs.Add($pkg[$i]) } - & "{{ just_executable() }}" _anvil-with-stable cargo coverage-gate @gateArgs --lcov target/coverage/lcov-all-features.info --lcov target/coverage/lcov-no-default.info + & cargo {{_anvil_stable_toolchain_args}} coverage-gate @gateArgs --lcov target/coverage/lcov-all-features.info --lcov target/coverage/lcov-no-default.info if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # --- pr-test members (shared with scheduled-test) --- diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/loom.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/loom.just index 4cc8441b3..5ddd82605 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/loom.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/loom.just @@ -55,7 +55,7 @@ anvil-loom: anvil-loom-validate-prereqs anvil-impact Write-Host 'anvil-loom: no affected packages; skipping' exit 0 } - $meta = & "{{ just_executable() }}" _anvil-with-stable cargo metadata --no-deps --format-version 1 | ConvertFrom-Json + $meta = & cargo {{_anvil_stable_toolchain_args}} metadata --no-deps --format-version 1 | ConvertFrom-Json if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Optional impact-scoping: recover bare package names from the @@ -108,7 +108,7 @@ anvil-loom: anvil-loom-validate-prereqs anvil-impact $env:RUSTFLAGS = "--cfg loom $($env:RUSTFLAGS)".Trim() foreach ($lt in $loomTargets) { Write-Host "anvil-loom: $($lt.pkg) :: $($lt.target)" - & "{{ just_executable() }}" _anvil-with-stable cargo test -p $lt.pkg --release --all-features --locked --test $lt.target -- --test-threads=1 + & cargo {{_anvil_stable_toolchain_args}} test -p $lt.pkg --release --all-features --locked --test $lt.target -- --test-threads=1 if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } } diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/mutants-diff.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/mutants-diff.just index e3c3c4234..565edeb29 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/mutants-diff.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/mutants-diff.just @@ -43,7 +43,7 @@ anvil-mutants-diff: anvil-mutants-diff-validate-prereqs anvil-impact $diff_path = Join-Path $tmp_dir 'anvil-mutants-diff.diff' git diff "$base..HEAD" --output=$diff_path if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - & "{{ just_executable() }}" _anvil-with-stable cargo mutants --in-diff $diff_path --no-shuffle --jobs 0 + & cargo {{_anvil_stable_toolchain_args}} mutants --in-diff $diff_path --no-shuffle --jobs 0 if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # --- pr-mutants members --- diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/mutants-full.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/mutants-full.just index 4009e7577..e3e32a16b 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/mutants-full.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/mutants-full.just @@ -17,7 +17,7 @@ anvil-mutants-full: anvil-mutants-full-validate-prereqs Write-Host 'anvil-mutants-full: aarch64-pc-windows-msvc -- cargo-mutants does not build here (winapi); skipping' exit 0 } - & "{{ just_executable() }}" _anvil-with-stable cargo mutants --workspace --no-shuffle --jobs 0 + & cargo {{_anvil_stable_toolchain_args}} mutants --workspace --no-shuffle --jobs 0 if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # --- scheduled-exhaustive members (mutants-full reuses cargo-mutants; diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/readme-check.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/readme-check.just index 615fa4430..aea186413 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/readme-check.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/readme-check.just @@ -47,7 +47,7 @@ anvil-readme-check: anvil-readme-check-validate-prereqs # Unscoped: a change to the workspace-level README template is a repo-level # input cargo-delta cannot map to a package, so this check always runs the # full library set rather than an impact subset. - $metadataJson = & "{{ just_executable() }}" _anvil-with-stable cargo metadata --no-deps --format-version 1 + $metadataJson = & cargo {{_anvil_stable_toolchain_args}} metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-readme-check: cargo metadata failed' exit $LASTEXITCODE @@ -89,7 +89,7 @@ anvil-readme-check: anvil-readme-check-validate-prereqs } $args = @('doc2readme', '--check') if ($relTemplate) { $args += @('--template', $relTemplate) } - & "{{ just_executable() }}" _anvil-with-stable cargo @args + & cargo {{_anvil_stable_toolchain_args}} @args if ($LASTEXITCODE -ne 0) { $hadFailure = $true } } finally { Pop-Location diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/semver-check.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/semver-check.just index 7cbac0ac6..6c68adfb7 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/semver-check.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/semver-check.just @@ -65,7 +65,7 @@ anvil-semver-check: anvil-semver-check-validate-prereqs anvil-impact # we got here via impact-scoping (cloud workflows) or full-workspace # fallback (local). $libPkgs = @{} - $metadataJson = & "{{ just_executable() }}" _anvil-with-stable cargo metadata --no-deps --format-version 1 + $metadataJson = & cargo {{_anvil_stable_toolchain_args}} metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-semver-check: cargo metadata failed' exit $LASTEXITCODE @@ -142,7 +142,7 @@ anvil-semver-check: anvil-semver-check-validate-prereqs anvil-impact } Write-Host "anvil-semver-check: $p (baseline $base)" - $outputLines = @(& "{{ just_executable() }}" _anvil-with-stable cargo semver-checks --package $p --baseline-rev $base 2>&1) + $outputLines = @(& cargo {{_anvil_stable_toolchain_args}} semver-checks --package $p --baseline-rev $base 2>&1) $semverExit = $LASTEXITCODE $output = ($outputLines | Out-String) if ($semverExit -eq 100) { diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/spellcheck.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/spellcheck.just index 805d43cc0..cdf2aef95 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/spellcheck.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/spellcheck.just @@ -42,9 +42,9 @@ anvil-spellcheck: anvil-spellcheck-validate-prereqs # ignores user-curated dictionaries (`extra_dictionaries`, custom # hunspell langs, etc.). if (Test-Path 'spellcheck.toml') { - & "{{ just_executable() }}" _anvil-with-stable cargo spellcheck --cfg spellcheck.toml check --code 1 + & cargo {{_anvil_stable_toolchain_args}} spellcheck --cfg spellcheck.toml check --code 1 } else { - & "{{ just_executable() }}" _anvil-with-stable cargo spellcheck check --code 1 + & cargo {{_anvil_stable_toolchain_args}} spellcheck check --code 1 } if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } diff --git a/crates/cargo-anvil/templates/justfiles/anvil/impact.just b/crates/cargo-anvil/templates/justfiles/anvil/impact.just index 56223c8aa..4f05394c8 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/impact.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/impact.just @@ -241,9 +241,13 @@ _anvil-impact-snapshot: # selections are already valid rustup overrides. For a selecting # toolchain file, ask rustup for the concrete active identifier so # `path` selections are not mistaken for named toolchains. - $baselineToolchain = (& "{{ just_executable() }}" _anvil-resolve-stable for-environment | Out-String).Trim() - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - if ([string]::IsNullOrWhiteSpace($baselineToolchain)) { + $stableArgs = {{_anvil_stable_toolchain_args}} + $baselineToolchain = if ($stableArgs.Count -eq 1) { + $stableArgs[0].Substring(1) + } else { + $null + } + if ($null -eq $baselineToolchain) { if (-not (Get-Command rustup -ErrorAction SilentlyContinue)) { Write-Error 'anvil-impact: rustup is required to resolve the active repository toolchain for the baseline snapshot' exit 1 @@ -318,7 +322,7 @@ _anvil-impact-snapshot: Write-Host 'anvil-impact: current snapshot up to date' } else { Write-Host 'anvil-impact: snapshotting working tree' - $curSnap = & "{{ just_executable() }}" _anvil-with-stable cargo delta @deltaArgs snapshot + $curSnap = & cargo {{_anvil_stable_toolchain_args}} delta @deltaArgs snapshot if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-impact: cargo delta snapshot (current) failed'; exit 1 } Set-Content -LiteralPath $currentJson -Value $curSnap -Encoding UTF8 Set-Content -LiteralPath $currentKeyFile -Value $currentState -Encoding UTF8 -NoNewline @@ -436,7 +440,7 @@ anvil-impact: _anvil-impact-snapshot # early), so default to an empty object so impact.json always exists -- # the freshness check above keys on its presence, and a missing file # would force a recompute on every invocation. - $impactOut = (& "{{ just_executable() }}" _anvil-with-stable cargo delta @deltaArgs impact --baseline $baselineJson --current $currentJson --format json | Out-String) + $impactOut = (& cargo {{_anvil_stable_toolchain_args}} delta @deltaArgs impact --baseline $baselineJson --current $currentJson --format json | Out-String) if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-impact: cargo delta impact failed'; exit 1 } if ([string]::IsNullOrWhiteSpace($impactOut)) { $impactOut = '{}' } Set-Content -LiteralPath $impactJson -Value $impactOut.Trim() -Encoding UTF8 @@ -513,7 +517,7 @@ _anvil-impact-format tier impactJson: # nested workspaces cargo-delta can instead emit a manifest directory # leaf; accept any reported identifier only when every matching namespace # resolves to the *same* workspace package. - $metadataJson = & "{{ just_executable() }}" _anvil-with-stable cargo metadata --no-deps --format-version 1 + $metadataJson = & cargo {{_anvil_stable_toolchain_args}} metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-impact: cargo metadata failed' exit $LASTEXITCODE diff --git a/crates/cargo-anvil/templates/justfiles/anvil/tools.just b/crates/cargo-anvil/templates/justfiles/anvil/tools.just index 218dc28fd..41b9336f1 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/tools.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/tools.just @@ -177,37 +177,318 @@ anvil-tool-pwsh-validate-prereqs: [script("pwsh", "-NoProfile")] _anvil-resolve-stable action="resolve": $ErrorActionPreference = 'Stop' - $resolverArgs = @(switch ('{{action}}') { - 'resolve' { @() } - 'for-environment' { @('-ForEnvironment') } - 'install' { @('-InstallIfMissing') } - 'apply-file-options' { @('-ApplyToolchainFileOptions') } - 'validate-workspace-msrv' { @('-ValidateWorkspaceMsrv') } - 'msrv' { @('-MsrvToolchain') } - 'install-msrv' { @('-InstallMsrvIfNeeded') } - default { - Write-Error "_anvil-resolve-stable: unknown action '{{action}}'" - exit 2 - } - }) - & pwsh -NoProfile -File '.anvil/resolve-stable-toolchain.ps1' @resolverArgs - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - -# Run one command under the selected stable compiler. A repository toolchain -# file yields no override so rustup processes its complete configuration. -[script("pwsh", "-NoProfile")] -_anvil-with-stable command *args: - $ErrorActionPreference = 'Stop' - $toolchain = (& "{{ just_executable() }}" _anvil-resolve-stable for-environment | Out-String).Trim() - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - if ([string]::IsNullOrWhiteSpace($toolchain)) { - Remove-Item Env:\RUSTUP_TOOLCHAIN -ErrorAction SilentlyContinue + Set-StrictMode -Version 3 + + $action = '{{action}}' + $validActions = @( + 'resolve', + 'for-environment', + 'install', + 'apply-file-options', + 'validate-workspace-msrv', + 'msrv', + 'install-msrv', + 'emit-file-options' + ) + if ($action -notin $validActions) { + Write-Error "_anvil-resolve-stable: unknown action '$action'" + exit 2 + } + + $repoRoot = '{{justfile_directory()}}' + + function Get-RootToolchainFilePath { + foreach ($name in @('rust-toolchain', 'rust-toolchain.toml')) { + $path = Join-Path $repoRoot $name + if (Test-Path -LiteralPath $path -PathType Leaf) { + return $path + } + } + return $null + } + + function Read-RootToolchainFile([string] $path) { + $content = Get-Content -LiteralPath $path -Raw + if ([string]::IsNullOrWhiteSpace($content)) { + throw "anvil: root toolchain file '$path' is empty; remove it or declare a toolchain configuration" + } + return $content + } + + function Get-SelectingToolchainFile { + $path = Get-RootToolchainFilePath + if ($null -eq $path) { + return $null + } + + $content = Read-RootToolchainFile $path + if ((Split-Path -Leaf $path) -eq 'rust-toolchain' -and $content.TrimStart() -notmatch '^\[') { + return [pscustomobject]@{ + Path = $path + Value = $content.Trim() + Selector = 'channel' + Source = 'file' + Installable = $false + } + } + + $inToolchain = $false + foreach ($line in $content -split '\r?\n') { + if ($line -match '^\s*\[\s*toolchain\s*\]\s*(?:#.*)?$') { + $inToolchain = $true + continue + } + if ($line -match '^\s*\[') { + $inToolchain = $false + continue + } + if ($inToolchain -and $line -match '^\s*["'']?(channel|path)["'']?\s*=\s*(["''])([^"'']+)\2\s*(?:#.*)?$') { + $value = $Matches[3] + if ($Matches[1] -eq 'path' -and -not [IO.Path]::IsPathRooted($value)) { + $value = [IO.Path]::GetFullPath((Join-Path $repoRoot $value)) + } + return [pscustomobject]@{ + Path = $path + Value = $value + Selector = $Matches[1] + Source = 'file' + Installable = $false + } + } + } + return $null + } + + function Get-ToolchainFileOptions { + $path = Get-RootToolchainFilePath + if ($null -eq $path) { + return $null + } + + $content = Read-RootToolchainFile $path + $section = [regex]::Match( + $content, + '(?ms)^\s*\[\s*toolchain\s*\]\s*(?:#.*)?$(.*?)(?=^\s*\[|\z)' + ) + if (-not $section.Success) { + return $null + } + + $body = $section.Groups[1].Value + $profileMatch = [regex]::Match( + $body, + '(?m)^\s*["'']?profile["'']?\s*=\s*(["''])([^"'']+)\1\s*(?:#.*)?$' + ) + $readArray = { + param([string] $name) + $arrayMatch = [regex]::Match( + $body, + "(?ms)^\s*[`"']?$name[`"']?\s*=\s*\[(.*?)\]" + ) + if (-not $arrayMatch.Success) { + return @() + } + return @( + [regex]::Matches($arrayMatch.Groups[1].Value, '(["''])([^"'']+)\1') | + ForEach-Object { $_.Groups[2].Value } + ) + } + + return [pscustomobject]@{ + Path = $path + Profile = if ($profileMatch.Success) { $profileMatch.Groups[2].Value } else { $null } + Components = @(& $readArray 'components') + Targets = @(& $readArray 'targets') + } + } + + function Get-RootMsrv([switch] $AllowMissing) { + $manifestPath = Join-Path $repoRoot 'Cargo.toml' + if (-not (Test-Path -LiteralPath $manifestPath -PathType Leaf)) { + throw "anvil: Cargo.toml not found at repository root '$repoRoot'" + } + + $values = @{} + $section = '' + foreach ($line in (Get-Content -LiteralPath $manifestPath)) { + if ($line -match '^\s*\[\s*([^\]]+)\s*\]\s*(?:#.*)?$') { + $section = $Matches[1].Trim() + continue + } + if ($section -in @('workspace.package', 'package') -and + $line -match '^\s*["'']?rust-version["'']?\s*=\s*(["''])([^"'']+)\1\s*(?:#.*)?$') { + $values[$section] = $Matches[2] + } + } + + if ($values.ContainsKey('workspace.package')) { + return $values['workspace.package'] + } + if ($values.ContainsKey('package')) { + return $values['package'] + } + + if ($AllowMissing) { + return $null + } + throw 'anvil: no selecting rust-toolchain(.toml) and no root [workspace.package] or [package] rust-version; declare an MSRV or select a toolchain explicitly' + } + + function Assert-WorkspaceMsrvCompatibility { + $manifestPath = Join-Path $repoRoot 'Cargo.toml' + $metadataText = (& cargo metadata --manifest-path $manifestPath --format-version 1 --no-deps 2>&1 | Out-String) + if ($LASTEXITCODE -ne 0) { + throw "anvil: cargo metadata failed while validating workspace MSRVs:`n$metadataText" + } + $metadata = $metadataText | ConvertFrom-Json + $memberIds = [Collections.Generic.HashSet[string]]::new( + [string[]] $metadata.workspace_members, + [StringComparer]::Ordinal + ) + $members = @($metadata.packages | Where-Object { $memberIds.Contains([string] $_.id) }) + $missing = @($members | Where-Object { [string]::IsNullOrWhiteSpace([string] $_.rust_version) } | ForEach-Object name) + if ($missing.Count -gt 0) { + throw "anvil: workspace packages without a resolved rust-version: $($missing -join ', '); declare one or select a toolchain explicitly" + } + + $rootMsrv = Get-RootMsrv + $newer = @( + $members | + Where-Object { [version] ([string] $_.rust_version) -gt [version] $rootMsrv } | + ForEach-Object { "$($_.name) ($($_.rust_version))" } + ) + if ($newer.Count -gt 0) { + throw "anvil: workspace packages require a compiler newer than the root MSRV $rootMsrv`: $($newer -join ', '); raise the root MSRV or select one catalog toolchain explicitly with rust-toolchain.toml" + } + } + + function Assert-CompleteInternalToolchainConfiguration { + if (-not [string]::IsNullOrWhiteSpace($env:ANVIL_MSRV_TOOLCHAIN) -and + [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN) -and + $null -eq $fileSelection) { + throw 'anvil: ANVIL_MSRV_TOOLCHAIN is set without RUSTUP_TOOLCHAIN and no repository toolchain file selects stable checks; set RUSTUP_TOOLCHAIN to the provisioned stable toolchain or unset ANVIL_MSRV_TOOLCHAIN' + } + } + + function Get-MsrvSelection { + $msrv = Get-RootMsrv -AllowMissing + if ([string]::IsNullOrWhiteSpace($msrv)) { + return $null + } + + if (-not [string]::IsNullOrWhiteSpace($env:ANVIL_MSRV_TOOLCHAIN)) { + return [pscustomobject]@{ + Value = $env:ANVIL_MSRV_TOOLCHAIN + Source = 'mapped-msrv' + Installable = $false + Mapped = $true + } + } + return [pscustomobject]@{ Value = $msrv; Source = 'msrv'; Installable = $true; Mapped = $false } + } + + function Apply-ToolchainFileOptions([object] $toolchain) { + if ($toolchain.Source -eq 'file' -or $null -eq $toolchainFileOptions) { + return + } + foreach ($component in $toolchainFileOptions.Components) { + rustup component add --toolchain $toolchain.Value $component + if ($LASTEXITCODE -ne 0) { + Write-Warning "anvil: toolchain-file component '$component' is unavailable for '$($toolchain.Value)'; rustup would skip an unavailable file component" + } + } + foreach ($target in $toolchainFileOptions.Targets) { + rustup target add --toolchain $toolchain.Value $target + if ($LASTEXITCODE -ne 0) { + Write-Warning "anvil: toolchain-file target '$target' is unavailable for '$($toolchain.Value)'; rustup would skip an unavailable file target" + } + } + } + + function Install-Toolchain([object] $toolchain, [string] $label) { + if ($toolchain.Installable) { + $installed = rustup toolchain list + if ($LASTEXITCODE -ne 0) { + throw 'anvil: rustup toolchain list failed' + } + if ($installed -notmatch "(?m)^$([regex]::Escape($toolchain.Value))(?:-|$)") { + $profile = if ($null -ne $toolchainFileOptions -and $toolchainFileOptions.Profile) { + $toolchainFileOptions.Profile + } else { + 'minimal' + } + Write-Host "anvil: installing $label toolchain '$($toolchain.Value)'" + rustup toolchain install $toolchain.Value --profile $profile + if ($LASTEXITCODE -ne 0) { + throw "anvil: failed to install $label toolchain '$($toolchain.Value)'" + } + } + } + Apply-ToolchainFileOptions $toolchain + } + + $fileSelection = Get-SelectingToolchainFile + $toolchainFileOptions = Get-ToolchainFileOptions + + if ($action -eq 'emit-file-options') { + if ($null -ne $toolchainFileOptions) { + $toolchainFileOptions | ConvertTo-Json -Compress + } + exit 0 + } + + if ($action -eq 'validate-workspace-msrv') { + Assert-CompleteInternalToolchainConfiguration + $skipWorkspaceMsrvValidation = -not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN) + if (-not $skipWorkspaceMsrvValidation -and $null -eq $fileSelection) { + Assert-WorkspaceMsrvCompatibility + } + exit 0 + } + + if ($action -in @('msrv', 'install-msrv')) { + $msrvSelection = Get-MsrvSelection + if ($null -eq $msrvSelection) { + exit 0 + } + if ($action -eq 'install-msrv') { + if ($msrvSelection.Mapped) { + & cargo "+$($msrvSelection.Value)" --version *> $null + if ($LASTEXITCODE -ne 0) { + throw "anvil: mapped MSRV toolchain '$($msrvSelection.Value)' is unavailable; provision ANVIL_MSRV_TOOLCHAIN or unset it to let Anvil install the declared public MSRV" + } + } + Install-Toolchain $msrvSelection 'MSRV' + } else { + $msrvSelection.Value + } + exit 0 + } + + Assert-CompleteInternalToolchainConfiguration + $selection = if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { + [pscustomobject]@{ Value = $env:RUSTUP_TOOLCHAIN; Source = 'environment'; Installable = $false } + } elseif ($null -ne $fileSelection) { + $fileSelection + } else { + [pscustomobject]@{ Value = Get-RootMsrv; Source = 'msrv'; Installable = $true } + } + + if ($action -eq 'apply-file-options') { + Apply-ToolchainFileOptions $selection + exit 0 + } + + if ($action -eq 'install') { + Install-Toolchain $selection 'stable' + exit 0 + } + + if ($action -eq 'for-environment' -and $selection.Source -eq 'file') { + '' } else { - $env:RUSTUP_TOOLCHAIN = $toolchain + $selection.Value } - Set-Location '{{invocation_directory_native()}}' - & '{{command}}' {{args}} - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # _install-tool: install a cargo subcommand at the catalog version, # or no-op if it is already installed at or above that version. The @@ -239,7 +520,7 @@ _install-tool name version installer source_prereq="": # cached binaries and fail with "binary already exists in destination". $installed = $null $pattern = '^' + [regex]::Escape($name) + ' v(\S+):' - foreach ($line in (& "{{ just_executable() }}" _anvil-with-stable cargo install --list 2>$null)) { + foreach ($line in (& cargo {{_anvil_stable_toolchain_args}} install --list 2>$null)) { if ($line -match $pattern) { $installed = $Matches[1]; break } } if ($installed) { @@ -257,7 +538,7 @@ _install-tool name version installer source_prereq="": if ($installer -eq 'binstall') { if (-not (Get-Command cargo-binstall -ErrorAction SilentlyContinue)) { Write-Host ' Bootstrapping cargo-binstall' - & "{{ just_executable() }}" _anvil-with-stable cargo install --locked cargo-binstall + & cargo {{_anvil_stable_toolchain_args}} install --locked cargo-binstall if ($LASTEXITCODE -ne 0) { Write-Error 'cargo-binstall bootstrap failed' exit $LASTEXITCODE @@ -271,7 +552,7 @@ _install-tool name version installer source_prereq="": $binstallArgs += @('--disable-strategies', 'compile') } $binstallArgs += @($name, '--version', "=$version") - & "{{ just_executable() }}" _anvil-with-stable cargo @binstallArgs + & cargo {{_anvil_stable_toolchain_args}} @binstallArgs if ($LASTEXITCODE -eq 0) { exit 0 } Write-Host ' binstall failed; falling back to cargo install' -ForegroundColor Yellow } @@ -279,7 +560,7 @@ _install-tool name version installer source_prereq="": & "{{just_executable()}}" $sourcePrereq if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } } - & "{{ just_executable() }}" _anvil-with-stable cargo install --locked $name --version "=$version" + & cargo {{_anvil_stable_toolchain_args}} install --locked $name --version "=$version" if ($LASTEXITCODE -ne 0) { Write-Error "$name install FAILED" exit $LASTEXITCODE @@ -295,7 +576,7 @@ _check-tool name version: $installed = $null $pattern = '^' + [regex]::Escape($name) + ' v(\S+):' - foreach ($line in (& "{{ just_executable() }}" _anvil-with-stable cargo install --list 2>$null)) { + foreach ($line in (& cargo {{_anvil_stable_toolchain_args}} install --list 2>$null)) { if ($line -match $pattern) { $installed = $Matches[1]; break } } if (-not $installed) { @@ -376,14 +657,14 @@ _install-component toolchain component: } if ($null -ne $subcommand) { if ($toolchain -eq 'default') { - & "{{ just_executable() }}" _anvil-with-stable cargo $subcommand --version *> $null + & cargo {{_anvil_stable_toolchain_args}} $subcommand --version *> $null } else { & cargo "+$toolchain" $subcommand --version *> $null } $present = ($LASTEXITCODE -eq 0) } else { $sysroot = if ($toolchain -eq 'default') { - & "{{ just_executable() }}" _anvil-with-stable rustc --print sysroot 2>$null + & rustc {{_anvil_stable_toolchain_args}} --print sysroot 2>$null } else { & rustc "+$toolchain" --print sysroot 2>$null } @@ -444,14 +725,14 @@ _check-component toolchain component: } if ($null -ne $subcommand) { if ($toolchain -eq 'default') { - & "{{ just_executable() }}" _anvil-with-stable cargo $subcommand --version *> $null + & cargo {{_anvil_stable_toolchain_args}} $subcommand --version *> $null } else { & cargo "+$toolchain" $subcommand --version *> $null } $present = ($LASTEXITCODE -eq 0) } else { $sysroot = if ($toolchain -eq 'default') { - & "{{ just_executable() }}" _anvil-with-stable rustc --print sysroot 2>$null + & rustc {{_anvil_stable_toolchain_args}} --print sysroot 2>$null } else { & rustc "+$toolchain" --print sysroot 2>$null } diff --git a/crates/cargo-anvil/templates/justfiles/anvil/versions.just b/crates/cargo-anvil/templates/justfiles/anvil/versions.just index e2441b62a..70a33d8b3 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/versions.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/versions.just @@ -5,6 +5,8 @@ # Update behaviour: https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/updates.md # # Pinned versions used by the anvil check tree. + +set lazy # # Everything anvil installs (cargo subcommands + rustup toolchains) # is pinned here. The pinning policy: @@ -30,9 +32,102 @@ # Rustup toolchains # ============================================================================ -# Stable checks select their compiler through `_anvil-with-stable` at the -# individual command site. `RUSTUP_TOOLCHAIN` is an input override only; Anvil -# never exports a resolved value globally into unrelated recipes or helpers. +# Stable commands interpolate this lazy PowerShell argument-array expression +# directly at each command site. `RUSTUP_TOOLCHAIN` is an input override only; +# Anvil never exports a resolved value globally into unrelated recipes or +# helpers. +[private] +_anvil_stable_toolchain_selection_script := ''' +param([string] $RepoRoot) + +$ErrorActionPreference = 'Stop' +Set-StrictMode -Version 3 + +function Write-ExplicitToolchainArgs([string] $Toolchain) { + Write-Output ("@('+" + $Toolchain.Replace("'", "''") + "')") +} + +if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { + Write-ExplicitToolchainArgs $env:RUSTUP_TOOLCHAIN + exit 0 +} + +$toolchainFile = $null +foreach ($name in @('rust-toolchain', 'rust-toolchain.toml')) { + $candidate = Join-Path $RepoRoot $name + if (Test-Path -LiteralPath $candidate -PathType Leaf) { + $toolchainFile = $candidate + break + } +} + +$selectingFile = $false +if ($null -ne $toolchainFile) { + $content = Get-Content -LiteralPath $toolchainFile -Raw + if ([string]::IsNullOrWhiteSpace($content)) { + throw "anvil: root toolchain file '$toolchainFile' is empty; remove it or declare a toolchain configuration" + } + if ((Split-Path -Leaf $toolchainFile) -eq 'rust-toolchain' -and $content.TrimStart() -notmatch '^\[') { + $selectingFile = $true + } else { + $inToolchain = $false + foreach ($line in $content -split '\r?\n') { + if ($line -match '^\s*\[\s*toolchain\s*\]\s*(?:#.*)?$') { + $inToolchain = $true + continue + } + if ($line -match '^\s*\[') { + $inToolchain = $false + continue + } + if ($inToolchain -and $line -match '^\s*["'']?(channel|path)["'']?\s*=\s*(["''])([^"'']+)\2\s*(?:#.*)?$') { + $selectingFile = $true + break + } + } + } +} + +if ($selectingFile) { + Write-Output '@()' + exit 0 +} + +if (-not [string]::IsNullOrWhiteSpace($env:ANVIL_MSRV_TOOLCHAIN)) { + throw 'anvil: ANVIL_MSRV_TOOLCHAIN is set without RUSTUP_TOOLCHAIN and no repository toolchain file selects stable checks; set RUSTUP_TOOLCHAIN to the provisioned stable toolchain or unset ANVIL_MSRV_TOOLCHAIN' +} + +$manifestPath = Join-Path $RepoRoot 'Cargo.toml' +if (-not (Test-Path -LiteralPath $manifestPath -PathType Leaf)) { + throw "anvil: Cargo.toml not found at repository root '$RepoRoot'" +} + +$values = @{} +$section = '' +foreach ($line in (Get-Content -LiteralPath $manifestPath)) { + if ($line -match '^\s*\[\s*([^\]]+)\s*\]\s*(?:#.*)?$') { + $section = $Matches[1].Trim() + continue + } + if ($section -in @('workspace.package', 'package') -and + $line -match '^\s*["'']?rust-version["'']?\s*=\s*(["''])([^"'']+)\1\s*(?:#.*)?$') { + $values[$section] = $Matches[2] + } +} + +$msrv = if ($values.ContainsKey('workspace.package')) { + $values['workspace.package'] +} elseif ($values.ContainsKey('package')) { + $values['package'] +} else { + throw 'anvil: no selecting rust-toolchain(.toml) and no root [workspace.package] or [package] rust-version; declare an MSRV or select a toolchain explicitly' +} +Write-ExplicitToolchainArgs $msrv +exit 0 +''' + +[private] +_anvil_stable_toolchain_args := if os_family() == "windows" { shell("& ([scriptblock]::Create('" + replace(_anvil_stable_toolchain_selection_script, "'", "''") + "')) '" + replace(justfile_directory(), "'", "''") + "';") } else { shell("pwsh -NoProfile -Command '& ([scriptblock]::Create($args[0])) $args[1]' " + quote(_anvil_stable_toolchain_selection_script) + " " + quote(justfile_directory())) } # General nightly used by udeps, miri, careful, and any future # nightly-dependent check. Bumped on a regular cadence (monthly is a diff --git a/crates/cargo-anvil/tests/container_upgrade.rs b/crates/cargo-anvil/tests/container_upgrade.rs index 933c3e94a..dcd7e84cd 100644 --- a/crates/cargo-anvil/tests/container_upgrade.rs +++ b/crates/cargo-anvil/tests/container_upgrade.rs @@ -13,12 +13,13 @@ reason = "integration tests panic on unmet preconditions for readable failure output" )] -//! Consumer-upgrade coverage for the container asset relocation. +//! Consumer-upgrade coverage for generated artifact retirement and relocation. //! //! The snapshot tests describe a fresh tree. This exercises the path an //! existing adopter actually takes: a repository generated before the move, //! with its `.anvil.lock` and its assets under `justfiles/anvil/container/`, -//! updated by a binary that emits `.anvil/container/`. +//! updated by a binary that emits `.anvil/container/` and no longer emits the +//! standalone stable-toolchain resolver. use std::path::Path; @@ -29,6 +30,7 @@ use tempfile::TempDir; /// A hand-authored customization file: never catalog-tracked, so `cargo anvil` /// can neither move it nor report it. The drivers warn about one left here. const LEGACY_CUSTOMIZE: &str = "justfiles/anvil/container/customize.sh"; +const LEGACY_RESOLVER: &str = ".anvil/resolve-stable-toolchain.ps1"; /// Where a generated container asset lived before the move. Every asset, /// including the entry recipe, sat directly under `justfiles/anvil/container/`. @@ -94,18 +96,16 @@ fn rewind_to_pre_move_layout(root: &Path) { .unwrap_or_else(|| panic!("{current} must be tracked by the fresh lock")); manifest.files.insert(previous, checksum); } - let resolver = ".anvil/resolve-stable-toolchain.ps1"; - std::fs::remove_file(root.join(resolver)).unwrap(); - manifest - .files - .remove(resolver) - .unwrap_or_else(|| panic!("{resolver} must be tracked by the fresh lock")); + write(&root.join(LEGACY_RESOLVER), ""); + manifest.files.insert( + LEGACY_RESOLVER.to_owned(), + "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855".to_owned(), + ); // Provenance of the older build. It is recorded, never a gate. manifest.catalog_checksum = Some("sha256:0000000000000000000000000000000000000000000000000000000000000000".to_owned()); manifest.tool_version = Some("0.2.0".to_owned()); manifest.save(root).unwrap(); std::fs::remove_dir(root.join(".anvil/container")).unwrap(); - std::fs::remove_dir(root.join(".anvil")).unwrap(); } fn decision_for(outcome: &RunOutcome, path: &str) -> Decision { @@ -142,6 +142,19 @@ fn upgrading_from_the_pre_move_layout_relocates_generated_container_assets() { assert!(outcome.applied); let manifest = Manifest::load(root).unwrap(); + assert!( + !root.join(LEGACY_RESOLVER).exists(), + "the untouched previously-owned resolver must be removed" + ); + assert!( + !manifest.files.contains_key(LEGACY_RESOLVER), + "the retired resolver must be dropped from the lock" + ); + assert_eq!( + decision_for(&outcome, LEGACY_RESOLVER), + Decision::Remove, + "an untouched retired resolver must be removed during upgrade" + ); for (current, previous) in &assets { // Every asset is re-emitted at its new location and tracked there. assert!(root.join(current).is_file(), "{current} must be written at the new location"); diff --git a/crates/cargo-anvil/tests/impact.rs b/crates/cargo-anvil/tests/impact.rs index 40505993f..2aa59febe 100644 --- a/crates/cargo-anvil/tests/impact.rs +++ b/crates/cargo-anvil/tests/impact.rs @@ -840,7 +840,11 @@ fn impact_falls_back_to_full_workspace_when_base_has_no_workspace() { // The introducing commit: add the cargo workspace + emit the anvil tree. write( &root.join("Cargo.toml"), - "[workspace]\nresolver = \"2\"\nmembers = [\"crates/*\"]\n", + "[workspace]\nresolver = \"2\"\nmembers = [\"crates/*\"]\n\n[workspace.package]\nrust-version = \"1.97\"\n", + ); + write( + &root.join("Justfile"), + "set unstable\nset windows-shell := [\"pwsh\", \"-NoProfile\", \"-Command\"]\n", ); write( &root.join("crates/alpha/Cargo.toml"), @@ -883,10 +887,19 @@ fn fake_cargo(dir: &Path, log: &Path) { fs::create_dir_all(dir).unwrap(); #[cfg(windows)] { - // `.cmd` so pwsh's `& cargo` resolves it via PATHEXT before any real - // `cargo.exe` on a later PATH entry. - let script = format!("@echo off\r\n>>\"{}\" echo %*\r\nexit /b 0\r\n", log.display()); - fs::write(dir.join("cargo.cmd"), script).unwrap(); + // Script shims receive the inline argument-array expression as one + // nested array, whereas native Cargo flattens it. Normalize the shim + // input before recording the native-process argv contract. + let script = format!( + "$effectiveArgs = @()\n\ + foreach ($argument in $args) {{\n\ + \x20 if ($argument -is [Array]) {{ $effectiveArgs += @($argument) }} else {{ $effectiveArgs += $argument }}\n\ + }}\n\ + Add-Content -LiteralPath '{}' -Value ($effectiveArgs -join ' ')\n\ + exit 0\n", + log.display() + ); + fs::write(dir.join("cargo.ps1"), script).unwrap(); } #[cfg(unix)] { @@ -1046,11 +1059,17 @@ fn fake_cargo_with_metadata(dir: &Path, log: &Path, metadata_json: &Path) { #[cfg(windows)] { let script = format!( - "@echo off\r\nif \"%1\"==\"metadata\" (\r\n type \"{meta}\"\r\n exit /b 0\r\n)\r\n>>\"{log}\" echo %*\r\nexit /b 0\r\n", + "$effectiveArgs = @()\n\ + foreach ($argument in $args) {{\n\ + \x20 if ($argument -is [Array]) {{ $effectiveArgs += @($argument) }} else {{ $effectiveArgs += $argument }}\n\ + }}\n\ + if ($effectiveArgs -contains 'metadata') {{ Get-Content -Raw -LiteralPath '{meta}'; exit 0 }}\n\ + Add-Content -LiteralPath '{log}' -Value ($effectiveArgs -join ' ')\n\ + exit 0\n", meta = metadata_json.display(), log = log.display() ); - fs::write(dir.join("cargo.cmd"), script).unwrap(); + fs::write(dir.join("cargo.ps1"), script).unwrap(); } #[cfg(unix)] { @@ -1353,7 +1372,11 @@ fn impact_format_maps_proc_macro_target_name_to_its_package() { let root = tmp.path(); write( &root.join("Cargo.toml"), - "[workspace]\nresolver = \"2\"\nmembers = [\"crates/*\"]\n", + "[workspace]\nresolver = \"2\"\nmembers = [\"crates/*\"]\n\n[workspace.package]\nrust-version = \"1.97\"\n", + ); + write( + &root.join("Justfile"), + "set unstable\nset windows-shell := [\"pwsh\", \"-NoProfile\", \"-Command\"]\n", ); write( &root.join("crates/my-macro/Cargo.toml"), @@ -1391,14 +1414,16 @@ fn snapshot_toolchain_probe(caller_toolchain: Option<&str>, toolchain_file: bool git(root, &["add", "rust-toolchain.toml"]); git(root, &["commit", "-q", "-m", "select local toolchain"]); } - // cargo shim: log the active RUSTUP_TOOLCHAIN on each `delta snapshot` - // (emitting `{}` as the snapshot), satisfy the cargo-delta prereq probe - // (`cargo install --list`), and no-op everything else. + // cargo shim: log the effective explicit argument or RUSTUP_TOOLCHAIN on + // each `delta snapshot` (emitting `{}` as the snapshot), satisfy the + // cargo-delta prereq probe (`cargo install --list`), and no-op everything + // else. let shim = ShimBin::new(&[ ( "cargo.ps1", "if (($args -contains 'delta') -and ($args -contains 'snapshot')) {\n\ - \x20 $tc = if (Test-Path Env:\\RUSTUP_TOOLCHAIN) { $env:RUSTUP_TOOLCHAIN } else { '' }\n\ + \x20 $explicit = @($args | Where-Object { $_ -like '+*' } | Select-Object -First 1)\n\ + \x20 $tc = if ($explicit.Count -eq 1) { $explicit[0].Substring(1) } elseif (Test-Path Env:\\RUSTUP_TOOLCHAIN) { $env:RUSTUP_TOOLCHAIN } else { '' }\n\ \x20 Add-Content -LiteralPath $env:ANVIL_TEST_LOG -Value $tc\n\ \x20 Write-Output '{}'\n\ \x20 exit 0\n\ diff --git a/crates/cargo-anvil/tests/recipe_contracts.rs b/crates/cargo-anvil/tests/recipe_contracts.rs index 3c9773495..4dbe8e7df 100644 --- a/crates/cargo-anvil/tests/recipe_contracts.rs +++ b/crates/cargo-anvil/tests/recipe_contracts.rs @@ -25,8 +25,16 @@ const SEMVER: &str = include_str!("../templates/justfiles/anvil/checks/semver-ch const EXTERNAL_TYPES: &str = include_str!("../templates/justfiles/anvil/checks/external-types.just"); const TOOLS: &str = include_str!("../templates/justfiles/anvil/tools.just"); const VERSIONS: &str = include_str!("../templates/justfiles/anvil/versions.just"); -const STABLE_TOOLCHAIN_RESOLVER: &str = include_str!("../templates/anvil/resolve-stable-toolchain.ps1"); const FAKE_CARGO_PS1: &str = r#" +$effectiveArgs = @() +foreach ($argument in $args) { + if ($argument -is [Array]) { + $effectiveArgs += @($argument) + } else { + $effectiveArgs += $argument + } +} +$args = $effectiveArgs $joined = $args -join ' ' if ($env:FAKE_CARGO_LOG) { Add-Content -LiteralPath $env:FAKE_CARGO_LOG -Value $joined @@ -141,24 +149,17 @@ fn tools_available() -> bool { fn fixture(imports: &[(&str, &str)], dependency_recipes: &[&str]) -> TempDir { let tmp = TempDir::new().unwrap(); - let mut justfile = String::from("set unstable\nset allow-duplicate-recipes\n\n"); + let mut justfile = + String::from("set unstable\nset allow-duplicate-recipes\nset windows-shell := [\"pwsh\", \"-NoProfile\", \"-Command\"]\n\n"); // Focused fixtures need this shared variable, but the real version catalog // already defines it and Just rejects duplicate definitions. if !imports.iter().any(|(name, _)| *name == "versions.just") { - justfile.push_str("rust_nightly := \"nightly-test\"\n\n"); + justfile.push_str("rust_nightly := \"nightly-test\"\n_anvil_stable_toolchain_args := \"@()\"\n\n"); } for (name, contents) in imports { write(&tmp.path().join(name), contents); writeln!(justfile, "import '{name}'").unwrap(); } - if !imports.iter().any(|(name, _)| *name == "tools.just") { - justfile.push_str( - "\n[script(\"pwsh\", \"-NoProfile\")]\n\ - _anvil-with-stable command *args:\n\ - \x20 & '{{command}}' {{args}}\n\ - \x20 if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }\n", - ); - } justfile.push('\n'); for recipe in dependency_recipes { justfile.push_str(recipe); @@ -169,7 +170,6 @@ fn fixture(imports: &[(&str, &str)], dependency_recipes: &[&str]) -> TempDir { &tmp.path().join("Cargo.toml"), "[package]\nname = \"fixture\"\nversion = \"0.1.0\"\nrust-version = \"1.97\"\n", ); - write(&tmp.path().join(".anvil/resolve-stable-toolchain.ps1"), STABLE_TOOLCHAIN_RESOLVER); let bin = tmp.path().join("fake-bin"); fs::create_dir_all(&bin).unwrap(); @@ -213,37 +213,44 @@ fn assert_failed(output: &Output, context: &str) { } #[test] -fn stable_toolchain_helper_preserves_invocation_directory_and_arguments() { +fn stable_command_runs_directly_with_explicit_toolchain_arguments() { if !tools_available() { return; } let tmp = fixture(&[("versions.just", VERSIONS), ("tools.just", TOOLS)], &[]); - let crate_dir = tmp.path().join("crates/fixture"); - fs::create_dir_all(&crate_dir).unwrap(); + let justfile_path = tmp.path().join("Justfile"); + let mut justfile = fs::read_to_string(&justfile_path).unwrap(); + justfile.push_str( + "\n[script(\"pwsh\", \"-NoProfile\")]\n\ + _anvil-test-stable-command:\n\ + \x20 & cargo {{_anvil_stable_toolchain_args}} doc2readme --check\n\ + \x20 if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }\n", + ); + write(&justfile_path, &justfile); let args_log = tmp.path().join("cargo-args.log"); let cwd_log = tmp.path().join("cargo-cwd.log"); - let output = run_just_from( + let output = run_just( tmp.path(), - &crate_dir, - &["_anvil-with-stable", "cargo", "doc2readme", "--check"], + &["_anvil-test-stable-command"], &[ ("FAKE_CARGO_LOG", args_log.as_os_str()), ("FAKE_CARGO_CWD_LOG", cwd_log.as_os_str()), + ("RUSTUP_TOOLCHAIN", OsStr::new("test-stable")), ], ); assert!( output.status.success(), - "stable helper should preserve command context:\nstdout:\n{}\nstderr:\n{}", + "direct stable command should preserve arguments:\nstdout:\n{}\nstderr:\n{}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr) ); - assert_eq!(fs::read_to_string(args_log).unwrap().trim(), "doc2readme --check"); + assert_eq!(fs::read_to_string(args_log).unwrap().trim(), "+test-stable doc2readme --check"); assert_eq!( fs::canonicalize(fs::read_to_string(cwd_log).unwrap().trim()).unwrap(), - fs::canonicalize(crate_dir).unwrap(), - "scoped command must run from the nested Just invocation directory" + fs::canonicalize(tmp.path()).unwrap(), + "the command must run directly in its recipe process" ); } diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap index 1f011e5fb..eaf98382e 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap @@ -95,7 +95,6 @@ CMD ["bash"] !justfiles/anvil/*.just !justfiles/anvil/checks/*.just !justfiles/anvil/groups/*.just -!.anvil/resolve-stable-toolchain.ps1 !.anvil/container/* .anvil/container/*/* .anvil/container/customize.sh @@ -372,10 +371,7 @@ if ($LASTEXITCODE -ne 0 -or -not $repoRoot) { throw 'anvil-container must run from a Git repository.' } -$inputs = @( - 'Cargo.toml', - '.anvil/resolve-stable-toolchain.ps1' -) +$inputs = @('Cargo.toml') foreach ($toolchainFile in @('rust-toolchain', 'rust-toolchain.toml')) { if (Test-Path -LiteralPath (Join-Path $repoRoot $toolchainFile) -PathType Leaf) { $inputs += $toolchainFile @@ -458,7 +454,7 @@ if [[ ! "$base_image" =~ @sha256:[0-9a-fA-F]{64}$ ]]; then echo 'anvil-container: ANVIL_CONTAINER_BASE_IMAGE must be pinned by sha256 digest (image@sha256:<64 hex characters>).' >&2 exit 1 fi -inputs=(Cargo.toml .anvil/resolve-stable-toolchain.ps1) +inputs=(Cargo.toml) for toolchain_file in rust-toolchain rust-toolchain.toml; do if [[ -f "$repo_root/$toolchain_file" ]]; then inputs+=("$toolchain_file") @@ -1193,318 +1189,6 @@ if (($# == 0)); then fi docker "${run_args[@]}" "$image" just "$@" -=== .anvil/resolve-stable-toolchain.ps1 === -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# GENERATED BY cargo-anvil. DO NOT EDIT DIRECTLY. - -param( - [switch] $ValidateWorkspaceMsrv, - [switch] $InstallIfMissing, - [switch] $ForEnvironment, - [switch] $MsrvToolchain, - [switch] $InstallMsrvIfNeeded, - [switch] $EmitToolchainFileOptions, - [switch] $ApplyToolchainFileOptions -) - -$ErrorActionPreference = 'Stop' -Set-StrictMode -Version 3 - -$repoRoot = Split-Path -Parent $PSScriptRoot - -function Get-RootToolchainFilePath { - foreach ($name in @('rust-toolchain', 'rust-toolchain.toml')) { - $path = Join-Path $repoRoot $name - if (Test-Path -LiteralPath $path -PathType Leaf) { - return $path - } - } - return $null -} - -function Read-RootToolchainFile([string] $path) { - $content = Get-Content -LiteralPath $path -Raw - if ([string]::IsNullOrWhiteSpace($content)) { - throw "anvil: root toolchain file '$path' is empty; remove it or declare a toolchain configuration" - } - return $content -} - -function Get-SelectingToolchainFile { - $path = Get-RootToolchainFilePath - if ($null -eq $path) { - return $null - } - - $content = Read-RootToolchainFile $path - if ((Split-Path -Leaf $path) -eq 'rust-toolchain' -and $content.TrimStart() -notmatch '^\[') { - return [pscustomobject]@{ - Path = $path - Value = $content.Trim() - Selector = 'channel' - Source = 'file' - Installable = $false - } - } - - $inToolchain = $false - foreach ($line in $content -split '\r?\n') { - if ($line -match '^\s*\[\s*toolchain\s*\]\s*(?:#.*)?$') { - $inToolchain = $true - continue - } - if ($line -match '^\s*\[') { - $inToolchain = $false - continue - } - if ($inToolchain -and $line -match '^\s*["'']?(channel|path)["'']?\s*=\s*(["''])([^"'']+)\2\s*(?:#.*)?$') { - $value = $Matches[3] - if ($Matches[1] -eq 'path' -and -not [IO.Path]::IsPathRooted($value)) { - $value = [IO.Path]::GetFullPath((Join-Path $repoRoot $value)) - } - return [pscustomobject]@{ - Path = $path - Value = $value - Selector = $Matches[1] - Source = 'file' - Installable = $false - } - } - } - return $null -} - -function Get-ToolchainFileOptions { - $path = Get-RootToolchainFilePath - if ($null -eq $path) { - return $null - } - - $content = Read-RootToolchainFile $path - $section = [regex]::Match( - $content, - '(?ms)^\s*\[\s*toolchain\s*\]\s*(?:#.*)?$(.*?)(?=^\s*\[|\z)' - ) - if (-not $section.Success) { - return $null - } - - $body = $section.Groups[1].Value - $profileMatch = [regex]::Match( - $body, - '(?m)^\s*["'']?profile["'']?\s*=\s*(["''])([^"'']+)\1\s*(?:#.*)?$' - ) - $readArray = { - param([string] $name) - $arrayMatch = [regex]::Match( - $body, - "(?ms)^\s*[`"']?$name[`"']?\s*=\s*\[(.*?)\]" - ) - if (-not $arrayMatch.Success) { - return @() - } - return @( - [regex]::Matches($arrayMatch.Groups[1].Value, '(["''])([^"'']+)\1') | - ForEach-Object { $_.Groups[2].Value } - ) - } - - return [pscustomobject]@{ - Path = $path - Profile = if ($profileMatch.Success) { $profileMatch.Groups[2].Value } else { $null } - Components = @(& $readArray 'components') - Targets = @(& $readArray 'targets') - } -} - -function Get-RootMsrv([switch] $AllowMissing) { - $manifestPath = Join-Path $repoRoot 'Cargo.toml' - if (-not (Test-Path -LiteralPath $manifestPath -PathType Leaf)) { - throw "anvil: Cargo.toml not found at repository root '$repoRoot'" - } - - $values = @{} - $section = '' - foreach ($line in (Get-Content -LiteralPath $manifestPath)) { - if ($line -match '^\s*\[\s*([^\]]+)\s*\]\s*(?:#.*)?$') { - $section = $Matches[1].Trim() - continue - } - if ($section -in @('workspace.package', 'package') -and - $line -match '^\s*["'']?rust-version["'']?\s*=\s*(["''])([^"'']+)\1\s*(?:#.*)?$') { - $values[$section] = $Matches[2] - } - } - - if ($values.ContainsKey('workspace.package')) { - return $values['workspace.package'] - } - if ($values.ContainsKey('package')) { - return $values['package'] - } - - if ($AllowMissing) { - return $null - } - throw 'anvil: no selecting rust-toolchain(.toml) and no root [workspace.package] or [package] rust-version; declare an MSRV or select a toolchain explicitly' -} - -function Assert-WorkspaceMsrvCompatibility { - $manifestPath = Join-Path $repoRoot 'Cargo.toml' - $metadataText = (& cargo metadata --manifest-path $manifestPath --format-version 1 --no-deps 2>&1 | Out-String) - if ($LASTEXITCODE -ne 0) { - throw "anvil: cargo metadata failed while validating workspace MSRVs:`n$metadataText" - } - $metadata = $metadataText | ConvertFrom-Json - $memberIds = [Collections.Generic.HashSet[string]]::new( - [string[]] $metadata.workspace_members, - [StringComparer]::Ordinal - ) - $members = @($metadata.packages | Where-Object { $memberIds.Contains([string] $_.id) }) - $missing = @($members | Where-Object { [string]::IsNullOrWhiteSpace([string] $_.rust_version) } | ForEach-Object name) - if ($missing.Count -gt 0) { - throw "anvil: workspace packages without a resolved rust-version: $($missing -join ', '); declare one or select a toolchain explicitly" - } - - $rootMsrv = Get-RootMsrv - $newer = @( - $members | - Where-Object { [version] ([string] $_.rust_version) -gt [version] $rootMsrv } | - ForEach-Object { "$($_.name) ($($_.rust_version))" } - ) - if ($newer.Count -gt 0) { - throw "anvil: workspace packages require a compiler newer than the root MSRV $rootMsrv`: $($newer -join ', '); raise the root MSRV or select one catalog toolchain explicitly with rust-toolchain.toml" - } -} - -function Assert-CompleteInternalToolchainConfiguration { - if (-not [string]::IsNullOrWhiteSpace($env:ANVIL_MSRV_TOOLCHAIN) -and - [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN) -and - $null -eq $fileSelection) { - throw 'anvil: ANVIL_MSRV_TOOLCHAIN is set without RUSTUP_TOOLCHAIN and no repository toolchain file selects stable checks; set RUSTUP_TOOLCHAIN to the provisioned stable toolchain or unset ANVIL_MSRV_TOOLCHAIN' - } -} - -function Get-MsrvSelection { - $msrv = Get-RootMsrv -AllowMissing - if ([string]::IsNullOrWhiteSpace($msrv)) { - return $null - } - - if (-not [string]::IsNullOrWhiteSpace($env:ANVIL_MSRV_TOOLCHAIN)) { - return [pscustomobject]@{ - Value = $env:ANVIL_MSRV_TOOLCHAIN - Source = 'mapped-msrv' - Installable = $false - Mapped = $true - } - } - return [pscustomobject]@{ Value = $msrv; Source = 'msrv'; Installable = $true; Mapped = $false } -} - -function Apply-ToolchainFileOptions([object] $toolchain) { - if ($toolchain.Source -eq 'file' -or $null -eq $toolchainFileOptions) { - return - } - foreach ($component in $toolchainFileOptions.Components) { - rustup component add --toolchain $toolchain.Value $component - if ($LASTEXITCODE -ne 0) { - Write-Warning "anvil: toolchain-file component '$component' is unavailable for '$($toolchain.Value)'; rustup would skip an unavailable file component" - } - } - foreach ($target in $toolchainFileOptions.Targets) { - rustup target add --toolchain $toolchain.Value $target - if ($LASTEXITCODE -ne 0) { - Write-Warning "anvil: toolchain-file target '$target' is unavailable for '$($toolchain.Value)'; rustup would skip an unavailable file target" - } - } -} - -function Install-Toolchain([object] $toolchain, [string] $label) { - if ($toolchain.Installable) { - $installed = rustup toolchain list - if ($LASTEXITCODE -ne 0) { - throw 'anvil: rustup toolchain list failed' - } - if ($installed -notmatch "(?m)^$([regex]::Escape($toolchain.Value))(?:-|$)") { - $profile = if ($null -ne $toolchainFileOptions -and $toolchainFileOptions.Profile) { - $toolchainFileOptions.Profile - } else { - 'minimal' - } - Write-Host "anvil: installing $label toolchain '$($toolchain.Value)'" - rustup toolchain install $toolchain.Value --profile $profile - if ($LASTEXITCODE -ne 0) { - throw "anvil: failed to install $label toolchain '$($toolchain.Value)'" - } - } - } - Apply-ToolchainFileOptions $toolchain -} - -$fileSelection = Get-SelectingToolchainFile -$toolchainFileOptions = Get-ToolchainFileOptions -if ($EmitToolchainFileOptions) { - if ($null -ne $toolchainFileOptions) { - $toolchainFileOptions | ConvertTo-Json -Compress - } - exit 0 -} -if ($ValidateWorkspaceMsrv) { - Assert-CompleteInternalToolchainConfiguration - $skipWorkspaceMsrvValidation = -not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN) - if (-not $skipWorkspaceMsrvValidation -and $null -eq $fileSelection) { - Assert-WorkspaceMsrvCompatibility - } - exit 0 -} - -if ($MsrvToolchain -or $InstallMsrvIfNeeded) { - $msrvSelection = Get-MsrvSelection - if ($null -eq $msrvSelection) { - exit 0 - } - if ($InstallMsrvIfNeeded) { - if ($msrvSelection.Mapped) { - & cargo "+$($msrvSelection.Value)" --version *> $null - if ($LASTEXITCODE -ne 0) { - throw "anvil: mapped MSRV toolchain '$($msrvSelection.Value)' is unavailable; provision ANVIL_MSRV_TOOLCHAIN or unset it to let Anvil install the declared public MSRV" - } - } - Install-Toolchain $msrvSelection 'MSRV' - } else { - $msrvSelection.Value - } - exit 0 -} - -Assert-CompleteInternalToolchainConfiguration -$selection = if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { - [pscustomobject]@{ Value = $env:RUSTUP_TOOLCHAIN; Source = 'environment'; Installable = $false } -} elseif ($null -ne $fileSelection) { - $fileSelection -} else { - [pscustomobject]@{ Value = Get-RootMsrv; Source = 'msrv'; Installable = $true } -} - -if ($ApplyToolchainFileOptions) { - Apply-ToolchainFileOptions $selection - exit 0 -} - -if ($InstallIfMissing) { - Install-Toolchain $selection 'stable' - exit 0 -} - -if ($ForEnvironment -and $selection.Source -eq 'file') { - '' -} else { - $selection.Value -} -exit 0 - === .delta.toml === # >>> anvil-managed: anvil-delta # Changes to these workspace-level inputs conservatively affect every package. @@ -2795,7 +2479,12 @@ steps: Write-Host "anvil: selected stable toolchain '$toolchain'" displayName: anvil setup (resolve stable toolchain) - pwsh: | - $version = (& just _anvil-with-stable rustc --version | Select-Object -Last 1).Split()[1] + $toolchain = (& just _anvil-resolve-stable for-environment | Out-String).Trim() + if ($LASTEXITCODE -ne 0) { + throw 'anvil: failed to select stable toolchain arguments' + } + $toolchainArgs = if ([string]::IsNullOrWhiteSpace($toolchain)) { @() } else { @("+$toolchain") } + $version = (& rustc @toolchainArgs --version | Select-Object -Last 1).Split()[1] Write-Host "##vso[task.setvariable variable=anvil_rustc_version]$version" displayName: anvil setup (capture rustc version) - task: Cache@2 @@ -3128,7 +2817,7 @@ anvil-aprz: anvil-aprz-validate-prereqs Write-Warning 'To fix: run `gh auth login` (recommended), or set $env:GITHUB_TOKEN to a GitHub token, then re-run.' } } - & "{{ just_executable() }}" _anvil-with-stable cargo aprz deps --error-if-high-risk --console appraisal + & cargo {{_anvil_stable_toolchain_args}} aprz deps --error-if-high-risk --console appraisal if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-aprz` recipe. @@ -3154,7 +2843,7 @@ anvil-aprz-validate-prereqs: anvil-tool-cargo-aprz-validate-prereqs [script("pwsh", "-NoProfile")] anvil-audit: anvil-audit-validate-prereqs $ErrorActionPreference = 'Stop' - & "{{ just_executable() }}" _anvil-with-stable cargo audit + & cargo {{_anvil_stable_toolchain_args}} audit if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-audit` recipe. @@ -3182,7 +2871,7 @@ anvil-bench: anvil-bench-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include affected) if ($include -eq '--skip') { Write-Host 'anvil-bench: no affected packages; skipping'; exit 0 } - & "{{ just_executable() }}" _anvil-with-stable cargo bench @(if ($include) { -split $include } else { '--workspace' }) --all-features --no-run + & cargo {{_anvil_stable_toolchain_args}} bench @(if ($include) { -split $include } else { '--workspace' }) --all-features --no-run if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # bench uses the cargo built-in in compile-only mode; no extra tool install @@ -3253,7 +2942,7 @@ anvil-bolero: anvil-bolero-validate-prereqs anvil-impact $packageNames.Add(($pkgArgs[++$i] -split '@', 2)[0]) } } else { - $metadataJson = & "{{ just_executable() }}" _anvil-with-stable cargo metadata --no-deps --format-version 1 + $metadataJson = & cargo {{_anvil_stable_toolchain_args}} metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-bolero: cargo metadata failed' exit $LASTEXITCODE @@ -3417,7 +3106,7 @@ anvil-cargo-hack: anvil-cargo-hack-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include required) if ($include -eq '--skip') { Write-Host 'anvil-cargo-hack: no affected packages; skipping'; exit 0 } - & "{{ just_executable() }}" _anvil-with-stable cargo hack @(if ($include) { -split $include } else { '--workspace' }) --feature-powerset --depth 2 check + & cargo {{_anvil_stable_toolchain_args}} hack @(if ($include) { -split $include } else { '--workspace' }) --feature-powerset --depth 2 check if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-cargo-hack` recipe. @@ -3456,7 +3145,7 @@ anvil-cargo-sort: anvil-cargo-sort-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include modified) if ($include -eq '--skip') { Write-Host 'anvil-cargo-sort: no modified packages; skipping'; exit 0 } - & "{{ just_executable() }}" _anvil-with-stable cargo sort --workspace --grouped --check --check-format + & cargo {{_anvil_stable_toolchain_args}} sort --workspace --grouped --check --check-format if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-cargo-sort` recipe. @@ -3489,7 +3178,7 @@ anvil-clippy: anvil-clippy-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include affected) if ($include -eq '--skip') { Write-Host 'anvil-clippy: no affected packages; skipping'; exit 0 } - & "{{ just_executable() }}" _anvil-with-stable cargo clippy @(if ($include) { -split $include } else { '--workspace' }) --all-targets --all-features --locked "--" '-D' 'warnings' + & cargo {{_anvil_stable_toolchain_args}} clippy @(if ($include) { -split $include } else { '--workspace' }) --all-targets --all-features --locked "--" '-D' 'warnings' if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-clippy` recipe. @@ -3517,7 +3206,7 @@ anvil-clippy-validate-prereqs: anvil-component-default-clippy-validate-prereqs [script("pwsh", "-NoProfile")] anvil-deny: anvil-deny-validate-prereqs $ErrorActionPreference = 'Stop' - & "{{ just_executable() }}" _anvil-with-stable cargo deny check + & cargo {{_anvil_stable_toolchain_args}} deny check if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-deny` recipe. @@ -3548,7 +3237,7 @@ anvil-doc-build: anvil-doc-build-validate-prereqs anvil-impact $include = (& "{{ just_executable() }}" _anvil-impact-include required) if ($include -eq '--skip') { Write-Host 'anvil-doc-build: no affected packages; skipping'; exit 0 } $env:RUSTDOCFLAGS = '-D warnings' - & "{{ just_executable() }}" _anvil-with-stable cargo doc @(if ($include) { -split $include } else { '--workspace' }) --all-features --no-deps + & cargo {{_anvil_stable_toolchain_args}} doc @(if ($include) { -split $include } else { '--workspace' }) --all-features --no-deps if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # doc-build, examples and doc-test are pure cargo built-ins; the rust @@ -3586,9 +3275,9 @@ anvil-doc-test: anvil-doc-test-validate-prereqs anvil-impact $include = (& "{{ just_executable() }}" _anvil-impact-include affected) if ($include -eq '--skip') { Write-Host 'anvil-doc-test: no affected packages; skipping'; exit 0 } $pkg = @(if ($include) { -split $include } else { '--workspace' }) - & "{{ just_executable() }}" _anvil-with-stable cargo test --doc @pkg --all-features --locked + & cargo {{_anvil_stable_toolchain_args}} test --doc @pkg --all-features --locked if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - & "{{ just_executable() }}" _anvil-with-stable cargo test --doc @pkg --locked + & cargo {{_anvil_stable_toolchain_args}} test --doc @pkg --locked if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-doc-test` recipe. @@ -3616,7 +3305,7 @@ anvil-ensure-no-cyclic-deps: anvil-ensure-no-cyclic-deps-validate-prereqs anvil- $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include modified) if ($include -eq '--skip') { Write-Host 'anvil-ensure-no-cyclic-deps: no modified packages; skipping'; exit 0 } - & "{{ just_executable() }}" _anvil-with-stable cargo ensure-no-cyclic-deps + & cargo {{_anvil_stable_toolchain_args}} ensure-no-cyclic-deps if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-ensure-no-cyclic-deps` recipe. @@ -3644,7 +3333,7 @@ anvil-ensure-no-default-features: anvil-ensure-no-default-features-validate-prer $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include modified) if ($include -eq '--skip') { Write-Host 'anvil-ensure-no-default-features: no modified packages; skipping'; exit 0 } - & "{{ just_executable() }}" _anvil-with-stable cargo ensure-no-default-features + & cargo {{_anvil_stable_toolchain_args}} ensure-no-default-features if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-ensure-no-default-features` recipe. @@ -3672,7 +3361,7 @@ anvil-examples: anvil-examples-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include affected) if ($include -eq '--skip') { Write-Host 'anvil-examples: no affected packages; skipping'; exit 0 } - & "{{ just_executable() }}" _anvil-with-stable cargo build @(if ($include) { -split $include } else { '--workspace' }) --examples --all-features --locked + & cargo {{_anvil_stable_toolchain_args}} build @(if ($include) { -split $include } else { '--workspace' }) --examples --all-features --locked if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-examples` recipe. @@ -3717,7 +3406,7 @@ anvil-external-types: anvil-external-types-validate-prereqs anvil-impact $pkg = @(if ($include) { -split $include } else { '--workspace' }) # Build pkg-name -> manifest-path map, restricted to library crates. $libPkgs = @{} - $metadataJson = & "{{ just_executable() }}" _anvil-with-stable cargo metadata --no-deps --format-version 1 + $metadataJson = & cargo {{_anvil_stable_toolchain_args}} metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-external-types: cargo metadata failed' exit $LASTEXITCODE @@ -3845,7 +3534,7 @@ anvil-license-headers: anvil-license-headers-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include modified) if ($include -eq '--skip') { Write-Host 'anvil-license-headers: no modified packages; skipping'; exit 0 } - & "{{ just_executable() }}" _anvil-with-stable cargo heather + & cargo {{_anvil_stable_toolchain_args}} heather if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-license-headers` recipe. @@ -3896,7 +3585,7 @@ anvil-llvm-cov: anvil-llvm-cov-validate-prereqs anvil-impact # never opts a changed package out of tests. $testOnlyPkg = [System.Collections.Generic.List[string]]::new() if ($pkg -contains '--package') { - $metadataJson = & "{{ just_executable() }}" _anvil-with-stable cargo metadata --no-deps --format-version 1 + $metadataJson = & cargo {{_anvil_stable_toolchain_args}} metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-llvm-cov: cargo metadata failed' exit $LASTEXITCODE @@ -4027,7 +3716,7 @@ anvil-llvm-cov: anvil-llvm-cov-validate-prereqs anvil-impact } $gateArgs.Add($pkg[$i]) } - & "{{ just_executable() }}" _anvil-with-stable cargo coverage-gate @gateArgs --lcov target/coverage/lcov-all-features.info --lcov target/coverage/lcov-no-default.info + & cargo {{_anvil_stable_toolchain_args}} coverage-gate @gateArgs --lcov target/coverage/lcov-all-features.info --lcov target/coverage/lcov-no-default.info if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # --- pr-test members (shared with scheduled-test) --- @@ -4098,7 +3787,7 @@ anvil-loom: anvil-loom-validate-prereqs anvil-impact Write-Host 'anvil-loom: no affected packages; skipping' exit 0 } - $meta = & "{{ just_executable() }}" _anvil-with-stable cargo metadata --no-deps --format-version 1 | ConvertFrom-Json + $meta = & cargo {{_anvil_stable_toolchain_args}} metadata --no-deps --format-version 1 | ConvertFrom-Json if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Optional impact-scoping: recover bare package names from the @@ -4151,7 +3840,7 @@ anvil-loom: anvil-loom-validate-prereqs anvil-impact $env:RUSTFLAGS = "--cfg loom $($env:RUSTFLAGS)".Trim() foreach ($lt in $loomTargets) { Write-Host "anvil-loom: $($lt.pkg) :: $($lt.target)" - & "{{ just_executable() }}" _anvil-with-stable cargo test -p $lt.pkg --release --all-features --locked --test $lt.target -- --test-threads=1 + & cargo {{_anvil_stable_toolchain_args}} test -p $lt.pkg --release --all-features --locked --test $lt.target -- --test-threads=1 if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } } @@ -4439,7 +4128,7 @@ anvil-mutants-diff: anvil-mutants-diff-validate-prereqs anvil-impact $diff_path = Join-Path $tmp_dir 'anvil-mutants-diff.diff' git diff "$base..HEAD" --output=$diff_path if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - & "{{ just_executable() }}" _anvil-with-stable cargo mutants --in-diff $diff_path --no-shuffle --jobs 0 + & cargo {{_anvil_stable_toolchain_args}} mutants --in-diff $diff_path --no-shuffle --jobs 0 if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # --- pr-mutants members --- @@ -4472,7 +4161,7 @@ anvil-mutants-full: anvil-mutants-full-validate-prereqs Write-Host 'anvil-mutants-full: aarch64-pc-windows-msvc -- cargo-mutants does not build here (winapi); skipping' exit 0 } - & "{{ just_executable() }}" _anvil-with-stable cargo mutants --workspace --no-shuffle --jobs 0 + & cargo {{_anvil_stable_toolchain_args}} mutants --workspace --no-shuffle --jobs 0 if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # --- scheduled-exhaustive members (mutants-full reuses cargo-mutants; @@ -4620,7 +4309,7 @@ anvil-readme-check: anvil-readme-check-validate-prereqs # Unscoped: a change to the workspace-level README template is a repo-level # input cargo-delta cannot map to a package, so this check always runs the # full library set rather than an impact subset. - $metadataJson = & "{{ just_executable() }}" _anvil-with-stable cargo metadata --no-deps --format-version 1 + $metadataJson = & cargo {{_anvil_stable_toolchain_args}} metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-readme-check: cargo metadata failed' exit $LASTEXITCODE @@ -4662,7 +4351,7 @@ anvil-readme-check: anvil-readme-check-validate-prereqs } $args = @('doc2readme', '--check') if ($relTemplate) { $args += @('--template', $relTemplate) } - & "{{ just_executable() }}" _anvil-with-stable cargo @args + & cargo {{_anvil_stable_toolchain_args}} @args if ($LASTEXITCODE -ne 0) { $hadFailure = $true } } finally { Pop-Location @@ -4746,7 +4435,7 @@ anvil-semver-check: anvil-semver-check-validate-prereqs anvil-impact # we got here via impact-scoping (cloud workflows) or full-workspace # fallback (local). $libPkgs = @{} - $metadataJson = & "{{ just_executable() }}" _anvil-with-stable cargo metadata --no-deps --format-version 1 + $metadataJson = & cargo {{_anvil_stable_toolchain_args}} metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-semver-check: cargo metadata failed' exit $LASTEXITCODE @@ -4823,7 +4512,7 @@ anvil-semver-check: anvil-semver-check-validate-prereqs anvil-impact } Write-Host "anvil-semver-check: $p (baseline $base)" - $outputLines = @(& "{{ just_executable() }}" _anvil-with-stable cargo semver-checks --package $p --baseline-rev $base 2>&1) + $outputLines = @(& cargo {{_anvil_stable_toolchain_args}} semver-checks --package $p --baseline-rev $base 2>&1) $semverExit = $LASTEXITCODE $output = ($outputLines | Out-String) if ($semverExit -eq 100) { @@ -4962,9 +4651,9 @@ anvil-spellcheck: anvil-spellcheck-validate-prereqs # ignores user-curated dictionaries (`extra_dictionaries`, custom # hunspell langs, etc.). if (Test-Path 'spellcheck.toml') { - & "{{ just_executable() }}" _anvil-with-stable cargo spellcheck --cfg spellcheck.toml check --code 1 + & cargo {{_anvil_stable_toolchain_args}} spellcheck --cfg spellcheck.toml check --code 1 } else { - & "{{ just_executable() }}" _anvil-with-stable cargo spellcheck check --code 1 + & cargo {{_anvil_stable_toolchain_args}} spellcheck check --code 1 } if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } @@ -5831,9 +5520,13 @@ _anvil-impact-snapshot: # selections are already valid rustup overrides. For a selecting # toolchain file, ask rustup for the concrete active identifier so # `path` selections are not mistaken for named toolchains. - $baselineToolchain = (& "{{ just_executable() }}" _anvil-resolve-stable for-environment | Out-String).Trim() - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - if ([string]::IsNullOrWhiteSpace($baselineToolchain)) { + $stableArgs = {{_anvil_stable_toolchain_args}} + $baselineToolchain = if ($stableArgs.Count -eq 1) { + $stableArgs[0].Substring(1) + } else { + $null + } + if ($null -eq $baselineToolchain) { if (-not (Get-Command rustup -ErrorAction SilentlyContinue)) { Write-Error 'anvil-impact: rustup is required to resolve the active repository toolchain for the baseline snapshot' exit 1 @@ -5908,7 +5601,7 @@ _anvil-impact-snapshot: Write-Host 'anvil-impact: current snapshot up to date' } else { Write-Host 'anvil-impact: snapshotting working tree' - $curSnap = & "{{ just_executable() }}" _anvil-with-stable cargo delta @deltaArgs snapshot + $curSnap = & cargo {{_anvil_stable_toolchain_args}} delta @deltaArgs snapshot if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-impact: cargo delta snapshot (current) failed'; exit 1 } Set-Content -LiteralPath $currentJson -Value $curSnap -Encoding UTF8 Set-Content -LiteralPath $currentKeyFile -Value $currentState -Encoding UTF8 -NoNewline @@ -6026,7 +5719,7 @@ anvil-impact: _anvil-impact-snapshot # early), so default to an empty object so impact.json always exists -- # the freshness check above keys on its presence, and a missing file # would force a recompute on every invocation. - $impactOut = (& "{{ just_executable() }}" _anvil-with-stable cargo delta @deltaArgs impact --baseline $baselineJson --current $currentJson --format json | Out-String) + $impactOut = (& cargo {{_anvil_stable_toolchain_args}} delta @deltaArgs impact --baseline $baselineJson --current $currentJson --format json | Out-String) if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-impact: cargo delta impact failed'; exit 1 } if ([string]::IsNullOrWhiteSpace($impactOut)) { $impactOut = '{}' } Set-Content -LiteralPath $impactJson -Value $impactOut.Trim() -Encoding UTF8 @@ -6103,7 +5796,7 @@ _anvil-impact-format tier impactJson: # nested workspaces cargo-delta can instead emit a manifest directory # leaf; accept any reported identifier only when every matching namespace # resolves to the *same* workspace package. - $metadataJson = & "{{ just_executable() }}" _anvil-with-stable cargo metadata --no-deps --format-version 1 + $metadataJson = & cargo {{_anvil_stable_toolchain_args}} metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-impact: cargo metadata failed' exit $LASTEXITCODE @@ -6685,37 +6378,318 @@ anvil-tool-pwsh-validate-prereqs: [script("pwsh", "-NoProfile")] _anvil-resolve-stable action="resolve": $ErrorActionPreference = 'Stop' - $resolverArgs = @(switch ('{{action}}') { - 'resolve' { @() } - 'for-environment' { @('-ForEnvironment') } - 'install' { @('-InstallIfMissing') } - 'apply-file-options' { @('-ApplyToolchainFileOptions') } - 'validate-workspace-msrv' { @('-ValidateWorkspaceMsrv') } - 'msrv' { @('-MsrvToolchain') } - 'install-msrv' { @('-InstallMsrvIfNeeded') } - default { - Write-Error "_anvil-resolve-stable: unknown action '{{action}}'" - exit 2 + Set-StrictMode -Version 3 + + $action = '{{action}}' + $validActions = @( + 'resolve', + 'for-environment', + 'install', + 'apply-file-options', + 'validate-workspace-msrv', + 'msrv', + 'install-msrv', + 'emit-file-options' + ) + if ($action -notin $validActions) { + Write-Error "_anvil-resolve-stable: unknown action '$action'" + exit 2 + } + + $repoRoot = '{{justfile_directory()}}' + + function Get-RootToolchainFilePath { + foreach ($name in @('rust-toolchain', 'rust-toolchain.toml')) { + $path = Join-Path $repoRoot $name + if (Test-Path -LiteralPath $path -PathType Leaf) { + return $path + } } - }) - & pwsh -NoProfile -File '.anvil/resolve-stable-toolchain.ps1' @resolverArgs - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + return $null + } -# Run one command under the selected stable compiler. A repository toolchain -# file yields no override so rustup processes its complete configuration. -[script("pwsh", "-NoProfile")] -_anvil-with-stable command *args: - $ErrorActionPreference = 'Stop' - $toolchain = (& "{{ just_executable() }}" _anvil-resolve-stable for-environment | Out-String).Trim() - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - if ([string]::IsNullOrWhiteSpace($toolchain)) { - Remove-Item Env:\RUSTUP_TOOLCHAIN -ErrorAction SilentlyContinue + function Read-RootToolchainFile([string] $path) { + $content = Get-Content -LiteralPath $path -Raw + if ([string]::IsNullOrWhiteSpace($content)) { + throw "anvil: root toolchain file '$path' is empty; remove it or declare a toolchain configuration" + } + return $content + } + + function Get-SelectingToolchainFile { + $path = Get-RootToolchainFilePath + if ($null -eq $path) { + return $null + } + + $content = Read-RootToolchainFile $path + if ((Split-Path -Leaf $path) -eq 'rust-toolchain' -and $content.TrimStart() -notmatch '^\[') { + return [pscustomobject]@{ + Path = $path + Value = $content.Trim() + Selector = 'channel' + Source = 'file' + Installable = $false + } + } + + $inToolchain = $false + foreach ($line in $content -split '\r?\n') { + if ($line -match '^\s*\[\s*toolchain\s*\]\s*(?:#.*)?$') { + $inToolchain = $true + continue + } + if ($line -match '^\s*\[') { + $inToolchain = $false + continue + } + if ($inToolchain -and $line -match '^\s*["'']?(channel|path)["'']?\s*=\s*(["''])([^"'']+)\2\s*(?:#.*)?$') { + $value = $Matches[3] + if ($Matches[1] -eq 'path' -and -not [IO.Path]::IsPathRooted($value)) { + $value = [IO.Path]::GetFullPath((Join-Path $repoRoot $value)) + } + return [pscustomobject]@{ + Path = $path + Value = $value + Selector = $Matches[1] + Source = 'file' + Installable = $false + } + } + } + return $null + } + + function Get-ToolchainFileOptions { + $path = Get-RootToolchainFilePath + if ($null -eq $path) { + return $null + } + + $content = Read-RootToolchainFile $path + $section = [regex]::Match( + $content, + '(?ms)^\s*\[\s*toolchain\s*\]\s*(?:#.*)?$(.*?)(?=^\s*\[|\z)' + ) + if (-not $section.Success) { + return $null + } + + $body = $section.Groups[1].Value + $profileMatch = [regex]::Match( + $body, + '(?m)^\s*["'']?profile["'']?\s*=\s*(["''])([^"'']+)\1\s*(?:#.*)?$' + ) + $readArray = { + param([string] $name) + $arrayMatch = [regex]::Match( + $body, + "(?ms)^\s*[`"']?$name[`"']?\s*=\s*\[(.*?)\]" + ) + if (-not $arrayMatch.Success) { + return @() + } + return @( + [regex]::Matches($arrayMatch.Groups[1].Value, '(["''])([^"'']+)\1') | + ForEach-Object { $_.Groups[2].Value } + ) + } + + return [pscustomobject]@{ + Path = $path + Profile = if ($profileMatch.Success) { $profileMatch.Groups[2].Value } else { $null } + Components = @(& $readArray 'components') + Targets = @(& $readArray 'targets') + } + } + + function Get-RootMsrv([switch] $AllowMissing) { + $manifestPath = Join-Path $repoRoot 'Cargo.toml' + if (-not (Test-Path -LiteralPath $manifestPath -PathType Leaf)) { + throw "anvil: Cargo.toml not found at repository root '$repoRoot'" + } + + $values = @{} + $section = '' + foreach ($line in (Get-Content -LiteralPath $manifestPath)) { + if ($line -match '^\s*\[\s*([^\]]+)\s*\]\s*(?:#.*)?$') { + $section = $Matches[1].Trim() + continue + } + if ($section -in @('workspace.package', 'package') -and + $line -match '^\s*["'']?rust-version["'']?\s*=\s*(["''])([^"'']+)\1\s*(?:#.*)?$') { + $values[$section] = $Matches[2] + } + } + + if ($values.ContainsKey('workspace.package')) { + return $values['workspace.package'] + } + if ($values.ContainsKey('package')) { + return $values['package'] + } + + if ($AllowMissing) { + return $null + } + throw 'anvil: no selecting rust-toolchain(.toml) and no root [workspace.package] or [package] rust-version; declare an MSRV or select a toolchain explicitly' + } + + function Assert-WorkspaceMsrvCompatibility { + $manifestPath = Join-Path $repoRoot 'Cargo.toml' + $metadataText = (& cargo metadata --manifest-path $manifestPath --format-version 1 --no-deps 2>&1 | Out-String) + if ($LASTEXITCODE -ne 0) { + throw "anvil: cargo metadata failed while validating workspace MSRVs:`n$metadataText" + } + $metadata = $metadataText | ConvertFrom-Json + $memberIds = [Collections.Generic.HashSet[string]]::new( + [string[]] $metadata.workspace_members, + [StringComparer]::Ordinal + ) + $members = @($metadata.packages | Where-Object { $memberIds.Contains([string] $_.id) }) + $missing = @($members | Where-Object { [string]::IsNullOrWhiteSpace([string] $_.rust_version) } | ForEach-Object name) + if ($missing.Count -gt 0) { + throw "anvil: workspace packages without a resolved rust-version: $($missing -join ', '); declare one or select a toolchain explicitly" + } + + $rootMsrv = Get-RootMsrv + $newer = @( + $members | + Where-Object { [version] ([string] $_.rust_version) -gt [version] $rootMsrv } | + ForEach-Object { "$($_.name) ($($_.rust_version))" } + ) + if ($newer.Count -gt 0) { + throw "anvil: workspace packages require a compiler newer than the root MSRV $rootMsrv`: $($newer -join ', '); raise the root MSRV or select one catalog toolchain explicitly with rust-toolchain.toml" + } + } + + function Assert-CompleteInternalToolchainConfiguration { + if (-not [string]::IsNullOrWhiteSpace($env:ANVIL_MSRV_TOOLCHAIN) -and + [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN) -and + $null -eq $fileSelection) { + throw 'anvil: ANVIL_MSRV_TOOLCHAIN is set without RUSTUP_TOOLCHAIN and no repository toolchain file selects stable checks; set RUSTUP_TOOLCHAIN to the provisioned stable toolchain or unset ANVIL_MSRV_TOOLCHAIN' + } + } + + function Get-MsrvSelection { + $msrv = Get-RootMsrv -AllowMissing + if ([string]::IsNullOrWhiteSpace($msrv)) { + return $null + } + + if (-not [string]::IsNullOrWhiteSpace($env:ANVIL_MSRV_TOOLCHAIN)) { + return [pscustomobject]@{ + Value = $env:ANVIL_MSRV_TOOLCHAIN + Source = 'mapped-msrv' + Installable = $false + Mapped = $true + } + } + return [pscustomobject]@{ Value = $msrv; Source = 'msrv'; Installable = $true; Mapped = $false } + } + + function Apply-ToolchainFileOptions([object] $toolchain) { + if ($toolchain.Source -eq 'file' -or $null -eq $toolchainFileOptions) { + return + } + foreach ($component in $toolchainFileOptions.Components) { + rustup component add --toolchain $toolchain.Value $component + if ($LASTEXITCODE -ne 0) { + Write-Warning "anvil: toolchain-file component '$component' is unavailable for '$($toolchain.Value)'; rustup would skip an unavailable file component" + } + } + foreach ($target in $toolchainFileOptions.Targets) { + rustup target add --toolchain $toolchain.Value $target + if ($LASTEXITCODE -ne 0) { + Write-Warning "anvil: toolchain-file target '$target' is unavailable for '$($toolchain.Value)'; rustup would skip an unavailable file target" + } + } + } + + function Install-Toolchain([object] $toolchain, [string] $label) { + if ($toolchain.Installable) { + $installed = rustup toolchain list + if ($LASTEXITCODE -ne 0) { + throw 'anvil: rustup toolchain list failed' + } + if ($installed -notmatch "(?m)^$([regex]::Escape($toolchain.Value))(?:-|$)") { + $profile = if ($null -ne $toolchainFileOptions -and $toolchainFileOptions.Profile) { + $toolchainFileOptions.Profile + } else { + 'minimal' + } + Write-Host "anvil: installing $label toolchain '$($toolchain.Value)'" + rustup toolchain install $toolchain.Value --profile $profile + if ($LASTEXITCODE -ne 0) { + throw "anvil: failed to install $label toolchain '$($toolchain.Value)'" + } + } + } + Apply-ToolchainFileOptions $toolchain + } + + $fileSelection = Get-SelectingToolchainFile + $toolchainFileOptions = Get-ToolchainFileOptions + + if ($action -eq 'emit-file-options') { + if ($null -ne $toolchainFileOptions) { + $toolchainFileOptions | ConvertTo-Json -Compress + } + exit 0 + } + + if ($action -eq 'validate-workspace-msrv') { + Assert-CompleteInternalToolchainConfiguration + $skipWorkspaceMsrvValidation = -not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN) + if (-not $skipWorkspaceMsrvValidation -and $null -eq $fileSelection) { + Assert-WorkspaceMsrvCompatibility + } + exit 0 + } + + if ($action -in @('msrv', 'install-msrv')) { + $msrvSelection = Get-MsrvSelection + if ($null -eq $msrvSelection) { + exit 0 + } + if ($action -eq 'install-msrv') { + if ($msrvSelection.Mapped) { + & cargo "+$($msrvSelection.Value)" --version *> $null + if ($LASTEXITCODE -ne 0) { + throw "anvil: mapped MSRV toolchain '$($msrvSelection.Value)' is unavailable; provision ANVIL_MSRV_TOOLCHAIN or unset it to let Anvil install the declared public MSRV" + } + } + Install-Toolchain $msrvSelection 'MSRV' + } else { + $msrvSelection.Value + } + exit 0 + } + + Assert-CompleteInternalToolchainConfiguration + $selection = if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { + [pscustomobject]@{ Value = $env:RUSTUP_TOOLCHAIN; Source = 'environment'; Installable = $false } + } elseif ($null -ne $fileSelection) { + $fileSelection } else { - $env:RUSTUP_TOOLCHAIN = $toolchain + [pscustomobject]@{ Value = Get-RootMsrv; Source = 'msrv'; Installable = $true } + } + + if ($action -eq 'apply-file-options') { + Apply-ToolchainFileOptions $selection + exit 0 + } + + if ($action -eq 'install') { + Install-Toolchain $selection 'stable' + exit 0 + } + + if ($action -eq 'for-environment' -and $selection.Source -eq 'file') { + '' + } else { + $selection.Value } - Set-Location '{{invocation_directory_native()}}' - & '{{command}}' {{args}} - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # _install-tool: install a cargo subcommand at the catalog version, # or no-op if it is already installed at or above that version. The @@ -6747,7 +6721,7 @@ _install-tool name version installer source_prereq="": # cached binaries and fail with "binary already exists in destination". $installed = $null $pattern = '^' + [regex]::Escape($name) + ' v(\S+):' - foreach ($line in (& "{{ just_executable() }}" _anvil-with-stable cargo install --list 2>$null)) { + foreach ($line in (& cargo {{_anvil_stable_toolchain_args}} install --list 2>$null)) { if ($line -match $pattern) { $installed = $Matches[1]; break } } if ($installed) { @@ -6765,7 +6739,7 @@ _install-tool name version installer source_prereq="": if ($installer -eq 'binstall') { if (-not (Get-Command cargo-binstall -ErrorAction SilentlyContinue)) { Write-Host ' Bootstrapping cargo-binstall' - & "{{ just_executable() }}" _anvil-with-stable cargo install --locked cargo-binstall + & cargo {{_anvil_stable_toolchain_args}} install --locked cargo-binstall if ($LASTEXITCODE -ne 0) { Write-Error 'cargo-binstall bootstrap failed' exit $LASTEXITCODE @@ -6779,7 +6753,7 @@ _install-tool name version installer source_prereq="": $binstallArgs += @('--disable-strategies', 'compile') } $binstallArgs += @($name, '--version', "=$version") - & "{{ just_executable() }}" _anvil-with-stable cargo @binstallArgs + & cargo {{_anvil_stable_toolchain_args}} @binstallArgs if ($LASTEXITCODE -eq 0) { exit 0 } Write-Host ' binstall failed; falling back to cargo install' -ForegroundColor Yellow } @@ -6787,7 +6761,7 @@ _install-tool name version installer source_prereq="": & "{{just_executable()}}" $sourcePrereq if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } } - & "{{ just_executable() }}" _anvil-with-stable cargo install --locked $name --version "=$version" + & cargo {{_anvil_stable_toolchain_args}} install --locked $name --version "=$version" if ($LASTEXITCODE -ne 0) { Write-Error "$name install FAILED" exit $LASTEXITCODE @@ -6803,7 +6777,7 @@ _check-tool name version: $installed = $null $pattern = '^' + [regex]::Escape($name) + ' v(\S+):' - foreach ($line in (& "{{ just_executable() }}" _anvil-with-stable cargo install --list 2>$null)) { + foreach ($line in (& cargo {{_anvil_stable_toolchain_args}} install --list 2>$null)) { if ($line -match $pattern) { $installed = $Matches[1]; break } } if (-not $installed) { @@ -6884,14 +6858,14 @@ _install-component toolchain component: } if ($null -ne $subcommand) { if ($toolchain -eq 'default') { - & "{{ just_executable() }}" _anvil-with-stable cargo $subcommand --version *> $null + & cargo {{_anvil_stable_toolchain_args}} $subcommand --version *> $null } else { & cargo "+$toolchain" $subcommand --version *> $null } $present = ($LASTEXITCODE -eq 0) } else { $sysroot = if ($toolchain -eq 'default') { - & "{{ just_executable() }}" _anvil-with-stable rustc --print sysroot 2>$null + & rustc {{_anvil_stable_toolchain_args}} --print sysroot 2>$null } else { & rustc "+$toolchain" --print sysroot 2>$null } @@ -6952,14 +6926,14 @@ _check-component toolchain component: } if ($null -ne $subcommand) { if ($toolchain -eq 'default') { - & "{{ just_executable() }}" _anvil-with-stable cargo $subcommand --version *> $null + & cargo {{_anvil_stable_toolchain_args}} $subcommand --version *> $null } else { & cargo "+$toolchain" $subcommand --version *> $null } $present = ($LASTEXITCODE -eq 0) } else { $sysroot = if ($toolchain -eq 'default') { - & "{{ just_executable() }}" _anvil-with-stable rustc --print sysroot 2>$null + & rustc {{_anvil_stable_toolchain_args}} --print sysroot 2>$null } else { & rustc "+$toolchain" --print sysroot 2>$null } @@ -7296,6 +7270,8 @@ anvil-tool-cargo-udeps-validate-prereqs: (_check-tool "cargo-udeps" cargo_udeps_ # Update behaviour: https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/updates.md # # Pinned versions used by the anvil check tree. + +set lazy # # Everything anvil installs (cargo subcommands + rustup toolchains) # is pinned here. The pinning policy: @@ -7321,9 +7297,102 @@ anvil-tool-cargo-udeps-validate-prereqs: (_check-tool "cargo-udeps" cargo_udeps_ # Rustup toolchains # ============================================================================ -# Stable checks select their compiler through `_anvil-with-stable` at the -# individual command site. `RUSTUP_TOOLCHAIN` is an input override only; Anvil -# never exports a resolved value globally into unrelated recipes or helpers. +# Stable commands interpolate this lazy PowerShell argument-array expression +# directly at each command site. `RUSTUP_TOOLCHAIN` is an input override only; +# Anvil never exports a resolved value globally into unrelated recipes or +# helpers. +[private] +_anvil_stable_toolchain_selection_script := ''' +param([string] $RepoRoot) + +$ErrorActionPreference = 'Stop' +Set-StrictMode -Version 3 + +function Write-ExplicitToolchainArgs([string] $Toolchain) { + Write-Output ("@('+" + $Toolchain.Replace("'", "''") + "')") +} + +if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { + Write-ExplicitToolchainArgs $env:RUSTUP_TOOLCHAIN + exit 0 +} + +$toolchainFile = $null +foreach ($name in @('rust-toolchain', 'rust-toolchain.toml')) { + $candidate = Join-Path $RepoRoot $name + if (Test-Path -LiteralPath $candidate -PathType Leaf) { + $toolchainFile = $candidate + break + } +} + +$selectingFile = $false +if ($null -ne $toolchainFile) { + $content = Get-Content -LiteralPath $toolchainFile -Raw + if ([string]::IsNullOrWhiteSpace($content)) { + throw "anvil: root toolchain file '$toolchainFile' is empty; remove it or declare a toolchain configuration" + } + if ((Split-Path -Leaf $toolchainFile) -eq 'rust-toolchain' -and $content.TrimStart() -notmatch '^\[') { + $selectingFile = $true + } else { + $inToolchain = $false + foreach ($line in $content -split '\r?\n') { + if ($line -match '^\s*\[\s*toolchain\s*\]\s*(?:#.*)?$') { + $inToolchain = $true + continue + } + if ($line -match '^\s*\[') { + $inToolchain = $false + continue + } + if ($inToolchain -and $line -match '^\s*["'']?(channel|path)["'']?\s*=\s*(["''])([^"'']+)\2\s*(?:#.*)?$') { + $selectingFile = $true + break + } + } + } +} + +if ($selectingFile) { + Write-Output '@()' + exit 0 +} + +if (-not [string]::IsNullOrWhiteSpace($env:ANVIL_MSRV_TOOLCHAIN)) { + throw 'anvil: ANVIL_MSRV_TOOLCHAIN is set without RUSTUP_TOOLCHAIN and no repository toolchain file selects stable checks; set RUSTUP_TOOLCHAIN to the provisioned stable toolchain or unset ANVIL_MSRV_TOOLCHAIN' +} + +$manifestPath = Join-Path $RepoRoot 'Cargo.toml' +if (-not (Test-Path -LiteralPath $manifestPath -PathType Leaf)) { + throw "anvil: Cargo.toml not found at repository root '$RepoRoot'" +} + +$values = @{} +$section = '' +foreach ($line in (Get-Content -LiteralPath $manifestPath)) { + if ($line -match '^\s*\[\s*([^\]]+)\s*\]\s*(?:#.*)?$') { + $section = $Matches[1].Trim() + continue + } + if ($section -in @('workspace.package', 'package') -and + $line -match '^\s*["'']?rust-version["'']?\s*=\s*(["''])([^"'']+)\1\s*(?:#.*)?$') { + $values[$section] = $Matches[2] + } +} + +$msrv = if ($values.ContainsKey('workspace.package')) { + $values['workspace.package'] +} elseif ($values.ContainsKey('package')) { + $values['package'] +} else { + throw 'anvil: no selecting rust-toolchain(.toml) and no root [workspace.package] or [package] rust-version; declare an MSRV or select a toolchain explicitly' +} +Write-ExplicitToolchainArgs $msrv +exit 0 +''' + +[private] +_anvil_stable_toolchain_args := if os_family() == "windows" { shell("& ([scriptblock]::Create('" + replace(_anvil_stable_toolchain_selection_script, "'", "''") + "')) '" + replace(justfile_directory(), "'", "''") + "';") } else { shell("pwsh -NoProfile -Command '& ([scriptblock]::Create($args[0])) $args[1]' " + quote(_anvil_stable_toolchain_selection_script) + " " + quote(justfile_directory())) } # General nightly used by udeps, miri, careful, and any future # nightly-dependent check. Bumped on a regular cadence (monthly is a diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap index a3d134bfd..79f61e721 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap @@ -95,7 +95,6 @@ CMD ["bash"] !justfiles/anvil/*.just !justfiles/anvil/checks/*.just !justfiles/anvil/groups/*.just -!.anvil/resolve-stable-toolchain.ps1 !.anvil/container/* .anvil/container/*/* .anvil/container/customize.sh @@ -372,10 +371,7 @@ if ($LASTEXITCODE -ne 0 -or -not $repoRoot) { throw 'anvil-container must run from a Git repository.' } -$inputs = @( - 'Cargo.toml', - '.anvil/resolve-stable-toolchain.ps1' -) +$inputs = @('Cargo.toml') foreach ($toolchainFile in @('rust-toolchain', 'rust-toolchain.toml')) { if (Test-Path -LiteralPath (Join-Path $repoRoot $toolchainFile) -PathType Leaf) { $inputs += $toolchainFile @@ -458,7 +454,7 @@ if [[ ! "$base_image" =~ @sha256:[0-9a-fA-F]{64}$ ]]; then echo 'anvil-container: ANVIL_CONTAINER_BASE_IMAGE must be pinned by sha256 digest (image@sha256:<64 hex characters>).' >&2 exit 1 fi -inputs=(Cargo.toml .anvil/resolve-stable-toolchain.ps1) +inputs=(Cargo.toml) for toolchain_file in rust-toolchain rust-toolchain.toml; do if [[ -f "$repo_root/$toolchain_file" ]]; then inputs+=("$toolchain_file") @@ -1193,318 +1189,6 @@ if (($# == 0)); then fi docker "${run_args[@]}" "$image" just "$@" -=== .anvil/resolve-stable-toolchain.ps1 === -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# GENERATED BY cargo-anvil. DO NOT EDIT DIRECTLY. - -param( - [switch] $ValidateWorkspaceMsrv, - [switch] $InstallIfMissing, - [switch] $ForEnvironment, - [switch] $MsrvToolchain, - [switch] $InstallMsrvIfNeeded, - [switch] $EmitToolchainFileOptions, - [switch] $ApplyToolchainFileOptions -) - -$ErrorActionPreference = 'Stop' -Set-StrictMode -Version 3 - -$repoRoot = Split-Path -Parent $PSScriptRoot - -function Get-RootToolchainFilePath { - foreach ($name in @('rust-toolchain', 'rust-toolchain.toml')) { - $path = Join-Path $repoRoot $name - if (Test-Path -LiteralPath $path -PathType Leaf) { - return $path - } - } - return $null -} - -function Read-RootToolchainFile([string] $path) { - $content = Get-Content -LiteralPath $path -Raw - if ([string]::IsNullOrWhiteSpace($content)) { - throw "anvil: root toolchain file '$path' is empty; remove it or declare a toolchain configuration" - } - return $content -} - -function Get-SelectingToolchainFile { - $path = Get-RootToolchainFilePath - if ($null -eq $path) { - return $null - } - - $content = Read-RootToolchainFile $path - if ((Split-Path -Leaf $path) -eq 'rust-toolchain' -and $content.TrimStart() -notmatch '^\[') { - return [pscustomobject]@{ - Path = $path - Value = $content.Trim() - Selector = 'channel' - Source = 'file' - Installable = $false - } - } - - $inToolchain = $false - foreach ($line in $content -split '\r?\n') { - if ($line -match '^\s*\[\s*toolchain\s*\]\s*(?:#.*)?$') { - $inToolchain = $true - continue - } - if ($line -match '^\s*\[') { - $inToolchain = $false - continue - } - if ($inToolchain -and $line -match '^\s*["'']?(channel|path)["'']?\s*=\s*(["''])([^"'']+)\2\s*(?:#.*)?$') { - $value = $Matches[3] - if ($Matches[1] -eq 'path' -and -not [IO.Path]::IsPathRooted($value)) { - $value = [IO.Path]::GetFullPath((Join-Path $repoRoot $value)) - } - return [pscustomobject]@{ - Path = $path - Value = $value - Selector = $Matches[1] - Source = 'file' - Installable = $false - } - } - } - return $null -} - -function Get-ToolchainFileOptions { - $path = Get-RootToolchainFilePath - if ($null -eq $path) { - return $null - } - - $content = Read-RootToolchainFile $path - $section = [regex]::Match( - $content, - '(?ms)^\s*\[\s*toolchain\s*\]\s*(?:#.*)?$(.*?)(?=^\s*\[|\z)' - ) - if (-not $section.Success) { - return $null - } - - $body = $section.Groups[1].Value - $profileMatch = [regex]::Match( - $body, - '(?m)^\s*["'']?profile["'']?\s*=\s*(["''])([^"'']+)\1\s*(?:#.*)?$' - ) - $readArray = { - param([string] $name) - $arrayMatch = [regex]::Match( - $body, - "(?ms)^\s*[`"']?$name[`"']?\s*=\s*\[(.*?)\]" - ) - if (-not $arrayMatch.Success) { - return @() - } - return @( - [regex]::Matches($arrayMatch.Groups[1].Value, '(["''])([^"'']+)\1') | - ForEach-Object { $_.Groups[2].Value } - ) - } - - return [pscustomobject]@{ - Path = $path - Profile = if ($profileMatch.Success) { $profileMatch.Groups[2].Value } else { $null } - Components = @(& $readArray 'components') - Targets = @(& $readArray 'targets') - } -} - -function Get-RootMsrv([switch] $AllowMissing) { - $manifestPath = Join-Path $repoRoot 'Cargo.toml' - if (-not (Test-Path -LiteralPath $manifestPath -PathType Leaf)) { - throw "anvil: Cargo.toml not found at repository root '$repoRoot'" - } - - $values = @{} - $section = '' - foreach ($line in (Get-Content -LiteralPath $manifestPath)) { - if ($line -match '^\s*\[\s*([^\]]+)\s*\]\s*(?:#.*)?$') { - $section = $Matches[1].Trim() - continue - } - if ($section -in @('workspace.package', 'package') -and - $line -match '^\s*["'']?rust-version["'']?\s*=\s*(["''])([^"'']+)\1\s*(?:#.*)?$') { - $values[$section] = $Matches[2] - } - } - - if ($values.ContainsKey('workspace.package')) { - return $values['workspace.package'] - } - if ($values.ContainsKey('package')) { - return $values['package'] - } - - if ($AllowMissing) { - return $null - } - throw 'anvil: no selecting rust-toolchain(.toml) and no root [workspace.package] or [package] rust-version; declare an MSRV or select a toolchain explicitly' -} - -function Assert-WorkspaceMsrvCompatibility { - $manifestPath = Join-Path $repoRoot 'Cargo.toml' - $metadataText = (& cargo metadata --manifest-path $manifestPath --format-version 1 --no-deps 2>&1 | Out-String) - if ($LASTEXITCODE -ne 0) { - throw "anvil: cargo metadata failed while validating workspace MSRVs:`n$metadataText" - } - $metadata = $metadataText | ConvertFrom-Json - $memberIds = [Collections.Generic.HashSet[string]]::new( - [string[]] $metadata.workspace_members, - [StringComparer]::Ordinal - ) - $members = @($metadata.packages | Where-Object { $memberIds.Contains([string] $_.id) }) - $missing = @($members | Where-Object { [string]::IsNullOrWhiteSpace([string] $_.rust_version) } | ForEach-Object name) - if ($missing.Count -gt 0) { - throw "anvil: workspace packages without a resolved rust-version: $($missing -join ', '); declare one or select a toolchain explicitly" - } - - $rootMsrv = Get-RootMsrv - $newer = @( - $members | - Where-Object { [version] ([string] $_.rust_version) -gt [version] $rootMsrv } | - ForEach-Object { "$($_.name) ($($_.rust_version))" } - ) - if ($newer.Count -gt 0) { - throw "anvil: workspace packages require a compiler newer than the root MSRV $rootMsrv`: $($newer -join ', '); raise the root MSRV or select one catalog toolchain explicitly with rust-toolchain.toml" - } -} - -function Assert-CompleteInternalToolchainConfiguration { - if (-not [string]::IsNullOrWhiteSpace($env:ANVIL_MSRV_TOOLCHAIN) -and - [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN) -and - $null -eq $fileSelection) { - throw 'anvil: ANVIL_MSRV_TOOLCHAIN is set without RUSTUP_TOOLCHAIN and no repository toolchain file selects stable checks; set RUSTUP_TOOLCHAIN to the provisioned stable toolchain or unset ANVIL_MSRV_TOOLCHAIN' - } -} - -function Get-MsrvSelection { - $msrv = Get-RootMsrv -AllowMissing - if ([string]::IsNullOrWhiteSpace($msrv)) { - return $null - } - - if (-not [string]::IsNullOrWhiteSpace($env:ANVIL_MSRV_TOOLCHAIN)) { - return [pscustomobject]@{ - Value = $env:ANVIL_MSRV_TOOLCHAIN - Source = 'mapped-msrv' - Installable = $false - Mapped = $true - } - } - return [pscustomobject]@{ Value = $msrv; Source = 'msrv'; Installable = $true; Mapped = $false } -} - -function Apply-ToolchainFileOptions([object] $toolchain) { - if ($toolchain.Source -eq 'file' -or $null -eq $toolchainFileOptions) { - return - } - foreach ($component in $toolchainFileOptions.Components) { - rustup component add --toolchain $toolchain.Value $component - if ($LASTEXITCODE -ne 0) { - Write-Warning "anvil: toolchain-file component '$component' is unavailable for '$($toolchain.Value)'; rustup would skip an unavailable file component" - } - } - foreach ($target in $toolchainFileOptions.Targets) { - rustup target add --toolchain $toolchain.Value $target - if ($LASTEXITCODE -ne 0) { - Write-Warning "anvil: toolchain-file target '$target' is unavailable for '$($toolchain.Value)'; rustup would skip an unavailable file target" - } - } -} - -function Install-Toolchain([object] $toolchain, [string] $label) { - if ($toolchain.Installable) { - $installed = rustup toolchain list - if ($LASTEXITCODE -ne 0) { - throw 'anvil: rustup toolchain list failed' - } - if ($installed -notmatch "(?m)^$([regex]::Escape($toolchain.Value))(?:-|$)") { - $profile = if ($null -ne $toolchainFileOptions -and $toolchainFileOptions.Profile) { - $toolchainFileOptions.Profile - } else { - 'minimal' - } - Write-Host "anvil: installing $label toolchain '$($toolchain.Value)'" - rustup toolchain install $toolchain.Value --profile $profile - if ($LASTEXITCODE -ne 0) { - throw "anvil: failed to install $label toolchain '$($toolchain.Value)'" - } - } - } - Apply-ToolchainFileOptions $toolchain -} - -$fileSelection = Get-SelectingToolchainFile -$toolchainFileOptions = Get-ToolchainFileOptions -if ($EmitToolchainFileOptions) { - if ($null -ne $toolchainFileOptions) { - $toolchainFileOptions | ConvertTo-Json -Compress - } - exit 0 -} -if ($ValidateWorkspaceMsrv) { - Assert-CompleteInternalToolchainConfiguration - $skipWorkspaceMsrvValidation = -not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN) - if (-not $skipWorkspaceMsrvValidation -and $null -eq $fileSelection) { - Assert-WorkspaceMsrvCompatibility - } - exit 0 -} - -if ($MsrvToolchain -or $InstallMsrvIfNeeded) { - $msrvSelection = Get-MsrvSelection - if ($null -eq $msrvSelection) { - exit 0 - } - if ($InstallMsrvIfNeeded) { - if ($msrvSelection.Mapped) { - & cargo "+$($msrvSelection.Value)" --version *> $null - if ($LASTEXITCODE -ne 0) { - throw "anvil: mapped MSRV toolchain '$($msrvSelection.Value)' is unavailable; provision ANVIL_MSRV_TOOLCHAIN or unset it to let Anvil install the declared public MSRV" - } - } - Install-Toolchain $msrvSelection 'MSRV' - } else { - $msrvSelection.Value - } - exit 0 -} - -Assert-CompleteInternalToolchainConfiguration -$selection = if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { - [pscustomobject]@{ Value = $env:RUSTUP_TOOLCHAIN; Source = 'environment'; Installable = $false } -} elseif ($null -ne $fileSelection) { - $fileSelection -} else { - [pscustomobject]@{ Value = Get-RootMsrv; Source = 'msrv'; Installable = $true } -} - -if ($ApplyToolchainFileOptions) { - Apply-ToolchainFileOptions $selection - exit 0 -} - -if ($InstallIfMissing) { - Install-Toolchain $selection 'stable' - exit 0 -} - -if ($ForEnvironment -and $selection.Source -eq 'file') { - '' -} else { - $selection.Value -} -exit 0 - === .delta.toml === # >>> anvil-managed: anvil-delta # Changes to these workspace-level inputs conservatively affect every package. @@ -1946,7 +1630,12 @@ runs: - id: rustc-version shell: pwsh -NoProfile -Command ". '{0}'" run: | - $version = (& just _anvil-with-stable rustc --version | Select-Object -Last 1).Split()[1] + $toolchain = (& just _anvil-resolve-stable for-environment | Out-String).Trim() + if ($LASTEXITCODE -ne 0) { + throw 'anvil: failed to select stable toolchain arguments' + } + $toolchainArgs = if ([string]::IsNullOrWhiteSpace($toolchain)) { @() } else { @("+$toolchain") } + $version = (& rustc @toolchainArgs --version | Select-Object -Last 1).Split()[1] "version=$version" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append - name: Restore cargo cache id: cargo-cache @@ -2928,7 +2617,7 @@ anvil-aprz: anvil-aprz-validate-prereqs Write-Warning 'To fix: run `gh auth login` (recommended), or set $env:GITHUB_TOKEN to a GitHub token, then re-run.' } } - & "{{ just_executable() }}" _anvil-with-stable cargo aprz deps --error-if-high-risk --console appraisal + & cargo {{_anvil_stable_toolchain_args}} aprz deps --error-if-high-risk --console appraisal if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-aprz` recipe. @@ -2954,7 +2643,7 @@ anvil-aprz-validate-prereqs: anvil-tool-cargo-aprz-validate-prereqs [script("pwsh", "-NoProfile")] anvil-audit: anvil-audit-validate-prereqs $ErrorActionPreference = 'Stop' - & "{{ just_executable() }}" _anvil-with-stable cargo audit + & cargo {{_anvil_stable_toolchain_args}} audit if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-audit` recipe. @@ -2982,7 +2671,7 @@ anvil-bench: anvil-bench-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include affected) if ($include -eq '--skip') { Write-Host 'anvil-bench: no affected packages; skipping'; exit 0 } - & "{{ just_executable() }}" _anvil-with-stable cargo bench @(if ($include) { -split $include } else { '--workspace' }) --all-features --no-run + & cargo {{_anvil_stable_toolchain_args}} bench @(if ($include) { -split $include } else { '--workspace' }) --all-features --no-run if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # bench uses the cargo built-in in compile-only mode; no extra tool install @@ -3053,7 +2742,7 @@ anvil-bolero: anvil-bolero-validate-prereqs anvil-impact $packageNames.Add(($pkgArgs[++$i] -split '@', 2)[0]) } } else { - $metadataJson = & "{{ just_executable() }}" _anvil-with-stable cargo metadata --no-deps --format-version 1 + $metadataJson = & cargo {{_anvil_stable_toolchain_args}} metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-bolero: cargo metadata failed' exit $LASTEXITCODE @@ -3217,7 +2906,7 @@ anvil-cargo-hack: anvil-cargo-hack-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include required) if ($include -eq '--skip') { Write-Host 'anvil-cargo-hack: no affected packages; skipping'; exit 0 } - & "{{ just_executable() }}" _anvil-with-stable cargo hack @(if ($include) { -split $include } else { '--workspace' }) --feature-powerset --depth 2 check + & cargo {{_anvil_stable_toolchain_args}} hack @(if ($include) { -split $include } else { '--workspace' }) --feature-powerset --depth 2 check if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-cargo-hack` recipe. @@ -3256,7 +2945,7 @@ anvil-cargo-sort: anvil-cargo-sort-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include modified) if ($include -eq '--skip') { Write-Host 'anvil-cargo-sort: no modified packages; skipping'; exit 0 } - & "{{ just_executable() }}" _anvil-with-stable cargo sort --workspace --grouped --check --check-format + & cargo {{_anvil_stable_toolchain_args}} sort --workspace --grouped --check --check-format if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-cargo-sort` recipe. @@ -3289,7 +2978,7 @@ anvil-clippy: anvil-clippy-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include affected) if ($include -eq '--skip') { Write-Host 'anvil-clippy: no affected packages; skipping'; exit 0 } - & "{{ just_executable() }}" _anvil-with-stable cargo clippy @(if ($include) { -split $include } else { '--workspace' }) --all-targets --all-features --locked "--" '-D' 'warnings' + & cargo {{_anvil_stable_toolchain_args}} clippy @(if ($include) { -split $include } else { '--workspace' }) --all-targets --all-features --locked "--" '-D' 'warnings' if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-clippy` recipe. @@ -3317,7 +3006,7 @@ anvil-clippy-validate-prereqs: anvil-component-default-clippy-validate-prereqs [script("pwsh", "-NoProfile")] anvil-deny: anvil-deny-validate-prereqs $ErrorActionPreference = 'Stop' - & "{{ just_executable() }}" _anvil-with-stable cargo deny check + & cargo {{_anvil_stable_toolchain_args}} deny check if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-deny` recipe. @@ -3348,7 +3037,7 @@ anvil-doc-build: anvil-doc-build-validate-prereqs anvil-impact $include = (& "{{ just_executable() }}" _anvil-impact-include required) if ($include -eq '--skip') { Write-Host 'anvil-doc-build: no affected packages; skipping'; exit 0 } $env:RUSTDOCFLAGS = '-D warnings' - & "{{ just_executable() }}" _anvil-with-stable cargo doc @(if ($include) { -split $include } else { '--workspace' }) --all-features --no-deps + & cargo {{_anvil_stable_toolchain_args}} doc @(if ($include) { -split $include } else { '--workspace' }) --all-features --no-deps if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # doc-build, examples and doc-test are pure cargo built-ins; the rust @@ -3386,9 +3075,9 @@ anvil-doc-test: anvil-doc-test-validate-prereqs anvil-impact $include = (& "{{ just_executable() }}" _anvil-impact-include affected) if ($include -eq '--skip') { Write-Host 'anvil-doc-test: no affected packages; skipping'; exit 0 } $pkg = @(if ($include) { -split $include } else { '--workspace' }) - & "{{ just_executable() }}" _anvil-with-stable cargo test --doc @pkg --all-features --locked + & cargo {{_anvil_stable_toolchain_args}} test --doc @pkg --all-features --locked if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - & "{{ just_executable() }}" _anvil-with-stable cargo test --doc @pkg --locked + & cargo {{_anvil_stable_toolchain_args}} test --doc @pkg --locked if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-doc-test` recipe. @@ -3416,7 +3105,7 @@ anvil-ensure-no-cyclic-deps: anvil-ensure-no-cyclic-deps-validate-prereqs anvil- $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include modified) if ($include -eq '--skip') { Write-Host 'anvil-ensure-no-cyclic-deps: no modified packages; skipping'; exit 0 } - & "{{ just_executable() }}" _anvil-with-stable cargo ensure-no-cyclic-deps + & cargo {{_anvil_stable_toolchain_args}} ensure-no-cyclic-deps if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-ensure-no-cyclic-deps` recipe. @@ -3444,7 +3133,7 @@ anvil-ensure-no-default-features: anvil-ensure-no-default-features-validate-prer $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include modified) if ($include -eq '--skip') { Write-Host 'anvil-ensure-no-default-features: no modified packages; skipping'; exit 0 } - & "{{ just_executable() }}" _anvil-with-stable cargo ensure-no-default-features + & cargo {{_anvil_stable_toolchain_args}} ensure-no-default-features if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-ensure-no-default-features` recipe. @@ -3472,7 +3161,7 @@ anvil-examples: anvil-examples-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include affected) if ($include -eq '--skip') { Write-Host 'anvil-examples: no affected packages; skipping'; exit 0 } - & "{{ just_executable() }}" _anvil-with-stable cargo build @(if ($include) { -split $include } else { '--workspace' }) --examples --all-features --locked + & cargo {{_anvil_stable_toolchain_args}} build @(if ($include) { -split $include } else { '--workspace' }) --examples --all-features --locked if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-examples` recipe. @@ -3517,7 +3206,7 @@ anvil-external-types: anvil-external-types-validate-prereqs anvil-impact $pkg = @(if ($include) { -split $include } else { '--workspace' }) # Build pkg-name -> manifest-path map, restricted to library crates. $libPkgs = @{} - $metadataJson = & "{{ just_executable() }}" _anvil-with-stable cargo metadata --no-deps --format-version 1 + $metadataJson = & cargo {{_anvil_stable_toolchain_args}} metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-external-types: cargo metadata failed' exit $LASTEXITCODE @@ -3645,7 +3334,7 @@ anvil-license-headers: anvil-license-headers-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include modified) if ($include -eq '--skip') { Write-Host 'anvil-license-headers: no modified packages; skipping'; exit 0 } - & "{{ just_executable() }}" _anvil-with-stable cargo heather + & cargo {{_anvil_stable_toolchain_args}} heather if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-license-headers` recipe. @@ -3696,7 +3385,7 @@ anvil-llvm-cov: anvil-llvm-cov-validate-prereqs anvil-impact # never opts a changed package out of tests. $testOnlyPkg = [System.Collections.Generic.List[string]]::new() if ($pkg -contains '--package') { - $metadataJson = & "{{ just_executable() }}" _anvil-with-stable cargo metadata --no-deps --format-version 1 + $metadataJson = & cargo {{_anvil_stable_toolchain_args}} metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-llvm-cov: cargo metadata failed' exit $LASTEXITCODE @@ -3827,7 +3516,7 @@ anvil-llvm-cov: anvil-llvm-cov-validate-prereqs anvil-impact } $gateArgs.Add($pkg[$i]) } - & "{{ just_executable() }}" _anvil-with-stable cargo coverage-gate @gateArgs --lcov target/coverage/lcov-all-features.info --lcov target/coverage/lcov-no-default.info + & cargo {{_anvil_stable_toolchain_args}} coverage-gate @gateArgs --lcov target/coverage/lcov-all-features.info --lcov target/coverage/lcov-no-default.info if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # --- pr-test members (shared with scheduled-test) --- @@ -3898,7 +3587,7 @@ anvil-loom: anvil-loom-validate-prereqs anvil-impact Write-Host 'anvil-loom: no affected packages; skipping' exit 0 } - $meta = & "{{ just_executable() }}" _anvil-with-stable cargo metadata --no-deps --format-version 1 | ConvertFrom-Json + $meta = & cargo {{_anvil_stable_toolchain_args}} metadata --no-deps --format-version 1 | ConvertFrom-Json if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Optional impact-scoping: recover bare package names from the @@ -3951,7 +3640,7 @@ anvil-loom: anvil-loom-validate-prereqs anvil-impact $env:RUSTFLAGS = "--cfg loom $($env:RUSTFLAGS)".Trim() foreach ($lt in $loomTargets) { Write-Host "anvil-loom: $($lt.pkg) :: $($lt.target)" - & "{{ just_executable() }}" _anvil-with-stable cargo test -p $lt.pkg --release --all-features --locked --test $lt.target -- --test-threads=1 + & cargo {{_anvil_stable_toolchain_args}} test -p $lt.pkg --release --all-features --locked --test $lt.target -- --test-threads=1 if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } } @@ -4239,7 +3928,7 @@ anvil-mutants-diff: anvil-mutants-diff-validate-prereqs anvil-impact $diff_path = Join-Path $tmp_dir 'anvil-mutants-diff.diff' git diff "$base..HEAD" --output=$diff_path if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - & "{{ just_executable() }}" _anvil-with-stable cargo mutants --in-diff $diff_path --no-shuffle --jobs 0 + & cargo {{_anvil_stable_toolchain_args}} mutants --in-diff $diff_path --no-shuffle --jobs 0 if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # --- pr-mutants members --- @@ -4272,7 +3961,7 @@ anvil-mutants-full: anvil-mutants-full-validate-prereqs Write-Host 'anvil-mutants-full: aarch64-pc-windows-msvc -- cargo-mutants does not build here (winapi); skipping' exit 0 } - & "{{ just_executable() }}" _anvil-with-stable cargo mutants --workspace --no-shuffle --jobs 0 + & cargo {{_anvil_stable_toolchain_args}} mutants --workspace --no-shuffle --jobs 0 if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # --- scheduled-exhaustive members (mutants-full reuses cargo-mutants; @@ -4420,7 +4109,7 @@ anvil-readme-check: anvil-readme-check-validate-prereqs # Unscoped: a change to the workspace-level README template is a repo-level # input cargo-delta cannot map to a package, so this check always runs the # full library set rather than an impact subset. - $metadataJson = & "{{ just_executable() }}" _anvil-with-stable cargo metadata --no-deps --format-version 1 + $metadataJson = & cargo {{_anvil_stable_toolchain_args}} metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-readme-check: cargo metadata failed' exit $LASTEXITCODE @@ -4462,7 +4151,7 @@ anvil-readme-check: anvil-readme-check-validate-prereqs } $args = @('doc2readme', '--check') if ($relTemplate) { $args += @('--template', $relTemplate) } - & "{{ just_executable() }}" _anvil-with-stable cargo @args + & cargo {{_anvil_stable_toolchain_args}} @args if ($LASTEXITCODE -ne 0) { $hadFailure = $true } } finally { Pop-Location @@ -4546,7 +4235,7 @@ anvil-semver-check: anvil-semver-check-validate-prereqs anvil-impact # we got here via impact-scoping (cloud workflows) or full-workspace # fallback (local). $libPkgs = @{} - $metadataJson = & "{{ just_executable() }}" _anvil-with-stable cargo metadata --no-deps --format-version 1 + $metadataJson = & cargo {{_anvil_stable_toolchain_args}} metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-semver-check: cargo metadata failed' exit $LASTEXITCODE @@ -4623,7 +4312,7 @@ anvil-semver-check: anvil-semver-check-validate-prereqs anvil-impact } Write-Host "anvil-semver-check: $p (baseline $base)" - $outputLines = @(& "{{ just_executable() }}" _anvil-with-stable cargo semver-checks --package $p --baseline-rev $base 2>&1) + $outputLines = @(& cargo {{_anvil_stable_toolchain_args}} semver-checks --package $p --baseline-rev $base 2>&1) $semverExit = $LASTEXITCODE $output = ($outputLines | Out-String) if ($semverExit -eq 100) { @@ -4762,9 +4451,9 @@ anvil-spellcheck: anvil-spellcheck-validate-prereqs # ignores user-curated dictionaries (`extra_dictionaries`, custom # hunspell langs, etc.). if (Test-Path 'spellcheck.toml') { - & "{{ just_executable() }}" _anvil-with-stable cargo spellcheck --cfg spellcheck.toml check --code 1 + & cargo {{_anvil_stable_toolchain_args}} spellcheck --cfg spellcheck.toml check --code 1 } else { - & "{{ just_executable() }}" _anvil-with-stable cargo spellcheck check --code 1 + & cargo {{_anvil_stable_toolchain_args}} spellcheck check --code 1 } if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } @@ -5631,9 +5320,13 @@ _anvil-impact-snapshot: # selections are already valid rustup overrides. For a selecting # toolchain file, ask rustup for the concrete active identifier so # `path` selections are not mistaken for named toolchains. - $baselineToolchain = (& "{{ just_executable() }}" _anvil-resolve-stable for-environment | Out-String).Trim() - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - if ([string]::IsNullOrWhiteSpace($baselineToolchain)) { + $stableArgs = {{_anvil_stable_toolchain_args}} + $baselineToolchain = if ($stableArgs.Count -eq 1) { + $stableArgs[0].Substring(1) + } else { + $null + } + if ($null -eq $baselineToolchain) { if (-not (Get-Command rustup -ErrorAction SilentlyContinue)) { Write-Error 'anvil-impact: rustup is required to resolve the active repository toolchain for the baseline snapshot' exit 1 @@ -5708,7 +5401,7 @@ _anvil-impact-snapshot: Write-Host 'anvil-impact: current snapshot up to date' } else { Write-Host 'anvil-impact: snapshotting working tree' - $curSnap = & "{{ just_executable() }}" _anvil-with-stable cargo delta @deltaArgs snapshot + $curSnap = & cargo {{_anvil_stable_toolchain_args}} delta @deltaArgs snapshot if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-impact: cargo delta snapshot (current) failed'; exit 1 } Set-Content -LiteralPath $currentJson -Value $curSnap -Encoding UTF8 Set-Content -LiteralPath $currentKeyFile -Value $currentState -Encoding UTF8 -NoNewline @@ -5826,7 +5519,7 @@ anvil-impact: _anvil-impact-snapshot # early), so default to an empty object so impact.json always exists -- # the freshness check above keys on its presence, and a missing file # would force a recompute on every invocation. - $impactOut = (& "{{ just_executable() }}" _anvil-with-stable cargo delta @deltaArgs impact --baseline $baselineJson --current $currentJson --format json | Out-String) + $impactOut = (& cargo {{_anvil_stable_toolchain_args}} delta @deltaArgs impact --baseline $baselineJson --current $currentJson --format json | Out-String) if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-impact: cargo delta impact failed'; exit 1 } if ([string]::IsNullOrWhiteSpace($impactOut)) { $impactOut = '{}' } Set-Content -LiteralPath $impactJson -Value $impactOut.Trim() -Encoding UTF8 @@ -5903,7 +5596,7 @@ _anvil-impact-format tier impactJson: # nested workspaces cargo-delta can instead emit a manifest directory # leaf; accept any reported identifier only when every matching namespace # resolves to the *same* workspace package. - $metadataJson = & "{{ just_executable() }}" _anvil-with-stable cargo metadata --no-deps --format-version 1 + $metadataJson = & cargo {{_anvil_stable_toolchain_args}} metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-impact: cargo metadata failed' exit $LASTEXITCODE @@ -6485,37 +6178,318 @@ anvil-tool-pwsh-validate-prereqs: [script("pwsh", "-NoProfile")] _anvil-resolve-stable action="resolve": $ErrorActionPreference = 'Stop' - $resolverArgs = @(switch ('{{action}}') { - 'resolve' { @() } - 'for-environment' { @('-ForEnvironment') } - 'install' { @('-InstallIfMissing') } - 'apply-file-options' { @('-ApplyToolchainFileOptions') } - 'validate-workspace-msrv' { @('-ValidateWorkspaceMsrv') } - 'msrv' { @('-MsrvToolchain') } - 'install-msrv' { @('-InstallMsrvIfNeeded') } - default { - Write-Error "_anvil-resolve-stable: unknown action '{{action}}'" - exit 2 + Set-StrictMode -Version 3 + + $action = '{{action}}' + $validActions = @( + 'resolve', + 'for-environment', + 'install', + 'apply-file-options', + 'validate-workspace-msrv', + 'msrv', + 'install-msrv', + 'emit-file-options' + ) + if ($action -notin $validActions) { + Write-Error "_anvil-resolve-stable: unknown action '$action'" + exit 2 + } + + $repoRoot = '{{justfile_directory()}}' + + function Get-RootToolchainFilePath { + foreach ($name in @('rust-toolchain', 'rust-toolchain.toml')) { + $path = Join-Path $repoRoot $name + if (Test-Path -LiteralPath $path -PathType Leaf) { + return $path + } } - }) - & pwsh -NoProfile -File '.anvil/resolve-stable-toolchain.ps1' @resolverArgs - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + return $null + } -# Run one command under the selected stable compiler. A repository toolchain -# file yields no override so rustup processes its complete configuration. -[script("pwsh", "-NoProfile")] -_anvil-with-stable command *args: - $ErrorActionPreference = 'Stop' - $toolchain = (& "{{ just_executable() }}" _anvil-resolve-stable for-environment | Out-String).Trim() - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - if ([string]::IsNullOrWhiteSpace($toolchain)) { - Remove-Item Env:\RUSTUP_TOOLCHAIN -ErrorAction SilentlyContinue + function Read-RootToolchainFile([string] $path) { + $content = Get-Content -LiteralPath $path -Raw + if ([string]::IsNullOrWhiteSpace($content)) { + throw "anvil: root toolchain file '$path' is empty; remove it or declare a toolchain configuration" + } + return $content + } + + function Get-SelectingToolchainFile { + $path = Get-RootToolchainFilePath + if ($null -eq $path) { + return $null + } + + $content = Read-RootToolchainFile $path + if ((Split-Path -Leaf $path) -eq 'rust-toolchain' -and $content.TrimStart() -notmatch '^\[') { + return [pscustomobject]@{ + Path = $path + Value = $content.Trim() + Selector = 'channel' + Source = 'file' + Installable = $false + } + } + + $inToolchain = $false + foreach ($line in $content -split '\r?\n') { + if ($line -match '^\s*\[\s*toolchain\s*\]\s*(?:#.*)?$') { + $inToolchain = $true + continue + } + if ($line -match '^\s*\[') { + $inToolchain = $false + continue + } + if ($inToolchain -and $line -match '^\s*["'']?(channel|path)["'']?\s*=\s*(["''])([^"'']+)\2\s*(?:#.*)?$') { + $value = $Matches[3] + if ($Matches[1] -eq 'path' -and -not [IO.Path]::IsPathRooted($value)) { + $value = [IO.Path]::GetFullPath((Join-Path $repoRoot $value)) + } + return [pscustomobject]@{ + Path = $path + Value = $value + Selector = $Matches[1] + Source = 'file' + Installable = $false + } + } + } + return $null + } + + function Get-ToolchainFileOptions { + $path = Get-RootToolchainFilePath + if ($null -eq $path) { + return $null + } + + $content = Read-RootToolchainFile $path + $section = [regex]::Match( + $content, + '(?ms)^\s*\[\s*toolchain\s*\]\s*(?:#.*)?$(.*?)(?=^\s*\[|\z)' + ) + if (-not $section.Success) { + return $null + } + + $body = $section.Groups[1].Value + $profileMatch = [regex]::Match( + $body, + '(?m)^\s*["'']?profile["'']?\s*=\s*(["''])([^"'']+)\1\s*(?:#.*)?$' + ) + $readArray = { + param([string] $name) + $arrayMatch = [regex]::Match( + $body, + "(?ms)^\s*[`"']?$name[`"']?\s*=\s*\[(.*?)\]" + ) + if (-not $arrayMatch.Success) { + return @() + } + return @( + [regex]::Matches($arrayMatch.Groups[1].Value, '(["''])([^"'']+)\1') | + ForEach-Object { $_.Groups[2].Value } + ) + } + + return [pscustomobject]@{ + Path = $path + Profile = if ($profileMatch.Success) { $profileMatch.Groups[2].Value } else { $null } + Components = @(& $readArray 'components') + Targets = @(& $readArray 'targets') + } + } + + function Get-RootMsrv([switch] $AllowMissing) { + $manifestPath = Join-Path $repoRoot 'Cargo.toml' + if (-not (Test-Path -LiteralPath $manifestPath -PathType Leaf)) { + throw "anvil: Cargo.toml not found at repository root '$repoRoot'" + } + + $values = @{} + $section = '' + foreach ($line in (Get-Content -LiteralPath $manifestPath)) { + if ($line -match '^\s*\[\s*([^\]]+)\s*\]\s*(?:#.*)?$') { + $section = $Matches[1].Trim() + continue + } + if ($section -in @('workspace.package', 'package') -and + $line -match '^\s*["'']?rust-version["'']?\s*=\s*(["''])([^"'']+)\1\s*(?:#.*)?$') { + $values[$section] = $Matches[2] + } + } + + if ($values.ContainsKey('workspace.package')) { + return $values['workspace.package'] + } + if ($values.ContainsKey('package')) { + return $values['package'] + } + + if ($AllowMissing) { + return $null + } + throw 'anvil: no selecting rust-toolchain(.toml) and no root [workspace.package] or [package] rust-version; declare an MSRV or select a toolchain explicitly' + } + + function Assert-WorkspaceMsrvCompatibility { + $manifestPath = Join-Path $repoRoot 'Cargo.toml' + $metadataText = (& cargo metadata --manifest-path $manifestPath --format-version 1 --no-deps 2>&1 | Out-String) + if ($LASTEXITCODE -ne 0) { + throw "anvil: cargo metadata failed while validating workspace MSRVs:`n$metadataText" + } + $metadata = $metadataText | ConvertFrom-Json + $memberIds = [Collections.Generic.HashSet[string]]::new( + [string[]] $metadata.workspace_members, + [StringComparer]::Ordinal + ) + $members = @($metadata.packages | Where-Object { $memberIds.Contains([string] $_.id) }) + $missing = @($members | Where-Object { [string]::IsNullOrWhiteSpace([string] $_.rust_version) } | ForEach-Object name) + if ($missing.Count -gt 0) { + throw "anvil: workspace packages without a resolved rust-version: $($missing -join ', '); declare one or select a toolchain explicitly" + } + + $rootMsrv = Get-RootMsrv + $newer = @( + $members | + Where-Object { [version] ([string] $_.rust_version) -gt [version] $rootMsrv } | + ForEach-Object { "$($_.name) ($($_.rust_version))" } + ) + if ($newer.Count -gt 0) { + throw "anvil: workspace packages require a compiler newer than the root MSRV $rootMsrv`: $($newer -join ', '); raise the root MSRV or select one catalog toolchain explicitly with rust-toolchain.toml" + } + } + + function Assert-CompleteInternalToolchainConfiguration { + if (-not [string]::IsNullOrWhiteSpace($env:ANVIL_MSRV_TOOLCHAIN) -and + [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN) -and + $null -eq $fileSelection) { + throw 'anvil: ANVIL_MSRV_TOOLCHAIN is set without RUSTUP_TOOLCHAIN and no repository toolchain file selects stable checks; set RUSTUP_TOOLCHAIN to the provisioned stable toolchain or unset ANVIL_MSRV_TOOLCHAIN' + } + } + + function Get-MsrvSelection { + $msrv = Get-RootMsrv -AllowMissing + if ([string]::IsNullOrWhiteSpace($msrv)) { + return $null + } + + if (-not [string]::IsNullOrWhiteSpace($env:ANVIL_MSRV_TOOLCHAIN)) { + return [pscustomobject]@{ + Value = $env:ANVIL_MSRV_TOOLCHAIN + Source = 'mapped-msrv' + Installable = $false + Mapped = $true + } + } + return [pscustomobject]@{ Value = $msrv; Source = 'msrv'; Installable = $true; Mapped = $false } + } + + function Apply-ToolchainFileOptions([object] $toolchain) { + if ($toolchain.Source -eq 'file' -or $null -eq $toolchainFileOptions) { + return + } + foreach ($component in $toolchainFileOptions.Components) { + rustup component add --toolchain $toolchain.Value $component + if ($LASTEXITCODE -ne 0) { + Write-Warning "anvil: toolchain-file component '$component' is unavailable for '$($toolchain.Value)'; rustup would skip an unavailable file component" + } + } + foreach ($target in $toolchainFileOptions.Targets) { + rustup target add --toolchain $toolchain.Value $target + if ($LASTEXITCODE -ne 0) { + Write-Warning "anvil: toolchain-file target '$target' is unavailable for '$($toolchain.Value)'; rustup would skip an unavailable file target" + } + } + } + + function Install-Toolchain([object] $toolchain, [string] $label) { + if ($toolchain.Installable) { + $installed = rustup toolchain list + if ($LASTEXITCODE -ne 0) { + throw 'anvil: rustup toolchain list failed' + } + if ($installed -notmatch "(?m)^$([regex]::Escape($toolchain.Value))(?:-|$)") { + $profile = if ($null -ne $toolchainFileOptions -and $toolchainFileOptions.Profile) { + $toolchainFileOptions.Profile + } else { + 'minimal' + } + Write-Host "anvil: installing $label toolchain '$($toolchain.Value)'" + rustup toolchain install $toolchain.Value --profile $profile + if ($LASTEXITCODE -ne 0) { + throw "anvil: failed to install $label toolchain '$($toolchain.Value)'" + } + } + } + Apply-ToolchainFileOptions $toolchain + } + + $fileSelection = Get-SelectingToolchainFile + $toolchainFileOptions = Get-ToolchainFileOptions + + if ($action -eq 'emit-file-options') { + if ($null -ne $toolchainFileOptions) { + $toolchainFileOptions | ConvertTo-Json -Compress + } + exit 0 + } + + if ($action -eq 'validate-workspace-msrv') { + Assert-CompleteInternalToolchainConfiguration + $skipWorkspaceMsrvValidation = -not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN) + if (-not $skipWorkspaceMsrvValidation -and $null -eq $fileSelection) { + Assert-WorkspaceMsrvCompatibility + } + exit 0 + } + + if ($action -in @('msrv', 'install-msrv')) { + $msrvSelection = Get-MsrvSelection + if ($null -eq $msrvSelection) { + exit 0 + } + if ($action -eq 'install-msrv') { + if ($msrvSelection.Mapped) { + & cargo "+$($msrvSelection.Value)" --version *> $null + if ($LASTEXITCODE -ne 0) { + throw "anvil: mapped MSRV toolchain '$($msrvSelection.Value)' is unavailable; provision ANVIL_MSRV_TOOLCHAIN or unset it to let Anvil install the declared public MSRV" + } + } + Install-Toolchain $msrvSelection 'MSRV' + } else { + $msrvSelection.Value + } + exit 0 + } + + Assert-CompleteInternalToolchainConfiguration + $selection = if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { + [pscustomobject]@{ Value = $env:RUSTUP_TOOLCHAIN; Source = 'environment'; Installable = $false } + } elseif ($null -ne $fileSelection) { + $fileSelection } else { - $env:RUSTUP_TOOLCHAIN = $toolchain + [pscustomobject]@{ Value = Get-RootMsrv; Source = 'msrv'; Installable = $true } + } + + if ($action -eq 'apply-file-options') { + Apply-ToolchainFileOptions $selection + exit 0 + } + + if ($action -eq 'install') { + Install-Toolchain $selection 'stable' + exit 0 + } + + if ($action -eq 'for-environment' -and $selection.Source -eq 'file') { + '' + } else { + $selection.Value } - Set-Location '{{invocation_directory_native()}}' - & '{{command}}' {{args}} - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # _install-tool: install a cargo subcommand at the catalog version, # or no-op if it is already installed at or above that version. The @@ -6547,7 +6521,7 @@ _install-tool name version installer source_prereq="": # cached binaries and fail with "binary already exists in destination". $installed = $null $pattern = '^' + [regex]::Escape($name) + ' v(\S+):' - foreach ($line in (& "{{ just_executable() }}" _anvil-with-stable cargo install --list 2>$null)) { + foreach ($line in (& cargo {{_anvil_stable_toolchain_args}} install --list 2>$null)) { if ($line -match $pattern) { $installed = $Matches[1]; break } } if ($installed) { @@ -6565,7 +6539,7 @@ _install-tool name version installer source_prereq="": if ($installer -eq 'binstall') { if (-not (Get-Command cargo-binstall -ErrorAction SilentlyContinue)) { Write-Host ' Bootstrapping cargo-binstall' - & "{{ just_executable() }}" _anvil-with-stable cargo install --locked cargo-binstall + & cargo {{_anvil_stable_toolchain_args}} install --locked cargo-binstall if ($LASTEXITCODE -ne 0) { Write-Error 'cargo-binstall bootstrap failed' exit $LASTEXITCODE @@ -6579,7 +6553,7 @@ _install-tool name version installer source_prereq="": $binstallArgs += @('--disable-strategies', 'compile') } $binstallArgs += @($name, '--version', "=$version") - & "{{ just_executable() }}" _anvil-with-stable cargo @binstallArgs + & cargo {{_anvil_stable_toolchain_args}} @binstallArgs if ($LASTEXITCODE -eq 0) { exit 0 } Write-Host ' binstall failed; falling back to cargo install' -ForegroundColor Yellow } @@ -6587,7 +6561,7 @@ _install-tool name version installer source_prereq="": & "{{just_executable()}}" $sourcePrereq if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } } - & "{{ just_executable() }}" _anvil-with-stable cargo install --locked $name --version "=$version" + & cargo {{_anvil_stable_toolchain_args}} install --locked $name --version "=$version" if ($LASTEXITCODE -ne 0) { Write-Error "$name install FAILED" exit $LASTEXITCODE @@ -6603,7 +6577,7 @@ _check-tool name version: $installed = $null $pattern = '^' + [regex]::Escape($name) + ' v(\S+):' - foreach ($line in (& "{{ just_executable() }}" _anvil-with-stable cargo install --list 2>$null)) { + foreach ($line in (& cargo {{_anvil_stable_toolchain_args}} install --list 2>$null)) { if ($line -match $pattern) { $installed = $Matches[1]; break } } if (-not $installed) { @@ -6684,14 +6658,14 @@ _install-component toolchain component: } if ($null -ne $subcommand) { if ($toolchain -eq 'default') { - & "{{ just_executable() }}" _anvil-with-stable cargo $subcommand --version *> $null + & cargo {{_anvil_stable_toolchain_args}} $subcommand --version *> $null } else { & cargo "+$toolchain" $subcommand --version *> $null } $present = ($LASTEXITCODE -eq 0) } else { $sysroot = if ($toolchain -eq 'default') { - & "{{ just_executable() }}" _anvil-with-stable rustc --print sysroot 2>$null + & rustc {{_anvil_stable_toolchain_args}} --print sysroot 2>$null } else { & rustc "+$toolchain" --print sysroot 2>$null } @@ -6752,14 +6726,14 @@ _check-component toolchain component: } if ($null -ne $subcommand) { if ($toolchain -eq 'default') { - & "{{ just_executable() }}" _anvil-with-stable cargo $subcommand --version *> $null + & cargo {{_anvil_stable_toolchain_args}} $subcommand --version *> $null } else { & cargo "+$toolchain" $subcommand --version *> $null } $present = ($LASTEXITCODE -eq 0) } else { $sysroot = if ($toolchain -eq 'default') { - & "{{ just_executable() }}" _anvil-with-stable rustc --print sysroot 2>$null + & rustc {{_anvil_stable_toolchain_args}} --print sysroot 2>$null } else { & rustc "+$toolchain" --print sysroot 2>$null } @@ -7096,6 +7070,8 @@ anvil-tool-cargo-udeps-validate-prereqs: (_check-tool "cargo-udeps" cargo_udeps_ # Update behaviour: https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/updates.md # # Pinned versions used by the anvil check tree. + +set lazy # # Everything anvil installs (cargo subcommands + rustup toolchains) # is pinned here. The pinning policy: @@ -7121,9 +7097,102 @@ anvil-tool-cargo-udeps-validate-prereqs: (_check-tool "cargo-udeps" cargo_udeps_ # Rustup toolchains # ============================================================================ -# Stable checks select their compiler through `_anvil-with-stable` at the -# individual command site. `RUSTUP_TOOLCHAIN` is an input override only; Anvil -# never exports a resolved value globally into unrelated recipes or helpers. +# Stable commands interpolate this lazy PowerShell argument-array expression +# directly at each command site. `RUSTUP_TOOLCHAIN` is an input override only; +# Anvil never exports a resolved value globally into unrelated recipes or +# helpers. +[private] +_anvil_stable_toolchain_selection_script := ''' +param([string] $RepoRoot) + +$ErrorActionPreference = 'Stop' +Set-StrictMode -Version 3 + +function Write-ExplicitToolchainArgs([string] $Toolchain) { + Write-Output ("@('+" + $Toolchain.Replace("'", "''") + "')") +} + +if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { + Write-ExplicitToolchainArgs $env:RUSTUP_TOOLCHAIN + exit 0 +} + +$toolchainFile = $null +foreach ($name in @('rust-toolchain', 'rust-toolchain.toml')) { + $candidate = Join-Path $RepoRoot $name + if (Test-Path -LiteralPath $candidate -PathType Leaf) { + $toolchainFile = $candidate + break + } +} + +$selectingFile = $false +if ($null -ne $toolchainFile) { + $content = Get-Content -LiteralPath $toolchainFile -Raw + if ([string]::IsNullOrWhiteSpace($content)) { + throw "anvil: root toolchain file '$toolchainFile' is empty; remove it or declare a toolchain configuration" + } + if ((Split-Path -Leaf $toolchainFile) -eq 'rust-toolchain' -and $content.TrimStart() -notmatch '^\[') { + $selectingFile = $true + } else { + $inToolchain = $false + foreach ($line in $content -split '\r?\n') { + if ($line -match '^\s*\[\s*toolchain\s*\]\s*(?:#.*)?$') { + $inToolchain = $true + continue + } + if ($line -match '^\s*\[') { + $inToolchain = $false + continue + } + if ($inToolchain -and $line -match '^\s*["'']?(channel|path)["'']?\s*=\s*(["''])([^"'']+)\2\s*(?:#.*)?$') { + $selectingFile = $true + break + } + } + } +} + +if ($selectingFile) { + Write-Output '@()' + exit 0 +} + +if (-not [string]::IsNullOrWhiteSpace($env:ANVIL_MSRV_TOOLCHAIN)) { + throw 'anvil: ANVIL_MSRV_TOOLCHAIN is set without RUSTUP_TOOLCHAIN and no repository toolchain file selects stable checks; set RUSTUP_TOOLCHAIN to the provisioned stable toolchain or unset ANVIL_MSRV_TOOLCHAIN' +} + +$manifestPath = Join-Path $RepoRoot 'Cargo.toml' +if (-not (Test-Path -LiteralPath $manifestPath -PathType Leaf)) { + throw "anvil: Cargo.toml not found at repository root '$RepoRoot'" +} + +$values = @{} +$section = '' +foreach ($line in (Get-Content -LiteralPath $manifestPath)) { + if ($line -match '^\s*\[\s*([^\]]+)\s*\]\s*(?:#.*)?$') { + $section = $Matches[1].Trim() + continue + } + if ($section -in @('workspace.package', 'package') -and + $line -match '^\s*["'']?rust-version["'']?\s*=\s*(["''])([^"'']+)\1\s*(?:#.*)?$') { + $values[$section] = $Matches[2] + } +} + +$msrv = if ($values.ContainsKey('workspace.package')) { + $values['workspace.package'] +} elseif ($values.ContainsKey('package')) { + $values['package'] +} else { + throw 'anvil: no selecting rust-toolchain(.toml) and no root [workspace.package] or [package] rust-version; declare an MSRV or select a toolchain explicitly' +} +Write-ExplicitToolchainArgs $msrv +exit 0 +''' + +[private] +_anvil_stable_toolchain_args := if os_family() == "windows" { shell("& ([scriptblock]::Create('" + replace(_anvil_stable_toolchain_selection_script, "'", "''") + "')) '" + replace(justfile_directory(), "'", "''") + "';") } else { shell("pwsh -NoProfile -Command '& ([scriptblock]::Create($args[0])) $args[1]' " + quote(_anvil_stable_toolchain_selection_script) + " " + quote(justfile_directory())) } # General nightly used by udeps, miri, careful, and any future # nightly-dependent check. Bumped on a regular cadence (monthly is a diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap index e98115c21..f1ef48813 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap @@ -95,7 +95,6 @@ CMD ["bash"] !justfiles/anvil/*.just !justfiles/anvil/checks/*.just !justfiles/anvil/groups/*.just -!.anvil/resolve-stable-toolchain.ps1 !.anvil/container/* .anvil/container/*/* .anvil/container/customize.sh @@ -372,10 +371,7 @@ if ($LASTEXITCODE -ne 0 -or -not $repoRoot) { throw 'anvil-container must run from a Git repository.' } -$inputs = @( - 'Cargo.toml', - '.anvil/resolve-stable-toolchain.ps1' -) +$inputs = @('Cargo.toml') foreach ($toolchainFile in @('rust-toolchain', 'rust-toolchain.toml')) { if (Test-Path -LiteralPath (Join-Path $repoRoot $toolchainFile) -PathType Leaf) { $inputs += $toolchainFile @@ -458,7 +454,7 @@ if [[ ! "$base_image" =~ @sha256:[0-9a-fA-F]{64}$ ]]; then echo 'anvil-container: ANVIL_CONTAINER_BASE_IMAGE must be pinned by sha256 digest (image@sha256:<64 hex characters>).' >&2 exit 1 fi -inputs=(Cargo.toml .anvil/resolve-stable-toolchain.ps1) +inputs=(Cargo.toml) for toolchain_file in rust-toolchain rust-toolchain.toml; do if [[ -f "$repo_root/$toolchain_file" ]]; then inputs+=("$toolchain_file") @@ -1193,318 +1189,6 @@ if (($# == 0)); then fi docker "${run_args[@]}" "$image" just "$@" -=== .anvil/resolve-stable-toolchain.ps1 === -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# GENERATED BY cargo-anvil. DO NOT EDIT DIRECTLY. - -param( - [switch] $ValidateWorkspaceMsrv, - [switch] $InstallIfMissing, - [switch] $ForEnvironment, - [switch] $MsrvToolchain, - [switch] $InstallMsrvIfNeeded, - [switch] $EmitToolchainFileOptions, - [switch] $ApplyToolchainFileOptions -) - -$ErrorActionPreference = 'Stop' -Set-StrictMode -Version 3 - -$repoRoot = Split-Path -Parent $PSScriptRoot - -function Get-RootToolchainFilePath { - foreach ($name in @('rust-toolchain', 'rust-toolchain.toml')) { - $path = Join-Path $repoRoot $name - if (Test-Path -LiteralPath $path -PathType Leaf) { - return $path - } - } - return $null -} - -function Read-RootToolchainFile([string] $path) { - $content = Get-Content -LiteralPath $path -Raw - if ([string]::IsNullOrWhiteSpace($content)) { - throw "anvil: root toolchain file '$path' is empty; remove it or declare a toolchain configuration" - } - return $content -} - -function Get-SelectingToolchainFile { - $path = Get-RootToolchainFilePath - if ($null -eq $path) { - return $null - } - - $content = Read-RootToolchainFile $path - if ((Split-Path -Leaf $path) -eq 'rust-toolchain' -and $content.TrimStart() -notmatch '^\[') { - return [pscustomobject]@{ - Path = $path - Value = $content.Trim() - Selector = 'channel' - Source = 'file' - Installable = $false - } - } - - $inToolchain = $false - foreach ($line in $content -split '\r?\n') { - if ($line -match '^\s*\[\s*toolchain\s*\]\s*(?:#.*)?$') { - $inToolchain = $true - continue - } - if ($line -match '^\s*\[') { - $inToolchain = $false - continue - } - if ($inToolchain -and $line -match '^\s*["'']?(channel|path)["'']?\s*=\s*(["''])([^"'']+)\2\s*(?:#.*)?$') { - $value = $Matches[3] - if ($Matches[1] -eq 'path' -and -not [IO.Path]::IsPathRooted($value)) { - $value = [IO.Path]::GetFullPath((Join-Path $repoRoot $value)) - } - return [pscustomobject]@{ - Path = $path - Value = $value - Selector = $Matches[1] - Source = 'file' - Installable = $false - } - } - } - return $null -} - -function Get-ToolchainFileOptions { - $path = Get-RootToolchainFilePath - if ($null -eq $path) { - return $null - } - - $content = Read-RootToolchainFile $path - $section = [regex]::Match( - $content, - '(?ms)^\s*\[\s*toolchain\s*\]\s*(?:#.*)?$(.*?)(?=^\s*\[|\z)' - ) - if (-not $section.Success) { - return $null - } - - $body = $section.Groups[1].Value - $profileMatch = [regex]::Match( - $body, - '(?m)^\s*["'']?profile["'']?\s*=\s*(["''])([^"'']+)\1\s*(?:#.*)?$' - ) - $readArray = { - param([string] $name) - $arrayMatch = [regex]::Match( - $body, - "(?ms)^\s*[`"']?$name[`"']?\s*=\s*\[(.*?)\]" - ) - if (-not $arrayMatch.Success) { - return @() - } - return @( - [regex]::Matches($arrayMatch.Groups[1].Value, '(["''])([^"'']+)\1') | - ForEach-Object { $_.Groups[2].Value } - ) - } - - return [pscustomobject]@{ - Path = $path - Profile = if ($profileMatch.Success) { $profileMatch.Groups[2].Value } else { $null } - Components = @(& $readArray 'components') - Targets = @(& $readArray 'targets') - } -} - -function Get-RootMsrv([switch] $AllowMissing) { - $manifestPath = Join-Path $repoRoot 'Cargo.toml' - if (-not (Test-Path -LiteralPath $manifestPath -PathType Leaf)) { - throw "anvil: Cargo.toml not found at repository root '$repoRoot'" - } - - $values = @{} - $section = '' - foreach ($line in (Get-Content -LiteralPath $manifestPath)) { - if ($line -match '^\s*\[\s*([^\]]+)\s*\]\s*(?:#.*)?$') { - $section = $Matches[1].Trim() - continue - } - if ($section -in @('workspace.package', 'package') -and - $line -match '^\s*["'']?rust-version["'']?\s*=\s*(["''])([^"'']+)\1\s*(?:#.*)?$') { - $values[$section] = $Matches[2] - } - } - - if ($values.ContainsKey('workspace.package')) { - return $values['workspace.package'] - } - if ($values.ContainsKey('package')) { - return $values['package'] - } - - if ($AllowMissing) { - return $null - } - throw 'anvil: no selecting rust-toolchain(.toml) and no root [workspace.package] or [package] rust-version; declare an MSRV or select a toolchain explicitly' -} - -function Assert-WorkspaceMsrvCompatibility { - $manifestPath = Join-Path $repoRoot 'Cargo.toml' - $metadataText = (& cargo metadata --manifest-path $manifestPath --format-version 1 --no-deps 2>&1 | Out-String) - if ($LASTEXITCODE -ne 0) { - throw "anvil: cargo metadata failed while validating workspace MSRVs:`n$metadataText" - } - $metadata = $metadataText | ConvertFrom-Json - $memberIds = [Collections.Generic.HashSet[string]]::new( - [string[]] $metadata.workspace_members, - [StringComparer]::Ordinal - ) - $members = @($metadata.packages | Where-Object { $memberIds.Contains([string] $_.id) }) - $missing = @($members | Where-Object { [string]::IsNullOrWhiteSpace([string] $_.rust_version) } | ForEach-Object name) - if ($missing.Count -gt 0) { - throw "anvil: workspace packages without a resolved rust-version: $($missing -join ', '); declare one or select a toolchain explicitly" - } - - $rootMsrv = Get-RootMsrv - $newer = @( - $members | - Where-Object { [version] ([string] $_.rust_version) -gt [version] $rootMsrv } | - ForEach-Object { "$($_.name) ($($_.rust_version))" } - ) - if ($newer.Count -gt 0) { - throw "anvil: workspace packages require a compiler newer than the root MSRV $rootMsrv`: $($newer -join ', '); raise the root MSRV or select one catalog toolchain explicitly with rust-toolchain.toml" - } -} - -function Assert-CompleteInternalToolchainConfiguration { - if (-not [string]::IsNullOrWhiteSpace($env:ANVIL_MSRV_TOOLCHAIN) -and - [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN) -and - $null -eq $fileSelection) { - throw 'anvil: ANVIL_MSRV_TOOLCHAIN is set without RUSTUP_TOOLCHAIN and no repository toolchain file selects stable checks; set RUSTUP_TOOLCHAIN to the provisioned stable toolchain or unset ANVIL_MSRV_TOOLCHAIN' - } -} - -function Get-MsrvSelection { - $msrv = Get-RootMsrv -AllowMissing - if ([string]::IsNullOrWhiteSpace($msrv)) { - return $null - } - - if (-not [string]::IsNullOrWhiteSpace($env:ANVIL_MSRV_TOOLCHAIN)) { - return [pscustomobject]@{ - Value = $env:ANVIL_MSRV_TOOLCHAIN - Source = 'mapped-msrv' - Installable = $false - Mapped = $true - } - } - return [pscustomobject]@{ Value = $msrv; Source = 'msrv'; Installable = $true; Mapped = $false } -} - -function Apply-ToolchainFileOptions([object] $toolchain) { - if ($toolchain.Source -eq 'file' -or $null -eq $toolchainFileOptions) { - return - } - foreach ($component in $toolchainFileOptions.Components) { - rustup component add --toolchain $toolchain.Value $component - if ($LASTEXITCODE -ne 0) { - Write-Warning "anvil: toolchain-file component '$component' is unavailable for '$($toolchain.Value)'; rustup would skip an unavailable file component" - } - } - foreach ($target in $toolchainFileOptions.Targets) { - rustup target add --toolchain $toolchain.Value $target - if ($LASTEXITCODE -ne 0) { - Write-Warning "anvil: toolchain-file target '$target' is unavailable for '$($toolchain.Value)'; rustup would skip an unavailable file target" - } - } -} - -function Install-Toolchain([object] $toolchain, [string] $label) { - if ($toolchain.Installable) { - $installed = rustup toolchain list - if ($LASTEXITCODE -ne 0) { - throw 'anvil: rustup toolchain list failed' - } - if ($installed -notmatch "(?m)^$([regex]::Escape($toolchain.Value))(?:-|$)") { - $profile = if ($null -ne $toolchainFileOptions -and $toolchainFileOptions.Profile) { - $toolchainFileOptions.Profile - } else { - 'minimal' - } - Write-Host "anvil: installing $label toolchain '$($toolchain.Value)'" - rustup toolchain install $toolchain.Value --profile $profile - if ($LASTEXITCODE -ne 0) { - throw "anvil: failed to install $label toolchain '$($toolchain.Value)'" - } - } - } - Apply-ToolchainFileOptions $toolchain -} - -$fileSelection = Get-SelectingToolchainFile -$toolchainFileOptions = Get-ToolchainFileOptions -if ($EmitToolchainFileOptions) { - if ($null -ne $toolchainFileOptions) { - $toolchainFileOptions | ConvertTo-Json -Compress - } - exit 0 -} -if ($ValidateWorkspaceMsrv) { - Assert-CompleteInternalToolchainConfiguration - $skipWorkspaceMsrvValidation = -not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN) - if (-not $skipWorkspaceMsrvValidation -and $null -eq $fileSelection) { - Assert-WorkspaceMsrvCompatibility - } - exit 0 -} - -if ($MsrvToolchain -or $InstallMsrvIfNeeded) { - $msrvSelection = Get-MsrvSelection - if ($null -eq $msrvSelection) { - exit 0 - } - if ($InstallMsrvIfNeeded) { - if ($msrvSelection.Mapped) { - & cargo "+$($msrvSelection.Value)" --version *> $null - if ($LASTEXITCODE -ne 0) { - throw "anvil: mapped MSRV toolchain '$($msrvSelection.Value)' is unavailable; provision ANVIL_MSRV_TOOLCHAIN or unset it to let Anvil install the declared public MSRV" - } - } - Install-Toolchain $msrvSelection 'MSRV' - } else { - $msrvSelection.Value - } - exit 0 -} - -Assert-CompleteInternalToolchainConfiguration -$selection = if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { - [pscustomobject]@{ Value = $env:RUSTUP_TOOLCHAIN; Source = 'environment'; Installable = $false } -} elseif ($null -ne $fileSelection) { - $fileSelection -} else { - [pscustomobject]@{ Value = Get-RootMsrv; Source = 'msrv'; Installable = $true } -} - -if ($ApplyToolchainFileOptions) { - Apply-ToolchainFileOptions $selection - exit 0 -} - -if ($InstallIfMissing) { - Install-Toolchain $selection 'stable' - exit 0 -} - -if ($ForEnvironment -and $selection.Source -eq 'file') { - '' -} else { - $selection.Value -} -exit 0 - === .delta.toml === # >>> anvil-managed: anvil-delta # Changes to these workspace-level inputs conservatively affect every package. @@ -1778,7 +1462,7 @@ anvil-aprz: anvil-aprz-validate-prereqs Write-Warning 'To fix: run `gh auth login` (recommended), or set $env:GITHUB_TOKEN to a GitHub token, then re-run.' } } - & "{{ just_executable() }}" _anvil-with-stable cargo aprz deps --error-if-high-risk --console appraisal + & cargo {{_anvil_stable_toolchain_args}} aprz deps --error-if-high-risk --console appraisal if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-aprz` recipe. @@ -1804,7 +1488,7 @@ anvil-aprz-validate-prereqs: anvil-tool-cargo-aprz-validate-prereqs [script("pwsh", "-NoProfile")] anvil-audit: anvil-audit-validate-prereqs $ErrorActionPreference = 'Stop' - & "{{ just_executable() }}" _anvil-with-stable cargo audit + & cargo {{_anvil_stable_toolchain_args}} audit if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-audit` recipe. @@ -1832,7 +1516,7 @@ anvil-bench: anvil-bench-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include affected) if ($include -eq '--skip') { Write-Host 'anvil-bench: no affected packages; skipping'; exit 0 } - & "{{ just_executable() }}" _anvil-with-stable cargo bench @(if ($include) { -split $include } else { '--workspace' }) --all-features --no-run + & cargo {{_anvil_stable_toolchain_args}} bench @(if ($include) { -split $include } else { '--workspace' }) --all-features --no-run if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # bench uses the cargo built-in in compile-only mode; no extra tool install @@ -1903,7 +1587,7 @@ anvil-bolero: anvil-bolero-validate-prereqs anvil-impact $packageNames.Add(($pkgArgs[++$i] -split '@', 2)[0]) } } else { - $metadataJson = & "{{ just_executable() }}" _anvil-with-stable cargo metadata --no-deps --format-version 1 + $metadataJson = & cargo {{_anvil_stable_toolchain_args}} metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-bolero: cargo metadata failed' exit $LASTEXITCODE @@ -2067,7 +1751,7 @@ anvil-cargo-hack: anvil-cargo-hack-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include required) if ($include -eq '--skip') { Write-Host 'anvil-cargo-hack: no affected packages; skipping'; exit 0 } - & "{{ just_executable() }}" _anvil-with-stable cargo hack @(if ($include) { -split $include } else { '--workspace' }) --feature-powerset --depth 2 check + & cargo {{_anvil_stable_toolchain_args}} hack @(if ($include) { -split $include } else { '--workspace' }) --feature-powerset --depth 2 check if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-cargo-hack` recipe. @@ -2106,7 +1790,7 @@ anvil-cargo-sort: anvil-cargo-sort-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include modified) if ($include -eq '--skip') { Write-Host 'anvil-cargo-sort: no modified packages; skipping'; exit 0 } - & "{{ just_executable() }}" _anvil-with-stable cargo sort --workspace --grouped --check --check-format + & cargo {{_anvil_stable_toolchain_args}} sort --workspace --grouped --check --check-format if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-cargo-sort` recipe. @@ -2139,7 +1823,7 @@ anvil-clippy: anvil-clippy-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include affected) if ($include -eq '--skip') { Write-Host 'anvil-clippy: no affected packages; skipping'; exit 0 } - & "{{ just_executable() }}" _anvil-with-stable cargo clippy @(if ($include) { -split $include } else { '--workspace' }) --all-targets --all-features --locked "--" '-D' 'warnings' + & cargo {{_anvil_stable_toolchain_args}} clippy @(if ($include) { -split $include } else { '--workspace' }) --all-targets --all-features --locked "--" '-D' 'warnings' if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-clippy` recipe. @@ -2167,7 +1851,7 @@ anvil-clippy-validate-prereqs: anvil-component-default-clippy-validate-prereqs [script("pwsh", "-NoProfile")] anvil-deny: anvil-deny-validate-prereqs $ErrorActionPreference = 'Stop' - & "{{ just_executable() }}" _anvil-with-stable cargo deny check + & cargo {{_anvil_stable_toolchain_args}} deny check if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-deny` recipe. @@ -2198,7 +1882,7 @@ anvil-doc-build: anvil-doc-build-validate-prereqs anvil-impact $include = (& "{{ just_executable() }}" _anvil-impact-include required) if ($include -eq '--skip') { Write-Host 'anvil-doc-build: no affected packages; skipping'; exit 0 } $env:RUSTDOCFLAGS = '-D warnings' - & "{{ just_executable() }}" _anvil-with-stable cargo doc @(if ($include) { -split $include } else { '--workspace' }) --all-features --no-deps + & cargo {{_anvil_stable_toolchain_args}} doc @(if ($include) { -split $include } else { '--workspace' }) --all-features --no-deps if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # doc-build, examples and doc-test are pure cargo built-ins; the rust @@ -2236,9 +1920,9 @@ anvil-doc-test: anvil-doc-test-validate-prereqs anvil-impact $include = (& "{{ just_executable() }}" _anvil-impact-include affected) if ($include -eq '--skip') { Write-Host 'anvil-doc-test: no affected packages; skipping'; exit 0 } $pkg = @(if ($include) { -split $include } else { '--workspace' }) - & "{{ just_executable() }}" _anvil-with-stable cargo test --doc @pkg --all-features --locked + & cargo {{_anvil_stable_toolchain_args}} test --doc @pkg --all-features --locked if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - & "{{ just_executable() }}" _anvil-with-stable cargo test --doc @pkg --locked + & cargo {{_anvil_stable_toolchain_args}} test --doc @pkg --locked if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-doc-test` recipe. @@ -2266,7 +1950,7 @@ anvil-ensure-no-cyclic-deps: anvil-ensure-no-cyclic-deps-validate-prereqs anvil- $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include modified) if ($include -eq '--skip') { Write-Host 'anvil-ensure-no-cyclic-deps: no modified packages; skipping'; exit 0 } - & "{{ just_executable() }}" _anvil-with-stable cargo ensure-no-cyclic-deps + & cargo {{_anvil_stable_toolchain_args}} ensure-no-cyclic-deps if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-ensure-no-cyclic-deps` recipe. @@ -2294,7 +1978,7 @@ anvil-ensure-no-default-features: anvil-ensure-no-default-features-validate-prer $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include modified) if ($include -eq '--skip') { Write-Host 'anvil-ensure-no-default-features: no modified packages; skipping'; exit 0 } - & "{{ just_executable() }}" _anvil-with-stable cargo ensure-no-default-features + & cargo {{_anvil_stable_toolchain_args}} ensure-no-default-features if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-ensure-no-default-features` recipe. @@ -2322,7 +2006,7 @@ anvil-examples: anvil-examples-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include affected) if ($include -eq '--skip') { Write-Host 'anvil-examples: no affected packages; skipping'; exit 0 } - & "{{ just_executable() }}" _anvil-with-stable cargo build @(if ($include) { -split $include } else { '--workspace' }) --examples --all-features --locked + & cargo {{_anvil_stable_toolchain_args}} build @(if ($include) { -split $include } else { '--workspace' }) --examples --all-features --locked if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-examples` recipe. @@ -2367,7 +2051,7 @@ anvil-external-types: anvil-external-types-validate-prereqs anvil-impact $pkg = @(if ($include) { -split $include } else { '--workspace' }) # Build pkg-name -> manifest-path map, restricted to library crates. $libPkgs = @{} - $metadataJson = & "{{ just_executable() }}" _anvil-with-stable cargo metadata --no-deps --format-version 1 + $metadataJson = & cargo {{_anvil_stable_toolchain_args}} metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-external-types: cargo metadata failed' exit $LASTEXITCODE @@ -2495,7 +2179,7 @@ anvil-license-headers: anvil-license-headers-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include modified) if ($include -eq '--skip') { Write-Host 'anvil-license-headers: no modified packages; skipping'; exit 0 } - & "{{ just_executable() }}" _anvil-with-stable cargo heather + & cargo {{_anvil_stable_toolchain_args}} heather if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-license-headers` recipe. @@ -2546,7 +2230,7 @@ anvil-llvm-cov: anvil-llvm-cov-validate-prereqs anvil-impact # never opts a changed package out of tests. $testOnlyPkg = [System.Collections.Generic.List[string]]::new() if ($pkg -contains '--package') { - $metadataJson = & "{{ just_executable() }}" _anvil-with-stable cargo metadata --no-deps --format-version 1 + $metadataJson = & cargo {{_anvil_stable_toolchain_args}} metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-llvm-cov: cargo metadata failed' exit $LASTEXITCODE @@ -2677,7 +2361,7 @@ anvil-llvm-cov: anvil-llvm-cov-validate-prereqs anvil-impact } $gateArgs.Add($pkg[$i]) } - & "{{ just_executable() }}" _anvil-with-stable cargo coverage-gate @gateArgs --lcov target/coverage/lcov-all-features.info --lcov target/coverage/lcov-no-default.info + & cargo {{_anvil_stable_toolchain_args}} coverage-gate @gateArgs --lcov target/coverage/lcov-all-features.info --lcov target/coverage/lcov-no-default.info if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # --- pr-test members (shared with scheduled-test) --- @@ -2748,7 +2432,7 @@ anvil-loom: anvil-loom-validate-prereqs anvil-impact Write-Host 'anvil-loom: no affected packages; skipping' exit 0 } - $meta = & "{{ just_executable() }}" _anvil-with-stable cargo metadata --no-deps --format-version 1 | ConvertFrom-Json + $meta = & cargo {{_anvil_stable_toolchain_args}} metadata --no-deps --format-version 1 | ConvertFrom-Json if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Optional impact-scoping: recover bare package names from the @@ -2801,7 +2485,7 @@ anvil-loom: anvil-loom-validate-prereqs anvil-impact $env:RUSTFLAGS = "--cfg loom $($env:RUSTFLAGS)".Trim() foreach ($lt in $loomTargets) { Write-Host "anvil-loom: $($lt.pkg) :: $($lt.target)" - & "{{ just_executable() }}" _anvil-with-stable cargo test -p $lt.pkg --release --all-features --locked --test $lt.target -- --test-threads=1 + & cargo {{_anvil_stable_toolchain_args}} test -p $lt.pkg --release --all-features --locked --test $lt.target -- --test-threads=1 if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } } @@ -3089,7 +2773,7 @@ anvil-mutants-diff: anvil-mutants-diff-validate-prereqs anvil-impact $diff_path = Join-Path $tmp_dir 'anvil-mutants-diff.diff' git diff "$base..HEAD" --output=$diff_path if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - & "{{ just_executable() }}" _anvil-with-stable cargo mutants --in-diff $diff_path --no-shuffle --jobs 0 + & cargo {{_anvil_stable_toolchain_args}} mutants --in-diff $diff_path --no-shuffle --jobs 0 if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # --- pr-mutants members --- @@ -3122,7 +2806,7 @@ anvil-mutants-full: anvil-mutants-full-validate-prereqs Write-Host 'anvil-mutants-full: aarch64-pc-windows-msvc -- cargo-mutants does not build here (winapi); skipping' exit 0 } - & "{{ just_executable() }}" _anvil-with-stable cargo mutants --workspace --no-shuffle --jobs 0 + & cargo {{_anvil_stable_toolchain_args}} mutants --workspace --no-shuffle --jobs 0 if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # --- scheduled-exhaustive members (mutants-full reuses cargo-mutants; @@ -3270,7 +2954,7 @@ anvil-readme-check: anvil-readme-check-validate-prereqs # Unscoped: a change to the workspace-level README template is a repo-level # input cargo-delta cannot map to a package, so this check always runs the # full library set rather than an impact subset. - $metadataJson = & "{{ just_executable() }}" _anvil-with-stable cargo metadata --no-deps --format-version 1 + $metadataJson = & cargo {{_anvil_stable_toolchain_args}} metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-readme-check: cargo metadata failed' exit $LASTEXITCODE @@ -3312,7 +2996,7 @@ anvil-readme-check: anvil-readme-check-validate-prereqs } $args = @('doc2readme', '--check') if ($relTemplate) { $args += @('--template', $relTemplate) } - & "{{ just_executable() }}" _anvil-with-stable cargo @args + & cargo {{_anvil_stable_toolchain_args}} @args if ($LASTEXITCODE -ne 0) { $hadFailure = $true } } finally { Pop-Location @@ -3396,7 +3080,7 @@ anvil-semver-check: anvil-semver-check-validate-prereqs anvil-impact # we got here via impact-scoping (cloud workflows) or full-workspace # fallback (local). $libPkgs = @{} - $metadataJson = & "{{ just_executable() }}" _anvil-with-stable cargo metadata --no-deps --format-version 1 + $metadataJson = & cargo {{_anvil_stable_toolchain_args}} metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-semver-check: cargo metadata failed' exit $LASTEXITCODE @@ -3473,7 +3157,7 @@ anvil-semver-check: anvil-semver-check-validate-prereqs anvil-impact } Write-Host "anvil-semver-check: $p (baseline $base)" - $outputLines = @(& "{{ just_executable() }}" _anvil-with-stable cargo semver-checks --package $p --baseline-rev $base 2>&1) + $outputLines = @(& cargo {{_anvil_stable_toolchain_args}} semver-checks --package $p --baseline-rev $base 2>&1) $semverExit = $LASTEXITCODE $output = ($outputLines | Out-String) if ($semverExit -eq 100) { @@ -3612,9 +3296,9 @@ anvil-spellcheck: anvil-spellcheck-validate-prereqs # ignores user-curated dictionaries (`extra_dictionaries`, custom # hunspell langs, etc.). if (Test-Path 'spellcheck.toml') { - & "{{ just_executable() }}" _anvil-with-stable cargo spellcheck --cfg spellcheck.toml check --code 1 + & cargo {{_anvil_stable_toolchain_args}} spellcheck --cfg spellcheck.toml check --code 1 } else { - & "{{ just_executable() }}" _anvil-with-stable cargo spellcheck check --code 1 + & cargo {{_anvil_stable_toolchain_args}} spellcheck check --code 1 } if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } @@ -4481,9 +4165,13 @@ _anvil-impact-snapshot: # selections are already valid rustup overrides. For a selecting # toolchain file, ask rustup for the concrete active identifier so # `path` selections are not mistaken for named toolchains. - $baselineToolchain = (& "{{ just_executable() }}" _anvil-resolve-stable for-environment | Out-String).Trim() - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - if ([string]::IsNullOrWhiteSpace($baselineToolchain)) { + $stableArgs = {{_anvil_stable_toolchain_args}} + $baselineToolchain = if ($stableArgs.Count -eq 1) { + $stableArgs[0].Substring(1) + } else { + $null + } + if ($null -eq $baselineToolchain) { if (-not (Get-Command rustup -ErrorAction SilentlyContinue)) { Write-Error 'anvil-impact: rustup is required to resolve the active repository toolchain for the baseline snapshot' exit 1 @@ -4558,7 +4246,7 @@ _anvil-impact-snapshot: Write-Host 'anvil-impact: current snapshot up to date' } else { Write-Host 'anvil-impact: snapshotting working tree' - $curSnap = & "{{ just_executable() }}" _anvil-with-stable cargo delta @deltaArgs snapshot + $curSnap = & cargo {{_anvil_stable_toolchain_args}} delta @deltaArgs snapshot if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-impact: cargo delta snapshot (current) failed'; exit 1 } Set-Content -LiteralPath $currentJson -Value $curSnap -Encoding UTF8 Set-Content -LiteralPath $currentKeyFile -Value $currentState -Encoding UTF8 -NoNewline @@ -4676,7 +4364,7 @@ anvil-impact: _anvil-impact-snapshot # early), so default to an empty object so impact.json always exists -- # the freshness check above keys on its presence, and a missing file # would force a recompute on every invocation. - $impactOut = (& "{{ just_executable() }}" _anvil-with-stable cargo delta @deltaArgs impact --baseline $baselineJson --current $currentJson --format json | Out-String) + $impactOut = (& cargo {{_anvil_stable_toolchain_args}} delta @deltaArgs impact --baseline $baselineJson --current $currentJson --format json | Out-String) if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-impact: cargo delta impact failed'; exit 1 } if ([string]::IsNullOrWhiteSpace($impactOut)) { $impactOut = '{}' } Set-Content -LiteralPath $impactJson -Value $impactOut.Trim() -Encoding UTF8 @@ -4753,7 +4441,7 @@ _anvil-impact-format tier impactJson: # nested workspaces cargo-delta can instead emit a manifest directory # leaf; accept any reported identifier only when every matching namespace # resolves to the *same* workspace package. - $metadataJson = & "{{ just_executable() }}" _anvil-with-stable cargo metadata --no-deps --format-version 1 + $metadataJson = & cargo {{_anvil_stable_toolchain_args}} metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-impact: cargo metadata failed' exit $LASTEXITCODE @@ -5335,37 +5023,318 @@ anvil-tool-pwsh-validate-prereqs: [script("pwsh", "-NoProfile")] _anvil-resolve-stable action="resolve": $ErrorActionPreference = 'Stop' - $resolverArgs = @(switch ('{{action}}') { - 'resolve' { @() } - 'for-environment' { @('-ForEnvironment') } - 'install' { @('-InstallIfMissing') } - 'apply-file-options' { @('-ApplyToolchainFileOptions') } - 'validate-workspace-msrv' { @('-ValidateWorkspaceMsrv') } - 'msrv' { @('-MsrvToolchain') } - 'install-msrv' { @('-InstallMsrvIfNeeded') } - default { - Write-Error "_anvil-resolve-stable: unknown action '{{action}}'" - exit 2 + Set-StrictMode -Version 3 + + $action = '{{action}}' + $validActions = @( + 'resolve', + 'for-environment', + 'install', + 'apply-file-options', + 'validate-workspace-msrv', + 'msrv', + 'install-msrv', + 'emit-file-options' + ) + if ($action -notin $validActions) { + Write-Error "_anvil-resolve-stable: unknown action '$action'" + exit 2 + } + + $repoRoot = '{{justfile_directory()}}' + + function Get-RootToolchainFilePath { + foreach ($name in @('rust-toolchain', 'rust-toolchain.toml')) { + $path = Join-Path $repoRoot $name + if (Test-Path -LiteralPath $path -PathType Leaf) { + return $path + } } - }) - & pwsh -NoProfile -File '.anvil/resolve-stable-toolchain.ps1' @resolverArgs - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + return $null + } -# Run one command under the selected stable compiler. A repository toolchain -# file yields no override so rustup processes its complete configuration. -[script("pwsh", "-NoProfile")] -_anvil-with-stable command *args: - $ErrorActionPreference = 'Stop' - $toolchain = (& "{{ just_executable() }}" _anvil-resolve-stable for-environment | Out-String).Trim() - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - if ([string]::IsNullOrWhiteSpace($toolchain)) { - Remove-Item Env:\RUSTUP_TOOLCHAIN -ErrorAction SilentlyContinue + function Read-RootToolchainFile([string] $path) { + $content = Get-Content -LiteralPath $path -Raw + if ([string]::IsNullOrWhiteSpace($content)) { + throw "anvil: root toolchain file '$path' is empty; remove it or declare a toolchain configuration" + } + return $content + } + + function Get-SelectingToolchainFile { + $path = Get-RootToolchainFilePath + if ($null -eq $path) { + return $null + } + + $content = Read-RootToolchainFile $path + if ((Split-Path -Leaf $path) -eq 'rust-toolchain' -and $content.TrimStart() -notmatch '^\[') { + return [pscustomobject]@{ + Path = $path + Value = $content.Trim() + Selector = 'channel' + Source = 'file' + Installable = $false + } + } + + $inToolchain = $false + foreach ($line in $content -split '\r?\n') { + if ($line -match '^\s*\[\s*toolchain\s*\]\s*(?:#.*)?$') { + $inToolchain = $true + continue + } + if ($line -match '^\s*\[') { + $inToolchain = $false + continue + } + if ($inToolchain -and $line -match '^\s*["'']?(channel|path)["'']?\s*=\s*(["''])([^"'']+)\2\s*(?:#.*)?$') { + $value = $Matches[3] + if ($Matches[1] -eq 'path' -and -not [IO.Path]::IsPathRooted($value)) { + $value = [IO.Path]::GetFullPath((Join-Path $repoRoot $value)) + } + return [pscustomobject]@{ + Path = $path + Value = $value + Selector = $Matches[1] + Source = 'file' + Installable = $false + } + } + } + return $null + } + + function Get-ToolchainFileOptions { + $path = Get-RootToolchainFilePath + if ($null -eq $path) { + return $null + } + + $content = Read-RootToolchainFile $path + $section = [regex]::Match( + $content, + '(?ms)^\s*\[\s*toolchain\s*\]\s*(?:#.*)?$(.*?)(?=^\s*\[|\z)' + ) + if (-not $section.Success) { + return $null + } + + $body = $section.Groups[1].Value + $profileMatch = [regex]::Match( + $body, + '(?m)^\s*["'']?profile["'']?\s*=\s*(["''])([^"'']+)\1\s*(?:#.*)?$' + ) + $readArray = { + param([string] $name) + $arrayMatch = [regex]::Match( + $body, + "(?ms)^\s*[`"']?$name[`"']?\s*=\s*\[(.*?)\]" + ) + if (-not $arrayMatch.Success) { + return @() + } + return @( + [regex]::Matches($arrayMatch.Groups[1].Value, '(["''])([^"'']+)\1') | + ForEach-Object { $_.Groups[2].Value } + ) + } + + return [pscustomobject]@{ + Path = $path + Profile = if ($profileMatch.Success) { $profileMatch.Groups[2].Value } else { $null } + Components = @(& $readArray 'components') + Targets = @(& $readArray 'targets') + } + } + + function Get-RootMsrv([switch] $AllowMissing) { + $manifestPath = Join-Path $repoRoot 'Cargo.toml' + if (-not (Test-Path -LiteralPath $manifestPath -PathType Leaf)) { + throw "anvil: Cargo.toml not found at repository root '$repoRoot'" + } + + $values = @{} + $section = '' + foreach ($line in (Get-Content -LiteralPath $manifestPath)) { + if ($line -match '^\s*\[\s*([^\]]+)\s*\]\s*(?:#.*)?$') { + $section = $Matches[1].Trim() + continue + } + if ($section -in @('workspace.package', 'package') -and + $line -match '^\s*["'']?rust-version["'']?\s*=\s*(["''])([^"'']+)\1\s*(?:#.*)?$') { + $values[$section] = $Matches[2] + } + } + + if ($values.ContainsKey('workspace.package')) { + return $values['workspace.package'] + } + if ($values.ContainsKey('package')) { + return $values['package'] + } + + if ($AllowMissing) { + return $null + } + throw 'anvil: no selecting rust-toolchain(.toml) and no root [workspace.package] or [package] rust-version; declare an MSRV or select a toolchain explicitly' + } + + function Assert-WorkspaceMsrvCompatibility { + $manifestPath = Join-Path $repoRoot 'Cargo.toml' + $metadataText = (& cargo metadata --manifest-path $manifestPath --format-version 1 --no-deps 2>&1 | Out-String) + if ($LASTEXITCODE -ne 0) { + throw "anvil: cargo metadata failed while validating workspace MSRVs:`n$metadataText" + } + $metadata = $metadataText | ConvertFrom-Json + $memberIds = [Collections.Generic.HashSet[string]]::new( + [string[]] $metadata.workspace_members, + [StringComparer]::Ordinal + ) + $members = @($metadata.packages | Where-Object { $memberIds.Contains([string] $_.id) }) + $missing = @($members | Where-Object { [string]::IsNullOrWhiteSpace([string] $_.rust_version) } | ForEach-Object name) + if ($missing.Count -gt 0) { + throw "anvil: workspace packages without a resolved rust-version: $($missing -join ', '); declare one or select a toolchain explicitly" + } + + $rootMsrv = Get-RootMsrv + $newer = @( + $members | + Where-Object { [version] ([string] $_.rust_version) -gt [version] $rootMsrv } | + ForEach-Object { "$($_.name) ($($_.rust_version))" } + ) + if ($newer.Count -gt 0) { + throw "anvil: workspace packages require a compiler newer than the root MSRV $rootMsrv`: $($newer -join ', '); raise the root MSRV or select one catalog toolchain explicitly with rust-toolchain.toml" + } + } + + function Assert-CompleteInternalToolchainConfiguration { + if (-not [string]::IsNullOrWhiteSpace($env:ANVIL_MSRV_TOOLCHAIN) -and + [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN) -and + $null -eq $fileSelection) { + throw 'anvil: ANVIL_MSRV_TOOLCHAIN is set without RUSTUP_TOOLCHAIN and no repository toolchain file selects stable checks; set RUSTUP_TOOLCHAIN to the provisioned stable toolchain or unset ANVIL_MSRV_TOOLCHAIN' + } + } + + function Get-MsrvSelection { + $msrv = Get-RootMsrv -AllowMissing + if ([string]::IsNullOrWhiteSpace($msrv)) { + return $null + } + + if (-not [string]::IsNullOrWhiteSpace($env:ANVIL_MSRV_TOOLCHAIN)) { + return [pscustomobject]@{ + Value = $env:ANVIL_MSRV_TOOLCHAIN + Source = 'mapped-msrv' + Installable = $false + Mapped = $true + } + } + return [pscustomobject]@{ Value = $msrv; Source = 'msrv'; Installable = $true; Mapped = $false } + } + + function Apply-ToolchainFileOptions([object] $toolchain) { + if ($toolchain.Source -eq 'file' -or $null -eq $toolchainFileOptions) { + return + } + foreach ($component in $toolchainFileOptions.Components) { + rustup component add --toolchain $toolchain.Value $component + if ($LASTEXITCODE -ne 0) { + Write-Warning "anvil: toolchain-file component '$component' is unavailable for '$($toolchain.Value)'; rustup would skip an unavailable file component" + } + } + foreach ($target in $toolchainFileOptions.Targets) { + rustup target add --toolchain $toolchain.Value $target + if ($LASTEXITCODE -ne 0) { + Write-Warning "anvil: toolchain-file target '$target' is unavailable for '$($toolchain.Value)'; rustup would skip an unavailable file target" + } + } + } + + function Install-Toolchain([object] $toolchain, [string] $label) { + if ($toolchain.Installable) { + $installed = rustup toolchain list + if ($LASTEXITCODE -ne 0) { + throw 'anvil: rustup toolchain list failed' + } + if ($installed -notmatch "(?m)^$([regex]::Escape($toolchain.Value))(?:-|$)") { + $profile = if ($null -ne $toolchainFileOptions -and $toolchainFileOptions.Profile) { + $toolchainFileOptions.Profile + } else { + 'minimal' + } + Write-Host "anvil: installing $label toolchain '$($toolchain.Value)'" + rustup toolchain install $toolchain.Value --profile $profile + if ($LASTEXITCODE -ne 0) { + throw "anvil: failed to install $label toolchain '$($toolchain.Value)'" + } + } + } + Apply-ToolchainFileOptions $toolchain + } + + $fileSelection = Get-SelectingToolchainFile + $toolchainFileOptions = Get-ToolchainFileOptions + + if ($action -eq 'emit-file-options') { + if ($null -ne $toolchainFileOptions) { + $toolchainFileOptions | ConvertTo-Json -Compress + } + exit 0 + } + + if ($action -eq 'validate-workspace-msrv') { + Assert-CompleteInternalToolchainConfiguration + $skipWorkspaceMsrvValidation = -not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN) + if (-not $skipWorkspaceMsrvValidation -and $null -eq $fileSelection) { + Assert-WorkspaceMsrvCompatibility + } + exit 0 + } + + if ($action -in @('msrv', 'install-msrv')) { + $msrvSelection = Get-MsrvSelection + if ($null -eq $msrvSelection) { + exit 0 + } + if ($action -eq 'install-msrv') { + if ($msrvSelection.Mapped) { + & cargo "+$($msrvSelection.Value)" --version *> $null + if ($LASTEXITCODE -ne 0) { + throw "anvil: mapped MSRV toolchain '$($msrvSelection.Value)' is unavailable; provision ANVIL_MSRV_TOOLCHAIN or unset it to let Anvil install the declared public MSRV" + } + } + Install-Toolchain $msrvSelection 'MSRV' + } else { + $msrvSelection.Value + } + exit 0 + } + + Assert-CompleteInternalToolchainConfiguration + $selection = if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { + [pscustomobject]@{ Value = $env:RUSTUP_TOOLCHAIN; Source = 'environment'; Installable = $false } + } elseif ($null -ne $fileSelection) { + $fileSelection } else { - $env:RUSTUP_TOOLCHAIN = $toolchain + [pscustomobject]@{ Value = Get-RootMsrv; Source = 'msrv'; Installable = $true } + } + + if ($action -eq 'apply-file-options') { + Apply-ToolchainFileOptions $selection + exit 0 + } + + if ($action -eq 'install') { + Install-Toolchain $selection 'stable' + exit 0 + } + + if ($action -eq 'for-environment' -and $selection.Source -eq 'file') { + '' + } else { + $selection.Value } - Set-Location '{{invocation_directory_native()}}' - & '{{command}}' {{args}} - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # _install-tool: install a cargo subcommand at the catalog version, # or no-op if it is already installed at or above that version. The @@ -5397,7 +5366,7 @@ _install-tool name version installer source_prereq="": # cached binaries and fail with "binary already exists in destination". $installed = $null $pattern = '^' + [regex]::Escape($name) + ' v(\S+):' - foreach ($line in (& "{{ just_executable() }}" _anvil-with-stable cargo install --list 2>$null)) { + foreach ($line in (& cargo {{_anvil_stable_toolchain_args}} install --list 2>$null)) { if ($line -match $pattern) { $installed = $Matches[1]; break } } if ($installed) { @@ -5415,7 +5384,7 @@ _install-tool name version installer source_prereq="": if ($installer -eq 'binstall') { if (-not (Get-Command cargo-binstall -ErrorAction SilentlyContinue)) { Write-Host ' Bootstrapping cargo-binstall' - & "{{ just_executable() }}" _anvil-with-stable cargo install --locked cargo-binstall + & cargo {{_anvil_stable_toolchain_args}} install --locked cargo-binstall if ($LASTEXITCODE -ne 0) { Write-Error 'cargo-binstall bootstrap failed' exit $LASTEXITCODE @@ -5429,7 +5398,7 @@ _install-tool name version installer source_prereq="": $binstallArgs += @('--disable-strategies', 'compile') } $binstallArgs += @($name, '--version', "=$version") - & "{{ just_executable() }}" _anvil-with-stable cargo @binstallArgs + & cargo {{_anvil_stable_toolchain_args}} @binstallArgs if ($LASTEXITCODE -eq 0) { exit 0 } Write-Host ' binstall failed; falling back to cargo install' -ForegroundColor Yellow } @@ -5437,7 +5406,7 @@ _install-tool name version installer source_prereq="": & "{{just_executable()}}" $sourcePrereq if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } } - & "{{ just_executable() }}" _anvil-with-stable cargo install --locked $name --version "=$version" + & cargo {{_anvil_stable_toolchain_args}} install --locked $name --version "=$version" if ($LASTEXITCODE -ne 0) { Write-Error "$name install FAILED" exit $LASTEXITCODE @@ -5453,7 +5422,7 @@ _check-tool name version: $installed = $null $pattern = '^' + [regex]::Escape($name) + ' v(\S+):' - foreach ($line in (& "{{ just_executable() }}" _anvil-with-stable cargo install --list 2>$null)) { + foreach ($line in (& cargo {{_anvil_stable_toolchain_args}} install --list 2>$null)) { if ($line -match $pattern) { $installed = $Matches[1]; break } } if (-not $installed) { @@ -5534,14 +5503,14 @@ _install-component toolchain component: } if ($null -ne $subcommand) { if ($toolchain -eq 'default') { - & "{{ just_executable() }}" _anvil-with-stable cargo $subcommand --version *> $null + & cargo {{_anvil_stable_toolchain_args}} $subcommand --version *> $null } else { & cargo "+$toolchain" $subcommand --version *> $null } $present = ($LASTEXITCODE -eq 0) } else { $sysroot = if ($toolchain -eq 'default') { - & "{{ just_executable() }}" _anvil-with-stable rustc --print sysroot 2>$null + & rustc {{_anvil_stable_toolchain_args}} --print sysroot 2>$null } else { & rustc "+$toolchain" --print sysroot 2>$null } @@ -5602,14 +5571,14 @@ _check-component toolchain component: } if ($null -ne $subcommand) { if ($toolchain -eq 'default') { - & "{{ just_executable() }}" _anvil-with-stable cargo $subcommand --version *> $null + & cargo {{_anvil_stable_toolchain_args}} $subcommand --version *> $null } else { & cargo "+$toolchain" $subcommand --version *> $null } $present = ($LASTEXITCODE -eq 0) } else { $sysroot = if ($toolchain -eq 'default') { - & "{{ just_executable() }}" _anvil-with-stable rustc --print sysroot 2>$null + & rustc {{_anvil_stable_toolchain_args}} --print sysroot 2>$null } else { & rustc "+$toolchain" --print sysroot 2>$null } @@ -5946,6 +5915,8 @@ anvil-tool-cargo-udeps-validate-prereqs: (_check-tool "cargo-udeps" cargo_udeps_ # Update behaviour: https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/updates.md # # Pinned versions used by the anvil check tree. + +set lazy # # Everything anvil installs (cargo subcommands + rustup toolchains) # is pinned here. The pinning policy: @@ -5971,9 +5942,102 @@ anvil-tool-cargo-udeps-validate-prereqs: (_check-tool "cargo-udeps" cargo_udeps_ # Rustup toolchains # ============================================================================ -# Stable checks select their compiler through `_anvil-with-stable` at the -# individual command site. `RUSTUP_TOOLCHAIN` is an input override only; Anvil -# never exports a resolved value globally into unrelated recipes or helpers. +# Stable commands interpolate this lazy PowerShell argument-array expression +# directly at each command site. `RUSTUP_TOOLCHAIN` is an input override only; +# Anvil never exports a resolved value globally into unrelated recipes or +# helpers. +[private] +_anvil_stable_toolchain_selection_script := ''' +param([string] $RepoRoot) + +$ErrorActionPreference = 'Stop' +Set-StrictMode -Version 3 + +function Write-ExplicitToolchainArgs([string] $Toolchain) { + Write-Output ("@('+" + $Toolchain.Replace("'", "''") + "')") +} + +if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { + Write-ExplicitToolchainArgs $env:RUSTUP_TOOLCHAIN + exit 0 +} + +$toolchainFile = $null +foreach ($name in @('rust-toolchain', 'rust-toolchain.toml')) { + $candidate = Join-Path $RepoRoot $name + if (Test-Path -LiteralPath $candidate -PathType Leaf) { + $toolchainFile = $candidate + break + } +} + +$selectingFile = $false +if ($null -ne $toolchainFile) { + $content = Get-Content -LiteralPath $toolchainFile -Raw + if ([string]::IsNullOrWhiteSpace($content)) { + throw "anvil: root toolchain file '$toolchainFile' is empty; remove it or declare a toolchain configuration" + } + if ((Split-Path -Leaf $toolchainFile) -eq 'rust-toolchain' -and $content.TrimStart() -notmatch '^\[') { + $selectingFile = $true + } else { + $inToolchain = $false + foreach ($line in $content -split '\r?\n') { + if ($line -match '^\s*\[\s*toolchain\s*\]\s*(?:#.*)?$') { + $inToolchain = $true + continue + } + if ($line -match '^\s*\[') { + $inToolchain = $false + continue + } + if ($inToolchain -and $line -match '^\s*["'']?(channel|path)["'']?\s*=\s*(["''])([^"'']+)\2\s*(?:#.*)?$') { + $selectingFile = $true + break + } + } + } +} + +if ($selectingFile) { + Write-Output '@()' + exit 0 +} + +if (-not [string]::IsNullOrWhiteSpace($env:ANVIL_MSRV_TOOLCHAIN)) { + throw 'anvil: ANVIL_MSRV_TOOLCHAIN is set without RUSTUP_TOOLCHAIN and no repository toolchain file selects stable checks; set RUSTUP_TOOLCHAIN to the provisioned stable toolchain or unset ANVIL_MSRV_TOOLCHAIN' +} + +$manifestPath = Join-Path $RepoRoot 'Cargo.toml' +if (-not (Test-Path -LiteralPath $manifestPath -PathType Leaf)) { + throw "anvil: Cargo.toml not found at repository root '$RepoRoot'" +} + +$values = @{} +$section = '' +foreach ($line in (Get-Content -LiteralPath $manifestPath)) { + if ($line -match '^\s*\[\s*([^\]]+)\s*\]\s*(?:#.*)?$') { + $section = $Matches[1].Trim() + continue + } + if ($section -in @('workspace.package', 'package') -and + $line -match '^\s*["'']?rust-version["'']?\s*=\s*(["''])([^"'']+)\1\s*(?:#.*)?$') { + $values[$section] = $Matches[2] + } +} + +$msrv = if ($values.ContainsKey('workspace.package')) { + $values['workspace.package'] +} elseif ($values.ContainsKey('package')) { + $values['package'] +} else { + throw 'anvil: no selecting rust-toolchain(.toml) and no root [workspace.package] or [package] rust-version; declare an MSRV or select a toolchain explicitly' +} +Write-ExplicitToolchainArgs $msrv +exit 0 +''' + +[private] +_anvil_stable_toolchain_args := if os_family() == "windows" { shell("& ([scriptblock]::Create('" + replace(_anvil_stable_toolchain_selection_script, "'", "''") + "')) '" + replace(justfile_directory(), "'", "''") + "';") } else { shell("pwsh -NoProfile -Command '& ([scriptblock]::Create($args[0])) $args[1]' " + quote(_anvil_stable_toolchain_selection_script) + " " + quote(justfile_directory())) } # General nightly used by udeps, miri, careful, and any future # nightly-dependent check. Bumped on a regular cadence (monthly is a diff --git a/justfiles/anvil/checks/aprz.just b/justfiles/anvil/checks/aprz.just index 897ec37c1..90c5b428f 100644 --- a/justfiles/anvil/checks/aprz.just +++ b/justfiles/anvil/checks/aprz.just @@ -44,7 +44,7 @@ anvil-aprz: anvil-aprz-validate-prereqs Write-Warning 'To fix: run `gh auth login` (recommended), or set $env:GITHUB_TOKEN to a GitHub token, then re-run.' } } - & "{{ just_executable() }}" _anvil-with-stable cargo aprz deps --error-if-high-risk --console appraisal + & cargo {{_anvil_stable_toolchain_args}} aprz deps --error-if-high-risk --console appraisal if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-aprz` recipe. diff --git a/justfiles/anvil/checks/audit.just b/justfiles/anvil/checks/audit.just index d0aef3197..a43c5de01 100644 --- a/justfiles/anvil/checks/audit.just +++ b/justfiles/anvil/checks/audit.just @@ -12,7 +12,7 @@ [script("pwsh", "-NoProfile")] anvil-audit: anvil-audit-validate-prereqs $ErrorActionPreference = 'Stop' - & "{{ just_executable() }}" _anvil-with-stable cargo audit + & cargo {{_anvil_stable_toolchain_args}} audit if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-audit` recipe. diff --git a/justfiles/anvil/checks/bench.just b/justfiles/anvil/checks/bench.just index 7f8ca006f..0158612e4 100644 --- a/justfiles/anvil/checks/bench.just +++ b/justfiles/anvil/checks/bench.just @@ -14,7 +14,7 @@ anvil-bench: anvil-bench-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include affected) if ($include -eq '--skip') { Write-Host 'anvil-bench: no affected packages; skipping'; exit 0 } - & "{{ just_executable() }}" _anvil-with-stable cargo bench @(if ($include) { -split $include } else { '--workspace' }) --all-features --no-run + & cargo {{_anvil_stable_toolchain_args}} bench @(if ($include) { -split $include } else { '--workspace' }) --all-features --no-run if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # bench uses the cargo built-in in compile-only mode; no extra tool install diff --git a/justfiles/anvil/checks/bolero.just b/justfiles/anvil/checks/bolero.just index 306bb9eea..8ba438b39 100644 --- a/justfiles/anvil/checks/bolero.just +++ b/justfiles/anvil/checks/bolero.just @@ -54,7 +54,7 @@ anvil-bolero: anvil-bolero-validate-prereqs anvil-impact $packageNames.Add(($pkgArgs[++$i] -split '@', 2)[0]) } } else { - $metadataJson = & "{{ just_executable() }}" _anvil-with-stable cargo metadata --no-deps --format-version 1 + $metadataJson = & cargo {{_anvil_stable_toolchain_args}} metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-bolero: cargo metadata failed' exit $LASTEXITCODE diff --git a/justfiles/anvil/checks/cargo-hack.just b/justfiles/anvil/checks/cargo-hack.just index c42680435..0377fb4da 100644 --- a/justfiles/anvil/checks/cargo-hack.just +++ b/justfiles/anvil/checks/cargo-hack.just @@ -16,7 +16,7 @@ anvil-cargo-hack: anvil-cargo-hack-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include required) if ($include -eq '--skip') { Write-Host 'anvil-cargo-hack: no affected packages; skipping'; exit 0 } - & "{{ just_executable() }}" _anvil-with-stable cargo hack @(if ($include) { -split $include } else { '--workspace' }) --feature-powerset --depth 2 check + & cargo {{_anvil_stable_toolchain_args}} hack @(if ($include) { -split $include } else { '--workspace' }) --feature-powerset --depth 2 check if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-cargo-hack` recipe. diff --git a/justfiles/anvil/checks/cargo-sort.just b/justfiles/anvil/checks/cargo-sort.just index 8de8ad7a9..dcf5d192d 100644 --- a/justfiles/anvil/checks/cargo-sort.just +++ b/justfiles/anvil/checks/cargo-sort.just @@ -25,7 +25,7 @@ anvil-cargo-sort: anvil-cargo-sort-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include modified) if ($include -eq '--skip') { Write-Host 'anvil-cargo-sort: no modified packages; skipping'; exit 0 } - & "{{ just_executable() }}" _anvil-with-stable cargo sort --workspace --grouped --check --check-format + & cargo {{_anvil_stable_toolchain_args}} sort --workspace --grouped --check --check-format if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-cargo-sort` recipe. diff --git a/justfiles/anvil/checks/clippy.just b/justfiles/anvil/checks/clippy.just index 5b9a8eb57..a0354f2c4 100644 --- a/justfiles/anvil/checks/clippy.just +++ b/justfiles/anvil/checks/clippy.just @@ -19,7 +19,7 @@ anvil-clippy: anvil-clippy-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include affected) if ($include -eq '--skip') { Write-Host 'anvil-clippy: no affected packages; skipping'; exit 0 } - & "{{ just_executable() }}" _anvil-with-stable cargo clippy @(if ($include) { -split $include } else { '--workspace' }) --all-targets --all-features --locked "--" '-D' 'warnings' + & cargo {{_anvil_stable_toolchain_args}} clippy @(if ($include) { -split $include } else { '--workspace' }) --all-targets --all-features --locked "--" '-D' 'warnings' if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-clippy` recipe. diff --git a/justfiles/anvil/checks/deny.just b/justfiles/anvil/checks/deny.just index 17d098305..c798669b5 100644 --- a/justfiles/anvil/checks/deny.just +++ b/justfiles/anvil/checks/deny.just @@ -14,7 +14,7 @@ [script("pwsh", "-NoProfile")] anvil-deny: anvil-deny-validate-prereqs $ErrorActionPreference = 'Stop' - & "{{ just_executable() }}" _anvil-with-stable cargo deny check + & cargo {{_anvil_stable_toolchain_args}} deny check if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-deny` recipe. diff --git a/justfiles/anvil/checks/doc-build.just b/justfiles/anvil/checks/doc-build.just index b29336163..c02ad20bc 100644 --- a/justfiles/anvil/checks/doc-build.just +++ b/justfiles/anvil/checks/doc-build.just @@ -17,7 +17,7 @@ anvil-doc-build: anvil-doc-build-validate-prereqs anvil-impact $include = (& "{{ just_executable() }}" _anvil-impact-include required) if ($include -eq '--skip') { Write-Host 'anvil-doc-build: no affected packages; skipping'; exit 0 } $env:RUSTDOCFLAGS = '-D warnings' - & "{{ just_executable() }}" _anvil-with-stable cargo doc @(if ($include) { -split $include } else { '--workspace' }) --all-features --no-deps + & cargo {{_anvil_stable_toolchain_args}} doc @(if ($include) { -split $include } else { '--workspace' }) --all-features --no-deps if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # doc-build, examples and doc-test are pure cargo built-ins; the rust diff --git a/justfiles/anvil/checks/doc-test.just b/justfiles/anvil/checks/doc-test.just index 1480f48d5..97f959768 100644 --- a/justfiles/anvil/checks/doc-test.just +++ b/justfiles/anvil/checks/doc-test.just @@ -20,9 +20,9 @@ anvil-doc-test: anvil-doc-test-validate-prereqs anvil-impact $include = (& "{{ just_executable() }}" _anvil-impact-include affected) if ($include -eq '--skip') { Write-Host 'anvil-doc-test: no affected packages; skipping'; exit 0 } $pkg = @(if ($include) { -split $include } else { '--workspace' }) - & "{{ just_executable() }}" _anvil-with-stable cargo test --doc @pkg --all-features --locked + & cargo {{_anvil_stable_toolchain_args}} test --doc @pkg --all-features --locked if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - & "{{ just_executable() }}" _anvil-with-stable cargo test --doc @pkg --locked + & cargo {{_anvil_stable_toolchain_args}} test --doc @pkg --locked if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-doc-test` recipe. diff --git a/justfiles/anvil/checks/ensure-no-cyclic-deps.just b/justfiles/anvil/checks/ensure-no-cyclic-deps.just index e74beab7c..57f360170 100644 --- a/justfiles/anvil/checks/ensure-no-cyclic-deps.just +++ b/justfiles/anvil/checks/ensure-no-cyclic-deps.just @@ -14,7 +14,7 @@ anvil-ensure-no-cyclic-deps: anvil-ensure-no-cyclic-deps-validate-prereqs anvil- $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include modified) if ($include -eq '--skip') { Write-Host 'anvil-ensure-no-cyclic-deps: no modified packages; skipping'; exit 0 } - & "{{ just_executable() }}" _anvil-with-stable cargo ensure-no-cyclic-deps + & cargo {{_anvil_stable_toolchain_args}} ensure-no-cyclic-deps if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-ensure-no-cyclic-deps` recipe. diff --git a/justfiles/anvil/checks/ensure-no-default-features.just b/justfiles/anvil/checks/ensure-no-default-features.just index 9def94fa3..cee4d24d5 100644 --- a/justfiles/anvil/checks/ensure-no-default-features.just +++ b/justfiles/anvil/checks/ensure-no-default-features.just @@ -14,7 +14,7 @@ anvil-ensure-no-default-features: anvil-ensure-no-default-features-validate-prer $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include modified) if ($include -eq '--skip') { Write-Host 'anvil-ensure-no-default-features: no modified packages; skipping'; exit 0 } - & "{{ just_executable() }}" _anvil-with-stable cargo ensure-no-default-features + & cargo {{_anvil_stable_toolchain_args}} ensure-no-default-features if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-ensure-no-default-features` recipe. diff --git a/justfiles/anvil/checks/examples.just b/justfiles/anvil/checks/examples.just index 47ab115f9..de3b11ef5 100644 --- a/justfiles/anvil/checks/examples.just +++ b/justfiles/anvil/checks/examples.just @@ -14,7 +14,7 @@ anvil-examples: anvil-examples-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include affected) if ($include -eq '--skip') { Write-Host 'anvil-examples: no affected packages; skipping'; exit 0 } - & "{{ just_executable() }}" _anvil-with-stable cargo build @(if ($include) { -split $include } else { '--workspace' }) --examples --all-features --locked + & cargo {{_anvil_stable_toolchain_args}} build @(if ($include) { -split $include } else { '--workspace' }) --examples --all-features --locked if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-examples` recipe. diff --git a/justfiles/anvil/checks/external-types.just b/justfiles/anvil/checks/external-types.just index 87287a42a..d659a3d4d 100644 --- a/justfiles/anvil/checks/external-types.just +++ b/justfiles/anvil/checks/external-types.just @@ -31,7 +31,7 @@ anvil-external-types: anvil-external-types-validate-prereqs anvil-impact $pkg = @(if ($include) { -split $include } else { '--workspace' }) # Build pkg-name -> manifest-path map, restricted to library crates. $libPkgs = @{} - $metadataJson = & "{{ just_executable() }}" _anvil-with-stable cargo metadata --no-deps --format-version 1 + $metadataJson = & cargo {{_anvil_stable_toolchain_args}} metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-external-types: cargo metadata failed' exit $LASTEXITCODE diff --git a/justfiles/anvil/checks/license-headers.just b/justfiles/anvil/checks/license-headers.just index 7f489f48a..fb794c32e 100644 --- a/justfiles/anvil/checks/license-headers.just +++ b/justfiles/anvil/checks/license-headers.just @@ -14,7 +14,7 @@ anvil-license-headers: anvil-license-headers-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include modified) if ($include -eq '--skip') { Write-Host 'anvil-license-headers: no modified packages; skipping'; exit 0 } - & "{{ just_executable() }}" _anvil-with-stable cargo heather + & cargo {{_anvil_stable_toolchain_args}} heather if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-license-headers` recipe. diff --git a/justfiles/anvil/checks/llvm-cov.just b/justfiles/anvil/checks/llvm-cov.just index fbace94e0..1096d2743 100644 --- a/justfiles/anvil/checks/llvm-cov.just +++ b/justfiles/anvil/checks/llvm-cov.just @@ -37,7 +37,7 @@ anvil-llvm-cov: anvil-llvm-cov-validate-prereqs anvil-impact # never opts a changed package out of tests. $testOnlyPkg = [System.Collections.Generic.List[string]]::new() if ($pkg -contains '--package') { - $metadataJson = & "{{ just_executable() }}" _anvil-with-stable cargo metadata --no-deps --format-version 1 + $metadataJson = & cargo {{_anvil_stable_toolchain_args}} metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-llvm-cov: cargo metadata failed' exit $LASTEXITCODE @@ -168,7 +168,7 @@ anvil-llvm-cov: anvil-llvm-cov-validate-prereqs anvil-impact } $gateArgs.Add($pkg[$i]) } - & "{{ just_executable() }}" _anvil-with-stable cargo coverage-gate @gateArgs --lcov target/coverage/lcov-all-features.info --lcov target/coverage/lcov-no-default.info + & cargo {{_anvil_stable_toolchain_args}} coverage-gate @gateArgs --lcov target/coverage/lcov-all-features.info --lcov target/coverage/lcov-no-default.info if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # --- pr-test members (shared with scheduled-test) --- diff --git a/justfiles/anvil/checks/loom.just b/justfiles/anvil/checks/loom.just index 4cc8441b3..5ddd82605 100644 --- a/justfiles/anvil/checks/loom.just +++ b/justfiles/anvil/checks/loom.just @@ -55,7 +55,7 @@ anvil-loom: anvil-loom-validate-prereqs anvil-impact Write-Host 'anvil-loom: no affected packages; skipping' exit 0 } - $meta = & "{{ just_executable() }}" _anvil-with-stable cargo metadata --no-deps --format-version 1 | ConvertFrom-Json + $meta = & cargo {{_anvil_stable_toolchain_args}} metadata --no-deps --format-version 1 | ConvertFrom-Json if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Optional impact-scoping: recover bare package names from the @@ -108,7 +108,7 @@ anvil-loom: anvil-loom-validate-prereqs anvil-impact $env:RUSTFLAGS = "--cfg loom $($env:RUSTFLAGS)".Trim() foreach ($lt in $loomTargets) { Write-Host "anvil-loom: $($lt.pkg) :: $($lt.target)" - & "{{ just_executable() }}" _anvil-with-stable cargo test -p $lt.pkg --release --all-features --locked --test $lt.target -- --test-threads=1 + & cargo {{_anvil_stable_toolchain_args}} test -p $lt.pkg --release --all-features --locked --test $lt.target -- --test-threads=1 if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } } diff --git a/justfiles/anvil/checks/mutants-diff.just b/justfiles/anvil/checks/mutants-diff.just index e3c3c4234..565edeb29 100644 --- a/justfiles/anvil/checks/mutants-diff.just +++ b/justfiles/anvil/checks/mutants-diff.just @@ -43,7 +43,7 @@ anvil-mutants-diff: anvil-mutants-diff-validate-prereqs anvil-impact $diff_path = Join-Path $tmp_dir 'anvil-mutants-diff.diff' git diff "$base..HEAD" --output=$diff_path if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - & "{{ just_executable() }}" _anvil-with-stable cargo mutants --in-diff $diff_path --no-shuffle --jobs 0 + & cargo {{_anvil_stable_toolchain_args}} mutants --in-diff $diff_path --no-shuffle --jobs 0 if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # --- pr-mutants members --- diff --git a/justfiles/anvil/checks/mutants-full.just b/justfiles/anvil/checks/mutants-full.just index 4009e7577..e3e32a16b 100644 --- a/justfiles/anvil/checks/mutants-full.just +++ b/justfiles/anvil/checks/mutants-full.just @@ -17,7 +17,7 @@ anvil-mutants-full: anvil-mutants-full-validate-prereqs Write-Host 'anvil-mutants-full: aarch64-pc-windows-msvc -- cargo-mutants does not build here (winapi); skipping' exit 0 } - & "{{ just_executable() }}" _anvil-with-stable cargo mutants --workspace --no-shuffle --jobs 0 + & cargo {{_anvil_stable_toolchain_args}} mutants --workspace --no-shuffle --jobs 0 if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # --- scheduled-exhaustive members (mutants-full reuses cargo-mutants; diff --git a/justfiles/anvil/checks/readme-check.just b/justfiles/anvil/checks/readme-check.just index 615fa4430..aea186413 100644 --- a/justfiles/anvil/checks/readme-check.just +++ b/justfiles/anvil/checks/readme-check.just @@ -47,7 +47,7 @@ anvil-readme-check: anvil-readme-check-validate-prereqs # Unscoped: a change to the workspace-level README template is a repo-level # input cargo-delta cannot map to a package, so this check always runs the # full library set rather than an impact subset. - $metadataJson = & "{{ just_executable() }}" _anvil-with-stable cargo metadata --no-deps --format-version 1 + $metadataJson = & cargo {{_anvil_stable_toolchain_args}} metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-readme-check: cargo metadata failed' exit $LASTEXITCODE @@ -89,7 +89,7 @@ anvil-readme-check: anvil-readme-check-validate-prereqs } $args = @('doc2readme', '--check') if ($relTemplate) { $args += @('--template', $relTemplate) } - & "{{ just_executable() }}" _anvil-with-stable cargo @args + & cargo {{_anvil_stable_toolchain_args}} @args if ($LASTEXITCODE -ne 0) { $hadFailure = $true } } finally { Pop-Location diff --git a/justfiles/anvil/checks/semver-check.just b/justfiles/anvil/checks/semver-check.just index 7cbac0ac6..6c68adfb7 100644 --- a/justfiles/anvil/checks/semver-check.just +++ b/justfiles/anvil/checks/semver-check.just @@ -65,7 +65,7 @@ anvil-semver-check: anvil-semver-check-validate-prereqs anvil-impact # we got here via impact-scoping (cloud workflows) or full-workspace # fallback (local). $libPkgs = @{} - $metadataJson = & "{{ just_executable() }}" _anvil-with-stable cargo metadata --no-deps --format-version 1 + $metadataJson = & cargo {{_anvil_stable_toolchain_args}} metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-semver-check: cargo metadata failed' exit $LASTEXITCODE @@ -142,7 +142,7 @@ anvil-semver-check: anvil-semver-check-validate-prereqs anvil-impact } Write-Host "anvil-semver-check: $p (baseline $base)" - $outputLines = @(& "{{ just_executable() }}" _anvil-with-stable cargo semver-checks --package $p --baseline-rev $base 2>&1) + $outputLines = @(& cargo {{_anvil_stable_toolchain_args}} semver-checks --package $p --baseline-rev $base 2>&1) $semverExit = $LASTEXITCODE $output = ($outputLines | Out-String) if ($semverExit -eq 100) { diff --git a/justfiles/anvil/checks/spellcheck.just b/justfiles/anvil/checks/spellcheck.just index 805d43cc0..cdf2aef95 100644 --- a/justfiles/anvil/checks/spellcheck.just +++ b/justfiles/anvil/checks/spellcheck.just @@ -42,9 +42,9 @@ anvil-spellcheck: anvil-spellcheck-validate-prereqs # ignores user-curated dictionaries (`extra_dictionaries`, custom # hunspell langs, etc.). if (Test-Path 'spellcheck.toml') { - & "{{ just_executable() }}" _anvil-with-stable cargo spellcheck --cfg spellcheck.toml check --code 1 + & cargo {{_anvil_stable_toolchain_args}} spellcheck --cfg spellcheck.toml check --code 1 } else { - & "{{ just_executable() }}" _anvil-with-stable cargo spellcheck check --code 1 + & cargo {{_anvil_stable_toolchain_args}} spellcheck check --code 1 } if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } diff --git a/justfiles/anvil/impact.just b/justfiles/anvil/impact.just index 56223c8aa..4f05394c8 100644 --- a/justfiles/anvil/impact.just +++ b/justfiles/anvil/impact.just @@ -241,9 +241,13 @@ _anvil-impact-snapshot: # selections are already valid rustup overrides. For a selecting # toolchain file, ask rustup for the concrete active identifier so # `path` selections are not mistaken for named toolchains. - $baselineToolchain = (& "{{ just_executable() }}" _anvil-resolve-stable for-environment | Out-String).Trim() - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - if ([string]::IsNullOrWhiteSpace($baselineToolchain)) { + $stableArgs = {{_anvil_stable_toolchain_args}} + $baselineToolchain = if ($stableArgs.Count -eq 1) { + $stableArgs[0].Substring(1) + } else { + $null + } + if ($null -eq $baselineToolchain) { if (-not (Get-Command rustup -ErrorAction SilentlyContinue)) { Write-Error 'anvil-impact: rustup is required to resolve the active repository toolchain for the baseline snapshot' exit 1 @@ -318,7 +322,7 @@ _anvil-impact-snapshot: Write-Host 'anvil-impact: current snapshot up to date' } else { Write-Host 'anvil-impact: snapshotting working tree' - $curSnap = & "{{ just_executable() }}" _anvil-with-stable cargo delta @deltaArgs snapshot + $curSnap = & cargo {{_anvil_stable_toolchain_args}} delta @deltaArgs snapshot if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-impact: cargo delta snapshot (current) failed'; exit 1 } Set-Content -LiteralPath $currentJson -Value $curSnap -Encoding UTF8 Set-Content -LiteralPath $currentKeyFile -Value $currentState -Encoding UTF8 -NoNewline @@ -436,7 +440,7 @@ anvil-impact: _anvil-impact-snapshot # early), so default to an empty object so impact.json always exists -- # the freshness check above keys on its presence, and a missing file # would force a recompute on every invocation. - $impactOut = (& "{{ just_executable() }}" _anvil-with-stable cargo delta @deltaArgs impact --baseline $baselineJson --current $currentJson --format json | Out-String) + $impactOut = (& cargo {{_anvil_stable_toolchain_args}} delta @deltaArgs impact --baseline $baselineJson --current $currentJson --format json | Out-String) if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-impact: cargo delta impact failed'; exit 1 } if ([string]::IsNullOrWhiteSpace($impactOut)) { $impactOut = '{}' } Set-Content -LiteralPath $impactJson -Value $impactOut.Trim() -Encoding UTF8 @@ -513,7 +517,7 @@ _anvil-impact-format tier impactJson: # nested workspaces cargo-delta can instead emit a manifest directory # leaf; accept any reported identifier only when every matching namespace # resolves to the *same* workspace package. - $metadataJson = & "{{ just_executable() }}" _anvil-with-stable cargo metadata --no-deps --format-version 1 + $metadataJson = & cargo {{_anvil_stable_toolchain_args}} metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-impact: cargo metadata failed' exit $LASTEXITCODE diff --git a/justfiles/anvil/tools.just b/justfiles/anvil/tools.just index 218dc28fd..41b9336f1 100644 --- a/justfiles/anvil/tools.just +++ b/justfiles/anvil/tools.just @@ -177,37 +177,318 @@ anvil-tool-pwsh-validate-prereqs: [script("pwsh", "-NoProfile")] _anvil-resolve-stable action="resolve": $ErrorActionPreference = 'Stop' - $resolverArgs = @(switch ('{{action}}') { - 'resolve' { @() } - 'for-environment' { @('-ForEnvironment') } - 'install' { @('-InstallIfMissing') } - 'apply-file-options' { @('-ApplyToolchainFileOptions') } - 'validate-workspace-msrv' { @('-ValidateWorkspaceMsrv') } - 'msrv' { @('-MsrvToolchain') } - 'install-msrv' { @('-InstallMsrvIfNeeded') } - default { - Write-Error "_anvil-resolve-stable: unknown action '{{action}}'" - exit 2 - } - }) - & pwsh -NoProfile -File '.anvil/resolve-stable-toolchain.ps1' @resolverArgs - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - -# Run one command under the selected stable compiler. A repository toolchain -# file yields no override so rustup processes its complete configuration. -[script("pwsh", "-NoProfile")] -_anvil-with-stable command *args: - $ErrorActionPreference = 'Stop' - $toolchain = (& "{{ just_executable() }}" _anvil-resolve-stable for-environment | Out-String).Trim() - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - if ([string]::IsNullOrWhiteSpace($toolchain)) { - Remove-Item Env:\RUSTUP_TOOLCHAIN -ErrorAction SilentlyContinue + Set-StrictMode -Version 3 + + $action = '{{action}}' + $validActions = @( + 'resolve', + 'for-environment', + 'install', + 'apply-file-options', + 'validate-workspace-msrv', + 'msrv', + 'install-msrv', + 'emit-file-options' + ) + if ($action -notin $validActions) { + Write-Error "_anvil-resolve-stable: unknown action '$action'" + exit 2 + } + + $repoRoot = '{{justfile_directory()}}' + + function Get-RootToolchainFilePath { + foreach ($name in @('rust-toolchain', 'rust-toolchain.toml')) { + $path = Join-Path $repoRoot $name + if (Test-Path -LiteralPath $path -PathType Leaf) { + return $path + } + } + return $null + } + + function Read-RootToolchainFile([string] $path) { + $content = Get-Content -LiteralPath $path -Raw + if ([string]::IsNullOrWhiteSpace($content)) { + throw "anvil: root toolchain file '$path' is empty; remove it or declare a toolchain configuration" + } + return $content + } + + function Get-SelectingToolchainFile { + $path = Get-RootToolchainFilePath + if ($null -eq $path) { + return $null + } + + $content = Read-RootToolchainFile $path + if ((Split-Path -Leaf $path) -eq 'rust-toolchain' -and $content.TrimStart() -notmatch '^\[') { + return [pscustomobject]@{ + Path = $path + Value = $content.Trim() + Selector = 'channel' + Source = 'file' + Installable = $false + } + } + + $inToolchain = $false + foreach ($line in $content -split '\r?\n') { + if ($line -match '^\s*\[\s*toolchain\s*\]\s*(?:#.*)?$') { + $inToolchain = $true + continue + } + if ($line -match '^\s*\[') { + $inToolchain = $false + continue + } + if ($inToolchain -and $line -match '^\s*["'']?(channel|path)["'']?\s*=\s*(["''])([^"'']+)\2\s*(?:#.*)?$') { + $value = $Matches[3] + if ($Matches[1] -eq 'path' -and -not [IO.Path]::IsPathRooted($value)) { + $value = [IO.Path]::GetFullPath((Join-Path $repoRoot $value)) + } + return [pscustomobject]@{ + Path = $path + Value = $value + Selector = $Matches[1] + Source = 'file' + Installable = $false + } + } + } + return $null + } + + function Get-ToolchainFileOptions { + $path = Get-RootToolchainFilePath + if ($null -eq $path) { + return $null + } + + $content = Read-RootToolchainFile $path + $section = [regex]::Match( + $content, + '(?ms)^\s*\[\s*toolchain\s*\]\s*(?:#.*)?$(.*?)(?=^\s*\[|\z)' + ) + if (-not $section.Success) { + return $null + } + + $body = $section.Groups[1].Value + $profileMatch = [regex]::Match( + $body, + '(?m)^\s*["'']?profile["'']?\s*=\s*(["''])([^"'']+)\1\s*(?:#.*)?$' + ) + $readArray = { + param([string] $name) + $arrayMatch = [regex]::Match( + $body, + "(?ms)^\s*[`"']?$name[`"']?\s*=\s*\[(.*?)\]" + ) + if (-not $arrayMatch.Success) { + return @() + } + return @( + [regex]::Matches($arrayMatch.Groups[1].Value, '(["''])([^"'']+)\1') | + ForEach-Object { $_.Groups[2].Value } + ) + } + + return [pscustomobject]@{ + Path = $path + Profile = if ($profileMatch.Success) { $profileMatch.Groups[2].Value } else { $null } + Components = @(& $readArray 'components') + Targets = @(& $readArray 'targets') + } + } + + function Get-RootMsrv([switch] $AllowMissing) { + $manifestPath = Join-Path $repoRoot 'Cargo.toml' + if (-not (Test-Path -LiteralPath $manifestPath -PathType Leaf)) { + throw "anvil: Cargo.toml not found at repository root '$repoRoot'" + } + + $values = @{} + $section = '' + foreach ($line in (Get-Content -LiteralPath $manifestPath)) { + if ($line -match '^\s*\[\s*([^\]]+)\s*\]\s*(?:#.*)?$') { + $section = $Matches[1].Trim() + continue + } + if ($section -in @('workspace.package', 'package') -and + $line -match '^\s*["'']?rust-version["'']?\s*=\s*(["''])([^"'']+)\1\s*(?:#.*)?$') { + $values[$section] = $Matches[2] + } + } + + if ($values.ContainsKey('workspace.package')) { + return $values['workspace.package'] + } + if ($values.ContainsKey('package')) { + return $values['package'] + } + + if ($AllowMissing) { + return $null + } + throw 'anvil: no selecting rust-toolchain(.toml) and no root [workspace.package] or [package] rust-version; declare an MSRV or select a toolchain explicitly' + } + + function Assert-WorkspaceMsrvCompatibility { + $manifestPath = Join-Path $repoRoot 'Cargo.toml' + $metadataText = (& cargo metadata --manifest-path $manifestPath --format-version 1 --no-deps 2>&1 | Out-String) + if ($LASTEXITCODE -ne 0) { + throw "anvil: cargo metadata failed while validating workspace MSRVs:`n$metadataText" + } + $metadata = $metadataText | ConvertFrom-Json + $memberIds = [Collections.Generic.HashSet[string]]::new( + [string[]] $metadata.workspace_members, + [StringComparer]::Ordinal + ) + $members = @($metadata.packages | Where-Object { $memberIds.Contains([string] $_.id) }) + $missing = @($members | Where-Object { [string]::IsNullOrWhiteSpace([string] $_.rust_version) } | ForEach-Object name) + if ($missing.Count -gt 0) { + throw "anvil: workspace packages without a resolved rust-version: $($missing -join ', '); declare one or select a toolchain explicitly" + } + + $rootMsrv = Get-RootMsrv + $newer = @( + $members | + Where-Object { [version] ([string] $_.rust_version) -gt [version] $rootMsrv } | + ForEach-Object { "$($_.name) ($($_.rust_version))" } + ) + if ($newer.Count -gt 0) { + throw "anvil: workspace packages require a compiler newer than the root MSRV $rootMsrv`: $($newer -join ', '); raise the root MSRV or select one catalog toolchain explicitly with rust-toolchain.toml" + } + } + + function Assert-CompleteInternalToolchainConfiguration { + if (-not [string]::IsNullOrWhiteSpace($env:ANVIL_MSRV_TOOLCHAIN) -and + [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN) -and + $null -eq $fileSelection) { + throw 'anvil: ANVIL_MSRV_TOOLCHAIN is set without RUSTUP_TOOLCHAIN and no repository toolchain file selects stable checks; set RUSTUP_TOOLCHAIN to the provisioned stable toolchain or unset ANVIL_MSRV_TOOLCHAIN' + } + } + + function Get-MsrvSelection { + $msrv = Get-RootMsrv -AllowMissing + if ([string]::IsNullOrWhiteSpace($msrv)) { + return $null + } + + if (-not [string]::IsNullOrWhiteSpace($env:ANVIL_MSRV_TOOLCHAIN)) { + return [pscustomobject]@{ + Value = $env:ANVIL_MSRV_TOOLCHAIN + Source = 'mapped-msrv' + Installable = $false + Mapped = $true + } + } + return [pscustomobject]@{ Value = $msrv; Source = 'msrv'; Installable = $true; Mapped = $false } + } + + function Apply-ToolchainFileOptions([object] $toolchain) { + if ($toolchain.Source -eq 'file' -or $null -eq $toolchainFileOptions) { + return + } + foreach ($component in $toolchainFileOptions.Components) { + rustup component add --toolchain $toolchain.Value $component + if ($LASTEXITCODE -ne 0) { + Write-Warning "anvil: toolchain-file component '$component' is unavailable for '$($toolchain.Value)'; rustup would skip an unavailable file component" + } + } + foreach ($target in $toolchainFileOptions.Targets) { + rustup target add --toolchain $toolchain.Value $target + if ($LASTEXITCODE -ne 0) { + Write-Warning "anvil: toolchain-file target '$target' is unavailable for '$($toolchain.Value)'; rustup would skip an unavailable file target" + } + } + } + + function Install-Toolchain([object] $toolchain, [string] $label) { + if ($toolchain.Installable) { + $installed = rustup toolchain list + if ($LASTEXITCODE -ne 0) { + throw 'anvil: rustup toolchain list failed' + } + if ($installed -notmatch "(?m)^$([regex]::Escape($toolchain.Value))(?:-|$)") { + $profile = if ($null -ne $toolchainFileOptions -and $toolchainFileOptions.Profile) { + $toolchainFileOptions.Profile + } else { + 'minimal' + } + Write-Host "anvil: installing $label toolchain '$($toolchain.Value)'" + rustup toolchain install $toolchain.Value --profile $profile + if ($LASTEXITCODE -ne 0) { + throw "anvil: failed to install $label toolchain '$($toolchain.Value)'" + } + } + } + Apply-ToolchainFileOptions $toolchain + } + + $fileSelection = Get-SelectingToolchainFile + $toolchainFileOptions = Get-ToolchainFileOptions + + if ($action -eq 'emit-file-options') { + if ($null -ne $toolchainFileOptions) { + $toolchainFileOptions | ConvertTo-Json -Compress + } + exit 0 + } + + if ($action -eq 'validate-workspace-msrv') { + Assert-CompleteInternalToolchainConfiguration + $skipWorkspaceMsrvValidation = -not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN) + if (-not $skipWorkspaceMsrvValidation -and $null -eq $fileSelection) { + Assert-WorkspaceMsrvCompatibility + } + exit 0 + } + + if ($action -in @('msrv', 'install-msrv')) { + $msrvSelection = Get-MsrvSelection + if ($null -eq $msrvSelection) { + exit 0 + } + if ($action -eq 'install-msrv') { + if ($msrvSelection.Mapped) { + & cargo "+$($msrvSelection.Value)" --version *> $null + if ($LASTEXITCODE -ne 0) { + throw "anvil: mapped MSRV toolchain '$($msrvSelection.Value)' is unavailable; provision ANVIL_MSRV_TOOLCHAIN or unset it to let Anvil install the declared public MSRV" + } + } + Install-Toolchain $msrvSelection 'MSRV' + } else { + $msrvSelection.Value + } + exit 0 + } + + Assert-CompleteInternalToolchainConfiguration + $selection = if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { + [pscustomobject]@{ Value = $env:RUSTUP_TOOLCHAIN; Source = 'environment'; Installable = $false } + } elseif ($null -ne $fileSelection) { + $fileSelection + } else { + [pscustomobject]@{ Value = Get-RootMsrv; Source = 'msrv'; Installable = $true } + } + + if ($action -eq 'apply-file-options') { + Apply-ToolchainFileOptions $selection + exit 0 + } + + if ($action -eq 'install') { + Install-Toolchain $selection 'stable' + exit 0 + } + + if ($action -eq 'for-environment' -and $selection.Source -eq 'file') { + '' } else { - $env:RUSTUP_TOOLCHAIN = $toolchain + $selection.Value } - Set-Location '{{invocation_directory_native()}}' - & '{{command}}' {{args}} - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # _install-tool: install a cargo subcommand at the catalog version, # or no-op if it is already installed at or above that version. The @@ -239,7 +520,7 @@ _install-tool name version installer source_prereq="": # cached binaries and fail with "binary already exists in destination". $installed = $null $pattern = '^' + [regex]::Escape($name) + ' v(\S+):' - foreach ($line in (& "{{ just_executable() }}" _anvil-with-stable cargo install --list 2>$null)) { + foreach ($line in (& cargo {{_anvil_stable_toolchain_args}} install --list 2>$null)) { if ($line -match $pattern) { $installed = $Matches[1]; break } } if ($installed) { @@ -257,7 +538,7 @@ _install-tool name version installer source_prereq="": if ($installer -eq 'binstall') { if (-not (Get-Command cargo-binstall -ErrorAction SilentlyContinue)) { Write-Host ' Bootstrapping cargo-binstall' - & "{{ just_executable() }}" _anvil-with-stable cargo install --locked cargo-binstall + & cargo {{_anvil_stable_toolchain_args}} install --locked cargo-binstall if ($LASTEXITCODE -ne 0) { Write-Error 'cargo-binstall bootstrap failed' exit $LASTEXITCODE @@ -271,7 +552,7 @@ _install-tool name version installer source_prereq="": $binstallArgs += @('--disable-strategies', 'compile') } $binstallArgs += @($name, '--version', "=$version") - & "{{ just_executable() }}" _anvil-with-stable cargo @binstallArgs + & cargo {{_anvil_stable_toolchain_args}} @binstallArgs if ($LASTEXITCODE -eq 0) { exit 0 } Write-Host ' binstall failed; falling back to cargo install' -ForegroundColor Yellow } @@ -279,7 +560,7 @@ _install-tool name version installer source_prereq="": & "{{just_executable()}}" $sourcePrereq if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } } - & "{{ just_executable() }}" _anvil-with-stable cargo install --locked $name --version "=$version" + & cargo {{_anvil_stable_toolchain_args}} install --locked $name --version "=$version" if ($LASTEXITCODE -ne 0) { Write-Error "$name install FAILED" exit $LASTEXITCODE @@ -295,7 +576,7 @@ _check-tool name version: $installed = $null $pattern = '^' + [regex]::Escape($name) + ' v(\S+):' - foreach ($line in (& "{{ just_executable() }}" _anvil-with-stable cargo install --list 2>$null)) { + foreach ($line in (& cargo {{_anvil_stable_toolchain_args}} install --list 2>$null)) { if ($line -match $pattern) { $installed = $Matches[1]; break } } if (-not $installed) { @@ -376,14 +657,14 @@ _install-component toolchain component: } if ($null -ne $subcommand) { if ($toolchain -eq 'default') { - & "{{ just_executable() }}" _anvil-with-stable cargo $subcommand --version *> $null + & cargo {{_anvil_stable_toolchain_args}} $subcommand --version *> $null } else { & cargo "+$toolchain" $subcommand --version *> $null } $present = ($LASTEXITCODE -eq 0) } else { $sysroot = if ($toolchain -eq 'default') { - & "{{ just_executable() }}" _anvil-with-stable rustc --print sysroot 2>$null + & rustc {{_anvil_stable_toolchain_args}} --print sysroot 2>$null } else { & rustc "+$toolchain" --print sysroot 2>$null } @@ -444,14 +725,14 @@ _check-component toolchain component: } if ($null -ne $subcommand) { if ($toolchain -eq 'default') { - & "{{ just_executable() }}" _anvil-with-stable cargo $subcommand --version *> $null + & cargo {{_anvil_stable_toolchain_args}} $subcommand --version *> $null } else { & cargo "+$toolchain" $subcommand --version *> $null } $present = ($LASTEXITCODE -eq 0) } else { $sysroot = if ($toolchain -eq 'default') { - & "{{ just_executable() }}" _anvil-with-stable rustc --print sysroot 2>$null + & rustc {{_anvil_stable_toolchain_args}} --print sysroot 2>$null } else { & rustc "+$toolchain" --print sysroot 2>$null } diff --git a/justfiles/anvil/versions.just b/justfiles/anvil/versions.just index e2441b62a..70a33d8b3 100644 --- a/justfiles/anvil/versions.just +++ b/justfiles/anvil/versions.just @@ -5,6 +5,8 @@ # Update behaviour: https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/updates.md # # Pinned versions used by the anvil check tree. + +set lazy # # Everything anvil installs (cargo subcommands + rustup toolchains) # is pinned here. The pinning policy: @@ -30,9 +32,102 @@ # Rustup toolchains # ============================================================================ -# Stable checks select their compiler through `_anvil-with-stable` at the -# individual command site. `RUSTUP_TOOLCHAIN` is an input override only; Anvil -# never exports a resolved value globally into unrelated recipes or helpers. +# Stable commands interpolate this lazy PowerShell argument-array expression +# directly at each command site. `RUSTUP_TOOLCHAIN` is an input override only; +# Anvil never exports a resolved value globally into unrelated recipes or +# helpers. +[private] +_anvil_stable_toolchain_selection_script := ''' +param([string] $RepoRoot) + +$ErrorActionPreference = 'Stop' +Set-StrictMode -Version 3 + +function Write-ExplicitToolchainArgs([string] $Toolchain) { + Write-Output ("@('+" + $Toolchain.Replace("'", "''") + "')") +} + +if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { + Write-ExplicitToolchainArgs $env:RUSTUP_TOOLCHAIN + exit 0 +} + +$toolchainFile = $null +foreach ($name in @('rust-toolchain', 'rust-toolchain.toml')) { + $candidate = Join-Path $RepoRoot $name + if (Test-Path -LiteralPath $candidate -PathType Leaf) { + $toolchainFile = $candidate + break + } +} + +$selectingFile = $false +if ($null -ne $toolchainFile) { + $content = Get-Content -LiteralPath $toolchainFile -Raw + if ([string]::IsNullOrWhiteSpace($content)) { + throw "anvil: root toolchain file '$toolchainFile' is empty; remove it or declare a toolchain configuration" + } + if ((Split-Path -Leaf $toolchainFile) -eq 'rust-toolchain' -and $content.TrimStart() -notmatch '^\[') { + $selectingFile = $true + } else { + $inToolchain = $false + foreach ($line in $content -split '\r?\n') { + if ($line -match '^\s*\[\s*toolchain\s*\]\s*(?:#.*)?$') { + $inToolchain = $true + continue + } + if ($line -match '^\s*\[') { + $inToolchain = $false + continue + } + if ($inToolchain -and $line -match '^\s*["'']?(channel|path)["'']?\s*=\s*(["''])([^"'']+)\2\s*(?:#.*)?$') { + $selectingFile = $true + break + } + } + } +} + +if ($selectingFile) { + Write-Output '@()' + exit 0 +} + +if (-not [string]::IsNullOrWhiteSpace($env:ANVIL_MSRV_TOOLCHAIN)) { + throw 'anvil: ANVIL_MSRV_TOOLCHAIN is set without RUSTUP_TOOLCHAIN and no repository toolchain file selects stable checks; set RUSTUP_TOOLCHAIN to the provisioned stable toolchain or unset ANVIL_MSRV_TOOLCHAIN' +} + +$manifestPath = Join-Path $RepoRoot 'Cargo.toml' +if (-not (Test-Path -LiteralPath $manifestPath -PathType Leaf)) { + throw "anvil: Cargo.toml not found at repository root '$RepoRoot'" +} + +$values = @{} +$section = '' +foreach ($line in (Get-Content -LiteralPath $manifestPath)) { + if ($line -match '^\s*\[\s*([^\]]+)\s*\]\s*(?:#.*)?$') { + $section = $Matches[1].Trim() + continue + } + if ($section -in @('workspace.package', 'package') -and + $line -match '^\s*["'']?rust-version["'']?\s*=\s*(["''])([^"'']+)\1\s*(?:#.*)?$') { + $values[$section] = $Matches[2] + } +} + +$msrv = if ($values.ContainsKey('workspace.package')) { + $values['workspace.package'] +} elseif ($values.ContainsKey('package')) { + $values['package'] +} else { + throw 'anvil: no selecting rust-toolchain(.toml) and no root [workspace.package] or [package] rust-version; declare an MSRV or select a toolchain explicitly' +} +Write-ExplicitToolchainArgs $msrv +exit 0 +''' + +[private] +_anvil_stable_toolchain_args := if os_family() == "windows" { shell("& ([scriptblock]::Create('" + replace(_anvil_stable_toolchain_selection_script, "'", "''") + "')) '" + replace(justfile_directory(), "'", "''") + "';") } else { shell("pwsh -NoProfile -Command '& ([scriptblock]::Create($args[0])) $args[1]' " + quote(_anvil_stable_toolchain_selection_script) + " " + quote(justfile_directory())) } # General nightly used by udeps, miri, careful, and any future # nightly-dependent check. Bumped on a regular cadence (monthly is a From 64297ae8c7623214ae277ef236970bb097b2c5ef Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Fri, 28 Aug 2026 15:35:57 +0200 Subject: [PATCH 19/32] Fix lazy toolchain selection portability Rely on Just's normal lazy evaluation and derive the repository root from the selector working directory so the embedded PowerShell works on Unix and Windows. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: f7996e6f-5c20-4d7a-b2f7-690af95e557e --- .anvil.lock | 4 ++-- crates/cargo-anvil/templates/justfiles/anvil/versions.just | 5 ++--- justfiles/anvil/versions.just | 5 ++--- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/.anvil.lock b/.anvil.lock index 3704d4038..e01d0bf93 100644 --- a/.anvil.lock +++ b/.anvil.lock @@ -1,7 +1,7 @@ version = 1 tool = "anvil" tool_version = "0.5.0" -catalog_checksum = "sha256:1d75baef11d88ed6b10b92ace1d4c865a11680544fc295d4d3bd8ad1e5aa5149" +catalog_checksum = "sha256:452f8b2718303029ed581911d09b6b23842f5771b4433e7765b84893bcbf5810" [[file]] path = ".anvil/container/Containerfile" @@ -265,7 +265,7 @@ checksum = "sha256:854a3913c6538d361bc16ff7aa08571678eab00a4773cdd5ba1499b76c018 [[file]] path = "justfiles/anvil/versions.just" -checksum = "sha256:e89afcdbf13139ca0fbd422d934107ce68744bad773cd40d33ea53e2b076f262" +checksum = "sha256:aea1bb7d439d73791da04cef0522f09105d10c407b8fcadd9489b7f52dbfe4e1" [[region]] host = ".delta.toml" diff --git a/crates/cargo-anvil/templates/justfiles/anvil/versions.just b/crates/cargo-anvil/templates/justfiles/anvil/versions.just index 70a33d8b3..ae8e9bda1 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/versions.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/versions.just @@ -6,7 +6,6 @@ # # Pinned versions used by the anvil check tree. -set lazy # # Everything anvil installs (cargo subcommands + rustup toolchains) # is pinned here. The pinning policy: @@ -38,7 +37,7 @@ set lazy # helpers. [private] _anvil_stable_toolchain_selection_script := ''' -param([string] $RepoRoot) +$RepoRoot = (Get-Location).Path $ErrorActionPreference = 'Stop' Set-StrictMode -Version 3 @@ -127,7 +126,7 @@ exit 0 ''' [private] -_anvil_stable_toolchain_args := if os_family() == "windows" { shell("& ([scriptblock]::Create('" + replace(_anvil_stable_toolchain_selection_script, "'", "''") + "')) '" + replace(justfile_directory(), "'", "''") + "';") } else { shell("pwsh -NoProfile -Command '& ([scriptblock]::Create($args[0])) $args[1]' " + quote(_anvil_stable_toolchain_selection_script) + " " + quote(justfile_directory())) } +_anvil_stable_toolchain_args := if os_family() == "windows" { shell("& ([scriptblock]::Create('" + replace(_anvil_stable_toolchain_selection_script, "'", "''") + "'));") } else { shell("pwsh -NoProfile -Command '& ([scriptblock]::Create($args[0]))' " + quote(_anvil_stable_toolchain_selection_script)) } # General nightly used by udeps, miri, careful, and any future # nightly-dependent check. Bumped on a regular cadence (monthly is a diff --git a/justfiles/anvil/versions.just b/justfiles/anvil/versions.just index 70a33d8b3..ae8e9bda1 100644 --- a/justfiles/anvil/versions.just +++ b/justfiles/anvil/versions.just @@ -6,7 +6,6 @@ # # Pinned versions used by the anvil check tree. -set lazy # # Everything anvil installs (cargo subcommands + rustup toolchains) # is pinned here. The pinning policy: @@ -38,7 +37,7 @@ set lazy # helpers. [private] _anvil_stable_toolchain_selection_script := ''' -param([string] $RepoRoot) +$RepoRoot = (Get-Location).Path $ErrorActionPreference = 'Stop' Set-StrictMode -Version 3 @@ -127,7 +126,7 @@ exit 0 ''' [private] -_anvil_stable_toolchain_args := if os_family() == "windows" { shell("& ([scriptblock]::Create('" + replace(_anvil_stable_toolchain_selection_script, "'", "''") + "')) '" + replace(justfile_directory(), "'", "''") + "';") } else { shell("pwsh -NoProfile -Command '& ([scriptblock]::Create($args[0])) $args[1]' " + quote(_anvil_stable_toolchain_selection_script) + " " + quote(justfile_directory())) } +_anvil_stable_toolchain_args := if os_family() == "windows" { shell("& ([scriptblock]::Create('" + replace(_anvil_stable_toolchain_selection_script, "'", "''") + "'));") } else { shell("pwsh -NoProfile -Command '& ([scriptblock]::Create($args[0]))' " + quote(_anvil_stable_toolchain_selection_script)) } # General nightly used by udeps, miri, careful, and any future # nightly-dependent check. Bumped on a regular cadence (monthly is a From f72dfafc5604ddbe675b227f6079479e185b4525 Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Fri, 28 Aug 2026 16:23:58 +0200 Subject: [PATCH 20/32] Integrate stable provisioning into setup Make stable toolchain preparation a shared setup prerequisite and evaluate the optional toolchain argument directly inside PowerShell recipes without host-specific shell invocation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: f7996e6f-5c20-4d7a-b2f7-690af95e557e --- .anvil.lock | 20 +-- .anvil/container/Containerfile | 1 - .github/actions/anvil-setup/action.yml | 29 ++-- crates/cargo-anvil/docs/design/ado.md | 60 ++++---- crates/cargo-anvil/docs/design/checks.md | 8 +- crates/cargo-anvil/docs/design/containers.md | 11 +- crates/cargo-anvil/docs/design/github.md | 42 ++++-- crates/cargo-anvil/docs/design/local.md | 49 +++++-- crates/cargo-anvil/src/anvil/artifacts/ado.rs | 11 ++ .../src/anvil/artifacts/container.rs | 2 + .../cargo-anvil/src/anvil/artifacts/github.rs | 3 + .../src/anvil/artifacts/justfile.rs | 138 ++++++++++++++++-- .../cargo-anvil/templates/ado/steps/setup.yml | 34 ++--- .../templates/anvil/container/Containerfile | 1 - .../templates/github/setup-action.yml | 29 ++-- .../justfiles/anvil/checks/bench.just | 2 +- .../justfiles/anvil/checks/doc-build.just | 2 +- .../justfiles/anvil/checks/doc-test.just | 2 +- .../justfiles/anvil/checks/examples.just | 2 +- .../justfiles/anvil/checks/loom.just | 2 +- .../templates/justfiles/anvil/tools.just | 53 +++++-- .../templates/justfiles/anvil/versions.just | 32 ++-- .../snapshots/snapshots__ado_backend.snap | 133 +++++++++-------- .../snapshots/snapshots__github_backend.snap | 128 ++++++++-------- .../snapshots/snapshots__local_only.snap | 99 ++++++++----- justfiles/anvil/checks/bench.just | 2 +- justfiles/anvil/checks/doc-build.just | 2 +- justfiles/anvil/checks/doc-test.just | 2 +- justfiles/anvil/checks/examples.just | 2 +- justfiles/anvil/checks/loom.just | 2 +- justfiles/anvil/tools.just | 53 +++++-- justfiles/anvil/versions.just | 32 ++-- 32 files changed, 614 insertions(+), 374 deletions(-) diff --git a/.anvil.lock b/.anvil.lock index e01d0bf93..2c0dfb63d 100644 --- a/.anvil.lock +++ b/.anvil.lock @@ -1,11 +1,11 @@ version = 1 tool = "anvil" tool_version = "0.5.0" -catalog_checksum = "sha256:452f8b2718303029ed581911d09b6b23842f5771b4433e7765b84893bcbf5810" +catalog_checksum = "sha256:f446b3ec3e2958410d61ce294759410e2e01ff6f651b71ee168e2d2d8e9bf48b" [[file]] path = ".anvil/container/Containerfile" -checksum = "sha256:ddd1cd7500a32b3a0bd93a5a60e96674fe4ba667d3ceb44f5207c447a0907c77" +checksum = "sha256:317a2bfede7c6679750dc37a5b9828419219a86e98f8568cb9106c1460677271" [[file]] path = ".anvil/container/Containerfile.dockerignore" @@ -49,7 +49,7 @@ checksum = "sha256:ff8def6c0786b6e146c4b633dfe38cb9b8ede398345516cca32bbcd558608 [[file]] path = ".github/actions/anvil-setup/action.yml" -checksum = "sha256:367475db03acf48a37c0b205415f5e7120bcb200bba1d27ebb4144cd813d17ae" +checksum = "sha256:259d59fb9381b5e29fe2f0a123e717ba62aaecd25955906fd943f385eca22067" [[file]] path = ".github/actions/anvil-setup/just-problem-matcher.json" @@ -81,7 +81,7 @@ checksum = "sha256:ef33da620683628208b4c5ad51f96f5f6424fe1d56c2d0475487228b1f1ae [[file]] path = "justfiles/anvil/checks/bench.just" -checksum = "sha256:d9f8defd7e08e8988974474ea8761242c572f25f3765da45435c9b62e87d9882" +checksum = "sha256:850793443ada2151a895ee2728e81f03da5430facde16522bcc2e0c314a28577" [[file]] path = "justfiles/anvil/checks/bolero.just" @@ -109,11 +109,11 @@ checksum = "sha256:c5604e8cb0eaa2baf4e2e2c70032d037d21873090375ffcf1ee99983965f6 [[file]] path = "justfiles/anvil/checks/doc-build.just" -checksum = "sha256:a759a5eee188026e41f411207593293003547e2c17b7ff0227ba2e682211ab78" +checksum = "sha256:9e187f61cb1ef9d11fab2e402af8a882b77f6ed17ca332afb9ae0b1d2112e95b" [[file]] path = "justfiles/anvil/checks/doc-test.just" -checksum = "sha256:582329e6695e675ea7653b46baf3609b525e12db2765e59d12b74cf644b12491" +checksum = "sha256:0e54c767de8e57a523a8b1b589a88ab6aca3169ac97d157c42d509d429f4b024" [[file]] path = "justfiles/anvil/checks/ensure-no-cyclic-deps.just" @@ -125,7 +125,7 @@ checksum = "sha256:38e089afe176aea92c93ac6758154b206b6fc8940931035c0d059449fa824 [[file]] path = "justfiles/anvil/checks/examples.just" -checksum = "sha256:3e1b1d5315defdac91bc6ea6495bc015cd0bd4cc468dc317c6ca80464e0d70f0" +checksum = "sha256:8d01fa3e303a9f2b768213636f12c68cc8b41932b68549239e9f10d3be43507b" [[file]] path = "justfiles/anvil/checks/external-types.just" @@ -145,7 +145,7 @@ checksum = "sha256:cb23283ac3d377b1219c904f4de93b05b437b48c93629fec617307914bba6 [[file]] path = "justfiles/anvil/checks/loom.just" -checksum = "sha256:1e68ac182738e89717c53f3e09993371e1f00baaac7cb7ad8463d79527c8bc4b" +checksum = "sha256:7d7cb6e9845a97be18dbe93d8b04860386dc43a5f2033816c3224aa460c5aa63" [[file]] path = "justfiles/anvil/checks/miri-race-coverage.just" @@ -261,11 +261,11 @@ checksum = "sha256:5b2d91569f3fe87cd5f516623a01b5c7ae855ad587870ef9a2ae6d5619dd9 [[file]] path = "justfiles/anvil/tools.just" -checksum = "sha256:854a3913c6538d361bc16ff7aa08571678eab00a4773cdd5ba1499b76c018b15" +checksum = "sha256:601762b80b52946fa00bdd7471de6ddfd26217f1a6f2c4ff324b13c7a751c8e3" [[file]] path = "justfiles/anvil/versions.just" -checksum = "sha256:aea1bb7d439d73791da04cef0522f09105d10c407b8fcadd9489b7f52dbfe4e1" +checksum = "sha256:ac2c5b1c1766003e1840ebf1fb092c6995c8f1804129620ce580d178e59ed7be" [[region]] host = ".delta.toml" diff --git a/.anvil/container/Containerfile b/.anvil/container/Containerfile index 43f3685ac..5709437ba 100644 --- a/.anvil/container/Containerfile +++ b/.anvil/container/Containerfile @@ -53,7 +53,6 @@ RUN --mount=type=cache,id=anvil-cargo-registry,target=/usr/local/cargo/registry --mount=type=cache,id=anvil-cargo-git,target=/usr/local/cargo/git \ --mount=type=cache,id=anvil-cargo-target,target=/tmp/anvil-target \ printf "anvil_runner := \"native\"\nimport 'justfiles/anvil/mod.just'\n" > Justfile \ - && just _anvil-resolve-stable install \ && CARGO_TARGET_DIR=/tmp/anvil-target just anvil-setup COPY .anvil/container/entrypoint.sh /usr/local/bin/anvil-container-entrypoint diff --git a/.github/actions/anvil-setup/action.yml b/.github/actions/anvil-setup/action.yml index cf3ad0863..2b4c47a5c 100644 --- a/.github/actions/anvil-setup/action.yml +++ b/.github/actions/anvil-setup/action.yml @@ -77,8 +77,9 @@ runs: } "Free after cleanup (GiB): $(Get-FreeDiskGiB)" - # The resolver is a standard Just recipe, so bootstrap Just before any - # selected-toolchain setup. cargo-binstall keeps this cold path fast. + # Stable provisioning is part of the generated Just setup graph, so + # bootstrap Just before asking it for the selected compiler version. + # cargo-binstall keeps this cold path fast. - name: Install cargo-binstall uses: cargo-bins/cargo-binstall@v1.21.0 # immutable release, the tag cannot be moved @@ -89,27 +90,15 @@ runs: cargo binstall --no-confirm --locked just || cargo install --locked just fi - - name: Resolve stable toolchain + - name: Provision stable toolchain and capture version + id: rustc-version shell: pwsh -NoProfile -Command ". '{0}'" run: | - & just _anvil-resolve-stable install - if ($LASTEXITCODE -ne 0) { - throw 'anvil: failed to install stable toolchain' + $version = (& just _anvil-stable-rustc-version | Select-Object -Last 1) + if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($version)) { + throw 'anvil: failed to provision the selected stable toolchain' } - $toolchain = (& just _anvil-resolve-stable | Out-String).Trim() - if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($toolchain)) { - throw 'anvil: failed to resolve stable toolchain' - } - Write-Host "anvil: selected stable toolchain '$toolchain'" - - id: rustc-version - shell: pwsh -NoProfile -Command ". '{0}'" - run: | - $toolchain = (& just _anvil-resolve-stable for-environment | Out-String).Trim() - if ($LASTEXITCODE -ne 0) { - throw 'anvil: failed to select stable toolchain arguments' - } - $toolchainArgs = if ([string]::IsNullOrWhiteSpace($toolchain)) { @() } else { @("+$toolchain") } - $version = (& rustc @toolchainArgs --version | Select-Object -Last 1).Split()[1] + $version = $version.Trim() "version=$version" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append - name: Restore cargo cache id: cargo-cache diff --git a/crates/cargo-anvil/docs/design/ado.md b/crates/cargo-anvil/docs/design/ado.md index 765bd0d21..54442b640 100644 --- a/crates/cargo-anvil/docs/design/ado.md +++ b/crates/cargo-anvil/docs/design/ado.md @@ -703,13 +703,16 @@ download to get scoping. ### `setup.yml` and `impact.yml` `setup.yml` is a step template that installs `just` -(`cargo install just --locked`) and then invokes the catalog setup recipes. It +(`cargo install just --locked`), provisions or prepares the selected stable +compiler through the setup dependency graph while capturing its cache-key +version, and then invokes the requested catalog setup recipe. It takes a single `group` parameter that controls which recipes run: - empty (default): runs `just anvil-setup` -- the full catalog. Use for "give me everything" flows. -- `none`: skips the catalog setup entirely. Used by `impact.yml`, which only - needs `cargo-delta` and installs it itself afterwards. +- `none`: skips the group/full tool fan-out after stable provisioning. Used by + `impact.yml`, which only needs `cargo-delta` and installs it itself + afterwards. - any other value (e.g. `pr-fast`, `scheduled-advisories`): runs `just anvil--setup` -- only the tools, components, and toolchains that group actually needs. Every per-group step template @@ -743,14 +746,19 @@ mechanics are in [local.md §4](./local.md#4-impact-scoping-via-the-anvil-impact ## 6. Rust toolchain -The user's root pipeline or compliance template installs Rust before the Anvil -stages run. The generated setup step prepares the catalog toolchain and invokes -`rustc --version` directly with either the selected `+toolchain` argument or no -override for a selecting repository file before restoring the cache. It does -not publish a resolved `RUSTUP_TOOLCHAIN` to subsequent steps. Each stable -recipe lazily selects the same optional argument and invokes Cargo or Rust -directly in its current process, while a selecting toolchain file remains -unoverridden so rustup processes the complete file. +The user's root pipeline or compliance template installs the Rust/rustup +bootstrap before the Anvil stages run. After bootstrapping Just, the generated +setup step calls `_anvil-stable-rustc-version`. Its +`anvil-toolchain-stable-install` dependency provisions a public selection or +prepares an externally provisioned internal selection before invoking `rustc` +directly with the lazy optional `+toolchain` argument. The resulting version +keys the cache. The template never invokes `_anvil-resolve-stable` itself and +does not publish a resolved `RUSTUP_TOOLCHAIN` to subsequent steps. Each stable +recipe evaluates the inline PowerShell array expression and invokes Cargo or +Rust directly in its current PowerShell process. Selection has no +expression-time subprocess, host-OS branch, or change to the adopter's +configured shell. A selecting toolchain file remains unoverridden so rustup +processes the complete file. Selection follows the shared local contract: an existing `RUSTUP_TOOLCHAIN`, then a root toolchain file with `channel` or `path`, then the root MSRV. Internal pipelines @@ -761,21 +769,21 @@ Setup reapplies explicit components and targets from a suppressed root toolchain file to the provisioned compiler. Unavailable options warn and are skipped, matching rustup's native toolchain-file behavior. -Anvil does not install stable Rust on ADO: - -- **1ESPT compliance.** Compliance pipelines provision Rust through msrustup. -- **Backend mapping is a repository decision.** Mapping a public MSRV to an internal - `ms-prod-*` channel belongs in the root pipeline, not the catalog. - -In the OSS / non-1ESPT case, the user adds a `RustInstaller@1` task (or a rustup -shell script) to their root pipeline before the anvil stages template runs. A typical -placement: a setup stage that `dependsOn`s nothing and runs first, followed by the anvil -stages. - -`anvil-tool-rustc-validate-prereqs` validates the selected compiler and the -workspace MSRV compatibility rule. For nightly-requiring checks, the matching -toolchain-validate-prereqs recipe fails with a suggestion to ask the team's pipeline -owner to add that dated nightly to msrustup. +In 1ESPT compliance pipelines, msrustup still provisions the selected +`RUSTUP_TOOLCHAIN`; the Anvil setup primitive treats that caller override as +external and only reapplies applicable repository file options. Mapping a +public MSRV to an internal `ms-prod-*` channel remains a root-pipeline policy. +In OSS/non-1ESPT pipelines, the user provides rustup (for example with +`RustInstaller@1`) and Anvil installs a missing public MSRV selection through +the same setup primitive. + +Shared Cargo-tool/default-component setup and stable-only leaf setup all depend +on `anvil-toolchain-stable-install`; Just deduplicates it for group/full +fan-out. `anvil-tool-rustc-validate-prereqs` remains read-only and validates +the selected compiler and workspace MSRV compatibility rule. For +nightly-requiring checks, the matching toolchain-validate-prereqs recipe fails +with a suggestion to ask the team's pipeline owner to add that dated nightly +to msrustup. ## 7. Caching diff --git a/crates/cargo-anvil/docs/design/checks.md b/crates/cargo-anvil/docs/design/checks.md index c0ad44472..d33b70816 100644 --- a/crates/cargo-anvil/docs/design/checks.md +++ b/crates/cargo-anvil/docs/design/checks.md @@ -167,7 +167,13 @@ Invocations shown without a pinned nightly or MSRV use the selected stable compiler. Their generated PowerShell recipes invoke Cargo directly with the lazy optional stable argument: an explicit `+toolchain` for a caller override or root MSRV, and no argument when a repository toolchain file selects the -compiler. They do not route the main command through a nested Just recipe. +compiler. The shared inline PowerShell array expression evaluates in that same +recipe process without a host-OS branch or selector subprocess. They do not +route the main command through a nested Just recipe. +Their paired setup recipes reach `anvil-toolchain-stable-install` directly or +through the shared Cargo-tool/default-component installer before any stable +command runs. Paired `*-validate-prereqs` recipes remain read-only and never +depend on installation. ### `pr-fast` diff --git a/crates/cargo-anvil/docs/design/containers.md b/crates/cargo-anvil/docs/design/containers.md index 2c7a75ec1..8ddeb62d6 100644 --- a/crates/cargo-anvil/docs/design/containers.md +++ b/crates/cargo-anvil/docs/design/containers.md @@ -173,10 +173,13 @@ available. Runtime execution uses `--pull=never` and never substitutes Container execution uses the same deterministic stable-toolchain selection as native execution: an existing `RUSTUP_TOOLCHAIN`, a selecting repository toolchain file, or the root manifest's MSRV. It fails rather than choosing a -runner or image default when none is available. Image setup uses the ordinary -toolchain setup recipe. Checks then evaluate the lazy optional `+toolchain` -argument and invoke Cargo or Rust directly in their current recipe process; no -resolved value is exported across unrelated recipe processes. +runner or image default when none is available. The Containerfile invokes only +`anvil-setup`; its transitive `anvil-toolchain-stable-install` prerequisite +provisions stable once before any stable Cargo tool installation. Checks then +evaluate the lazy inline PowerShell array expression and invoke Cargo or Rust +directly in their current recipe process. Selection uses no expression-time +subprocess or host-OS branch, and no resolved value is exported across unrelated +recipe processes. The restricted image-construction context does not contain workspace member manifests, so per-package MSRV compatibility validation runs in native and diff --git a/crates/cargo-anvil/docs/design/github.md b/crates/cargo-anvil/docs/design/github.md index 873353fbb..94535d658 100644 --- a/crates/cargo-anvil/docs/design/github.md +++ b/crates/cargo-anvil/docs/design/github.md @@ -735,13 +735,16 @@ result, and log link without check-suite attribution. ### `anvil-setup` `anvil-setup` is a composite action that installs `just` -(`cargo install just --locked`) and then invokes the catalog setup recipes. Its +(`cargo install just --locked`), provisions the selected stable compiler +through the setup dependency graph while capturing its cache-key version, and +then invokes the requested catalog setup recipe. Its `group` input controls which recipes run: - empty (default): runs `just anvil-setup binstall` -- the full catalog. Use for local "give me everything" flows. -- `none`: skips the catalog setup entirely. Used by `anvil-impact`, which only - needs `cargo-delta` and installs it itself afterwards. +- `none`: skips the group/full tool fan-out after stable provisioning. Used by + `anvil-impact`, which only needs `cargo-delta` and installs it itself + afterwards. - any other value (e.g. `pr-fast`, `scheduled-advisories`): runs `just anvil--setup binstall` -- only the tools, components, and toolchains that group actually needs. Ordinary group names contain only @@ -840,17 +843,21 @@ The check → bucket mapping is in ## 7. Rust toolchain -The setup action bootstraps cargo-binstall and Just using the runner environment, -then prepares the catalog's stable toolchain through the standard private Just -recipe before reading `rustc` or restoring build artifacts. It installs a -missing public channel through rustup and invokes `rustc` directly with either -the selected `+toolchain` argument or no override for a selecting repository -file. It does not publish a resolved `RUSTUP_TOOLCHAIN` through `GITHUB_ENV`. +The setup action bootstraps cargo-binstall and Just using the runner +environment, then calls `_anvil-stable-rustc-version`. That helper depends on +`anvil-toolchain-stable-install`, so the setup graph provisions a missing +public selection and replays applicable file options before invoking `rustc` +directly with the lazy optional `+toolchain` argument. The resulting version +keys the cache; the workflow never invokes `_anvil-resolve-stable` itself and +does not publish a resolved `RUSTUP_TOOLCHAIN` through `GITHUB_ENV`. Each stable recipe lazily selects the same optional argument and invokes Cargo -or Rust directly in its current process; selecting toolchain files remain -unoverridden so rustup processes them natively. All matrix legs therefore use -the same repository selection instead of the different stable versions that -runner images may carry, without changing unrelated recipe environments. +or Rust directly in its current PowerShell process. The selector is an inline +PowerShell array expression rather than an expression-time shell subprocess, so +it has no host-OS branch and does not alter the adopter's configured shell. +Selecting toolchain files remain unoverridden so rustup processes them natively. +All matrix legs therefore use the same repository selection instead of the +different stable versions that runner images may carry, without changing +unrelated recipe environments. Selection follows the shared local contract: an existing `RUSTUP_TOOLCHAIN`, then a root toolchain file with `channel` or `path`, then the root MSRV. There is no @@ -877,9 +884,12 @@ users that need additional preparation take ownership of the generated reusable implementation workflow and add the preparation there. The generated composite actions remain implementation details rather than a separately supported API. -`anvil-tool-rustc-validate-prereqs` (depended on by every check that needs rustc) -validates the installed `rustc` against the catalog minimum at recipe time; a -below-minimum `rustc` produces a clean failure message. +Setup recipes that need stable Rust depend on +`anvil-toolchain-stable-install`; shared Cargo-tool installation and +default-component installation carry the same prerequisite. Just deduplicates +it across each group/full setup invocation. +`anvil-tool-rustc-validate-prereqs` remains read-only and validates the +installed `rustc` and workspace MSRV contract at check time. ## 8. Caching diff --git a/crates/cargo-anvil/docs/design/local.md b/crates/cargo-anvil/docs/design/local.md index eaf3b2c60..ab0452f73 100644 --- a/crates/cargo-anvil/docs/design/local.md +++ b/crates/cargo-anvil/docs/design/local.md @@ -116,6 +116,16 @@ confirm the tool meets the catalog's pin. Missing or below-pin tools fail with a one-line install hint pointing at the matching `anvil-tool--install` recipe. The cost is a handful of cheap lookups per check, well under a second on a warm cache. +Setup and validation have separate dependency graphs. Every setup path that +uses stable Cargo/Rust or installs a Cargo tool reaches +`anvil-toolchain-stable-install` first. The shared Cargo-tool installer and +default-toolchain component installers own that prerequisite, while checks +that need only built-in stable Cargo (bench, doc-build, doc-test, examples, +and loom) depend on it directly. Just deduplicates the primitive when a group, +tier, or full setup fans out across many checks. The corresponding +`*-validate-prereqs` graph never depends on an install recipe and never mutates +the environment. + ### groups/ One file per cloud-workflow-visible group, `groups/.just`, defining @@ -401,13 +411,15 @@ without that stable override (and without a selecting repository toolchain file) is rejected as incomplete internal configuration. An empty root toolchain file is invalid and produces an actionable error. -`versions.just` holds a shared multiline PowerShell selection script and the -lazy, non-exported `_anvil_stable_toolchain_args` value that evaluates it. -`RUSTUP_TOOLCHAIN` is an input to selection, never a globally exported output. -The value is a PowerShell argument-array expression: `@()` for a selecting -repository toolchain file, or `@('+toolchain')` for a caller override or MSRV -fallback. Apostrophes in explicit toolchain names are escaped before the -expression is interpolated. +`versions.just` holds the selector as the lazy, non-exported +`_anvil_stable_toolchain_args` value. Its value is a multiline PowerShell array +expression, not a command for Just's expression-time `shell()` function. Just +interpolates the expression into an existing PowerShell recipe, where it +evaluates to no elements for a selecting repository toolchain file or one +`+toolchain` string for a caller override or MSRV fallback. Toolchain names, +including apostrophes, therefore remain runtime string data rather than source +text that needs escaping or reparsing. `RUSTUP_TOOLCHAIN` is an input to +selection, never a globally exported output. Each stable recipe executes Cargo or Rust directly in its current PowerShell recipe process, for example @@ -416,8 +428,10 @@ file therefore reaches native rustup unoverridden, including its components, targets, profile, and path semantics. Explicit caller/MSRV selections are visible as Cargo's `+toolchain` argument. Nightly and MSRV-specific checks keep their own explicit `+toolchain` arguments. No nested Just wrapper sits on the -main Cargo/Rust command path, and unrelated recipes and raw commands retain the -caller's normal environment. +main Cargo/Rust command path. Selection starts no subprocess, has no +OS-specific branch or host-shell quoting, and does not change the adopter's +configured shell. Unrelated recipes and raw commands retain the caller's normal +environment. An environment override or MSRV fallback normally causes rustup to ignore the repository toolchain file. Anvil therefore reads `profile`, `components`, and @@ -428,12 +442,17 @@ Explicit components and targets are added at setup time. If an option is unavail for the selected compiler, Anvil warns and skips it, matching rustup's handling of unavailable options read directly from a toolchain file. -The private `_anvil-resolve-stable` recipe contains the setup implementation -for installing public selections, replaying toolchain-file options, validating -workspace MSRVs, and mapping the MSRV in internal environments. Setup workflows -and container construction may invoke this ordinary recipe. It does not wrap -the Cargo/Rust command that performs a check and does not delegate to a -standalone script. +The public setup primitive `anvil-toolchain-stable-install` owns stable +provisioning. It delegates parsing, public installation, toolchain-file option +replay, and internal mapping to the private `_anvil-resolve-stable` +implementation. Leaf setup recipes reach the primitive directly or through the +shared Cargo-tool/default-component installers; group, tier, and +`anvil-setup` recipes inherit and deduplicate that dependency. Cloud version +capture uses the private `_anvil-stable-rustc-version` recipe, whose dependency +provisions stable before it invokes `rustc` directly with the lazy argument. +Workflows and container construction therefore invoke setup operations rather +than orchestrating `_anvil-resolve-stable` actions. Neither helper wraps a +Cargo/Rust command that performs a check, and no standalone script is involved. When selection falls back to the root MSRV, prerequisite validation reads `cargo metadata` and requires every workspace package to resolve a diff --git a/crates/cargo-anvil/src/anvil/artifacts/ado.rs b/crates/cargo-anvil/src/anvil/artifacts/ado.rs index 0bf61f645..99958e1da 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/ado.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/ado.rs @@ -214,8 +214,19 @@ mod tests { fn setup_step_takes_group_parameter_and_dispatches() { assert!(SETUP_STEP.contains("name: group")); assert!(SETUP_STEP.contains("just anvil-setup")); + assert!(SETUP_STEP.contains("just _anvil-stable-rustc-version")); + assert!(!SETUP_STEP.contains("_anvil-resolve-stable")); + assert!(!SETUP_STEP.contains("just anvil-toolchain-stable-install")); assert!(SETUP_STEP.contains("just anvil-${{ parameters.group }}-setup")); assert!(SETUP_STEP.contains("eq(parameters.group, 'none')")); + let just_bootstrap = SETUP_STEP.find("anvil setup (install just)").expect("setup must bootstrap Just"); + let version_capture = SETUP_STEP + .find("just _anvil-stable-rustc-version") + .expect("setup must capture the provisioned stable compiler version"); + assert!( + just_bootstrap < version_capture, + "Just must be bootstrapped before setup-driven stable version capture" + ); } #[test] diff --git a/crates/cargo-anvil/src/anvil/artifacts/container.rs b/crates/cargo-anvil/src/anvil/artifacts/container.rs index 7cbdcab1e..3ed8e30e6 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/container.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/container.rs @@ -226,6 +226,8 @@ mod tests { #[test] fn containerfile_installs_the_generated_toolset() { assert!(CONTAINERFILE.contains("just anvil-setup")); + assert!(!CONTAINERFILE.contains("_anvil-resolve-stable")); + assert!(!CONTAINERFILE.contains("just anvil-toolchain-stable-install")); assert!(CONTAINERFILE.contains("COPY . .")); assert!(IGNORE.contains("!.anvil/container/*")); assert!(!IGNORE.contains("resolve-stable-toolchain")); diff --git a/crates/cargo-anvil/src/anvil/artifacts/github.rs b/crates/cargo-anvil/src/anvil/artifacts/github.rs index 92fcb0788..0975a0b74 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/github.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/github.rs @@ -165,6 +165,9 @@ mod tests { fn setup_action_takes_group_input_and_dispatches() { assert!(SETUP_ACTION.contains("group:")); assert!(SETUP_ACTION.contains("just anvil-setup binstall")); + assert!(SETUP_ACTION.contains("just _anvil-stable-rustc-version")); + assert!(!SETUP_ACTION.contains("_anvil-resolve-stable")); + assert!(!SETUP_ACTION.contains("just anvil-toolchain-stable-install")); assert!(SETUP_ACTION.contains("ANVIL_GROUP: ${{ inputs.group }}")); assert!(SETUP_ACTION.contains("just \"anvil-$ANVIL_GROUP-setup\" binstall")); assert!(SETUP_ACTION.contains(r"^[a-z0-9-]+$")); diff --git a/crates/cargo-anvil/src/anvil/artifacts/justfile.rs b/crates/cargo-anvil/src/anvil/artifacts/justfile.rs index 0ac0af584..e416bc0e1 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/justfile.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/justfile.rs @@ -330,6 +330,7 @@ mod tests { assert!(TOOLS_JUST.contains("anvil-tool-cargo-deny-install")); assert!(TOOLS_JUST.contains("anvil-tool-cargo-deny-validate-prereqs")); assert!(TOOLS_JUST.contains("anvil-component-default-clippy-install")); + assert!(TOOLS_JUST.contains("anvil-toolchain-stable-install")); assert!(TOOLS_JUST.contains("anvil-toolchain-nightly-install")); } @@ -738,15 +739,13 @@ mod tests { } #[test] - fn stable_toolchain_selection_is_lazy_and_scoped_to_each_command() { + fn stable_toolchain_selection_is_scoped_to_each_command() { assert!(!VERSIONS_JUST.contains("export RUSTUP_TOOLCHAIN")); - assert!(VERSIONS_JUST.contains("_anvil_stable_toolchain_selection_script := '''")); - assert!(VERSIONS_JUST.contains("set lazy")); - assert!(VERSIONS_JUST.contains("_anvil_stable_toolchain_args :=")); - assert!(VERSIONS_JUST.contains("shell(\"& ([scriptblock]::Create('\"")); - assert!(VERSIONS_JUST.contains("quote(_anvil_stable_toolchain_selection_script)")); - assert!(VERSIONS_JUST.contains("Write-Output '@()'")); - assert!(VERSIONS_JUST.contains("\"@('+\"")); + assert!(VERSIONS_JUST.contains("_anvil_stable_toolchain_args := trim('''\n@(& {")); + assert!(VERSIONS_JUST.contains("Write-Output ('+' + $Toolchain)")); + assert!(!VERSIONS_JUST.contains("os_family()")); + assert!(!VERSIONS_JUST.contains("shell(")); + assert!(!VERSIONS_JUST.contains("[scriptblock]::Create")); assert!(TOOLS_JUST.contains("_anvil-resolve-stable action=\"resolve\":")); assert!(!TOOLS_JUST.contains("_anvil-with-stable")); assert!(TOOLS_JUST.contains("& cargo {{_anvil_stable_toolchain_args}}")); @@ -754,6 +753,64 @@ mod tests { assert!(TOOLS_JUST.contains("rustup target add --toolchain")); } + #[test] + fn setup_graph_owns_stable_toolchain_provisioning() { + assert!( + TOOLS_JUST.contains("anvil-toolchain-stable-install: (_anvil-resolve-stable \"install\")"), + "stable provisioning must have a setup-specific recipe" + ); + assert!( + TOOLS_JUST.contains("_install-tool name version installer source_prereq=\"\": anvil-toolchain-stable-install (_install-tool-core name version installer source_prereq)"), + "every shared Cargo-tool installation must provision stable first" + ); + assert!( + TOOLS_JUST.contains("& just _install-tool-core cargo-bolero"), + "platform-gated installers must use the already-provisioned core helper" + ); + assert!( + TOOLS_JUST.contains("& just _install-tool-core cargo-mutants"), + "platform-gated installers must use the already-provisioned core helper" + ); + for component in ["clippy", "rustfmt"] { + let expected = format!("anvil-component-default-{component}-install: anvil-toolchain-stable-install"); + assert!( + TOOLS_JUST.contains(&expected), + "default component '{component}' must provision stable first" + ); + } + assert!( + TOOLS_JUST.contains("_anvil-stable-rustc-version: anvil-toolchain-stable-install"), + "cloud version capture must use the setup dependency graph" + ); + + let checks = all_check_bodies(); + for check in ["bench", "doc-build", "doc-test", "examples", "loom"] { + let setup = format!("anvil-{check}-setup installer=\"install\": anvil-toolchain-stable-install"); + let validation = format!("anvil-{check}-validate-prereqs: anvil-tool-rustc-validate-prereqs"); + assert!(checks.contains(&setup), "{check} setup must provision stable"); + assert!( + checks.contains(&validation), + "{check} validation must remain read-only prerequisite validation" + ); + } + assert!( + !checks.contains("validate-prereqs: anvil-toolchain-stable-install"), + "validate-prereqs recipes must never install stable" + ); + + let groups = all_group_bodies(); + assert!( + groups.contains("(anvil-tool-cargo-delta-install installer)") && groups.contains("(anvil-bench-setup installer)"), + "representative group setup paths must reach stable provisioning through Cargo-tool or stable-only leaf setup" + ); + assert!( + TIERS_JUST.contains("anvil-full-setup installer=\"install\":") + && TIERS_JUST.contains("(anvil-pr-setup installer)") + && TIERS_JUST.contains("anvil-setup installer=\"install\": (anvil-full-setup installer)"), + "full and global setup must retain their transitive path to stable provisioning" + ); + } + #[test] fn checks_do_not_invoke_an_implicit_default_cargo() { for (path, body) in CHECK_FILES { @@ -803,7 +860,16 @@ mod tests { "import 'tools.just'\n\n", "[script(\"pwsh\", \"-NoProfile\")]\n", "_anvil-test-stable-args:\n", - " Write-Output \"{{_anvil_stable_toolchain_args}}\"\n", + " $stableArgs = {{_anvil_stable_toolchain_args}}\n", + " if ($stableArgs.Count -eq 0) {\n", + " Write-Output '@()'\n", + " exit 0\n", + " }\n", + " $escaped = ([string] $stableArgs[0]).Replace(\"'\", \"''\")\n", + " Write-Output (\"@('\" + $escaped + \"')\")\n\n", + "[script(\"pwsh\", \"-NoProfile\")]\n", + "_anvil-test-stable-command:\n", + " & cargo {{_anvil_stable_toolchain_args}} --version\n", ), ) .expect("Justfile fixture must be writable"); @@ -831,7 +897,7 @@ mod tests { command.args(["_anvil-resolve-stable", "for-environment"]); } ["-InstallIfMissing"] => { - command.args(["_anvil-resolve-stable", "install"]); + command.arg("anvil-toolchain-stable-install"); } ["-ApplyToolchainFileOptions"] => { command.args(["_anvil-resolve-stable", "apply-file-options"]); @@ -906,6 +972,58 @@ mod tests { assert_eq!(resolved(root, Some("team's-toolchain")), "@('+team''s-toolchain')"); } + #[test] + fn passes_selected_arguments_directly_to_cargo() { + let temp = fixture("[workspace.package]\nrust-version = \"1.93\"\n"); + let shim = TempDir::new().expect("cargo shim directory must be creatable"); + #[cfg(windows)] + fs::write(shim.path().join("cargo.cmd"), "@echo off\r\necho %*\r\nexit /b 0\r\n").expect("Cargo shim must be writable"); + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + + let cargo = shim.path().join("cargo"); + fs::write(&cargo, "#!/bin/sh\nprintf '%s\\n' \"$*\"\nexit 0\n").expect("Cargo shim must be writable"); + fs::set_permissions(&cargo, fs::Permissions::from_mode(0o755)).expect("Cargo shim must be executable"); + } + let mut paths = vec![shim.path().to_path_buf()]; + paths.extend(env::split_paths(&env::var_os("PATH").unwrap_or_default())); + let path = env::join_paths(paths).expect("shim PATH must be valid"); + + let output = command(temp.path()) + .arg("_anvil-test-stable-command") + .env("RUSTUP_TOOLCHAIN", "team's-toolchain") + .env("PATH", &path) + .output() + .expect("direct stable Cargo command must run"); + assert!( + output.status.success(), + "direct stable Cargo command failed: {}", + normalized_diagnostic(&output) + ); + assert_eq!( + String::from_utf8(output.stdout).expect("Cargo shim output must be UTF-8").trim(), + "+team's-toolchain --version" + ); + + fs::write(temp.path().join("rust-toolchain.toml"), "[toolchain]\nchannel = \"1.94\"\n") + .expect("toolchain fixture must be writable"); + let output = command(temp.path()) + .arg("_anvil-test-stable-command") + .env("PATH", path) + .output() + .expect("repository-selected Cargo command must run"); + assert!( + output.status.success(), + "repository-selected Cargo command failed: {}", + normalized_diagnostic(&output) + ); + assert_eq!( + String::from_utf8(output.stdout).expect("Cargo shim output must be UTF-8").trim(), + "--version" + ); + } + #[test] fn rejects_empty_legacy_toolchain_files_with_an_actionable_error() { for contents in ["", " \r\n\t"] { diff --git a/crates/cargo-anvil/templates/ado/steps/setup.yml b/crates/cargo-anvil/templates/ado/steps/setup.yml index ad3c6cbcc..c7352dd59 100644 --- a/crates/cargo-anvil/templates/ado/steps/setup.yml +++ b/crates/cargo-anvil/templates/ado/steps/setup.yml @@ -18,26 +18,20 @@ parameters: type: string default: '' steps: + - bash: | + if ! command -v just >/dev/null 2>&1 ; then + cargo install --locked just + fi + displayName: anvil setup (install just) + - pwsh: | - & just _anvil-resolve-stable apply-file-options - if ($LASTEXITCODE -ne 0) { - throw "anvil: stable toolchain option setup failed with exit code $LASTEXITCODE" - } - $toolchain = (& just _anvil-resolve-stable | Out-String).Trim() - if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($toolchain)) { - throw 'anvil: failed to resolve stable toolchain' - } - Write-Host "anvil: selected stable toolchain '$toolchain'" - displayName: anvil setup (resolve stable toolchain) - - pwsh: | - $toolchain = (& just _anvil-resolve-stable for-environment | Out-String).Trim() - if ($LASTEXITCODE -ne 0) { - throw 'anvil: failed to select stable toolchain arguments' + $version = (& just _anvil-stable-rustc-version | Select-Object -Last 1) + if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($version)) { + throw 'anvil: failed to provision the selected stable toolchain' } - $toolchainArgs = if ([string]::IsNullOrWhiteSpace($toolchain)) { @() } else { @("+$toolchain") } - $version = (& rustc @toolchainArgs --version | Select-Object -Last 1).Split()[1] + $version = $version.Trim() Write-Host "##vso[task.setvariable variable=anvil_rustc_version]$version" - displayName: anvil setup (capture rustc version) + displayName: anvil setup (provision stable toolchain and capture version) - task: Cache@2 inputs: # Same key shape as the GitHub side: OS + arch + rustc version + @@ -56,12 +50,6 @@ steps: $(HOME)/.cargo/.crates2.json displayName: anvil setup (cache cargo home) - - bash: | - if ! command -v just >/dev/null 2>&1 ; then - cargo install --locked just - fi - displayName: anvil setup (install just) - # Hand everything else off to the catalog recipe. Idempotent. ADO # uses the default `install` backend (source builds) because # cargo-binstall has unresolved compliance issues for internal ADO diff --git a/crates/cargo-anvil/templates/anvil/container/Containerfile b/crates/cargo-anvil/templates/anvil/container/Containerfile index 43f3685ac..5709437ba 100644 --- a/crates/cargo-anvil/templates/anvil/container/Containerfile +++ b/crates/cargo-anvil/templates/anvil/container/Containerfile @@ -53,7 +53,6 @@ RUN --mount=type=cache,id=anvil-cargo-registry,target=/usr/local/cargo/registry --mount=type=cache,id=anvil-cargo-git,target=/usr/local/cargo/git \ --mount=type=cache,id=anvil-cargo-target,target=/tmp/anvil-target \ printf "anvil_runner := \"native\"\nimport 'justfiles/anvil/mod.just'\n" > Justfile \ - && just _anvil-resolve-stable install \ && CARGO_TARGET_DIR=/tmp/anvil-target just anvil-setup COPY .anvil/container/entrypoint.sh /usr/local/bin/anvil-container-entrypoint diff --git a/crates/cargo-anvil/templates/github/setup-action.yml b/crates/cargo-anvil/templates/github/setup-action.yml index cf3ad0863..2b4c47a5c 100644 --- a/crates/cargo-anvil/templates/github/setup-action.yml +++ b/crates/cargo-anvil/templates/github/setup-action.yml @@ -77,8 +77,9 @@ runs: } "Free after cleanup (GiB): $(Get-FreeDiskGiB)" - # The resolver is a standard Just recipe, so bootstrap Just before any - # selected-toolchain setup. cargo-binstall keeps this cold path fast. + # Stable provisioning is part of the generated Just setup graph, so + # bootstrap Just before asking it for the selected compiler version. + # cargo-binstall keeps this cold path fast. - name: Install cargo-binstall uses: cargo-bins/cargo-binstall@v1.21.0 # immutable release, the tag cannot be moved @@ -89,27 +90,15 @@ runs: cargo binstall --no-confirm --locked just || cargo install --locked just fi - - name: Resolve stable toolchain + - name: Provision stable toolchain and capture version + id: rustc-version shell: pwsh -NoProfile -Command ". '{0}'" run: | - & just _anvil-resolve-stable install - if ($LASTEXITCODE -ne 0) { - throw 'anvil: failed to install stable toolchain' + $version = (& just _anvil-stable-rustc-version | Select-Object -Last 1) + if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($version)) { + throw 'anvil: failed to provision the selected stable toolchain' } - $toolchain = (& just _anvil-resolve-stable | Out-String).Trim() - if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($toolchain)) { - throw 'anvil: failed to resolve stable toolchain' - } - Write-Host "anvil: selected stable toolchain '$toolchain'" - - id: rustc-version - shell: pwsh -NoProfile -Command ". '{0}'" - run: | - $toolchain = (& just _anvil-resolve-stable for-environment | Out-String).Trim() - if ($LASTEXITCODE -ne 0) { - throw 'anvil: failed to select stable toolchain arguments' - } - $toolchainArgs = if ([string]::IsNullOrWhiteSpace($toolchain)) { @() } else { @("+$toolchain") } - $version = (& rustc @toolchainArgs --version | Select-Object -Last 1).Split()[1] + $version = $version.Trim() "version=$version" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append - name: Restore cargo cache id: cargo-cache diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/bench.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/bench.just index 0158612e4..b26880a81 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/bench.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/bench.just @@ -22,7 +22,7 @@ anvil-bench: anvil-bench-validate-prereqs anvil-impact # Install prerequisites for the `anvil-bench` recipe. [group("anvil-setup")] -anvil-bench-setup installer="install": anvil-tool-rustc-validate-prereqs +anvil-bench-setup installer="install": anvil-toolchain-stable-install # Validate prerequisites for the `anvil-bench` recipe. [group("anvil-setup")] diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/doc-build.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/doc-build.just index c02ad20bc..a8edeef7c 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/doc-build.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/doc-build.just @@ -26,7 +26,7 @@ anvil-doc-build: anvil-doc-build-validate-prereqs anvil-impact # Install prerequisites for the `anvil-doc-build` recipe. [group("anvil-setup")] -anvil-doc-build-setup installer="install": anvil-tool-rustc-validate-prereqs +anvil-doc-build-setup installer="install": anvil-toolchain-stable-install # Validate prerequisites for the `anvil-doc-build` recipe. [group("anvil-setup")] diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/doc-test.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/doc-test.just index 97f959768..ca6ade8c9 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/doc-test.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/doc-test.just @@ -27,7 +27,7 @@ anvil-doc-test: anvil-doc-test-validate-prereqs anvil-impact # Install prerequisites for the `anvil-doc-test` recipe. [group("anvil-setup")] -anvil-doc-test-setup installer="install": anvil-tool-rustc-validate-prereqs +anvil-doc-test-setup installer="install": anvil-toolchain-stable-install # Validate prerequisites for the `anvil-doc-test` recipe. [group("anvil-setup")] diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/examples.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/examples.just index de3b11ef5..fe447b7fb 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/examples.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/examples.just @@ -19,7 +19,7 @@ anvil-examples: anvil-examples-validate-prereqs anvil-impact # Install prerequisites for the `anvil-examples` recipe. [group("anvil-setup")] -anvil-examples-setup installer="install": anvil-tool-rustc-validate-prereqs +anvil-examples-setup installer="install": anvil-toolchain-stable-install # Validate prerequisites for the `anvil-examples` recipe. [group("anvil-setup")] diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/loom.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/loom.just index 5ddd82605..845381bc8 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/loom.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/loom.just @@ -118,7 +118,7 @@ anvil-loom: anvil-loom-validate-prereqs anvil-impact # Install prerequisites for the `anvil-loom` recipe. [group("anvil-setup")] -anvil-loom-setup installer="install": anvil-tool-rustc-validate-prereqs +anvil-loom-setup installer="install": anvil-toolchain-stable-install # Validate prerequisites for the `anvil-loom` recipe. [group("anvil-setup")] diff --git a/crates/cargo-anvil/templates/justfiles/anvil/tools.just b/crates/cargo-anvil/templates/justfiles/anvil/tools.just index 41b9336f1..31f8537d2 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/tools.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/tools.just @@ -128,13 +128,12 @@ anvil-tool-cargo-spellcheck-source-deps-check: } # ============================================================================ -# rustc + pwsh validate-prereqs (no install -- system prereqs) +# rustc + pwsh validate-prereqs # ============================================================================ # -# rustc is installed via rustup (https://rustup.rs); pwsh is installed -# via the platform's package manager. Both are presumed present on any -# machine running anvil; the validate recipes just surface a friendly -# error if not. +# Stable rustc is provisioned by `anvil-toolchain-stable-install` below. +# pwsh is installed via the platform's package manager. The validate recipes +# are read-only and only surface a friendly error if a prerequisite is absent. # Validate that rustc is available. [group("anvil-setup")] @@ -490,7 +489,32 @@ _anvil-resolve-stable action="resolve": $selection.Value } -# _install-tool: install a cargo subcommand at the catalog version, +# Provision the selected stable toolchain and replay any applicable +# toolchain-file options. This is the shared prerequisite for setup paths that +# execute stable Cargo/Rust or install Cargo tools; Just deduplicates it within +# each setup invocation. +[group("anvil-setup")] +anvil-toolchain-stable-install: (_anvil-resolve-stable "install") + +# Report the selected stable rustc version for cloud cache keys. Provisioning +# is a dependency so workflow surfaces invoke one setup-aware operation rather +# than orchestrating the resolver directly. +[private] +[script("pwsh", "-NoProfile")] +_anvil-stable-rustc-version: anvil-toolchain-stable-install + $versionLine = (& rustc {{_anvil_stable_toolchain_args}} --version | Select-Object -Last 1) + if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($versionLine)) { + Write-Error 'anvil: failed to read the selected stable rustc version' + exit 1 + } + $parts = ([string] $versionLine).Split() + if ($parts.Count -lt 2) { + Write-Error "anvil: unexpected rustc version output: $versionLine" + exit 1 + } + Write-Output $parts[1] + +# _install-tool-core: install a cargo subcommand at the catalog version, # or no-op if it is already installed at or above that version. The # `installer` parameter selects between: # - "install" (cargo install --locked, pure-source). Default. @@ -500,7 +524,7 @@ _anvil-resolve-stable action="resolve": # `source_prereq`, when set, runs immediately before a source installation, # including a binstall fallback. [script("pwsh", "-NoProfile")] -_install-tool name version installer source_prereq="": +_install-tool-core name version installer source_prereq="": $ErrorActionPreference = 'Stop' $name = '{{name}}' $version = '{{version}}' @@ -566,6 +590,9 @@ _install-tool name version installer source_prereq="": exit $LASTEXITCODE } +# Provision stable before entering the Cargo tool installer. +_install-tool name version installer source_prereq="": anvil-toolchain-stable-install (_install-tool-core name version installer source_prereq) + # _check-tool: verify a cargo subcommand is installed at or above the # pinned version. Errors with an install hint on missing or too-old. [script("pwsh", "-NoProfile")] @@ -790,7 +817,7 @@ anvil-toolchain-nightly-external-types-validate-prereqs: (_check-toolchain rust_ # Install the pinned `clippy` component for the default toolchain. [group("anvil-setup")] -anvil-component-default-clippy-install: (_install-component "default" "clippy") +anvil-component-default-clippy-install: anvil-toolchain-stable-install (_install-component "default" "clippy") # Validate that the pinned `clippy` component is available for the default toolchain. [group("anvil-setup")] @@ -798,7 +825,7 @@ anvil-component-default-clippy-validate-prereqs: (_check-component "default" "cl # Install the pinned `rustfmt` component for the default toolchain. [group("anvil-setup")] -anvil-component-default-rustfmt-install: (_install-component "default" "rustfmt") +anvil-component-default-rustfmt-install: anvil-toolchain-stable-install (_install-component "default" "rustfmt") # Validate that the pinned `rustfmt` component is available for the default toolchain. [group("anvil-setup")] @@ -885,12 +912,12 @@ anvil-tool-cargo-audit-validate-prereqs: (_check-tool "cargo-audit" cargo_audit_ # Install the pinned `cargo-bolero` tool. [group("anvil-setup")] [script("pwsh", "-NoProfile")] -anvil-tool-cargo-bolero-install installer="install": +anvil-tool-cargo-bolero-install installer="install": anvil-toolchain-stable-install if (-not $IsLinux) { Write-Host 'anvil-tool-cargo-bolero-install: non-Linux host -- skipping (cargo-bolero/libfuzzer is Linux-only)' exit 0 } - & just _install-tool cargo-bolero {{cargo_bolero_version}} {{installer}} + & just _install-tool-core cargo-bolero {{cargo_bolero_version}} {{installer}} exit $LASTEXITCODE # Validate that the pinned `cargo-bolero` tool is available. @@ -1002,12 +1029,12 @@ anvil-tool-cargo-llvm-cov-validate-prereqs: (_check-tool "cargo-llvm-cov" cargo_ # Install the pinned `cargo-mutants` tool. [group("anvil-setup")] [script("pwsh", "-NoProfile")] -anvil-tool-cargo-mutants-install installer="install": +anvil-tool-cargo-mutants-install installer="install": anvil-toolchain-stable-install if ($IsWindows -and ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64' -or $env:PROCESSOR_ARCHITEW6432 -eq 'ARM64')) { Write-Host 'anvil-tool-cargo-mutants-install: aarch64-pc-windows-msvc -- skipping (winapi build incompat)' exit 0 } - & just _install-tool cargo-mutants {{cargo_mutants_version}} {{installer}} + & just _install-tool-core cargo-mutants {{cargo_mutants_version}} {{installer}} exit $LASTEXITCODE # Validate that the pinned `cargo-mutants` tool is available. diff --git a/crates/cargo-anvil/templates/justfiles/anvil/versions.just b/crates/cargo-anvil/templates/justfiles/anvil/versions.just index ae8e9bda1..e4d7ef5a7 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/versions.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/versions.just @@ -31,24 +31,26 @@ # Rustup toolchains # ============================================================================ -# Stable commands interpolate this lazy PowerShell argument-array expression -# directly at each command site. `RUSTUP_TOOLCHAIN` is an input override only; -# Anvil never exports a resolved value globally into unrelated recipes or -# helpers. +# Stable commands interpolate this PowerShell array expression directly +# at each command site. Selection runs inside the existing PowerShell recipe +# process, so it needs no host-shell quoting or selector subprocess. +# `RUSTUP_TOOLCHAIN` is an input override only; Anvil never exports a resolved +# value globally into unrelated recipes or helpers. [private] -_anvil_stable_toolchain_selection_script := ''' +_anvil_stable_toolchain_args := trim(''' +@(& { $RepoRoot = (Get-Location).Path $ErrorActionPreference = 'Stop' Set-StrictMode -Version 3 -function Write-ExplicitToolchainArgs([string] $Toolchain) { - Write-Output ("@('+" + $Toolchain.Replace("'", "''") + "')") +function Write-ExplicitToolchainArg([string] $Toolchain) { + Write-Output ('+' + $Toolchain) } if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { - Write-ExplicitToolchainArgs $env:RUSTUP_TOOLCHAIN - exit 0 + Write-ExplicitToolchainArg $env:RUSTUP_TOOLCHAIN + return } $toolchainFile = $null @@ -88,8 +90,7 @@ if ($null -ne $toolchainFile) { } if ($selectingFile) { - Write-Output '@()' - exit 0 + return } if (-not [string]::IsNullOrWhiteSpace($env:ANVIL_MSRV_TOOLCHAIN)) { @@ -121,12 +122,9 @@ $msrv = if ($values.ContainsKey('workspace.package')) { } else { throw 'anvil: no selecting rust-toolchain(.toml) and no root [workspace.package] or [package] rust-version; declare an MSRV or select a toolchain explicitly' } -Write-ExplicitToolchainArgs $msrv -exit 0 -''' - -[private] -_anvil_stable_toolchain_args := if os_family() == "windows" { shell("& ([scriptblock]::Create('" + replace(_anvil_stable_toolchain_selection_script, "'", "''") + "'));") } else { shell("pwsh -NoProfile -Command '& ([scriptblock]::Create($args[0]))' " + quote(_anvil_stable_toolchain_selection_script)) } +Write-ExplicitToolchainArg $msrv +}) +''') # General nightly used by udeps, miri, careful, and any future # nightly-dependent check. Bumped on a regular cadence (monthly is a diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap index eaf98382e..2631249a6 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap @@ -58,7 +58,6 @@ RUN --mount=type=cache,id=anvil-cargo-registry,target=/usr/local/cargo/registry --mount=type=cache,id=anvil-cargo-git,target=/usr/local/cargo/git \ --mount=type=cache,id=anvil-cargo-target,target=/tmp/anvil-target \ printf "anvil_runner := \"native\"\nimport 'justfiles/anvil/mod.just'\n" > Justfile \ - && just _anvil-resolve-stable install \ && CARGO_TARGET_DIR=/tmp/anvil-target just anvil-setup COPY .anvil/container/entrypoint.sh /usr/local/bin/anvil-container-entrypoint @@ -2467,26 +2466,20 @@ parameters: type: string default: '' steps: + - bash: | + if ! command -v just >/dev/null 2>&1 ; then + cargo install --locked just + fi + displayName: anvil setup (install just) + - pwsh: | - & just _anvil-resolve-stable apply-file-options - if ($LASTEXITCODE -ne 0) { - throw "anvil: stable toolchain option setup failed with exit code $LASTEXITCODE" - } - $toolchain = (& just _anvil-resolve-stable | Out-String).Trim() - if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($toolchain)) { - throw 'anvil: failed to resolve stable toolchain' - } - Write-Host "anvil: selected stable toolchain '$toolchain'" - displayName: anvil setup (resolve stable toolchain) - - pwsh: | - $toolchain = (& just _anvil-resolve-stable for-environment | Out-String).Trim() - if ($LASTEXITCODE -ne 0) { - throw 'anvil: failed to select stable toolchain arguments' + $version = (& just _anvil-stable-rustc-version | Select-Object -Last 1) + if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($version)) { + throw 'anvil: failed to provision the selected stable toolchain' } - $toolchainArgs = if ([string]::IsNullOrWhiteSpace($toolchain)) { @() } else { @("+$toolchain") } - $version = (& rustc @toolchainArgs --version | Select-Object -Last 1).Split()[1] + $version = $version.Trim() Write-Host "##vso[task.setvariable variable=anvil_rustc_version]$version" - displayName: anvil setup (capture rustc version) + displayName: anvil setup (provision stable toolchain and capture version) - task: Cache@2 inputs: # Same key shape as the GitHub side: OS + arch + rustc version + @@ -2505,12 +2498,6 @@ steps: $(HOME)/.cargo/.crates2.json displayName: anvil setup (cache cargo home) - - bash: | - if ! command -v just >/dev/null 2>&1 ; then - cargo install --locked just - fi - displayName: anvil setup (install just) - # Hand everything else off to the catalog recipe. Idempotent. ADO # uses the default `install` backend (source builds) because # cargo-binstall has unresolved compliance issues for internal ADO @@ -2879,7 +2866,7 @@ anvil-bench: anvil-bench-validate-prereqs anvil-impact # Install prerequisites for the `anvil-bench` recipe. [group("anvil-setup")] -anvil-bench-setup installer="install": anvil-tool-rustc-validate-prereqs +anvil-bench-setup installer="install": anvil-toolchain-stable-install # Validate prerequisites for the `anvil-bench` recipe. [group("anvil-setup")] @@ -3246,7 +3233,7 @@ anvil-doc-build: anvil-doc-build-validate-prereqs anvil-impact # Install prerequisites for the `anvil-doc-build` recipe. [group("anvil-setup")] -anvil-doc-build-setup installer="install": anvil-tool-rustc-validate-prereqs +anvil-doc-build-setup installer="install": anvil-toolchain-stable-install # Validate prerequisites for the `anvil-doc-build` recipe. [group("anvil-setup")] @@ -3282,7 +3269,7 @@ anvil-doc-test: anvil-doc-test-validate-prereqs anvil-impact # Install prerequisites for the `anvil-doc-test` recipe. [group("anvil-setup")] -anvil-doc-test-setup installer="install": anvil-tool-rustc-validate-prereqs +anvil-doc-test-setup installer="install": anvil-toolchain-stable-install # Validate prerequisites for the `anvil-doc-test` recipe. [group("anvil-setup")] @@ -3366,7 +3353,7 @@ anvil-examples: anvil-examples-validate-prereqs anvil-impact # Install prerequisites for the `anvil-examples` recipe. [group("anvil-setup")] -anvil-examples-setup installer="install": anvil-tool-rustc-validate-prereqs +anvil-examples-setup installer="install": anvil-toolchain-stable-install # Validate prerequisites for the `anvil-examples` recipe. [group("anvil-setup")] @@ -3850,7 +3837,7 @@ anvil-loom: anvil-loom-validate-prereqs anvil-impact # Install prerequisites for the `anvil-loom` recipe. [group("anvil-setup")] -anvil-loom-setup installer="install": anvil-tool-rustc-validate-prereqs +anvil-loom-setup installer="install": anvil-toolchain-stable-install # Validate prerequisites for the `anvil-loom` recipe. [group("anvil-setup")] @@ -6329,13 +6316,12 @@ anvil-tool-cargo-spellcheck-source-deps-check: } # ============================================================================ -# rustc + pwsh validate-prereqs (no install -- system prereqs) +# rustc + pwsh validate-prereqs # ============================================================================ # -# rustc is installed via rustup (https://rustup.rs); pwsh is installed -# via the platform's package manager. Both are presumed present on any -# machine running anvil; the validate recipes just surface a friendly -# error if not. +# Stable rustc is provisioned by `anvil-toolchain-stable-install` below. +# pwsh is installed via the platform's package manager. The validate recipes +# are read-only and only surface a friendly error if a prerequisite is absent. # Validate that rustc is available. [group("anvil-setup")] @@ -6691,7 +6677,32 @@ _anvil-resolve-stable action="resolve": $selection.Value } -# _install-tool: install a cargo subcommand at the catalog version, +# Provision the selected stable toolchain and replay any applicable +# toolchain-file options. This is the shared prerequisite for setup paths that +# execute stable Cargo/Rust or install Cargo tools; Just deduplicates it within +# each setup invocation. +[group("anvil-setup")] +anvil-toolchain-stable-install: (_anvil-resolve-stable "install") + +# Report the selected stable rustc version for cloud cache keys. Provisioning +# is a dependency so workflow surfaces invoke one setup-aware operation rather +# than orchestrating the resolver directly. +[private] +[script("pwsh", "-NoProfile")] +_anvil-stable-rustc-version: anvil-toolchain-stable-install + $versionLine = (& rustc {{_anvil_stable_toolchain_args}} --version | Select-Object -Last 1) + if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($versionLine)) { + Write-Error 'anvil: failed to read the selected stable rustc version' + exit 1 + } + $parts = ([string] $versionLine).Split() + if ($parts.Count -lt 2) { + Write-Error "anvil: unexpected rustc version output: $versionLine" + exit 1 + } + Write-Output $parts[1] + +# _install-tool-core: install a cargo subcommand at the catalog version, # or no-op if it is already installed at or above that version. The # `installer` parameter selects between: # - "install" (cargo install --locked, pure-source). Default. @@ -6701,7 +6712,7 @@ _anvil-resolve-stable action="resolve": # `source_prereq`, when set, runs immediately before a source installation, # including a binstall fallback. [script("pwsh", "-NoProfile")] -_install-tool name version installer source_prereq="": +_install-tool-core name version installer source_prereq="": $ErrorActionPreference = 'Stop' $name = '{{name}}' $version = '{{version}}' @@ -6767,6 +6778,9 @@ _install-tool name version installer source_prereq="": exit $LASTEXITCODE } +# Provision stable before entering the Cargo tool installer. +_install-tool name version installer source_prereq="": anvil-toolchain-stable-install (_install-tool-core name version installer source_prereq) + # _check-tool: verify a cargo subcommand is installed at or above the # pinned version. Errors with an install hint on missing or too-old. [script("pwsh", "-NoProfile")] @@ -6991,7 +7005,7 @@ anvil-toolchain-nightly-external-types-validate-prereqs: (_check-toolchain rust_ # Install the pinned `clippy` component for the default toolchain. [group("anvil-setup")] -anvil-component-default-clippy-install: (_install-component "default" "clippy") +anvil-component-default-clippy-install: anvil-toolchain-stable-install (_install-component "default" "clippy") # Validate that the pinned `clippy` component is available for the default toolchain. [group("anvil-setup")] @@ -6999,7 +7013,7 @@ anvil-component-default-clippy-validate-prereqs: (_check-component "default" "cl # Install the pinned `rustfmt` component for the default toolchain. [group("anvil-setup")] -anvil-component-default-rustfmt-install: (_install-component "default" "rustfmt") +anvil-component-default-rustfmt-install: anvil-toolchain-stable-install (_install-component "default" "rustfmt") # Validate that the pinned `rustfmt` component is available for the default toolchain. [group("anvil-setup")] @@ -7086,12 +7100,12 @@ anvil-tool-cargo-audit-validate-prereqs: (_check-tool "cargo-audit" cargo_audit_ # Install the pinned `cargo-bolero` tool. [group("anvil-setup")] [script("pwsh", "-NoProfile")] -anvil-tool-cargo-bolero-install installer="install": +anvil-tool-cargo-bolero-install installer="install": anvil-toolchain-stable-install if (-not $IsLinux) { Write-Host 'anvil-tool-cargo-bolero-install: non-Linux host -- skipping (cargo-bolero/libfuzzer is Linux-only)' exit 0 } - & just _install-tool cargo-bolero {{cargo_bolero_version}} {{installer}} + & just _install-tool-core cargo-bolero {{cargo_bolero_version}} {{installer}} exit $LASTEXITCODE # Validate that the pinned `cargo-bolero` tool is available. @@ -7203,12 +7217,12 @@ anvil-tool-cargo-llvm-cov-validate-prereqs: (_check-tool "cargo-llvm-cov" cargo_ # Install the pinned `cargo-mutants` tool. [group("anvil-setup")] [script("pwsh", "-NoProfile")] -anvil-tool-cargo-mutants-install installer="install": +anvil-tool-cargo-mutants-install installer="install": anvil-toolchain-stable-install if ($IsWindows -and ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64' -or $env:PROCESSOR_ARCHITEW6432 -eq 'ARM64')) { Write-Host 'anvil-tool-cargo-mutants-install: aarch64-pc-windows-msvc -- skipping (winapi build incompat)' exit 0 } - & just _install-tool cargo-mutants {{cargo_mutants_version}} {{installer}} + & just _install-tool-core cargo-mutants {{cargo_mutants_version}} {{installer}} exit $LASTEXITCODE # Validate that the pinned `cargo-mutants` tool is available. @@ -7271,7 +7285,6 @@ anvil-tool-cargo-udeps-validate-prereqs: (_check-tool "cargo-udeps" cargo_udeps_ # # Pinned versions used by the anvil check tree. -set lazy # # Everything anvil installs (cargo subcommands + rustup toolchains) # is pinned here. The pinning policy: @@ -7297,24 +7310,26 @@ set lazy # Rustup toolchains # ============================================================================ -# Stable commands interpolate this lazy PowerShell argument-array expression -# directly at each command site. `RUSTUP_TOOLCHAIN` is an input override only; -# Anvil never exports a resolved value globally into unrelated recipes or -# helpers. +# Stable commands interpolate this PowerShell array expression directly +# at each command site. Selection runs inside the existing PowerShell recipe +# process, so it needs no host-shell quoting or selector subprocess. +# `RUSTUP_TOOLCHAIN` is an input override only; Anvil never exports a resolved +# value globally into unrelated recipes or helpers. [private] -_anvil_stable_toolchain_selection_script := ''' -param([string] $RepoRoot) +_anvil_stable_toolchain_args := trim(''' +@(& { +$RepoRoot = (Get-Location).Path $ErrorActionPreference = 'Stop' Set-StrictMode -Version 3 -function Write-ExplicitToolchainArgs([string] $Toolchain) { - Write-Output ("@('+" + $Toolchain.Replace("'", "''") + "')") +function Write-ExplicitToolchainArg([string] $Toolchain) { + Write-Output ('+' + $Toolchain) } if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { - Write-ExplicitToolchainArgs $env:RUSTUP_TOOLCHAIN - exit 0 + Write-ExplicitToolchainArg $env:RUSTUP_TOOLCHAIN + return } $toolchainFile = $null @@ -7354,8 +7369,7 @@ if ($null -ne $toolchainFile) { } if ($selectingFile) { - Write-Output '@()' - exit 0 + return } if (-not [string]::IsNullOrWhiteSpace($env:ANVIL_MSRV_TOOLCHAIN)) { @@ -7387,12 +7401,9 @@ $msrv = if ($values.ContainsKey('workspace.package')) { } else { throw 'anvil: no selecting rust-toolchain(.toml) and no root [workspace.package] or [package] rust-version; declare an MSRV or select a toolchain explicitly' } -Write-ExplicitToolchainArgs $msrv -exit 0 -''' - -[private] -_anvil_stable_toolchain_args := if os_family() == "windows" { shell("& ([scriptblock]::Create('" + replace(_anvil_stable_toolchain_selection_script, "'", "''") + "')) '" + replace(justfile_directory(), "'", "''") + "';") } else { shell("pwsh -NoProfile -Command '& ([scriptblock]::Create($args[0])) $args[1]' " + quote(_anvil_stable_toolchain_selection_script) + " " + quote(justfile_directory())) } +Write-ExplicitToolchainArg $msrv +}) +''') # General nightly used by udeps, miri, careful, and any future # nightly-dependent check. Bumped on a regular cadence (monthly is a diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap index 79f61e721..24bc62a51 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap @@ -58,7 +58,6 @@ RUN --mount=type=cache,id=anvil-cargo-registry,target=/usr/local/cargo/registry --mount=type=cache,id=anvil-cargo-git,target=/usr/local/cargo/git \ --mount=type=cache,id=anvil-cargo-target,target=/tmp/anvil-target \ printf "anvil_runner := \"native\"\nimport 'justfiles/anvil/mod.just'\n" > Justfile \ - && just _anvil-resolve-stable install \ && CARGO_TARGET_DIR=/tmp/anvil-target just anvil-setup COPY .anvil/container/entrypoint.sh /usr/local/bin/anvil-container-entrypoint @@ -1603,8 +1602,9 @@ runs: } "Free after cleanup (GiB): $(Get-FreeDiskGiB)" - # The resolver is a standard Just recipe, so bootstrap Just before any - # selected-toolchain setup. cargo-binstall keeps this cold path fast. + # Stable provisioning is part of the generated Just setup graph, so + # bootstrap Just before asking it for the selected compiler version. + # cargo-binstall keeps this cold path fast. - name: Install cargo-binstall uses: cargo-bins/cargo-binstall@v1.21.0 # immutable release, the tag cannot be moved @@ -1615,27 +1615,15 @@ runs: cargo binstall --no-confirm --locked just || cargo install --locked just fi - - name: Resolve stable toolchain + - name: Provision stable toolchain and capture version + id: rustc-version shell: pwsh -NoProfile -Command ". '{0}'" run: | - & just _anvil-resolve-stable install - if ($LASTEXITCODE -ne 0) { - throw 'anvil: failed to install stable toolchain' - } - $toolchain = (& just _anvil-resolve-stable | Out-String).Trim() - if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($toolchain)) { - throw 'anvil: failed to resolve stable toolchain' - } - Write-Host "anvil: selected stable toolchain '$toolchain'" - - id: rustc-version - shell: pwsh -NoProfile -Command ". '{0}'" - run: | - $toolchain = (& just _anvil-resolve-stable for-environment | Out-String).Trim() - if ($LASTEXITCODE -ne 0) { - throw 'anvil: failed to select stable toolchain arguments' + $version = (& just _anvil-stable-rustc-version | Select-Object -Last 1) + if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($version)) { + throw 'anvil: failed to provision the selected stable toolchain' } - $toolchainArgs = if ([string]::IsNullOrWhiteSpace($toolchain)) { @() } else { @("+$toolchain") } - $version = (& rustc @toolchainArgs --version | Select-Object -Last 1).Split()[1] + $version = $version.Trim() "version=$version" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append - name: Restore cargo cache id: cargo-cache @@ -2679,7 +2667,7 @@ anvil-bench: anvil-bench-validate-prereqs anvil-impact # Install prerequisites for the `anvil-bench` recipe. [group("anvil-setup")] -anvil-bench-setup installer="install": anvil-tool-rustc-validate-prereqs +anvil-bench-setup installer="install": anvil-toolchain-stable-install # Validate prerequisites for the `anvil-bench` recipe. [group("anvil-setup")] @@ -3046,7 +3034,7 @@ anvil-doc-build: anvil-doc-build-validate-prereqs anvil-impact # Install prerequisites for the `anvil-doc-build` recipe. [group("anvil-setup")] -anvil-doc-build-setup installer="install": anvil-tool-rustc-validate-prereqs +anvil-doc-build-setup installer="install": anvil-toolchain-stable-install # Validate prerequisites for the `anvil-doc-build` recipe. [group("anvil-setup")] @@ -3082,7 +3070,7 @@ anvil-doc-test: anvil-doc-test-validate-prereqs anvil-impact # Install prerequisites for the `anvil-doc-test` recipe. [group("anvil-setup")] -anvil-doc-test-setup installer="install": anvil-tool-rustc-validate-prereqs +anvil-doc-test-setup installer="install": anvil-toolchain-stable-install # Validate prerequisites for the `anvil-doc-test` recipe. [group("anvil-setup")] @@ -3166,7 +3154,7 @@ anvil-examples: anvil-examples-validate-prereqs anvil-impact # Install prerequisites for the `anvil-examples` recipe. [group("anvil-setup")] -anvil-examples-setup installer="install": anvil-tool-rustc-validate-prereqs +anvil-examples-setup installer="install": anvil-toolchain-stable-install # Validate prerequisites for the `anvil-examples` recipe. [group("anvil-setup")] @@ -3650,7 +3638,7 @@ anvil-loom: anvil-loom-validate-prereqs anvil-impact # Install prerequisites for the `anvil-loom` recipe. [group("anvil-setup")] -anvil-loom-setup installer="install": anvil-tool-rustc-validate-prereqs +anvil-loom-setup installer="install": anvil-toolchain-stable-install # Validate prerequisites for the `anvil-loom` recipe. [group("anvil-setup")] @@ -6129,13 +6117,12 @@ anvil-tool-cargo-spellcheck-source-deps-check: } # ============================================================================ -# rustc + pwsh validate-prereqs (no install -- system prereqs) +# rustc + pwsh validate-prereqs # ============================================================================ # -# rustc is installed via rustup (https://rustup.rs); pwsh is installed -# via the platform's package manager. Both are presumed present on any -# machine running anvil; the validate recipes just surface a friendly -# error if not. +# Stable rustc is provisioned by `anvil-toolchain-stable-install` below. +# pwsh is installed via the platform's package manager. The validate recipes +# are read-only and only surface a friendly error if a prerequisite is absent. # Validate that rustc is available. [group("anvil-setup")] @@ -6491,7 +6478,32 @@ _anvil-resolve-stable action="resolve": $selection.Value } -# _install-tool: install a cargo subcommand at the catalog version, +# Provision the selected stable toolchain and replay any applicable +# toolchain-file options. This is the shared prerequisite for setup paths that +# execute stable Cargo/Rust or install Cargo tools; Just deduplicates it within +# each setup invocation. +[group("anvil-setup")] +anvil-toolchain-stable-install: (_anvil-resolve-stable "install") + +# Report the selected stable rustc version for cloud cache keys. Provisioning +# is a dependency so workflow surfaces invoke one setup-aware operation rather +# than orchestrating the resolver directly. +[private] +[script("pwsh", "-NoProfile")] +_anvil-stable-rustc-version: anvil-toolchain-stable-install + $versionLine = (& rustc {{_anvil_stable_toolchain_args}} --version | Select-Object -Last 1) + if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($versionLine)) { + Write-Error 'anvil: failed to read the selected stable rustc version' + exit 1 + } + $parts = ([string] $versionLine).Split() + if ($parts.Count -lt 2) { + Write-Error "anvil: unexpected rustc version output: $versionLine" + exit 1 + } + Write-Output $parts[1] + +# _install-tool-core: install a cargo subcommand at the catalog version, # or no-op if it is already installed at or above that version. The # `installer` parameter selects between: # - "install" (cargo install --locked, pure-source). Default. @@ -6501,7 +6513,7 @@ _anvil-resolve-stable action="resolve": # `source_prereq`, when set, runs immediately before a source installation, # including a binstall fallback. [script("pwsh", "-NoProfile")] -_install-tool name version installer source_prereq="": +_install-tool-core name version installer source_prereq="": $ErrorActionPreference = 'Stop' $name = '{{name}}' $version = '{{version}}' @@ -6567,6 +6579,9 @@ _install-tool name version installer source_prereq="": exit $LASTEXITCODE } +# Provision stable before entering the Cargo tool installer. +_install-tool name version installer source_prereq="": anvil-toolchain-stable-install (_install-tool-core name version installer source_prereq) + # _check-tool: verify a cargo subcommand is installed at or above the # pinned version. Errors with an install hint on missing or too-old. [script("pwsh", "-NoProfile")] @@ -6791,7 +6806,7 @@ anvil-toolchain-nightly-external-types-validate-prereqs: (_check-toolchain rust_ # Install the pinned `clippy` component for the default toolchain. [group("anvil-setup")] -anvil-component-default-clippy-install: (_install-component "default" "clippy") +anvil-component-default-clippy-install: anvil-toolchain-stable-install (_install-component "default" "clippy") # Validate that the pinned `clippy` component is available for the default toolchain. [group("anvil-setup")] @@ -6799,7 +6814,7 @@ anvil-component-default-clippy-validate-prereqs: (_check-component "default" "cl # Install the pinned `rustfmt` component for the default toolchain. [group("anvil-setup")] -anvil-component-default-rustfmt-install: (_install-component "default" "rustfmt") +anvil-component-default-rustfmt-install: anvil-toolchain-stable-install (_install-component "default" "rustfmt") # Validate that the pinned `rustfmt` component is available for the default toolchain. [group("anvil-setup")] @@ -6886,12 +6901,12 @@ anvil-tool-cargo-audit-validate-prereqs: (_check-tool "cargo-audit" cargo_audit_ # Install the pinned `cargo-bolero` tool. [group("anvil-setup")] [script("pwsh", "-NoProfile")] -anvil-tool-cargo-bolero-install installer="install": +anvil-tool-cargo-bolero-install installer="install": anvil-toolchain-stable-install if (-not $IsLinux) { Write-Host 'anvil-tool-cargo-bolero-install: non-Linux host -- skipping (cargo-bolero/libfuzzer is Linux-only)' exit 0 } - & just _install-tool cargo-bolero {{cargo_bolero_version}} {{installer}} + & just _install-tool-core cargo-bolero {{cargo_bolero_version}} {{installer}} exit $LASTEXITCODE # Validate that the pinned `cargo-bolero` tool is available. @@ -7003,12 +7018,12 @@ anvil-tool-cargo-llvm-cov-validate-prereqs: (_check-tool "cargo-llvm-cov" cargo_ # Install the pinned `cargo-mutants` tool. [group("anvil-setup")] [script("pwsh", "-NoProfile")] -anvil-tool-cargo-mutants-install installer="install": +anvil-tool-cargo-mutants-install installer="install": anvil-toolchain-stable-install if ($IsWindows -and ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64' -or $env:PROCESSOR_ARCHITEW6432 -eq 'ARM64')) { Write-Host 'anvil-tool-cargo-mutants-install: aarch64-pc-windows-msvc -- skipping (winapi build incompat)' exit 0 } - & just _install-tool cargo-mutants {{cargo_mutants_version}} {{installer}} + & just _install-tool-core cargo-mutants {{cargo_mutants_version}} {{installer}} exit $LASTEXITCODE # Validate that the pinned `cargo-mutants` tool is available. @@ -7071,7 +7086,6 @@ anvil-tool-cargo-udeps-validate-prereqs: (_check-tool "cargo-udeps" cargo_udeps_ # # Pinned versions used by the anvil check tree. -set lazy # # Everything anvil installs (cargo subcommands + rustup toolchains) # is pinned here. The pinning policy: @@ -7097,24 +7111,26 @@ set lazy # Rustup toolchains # ============================================================================ -# Stable commands interpolate this lazy PowerShell argument-array expression -# directly at each command site. `RUSTUP_TOOLCHAIN` is an input override only; -# Anvil never exports a resolved value globally into unrelated recipes or -# helpers. +# Stable commands interpolate this PowerShell array expression directly +# at each command site. Selection runs inside the existing PowerShell recipe +# process, so it needs no host-shell quoting or selector subprocess. +# `RUSTUP_TOOLCHAIN` is an input override only; Anvil never exports a resolved +# value globally into unrelated recipes or helpers. [private] -_anvil_stable_toolchain_selection_script := ''' -param([string] $RepoRoot) +_anvil_stable_toolchain_args := trim(''' +@(& { +$RepoRoot = (Get-Location).Path $ErrorActionPreference = 'Stop' Set-StrictMode -Version 3 -function Write-ExplicitToolchainArgs([string] $Toolchain) { - Write-Output ("@('+" + $Toolchain.Replace("'", "''") + "')") +function Write-ExplicitToolchainArg([string] $Toolchain) { + Write-Output ('+' + $Toolchain) } if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { - Write-ExplicitToolchainArgs $env:RUSTUP_TOOLCHAIN - exit 0 + Write-ExplicitToolchainArg $env:RUSTUP_TOOLCHAIN + return } $toolchainFile = $null @@ -7154,8 +7170,7 @@ if ($null -ne $toolchainFile) { } if ($selectingFile) { - Write-Output '@()' - exit 0 + return } if (-not [string]::IsNullOrWhiteSpace($env:ANVIL_MSRV_TOOLCHAIN)) { @@ -7187,12 +7202,9 @@ $msrv = if ($values.ContainsKey('workspace.package')) { } else { throw 'anvil: no selecting rust-toolchain(.toml) and no root [workspace.package] or [package] rust-version; declare an MSRV or select a toolchain explicitly' } -Write-ExplicitToolchainArgs $msrv -exit 0 -''' - -[private] -_anvil_stable_toolchain_args := if os_family() == "windows" { shell("& ([scriptblock]::Create('" + replace(_anvil_stable_toolchain_selection_script, "'", "''") + "')) '" + replace(justfile_directory(), "'", "''") + "';") } else { shell("pwsh -NoProfile -Command '& ([scriptblock]::Create($args[0])) $args[1]' " + quote(_anvil_stable_toolchain_selection_script) + " " + quote(justfile_directory())) } +Write-ExplicitToolchainArg $msrv +}) +''') # General nightly used by udeps, miri, careful, and any future # nightly-dependent check. Bumped on a regular cadence (monthly is a diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap index f1ef48813..da4cf1800 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap @@ -58,7 +58,6 @@ RUN --mount=type=cache,id=anvil-cargo-registry,target=/usr/local/cargo/registry --mount=type=cache,id=anvil-cargo-git,target=/usr/local/cargo/git \ --mount=type=cache,id=anvil-cargo-target,target=/tmp/anvil-target \ printf "anvil_runner := \"native\"\nimport 'justfiles/anvil/mod.just'\n" > Justfile \ - && just _anvil-resolve-stable install \ && CARGO_TARGET_DIR=/tmp/anvil-target just anvil-setup COPY .anvil/container/entrypoint.sh /usr/local/bin/anvil-container-entrypoint @@ -1524,7 +1523,7 @@ anvil-bench: anvil-bench-validate-prereqs anvil-impact # Install prerequisites for the `anvil-bench` recipe. [group("anvil-setup")] -anvil-bench-setup installer="install": anvil-tool-rustc-validate-prereqs +anvil-bench-setup installer="install": anvil-toolchain-stable-install # Validate prerequisites for the `anvil-bench` recipe. [group("anvil-setup")] @@ -1891,7 +1890,7 @@ anvil-doc-build: anvil-doc-build-validate-prereqs anvil-impact # Install prerequisites for the `anvil-doc-build` recipe. [group("anvil-setup")] -anvil-doc-build-setup installer="install": anvil-tool-rustc-validate-prereqs +anvil-doc-build-setup installer="install": anvil-toolchain-stable-install # Validate prerequisites for the `anvil-doc-build` recipe. [group("anvil-setup")] @@ -1927,7 +1926,7 @@ anvil-doc-test: anvil-doc-test-validate-prereqs anvil-impact # Install prerequisites for the `anvil-doc-test` recipe. [group("anvil-setup")] -anvil-doc-test-setup installer="install": anvil-tool-rustc-validate-prereqs +anvil-doc-test-setup installer="install": anvil-toolchain-stable-install # Validate prerequisites for the `anvil-doc-test` recipe. [group("anvil-setup")] @@ -2011,7 +2010,7 @@ anvil-examples: anvil-examples-validate-prereqs anvil-impact # Install prerequisites for the `anvil-examples` recipe. [group("anvil-setup")] -anvil-examples-setup installer="install": anvil-tool-rustc-validate-prereqs +anvil-examples-setup installer="install": anvil-toolchain-stable-install # Validate prerequisites for the `anvil-examples` recipe. [group("anvil-setup")] @@ -2495,7 +2494,7 @@ anvil-loom: anvil-loom-validate-prereqs anvil-impact # Install prerequisites for the `anvil-loom` recipe. [group("anvil-setup")] -anvil-loom-setup installer="install": anvil-tool-rustc-validate-prereqs +anvil-loom-setup installer="install": anvil-toolchain-stable-install # Validate prerequisites for the `anvil-loom` recipe. [group("anvil-setup")] @@ -4974,13 +4973,12 @@ anvil-tool-cargo-spellcheck-source-deps-check: } # ============================================================================ -# rustc + pwsh validate-prereqs (no install -- system prereqs) +# rustc + pwsh validate-prereqs # ============================================================================ # -# rustc is installed via rustup (https://rustup.rs); pwsh is installed -# via the platform's package manager. Both are presumed present on any -# machine running anvil; the validate recipes just surface a friendly -# error if not. +# Stable rustc is provisioned by `anvil-toolchain-stable-install` below. +# pwsh is installed via the platform's package manager. The validate recipes +# are read-only and only surface a friendly error if a prerequisite is absent. # Validate that rustc is available. [group("anvil-setup")] @@ -5336,7 +5334,32 @@ _anvil-resolve-stable action="resolve": $selection.Value } -# _install-tool: install a cargo subcommand at the catalog version, +# Provision the selected stable toolchain and replay any applicable +# toolchain-file options. This is the shared prerequisite for setup paths that +# execute stable Cargo/Rust or install Cargo tools; Just deduplicates it within +# each setup invocation. +[group("anvil-setup")] +anvil-toolchain-stable-install: (_anvil-resolve-stable "install") + +# Report the selected stable rustc version for cloud cache keys. Provisioning +# is a dependency so workflow surfaces invoke one setup-aware operation rather +# than orchestrating the resolver directly. +[private] +[script("pwsh", "-NoProfile")] +_anvil-stable-rustc-version: anvil-toolchain-stable-install + $versionLine = (& rustc {{_anvil_stable_toolchain_args}} --version | Select-Object -Last 1) + if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($versionLine)) { + Write-Error 'anvil: failed to read the selected stable rustc version' + exit 1 + } + $parts = ([string] $versionLine).Split() + if ($parts.Count -lt 2) { + Write-Error "anvil: unexpected rustc version output: $versionLine" + exit 1 + } + Write-Output $parts[1] + +# _install-tool-core: install a cargo subcommand at the catalog version, # or no-op if it is already installed at or above that version. The # `installer` parameter selects between: # - "install" (cargo install --locked, pure-source). Default. @@ -5346,7 +5369,7 @@ _anvil-resolve-stable action="resolve": # `source_prereq`, when set, runs immediately before a source installation, # including a binstall fallback. [script("pwsh", "-NoProfile")] -_install-tool name version installer source_prereq="": +_install-tool-core name version installer source_prereq="": $ErrorActionPreference = 'Stop' $name = '{{name}}' $version = '{{version}}' @@ -5412,6 +5435,9 @@ _install-tool name version installer source_prereq="": exit $LASTEXITCODE } +# Provision stable before entering the Cargo tool installer. +_install-tool name version installer source_prereq="": anvil-toolchain-stable-install (_install-tool-core name version installer source_prereq) + # _check-tool: verify a cargo subcommand is installed at or above the # pinned version. Errors with an install hint on missing or too-old. [script("pwsh", "-NoProfile")] @@ -5636,7 +5662,7 @@ anvil-toolchain-nightly-external-types-validate-prereqs: (_check-toolchain rust_ # Install the pinned `clippy` component for the default toolchain. [group("anvil-setup")] -anvil-component-default-clippy-install: (_install-component "default" "clippy") +anvil-component-default-clippy-install: anvil-toolchain-stable-install (_install-component "default" "clippy") # Validate that the pinned `clippy` component is available for the default toolchain. [group("anvil-setup")] @@ -5644,7 +5670,7 @@ anvil-component-default-clippy-validate-prereqs: (_check-component "default" "cl # Install the pinned `rustfmt` component for the default toolchain. [group("anvil-setup")] -anvil-component-default-rustfmt-install: (_install-component "default" "rustfmt") +anvil-component-default-rustfmt-install: anvil-toolchain-stable-install (_install-component "default" "rustfmt") # Validate that the pinned `rustfmt` component is available for the default toolchain. [group("anvil-setup")] @@ -5731,12 +5757,12 @@ anvil-tool-cargo-audit-validate-prereqs: (_check-tool "cargo-audit" cargo_audit_ # Install the pinned `cargo-bolero` tool. [group("anvil-setup")] [script("pwsh", "-NoProfile")] -anvil-tool-cargo-bolero-install installer="install": +anvil-tool-cargo-bolero-install installer="install": anvil-toolchain-stable-install if (-not $IsLinux) { Write-Host 'anvil-tool-cargo-bolero-install: non-Linux host -- skipping (cargo-bolero/libfuzzer is Linux-only)' exit 0 } - & just _install-tool cargo-bolero {{cargo_bolero_version}} {{installer}} + & just _install-tool-core cargo-bolero {{cargo_bolero_version}} {{installer}} exit $LASTEXITCODE # Validate that the pinned `cargo-bolero` tool is available. @@ -5848,12 +5874,12 @@ anvil-tool-cargo-llvm-cov-validate-prereqs: (_check-tool "cargo-llvm-cov" cargo_ # Install the pinned `cargo-mutants` tool. [group("anvil-setup")] [script("pwsh", "-NoProfile")] -anvil-tool-cargo-mutants-install installer="install": +anvil-tool-cargo-mutants-install installer="install": anvil-toolchain-stable-install if ($IsWindows -and ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64' -or $env:PROCESSOR_ARCHITEW6432 -eq 'ARM64')) { Write-Host 'anvil-tool-cargo-mutants-install: aarch64-pc-windows-msvc -- skipping (winapi build incompat)' exit 0 } - & just _install-tool cargo-mutants {{cargo_mutants_version}} {{installer}} + & just _install-tool-core cargo-mutants {{cargo_mutants_version}} {{installer}} exit $LASTEXITCODE # Validate that the pinned `cargo-mutants` tool is available. @@ -5916,7 +5942,6 @@ anvil-tool-cargo-udeps-validate-prereqs: (_check-tool "cargo-udeps" cargo_udeps_ # # Pinned versions used by the anvil check tree. -set lazy # # Everything anvil installs (cargo subcommands + rustup toolchains) # is pinned here. The pinning policy: @@ -5942,24 +5967,26 @@ set lazy # Rustup toolchains # ============================================================================ -# Stable commands interpolate this lazy PowerShell argument-array expression -# directly at each command site. `RUSTUP_TOOLCHAIN` is an input override only; -# Anvil never exports a resolved value globally into unrelated recipes or -# helpers. +# Stable commands interpolate this PowerShell array expression directly +# at each command site. Selection runs inside the existing PowerShell recipe +# process, so it needs no host-shell quoting or selector subprocess. +# `RUSTUP_TOOLCHAIN` is an input override only; Anvil never exports a resolved +# value globally into unrelated recipes or helpers. [private] -_anvil_stable_toolchain_selection_script := ''' -param([string] $RepoRoot) +_anvil_stable_toolchain_args := trim(''' +@(& { +$RepoRoot = (Get-Location).Path $ErrorActionPreference = 'Stop' Set-StrictMode -Version 3 -function Write-ExplicitToolchainArgs([string] $Toolchain) { - Write-Output ("@('+" + $Toolchain.Replace("'", "''") + "')") +function Write-ExplicitToolchainArg([string] $Toolchain) { + Write-Output ('+' + $Toolchain) } if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { - Write-ExplicitToolchainArgs $env:RUSTUP_TOOLCHAIN - exit 0 + Write-ExplicitToolchainArg $env:RUSTUP_TOOLCHAIN + return } $toolchainFile = $null @@ -5999,8 +6026,7 @@ if ($null -ne $toolchainFile) { } if ($selectingFile) { - Write-Output '@()' - exit 0 + return } if (-not [string]::IsNullOrWhiteSpace($env:ANVIL_MSRV_TOOLCHAIN)) { @@ -6032,12 +6058,9 @@ $msrv = if ($values.ContainsKey('workspace.package')) { } else { throw 'anvil: no selecting rust-toolchain(.toml) and no root [workspace.package] or [package] rust-version; declare an MSRV or select a toolchain explicitly' } -Write-ExplicitToolchainArgs $msrv -exit 0 -''' - -[private] -_anvil_stable_toolchain_args := if os_family() == "windows" { shell("& ([scriptblock]::Create('" + replace(_anvil_stable_toolchain_selection_script, "'", "''") + "')) '" + replace(justfile_directory(), "'", "''") + "';") } else { shell("pwsh -NoProfile -Command '& ([scriptblock]::Create($args[0])) $args[1]' " + quote(_anvil_stable_toolchain_selection_script) + " " + quote(justfile_directory())) } +Write-ExplicitToolchainArg $msrv +}) +''') # General nightly used by udeps, miri, careful, and any future # nightly-dependent check. Bumped on a regular cadence (monthly is a diff --git a/justfiles/anvil/checks/bench.just b/justfiles/anvil/checks/bench.just index 0158612e4..b26880a81 100644 --- a/justfiles/anvil/checks/bench.just +++ b/justfiles/anvil/checks/bench.just @@ -22,7 +22,7 @@ anvil-bench: anvil-bench-validate-prereqs anvil-impact # Install prerequisites for the `anvil-bench` recipe. [group("anvil-setup")] -anvil-bench-setup installer="install": anvil-tool-rustc-validate-prereqs +anvil-bench-setup installer="install": anvil-toolchain-stable-install # Validate prerequisites for the `anvil-bench` recipe. [group("anvil-setup")] diff --git a/justfiles/anvil/checks/doc-build.just b/justfiles/anvil/checks/doc-build.just index c02ad20bc..a8edeef7c 100644 --- a/justfiles/anvil/checks/doc-build.just +++ b/justfiles/anvil/checks/doc-build.just @@ -26,7 +26,7 @@ anvil-doc-build: anvil-doc-build-validate-prereqs anvil-impact # Install prerequisites for the `anvil-doc-build` recipe. [group("anvil-setup")] -anvil-doc-build-setup installer="install": anvil-tool-rustc-validate-prereqs +anvil-doc-build-setup installer="install": anvil-toolchain-stable-install # Validate prerequisites for the `anvil-doc-build` recipe. [group("anvil-setup")] diff --git a/justfiles/anvil/checks/doc-test.just b/justfiles/anvil/checks/doc-test.just index 97f959768..ca6ade8c9 100644 --- a/justfiles/anvil/checks/doc-test.just +++ b/justfiles/anvil/checks/doc-test.just @@ -27,7 +27,7 @@ anvil-doc-test: anvil-doc-test-validate-prereqs anvil-impact # Install prerequisites for the `anvil-doc-test` recipe. [group("anvil-setup")] -anvil-doc-test-setup installer="install": anvil-tool-rustc-validate-prereqs +anvil-doc-test-setup installer="install": anvil-toolchain-stable-install # Validate prerequisites for the `anvil-doc-test` recipe. [group("anvil-setup")] diff --git a/justfiles/anvil/checks/examples.just b/justfiles/anvil/checks/examples.just index de3b11ef5..fe447b7fb 100644 --- a/justfiles/anvil/checks/examples.just +++ b/justfiles/anvil/checks/examples.just @@ -19,7 +19,7 @@ anvil-examples: anvil-examples-validate-prereqs anvil-impact # Install prerequisites for the `anvil-examples` recipe. [group("anvil-setup")] -anvil-examples-setup installer="install": anvil-tool-rustc-validate-prereqs +anvil-examples-setup installer="install": anvil-toolchain-stable-install # Validate prerequisites for the `anvil-examples` recipe. [group("anvil-setup")] diff --git a/justfiles/anvil/checks/loom.just b/justfiles/anvil/checks/loom.just index 5ddd82605..845381bc8 100644 --- a/justfiles/anvil/checks/loom.just +++ b/justfiles/anvil/checks/loom.just @@ -118,7 +118,7 @@ anvil-loom: anvil-loom-validate-prereqs anvil-impact # Install prerequisites for the `anvil-loom` recipe. [group("anvil-setup")] -anvil-loom-setup installer="install": anvil-tool-rustc-validate-prereqs +anvil-loom-setup installer="install": anvil-toolchain-stable-install # Validate prerequisites for the `anvil-loom` recipe. [group("anvil-setup")] diff --git a/justfiles/anvil/tools.just b/justfiles/anvil/tools.just index 41b9336f1..31f8537d2 100644 --- a/justfiles/anvil/tools.just +++ b/justfiles/anvil/tools.just @@ -128,13 +128,12 @@ anvil-tool-cargo-spellcheck-source-deps-check: } # ============================================================================ -# rustc + pwsh validate-prereqs (no install -- system prereqs) +# rustc + pwsh validate-prereqs # ============================================================================ # -# rustc is installed via rustup (https://rustup.rs); pwsh is installed -# via the platform's package manager. Both are presumed present on any -# machine running anvil; the validate recipes just surface a friendly -# error if not. +# Stable rustc is provisioned by `anvil-toolchain-stable-install` below. +# pwsh is installed via the platform's package manager. The validate recipes +# are read-only and only surface a friendly error if a prerequisite is absent. # Validate that rustc is available. [group("anvil-setup")] @@ -490,7 +489,32 @@ _anvil-resolve-stable action="resolve": $selection.Value } -# _install-tool: install a cargo subcommand at the catalog version, +# Provision the selected stable toolchain and replay any applicable +# toolchain-file options. This is the shared prerequisite for setup paths that +# execute stable Cargo/Rust or install Cargo tools; Just deduplicates it within +# each setup invocation. +[group("anvil-setup")] +anvil-toolchain-stable-install: (_anvil-resolve-stable "install") + +# Report the selected stable rustc version for cloud cache keys. Provisioning +# is a dependency so workflow surfaces invoke one setup-aware operation rather +# than orchestrating the resolver directly. +[private] +[script("pwsh", "-NoProfile")] +_anvil-stable-rustc-version: anvil-toolchain-stable-install + $versionLine = (& rustc {{_anvil_stable_toolchain_args}} --version | Select-Object -Last 1) + if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($versionLine)) { + Write-Error 'anvil: failed to read the selected stable rustc version' + exit 1 + } + $parts = ([string] $versionLine).Split() + if ($parts.Count -lt 2) { + Write-Error "anvil: unexpected rustc version output: $versionLine" + exit 1 + } + Write-Output $parts[1] + +# _install-tool-core: install a cargo subcommand at the catalog version, # or no-op if it is already installed at or above that version. The # `installer` parameter selects between: # - "install" (cargo install --locked, pure-source). Default. @@ -500,7 +524,7 @@ _anvil-resolve-stable action="resolve": # `source_prereq`, when set, runs immediately before a source installation, # including a binstall fallback. [script("pwsh", "-NoProfile")] -_install-tool name version installer source_prereq="": +_install-tool-core name version installer source_prereq="": $ErrorActionPreference = 'Stop' $name = '{{name}}' $version = '{{version}}' @@ -566,6 +590,9 @@ _install-tool name version installer source_prereq="": exit $LASTEXITCODE } +# Provision stable before entering the Cargo tool installer. +_install-tool name version installer source_prereq="": anvil-toolchain-stable-install (_install-tool-core name version installer source_prereq) + # _check-tool: verify a cargo subcommand is installed at or above the # pinned version. Errors with an install hint on missing or too-old. [script("pwsh", "-NoProfile")] @@ -790,7 +817,7 @@ anvil-toolchain-nightly-external-types-validate-prereqs: (_check-toolchain rust_ # Install the pinned `clippy` component for the default toolchain. [group("anvil-setup")] -anvil-component-default-clippy-install: (_install-component "default" "clippy") +anvil-component-default-clippy-install: anvil-toolchain-stable-install (_install-component "default" "clippy") # Validate that the pinned `clippy` component is available for the default toolchain. [group("anvil-setup")] @@ -798,7 +825,7 @@ anvil-component-default-clippy-validate-prereqs: (_check-component "default" "cl # Install the pinned `rustfmt` component for the default toolchain. [group("anvil-setup")] -anvil-component-default-rustfmt-install: (_install-component "default" "rustfmt") +anvil-component-default-rustfmt-install: anvil-toolchain-stable-install (_install-component "default" "rustfmt") # Validate that the pinned `rustfmt` component is available for the default toolchain. [group("anvil-setup")] @@ -885,12 +912,12 @@ anvil-tool-cargo-audit-validate-prereqs: (_check-tool "cargo-audit" cargo_audit_ # Install the pinned `cargo-bolero` tool. [group("anvil-setup")] [script("pwsh", "-NoProfile")] -anvil-tool-cargo-bolero-install installer="install": +anvil-tool-cargo-bolero-install installer="install": anvil-toolchain-stable-install if (-not $IsLinux) { Write-Host 'anvil-tool-cargo-bolero-install: non-Linux host -- skipping (cargo-bolero/libfuzzer is Linux-only)' exit 0 } - & just _install-tool cargo-bolero {{cargo_bolero_version}} {{installer}} + & just _install-tool-core cargo-bolero {{cargo_bolero_version}} {{installer}} exit $LASTEXITCODE # Validate that the pinned `cargo-bolero` tool is available. @@ -1002,12 +1029,12 @@ anvil-tool-cargo-llvm-cov-validate-prereqs: (_check-tool "cargo-llvm-cov" cargo_ # Install the pinned `cargo-mutants` tool. [group("anvil-setup")] [script("pwsh", "-NoProfile")] -anvil-tool-cargo-mutants-install installer="install": +anvil-tool-cargo-mutants-install installer="install": anvil-toolchain-stable-install if ($IsWindows -and ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64' -or $env:PROCESSOR_ARCHITEW6432 -eq 'ARM64')) { Write-Host 'anvil-tool-cargo-mutants-install: aarch64-pc-windows-msvc -- skipping (winapi build incompat)' exit 0 } - & just _install-tool cargo-mutants {{cargo_mutants_version}} {{installer}} + & just _install-tool-core cargo-mutants {{cargo_mutants_version}} {{installer}} exit $LASTEXITCODE # Validate that the pinned `cargo-mutants` tool is available. diff --git a/justfiles/anvil/versions.just b/justfiles/anvil/versions.just index ae8e9bda1..e4d7ef5a7 100644 --- a/justfiles/anvil/versions.just +++ b/justfiles/anvil/versions.just @@ -31,24 +31,26 @@ # Rustup toolchains # ============================================================================ -# Stable commands interpolate this lazy PowerShell argument-array expression -# directly at each command site. `RUSTUP_TOOLCHAIN` is an input override only; -# Anvil never exports a resolved value globally into unrelated recipes or -# helpers. +# Stable commands interpolate this PowerShell array expression directly +# at each command site. Selection runs inside the existing PowerShell recipe +# process, so it needs no host-shell quoting or selector subprocess. +# `RUSTUP_TOOLCHAIN` is an input override only; Anvil never exports a resolved +# value globally into unrelated recipes or helpers. [private] -_anvil_stable_toolchain_selection_script := ''' +_anvil_stable_toolchain_args := trim(''' +@(& { $RepoRoot = (Get-Location).Path $ErrorActionPreference = 'Stop' Set-StrictMode -Version 3 -function Write-ExplicitToolchainArgs([string] $Toolchain) { - Write-Output ("@('+" + $Toolchain.Replace("'", "''") + "')") +function Write-ExplicitToolchainArg([string] $Toolchain) { + Write-Output ('+' + $Toolchain) } if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { - Write-ExplicitToolchainArgs $env:RUSTUP_TOOLCHAIN - exit 0 + Write-ExplicitToolchainArg $env:RUSTUP_TOOLCHAIN + return } $toolchainFile = $null @@ -88,8 +90,7 @@ if ($null -ne $toolchainFile) { } if ($selectingFile) { - Write-Output '@()' - exit 0 + return } if (-not [string]::IsNullOrWhiteSpace($env:ANVIL_MSRV_TOOLCHAIN)) { @@ -121,12 +122,9 @@ $msrv = if ($values.ContainsKey('workspace.package')) { } else { throw 'anvil: no selecting rust-toolchain(.toml) and no root [workspace.package] or [package] rust-version; declare an MSRV or select a toolchain explicitly' } -Write-ExplicitToolchainArgs $msrv -exit 0 -''' - -[private] -_anvil_stable_toolchain_args := if os_family() == "windows" { shell("& ([scriptblock]::Create('" + replace(_anvil_stable_toolchain_selection_script, "'", "''") + "'));") } else { shell("pwsh -NoProfile -Command '& ([scriptblock]::Create($args[0]))' " + quote(_anvil_stable_toolchain_selection_script)) } +Write-ExplicitToolchainArg $msrv +}) +''') # General nightly used by udeps, miri, careful, and any future # nightly-dependent check. Bumped on a regular cadence (monthly is a From e7c4bd3de0af7c0140f58bf53774ec9c67343195 Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Fri, 28 Aug 2026 19:40:23 +0200 Subject: [PATCH 21/32] Simplify deterministic toolchain resolution Delegate repository toolchain files to rustup, keep command-scoped explicit selection, and validate workspace MSRVs with the selected compiler. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: f7996e6f-5c20-4d7a-b2f7-690af95e557e --- .anvil.lock | 6 +- crates/cargo-anvil/docs/design/checks.md | 10 +- crates/cargo-anvil/docs/design/local.md | 77 ++-- .../src/anvil/artifacts/justfile.rs | 346 +++++++++++------ .../templates/justfiles/anvil/tools.just | 287 +++++--------- .../templates/justfiles/anvil/versions.just | 73 ++-- crates/cargo-anvil/tests/impact.rs | 11 +- .../snapshots/snapshots__ado_backend.snap | 360 ++++++------------ .../snapshots/snapshots__github_backend.snap | 360 ++++++------------ .../snapshots/snapshots__local_only.snap | 360 ++++++------------ justfiles/anvil/tools.just | 287 +++++--------- justfiles/anvil/versions.just | 73 ++-- 12 files changed, 909 insertions(+), 1341 deletions(-) diff --git a/.anvil.lock b/.anvil.lock index 2c0dfb63d..11e2ef920 100644 --- a/.anvil.lock +++ b/.anvil.lock @@ -1,7 +1,7 @@ version = 1 tool = "anvil" tool_version = "0.5.0" -catalog_checksum = "sha256:f446b3ec3e2958410d61ce294759410e2e01ff6f651b71ee168e2d2d8e9bf48b" +catalog_checksum = "sha256:bea05c54e887000f8461f5bdd8f86375fc94cd481942b6c7b8ab2674d64164c6" [[file]] path = ".anvil/container/Containerfile" @@ -261,11 +261,11 @@ checksum = "sha256:5b2d91569f3fe87cd5f516623a01b5c7ae855ad587870ef9a2ae6d5619dd9 [[file]] path = "justfiles/anvil/tools.just" -checksum = "sha256:601762b80b52946fa00bdd7471de6ddfd26217f1a6f2c4ff324b13c7a751c8e3" +checksum = "sha256:a7ce0dfe39a65b44aa3a7d605f8819673012600b16822ae29c90e95a3b7e708a" [[file]] path = "justfiles/anvil/versions.just" -checksum = "sha256:ac2c5b1c1766003e1840ebf1fb092c6995c8f1804129620ce580d178e59ed7be" +checksum = "sha256:2e6f224497a5b4ea68ab096bafc65622faf9c846c5c8caf7ea68c76e72a791c1" [[region]] host = ".delta.toml" diff --git a/crates/cargo-anvil/docs/design/checks.md b/crates/cargo-anvil/docs/design/checks.md index d33b70816..61bed0901 100644 --- a/crates/cargo-anvil/docs/design/checks.md +++ b/crates/cargo-anvil/docs/design/checks.md @@ -166,10 +166,12 @@ that provided the strongest version of the check. Invocations shown without a pinned nightly or MSRV use the selected stable compiler. Their generated PowerShell recipes invoke Cargo directly with the lazy optional stable argument: an explicit `+toolchain` for a caller override -or root MSRV, and no argument when a repository toolchain file selects the -compiler. The shared inline PowerShell array expression evaluates in that same -recipe process without a host-OS branch or selector subprocess. They do not -route the main command through a nested Just recipe. +or root MSRV, or the concrete toolchain that rustup resolves from a repository +toolchain file. The shared inline PowerShell array expression evaluates in that +same recipe process without a host-OS branch. Its repository root is fixed by +`justfile_directory()`, so checks that enter crate directories retain the same +selection as setup. They do not route the main command through a nested Just +recipe. Their paired setup recipes reach `anvil-toolchain-stable-install` directly or through the shared Cargo-tool/default-component installer before any stable command runs. Paired `*-validate-prereqs` recipes remain read-only and never diff --git a/crates/cargo-anvil/docs/design/local.md b/crates/cargo-anvil/docs/design/local.md index ab0452f73..638a54373 100644 --- a/crates/cargo-anvil/docs/design/local.md +++ b/crates/cargo-anvil/docs/design/local.md @@ -261,7 +261,9 @@ install recipe (one per atomic resource); composition layers chain those. - `anvil-toolchain--install` — `rustup toolchain install` for a pinned nightly (e.g. `nightly-2026-02-10`). - `anvil-component---install` — `rustup component add` - on a specific toolchain. Depends on the matching toolchain-install recipe. + on a specific toolchain. Stable components explicitly target the toolchain + selected by an override, repository toolchain file, or MSRV fallback. Depends + on the matching toolchain-install recipe. Each has a matching `*-validate-prereqs` recipe that exits 0 when the resource is already present at or above its pin and fails with a one-line install hint otherwise. @@ -389,8 +391,8 @@ ambient stable toolchain. The selection order is: 1. `RUSTUP_TOOLCHAIN`, when the caller already set it. This is the standard rustup override and lets internal pipelines select an installed `ms-prod-*` toolchain. -2. The `channel` or `path` selected by a root `rust-toolchain` or - `rust-toolchain.toml`. The legacy file name wins when both exist, matching rustup. +2. A root `rust-toolchain` or `rust-toolchain.toml`, delegated to rustup without + Anvil parsing or modifying the file. 3. The root package or `[workspace.package]` `rust-version`, treated as the repository MSRV. @@ -404,8 +406,8 @@ floor should declare root `[workspace.package].rust-version` (or package `rust-version`) and have every workspace member inherit it or declare an equal or lower minimum. A repository with missing package MSRVs, or a member minimum newer than the root, must instead correct the root floor or add a root toolchain -file with `channel` or `path` to select the single compiler used by catalog -checks. Internal builds may set `RUSTUP_TOOLCHAIN` to a provisioned toolchain +file to select the single compiler used by catalog checks. Internal builds may +set `RUSTUP_TOOLCHAIN` to a provisioned toolchain rather than adding a public compiler pin. Setting `ANVIL_MSRV_TOOLCHAIN` without that stable override (and without a selecting repository toolchain file) is rejected as incomplete internal configuration. An empty root @@ -415,38 +417,35 @@ toolchain file is invalid and produces an actionable error. `_anvil_stable_toolchain_args` value. Its value is a multiline PowerShell array expression, not a command for Just's expression-time `shell()` function. Just interpolates the expression into an existing PowerShell recipe, where it -evaluates to no elements for a selecting repository toolchain file or one -`+toolchain` string for a caller override or MSRV fallback. Toolchain names, -including apostrophes, therefore remain runtime string data rather than source -text that needs escaping or reparsing. `RUSTUP_TOOLCHAIN` is an input to -selection, never a globally exported output. +evaluates to one `+toolchain` string. For a repository toolchain file, it asks +`rustup show active-toolchain` from the repository root and uses rustup's +resolved concrete toolchain. For an override or MSRV fallback, it uses that +value directly. Toolchain names, including apostrophes, therefore remain +runtime string data rather than source text that needs escaping or reparsing. +`RUSTUP_TOOLCHAIN` is an input to selection, never a globally exported output. +The expression embeds an escaped PowerShell literal produced from +`justfile_directory()`, matching the setup resolver's repository root. +Recipe-local `Set-Location` calls and Just's `--working-directory` option +therefore cannot redirect selection to a nested manifest. Each stable recipe executes Cargo or Rust directly in its current PowerShell recipe process, for example -`& cargo {{_anvil_stable_toolchain_args}} clippy ...`. A selecting toolchain -file therefore reaches native rustup unoverridden, including its components, -targets, profile, and path semantics. Explicit caller/MSRV selections are -visible as Cargo's `+toolchain` argument. Nightly and MSRV-specific checks keep -their own explicit `+toolchain` arguments. No nested Just wrapper sits on the -main Cargo/Rust command path. Selection starts no subprocess, has no -OS-specific branch or host-shell quoting, and does not change the adopter's +`& cargo {{_anvil_stable_toolchain_args}} clippy ...`. Repository toolchain-file +provisioning runs `rustc` from the repository root, so rustup natively owns the +file's channel, path, profile, components, and targets. Default Clippy and +rustfmt setup then passes `--toolchain` for the resolved concrete toolchain, so +the component is installed on the compiler their checks probe. Nightly and +MSRV-specific checks keep their own explicit `+toolchain` arguments. No nested +Just wrapper sits on the main Cargo/Rust command path. Selection has no +OS-specific branch or host-shell quoting and does not change the adopter's configured shell. Unrelated recipes and raw commands retain the caller's normal environment. -An environment override or MSRV fallback normally causes rustup to ignore the -repository toolchain file. Anvil therefore reads `profile`, `components`, and -`targets` from a root `[toolchain]` table and applies those options to the concrete -selected toolchain. A profile controls a public toolchain installation performed -by Anvil; externally provisioned toolchains retain their provisioner's profile. -Explicit components and targets are added at setup time. If an option is unavailable -for the selected compiler, Anvil warns and skips it, matching rustup's handling of -unavailable options read directly from a toolchain file. - The public setup primitive `anvil-toolchain-stable-install` owns stable -provisioning. It delegates parsing, public installation, toolchain-file option -replay, and internal mapping to the private `_anvil-resolve-stable` -implementation. Leaf setup recipes reach the primitive directly or through the -shared Cargo-tool/default-component installers; group, tier, and +provisioning. Rustup processes repository toolchain files directly; the private +`_anvil-resolve-stable` implementation handles public MSRV installation and +internal mapping. Leaf setup recipes reach the primitive directly or through +the shared Cargo-tool/default-component installers; group, tier, and `anvil-setup` recipes inherit and deduplicate that dependency. Cloud version capture uses the private `_anvil-stable-rustc-version` recipe, whose dependency provisions stable before it invokes `rustc` directly with the lazy argument. @@ -454,13 +453,17 @@ Workflows and container construction therefore invoke setup operations rather than orchestrating `_anvil-resolve-stable` actions. Neither helper wraps a Cargo/Rust command that performs a check, and no standalone script is involved. -When selection falls back to the root MSRV, prerequisite validation reads -`cargo metadata` and requires every workspace package to resolve a -`rust_version` no newer than the root floor. Lower member minima are valid -because the root compiler satisfies them. Workspaces with missing package -MSRVs or member minima above the root must correct the declarations or choose a -single catalog toolchain explicitly with a selecting toolchain file. Anvil does -not build a per-package toolchain matrix for this uncommon case. +When selection falls back to the root MSRV, Anvil reads the root manifest just +far enough to bootstrap that compiler, then prerequisite validation reads +`cargo + metadata`. Cargo performs the authoritative workspace +resolution; Anvil requires every workspace package to expose a `rust_version` +no newer than the root floor. Running metadata under that compiler avoids +depending on an ambient default during the fallback itself. +Lower member minima are valid because the root compiler satisfies them. +Workspaces with missing package MSRVs or member minima above the root must +correct the declarations or choose a single catalog toolchain explicitly with +a selecting toolchain file. Anvil does not build a per-package toolchain matrix +for this uncommon case. When the root manifest declares an MSRV, `anvil-msrv-test` runs affected-package unit and integration tests under that compiler with all features and with default diff --git a/crates/cargo-anvil/src/anvil/artifacts/justfile.rs b/crates/cargo-anvil/src/anvil/artifacts/justfile.rs index e416bc0e1..e15769547 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/justfile.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/justfile.rs @@ -741,16 +741,21 @@ mod tests { #[test] fn stable_toolchain_selection_is_scoped_to_each_command() { assert!(!VERSIONS_JUST.contains("export RUSTUP_TOOLCHAIN")); - assert!(VERSIONS_JUST.contains("_anvil_stable_toolchain_args := trim('''\n@(& {")); - assert!(VERSIONS_JUST.contains("Write-Output ('+' + $Toolchain)")); + assert!( + VERSIONS_JUST + .contains("_anvil_stable_toolchain_args := trim(\"@(& {\\n$RepoRoot = '\" + replace(justfile_directory(), \"'\", \"''\")") + ); + assert!(VERSIONS_JUST.contains("Write-Output ('+' + $env:RUSTUP_TOOLCHAIN)")); + assert!(!VERSIONS_JUST.contains("(Get-Location).Path")); assert!(!VERSIONS_JUST.contains("os_family()")); assert!(!VERSIONS_JUST.contains("shell(")); assert!(!VERSIONS_JUST.contains("[scriptblock]::Create")); assert!(TOOLS_JUST.contains("_anvil-resolve-stable action=\"resolve\":")); assert!(!TOOLS_JUST.contains("_anvil-with-stable")); assert!(TOOLS_JUST.contains("& cargo {{_anvil_stable_toolchain_args}}")); + assert!(VERSIONS_JUST.contains("rustup show active-toolchain")); assert!(TOOLS_JUST.contains("rustup component add --toolchain")); - assert!(TOOLS_JUST.contains("rustup target add --toolchain")); + assert!(!TOOLS_JUST.contains("rustup target add --toolchain")); } #[test] @@ -870,12 +875,20 @@ mod tests { "[script(\"pwsh\", \"-NoProfile\")]\n", "_anvil-test-stable-command:\n", " & cargo {{_anvil_stable_toolchain_args}} --version\n", + "\n[script(\"pwsh\", \"-NoProfile\")]\n", + "_anvil-test-stable-command-after-chdir:\n", + " Set-Location 'nested'\n", + " & cargo {{_anvil_stable_toolchain_args}} --version\n", ), ) .expect("Justfile fixture must be writable"); temp } + fn tools_available() -> bool { + Command::new("just").arg("--version").output().is_ok() && Command::new("pwsh").arg("--version").output().is_ok() + } + fn command(root: &Path) -> Command { let mut command = Command::new("just"); command @@ -899,9 +912,6 @@ mod tests { ["-InstallIfMissing"] => { command.arg("anvil-toolchain-stable-install"); } - ["-ApplyToolchainFileOptions"] => { - command.args(["_anvil-resolve-stable", "apply-file-options"]); - } ["-ValidateWorkspaceMsrv"] => { command.args(["_anvil-resolve-stable", "validate-workspace-msrv"]); } @@ -911,9 +921,6 @@ mod tests { ["-InstallMsrvIfNeeded"] => { command.args(["_anvil-resolve-stable", "install-msrv"]); } - ["-EmitToolchainFileOptions"] => { - command.args(["_anvil-resolve-stable", "emit-file-options"]); - } other => panic!("unsupported resolver arguments in test: {other:?}"), } match rustup_toolchain { @@ -955,29 +962,33 @@ mod tests { #[test] fn honors_environment_files_and_msrv_in_precedence_order() { + if !tools_available() { + return; + } let temp = fixture("[workspace.package]\nrust-version = \"1.93\"\n"); let root = temp.path(); assert_eq!(resolved(root, None), "@('+1.93')"); - fs::write(root.join("rust-toolchain.toml"), "[toolchain]\ncomponents = [\"clippy\"]\n") - .expect("toolchain fixture must be writable"); - assert_eq!(resolved(root, None), "@('+1.93')"); - - fs::write(root.join("rust-toolchain.toml"), "[toolchain]\nchannel = \"1.94\"\n").expect("toolchain fixture must be writable"); - assert_eq!(resolved(root, None), "@()"); - - fs::write(root.join("rust-toolchain"), "1.92\n").expect("legacy toolchain fixture must be writable"); - assert_eq!(resolved(root, None), "@()"); assert_eq!(resolved(root, Some("custom-toolchain")), "@('+custom-toolchain')"); assert_eq!(resolved(root, Some("team's-toolchain")), "@('+team''s-toolchain')"); } #[test] fn passes_selected_arguments_directly_to_cargo() { + if !tools_available() { + return; + } let temp = fixture("[workspace.package]\nrust-version = \"1.93\"\n"); let shim = TempDir::new().expect("cargo shim directory must be creatable"); #[cfg(windows)] - fs::write(shim.path().join("cargo.cmd"), "@echo off\r\necho %*\r\nexit /b 0\r\n").expect("Cargo shim must be writable"); + { + fs::write(shim.path().join("cargo.cmd"), "@echo off\r\necho %*\r\nexit /b 0\r\n").expect("Cargo shim must be writable"); + fs::write( + shim.path().join("rustup.cmd"), + "@echo off\r\nif \"%1 %2\"==\"show active-toolchain\" echo C:\\toolchains\\test toolchain (overridden by fixture)\r\nexit /b 0\r\n", + ) + .expect("rustup shim must be writable"); + } #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; @@ -985,6 +996,13 @@ mod tests { let cargo = shim.path().join("cargo"); fs::write(&cargo, "#!/bin/sh\nprintf '%s\\n' \"$*\"\nexit 0\n").expect("Cargo shim must be writable"); fs::set_permissions(&cargo, fs::Permissions::from_mode(0o755)).expect("Cargo shim must be executable"); + let rustup = shim.path().join("rustup"); + fs::write( + &rustup, + "#!/bin/sh\nif [ \"$*\" = 'show active-toolchain' ]; then echo '/toolchains/test toolchain (overridden by fixture)'; fi\nexit 0\n", + ) + .expect("rustup shim must be writable"); + fs::set_permissions(&rustup, fs::Permissions::from_mode(0o755)).expect("rustup shim must be executable"); } let mut paths = vec![shim.path().to_path_buf()]; paths.extend(env::split_paths(&env::var_os("PATH").unwrap_or_default())); @@ -1006,11 +1024,11 @@ mod tests { "+team's-toolchain --version" ); - fs::write(temp.path().join("rust-toolchain.toml"), "[toolchain]\nchannel = \"1.94\"\n") + fs::write(temp.path().join("rust-toolchain.toml"), "[toolchain]\ncomponents = [\"clippy\"]\n") .expect("toolchain fixture must be writable"); let output = command(temp.path()) .arg("_anvil-test-stable-command") - .env("PATH", path) + .env("PATH", &path) .output() .expect("repository-selected Cargo command must run"); assert!( @@ -1018,14 +1036,58 @@ mod tests { "repository-selected Cargo command failed: {}", normalized_diagnostic(&output) ); + let expected_file_toolchain = if cfg!(windows) { + "\"+C:\\toolchains\\test toolchain\" --version" + } else { + "+/toolchains/test toolchain --version" + }; assert_eq!( String::from_utf8(output.stdout).expect("Cargo shim output must be UTF-8").trim(), - "--version" + expected_file_toolchain + ); + + fs::remove_file(temp.path().join("rust-toolchain.toml")).expect("toolchain fixture must be removable"); + let nested = temp.path().join("nested"); + fs::create_dir(&nested).expect("nested working directory must be creatable"); + + let output = command(temp.path()) + .args(["--working-directory"]) + .arg(&nested) + .arg("_anvil-test-stable-command") + .env("PATH", &path) + .output() + .expect("stable Cargo command with an explicit working directory must run"); + assert!( + output.status.success(), + "working-directory stable Cargo command failed: {}", + normalized_diagnostic(&output) + ); + assert_eq!( + String::from_utf8(output.stdout).expect("Cargo shim output must be UTF-8").trim(), + "+1.93 --version" + ); + + let output = command(temp.path()) + .arg("_anvil-test-stable-command-after-chdir") + .env("PATH", path) + .output() + .expect("stable Cargo command after Set-Location must run"); + assert!( + output.status.success(), + "Set-Location stable Cargo command failed: {}", + normalized_diagnostic(&output) + ); + assert_eq!( + String::from_utf8(output.stdout).expect("Cargo shim output must be UTF-8").trim(), + "+1.93 --version" ); } #[test] fn rejects_empty_legacy_toolchain_files_with_an_actionable_error() { + if !tools_available() { + return; + } for contents in ["", " \r\n\t"] { let temp = fixture("[workspace.package]\nrust-version = \"1.93\"\n"); fs::write(temp.path().join("rust-toolchain"), contents).expect("toolchain fixture must be writable"); @@ -1034,99 +1096,116 @@ mod tests { assert!(!output.status.success(), "empty toolchain file must fail resolution"); let diagnostic = normalized_diagnostic(&output); assert!( - diagnostic.contains("root toolchain file") - && diagnostic.contains("is empty") - && diagnostic.contains("remove it") - && diagnostic.contains("declare a toolchain configuration"), + diagnostic.contains("rustup could not resolve") + && (diagnostic.contains("empty toolchain override") || diagnostic.contains("error parsing override file")), "unexpected resolver diagnostic: {diagnostic}" ); } } #[test] - fn reads_multiline_toolchain_file_install_options() { - let temp = fixture("[workspace.package]\nrust-version = \"1.93\"\n"); - fs::write( - temp.path().join("rust-toolchain.toml"), - concat!( - "[toolchain]\n", - "profile = \"minimal\"\n", - "components = [\n \"clippy\",\n \"rustfmt\",\n]\n", - "targets = [\n \"wasm32-unknown-unknown\",\n]\n", - ), - ) - .expect("toolchain fixture must be writable"); - - let output = run(temp.path(), &["-EmitToolchainFileOptions"], None); - assert!( - output.status.success(), - "toolchain option resolution failed: {}", - String::from_utf8_lossy(&output.stderr) - ); - let options = String::from_utf8(output.stdout).expect("resolver output must be UTF-8"); - for expected in [ - r#""Profile":"minimal""#, - r#""Components":["clippy","rustfmt"]"#, - r#""Targets":["wasm32-unknown-unknown"]"#, - ] { - assert!(options.contains(expected), "missing {expected} in {options}"); + fn installs_default_components_on_the_selected_stable_toolchain() { + if !tools_available() { + return; } - } - - #[test] - fn applies_toolchain_file_options_to_an_msrv_fallback() { let temp = fixture("[workspace.package]\nrust-version = \"1.93\"\n"); - fs::write( - temp.path().join("rust-toolchain.toml"), - concat!( - "[toolchain]\n", - "profile = \"complete\"\n", - "components = [\"clippy\"]\n", - "targets = [\"wasm32-unknown-unknown\"]\n", - ), - ) - .expect("toolchain fixture must be writable"); + let shim = TempDir::new().expect("component shim directory must be creatable"); + let cargo_log = shim.path().join("cargo.log"); + let rustup_log = shim.path().join("rustup.log"); + #[cfg(windows)] + { + fs::write( + shim.path().join("cargo.cmd"), + "@echo off\r\n>>\"%ANVIL_CARGO_LOG%\" echo %*\r\nexit /b 1\r\n", + ) + .expect("Cargo shim must be writable"); + fs::write( + shim.path().join("rustup.cmd"), + "@echo off\r\n>>\"%ANVIL_RUSTUP_LOG%\" echo %*\r\nif \"%1 %2\"==\"show active-toolchain\" echo 1.94-test-host (overridden by fixture)\r\nexit /b 0\r\n", + ) + .expect("rustup shim must be writable"); + } + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; - let shim = TempDir::new().expect("rustup shim directory must be creatable"); - let log = shim.path().join("rustup.log"); - fs::write( - shim.path().join("rustup.ps1"), - concat!( - "Add-Content -LiteralPath $env:ANVIL_TEST_LOG -Value ($args -join ' ')\n", - "if (($args -contains 'toolchain') -and ($args -contains 'list')) {\n", - " Write-Output 'stable-x86_64-pc-windows-msvc (default)'\n", - "}\n", - "exit 0\n", - ), - ) - .expect("rustup shim must be writable"); + for (name, log_variable, status) in [("cargo", "ANVIL_CARGO_LOG", 1), ("rustup", "ANVIL_RUSTUP_LOG", 0)] { + let executable = shim.path().join(name); + let extra = if name == "rustup" { + "if [ \"$*\" = 'show active-toolchain' ]; then echo '1.94-test-host (overridden by fixture)'; fi\n" + } else { + "" + }; + fs::write( + &executable, + format!("#!/bin/sh\nprintf '%s\\n' \"$*\" >> \"${log_variable}\"\n{extra}exit {status}\n"), + ) + .expect("component shim must be writable"); + fs::set_permissions(&executable, fs::Permissions::from_mode(0o755)).expect("component shim must be executable"); + } + } let mut paths = vec![shim.path().to_path_buf()]; paths.extend(env::split_paths(&env::var_os("PATH").unwrap_or_default())); + let path = env::join_paths(paths).expect("shim PATH must be valid"); + let run_install = |rustup_toolchain: Option<&str>| { + let mut command = command(temp.path()); + command + .args(["_install-component", "default", "clippy"]) + .env("ANVIL_CARGO_LOG", &cargo_log) + .env("ANVIL_RUSTUP_LOG", &rustup_log) + .env("PATH", &path); + if let Some(value) = rustup_toolchain { + command.env("RUSTUP_TOOLCHAIN", value); + } + command.output().expect("default component installation must run") + }; - let output = command(temp.path()) - .args(["_anvil-resolve-stable", "install"]) - .env("ANVIL_TEST_LOG", &log) - .env("PATH", env::join_paths(paths).expect("shim PATH must be valid")) - .output() - .expect("just must be available to test the generated resolver recipe"); + let output = run_install(Some("custom-toolchain")); assert!( output.status.success(), - "toolchain option application failed: {}", + "override-selected component installation failed: {}", normalized_diagnostic(&output) ); - let calls = fs::read_to_string(log).expect("rustup shim log must be readable"); - for expected in [ - "toolchain list", - "toolchain install 1.93 --profile complete", - "component add --toolchain 1.93 clippy", - "target add --toolchain 1.93 wasm32-unknown-unknown", - ] { - assert!(calls.contains(expected), "missing {expected} in rustup calls:\n{calls}"); - } + assert_eq!( + fs::read_to_string(&cargo_log).expect("Cargo shim log must be readable").trim(), + "+custom-toolchain clippy --version" + ); + assert_eq!( + fs::read_to_string(&rustup_log).expect("rustup shim log must be readable").trim(), + "component add --toolchain custom-toolchain clippy" + ); + + fs::write(temp.path().join("rust-toolchain.toml"), "[toolchain]\nchannel = \"1.94\"\n") + .expect("toolchain fixture must be writable"); + fs::write(&cargo_log, "").expect("Cargo shim log must be resettable"); + fs::write(&rustup_log, "").expect("rustup shim log must be resettable"); + let output = run_install(None); + assert!( + output.status.success(), + "repository-selected component installation failed: {}", + normalized_diagnostic(&output) + ); + assert_eq!( + fs::read_to_string(&cargo_log).expect("Cargo shim log must be readable").trim(), + "+1.94-test-host clippy --version" + ); + let rustup_call = fs::read_to_string(&rustup_log).expect("rustup shim log must be readable"); + assert!( + rustup_call + .lines() + .any(|line| line == "component add --toolchain 1.94-test-host clippy") + ); + assert!( + rustup_call.lines().any(|line| line == "show active-toolchain"), + "repository-selected component installation must ask rustup for the active toolchain" + ); } #[test] fn rejects_missing_or_too_new_workspace_msrvs() { + if !tools_available() { + return; + } let missing = fixture("[workspace]\nresolver = \"2\"\n"); let output = run(missing.path(), &[], None); assert!(!output.status.success()); @@ -1151,7 +1230,41 @@ mod tests { fs::write(member.join("lib.rs"), "").expect("member source must be writable"); } - let output = run(workspace.path(), &["-ValidateWorkspaceMsrv"], None); + let shim = TempDir::new().expect("Cargo shim directory must be creatable"); + let log = shim.path().join("cargo.log"); + fs::write( + shim.path().join("cargo.ps1"), + concat!( + "Add-Content -LiteralPath $env:ANVIL_TEST_LOG -Value ($args -join ' ')\n", + "$metadata = @{\n", + " workspace_members = @('a-id', 'b-id')\n", + " packages = @(\n", + " @{ id = 'a-id'; name = 'a'; rust_version = '1.91' }\n", + " @{ id = 'b-id'; name = 'b'; rust_version = $env:ANVIL_TEST_B_MSRV }\n", + " )\n", + "}\n", + "$metadata | ConvertTo-Json -Depth 4 -Compress\n", + "exit 0\n", + ), + ) + .expect("Cargo shim must be writable"); + let mut paths = vec![shim.path().to_path_buf()]; + paths.extend(env::split_paths(&env::var_os("PATH").unwrap_or_default())); + let path = env::join_paths(paths).expect("shim PATH must be valid"); + let run_validation = |b_msrv: &str, rustup_toolchain: Option<&str>| { + let mut command = command(workspace.path()); + command + .args(["_anvil-resolve-stable", "validate-workspace-msrv"]) + .env("ANVIL_TEST_B_MSRV", b_msrv) + .env("ANVIL_TEST_LOG", &log) + .env("PATH", &path); + if let Some(value) = rustup_toolchain { + command.env("RUSTUP_TOOLCHAIN", value); + } + command.output().expect("workspace MSRV validation must run") + }; + + let output = run_validation("1.93", None); assert!(!output.status.success()); let diagnostic = normalized_diagnostic(&output); assert!( @@ -1159,24 +1272,27 @@ mod tests { "unexpected resolver diagnostic: {diagnostic}" ); - fs::write( - workspace.path().join("b/Cargo.toml"), - "[package]\nname = \"b\"\nversion = \"0.1.0\"\nedition = \"2021\"\nrust-version = \"1.92\"\n[lib]\npath = \"lib.rs\"\n", - ) - .expect("member manifest must be writable"); - let output = run(workspace.path(), &["-ValidateWorkspaceMsrv"], None); + let output = run_validation("1.92", None); assert!( output.status.success(), "member MSRVs at or below the root must be compatible: {}", normalized_diagnostic(&output) ); + let calls = fs::read_to_string(&log).expect("Cargo shim log must be readable"); + assert!( + calls.lines().all(|line| line.starts_with("+1.92 metadata ")), + "workspace compatibility metadata must use the root MSRV toolchain:\n{calls}" + ); - let output = run(workspace.path(), &["-ValidateWorkspaceMsrv"], Some("explicit-toolchain")); + let output = run_validation("1.93", Some("explicit-toolchain")); assert!(output.status.success(), "explicit override must permit differing MSRVs"); } #[test] fn rejects_an_unpaired_internal_msrv_mapping_for_stable_selection() { + if !tools_available() { + return; + } let temp = fixture("[workspace.package]\nrust-version = \"1.93\"\n"); let run_with_mapping = |root: &Path| { command(root) @@ -1195,19 +1311,13 @@ mod tests { && diagnostic.contains("unset ANVIL_MSRV_TOOLCHAIN"), "unexpected resolver diagnostic: {diagnostic}" ); - - fs::write(temp.path().join("rust-toolchain.toml"), "[toolchain]\nchannel = \"1.94\"\n") - .expect("toolchain fixture must be writable"); - let output = run_with_mapping(temp.path()); - assert!( - output.status.success(), - "a repository-selected stable toolchain completes the configuration: {}", - normalized_diagnostic(&output) - ); } #[test] fn provisions_public_and_mapped_msrv_toolchains() { + if !tools_available() { + return; + } let run_install = |root: &Path, mapped: Option<&str>, installed: &str, cargo_exit: i32| { let shim = TempDir::new().expect("toolchain shim directory must be creatable"); let rustup_log = shim.path().join("rustup.log"); @@ -1247,11 +1357,6 @@ mod tests { }; let temp = fixture("[workspace.package]\nrust-version = \"1.93\"\n"); - fs::write( - temp.path().join("rust-toolchain.toml"), - "[toolchain]\ncomponents = [\"clippy\"]\ntargets = [\"wasm32-unknown-unknown\"]\n", - ) - .expect("toolchain fixture must be writable"); let (output, rustup_calls, cargo_calls) = run_install(temp.path(), None, "1.93-x86_64-pc-windows-msvc", 0); assert!( @@ -1264,8 +1369,6 @@ mod tests { !rustup_calls.contains("toolchain install"), "installed MSRV must not be reinstalled" ); - assert!(rustup_calls.contains("component add --toolchain 1.93 clippy")); - assert!(rustup_calls.contains("target add --toolchain 1.93 wasm32-unknown-unknown")); assert!(cargo_calls.is_empty(), "public MSRV availability is checked through rustup"); let (output, rustup_calls, cargo_calls) = run_install(temp.path(), Some("ms-prod-1.93"), "", 0); @@ -1277,8 +1380,6 @@ mod tests { assert!(cargo_calls.contains("+ms-prod-1.93 --version")); assert!(!rustup_calls.contains("toolchain list")); assert!(!rustup_calls.contains("toolchain install")); - assert!(rustup_calls.contains("component add --toolchain ms-prod-1.93 clippy")); - assert!(rustup_calls.contains("target add --toolchain ms-prod-1.93 wasm32-unknown-unknown")); let (output, _, cargo_calls) = run_install(temp.path(), Some("ms-prod-1.93"), "", 9); assert!(!output.status.success(), "an unavailable mapped MSRV must fail"); @@ -1295,6 +1396,9 @@ mod tests { #[test] fn resolves_msrv_whenever_the_root_declares_one() { + if !tools_available() { + return; + } let no_msrv = fixture("[workspace]\nresolver = \"2\"\n"); fs::write(no_msrv.path().join("rust-toolchain.toml"), "[toolchain]\nchannel = \"1.94\"\n") .expect("toolchain fixture must be writable"); diff --git a/crates/cargo-anvil/templates/justfiles/anvil/tools.just b/crates/cargo-anvil/templates/justfiles/anvil/tools.just index 31f8537d2..0d6e45f92 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/tools.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/tools.just @@ -173,6 +173,8 @@ anvil-tool-pwsh-validate-prereqs: # ============================================================================ # Resolve or prepare the selected stable compiler without globally exporting it. +# Rustup owns toolchain-file parsing, installation, and file options. Anvil only +# handles explicit environment overrides and the root Cargo MSRV fallback. [script("pwsh", "-NoProfile")] _anvil-resolve-stable action="resolve": $ErrorActionPreference = 'Stop' @@ -183,11 +185,9 @@ _anvil-resolve-stable action="resolve": 'resolve', 'for-environment', 'install', - 'apply-file-options', 'validate-workspace-msrv', 'msrv', - 'install-msrv', - 'emit-file-options' + 'install-msrv' ) if ($action -notin $validActions) { Write-Error "_anvil-resolve-stable: unknown action '$action'" @@ -196,109 +196,11 @@ _anvil-resolve-stable action="resolve": $repoRoot = '{{justfile_directory()}}' - function Get-RootToolchainFilePath { - foreach ($name in @('rust-toolchain', 'rust-toolchain.toml')) { - $path = Join-Path $repoRoot $name - if (Test-Path -LiteralPath $path -PathType Leaf) { - return $path - } - } - return $null - } - - function Read-RootToolchainFile([string] $path) { - $content = Get-Content -LiteralPath $path -Raw - if ([string]::IsNullOrWhiteSpace($content)) { - throw "anvil: root toolchain file '$path' is empty; remove it or declare a toolchain configuration" - } - return $content - } - - function Get-SelectingToolchainFile { - $path = Get-RootToolchainFilePath - if ($null -eq $path) { - return $null - } - - $content = Read-RootToolchainFile $path - if ((Split-Path -Leaf $path) -eq 'rust-toolchain' -and $content.TrimStart() -notmatch '^\[') { - return [pscustomobject]@{ - Path = $path - Value = $content.Trim() - Selector = 'channel' - Source = 'file' - Installable = $false - } - } - - $inToolchain = $false - foreach ($line in $content -split '\r?\n') { - if ($line -match '^\s*\[\s*toolchain\s*\]\s*(?:#.*)?$') { - $inToolchain = $true - continue - } - if ($line -match '^\s*\[') { - $inToolchain = $false - continue - } - if ($inToolchain -and $line -match '^\s*["'']?(channel|path)["'']?\s*=\s*(["''])([^"'']+)\2\s*(?:#.*)?$') { - $value = $Matches[3] - if ($Matches[1] -eq 'path' -and -not [IO.Path]::IsPathRooted($value)) { - $value = [IO.Path]::GetFullPath((Join-Path $repoRoot $value)) - } - return [pscustomobject]@{ - Path = $path - Value = $value - Selector = $Matches[1] - Source = 'file' - Installable = $false - } - } - } - return $null - } - - function Get-ToolchainFileOptions { - $path = Get-RootToolchainFilePath - if ($null -eq $path) { - return $null - } - - $content = Read-RootToolchainFile $path - $section = [regex]::Match( - $content, - '(?ms)^\s*\[\s*toolchain\s*\]\s*(?:#.*)?$(.*?)(?=^\s*\[|\z)' - ) - if (-not $section.Success) { - return $null - } - - $body = $section.Groups[1].Value - $profileMatch = [regex]::Match( - $body, - '(?m)^\s*["'']?profile["'']?\s*=\s*(["''])([^"'']+)\1\s*(?:#.*)?$' + function Test-RootToolchainFile { + return ( + (Test-Path -LiteralPath (Join-Path $repoRoot 'rust-toolchain') -PathType Leaf) -or + (Test-Path -LiteralPath (Join-Path $repoRoot 'rust-toolchain.toml') -PathType Leaf) ) - $readArray = { - param([string] $name) - $arrayMatch = [regex]::Match( - $body, - "(?ms)^\s*[`"']?$name[`"']?\s*=\s*\[(.*?)\]" - ) - if (-not $arrayMatch.Success) { - return @() - } - return @( - [regex]::Matches($arrayMatch.Groups[1].Value, '(["''])([^"'']+)\1') | - ForEach-Object { $_.Groups[2].Value } - ) - } - - return [pscustomobject]@{ - Path = $path - Profile = if ($profileMatch.Success) { $profileMatch.Groups[2].Value } else { $null } - Components = @(& $readArray 'components') - Targets = @(& $readArray 'targets') - } } function Get-RootMsrv([switch] $AllowMissing) { @@ -335,7 +237,8 @@ _anvil-resolve-stable action="resolve": function Assert-WorkspaceMsrvCompatibility { $manifestPath = Join-Path $repoRoot 'Cargo.toml' - $metadataText = (& cargo metadata --manifest-path $manifestPath --format-version 1 --no-deps 2>&1 | Out-String) + $rootMsrv = Get-RootMsrv + $metadataText = (& cargo "+$rootMsrv" metadata --manifest-path $manifestPath --format-version 1 --no-deps 2>&1 | Out-String) if ($LASTEXITCODE -ne 0) { throw "anvil: cargo metadata failed while validating workspace MSRVs:`n$metadataText" } @@ -350,7 +253,6 @@ _anvil-resolve-stable action="resolve": throw "anvil: workspace packages without a resolved rust-version: $($missing -join ', '); declare one or select a toolchain explicitly" } - $rootMsrv = Get-RootMsrv $newer = @( $members | Where-Object { [version] ([string] $_.rust_version) -gt [version] $rootMsrv } | @@ -364,7 +266,7 @@ _anvil-resolve-stable action="resolve": function Assert-CompleteInternalToolchainConfiguration { if (-not [string]::IsNullOrWhiteSpace($env:ANVIL_MSRV_TOOLCHAIN) -and [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN) -and - $null -eq $fileSelection) { + -not (Test-RootToolchainFile)) { throw 'anvil: ANVIL_MSRV_TOOLCHAIN is set without RUSTUP_TOOLCHAIN and no repository toolchain file selects stable checks; set RUSTUP_TOOLCHAIN to the provisioned stable toolchain or unset ANVIL_MSRV_TOOLCHAIN' } } @@ -379,67 +281,30 @@ _anvil-resolve-stable action="resolve": return [pscustomobject]@{ Value = $env:ANVIL_MSRV_TOOLCHAIN Source = 'mapped-msrv' - Installable = $false Mapped = $true } } - return [pscustomobject]@{ Value = $msrv; Source = 'msrv'; Installable = $true; Mapped = $false } - } - - function Apply-ToolchainFileOptions([object] $toolchain) { - if ($toolchain.Source -eq 'file' -or $null -eq $toolchainFileOptions) { - return - } - foreach ($component in $toolchainFileOptions.Components) { - rustup component add --toolchain $toolchain.Value $component - if ($LASTEXITCODE -ne 0) { - Write-Warning "anvil: toolchain-file component '$component' is unavailable for '$($toolchain.Value)'; rustup would skip an unavailable file component" - } - } - foreach ($target in $toolchainFileOptions.Targets) { - rustup target add --toolchain $toolchain.Value $target - if ($LASTEXITCODE -ne 0) { - Write-Warning "anvil: toolchain-file target '$target' is unavailable for '$($toolchain.Value)'; rustup would skip an unavailable file target" - } - } + return [pscustomobject]@{ Value = $msrv; Source = 'msrv'; Mapped = $false } } function Install-Toolchain([object] $toolchain, [string] $label) { - if ($toolchain.Installable) { - $installed = rustup toolchain list + $installed = rustup toolchain list + if ($LASTEXITCODE -ne 0) { + throw 'anvil: rustup toolchain list failed' + } + if ($installed -notmatch "(?m)^$([regex]::Escape($toolchain.Value))(?:-|$)") { + Write-Host "anvil: installing $label toolchain '$($toolchain.Value)'" + rustup toolchain install $toolchain.Value --profile minimal if ($LASTEXITCODE -ne 0) { - throw 'anvil: rustup toolchain list failed' - } - if ($installed -notmatch "(?m)^$([regex]::Escape($toolchain.Value))(?:-|$)") { - $profile = if ($null -ne $toolchainFileOptions -and $toolchainFileOptions.Profile) { - $toolchainFileOptions.Profile - } else { - 'minimal' - } - Write-Host "anvil: installing $label toolchain '$($toolchain.Value)'" - rustup toolchain install $toolchain.Value --profile $profile - if ($LASTEXITCODE -ne 0) { - throw "anvil: failed to install $label toolchain '$($toolchain.Value)'" - } + throw "anvil: failed to install $label toolchain '$($toolchain.Value)'" } } - Apply-ToolchainFileOptions $toolchain - } - - $fileSelection = Get-SelectingToolchainFile - $toolchainFileOptions = Get-ToolchainFileOptions - - if ($action -eq 'emit-file-options') { - if ($null -ne $toolchainFileOptions) { - $toolchainFileOptions | ConvertTo-Json -Compress - } - exit 0 } if ($action -eq 'validate-workspace-msrv') { Assert-CompleteInternalToolchainConfiguration $skipWorkspaceMsrvValidation = -not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN) - if (-not $skipWorkspaceMsrvValidation -and $null -eq $fileSelection) { + if (-not $skipWorkspaceMsrvValidation -and -not (Test-RootToolchainFile)) { Assert-WorkspaceMsrvCompatibility } exit 0 @@ -456,8 +321,9 @@ _anvil-resolve-stable action="resolve": if ($LASTEXITCODE -ne 0) { throw "anvil: mapped MSRV toolchain '$($msrvSelection.Value)' is unavailable; provision ANVIL_MSRV_TOOLCHAIN or unset it to let Anvil install the declared public MSRV" } + } else { + Install-Toolchain $msrvSelection 'MSRV' } - Install-Toolchain $msrvSelection 'MSRV' } else { $msrvSelection.Value } @@ -466,20 +332,32 @@ _anvil-resolve-stable action="resolve": Assert-CompleteInternalToolchainConfiguration $selection = if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { - [pscustomobject]@{ Value = $env:RUSTUP_TOOLCHAIN; Source = 'environment'; Installable = $false } - } elseif ($null -ne $fileSelection) { - $fileSelection + [pscustomobject]@{ Value = $env:RUSTUP_TOOLCHAIN; Source = 'environment' } + } elseif (Test-RootToolchainFile) { + [pscustomobject]@{ Value = $null; Source = 'file' } } else { - [pscustomobject]@{ Value = Get-RootMsrv; Source = 'msrv'; Installable = $true } - } - - if ($action -eq 'apply-file-options') { - Apply-ToolchainFileOptions $selection - exit 0 + [pscustomobject]@{ Value = Get-RootMsrv; Source = 'msrv' } } if ($action -eq 'install') { - Install-Toolchain $selection 'stable' + if ($selection.Source -eq 'file') { + Push-Location $repoRoot + try { + & rustc --version + if ($LASTEXITCODE -ne 0) { + throw 'anvil: rustup could not provision the repository toolchain file' + } + } finally { + Pop-Location + } + } elseif ($selection.Source -eq 'environment') { + & cargo "+$($selection.Value)" --version *> $null + if ($LASTEXITCODE -ne 0) { + throw "anvil: selected stable toolchain '$($selection.Value)' is unavailable; provision RUSTUP_TOOLCHAIN or unset it" + } + } else { + Install-Toolchain $selection 'stable' + } exit 0 } @@ -489,10 +367,10 @@ _anvil-resolve-stable action="resolve": $selection.Value } -# Provision the selected stable toolchain and replay any applicable -# toolchain-file options. This is the shared prerequisite for setup paths that -# execute stable Cargo/Rust or install Cargo tools; Just deduplicates it within -# each setup invocation. +# Provision the selected stable toolchain. Repository toolchain files are +# processed natively by rustup. This is the shared prerequisite for setup paths +# that execute stable Cargo/Rust or install Cargo tools; Just deduplicates it +# within each setup invocation. [group("anvil-setup")] anvil-toolchain-stable-install: (_anvil-resolve-stable "install") @@ -654,11 +532,11 @@ _check-toolchain toolchain: exit 1 } -# _install-component: add a component to a toolchain. The toolchain is -# either the literal "default" (current rustup default) or a specific -# pinned toolchain string (which must already be installed -- the -# per-component setup recipes ensure this via a dependency on -# anvil--install). +# _install-component: add a component to a toolchain. The toolchain is either +# the literal "default" (Anvil's selected stable toolchain) or a specific pinned +# toolchain string (which must already be installed -- the per-component setup +# recipes ensure this via a dependency on anvil--install). Explicit +# stable selections target `rustup component add --toolchain`. # # Probe-first: if the component is already available, skip the # `rustup component add`. This keeps the recipe idempotent and robust @@ -672,6 +550,14 @@ _install-component toolchain component: $ErrorActionPreference = 'Stop' $toolchain = '{{toolchain}}' $component = '{{component}}' + $stableArgs = @() + $selectedStableToolchain = $null + if ($toolchain -eq 'default') { + $stableArgs = {{_anvil_stable_toolchain_args}} + if ($stableArgs.Count -eq 1) { + $selectedStableToolchain = ([string] $stableArgs[0]).Substring(1) + } + } # Probe whether the component is already present (see _check-component # for the rationale behind each detection method and the `+toolchain` @@ -684,14 +570,22 @@ _install-component toolchain component: } if ($null -ne $subcommand) { if ($toolchain -eq 'default') { - & cargo {{_anvil_stable_toolchain_args}} $subcommand --version *> $null + if ($null -ne $selectedStableToolchain) { + & cargo $stableArgs[0] $subcommand --version *> $null + } else { + & cargo $subcommand --version *> $null + } } else { & cargo "+$toolchain" $subcommand --version *> $null } $present = ($LASTEXITCODE -eq 0) } else { $sysroot = if ($toolchain -eq 'default') { - & rustc {{_anvil_stable_toolchain_args}} --print sysroot 2>$null + if ($null -ne $selectedStableToolchain) { + & rustc $stableArgs[0] --print sysroot 2>$null + } else { + & rustc --print sysroot 2>$null + } } else { & rustc "+$toolchain" --print sysroot 2>$null } @@ -715,8 +609,8 @@ _install-component toolchain component: } if ($toolchain -eq 'default') { - Write-Host "rustup component add $component (default toolchain)" - rustup component add $component + Write-Host "rustup component add --toolchain $selectedStableToolchain $component" + rustup component add --toolchain $selectedStableToolchain $component } else { Write-Host "rustup component add --toolchain $toolchain $component" rustup component add --toolchain $toolchain $component @@ -744,6 +638,14 @@ _check-component toolchain component: $ErrorActionPreference = 'Stop' $toolchain = '{{toolchain}}' $component = '{{component}}' + $stableArgs = @() + $selectedStableToolchain = $null + if ($toolchain -eq 'default') { + $stableArgs = {{_anvil_stable_toolchain_args}} + if ($stableArgs.Count -eq 1) { + $selectedStableToolchain = ([string] $stableArgs[0]).Substring(1) + } + } $subcommand = switch ($component) { 'clippy' { 'clippy' } 'rustfmt' { 'fmt' } @@ -752,14 +654,22 @@ _check-component toolchain component: } if ($null -ne $subcommand) { if ($toolchain -eq 'default') { - & cargo {{_anvil_stable_toolchain_args}} $subcommand --version *> $null + if ($null -ne $selectedStableToolchain) { + & cargo $stableArgs[0] $subcommand --version *> $null + } else { + & cargo $subcommand --version *> $null + } } else { & cargo "+$toolchain" $subcommand --version *> $null } $present = ($LASTEXITCODE -eq 0) } else { $sysroot = if ($toolchain -eq 'default') { - & rustc {{_anvil_stable_toolchain_args}} --print sysroot 2>$null + if ($null -ne $selectedStableToolchain) { + & rustc $stableArgs[0] --print sysroot 2>$null + } else { + & rustc --print sysroot 2>$null + } } else { & rustc "+$toolchain" --print sysroot 2>$null } @@ -781,7 +691,13 @@ _check-component toolchain component: } } if (-not $present) { - $cmd = if ($toolchain -eq 'default') { "rustup component add $component" } else { "rustup component add --toolchain $toolchain $component" } + $cmd = if ($null -ne $selectedStableToolchain) { + "rustup component add --toolchain $selectedStableToolchain $component" + } elseif ($toolchain -eq 'default') { + "rustup component add $component" + } else { + "rustup component add --toolchain $toolchain $component" + } Write-Error "anvil: component '$component' not installed on toolchain '$toolchain'. Run: $cmd" exit 1 } @@ -810,10 +726,9 @@ anvil-toolchain-nightly-external-types-validate-prereqs: (_check-toolchain rust_ # Rustup components # ============================================================================ # -# Default-toolchain components are installed via `rustup component add` -# (no toolchain spec). Nightly components depend on the toolchain -# being installed first (via the relevant anvil--install -# recipe) and then add the component. +# Stable components target Anvil's explicit selected toolchain. +# Nightly components depend on the toolchain being installed first (via the +# relevant anvil--install recipe) and then add the component. # Install the pinned `clippy` component for the default toolchain. [group("anvil-setup")] diff --git a/crates/cargo-anvil/templates/justfiles/anvil/versions.just b/crates/cargo-anvil/templates/justfiles/anvil/versions.just index e4d7ef5a7..1641ee8a3 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/versions.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/versions.just @@ -31,65 +31,42 @@ # Rustup toolchains # ============================================================================ -# Stable commands interpolate this PowerShell array expression directly -# at each command site. Selection runs inside the existing PowerShell recipe -# process, so it needs no host-shell quoting or selector subprocess. +# Stable commands interpolate this PowerShell array expression directly at each +# command site. Rustup resolves repository toolchain files from the repository +# root; Anvil only reads Cargo.toml when it needs the root MSRV fallback. # `RUSTUP_TOOLCHAIN` is an input override only; Anvil never exports a resolved # value globally into unrelated recipes or helpers. [private] -_anvil_stable_toolchain_args := trim(''' -@(& { -$RepoRoot = (Get-Location).Path - +_anvil_stable_toolchain_args := trim("@(& {\n$RepoRoot = '" + replace(justfile_directory(), "'", "''") + "'\n\n" + ''' $ErrorActionPreference = 'Stop' Set-StrictMode -Version 3 -function Write-ExplicitToolchainArg([string] $Toolchain) { - Write-Output ('+' + $Toolchain) -} - if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { - Write-ExplicitToolchainArg $env:RUSTUP_TOOLCHAIN + Write-Output ('+' + $env:RUSTUP_TOOLCHAIN) return } -$toolchainFile = $null -foreach ($name in @('rust-toolchain', 'rust-toolchain.toml')) { - $candidate = Join-Path $RepoRoot $name - if (Test-Path -LiteralPath $candidate -PathType Leaf) { - $toolchainFile = $candidate - break - } -} - -$selectingFile = $false -if ($null -ne $toolchainFile) { - $content = Get-Content -LiteralPath $toolchainFile -Raw - if ([string]::IsNullOrWhiteSpace($content)) { - throw "anvil: root toolchain file '$toolchainFile' is empty; remove it or declare a toolchain configuration" - } - if ((Split-Path -Leaf $toolchainFile) -eq 'rust-toolchain' -and $content.TrimStart() -notmatch '^\[') { - $selectingFile = $true - } else { - $inToolchain = $false - foreach ($line in $content -split '\r?\n') { - if ($line -match '^\s*\[\s*toolchain\s*\]\s*(?:#.*)?$') { - $inToolchain = $true - continue - } - if ($line -match '^\s*\[') { - $inToolchain = $false - continue - } - if ($inToolchain -and $line -match '^\s*["'']?(channel|path)["'']?\s*=\s*(["''])([^"'']+)\2\s*(?:#.*)?$') { - $selectingFile = $true - break - } +if ((Test-Path -LiteralPath (Join-Path $RepoRoot 'rust-toolchain') -PathType Leaf) -or + (Test-Path -LiteralPath (Join-Path $RepoRoot 'rust-toolchain.toml') -PathType Leaf)) { + Push-Location $RepoRoot + try { + $rustupOutput = @(& rustup show active-toolchain 2>&1) + if ($LASTEXITCODE -ne 0) { + throw "anvil: rustup could not resolve the repository toolchain file:`n$($rustupOutput -join "`n")" + } + $active = $rustupOutput | Select-Object -Last 1 + if ([string]::IsNullOrWhiteSpace([string] $active)) { + throw 'anvil: rustup returned no active repository toolchain' } + } finally { + Pop-Location } -} - -if ($selectingFile) { + $resolved = [string] $active + $sourceMarker = $resolved.LastIndexOf(' (') + if ($sourceMarker -ge 0) { + $resolved = $resolved.Substring(0, $sourceMarker) + } + Write-Output ('+' + $resolved) return } @@ -122,7 +99,7 @@ $msrv = if ($values.ContainsKey('workspace.package')) { } else { throw 'anvil: no selecting rust-toolchain(.toml) and no root [workspace.package] or [package] rust-version; declare an MSRV or select a toolchain explicitly' } -Write-ExplicitToolchainArg $msrv +Write-Output ('+' + $msrv) }) ''') diff --git a/crates/cargo-anvil/tests/impact.rs b/crates/cargo-anvil/tests/impact.rs index 2aa59febe..aaf8565f5 100644 --- a/crates/cargo-anvil/tests/impact.rs +++ b/crates/cargo-anvil/tests/impact.rs @@ -1074,7 +1074,7 @@ fn fake_cargo_with_metadata(dir: &Path, log: &Path, metadata_json: &Path) { #[cfg(unix)] { let script = format!( - "#!/bin/sh\nif [ \"$1\" = metadata ]; then cat '{meta}'; exit 0; fi\nprintf '%s\\n' \"$*\" >> '{log}'\nexit 0\n", + "#!/bin/sh\ncase \"$1\" in +*) shift ;; esac\nif [ \"$1\" = metadata ]; then cat '{meta}'; exit 0; fi\nprintf '%s\\n' \"$*\" >> '{log}'\nexit 0\n", meta = metadata_json.display(), log = log.display() ); @@ -1487,13 +1487,12 @@ fn impact_snapshot_uses_current_checkout_toolchain_for_both_trees() { "baseline and current snapshots must both use Anvil's selected MSRV" ); - // Case C: a toolchain file is selected natively in the current checkout. - // The baseline needs rustup's concrete active identifier because the file - // may contain a path that cannot be assigned directly to RUSTUP_TOOLCHAIN. + // Case C: rustup resolves the current checkout's toolchain file once and + // the concrete identifier selects both snapshots. let with_file = snapshot_toolchain_probe(None, true); assert_eq!( with_file, - vec!["file-active-toolchain".to_owned(), "".to_owned()], - "baseline must use the current file's concrete active toolchain while the current snapshot lets rustup process that file natively" + vec!["file-active-toolchain".to_owned(), "file-active-toolchain".to_owned()], + "baseline and current snapshots must both use the current file's concrete active toolchain" ); } diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap index 2631249a6..ed5ab45ae 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap @@ -6361,6 +6361,8 @@ anvil-tool-pwsh-validate-prereqs: # ============================================================================ # Resolve or prepare the selected stable compiler without globally exporting it. +# Rustup owns toolchain-file parsing, installation, and file options. Anvil only +# handles explicit environment overrides and the root Cargo MSRV fallback. [script("pwsh", "-NoProfile")] _anvil-resolve-stable action="resolve": $ErrorActionPreference = 'Stop' @@ -6371,11 +6373,9 @@ _anvil-resolve-stable action="resolve": 'resolve', 'for-environment', 'install', - 'apply-file-options', 'validate-workspace-msrv', 'msrv', - 'install-msrv', - 'emit-file-options' + 'install-msrv' ) if ($action -notin $validActions) { Write-Error "_anvil-resolve-stable: unknown action '$action'" @@ -6384,109 +6384,11 @@ _anvil-resolve-stable action="resolve": $repoRoot = '{{justfile_directory()}}' - function Get-RootToolchainFilePath { - foreach ($name in @('rust-toolchain', 'rust-toolchain.toml')) { - $path = Join-Path $repoRoot $name - if (Test-Path -LiteralPath $path -PathType Leaf) { - return $path - } - } - return $null - } - - function Read-RootToolchainFile([string] $path) { - $content = Get-Content -LiteralPath $path -Raw - if ([string]::IsNullOrWhiteSpace($content)) { - throw "anvil: root toolchain file '$path' is empty; remove it or declare a toolchain configuration" - } - return $content - } - - function Get-SelectingToolchainFile { - $path = Get-RootToolchainFilePath - if ($null -eq $path) { - return $null - } - - $content = Read-RootToolchainFile $path - if ((Split-Path -Leaf $path) -eq 'rust-toolchain' -and $content.TrimStart() -notmatch '^\[') { - return [pscustomobject]@{ - Path = $path - Value = $content.Trim() - Selector = 'channel' - Source = 'file' - Installable = $false - } - } - - $inToolchain = $false - foreach ($line in $content -split '\r?\n') { - if ($line -match '^\s*\[\s*toolchain\s*\]\s*(?:#.*)?$') { - $inToolchain = $true - continue - } - if ($line -match '^\s*\[') { - $inToolchain = $false - continue - } - if ($inToolchain -and $line -match '^\s*["'']?(channel|path)["'']?\s*=\s*(["''])([^"'']+)\2\s*(?:#.*)?$') { - $value = $Matches[3] - if ($Matches[1] -eq 'path' -and -not [IO.Path]::IsPathRooted($value)) { - $value = [IO.Path]::GetFullPath((Join-Path $repoRoot $value)) - } - return [pscustomobject]@{ - Path = $path - Value = $value - Selector = $Matches[1] - Source = 'file' - Installable = $false - } - } - } - return $null - } - - function Get-ToolchainFileOptions { - $path = Get-RootToolchainFilePath - if ($null -eq $path) { - return $null - } - - $content = Read-RootToolchainFile $path - $section = [regex]::Match( - $content, - '(?ms)^\s*\[\s*toolchain\s*\]\s*(?:#.*)?$(.*?)(?=^\s*\[|\z)' + function Test-RootToolchainFile { + return ( + (Test-Path -LiteralPath (Join-Path $repoRoot 'rust-toolchain') -PathType Leaf) -or + (Test-Path -LiteralPath (Join-Path $repoRoot 'rust-toolchain.toml') -PathType Leaf) ) - if (-not $section.Success) { - return $null - } - - $body = $section.Groups[1].Value - $profileMatch = [regex]::Match( - $body, - '(?m)^\s*["'']?profile["'']?\s*=\s*(["''])([^"'']+)\1\s*(?:#.*)?$' - ) - $readArray = { - param([string] $name) - $arrayMatch = [regex]::Match( - $body, - "(?ms)^\s*[`"']?$name[`"']?\s*=\s*\[(.*?)\]" - ) - if (-not $arrayMatch.Success) { - return @() - } - return @( - [regex]::Matches($arrayMatch.Groups[1].Value, '(["''])([^"'']+)\1') | - ForEach-Object { $_.Groups[2].Value } - ) - } - - return [pscustomobject]@{ - Path = $path - Profile = if ($profileMatch.Success) { $profileMatch.Groups[2].Value } else { $null } - Components = @(& $readArray 'components') - Targets = @(& $readArray 'targets') - } } function Get-RootMsrv([switch] $AllowMissing) { @@ -6523,7 +6425,8 @@ _anvil-resolve-stable action="resolve": function Assert-WorkspaceMsrvCompatibility { $manifestPath = Join-Path $repoRoot 'Cargo.toml' - $metadataText = (& cargo metadata --manifest-path $manifestPath --format-version 1 --no-deps 2>&1 | Out-String) + $rootMsrv = Get-RootMsrv + $metadataText = (& cargo "+$rootMsrv" metadata --manifest-path $manifestPath --format-version 1 --no-deps 2>&1 | Out-String) if ($LASTEXITCODE -ne 0) { throw "anvil: cargo metadata failed while validating workspace MSRVs:`n$metadataText" } @@ -6538,7 +6441,6 @@ _anvil-resolve-stable action="resolve": throw "anvil: workspace packages without a resolved rust-version: $($missing -join ', '); declare one or select a toolchain explicitly" } - $rootMsrv = Get-RootMsrv $newer = @( $members | Where-Object { [version] ([string] $_.rust_version) -gt [version] $rootMsrv } | @@ -6552,7 +6454,7 @@ _anvil-resolve-stable action="resolve": function Assert-CompleteInternalToolchainConfiguration { if (-not [string]::IsNullOrWhiteSpace($env:ANVIL_MSRV_TOOLCHAIN) -and [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN) -and - $null -eq $fileSelection) { + -not (Test-RootToolchainFile)) { throw 'anvil: ANVIL_MSRV_TOOLCHAIN is set without RUSTUP_TOOLCHAIN and no repository toolchain file selects stable checks; set RUSTUP_TOOLCHAIN to the provisioned stable toolchain or unset ANVIL_MSRV_TOOLCHAIN' } } @@ -6567,67 +6469,30 @@ _anvil-resolve-stable action="resolve": return [pscustomobject]@{ Value = $env:ANVIL_MSRV_TOOLCHAIN Source = 'mapped-msrv' - Installable = $false Mapped = $true } } - return [pscustomobject]@{ Value = $msrv; Source = 'msrv'; Installable = $true; Mapped = $false } - } - - function Apply-ToolchainFileOptions([object] $toolchain) { - if ($toolchain.Source -eq 'file' -or $null -eq $toolchainFileOptions) { - return - } - foreach ($component in $toolchainFileOptions.Components) { - rustup component add --toolchain $toolchain.Value $component - if ($LASTEXITCODE -ne 0) { - Write-Warning "anvil: toolchain-file component '$component' is unavailable for '$($toolchain.Value)'; rustup would skip an unavailable file component" - } - } - foreach ($target in $toolchainFileOptions.Targets) { - rustup target add --toolchain $toolchain.Value $target - if ($LASTEXITCODE -ne 0) { - Write-Warning "anvil: toolchain-file target '$target' is unavailable for '$($toolchain.Value)'; rustup would skip an unavailable file target" - } - } + return [pscustomobject]@{ Value = $msrv; Source = 'msrv'; Mapped = $false } } function Install-Toolchain([object] $toolchain, [string] $label) { - if ($toolchain.Installable) { - $installed = rustup toolchain list + $installed = rustup toolchain list + if ($LASTEXITCODE -ne 0) { + throw 'anvil: rustup toolchain list failed' + } + if ($installed -notmatch "(?m)^$([regex]::Escape($toolchain.Value))(?:-|$)") { + Write-Host "anvil: installing $label toolchain '$($toolchain.Value)'" + rustup toolchain install $toolchain.Value --profile minimal if ($LASTEXITCODE -ne 0) { - throw 'anvil: rustup toolchain list failed' - } - if ($installed -notmatch "(?m)^$([regex]::Escape($toolchain.Value))(?:-|$)") { - $profile = if ($null -ne $toolchainFileOptions -and $toolchainFileOptions.Profile) { - $toolchainFileOptions.Profile - } else { - 'minimal' - } - Write-Host "anvil: installing $label toolchain '$($toolchain.Value)'" - rustup toolchain install $toolchain.Value --profile $profile - if ($LASTEXITCODE -ne 0) { - throw "anvil: failed to install $label toolchain '$($toolchain.Value)'" - } + throw "anvil: failed to install $label toolchain '$($toolchain.Value)'" } } - Apply-ToolchainFileOptions $toolchain - } - - $fileSelection = Get-SelectingToolchainFile - $toolchainFileOptions = Get-ToolchainFileOptions - - if ($action -eq 'emit-file-options') { - if ($null -ne $toolchainFileOptions) { - $toolchainFileOptions | ConvertTo-Json -Compress - } - exit 0 } if ($action -eq 'validate-workspace-msrv') { Assert-CompleteInternalToolchainConfiguration $skipWorkspaceMsrvValidation = -not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN) - if (-not $skipWorkspaceMsrvValidation -and $null -eq $fileSelection) { + if (-not $skipWorkspaceMsrvValidation -and -not (Test-RootToolchainFile)) { Assert-WorkspaceMsrvCompatibility } exit 0 @@ -6644,8 +6509,9 @@ _anvil-resolve-stable action="resolve": if ($LASTEXITCODE -ne 0) { throw "anvil: mapped MSRV toolchain '$($msrvSelection.Value)' is unavailable; provision ANVIL_MSRV_TOOLCHAIN or unset it to let Anvil install the declared public MSRV" } + } else { + Install-Toolchain $msrvSelection 'MSRV' } - Install-Toolchain $msrvSelection 'MSRV' } else { $msrvSelection.Value } @@ -6654,20 +6520,32 @@ _anvil-resolve-stable action="resolve": Assert-CompleteInternalToolchainConfiguration $selection = if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { - [pscustomobject]@{ Value = $env:RUSTUP_TOOLCHAIN; Source = 'environment'; Installable = $false } - } elseif ($null -ne $fileSelection) { - $fileSelection + [pscustomobject]@{ Value = $env:RUSTUP_TOOLCHAIN; Source = 'environment' } + } elseif (Test-RootToolchainFile) { + [pscustomobject]@{ Value = $null; Source = 'file' } } else { - [pscustomobject]@{ Value = Get-RootMsrv; Source = 'msrv'; Installable = $true } - } - - if ($action -eq 'apply-file-options') { - Apply-ToolchainFileOptions $selection - exit 0 + [pscustomobject]@{ Value = Get-RootMsrv; Source = 'msrv' } } if ($action -eq 'install') { - Install-Toolchain $selection 'stable' + if ($selection.Source -eq 'file') { + Push-Location $repoRoot + try { + & rustc --version + if ($LASTEXITCODE -ne 0) { + throw 'anvil: rustup could not provision the repository toolchain file' + } + } finally { + Pop-Location + } + } elseif ($selection.Source -eq 'environment') { + & cargo "+$($selection.Value)" --version *> $null + if ($LASTEXITCODE -ne 0) { + throw "anvil: selected stable toolchain '$($selection.Value)' is unavailable; provision RUSTUP_TOOLCHAIN or unset it" + } + } else { + Install-Toolchain $selection 'stable' + } exit 0 } @@ -6677,10 +6555,10 @@ _anvil-resolve-stable action="resolve": $selection.Value } -# Provision the selected stable toolchain and replay any applicable -# toolchain-file options. This is the shared prerequisite for setup paths that -# execute stable Cargo/Rust or install Cargo tools; Just deduplicates it within -# each setup invocation. +# Provision the selected stable toolchain. Repository toolchain files are +# processed natively by rustup. This is the shared prerequisite for setup paths +# that execute stable Cargo/Rust or install Cargo tools; Just deduplicates it +# within each setup invocation. [group("anvil-setup")] anvil-toolchain-stable-install: (_anvil-resolve-stable "install") @@ -6842,11 +6720,11 @@ _check-toolchain toolchain: exit 1 } -# _install-component: add a component to a toolchain. The toolchain is -# either the literal "default" (current rustup default) or a specific -# pinned toolchain string (which must already be installed -- the -# per-component setup recipes ensure this via a dependency on -# anvil--install). +# _install-component: add a component to a toolchain. The toolchain is either +# the literal "default" (Anvil's selected stable toolchain) or a specific pinned +# toolchain string (which must already be installed -- the per-component setup +# recipes ensure this via a dependency on anvil--install). Explicit +# stable selections target `rustup component add --toolchain`. # # Probe-first: if the component is already available, skip the # `rustup component add`. This keeps the recipe idempotent and robust @@ -6860,6 +6738,14 @@ _install-component toolchain component: $ErrorActionPreference = 'Stop' $toolchain = '{{toolchain}}' $component = '{{component}}' + $stableArgs = @() + $selectedStableToolchain = $null + if ($toolchain -eq 'default') { + $stableArgs = {{_anvil_stable_toolchain_args}} + if ($stableArgs.Count -eq 1) { + $selectedStableToolchain = ([string] $stableArgs[0]).Substring(1) + } + } # Probe whether the component is already present (see _check-component # for the rationale behind each detection method and the `+toolchain` @@ -6872,14 +6758,22 @@ _install-component toolchain component: } if ($null -ne $subcommand) { if ($toolchain -eq 'default') { - & cargo {{_anvil_stable_toolchain_args}} $subcommand --version *> $null + if ($null -ne $selectedStableToolchain) { + & cargo $stableArgs[0] $subcommand --version *> $null + } else { + & cargo $subcommand --version *> $null + } } else { & cargo "+$toolchain" $subcommand --version *> $null } $present = ($LASTEXITCODE -eq 0) } else { $sysroot = if ($toolchain -eq 'default') { - & rustc {{_anvil_stable_toolchain_args}} --print sysroot 2>$null + if ($null -ne $selectedStableToolchain) { + & rustc $stableArgs[0] --print sysroot 2>$null + } else { + & rustc --print sysroot 2>$null + } } else { & rustc "+$toolchain" --print sysroot 2>$null } @@ -6903,8 +6797,8 @@ _install-component toolchain component: } if ($toolchain -eq 'default') { - Write-Host "rustup component add $component (default toolchain)" - rustup component add $component + Write-Host "rustup component add --toolchain $selectedStableToolchain $component" + rustup component add --toolchain $selectedStableToolchain $component } else { Write-Host "rustup component add --toolchain $toolchain $component" rustup component add --toolchain $toolchain $component @@ -6932,6 +6826,14 @@ _check-component toolchain component: $ErrorActionPreference = 'Stop' $toolchain = '{{toolchain}}' $component = '{{component}}' + $stableArgs = @() + $selectedStableToolchain = $null + if ($toolchain -eq 'default') { + $stableArgs = {{_anvil_stable_toolchain_args}} + if ($stableArgs.Count -eq 1) { + $selectedStableToolchain = ([string] $stableArgs[0]).Substring(1) + } + } $subcommand = switch ($component) { 'clippy' { 'clippy' } 'rustfmt' { 'fmt' } @@ -6940,14 +6842,22 @@ _check-component toolchain component: } if ($null -ne $subcommand) { if ($toolchain -eq 'default') { - & cargo {{_anvil_stable_toolchain_args}} $subcommand --version *> $null + if ($null -ne $selectedStableToolchain) { + & cargo $stableArgs[0] $subcommand --version *> $null + } else { + & cargo $subcommand --version *> $null + } } else { & cargo "+$toolchain" $subcommand --version *> $null } $present = ($LASTEXITCODE -eq 0) } else { $sysroot = if ($toolchain -eq 'default') { - & rustc {{_anvil_stable_toolchain_args}} --print sysroot 2>$null + if ($null -ne $selectedStableToolchain) { + & rustc $stableArgs[0] --print sysroot 2>$null + } else { + & rustc --print sysroot 2>$null + } } else { & rustc "+$toolchain" --print sysroot 2>$null } @@ -6969,7 +6879,13 @@ _check-component toolchain component: } } if (-not $present) { - $cmd = if ($toolchain -eq 'default') { "rustup component add $component" } else { "rustup component add --toolchain $toolchain $component" } + $cmd = if ($null -ne $selectedStableToolchain) { + "rustup component add --toolchain $selectedStableToolchain $component" + } elseif ($toolchain -eq 'default') { + "rustup component add $component" + } else { + "rustup component add --toolchain $toolchain $component" + } Write-Error "anvil: component '$component' not installed on toolchain '$toolchain'. Run: $cmd" exit 1 } @@ -6998,10 +6914,9 @@ anvil-toolchain-nightly-external-types-validate-prereqs: (_check-toolchain rust_ # Rustup components # ============================================================================ # -# Default-toolchain components are installed via `rustup component add` -# (no toolchain spec). Nightly components depend on the toolchain -# being installed first (via the relevant anvil--install -# recipe) and then add the component. +# Stable components target Anvil's explicit selected toolchain. +# Nightly components depend on the toolchain being installed first (via the +# relevant anvil--install recipe) and then add the component. # Install the pinned `clippy` component for the default toolchain. [group("anvil-setup")] @@ -7310,65 +7225,42 @@ anvil-tool-cargo-udeps-validate-prereqs: (_check-tool "cargo-udeps" cargo_udeps_ # Rustup toolchains # ============================================================================ -# Stable commands interpolate this PowerShell array expression directly -# at each command site. Selection runs inside the existing PowerShell recipe -# process, so it needs no host-shell quoting or selector subprocess. +# Stable commands interpolate this PowerShell array expression directly at each +# command site. Rustup resolves repository toolchain files from the repository +# root; Anvil only reads Cargo.toml when it needs the root MSRV fallback. # `RUSTUP_TOOLCHAIN` is an input override only; Anvil never exports a resolved # value globally into unrelated recipes or helpers. [private] -_anvil_stable_toolchain_args := trim(''' -@(& { -$RepoRoot = (Get-Location).Path - +_anvil_stable_toolchain_args := trim("@(& {\n$RepoRoot = '" + replace(justfile_directory(), "'", "''") + "'\n\n" + ''' $ErrorActionPreference = 'Stop' Set-StrictMode -Version 3 -function Write-ExplicitToolchainArg([string] $Toolchain) { - Write-Output ('+' + $Toolchain) -} - if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { - Write-ExplicitToolchainArg $env:RUSTUP_TOOLCHAIN + Write-Output ('+' + $env:RUSTUP_TOOLCHAIN) return } -$toolchainFile = $null -foreach ($name in @('rust-toolchain', 'rust-toolchain.toml')) { - $candidate = Join-Path $RepoRoot $name - if (Test-Path -LiteralPath $candidate -PathType Leaf) { - $toolchainFile = $candidate - break - } -} - -$selectingFile = $false -if ($null -ne $toolchainFile) { - $content = Get-Content -LiteralPath $toolchainFile -Raw - if ([string]::IsNullOrWhiteSpace($content)) { - throw "anvil: root toolchain file '$toolchainFile' is empty; remove it or declare a toolchain configuration" - } - if ((Split-Path -Leaf $toolchainFile) -eq 'rust-toolchain' -and $content.TrimStart() -notmatch '^\[') { - $selectingFile = $true - } else { - $inToolchain = $false - foreach ($line in $content -split '\r?\n') { - if ($line -match '^\s*\[\s*toolchain\s*\]\s*(?:#.*)?$') { - $inToolchain = $true - continue - } - if ($line -match '^\s*\[') { - $inToolchain = $false - continue - } - if ($inToolchain -and $line -match '^\s*["'']?(channel|path)["'']?\s*=\s*(["''])([^"'']+)\2\s*(?:#.*)?$') { - $selectingFile = $true - break - } +if ((Test-Path -LiteralPath (Join-Path $RepoRoot 'rust-toolchain') -PathType Leaf) -or + (Test-Path -LiteralPath (Join-Path $RepoRoot 'rust-toolchain.toml') -PathType Leaf)) { + Push-Location $RepoRoot + try { + $rustupOutput = @(& rustup show active-toolchain 2>&1) + if ($LASTEXITCODE -ne 0) { + throw "anvil: rustup could not resolve the repository toolchain file:`n$($rustupOutput -join "`n")" + } + $active = $rustupOutput | Select-Object -Last 1 + if ([string]::IsNullOrWhiteSpace([string] $active)) { + throw 'anvil: rustup returned no active repository toolchain' } + } finally { + Pop-Location } -} - -if ($selectingFile) { + $resolved = [string] $active + $sourceMarker = $resolved.LastIndexOf(' (') + if ($sourceMarker -ge 0) { + $resolved = $resolved.Substring(0, $sourceMarker) + } + Write-Output ('+' + $resolved) return } @@ -7401,7 +7293,7 @@ $msrv = if ($values.ContainsKey('workspace.package')) { } else { throw 'anvil: no selecting rust-toolchain(.toml) and no root [workspace.package] or [package] rust-version; declare an MSRV or select a toolchain explicitly' } -Write-ExplicitToolchainArg $msrv +Write-Output ('+' + $msrv) }) ''') diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap index 24bc62a51..714f7b6d0 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap @@ -6162,6 +6162,8 @@ anvil-tool-pwsh-validate-prereqs: # ============================================================================ # Resolve or prepare the selected stable compiler without globally exporting it. +# Rustup owns toolchain-file parsing, installation, and file options. Anvil only +# handles explicit environment overrides and the root Cargo MSRV fallback. [script("pwsh", "-NoProfile")] _anvil-resolve-stable action="resolve": $ErrorActionPreference = 'Stop' @@ -6172,11 +6174,9 @@ _anvil-resolve-stable action="resolve": 'resolve', 'for-environment', 'install', - 'apply-file-options', 'validate-workspace-msrv', 'msrv', - 'install-msrv', - 'emit-file-options' + 'install-msrv' ) if ($action -notin $validActions) { Write-Error "_anvil-resolve-stable: unknown action '$action'" @@ -6185,109 +6185,11 @@ _anvil-resolve-stable action="resolve": $repoRoot = '{{justfile_directory()}}' - function Get-RootToolchainFilePath { - foreach ($name in @('rust-toolchain', 'rust-toolchain.toml')) { - $path = Join-Path $repoRoot $name - if (Test-Path -LiteralPath $path -PathType Leaf) { - return $path - } - } - return $null - } - - function Read-RootToolchainFile([string] $path) { - $content = Get-Content -LiteralPath $path -Raw - if ([string]::IsNullOrWhiteSpace($content)) { - throw "anvil: root toolchain file '$path' is empty; remove it or declare a toolchain configuration" - } - return $content - } - - function Get-SelectingToolchainFile { - $path = Get-RootToolchainFilePath - if ($null -eq $path) { - return $null - } - - $content = Read-RootToolchainFile $path - if ((Split-Path -Leaf $path) -eq 'rust-toolchain' -and $content.TrimStart() -notmatch '^\[') { - return [pscustomobject]@{ - Path = $path - Value = $content.Trim() - Selector = 'channel' - Source = 'file' - Installable = $false - } - } - - $inToolchain = $false - foreach ($line in $content -split '\r?\n') { - if ($line -match '^\s*\[\s*toolchain\s*\]\s*(?:#.*)?$') { - $inToolchain = $true - continue - } - if ($line -match '^\s*\[') { - $inToolchain = $false - continue - } - if ($inToolchain -and $line -match '^\s*["'']?(channel|path)["'']?\s*=\s*(["''])([^"'']+)\2\s*(?:#.*)?$') { - $value = $Matches[3] - if ($Matches[1] -eq 'path' -and -not [IO.Path]::IsPathRooted($value)) { - $value = [IO.Path]::GetFullPath((Join-Path $repoRoot $value)) - } - return [pscustomobject]@{ - Path = $path - Value = $value - Selector = $Matches[1] - Source = 'file' - Installable = $false - } - } - } - return $null - } - - function Get-ToolchainFileOptions { - $path = Get-RootToolchainFilePath - if ($null -eq $path) { - return $null - } - - $content = Read-RootToolchainFile $path - $section = [regex]::Match( - $content, - '(?ms)^\s*\[\s*toolchain\s*\]\s*(?:#.*)?$(.*?)(?=^\s*\[|\z)' + function Test-RootToolchainFile { + return ( + (Test-Path -LiteralPath (Join-Path $repoRoot 'rust-toolchain') -PathType Leaf) -or + (Test-Path -LiteralPath (Join-Path $repoRoot 'rust-toolchain.toml') -PathType Leaf) ) - if (-not $section.Success) { - return $null - } - - $body = $section.Groups[1].Value - $profileMatch = [regex]::Match( - $body, - '(?m)^\s*["'']?profile["'']?\s*=\s*(["''])([^"'']+)\1\s*(?:#.*)?$' - ) - $readArray = { - param([string] $name) - $arrayMatch = [regex]::Match( - $body, - "(?ms)^\s*[`"']?$name[`"']?\s*=\s*\[(.*?)\]" - ) - if (-not $arrayMatch.Success) { - return @() - } - return @( - [regex]::Matches($arrayMatch.Groups[1].Value, '(["''])([^"'']+)\1') | - ForEach-Object { $_.Groups[2].Value } - ) - } - - return [pscustomobject]@{ - Path = $path - Profile = if ($profileMatch.Success) { $profileMatch.Groups[2].Value } else { $null } - Components = @(& $readArray 'components') - Targets = @(& $readArray 'targets') - } } function Get-RootMsrv([switch] $AllowMissing) { @@ -6324,7 +6226,8 @@ _anvil-resolve-stable action="resolve": function Assert-WorkspaceMsrvCompatibility { $manifestPath = Join-Path $repoRoot 'Cargo.toml' - $metadataText = (& cargo metadata --manifest-path $manifestPath --format-version 1 --no-deps 2>&1 | Out-String) + $rootMsrv = Get-RootMsrv + $metadataText = (& cargo "+$rootMsrv" metadata --manifest-path $manifestPath --format-version 1 --no-deps 2>&1 | Out-String) if ($LASTEXITCODE -ne 0) { throw "anvil: cargo metadata failed while validating workspace MSRVs:`n$metadataText" } @@ -6339,7 +6242,6 @@ _anvil-resolve-stable action="resolve": throw "anvil: workspace packages without a resolved rust-version: $($missing -join ', '); declare one or select a toolchain explicitly" } - $rootMsrv = Get-RootMsrv $newer = @( $members | Where-Object { [version] ([string] $_.rust_version) -gt [version] $rootMsrv } | @@ -6353,7 +6255,7 @@ _anvil-resolve-stable action="resolve": function Assert-CompleteInternalToolchainConfiguration { if (-not [string]::IsNullOrWhiteSpace($env:ANVIL_MSRV_TOOLCHAIN) -and [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN) -and - $null -eq $fileSelection) { + -not (Test-RootToolchainFile)) { throw 'anvil: ANVIL_MSRV_TOOLCHAIN is set without RUSTUP_TOOLCHAIN and no repository toolchain file selects stable checks; set RUSTUP_TOOLCHAIN to the provisioned stable toolchain or unset ANVIL_MSRV_TOOLCHAIN' } } @@ -6368,67 +6270,30 @@ _anvil-resolve-stable action="resolve": return [pscustomobject]@{ Value = $env:ANVIL_MSRV_TOOLCHAIN Source = 'mapped-msrv' - Installable = $false Mapped = $true } } - return [pscustomobject]@{ Value = $msrv; Source = 'msrv'; Installable = $true; Mapped = $false } - } - - function Apply-ToolchainFileOptions([object] $toolchain) { - if ($toolchain.Source -eq 'file' -or $null -eq $toolchainFileOptions) { - return - } - foreach ($component in $toolchainFileOptions.Components) { - rustup component add --toolchain $toolchain.Value $component - if ($LASTEXITCODE -ne 0) { - Write-Warning "anvil: toolchain-file component '$component' is unavailable for '$($toolchain.Value)'; rustup would skip an unavailable file component" - } - } - foreach ($target in $toolchainFileOptions.Targets) { - rustup target add --toolchain $toolchain.Value $target - if ($LASTEXITCODE -ne 0) { - Write-Warning "anvil: toolchain-file target '$target' is unavailable for '$($toolchain.Value)'; rustup would skip an unavailable file target" - } - } + return [pscustomobject]@{ Value = $msrv; Source = 'msrv'; Mapped = $false } } function Install-Toolchain([object] $toolchain, [string] $label) { - if ($toolchain.Installable) { - $installed = rustup toolchain list + $installed = rustup toolchain list + if ($LASTEXITCODE -ne 0) { + throw 'anvil: rustup toolchain list failed' + } + if ($installed -notmatch "(?m)^$([regex]::Escape($toolchain.Value))(?:-|$)") { + Write-Host "anvil: installing $label toolchain '$($toolchain.Value)'" + rustup toolchain install $toolchain.Value --profile minimal if ($LASTEXITCODE -ne 0) { - throw 'anvil: rustup toolchain list failed' - } - if ($installed -notmatch "(?m)^$([regex]::Escape($toolchain.Value))(?:-|$)") { - $profile = if ($null -ne $toolchainFileOptions -and $toolchainFileOptions.Profile) { - $toolchainFileOptions.Profile - } else { - 'minimal' - } - Write-Host "anvil: installing $label toolchain '$($toolchain.Value)'" - rustup toolchain install $toolchain.Value --profile $profile - if ($LASTEXITCODE -ne 0) { - throw "anvil: failed to install $label toolchain '$($toolchain.Value)'" - } + throw "anvil: failed to install $label toolchain '$($toolchain.Value)'" } } - Apply-ToolchainFileOptions $toolchain - } - - $fileSelection = Get-SelectingToolchainFile - $toolchainFileOptions = Get-ToolchainFileOptions - - if ($action -eq 'emit-file-options') { - if ($null -ne $toolchainFileOptions) { - $toolchainFileOptions | ConvertTo-Json -Compress - } - exit 0 } if ($action -eq 'validate-workspace-msrv') { Assert-CompleteInternalToolchainConfiguration $skipWorkspaceMsrvValidation = -not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN) - if (-not $skipWorkspaceMsrvValidation -and $null -eq $fileSelection) { + if (-not $skipWorkspaceMsrvValidation -and -not (Test-RootToolchainFile)) { Assert-WorkspaceMsrvCompatibility } exit 0 @@ -6445,8 +6310,9 @@ _anvil-resolve-stable action="resolve": if ($LASTEXITCODE -ne 0) { throw "anvil: mapped MSRV toolchain '$($msrvSelection.Value)' is unavailable; provision ANVIL_MSRV_TOOLCHAIN or unset it to let Anvil install the declared public MSRV" } + } else { + Install-Toolchain $msrvSelection 'MSRV' } - Install-Toolchain $msrvSelection 'MSRV' } else { $msrvSelection.Value } @@ -6455,20 +6321,32 @@ _anvil-resolve-stable action="resolve": Assert-CompleteInternalToolchainConfiguration $selection = if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { - [pscustomobject]@{ Value = $env:RUSTUP_TOOLCHAIN; Source = 'environment'; Installable = $false } - } elseif ($null -ne $fileSelection) { - $fileSelection + [pscustomobject]@{ Value = $env:RUSTUP_TOOLCHAIN; Source = 'environment' } + } elseif (Test-RootToolchainFile) { + [pscustomobject]@{ Value = $null; Source = 'file' } } else { - [pscustomobject]@{ Value = Get-RootMsrv; Source = 'msrv'; Installable = $true } - } - - if ($action -eq 'apply-file-options') { - Apply-ToolchainFileOptions $selection - exit 0 + [pscustomobject]@{ Value = Get-RootMsrv; Source = 'msrv' } } if ($action -eq 'install') { - Install-Toolchain $selection 'stable' + if ($selection.Source -eq 'file') { + Push-Location $repoRoot + try { + & rustc --version + if ($LASTEXITCODE -ne 0) { + throw 'anvil: rustup could not provision the repository toolchain file' + } + } finally { + Pop-Location + } + } elseif ($selection.Source -eq 'environment') { + & cargo "+$($selection.Value)" --version *> $null + if ($LASTEXITCODE -ne 0) { + throw "anvil: selected stable toolchain '$($selection.Value)' is unavailable; provision RUSTUP_TOOLCHAIN or unset it" + } + } else { + Install-Toolchain $selection 'stable' + } exit 0 } @@ -6478,10 +6356,10 @@ _anvil-resolve-stable action="resolve": $selection.Value } -# Provision the selected stable toolchain and replay any applicable -# toolchain-file options. This is the shared prerequisite for setup paths that -# execute stable Cargo/Rust or install Cargo tools; Just deduplicates it within -# each setup invocation. +# Provision the selected stable toolchain. Repository toolchain files are +# processed natively by rustup. This is the shared prerequisite for setup paths +# that execute stable Cargo/Rust or install Cargo tools; Just deduplicates it +# within each setup invocation. [group("anvil-setup")] anvil-toolchain-stable-install: (_anvil-resolve-stable "install") @@ -6643,11 +6521,11 @@ _check-toolchain toolchain: exit 1 } -# _install-component: add a component to a toolchain. The toolchain is -# either the literal "default" (current rustup default) or a specific -# pinned toolchain string (which must already be installed -- the -# per-component setup recipes ensure this via a dependency on -# anvil--install). +# _install-component: add a component to a toolchain. The toolchain is either +# the literal "default" (Anvil's selected stable toolchain) or a specific pinned +# toolchain string (which must already be installed -- the per-component setup +# recipes ensure this via a dependency on anvil--install). Explicit +# stable selections target `rustup component add --toolchain`. # # Probe-first: if the component is already available, skip the # `rustup component add`. This keeps the recipe idempotent and robust @@ -6661,6 +6539,14 @@ _install-component toolchain component: $ErrorActionPreference = 'Stop' $toolchain = '{{toolchain}}' $component = '{{component}}' + $stableArgs = @() + $selectedStableToolchain = $null + if ($toolchain -eq 'default') { + $stableArgs = {{_anvil_stable_toolchain_args}} + if ($stableArgs.Count -eq 1) { + $selectedStableToolchain = ([string] $stableArgs[0]).Substring(1) + } + } # Probe whether the component is already present (see _check-component # for the rationale behind each detection method and the `+toolchain` @@ -6673,14 +6559,22 @@ _install-component toolchain component: } if ($null -ne $subcommand) { if ($toolchain -eq 'default') { - & cargo {{_anvil_stable_toolchain_args}} $subcommand --version *> $null + if ($null -ne $selectedStableToolchain) { + & cargo $stableArgs[0] $subcommand --version *> $null + } else { + & cargo $subcommand --version *> $null + } } else { & cargo "+$toolchain" $subcommand --version *> $null } $present = ($LASTEXITCODE -eq 0) } else { $sysroot = if ($toolchain -eq 'default') { - & rustc {{_anvil_stable_toolchain_args}} --print sysroot 2>$null + if ($null -ne $selectedStableToolchain) { + & rustc $stableArgs[0] --print sysroot 2>$null + } else { + & rustc --print sysroot 2>$null + } } else { & rustc "+$toolchain" --print sysroot 2>$null } @@ -6704,8 +6598,8 @@ _install-component toolchain component: } if ($toolchain -eq 'default') { - Write-Host "rustup component add $component (default toolchain)" - rustup component add $component + Write-Host "rustup component add --toolchain $selectedStableToolchain $component" + rustup component add --toolchain $selectedStableToolchain $component } else { Write-Host "rustup component add --toolchain $toolchain $component" rustup component add --toolchain $toolchain $component @@ -6733,6 +6627,14 @@ _check-component toolchain component: $ErrorActionPreference = 'Stop' $toolchain = '{{toolchain}}' $component = '{{component}}' + $stableArgs = @() + $selectedStableToolchain = $null + if ($toolchain -eq 'default') { + $stableArgs = {{_anvil_stable_toolchain_args}} + if ($stableArgs.Count -eq 1) { + $selectedStableToolchain = ([string] $stableArgs[0]).Substring(1) + } + } $subcommand = switch ($component) { 'clippy' { 'clippy' } 'rustfmt' { 'fmt' } @@ -6741,14 +6643,22 @@ _check-component toolchain component: } if ($null -ne $subcommand) { if ($toolchain -eq 'default') { - & cargo {{_anvil_stable_toolchain_args}} $subcommand --version *> $null + if ($null -ne $selectedStableToolchain) { + & cargo $stableArgs[0] $subcommand --version *> $null + } else { + & cargo $subcommand --version *> $null + } } else { & cargo "+$toolchain" $subcommand --version *> $null } $present = ($LASTEXITCODE -eq 0) } else { $sysroot = if ($toolchain -eq 'default') { - & rustc {{_anvil_stable_toolchain_args}} --print sysroot 2>$null + if ($null -ne $selectedStableToolchain) { + & rustc $stableArgs[0] --print sysroot 2>$null + } else { + & rustc --print sysroot 2>$null + } } else { & rustc "+$toolchain" --print sysroot 2>$null } @@ -6770,7 +6680,13 @@ _check-component toolchain component: } } if (-not $present) { - $cmd = if ($toolchain -eq 'default') { "rustup component add $component" } else { "rustup component add --toolchain $toolchain $component" } + $cmd = if ($null -ne $selectedStableToolchain) { + "rustup component add --toolchain $selectedStableToolchain $component" + } elseif ($toolchain -eq 'default') { + "rustup component add $component" + } else { + "rustup component add --toolchain $toolchain $component" + } Write-Error "anvil: component '$component' not installed on toolchain '$toolchain'. Run: $cmd" exit 1 } @@ -6799,10 +6715,9 @@ anvil-toolchain-nightly-external-types-validate-prereqs: (_check-toolchain rust_ # Rustup components # ============================================================================ # -# Default-toolchain components are installed via `rustup component add` -# (no toolchain spec). Nightly components depend on the toolchain -# being installed first (via the relevant anvil--install -# recipe) and then add the component. +# Stable components target Anvil's explicit selected toolchain. +# Nightly components depend on the toolchain being installed first (via the +# relevant anvil--install recipe) and then add the component. # Install the pinned `clippy` component for the default toolchain. [group("anvil-setup")] @@ -7111,65 +7026,42 @@ anvil-tool-cargo-udeps-validate-prereqs: (_check-tool "cargo-udeps" cargo_udeps_ # Rustup toolchains # ============================================================================ -# Stable commands interpolate this PowerShell array expression directly -# at each command site. Selection runs inside the existing PowerShell recipe -# process, so it needs no host-shell quoting or selector subprocess. +# Stable commands interpolate this PowerShell array expression directly at each +# command site. Rustup resolves repository toolchain files from the repository +# root; Anvil only reads Cargo.toml when it needs the root MSRV fallback. # `RUSTUP_TOOLCHAIN` is an input override only; Anvil never exports a resolved # value globally into unrelated recipes or helpers. [private] -_anvil_stable_toolchain_args := trim(''' -@(& { -$RepoRoot = (Get-Location).Path - +_anvil_stable_toolchain_args := trim("@(& {\n$RepoRoot = '" + replace(justfile_directory(), "'", "''") + "'\n\n" + ''' $ErrorActionPreference = 'Stop' Set-StrictMode -Version 3 -function Write-ExplicitToolchainArg([string] $Toolchain) { - Write-Output ('+' + $Toolchain) -} - if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { - Write-ExplicitToolchainArg $env:RUSTUP_TOOLCHAIN + Write-Output ('+' + $env:RUSTUP_TOOLCHAIN) return } -$toolchainFile = $null -foreach ($name in @('rust-toolchain', 'rust-toolchain.toml')) { - $candidate = Join-Path $RepoRoot $name - if (Test-Path -LiteralPath $candidate -PathType Leaf) { - $toolchainFile = $candidate - break - } -} - -$selectingFile = $false -if ($null -ne $toolchainFile) { - $content = Get-Content -LiteralPath $toolchainFile -Raw - if ([string]::IsNullOrWhiteSpace($content)) { - throw "anvil: root toolchain file '$toolchainFile' is empty; remove it or declare a toolchain configuration" - } - if ((Split-Path -Leaf $toolchainFile) -eq 'rust-toolchain' -and $content.TrimStart() -notmatch '^\[') { - $selectingFile = $true - } else { - $inToolchain = $false - foreach ($line in $content -split '\r?\n') { - if ($line -match '^\s*\[\s*toolchain\s*\]\s*(?:#.*)?$') { - $inToolchain = $true - continue - } - if ($line -match '^\s*\[') { - $inToolchain = $false - continue - } - if ($inToolchain -and $line -match '^\s*["'']?(channel|path)["'']?\s*=\s*(["''])([^"'']+)\2\s*(?:#.*)?$') { - $selectingFile = $true - break - } +if ((Test-Path -LiteralPath (Join-Path $RepoRoot 'rust-toolchain') -PathType Leaf) -or + (Test-Path -LiteralPath (Join-Path $RepoRoot 'rust-toolchain.toml') -PathType Leaf)) { + Push-Location $RepoRoot + try { + $rustupOutput = @(& rustup show active-toolchain 2>&1) + if ($LASTEXITCODE -ne 0) { + throw "anvil: rustup could not resolve the repository toolchain file:`n$($rustupOutput -join "`n")" + } + $active = $rustupOutput | Select-Object -Last 1 + if ([string]::IsNullOrWhiteSpace([string] $active)) { + throw 'anvil: rustup returned no active repository toolchain' } + } finally { + Pop-Location } -} - -if ($selectingFile) { + $resolved = [string] $active + $sourceMarker = $resolved.LastIndexOf(' (') + if ($sourceMarker -ge 0) { + $resolved = $resolved.Substring(0, $sourceMarker) + } + Write-Output ('+' + $resolved) return } @@ -7202,7 +7094,7 @@ $msrv = if ($values.ContainsKey('workspace.package')) { } else { throw 'anvil: no selecting rust-toolchain(.toml) and no root [workspace.package] or [package] rust-version; declare an MSRV or select a toolchain explicitly' } -Write-ExplicitToolchainArg $msrv +Write-Output ('+' + $msrv) }) ''') diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap index da4cf1800..2bee1d427 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap @@ -5018,6 +5018,8 @@ anvil-tool-pwsh-validate-prereqs: # ============================================================================ # Resolve or prepare the selected stable compiler without globally exporting it. +# Rustup owns toolchain-file parsing, installation, and file options. Anvil only +# handles explicit environment overrides and the root Cargo MSRV fallback. [script("pwsh", "-NoProfile")] _anvil-resolve-stable action="resolve": $ErrorActionPreference = 'Stop' @@ -5028,11 +5030,9 @@ _anvil-resolve-stable action="resolve": 'resolve', 'for-environment', 'install', - 'apply-file-options', 'validate-workspace-msrv', 'msrv', - 'install-msrv', - 'emit-file-options' + 'install-msrv' ) if ($action -notin $validActions) { Write-Error "_anvil-resolve-stable: unknown action '$action'" @@ -5041,109 +5041,11 @@ _anvil-resolve-stable action="resolve": $repoRoot = '{{justfile_directory()}}' - function Get-RootToolchainFilePath { - foreach ($name in @('rust-toolchain', 'rust-toolchain.toml')) { - $path = Join-Path $repoRoot $name - if (Test-Path -LiteralPath $path -PathType Leaf) { - return $path - } - } - return $null - } - - function Read-RootToolchainFile([string] $path) { - $content = Get-Content -LiteralPath $path -Raw - if ([string]::IsNullOrWhiteSpace($content)) { - throw "anvil: root toolchain file '$path' is empty; remove it or declare a toolchain configuration" - } - return $content - } - - function Get-SelectingToolchainFile { - $path = Get-RootToolchainFilePath - if ($null -eq $path) { - return $null - } - - $content = Read-RootToolchainFile $path - if ((Split-Path -Leaf $path) -eq 'rust-toolchain' -and $content.TrimStart() -notmatch '^\[') { - return [pscustomobject]@{ - Path = $path - Value = $content.Trim() - Selector = 'channel' - Source = 'file' - Installable = $false - } - } - - $inToolchain = $false - foreach ($line in $content -split '\r?\n') { - if ($line -match '^\s*\[\s*toolchain\s*\]\s*(?:#.*)?$') { - $inToolchain = $true - continue - } - if ($line -match '^\s*\[') { - $inToolchain = $false - continue - } - if ($inToolchain -and $line -match '^\s*["'']?(channel|path)["'']?\s*=\s*(["''])([^"'']+)\2\s*(?:#.*)?$') { - $value = $Matches[3] - if ($Matches[1] -eq 'path' -and -not [IO.Path]::IsPathRooted($value)) { - $value = [IO.Path]::GetFullPath((Join-Path $repoRoot $value)) - } - return [pscustomobject]@{ - Path = $path - Value = $value - Selector = $Matches[1] - Source = 'file' - Installable = $false - } - } - } - return $null - } - - function Get-ToolchainFileOptions { - $path = Get-RootToolchainFilePath - if ($null -eq $path) { - return $null - } - - $content = Read-RootToolchainFile $path - $section = [regex]::Match( - $content, - '(?ms)^\s*\[\s*toolchain\s*\]\s*(?:#.*)?$(.*?)(?=^\s*\[|\z)' + function Test-RootToolchainFile { + return ( + (Test-Path -LiteralPath (Join-Path $repoRoot 'rust-toolchain') -PathType Leaf) -or + (Test-Path -LiteralPath (Join-Path $repoRoot 'rust-toolchain.toml') -PathType Leaf) ) - if (-not $section.Success) { - return $null - } - - $body = $section.Groups[1].Value - $profileMatch = [regex]::Match( - $body, - '(?m)^\s*["'']?profile["'']?\s*=\s*(["''])([^"'']+)\1\s*(?:#.*)?$' - ) - $readArray = { - param([string] $name) - $arrayMatch = [regex]::Match( - $body, - "(?ms)^\s*[`"']?$name[`"']?\s*=\s*\[(.*?)\]" - ) - if (-not $arrayMatch.Success) { - return @() - } - return @( - [regex]::Matches($arrayMatch.Groups[1].Value, '(["''])([^"'']+)\1') | - ForEach-Object { $_.Groups[2].Value } - ) - } - - return [pscustomobject]@{ - Path = $path - Profile = if ($profileMatch.Success) { $profileMatch.Groups[2].Value } else { $null } - Components = @(& $readArray 'components') - Targets = @(& $readArray 'targets') - } } function Get-RootMsrv([switch] $AllowMissing) { @@ -5180,7 +5082,8 @@ _anvil-resolve-stable action="resolve": function Assert-WorkspaceMsrvCompatibility { $manifestPath = Join-Path $repoRoot 'Cargo.toml' - $metadataText = (& cargo metadata --manifest-path $manifestPath --format-version 1 --no-deps 2>&1 | Out-String) + $rootMsrv = Get-RootMsrv + $metadataText = (& cargo "+$rootMsrv" metadata --manifest-path $manifestPath --format-version 1 --no-deps 2>&1 | Out-String) if ($LASTEXITCODE -ne 0) { throw "anvil: cargo metadata failed while validating workspace MSRVs:`n$metadataText" } @@ -5195,7 +5098,6 @@ _anvil-resolve-stable action="resolve": throw "anvil: workspace packages without a resolved rust-version: $($missing -join ', '); declare one or select a toolchain explicitly" } - $rootMsrv = Get-RootMsrv $newer = @( $members | Where-Object { [version] ([string] $_.rust_version) -gt [version] $rootMsrv } | @@ -5209,7 +5111,7 @@ _anvil-resolve-stable action="resolve": function Assert-CompleteInternalToolchainConfiguration { if (-not [string]::IsNullOrWhiteSpace($env:ANVIL_MSRV_TOOLCHAIN) -and [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN) -and - $null -eq $fileSelection) { + -not (Test-RootToolchainFile)) { throw 'anvil: ANVIL_MSRV_TOOLCHAIN is set without RUSTUP_TOOLCHAIN and no repository toolchain file selects stable checks; set RUSTUP_TOOLCHAIN to the provisioned stable toolchain or unset ANVIL_MSRV_TOOLCHAIN' } } @@ -5224,67 +5126,30 @@ _anvil-resolve-stable action="resolve": return [pscustomobject]@{ Value = $env:ANVIL_MSRV_TOOLCHAIN Source = 'mapped-msrv' - Installable = $false Mapped = $true } } - return [pscustomobject]@{ Value = $msrv; Source = 'msrv'; Installable = $true; Mapped = $false } - } - - function Apply-ToolchainFileOptions([object] $toolchain) { - if ($toolchain.Source -eq 'file' -or $null -eq $toolchainFileOptions) { - return - } - foreach ($component in $toolchainFileOptions.Components) { - rustup component add --toolchain $toolchain.Value $component - if ($LASTEXITCODE -ne 0) { - Write-Warning "anvil: toolchain-file component '$component' is unavailable for '$($toolchain.Value)'; rustup would skip an unavailable file component" - } - } - foreach ($target in $toolchainFileOptions.Targets) { - rustup target add --toolchain $toolchain.Value $target - if ($LASTEXITCODE -ne 0) { - Write-Warning "anvil: toolchain-file target '$target' is unavailable for '$($toolchain.Value)'; rustup would skip an unavailable file target" - } - } + return [pscustomobject]@{ Value = $msrv; Source = 'msrv'; Mapped = $false } } function Install-Toolchain([object] $toolchain, [string] $label) { - if ($toolchain.Installable) { - $installed = rustup toolchain list + $installed = rustup toolchain list + if ($LASTEXITCODE -ne 0) { + throw 'anvil: rustup toolchain list failed' + } + if ($installed -notmatch "(?m)^$([regex]::Escape($toolchain.Value))(?:-|$)") { + Write-Host "anvil: installing $label toolchain '$($toolchain.Value)'" + rustup toolchain install $toolchain.Value --profile minimal if ($LASTEXITCODE -ne 0) { - throw 'anvil: rustup toolchain list failed' - } - if ($installed -notmatch "(?m)^$([regex]::Escape($toolchain.Value))(?:-|$)") { - $profile = if ($null -ne $toolchainFileOptions -and $toolchainFileOptions.Profile) { - $toolchainFileOptions.Profile - } else { - 'minimal' - } - Write-Host "anvil: installing $label toolchain '$($toolchain.Value)'" - rustup toolchain install $toolchain.Value --profile $profile - if ($LASTEXITCODE -ne 0) { - throw "anvil: failed to install $label toolchain '$($toolchain.Value)'" - } + throw "anvil: failed to install $label toolchain '$($toolchain.Value)'" } } - Apply-ToolchainFileOptions $toolchain - } - - $fileSelection = Get-SelectingToolchainFile - $toolchainFileOptions = Get-ToolchainFileOptions - - if ($action -eq 'emit-file-options') { - if ($null -ne $toolchainFileOptions) { - $toolchainFileOptions | ConvertTo-Json -Compress - } - exit 0 } if ($action -eq 'validate-workspace-msrv') { Assert-CompleteInternalToolchainConfiguration $skipWorkspaceMsrvValidation = -not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN) - if (-not $skipWorkspaceMsrvValidation -and $null -eq $fileSelection) { + if (-not $skipWorkspaceMsrvValidation -and -not (Test-RootToolchainFile)) { Assert-WorkspaceMsrvCompatibility } exit 0 @@ -5301,8 +5166,9 @@ _anvil-resolve-stable action="resolve": if ($LASTEXITCODE -ne 0) { throw "anvil: mapped MSRV toolchain '$($msrvSelection.Value)' is unavailable; provision ANVIL_MSRV_TOOLCHAIN or unset it to let Anvil install the declared public MSRV" } + } else { + Install-Toolchain $msrvSelection 'MSRV' } - Install-Toolchain $msrvSelection 'MSRV' } else { $msrvSelection.Value } @@ -5311,20 +5177,32 @@ _anvil-resolve-stable action="resolve": Assert-CompleteInternalToolchainConfiguration $selection = if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { - [pscustomobject]@{ Value = $env:RUSTUP_TOOLCHAIN; Source = 'environment'; Installable = $false } - } elseif ($null -ne $fileSelection) { - $fileSelection + [pscustomobject]@{ Value = $env:RUSTUP_TOOLCHAIN; Source = 'environment' } + } elseif (Test-RootToolchainFile) { + [pscustomobject]@{ Value = $null; Source = 'file' } } else { - [pscustomobject]@{ Value = Get-RootMsrv; Source = 'msrv'; Installable = $true } - } - - if ($action -eq 'apply-file-options') { - Apply-ToolchainFileOptions $selection - exit 0 + [pscustomobject]@{ Value = Get-RootMsrv; Source = 'msrv' } } if ($action -eq 'install') { - Install-Toolchain $selection 'stable' + if ($selection.Source -eq 'file') { + Push-Location $repoRoot + try { + & rustc --version + if ($LASTEXITCODE -ne 0) { + throw 'anvil: rustup could not provision the repository toolchain file' + } + } finally { + Pop-Location + } + } elseif ($selection.Source -eq 'environment') { + & cargo "+$($selection.Value)" --version *> $null + if ($LASTEXITCODE -ne 0) { + throw "anvil: selected stable toolchain '$($selection.Value)' is unavailable; provision RUSTUP_TOOLCHAIN or unset it" + } + } else { + Install-Toolchain $selection 'stable' + } exit 0 } @@ -5334,10 +5212,10 @@ _anvil-resolve-stable action="resolve": $selection.Value } -# Provision the selected stable toolchain and replay any applicable -# toolchain-file options. This is the shared prerequisite for setup paths that -# execute stable Cargo/Rust or install Cargo tools; Just deduplicates it within -# each setup invocation. +# Provision the selected stable toolchain. Repository toolchain files are +# processed natively by rustup. This is the shared prerequisite for setup paths +# that execute stable Cargo/Rust or install Cargo tools; Just deduplicates it +# within each setup invocation. [group("anvil-setup")] anvil-toolchain-stable-install: (_anvil-resolve-stable "install") @@ -5499,11 +5377,11 @@ _check-toolchain toolchain: exit 1 } -# _install-component: add a component to a toolchain. The toolchain is -# either the literal "default" (current rustup default) or a specific -# pinned toolchain string (which must already be installed -- the -# per-component setup recipes ensure this via a dependency on -# anvil--install). +# _install-component: add a component to a toolchain. The toolchain is either +# the literal "default" (Anvil's selected stable toolchain) or a specific pinned +# toolchain string (which must already be installed -- the per-component setup +# recipes ensure this via a dependency on anvil--install). Explicit +# stable selections target `rustup component add --toolchain`. # # Probe-first: if the component is already available, skip the # `rustup component add`. This keeps the recipe idempotent and robust @@ -5517,6 +5395,14 @@ _install-component toolchain component: $ErrorActionPreference = 'Stop' $toolchain = '{{toolchain}}' $component = '{{component}}' + $stableArgs = @() + $selectedStableToolchain = $null + if ($toolchain -eq 'default') { + $stableArgs = {{_anvil_stable_toolchain_args}} + if ($stableArgs.Count -eq 1) { + $selectedStableToolchain = ([string] $stableArgs[0]).Substring(1) + } + } # Probe whether the component is already present (see _check-component # for the rationale behind each detection method and the `+toolchain` @@ -5529,14 +5415,22 @@ _install-component toolchain component: } if ($null -ne $subcommand) { if ($toolchain -eq 'default') { - & cargo {{_anvil_stable_toolchain_args}} $subcommand --version *> $null + if ($null -ne $selectedStableToolchain) { + & cargo $stableArgs[0] $subcommand --version *> $null + } else { + & cargo $subcommand --version *> $null + } } else { & cargo "+$toolchain" $subcommand --version *> $null } $present = ($LASTEXITCODE -eq 0) } else { $sysroot = if ($toolchain -eq 'default') { - & rustc {{_anvil_stable_toolchain_args}} --print sysroot 2>$null + if ($null -ne $selectedStableToolchain) { + & rustc $stableArgs[0] --print sysroot 2>$null + } else { + & rustc --print sysroot 2>$null + } } else { & rustc "+$toolchain" --print sysroot 2>$null } @@ -5560,8 +5454,8 @@ _install-component toolchain component: } if ($toolchain -eq 'default') { - Write-Host "rustup component add $component (default toolchain)" - rustup component add $component + Write-Host "rustup component add --toolchain $selectedStableToolchain $component" + rustup component add --toolchain $selectedStableToolchain $component } else { Write-Host "rustup component add --toolchain $toolchain $component" rustup component add --toolchain $toolchain $component @@ -5589,6 +5483,14 @@ _check-component toolchain component: $ErrorActionPreference = 'Stop' $toolchain = '{{toolchain}}' $component = '{{component}}' + $stableArgs = @() + $selectedStableToolchain = $null + if ($toolchain -eq 'default') { + $stableArgs = {{_anvil_stable_toolchain_args}} + if ($stableArgs.Count -eq 1) { + $selectedStableToolchain = ([string] $stableArgs[0]).Substring(1) + } + } $subcommand = switch ($component) { 'clippy' { 'clippy' } 'rustfmt' { 'fmt' } @@ -5597,14 +5499,22 @@ _check-component toolchain component: } if ($null -ne $subcommand) { if ($toolchain -eq 'default') { - & cargo {{_anvil_stable_toolchain_args}} $subcommand --version *> $null + if ($null -ne $selectedStableToolchain) { + & cargo $stableArgs[0] $subcommand --version *> $null + } else { + & cargo $subcommand --version *> $null + } } else { & cargo "+$toolchain" $subcommand --version *> $null } $present = ($LASTEXITCODE -eq 0) } else { $sysroot = if ($toolchain -eq 'default') { - & rustc {{_anvil_stable_toolchain_args}} --print sysroot 2>$null + if ($null -ne $selectedStableToolchain) { + & rustc $stableArgs[0] --print sysroot 2>$null + } else { + & rustc --print sysroot 2>$null + } } else { & rustc "+$toolchain" --print sysroot 2>$null } @@ -5626,7 +5536,13 @@ _check-component toolchain component: } } if (-not $present) { - $cmd = if ($toolchain -eq 'default') { "rustup component add $component" } else { "rustup component add --toolchain $toolchain $component" } + $cmd = if ($null -ne $selectedStableToolchain) { + "rustup component add --toolchain $selectedStableToolchain $component" + } elseif ($toolchain -eq 'default') { + "rustup component add $component" + } else { + "rustup component add --toolchain $toolchain $component" + } Write-Error "anvil: component '$component' not installed on toolchain '$toolchain'. Run: $cmd" exit 1 } @@ -5655,10 +5571,9 @@ anvil-toolchain-nightly-external-types-validate-prereqs: (_check-toolchain rust_ # Rustup components # ============================================================================ # -# Default-toolchain components are installed via `rustup component add` -# (no toolchain spec). Nightly components depend on the toolchain -# being installed first (via the relevant anvil--install -# recipe) and then add the component. +# Stable components target Anvil's explicit selected toolchain. +# Nightly components depend on the toolchain being installed first (via the +# relevant anvil--install recipe) and then add the component. # Install the pinned `clippy` component for the default toolchain. [group("anvil-setup")] @@ -5967,65 +5882,42 @@ anvil-tool-cargo-udeps-validate-prereqs: (_check-tool "cargo-udeps" cargo_udeps_ # Rustup toolchains # ============================================================================ -# Stable commands interpolate this PowerShell array expression directly -# at each command site. Selection runs inside the existing PowerShell recipe -# process, so it needs no host-shell quoting or selector subprocess. +# Stable commands interpolate this PowerShell array expression directly at each +# command site. Rustup resolves repository toolchain files from the repository +# root; Anvil only reads Cargo.toml when it needs the root MSRV fallback. # `RUSTUP_TOOLCHAIN` is an input override only; Anvil never exports a resolved # value globally into unrelated recipes or helpers. [private] -_anvil_stable_toolchain_args := trim(''' -@(& { -$RepoRoot = (Get-Location).Path - +_anvil_stable_toolchain_args := trim("@(& {\n$RepoRoot = '" + replace(justfile_directory(), "'", "''") + "'\n\n" + ''' $ErrorActionPreference = 'Stop' Set-StrictMode -Version 3 -function Write-ExplicitToolchainArg([string] $Toolchain) { - Write-Output ('+' + $Toolchain) -} - if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { - Write-ExplicitToolchainArg $env:RUSTUP_TOOLCHAIN + Write-Output ('+' + $env:RUSTUP_TOOLCHAIN) return } -$toolchainFile = $null -foreach ($name in @('rust-toolchain', 'rust-toolchain.toml')) { - $candidate = Join-Path $RepoRoot $name - if (Test-Path -LiteralPath $candidate -PathType Leaf) { - $toolchainFile = $candidate - break - } -} - -$selectingFile = $false -if ($null -ne $toolchainFile) { - $content = Get-Content -LiteralPath $toolchainFile -Raw - if ([string]::IsNullOrWhiteSpace($content)) { - throw "anvil: root toolchain file '$toolchainFile' is empty; remove it or declare a toolchain configuration" - } - if ((Split-Path -Leaf $toolchainFile) -eq 'rust-toolchain' -and $content.TrimStart() -notmatch '^\[') { - $selectingFile = $true - } else { - $inToolchain = $false - foreach ($line in $content -split '\r?\n') { - if ($line -match '^\s*\[\s*toolchain\s*\]\s*(?:#.*)?$') { - $inToolchain = $true - continue - } - if ($line -match '^\s*\[') { - $inToolchain = $false - continue - } - if ($inToolchain -and $line -match '^\s*["'']?(channel|path)["'']?\s*=\s*(["''])([^"'']+)\2\s*(?:#.*)?$') { - $selectingFile = $true - break - } +if ((Test-Path -LiteralPath (Join-Path $RepoRoot 'rust-toolchain') -PathType Leaf) -or + (Test-Path -LiteralPath (Join-Path $RepoRoot 'rust-toolchain.toml') -PathType Leaf)) { + Push-Location $RepoRoot + try { + $rustupOutput = @(& rustup show active-toolchain 2>&1) + if ($LASTEXITCODE -ne 0) { + throw "anvil: rustup could not resolve the repository toolchain file:`n$($rustupOutput -join "`n")" + } + $active = $rustupOutput | Select-Object -Last 1 + if ([string]::IsNullOrWhiteSpace([string] $active)) { + throw 'anvil: rustup returned no active repository toolchain' } + } finally { + Pop-Location } -} - -if ($selectingFile) { + $resolved = [string] $active + $sourceMarker = $resolved.LastIndexOf(' (') + if ($sourceMarker -ge 0) { + $resolved = $resolved.Substring(0, $sourceMarker) + } + Write-Output ('+' + $resolved) return } @@ -6058,7 +5950,7 @@ $msrv = if ($values.ContainsKey('workspace.package')) { } else { throw 'anvil: no selecting rust-toolchain(.toml) and no root [workspace.package] or [package] rust-version; declare an MSRV or select a toolchain explicitly' } -Write-ExplicitToolchainArg $msrv +Write-Output ('+' + $msrv) }) ''') diff --git a/justfiles/anvil/tools.just b/justfiles/anvil/tools.just index 31f8537d2..0d6e45f92 100644 --- a/justfiles/anvil/tools.just +++ b/justfiles/anvil/tools.just @@ -173,6 +173,8 @@ anvil-tool-pwsh-validate-prereqs: # ============================================================================ # Resolve or prepare the selected stable compiler without globally exporting it. +# Rustup owns toolchain-file parsing, installation, and file options. Anvil only +# handles explicit environment overrides and the root Cargo MSRV fallback. [script("pwsh", "-NoProfile")] _anvil-resolve-stable action="resolve": $ErrorActionPreference = 'Stop' @@ -183,11 +185,9 @@ _anvil-resolve-stable action="resolve": 'resolve', 'for-environment', 'install', - 'apply-file-options', 'validate-workspace-msrv', 'msrv', - 'install-msrv', - 'emit-file-options' + 'install-msrv' ) if ($action -notin $validActions) { Write-Error "_anvil-resolve-stable: unknown action '$action'" @@ -196,109 +196,11 @@ _anvil-resolve-stable action="resolve": $repoRoot = '{{justfile_directory()}}' - function Get-RootToolchainFilePath { - foreach ($name in @('rust-toolchain', 'rust-toolchain.toml')) { - $path = Join-Path $repoRoot $name - if (Test-Path -LiteralPath $path -PathType Leaf) { - return $path - } - } - return $null - } - - function Read-RootToolchainFile([string] $path) { - $content = Get-Content -LiteralPath $path -Raw - if ([string]::IsNullOrWhiteSpace($content)) { - throw "anvil: root toolchain file '$path' is empty; remove it or declare a toolchain configuration" - } - return $content - } - - function Get-SelectingToolchainFile { - $path = Get-RootToolchainFilePath - if ($null -eq $path) { - return $null - } - - $content = Read-RootToolchainFile $path - if ((Split-Path -Leaf $path) -eq 'rust-toolchain' -and $content.TrimStart() -notmatch '^\[') { - return [pscustomobject]@{ - Path = $path - Value = $content.Trim() - Selector = 'channel' - Source = 'file' - Installable = $false - } - } - - $inToolchain = $false - foreach ($line in $content -split '\r?\n') { - if ($line -match '^\s*\[\s*toolchain\s*\]\s*(?:#.*)?$') { - $inToolchain = $true - continue - } - if ($line -match '^\s*\[') { - $inToolchain = $false - continue - } - if ($inToolchain -and $line -match '^\s*["'']?(channel|path)["'']?\s*=\s*(["''])([^"'']+)\2\s*(?:#.*)?$') { - $value = $Matches[3] - if ($Matches[1] -eq 'path' -and -not [IO.Path]::IsPathRooted($value)) { - $value = [IO.Path]::GetFullPath((Join-Path $repoRoot $value)) - } - return [pscustomobject]@{ - Path = $path - Value = $value - Selector = $Matches[1] - Source = 'file' - Installable = $false - } - } - } - return $null - } - - function Get-ToolchainFileOptions { - $path = Get-RootToolchainFilePath - if ($null -eq $path) { - return $null - } - - $content = Read-RootToolchainFile $path - $section = [regex]::Match( - $content, - '(?ms)^\s*\[\s*toolchain\s*\]\s*(?:#.*)?$(.*?)(?=^\s*\[|\z)' - ) - if (-not $section.Success) { - return $null - } - - $body = $section.Groups[1].Value - $profileMatch = [regex]::Match( - $body, - '(?m)^\s*["'']?profile["'']?\s*=\s*(["''])([^"'']+)\1\s*(?:#.*)?$' + function Test-RootToolchainFile { + return ( + (Test-Path -LiteralPath (Join-Path $repoRoot 'rust-toolchain') -PathType Leaf) -or + (Test-Path -LiteralPath (Join-Path $repoRoot 'rust-toolchain.toml') -PathType Leaf) ) - $readArray = { - param([string] $name) - $arrayMatch = [regex]::Match( - $body, - "(?ms)^\s*[`"']?$name[`"']?\s*=\s*\[(.*?)\]" - ) - if (-not $arrayMatch.Success) { - return @() - } - return @( - [regex]::Matches($arrayMatch.Groups[1].Value, '(["''])([^"'']+)\1') | - ForEach-Object { $_.Groups[2].Value } - ) - } - - return [pscustomobject]@{ - Path = $path - Profile = if ($profileMatch.Success) { $profileMatch.Groups[2].Value } else { $null } - Components = @(& $readArray 'components') - Targets = @(& $readArray 'targets') - } } function Get-RootMsrv([switch] $AllowMissing) { @@ -335,7 +237,8 @@ _anvil-resolve-stable action="resolve": function Assert-WorkspaceMsrvCompatibility { $manifestPath = Join-Path $repoRoot 'Cargo.toml' - $metadataText = (& cargo metadata --manifest-path $manifestPath --format-version 1 --no-deps 2>&1 | Out-String) + $rootMsrv = Get-RootMsrv + $metadataText = (& cargo "+$rootMsrv" metadata --manifest-path $manifestPath --format-version 1 --no-deps 2>&1 | Out-String) if ($LASTEXITCODE -ne 0) { throw "anvil: cargo metadata failed while validating workspace MSRVs:`n$metadataText" } @@ -350,7 +253,6 @@ _anvil-resolve-stable action="resolve": throw "anvil: workspace packages without a resolved rust-version: $($missing -join ', '); declare one or select a toolchain explicitly" } - $rootMsrv = Get-RootMsrv $newer = @( $members | Where-Object { [version] ([string] $_.rust_version) -gt [version] $rootMsrv } | @@ -364,7 +266,7 @@ _anvil-resolve-stable action="resolve": function Assert-CompleteInternalToolchainConfiguration { if (-not [string]::IsNullOrWhiteSpace($env:ANVIL_MSRV_TOOLCHAIN) -and [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN) -and - $null -eq $fileSelection) { + -not (Test-RootToolchainFile)) { throw 'anvil: ANVIL_MSRV_TOOLCHAIN is set without RUSTUP_TOOLCHAIN and no repository toolchain file selects stable checks; set RUSTUP_TOOLCHAIN to the provisioned stable toolchain or unset ANVIL_MSRV_TOOLCHAIN' } } @@ -379,67 +281,30 @@ _anvil-resolve-stable action="resolve": return [pscustomobject]@{ Value = $env:ANVIL_MSRV_TOOLCHAIN Source = 'mapped-msrv' - Installable = $false Mapped = $true } } - return [pscustomobject]@{ Value = $msrv; Source = 'msrv'; Installable = $true; Mapped = $false } - } - - function Apply-ToolchainFileOptions([object] $toolchain) { - if ($toolchain.Source -eq 'file' -or $null -eq $toolchainFileOptions) { - return - } - foreach ($component in $toolchainFileOptions.Components) { - rustup component add --toolchain $toolchain.Value $component - if ($LASTEXITCODE -ne 0) { - Write-Warning "anvil: toolchain-file component '$component' is unavailable for '$($toolchain.Value)'; rustup would skip an unavailable file component" - } - } - foreach ($target in $toolchainFileOptions.Targets) { - rustup target add --toolchain $toolchain.Value $target - if ($LASTEXITCODE -ne 0) { - Write-Warning "anvil: toolchain-file target '$target' is unavailable for '$($toolchain.Value)'; rustup would skip an unavailable file target" - } - } + return [pscustomobject]@{ Value = $msrv; Source = 'msrv'; Mapped = $false } } function Install-Toolchain([object] $toolchain, [string] $label) { - if ($toolchain.Installable) { - $installed = rustup toolchain list + $installed = rustup toolchain list + if ($LASTEXITCODE -ne 0) { + throw 'anvil: rustup toolchain list failed' + } + if ($installed -notmatch "(?m)^$([regex]::Escape($toolchain.Value))(?:-|$)") { + Write-Host "anvil: installing $label toolchain '$($toolchain.Value)'" + rustup toolchain install $toolchain.Value --profile minimal if ($LASTEXITCODE -ne 0) { - throw 'anvil: rustup toolchain list failed' - } - if ($installed -notmatch "(?m)^$([regex]::Escape($toolchain.Value))(?:-|$)") { - $profile = if ($null -ne $toolchainFileOptions -and $toolchainFileOptions.Profile) { - $toolchainFileOptions.Profile - } else { - 'minimal' - } - Write-Host "anvil: installing $label toolchain '$($toolchain.Value)'" - rustup toolchain install $toolchain.Value --profile $profile - if ($LASTEXITCODE -ne 0) { - throw "anvil: failed to install $label toolchain '$($toolchain.Value)'" - } + throw "anvil: failed to install $label toolchain '$($toolchain.Value)'" } } - Apply-ToolchainFileOptions $toolchain - } - - $fileSelection = Get-SelectingToolchainFile - $toolchainFileOptions = Get-ToolchainFileOptions - - if ($action -eq 'emit-file-options') { - if ($null -ne $toolchainFileOptions) { - $toolchainFileOptions | ConvertTo-Json -Compress - } - exit 0 } if ($action -eq 'validate-workspace-msrv') { Assert-CompleteInternalToolchainConfiguration $skipWorkspaceMsrvValidation = -not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN) - if (-not $skipWorkspaceMsrvValidation -and $null -eq $fileSelection) { + if (-not $skipWorkspaceMsrvValidation -and -not (Test-RootToolchainFile)) { Assert-WorkspaceMsrvCompatibility } exit 0 @@ -456,8 +321,9 @@ _anvil-resolve-stable action="resolve": if ($LASTEXITCODE -ne 0) { throw "anvil: mapped MSRV toolchain '$($msrvSelection.Value)' is unavailable; provision ANVIL_MSRV_TOOLCHAIN or unset it to let Anvil install the declared public MSRV" } + } else { + Install-Toolchain $msrvSelection 'MSRV' } - Install-Toolchain $msrvSelection 'MSRV' } else { $msrvSelection.Value } @@ -466,20 +332,32 @@ _anvil-resolve-stable action="resolve": Assert-CompleteInternalToolchainConfiguration $selection = if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { - [pscustomobject]@{ Value = $env:RUSTUP_TOOLCHAIN; Source = 'environment'; Installable = $false } - } elseif ($null -ne $fileSelection) { - $fileSelection + [pscustomobject]@{ Value = $env:RUSTUP_TOOLCHAIN; Source = 'environment' } + } elseif (Test-RootToolchainFile) { + [pscustomobject]@{ Value = $null; Source = 'file' } } else { - [pscustomobject]@{ Value = Get-RootMsrv; Source = 'msrv'; Installable = $true } - } - - if ($action -eq 'apply-file-options') { - Apply-ToolchainFileOptions $selection - exit 0 + [pscustomobject]@{ Value = Get-RootMsrv; Source = 'msrv' } } if ($action -eq 'install') { - Install-Toolchain $selection 'stable' + if ($selection.Source -eq 'file') { + Push-Location $repoRoot + try { + & rustc --version + if ($LASTEXITCODE -ne 0) { + throw 'anvil: rustup could not provision the repository toolchain file' + } + } finally { + Pop-Location + } + } elseif ($selection.Source -eq 'environment') { + & cargo "+$($selection.Value)" --version *> $null + if ($LASTEXITCODE -ne 0) { + throw "anvil: selected stable toolchain '$($selection.Value)' is unavailable; provision RUSTUP_TOOLCHAIN or unset it" + } + } else { + Install-Toolchain $selection 'stable' + } exit 0 } @@ -489,10 +367,10 @@ _anvil-resolve-stable action="resolve": $selection.Value } -# Provision the selected stable toolchain and replay any applicable -# toolchain-file options. This is the shared prerequisite for setup paths that -# execute stable Cargo/Rust or install Cargo tools; Just deduplicates it within -# each setup invocation. +# Provision the selected stable toolchain. Repository toolchain files are +# processed natively by rustup. This is the shared prerequisite for setup paths +# that execute stable Cargo/Rust or install Cargo tools; Just deduplicates it +# within each setup invocation. [group("anvil-setup")] anvil-toolchain-stable-install: (_anvil-resolve-stable "install") @@ -654,11 +532,11 @@ _check-toolchain toolchain: exit 1 } -# _install-component: add a component to a toolchain. The toolchain is -# either the literal "default" (current rustup default) or a specific -# pinned toolchain string (which must already be installed -- the -# per-component setup recipes ensure this via a dependency on -# anvil--install). +# _install-component: add a component to a toolchain. The toolchain is either +# the literal "default" (Anvil's selected stable toolchain) or a specific pinned +# toolchain string (which must already be installed -- the per-component setup +# recipes ensure this via a dependency on anvil--install). Explicit +# stable selections target `rustup component add --toolchain`. # # Probe-first: if the component is already available, skip the # `rustup component add`. This keeps the recipe idempotent and robust @@ -672,6 +550,14 @@ _install-component toolchain component: $ErrorActionPreference = 'Stop' $toolchain = '{{toolchain}}' $component = '{{component}}' + $stableArgs = @() + $selectedStableToolchain = $null + if ($toolchain -eq 'default') { + $stableArgs = {{_anvil_stable_toolchain_args}} + if ($stableArgs.Count -eq 1) { + $selectedStableToolchain = ([string] $stableArgs[0]).Substring(1) + } + } # Probe whether the component is already present (see _check-component # for the rationale behind each detection method and the `+toolchain` @@ -684,14 +570,22 @@ _install-component toolchain component: } if ($null -ne $subcommand) { if ($toolchain -eq 'default') { - & cargo {{_anvil_stable_toolchain_args}} $subcommand --version *> $null + if ($null -ne $selectedStableToolchain) { + & cargo $stableArgs[0] $subcommand --version *> $null + } else { + & cargo $subcommand --version *> $null + } } else { & cargo "+$toolchain" $subcommand --version *> $null } $present = ($LASTEXITCODE -eq 0) } else { $sysroot = if ($toolchain -eq 'default') { - & rustc {{_anvil_stable_toolchain_args}} --print sysroot 2>$null + if ($null -ne $selectedStableToolchain) { + & rustc $stableArgs[0] --print sysroot 2>$null + } else { + & rustc --print sysroot 2>$null + } } else { & rustc "+$toolchain" --print sysroot 2>$null } @@ -715,8 +609,8 @@ _install-component toolchain component: } if ($toolchain -eq 'default') { - Write-Host "rustup component add $component (default toolchain)" - rustup component add $component + Write-Host "rustup component add --toolchain $selectedStableToolchain $component" + rustup component add --toolchain $selectedStableToolchain $component } else { Write-Host "rustup component add --toolchain $toolchain $component" rustup component add --toolchain $toolchain $component @@ -744,6 +638,14 @@ _check-component toolchain component: $ErrorActionPreference = 'Stop' $toolchain = '{{toolchain}}' $component = '{{component}}' + $stableArgs = @() + $selectedStableToolchain = $null + if ($toolchain -eq 'default') { + $stableArgs = {{_anvil_stable_toolchain_args}} + if ($stableArgs.Count -eq 1) { + $selectedStableToolchain = ([string] $stableArgs[0]).Substring(1) + } + } $subcommand = switch ($component) { 'clippy' { 'clippy' } 'rustfmt' { 'fmt' } @@ -752,14 +654,22 @@ _check-component toolchain component: } if ($null -ne $subcommand) { if ($toolchain -eq 'default') { - & cargo {{_anvil_stable_toolchain_args}} $subcommand --version *> $null + if ($null -ne $selectedStableToolchain) { + & cargo $stableArgs[0] $subcommand --version *> $null + } else { + & cargo $subcommand --version *> $null + } } else { & cargo "+$toolchain" $subcommand --version *> $null } $present = ($LASTEXITCODE -eq 0) } else { $sysroot = if ($toolchain -eq 'default') { - & rustc {{_anvil_stable_toolchain_args}} --print sysroot 2>$null + if ($null -ne $selectedStableToolchain) { + & rustc $stableArgs[0] --print sysroot 2>$null + } else { + & rustc --print sysroot 2>$null + } } else { & rustc "+$toolchain" --print sysroot 2>$null } @@ -781,7 +691,13 @@ _check-component toolchain component: } } if (-not $present) { - $cmd = if ($toolchain -eq 'default') { "rustup component add $component" } else { "rustup component add --toolchain $toolchain $component" } + $cmd = if ($null -ne $selectedStableToolchain) { + "rustup component add --toolchain $selectedStableToolchain $component" + } elseif ($toolchain -eq 'default') { + "rustup component add $component" + } else { + "rustup component add --toolchain $toolchain $component" + } Write-Error "anvil: component '$component' not installed on toolchain '$toolchain'. Run: $cmd" exit 1 } @@ -810,10 +726,9 @@ anvil-toolchain-nightly-external-types-validate-prereqs: (_check-toolchain rust_ # Rustup components # ============================================================================ # -# Default-toolchain components are installed via `rustup component add` -# (no toolchain spec). Nightly components depend on the toolchain -# being installed first (via the relevant anvil--install -# recipe) and then add the component. +# Stable components target Anvil's explicit selected toolchain. +# Nightly components depend on the toolchain being installed first (via the +# relevant anvil--install recipe) and then add the component. # Install the pinned `clippy` component for the default toolchain. [group("anvil-setup")] diff --git a/justfiles/anvil/versions.just b/justfiles/anvil/versions.just index e4d7ef5a7..1641ee8a3 100644 --- a/justfiles/anvil/versions.just +++ b/justfiles/anvil/versions.just @@ -31,65 +31,42 @@ # Rustup toolchains # ============================================================================ -# Stable commands interpolate this PowerShell array expression directly -# at each command site. Selection runs inside the existing PowerShell recipe -# process, so it needs no host-shell quoting or selector subprocess. +# Stable commands interpolate this PowerShell array expression directly at each +# command site. Rustup resolves repository toolchain files from the repository +# root; Anvil only reads Cargo.toml when it needs the root MSRV fallback. # `RUSTUP_TOOLCHAIN` is an input override only; Anvil never exports a resolved # value globally into unrelated recipes or helpers. [private] -_anvil_stable_toolchain_args := trim(''' -@(& { -$RepoRoot = (Get-Location).Path - +_anvil_stable_toolchain_args := trim("@(& {\n$RepoRoot = '" + replace(justfile_directory(), "'", "''") + "'\n\n" + ''' $ErrorActionPreference = 'Stop' Set-StrictMode -Version 3 -function Write-ExplicitToolchainArg([string] $Toolchain) { - Write-Output ('+' + $Toolchain) -} - if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { - Write-ExplicitToolchainArg $env:RUSTUP_TOOLCHAIN + Write-Output ('+' + $env:RUSTUP_TOOLCHAIN) return } -$toolchainFile = $null -foreach ($name in @('rust-toolchain', 'rust-toolchain.toml')) { - $candidate = Join-Path $RepoRoot $name - if (Test-Path -LiteralPath $candidate -PathType Leaf) { - $toolchainFile = $candidate - break - } -} - -$selectingFile = $false -if ($null -ne $toolchainFile) { - $content = Get-Content -LiteralPath $toolchainFile -Raw - if ([string]::IsNullOrWhiteSpace($content)) { - throw "anvil: root toolchain file '$toolchainFile' is empty; remove it or declare a toolchain configuration" - } - if ((Split-Path -Leaf $toolchainFile) -eq 'rust-toolchain' -and $content.TrimStart() -notmatch '^\[') { - $selectingFile = $true - } else { - $inToolchain = $false - foreach ($line in $content -split '\r?\n') { - if ($line -match '^\s*\[\s*toolchain\s*\]\s*(?:#.*)?$') { - $inToolchain = $true - continue - } - if ($line -match '^\s*\[') { - $inToolchain = $false - continue - } - if ($inToolchain -and $line -match '^\s*["'']?(channel|path)["'']?\s*=\s*(["''])([^"'']+)\2\s*(?:#.*)?$') { - $selectingFile = $true - break - } +if ((Test-Path -LiteralPath (Join-Path $RepoRoot 'rust-toolchain') -PathType Leaf) -or + (Test-Path -LiteralPath (Join-Path $RepoRoot 'rust-toolchain.toml') -PathType Leaf)) { + Push-Location $RepoRoot + try { + $rustupOutput = @(& rustup show active-toolchain 2>&1) + if ($LASTEXITCODE -ne 0) { + throw "anvil: rustup could not resolve the repository toolchain file:`n$($rustupOutput -join "`n")" + } + $active = $rustupOutput | Select-Object -Last 1 + if ([string]::IsNullOrWhiteSpace([string] $active)) { + throw 'anvil: rustup returned no active repository toolchain' } + } finally { + Pop-Location } -} - -if ($selectingFile) { + $resolved = [string] $active + $sourceMarker = $resolved.LastIndexOf(' (') + if ($sourceMarker -ge 0) { + $resolved = $resolved.Substring(0, $sourceMarker) + } + Write-Output ('+' + $resolved) return } @@ -122,7 +99,7 @@ $msrv = if ($values.ContainsKey('workspace.package')) { } else { throw 'anvil: no selecting rust-toolchain(.toml) and no root [workspace.package] or [package] rust-version; declare an MSRV or select a toolchain explicitly' } -Write-ExplicitToolchainArg $msrv +Write-Output ('+' + $msrv) }) ''') From cb343001223448e4de36fb1a17b8252297560646 Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Mon, 31 Aug 2026 10:58:27 +0200 Subject: [PATCH 22/32] Harden deterministic toolchain setup Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: f7996e6f-5c20-4d7a-b2f7-690af95e557e --- .anvil.lock | 6 +- crates/cargo-anvil/docs/design/ado.md | 32 ++++---- crates/cargo-anvil/docs/design/local.md | 11 +-- crates/cargo-anvil/src/anvil/artifacts/ado.rs | 20 +++-- .../src/anvil/artifacts/justfile.rs | 35 ++++++--- .../cargo-anvil/templates/ado/steps/setup.yml | 40 +++++----- .../templates/justfiles/anvil/tools.just | 29 ++++---- .../templates/justfiles/anvil/versions.just | 4 +- .../snapshots/snapshots__ado_backend.snap | 73 +++++++++---------- .../snapshots/snapshots__github_backend.snap | 33 +++++---- .../snapshots/snapshots__local_only.snap | 33 +++++---- justfiles/anvil/tools.just | 29 ++++---- justfiles/anvil/versions.just | 4 +- 13 files changed, 184 insertions(+), 165 deletions(-) diff --git a/.anvil.lock b/.anvil.lock index 11e2ef920..566580d70 100644 --- a/.anvil.lock +++ b/.anvil.lock @@ -1,7 +1,7 @@ version = 1 tool = "anvil" tool_version = "0.5.0" -catalog_checksum = "sha256:bea05c54e887000f8461f5bdd8f86375fc94cd481942b6c7b8ab2674d64164c6" +catalog_checksum = "sha256:c210df505425c2041a55fd4e857f552ce0646ebf01f5866d6834ecf9ec8b86d1" [[file]] path = ".anvil/container/Containerfile" @@ -261,11 +261,11 @@ checksum = "sha256:5b2d91569f3fe87cd5f516623a01b5c7ae855ad587870ef9a2ae6d5619dd9 [[file]] path = "justfiles/anvil/tools.just" -checksum = "sha256:a7ce0dfe39a65b44aa3a7d605f8819673012600b16822ae29c90e95a3b7e708a" +checksum = "sha256:3f5d1ceb41d9b879a2944e59ee0f780cd1798ad7fa52956d717614180002db9b" [[file]] path = "justfiles/anvil/versions.just" -checksum = "sha256:2e6f224497a5b4ea68ab096bafc65622faf9c846c5c8caf7ea68c76e72a791c1" +checksum = "sha256:6b269db74cb0b38dfdd5b7392d3301a7e603ac0c62d592136eb237289272ddef" [[region]] host = ".delta.toml" diff --git a/crates/cargo-anvil/docs/design/ado.md b/crates/cargo-anvil/docs/design/ado.md index 54442b640..97f36b20b 100644 --- a/crates/cargo-anvil/docs/design/ado.md +++ b/crates/cargo-anvil/docs/design/ado.md @@ -747,18 +747,16 @@ mechanics are in [local.md §4](./local.md#4-impact-scoping-via-the-anvil-impact ## 6. Rust toolchain The user's root pipeline or compliance template installs the Rust/rustup -bootstrap before the Anvil stages run. After bootstrapping Just, the generated -setup step calls `_anvil-stable-rustc-version`. Its -`anvil-toolchain-stable-install` dependency provisions a public selection or -prepares an externally provisioned internal selection before invoking `rustc` -directly with the lazy optional `+toolchain` argument. The resulting version -keys the cache. The template never invokes `_anvil-resolve-stable` itself and -does not publish a resolved `RUSTUP_TOOLCHAIN` to subsequent steps. Each stable -recipe evaluates the inline PowerShell array expression and invokes Cargo or -Rust directly in its current PowerShell process. Selection has no -expression-time subprocess, host-OS branch, or change to the adopter's -configured shell. A selecting toolchain file remains unoverridden so rustup -processes the complete file. +bootstrap before the Anvil stages run. The generated setup step restores Cargo +home before bootstrapping Just, then calls `anvil-toolchain-stable-install` to +provision a public selection or prepare an externally provisioned internal +selection. The template never invokes `_anvil-resolve-stable` itself and does +not publish a resolved `RUSTUP_TOOLCHAIN` to subsequent steps. Each stable recipe +evaluates the inline PowerShell array expression and invokes Cargo or Rust +directly in its current PowerShell process. Selection has no expression-time +subprocess, host-OS branch, or change to the adopter's configured shell. A +selecting toolchain file remains unoverridden so rustup processes the complete +file. Selection follows the shared local contract: an existing `RUSTUP_TOOLCHAIN`, then a root toolchain file with `channel` or `path`, then the root MSRV. Internal pipelines @@ -780,16 +778,18 @@ the same setup primitive. Shared Cargo-tool/default-component setup and stable-only leaf setup all depend on `anvil-toolchain-stable-install`; Just deduplicates it for group/full fan-out. `anvil-tool-rustc-validate-prereqs` remains read-only and validates -the selected compiler and workspace MSRV compatibility rule. For +that `rustc` is available plus the workspace MSRV compatibility rule. For nightly-requiring checks, the matching toolchain-validate-prereqs recipe fails with a suggestion to ask the team's pipeline owner to add that dated nightly to msrustup. ## 7. Caching -`setup.yml` computes a cache key from agent OS and architecture, the actual -`rustc --version`, and hashes of `Cargo.lock`, `.cargo/config.toml`, -`rust-toolchain.toml`, and `versions.just`. It uses the ADO `Cache@2` task. +`setup.yml` restores Cargo home before bootstrapping Just so a warm job does not +compile Just before reaching the cache. The ADO `Cache@2` key uses agent OS and +architecture plus hashes of the root Cargo manifests, Cargo configuration, +toolchain files, and `versions.just`. The selected compiler version is omitted +because rustup toolchains are outside Cargo home. The cache covers: diff --git a/crates/cargo-anvil/docs/design/local.md b/crates/cargo-anvil/docs/design/local.md index 638a54373..e641430d4 100644 --- a/crates/cargo-anvil/docs/design/local.md +++ b/crates/cargo-anvil/docs/design/local.md @@ -342,8 +342,8 @@ install recipe can run: Trade-off acknowledged: `cargo install --locked` is slow on a cold cache (several minutes for the full catalog). It is also the most reliable mechanism in restricted networks. Caching (via the GH cache action and the ADO pipeline workspace cache) is -configured by the setup action/template to key on `Cargo.lock`, the toolchain -channel, and `versions.just`. See +configured by the setup action/template to key on repository manifests, +toolchain configuration, and `versions.just`. See [github.md](./github.md#caching) and [ado.md](./ado.md#caching). #### 3.3.1 System-level prerequisites @@ -446,7 +446,7 @@ provisioning. Rustup processes repository toolchain files directly; the private `_anvil-resolve-stable` implementation handles public MSRV installation and internal mapping. Leaf setup recipes reach the primitive directly or through the shared Cargo-tool/default-component installers; group, tier, and -`anvil-setup` recipes inherit and deduplicate that dependency. Cloud version +`anvil-setup` recipes inherit and deduplicate that dependency. GitHub version capture uses the private `_anvil-stable-rustc-version` recipe, whose dependency provisions stable before it invokes `rustc` directly with the lazy argument. Workflows and container construction therefore invoke setup operations rather @@ -471,8 +471,9 @@ features. A selecting toolchain file does not suppress this minimum-version run. `ANVIL_MSRV_TOOLCHAIN` optionally maps the declared MSRV to a provisioned internal toolchain. Without a root MSRV the recipe is a no-op. -`anvil-tool-rustc-validate-prereqs` verifies the selected compiler and the -workspace MSRV compatibility rule. Per-check toolchain requirements (for +`anvil-tool-rustc-validate-prereqs` verifies that `rustc` is available and +enforces the workspace MSRV compatibility rule. Stable setup separately +provisions the selected compiler. Per-check toolchain requirements (for example, miri, careful, and udeps need nightly) remain enforced by the matching `anvil-toolchain--validate-prereqs` recipe. Explicit `cargo +toolchain` diff --git a/crates/cargo-anvil/src/anvil/artifacts/ado.rs b/crates/cargo-anvil/src/anvil/artifacts/ado.rs index 99958e1da..3db58af09 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/ado.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/ado.rs @@ -214,18 +214,24 @@ mod tests { fn setup_step_takes_group_parameter_and_dispatches() { assert!(SETUP_STEP.contains("name: group")); assert!(SETUP_STEP.contains("just anvil-setup")); - assert!(SETUP_STEP.contains("just _anvil-stable-rustc-version")); + assert!(SETUP_STEP.contains("just anvil-toolchain-stable-install")); assert!(!SETUP_STEP.contains("_anvil-resolve-stable")); - assert!(!SETUP_STEP.contains("just anvil-toolchain-stable-install")); assert!(SETUP_STEP.contains("just anvil-${{ parameters.group }}-setup")); assert!(SETUP_STEP.contains("eq(parameters.group, 'none')")); + let cache_restore = SETUP_STEP + .find("anvil setup (cache cargo home)") + .expect("setup must restore Cargo home"); let just_bootstrap = SETUP_STEP.find("anvil setup (install just)").expect("setup must bootstrap Just"); - let version_capture = SETUP_STEP - .find("just _anvil-stable-rustc-version") - .expect("setup must capture the provisioned stable compiler version"); + let stable_provisioning = SETUP_STEP + .find("just anvil-toolchain-stable-install") + .expect("setup must provision the selected stable compiler"); assert!( - just_bootstrap < version_capture, - "Just must be bootstrapped before setup-driven stable version capture" + cache_restore < just_bootstrap, + "Cargo home must be restored before Just is bootstrapped" + ); + assert!( + just_bootstrap < stable_provisioning, + "Just must be bootstrapped before setup-driven stable provisioning" ); } diff --git a/crates/cargo-anvil/src/anvil/artifacts/justfile.rs b/crates/cargo-anvil/src/anvil/artifacts/justfile.rs index e15769547..3e17432c5 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/justfile.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/justfile.rs @@ -750,7 +750,7 @@ mod tests { assert!(!VERSIONS_JUST.contains("os_family()")); assert!(!VERSIONS_JUST.contains("shell(")); assert!(!VERSIONS_JUST.contains("[scriptblock]::Create")); - assert!(TOOLS_JUST.contains("_anvil-resolve-stable action=\"resolve\":")); + assert!(TOOLS_JUST.contains("_anvil-resolve-stable action=\"install\":")); assert!(!TOOLS_JUST.contains("_anvil-with-stable")); assert!(TOOLS_JUST.contains("& cargo {{_anvil_stable_toolchain_args}}")); assert!(VERSIONS_JUST.contains("rustup show active-toolchain")); @@ -847,12 +847,15 @@ mod tests { use std::process::{Command, Output}; use std::{env, fs}; - use tempfile::TempDir; + use tempfile::{Builder, TempDir}; use super::{TOOLS_JUST, VERSIONS_JUST}; fn fixture(cargo_toml: &str) -> TempDir { - let temp = TempDir::new().expect("temporary repository must be creatable"); + let temp = Builder::new() + .prefix("anvil repo's [copy] (fork) ") + .tempdir() + .expect("temporary repository must be creatable"); fs::write(temp.path().join("Cargo.toml"), cargo_toml).expect("manifest fixture must be writable"); fs::write(temp.path().join("versions.just"), VERSIONS_JUST).expect("versions fixture must be writable"); fs::write(temp.path().join("tools.just"), TOOLS_JUST).expect("tools fixture must be writable"); @@ -906,9 +909,6 @@ mod tests { [] => { command.arg("_anvil-test-stable-args"); } - ["-ForEnvironment"] => { - command.args(["_anvil-resolve-stable", "for-environment"]); - } ["-InstallIfMissing"] => { command.arg("anvil-toolchain-stable-install"); } @@ -985,7 +985,7 @@ mod tests { fs::write(shim.path().join("cargo.cmd"), "@echo off\r\necho %*\r\nexit /b 0\r\n").expect("Cargo shim must be writable"); fs::write( shim.path().join("rustup.cmd"), - "@echo off\r\nif \"%1 %2\"==\"show active-toolchain\" echo C:\\toolchains\\test toolchain (overridden by fixture)\r\nexit /b 0\r\n", + "@echo off\r\nif \"%1 %2\"==\"show active-toolchain\" echo C:\\toolchains\\test toolchain (overridden by 'checkout (fork)')\r\nexit /b 0\r\n", ) .expect("rustup shim must be writable"); } @@ -999,7 +999,7 @@ mod tests { let rustup = shim.path().join("rustup"); fs::write( &rustup, - "#!/bin/sh\nif [ \"$*\" = 'show active-toolchain' ]; then echo '/toolchains/test toolchain (overridden by fixture)'; fi\nexit 0\n", + "#!/bin/sh\nif [ \"$*\" = 'show active-toolchain' ]; then echo \"/toolchains/test toolchain (overridden by 'checkout (fork)')\"; fi\nexit 0\n", ) .expect("rustup shim must be writable"); fs::set_permissions(&rustup, fs::Permissions::from_mode(0o755)).expect("rustup shim must be executable"); @@ -1097,7 +1097,8 @@ mod tests { let diagnostic = normalized_diagnostic(&output); assert!( diagnostic.contains("rustup could not resolve") - && (diagnostic.contains("empty toolchain override") || diagnostic.contains("error parsing override file")), + && diagnostic.contains("override") + && (diagnostic.contains("empty") || diagnostic.contains("error parsing")), "unexpected resolver diagnostic: {diagnostic}" ); } @@ -1272,7 +1273,7 @@ mod tests { "unexpected resolver diagnostic: {diagnostic}" ); - let output = run_validation("1.92", None); + let output = run_validation("1.92.0", None); assert!( output.status.success(), "member MSRVs at or below the root must be compatible: {}", @@ -1322,11 +1323,16 @@ mod tests { let shim = TempDir::new().expect("toolchain shim directory must be creatable"); let rustup_log = shim.path().join("rustup.log"); let cargo_log = shim.path().join("cargo.log"); + let installed_output = installed + .lines() + .map(|line| format!("Write-Output '{line}'")) + .collect::>() + .join("\n"); fs::write( shim.path().join("rustup.ps1"), format!( "Add-Content -LiteralPath $env:ANVIL_RUSTUP_LOG -Value ($args -join ' ')\n\ - if (($args -contains 'toolchain') -and ($args -contains 'list')) {{ Write-Output '{installed}' }}\n\ + if (($args -contains 'toolchain') -and ($args -contains 'list')) {{ {installed_output} }}\n\ exit 0\n" ), ) @@ -1358,7 +1364,12 @@ mod tests { let temp = fixture("[workspace.package]\nrust-version = \"1.93\"\n"); - let (output, rustup_calls, cargo_calls) = run_install(temp.path(), None, "1.93-x86_64-pc-windows-msvc", 0); + let (output, rustup_calls, cargo_calls) = run_install( + temp.path(), + None, + "nightly-2026-01-01-x86_64-pc-windows-msvc\n1.93-x86_64-pc-windows-msvc", + 0, + ); assert!( output.status.success(), "installed public MSRV provisioning failed: {}", diff --git a/crates/cargo-anvil/templates/ado/steps/setup.yml b/crates/cargo-anvil/templates/ado/steps/setup.yml index c7352dd59..b949ee078 100644 --- a/crates/cargo-anvil/templates/ado/steps/setup.yml +++ b/crates/cargo-anvil/templates/ado/steps/setup.yml @@ -18,30 +18,15 @@ parameters: type: string default: '' steps: - - bash: | - if ! command -v just >/dev/null 2>&1 ; then - cargo install --locked just - fi - displayName: anvil setup (install just) - - - pwsh: | - $version = (& just _anvil-stable-rustc-version | Select-Object -Last 1) - if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($version)) { - throw 'anvil: failed to provision the selected stable toolchain' - } - $version = $version.Trim() - Write-Host "##vso[task.setvariable variable=anvil_rustc_version]$version" - displayName: anvil setup (provision stable toolchain and capture version) + # Restore Cargo's registry and installed binaries before bootstrapping Just, + # so warm ADO jobs do not compile Just again before reaching the cache. + # Rustup toolchains are not stored under Cargo home, so the selected compiler + # version does not affect this cache's contents. - task: Cache@2 inputs: - # Same key shape as the GitHub side: OS + arch + rustc version + - # lockfile hashes + catalog hash. Including arch keeps any - # future ARM pool an adopter adds from colliding with x86_64 on - # the same OS namespace. - key: 'anvil-v1 | "$(Agent.OS)" | "$(Agent.OSArchitecture)" | rust$(anvil_rustc_version) | Cargo.toml | Cargo.lock | .cargo/config.toml | rust-toolchain | rust-toolchain.toml | justfiles/anvil/versions.just' + key: 'anvil-v2 | "$(Agent.OS)" | "$(Agent.OSArchitecture)" | Cargo.toml | Cargo.lock | .cargo/config.toml | rust-toolchain | rust-toolchain.toml | justfiles/anvil/versions.just' restoreKeys: | - anvil-v1 | "$(Agent.OS)" | "$(Agent.OSArchitecture)" | rust$(anvil_rustc_version) - anvil-v1 | "$(Agent.OS)" | "$(Agent.OSArchitecture)" + anvil-v2 | "$(Agent.OS)" | "$(Agent.OSArchitecture)" path: | $(HOME)/.cargo/registry/cache/ $(HOME)/.cargo/registry/index/ @@ -50,6 +35,19 @@ steps: $(HOME)/.cargo/.crates2.json displayName: anvil setup (cache cargo home) + - bash: | + if ! command -v just >/dev/null 2>&1 ; then + cargo install --locked just + fi + displayName: anvil setup (install just) + + - pwsh: | + & just anvil-toolchain-stable-install + if ($LASTEXITCODE -ne 0) { + throw 'anvil: failed to provision the selected stable toolchain' + } + displayName: anvil setup (provision stable toolchain) + # Hand everything else off to the catalog recipe. Idempotent. ADO # uses the default `install` backend (source builds) because # cargo-binstall has unresolved compliance issues for internal ADO diff --git a/crates/cargo-anvil/templates/justfiles/anvil/tools.just b/crates/cargo-anvil/templates/justfiles/anvil/tools.just index 0d6e45f92..dc2409c73 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/tools.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/tools.just @@ -176,14 +176,12 @@ anvil-tool-pwsh-validate-prereqs: # Rustup owns toolchain-file parsing, installation, and file options. Anvil only # handles explicit environment overrides and the root Cargo MSRV fallback. [script("pwsh", "-NoProfile")] -_anvil-resolve-stable action="resolve": +_anvil-resolve-stable action="install": $ErrorActionPreference = 'Stop' Set-StrictMode -Version 3 $action = '{{action}}' $validActions = @( - 'resolve', - 'for-environment', 'install', 'validate-workspace-msrv', 'msrv', @@ -194,7 +192,7 @@ _anvil-resolve-stable action="resolve": exit 2 } - $repoRoot = '{{justfile_directory()}}' + $repoRoot = '{{replace(justfile_directory(), "'", "''")}}' function Test-RootToolchainFile { return ( @@ -253,9 +251,18 @@ _anvil-resolve-stable action="resolve": throw "anvil: workspace packages without a resolved rust-version: $($missing -join ', '); declare one or select a toolchain explicitly" } + function ConvertTo-ComparableRustVersion([string] $value) { + if ($value -notmatch '^\s*(\d+)\.(\d+)(?:\.(\d+))?\s*$') { + throw "anvil: invalid resolved rust-version '$value'" + } + $patch = if ([string]::IsNullOrWhiteSpace($Matches[3])) { 0 } else { [int] $Matches[3] } + return [version]::new([int] $Matches[1], [int] $Matches[2], $patch) + } + + $rootVersion = ConvertTo-ComparableRustVersion $rootMsrv $newer = @( $members | - Where-Object { [version] ([string] $_.rust_version) -gt [version] $rootMsrv } | + Where-Object { (ConvertTo-ComparableRustVersion ([string] $_.rust_version)) -gt $rootVersion } | ForEach-Object { "$($_.name) ($($_.rust_version))" } ) if ($newer.Count -gt 0) { @@ -292,7 +299,7 @@ _anvil-resolve-stable action="resolve": if ($LASTEXITCODE -ne 0) { throw 'anvil: rustup toolchain list failed' } - if ($installed -notmatch "(?m)^$([regex]::Escape($toolchain.Value))(?:-|$)") { + if (($installed -join "`n") -notmatch "(?m)^$([regex]::Escape($toolchain.Value))(?:-|$)") { Write-Host "anvil: installing $label toolchain '$($toolchain.Value)'" rustup toolchain install $toolchain.Value --profile minimal if ($LASTEXITCODE -ne 0) { @@ -341,7 +348,7 @@ _anvil-resolve-stable action="resolve": if ($action -eq 'install') { if ($selection.Source -eq 'file') { - Push-Location $repoRoot + Push-Location -LiteralPath $repoRoot try { & rustc --version if ($LASTEXITCODE -ne 0) { @@ -361,12 +368,6 @@ _anvil-resolve-stable action="resolve": exit 0 } - if ($action -eq 'for-environment' -and $selection.Source -eq 'file') { - '' - } else { - $selection.Value - } - # Provision the selected stable toolchain. Repository toolchain files are # processed natively by rustup. This is the shared prerequisite for setup paths # that execute stable Cargo/Rust or install Cargo tools; Just deduplicates it @@ -374,7 +375,7 @@ _anvil-resolve-stable action="resolve": [group("anvil-setup")] anvil-toolchain-stable-install: (_anvil-resolve-stable "install") -# Report the selected stable rustc version for cloud cache keys. Provisioning +# Report the selected stable rustc version for GitHub cache keys. Provisioning # is a dependency so workflow surfaces invoke one setup-aware operation rather # than orchestrating the resolver directly. [private] diff --git a/crates/cargo-anvil/templates/justfiles/anvil/versions.just b/crates/cargo-anvil/templates/justfiles/anvil/versions.just index 1641ee8a3..16a523aaa 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/versions.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/versions.just @@ -48,7 +48,7 @@ if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { if ((Test-Path -LiteralPath (Join-Path $RepoRoot 'rust-toolchain') -PathType Leaf) -or (Test-Path -LiteralPath (Join-Path $RepoRoot 'rust-toolchain.toml') -PathType Leaf)) { - Push-Location $RepoRoot + Push-Location -LiteralPath $RepoRoot try { $rustupOutput = @(& rustup show active-toolchain 2>&1) if ($LASTEXITCODE -ne 0) { @@ -62,7 +62,7 @@ if ((Test-Path -LiteralPath (Join-Path $RepoRoot 'rust-toolchain') -PathType Lea Pop-Location } $resolved = [string] $active - $sourceMarker = $resolved.LastIndexOf(' (') + $sourceMarker = $resolved.IndexOf(' (') if ($sourceMarker -ge 0) { $resolved = $resolved.Substring(0, $sourceMarker) } diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap index ed5ab45ae..2abee2715 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap @@ -2466,30 +2466,15 @@ parameters: type: string default: '' steps: - - bash: | - if ! command -v just >/dev/null 2>&1 ; then - cargo install --locked just - fi - displayName: anvil setup (install just) - - - pwsh: | - $version = (& just _anvil-stable-rustc-version | Select-Object -Last 1) - if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($version)) { - throw 'anvil: failed to provision the selected stable toolchain' - } - $version = $version.Trim() - Write-Host "##vso[task.setvariable variable=anvil_rustc_version]$version" - displayName: anvil setup (provision stable toolchain and capture version) + # Restore Cargo's registry and installed binaries before bootstrapping Just, + # so warm ADO jobs do not compile Just again before reaching the cache. + # Rustup toolchains are not stored under Cargo home, so the selected compiler + # version does not affect this cache's contents. - task: Cache@2 inputs: - # Same key shape as the GitHub side: OS + arch + rustc version + - # lockfile hashes + catalog hash. Including arch keeps any - # future ARM pool an adopter adds from colliding with x86_64 on - # the same OS namespace. - key: 'anvil-v1 | "$(Agent.OS)" | "$(Agent.OSArchitecture)" | rust$(anvil_rustc_version) | Cargo.toml | Cargo.lock | .cargo/config.toml | rust-toolchain | rust-toolchain.toml | justfiles/anvil/versions.just' + key: 'anvil-v2 | "$(Agent.OS)" | "$(Agent.OSArchitecture)" | Cargo.toml | Cargo.lock | .cargo/config.toml | rust-toolchain | rust-toolchain.toml | justfiles/anvil/versions.just' restoreKeys: | - anvil-v1 | "$(Agent.OS)" | "$(Agent.OSArchitecture)" | rust$(anvil_rustc_version) - anvil-v1 | "$(Agent.OS)" | "$(Agent.OSArchitecture)" + anvil-v2 | "$(Agent.OS)" | "$(Agent.OSArchitecture)" path: | $(HOME)/.cargo/registry/cache/ $(HOME)/.cargo/registry/index/ @@ -2498,6 +2483,19 @@ steps: $(HOME)/.cargo/.crates2.json displayName: anvil setup (cache cargo home) + - bash: | + if ! command -v just >/dev/null 2>&1 ; then + cargo install --locked just + fi + displayName: anvil setup (install just) + + - pwsh: | + & just anvil-toolchain-stable-install + if ($LASTEXITCODE -ne 0) { + throw 'anvil: failed to provision the selected stable toolchain' + } + displayName: anvil setup (provision stable toolchain) + # Hand everything else off to the catalog recipe. Idempotent. ADO # uses the default `install` backend (source builds) because # cargo-binstall has unresolved compliance issues for internal ADO @@ -6364,14 +6362,12 @@ anvil-tool-pwsh-validate-prereqs: # Rustup owns toolchain-file parsing, installation, and file options. Anvil only # handles explicit environment overrides and the root Cargo MSRV fallback. [script("pwsh", "-NoProfile")] -_anvil-resolve-stable action="resolve": +_anvil-resolve-stable action="install": $ErrorActionPreference = 'Stop' Set-StrictMode -Version 3 $action = '{{action}}' $validActions = @( - 'resolve', - 'for-environment', 'install', 'validate-workspace-msrv', 'msrv', @@ -6382,7 +6378,7 @@ _anvil-resolve-stable action="resolve": exit 2 } - $repoRoot = '{{justfile_directory()}}' + $repoRoot = '{{replace(justfile_directory(), "'", "''")}}' function Test-RootToolchainFile { return ( @@ -6441,9 +6437,18 @@ _anvil-resolve-stable action="resolve": throw "anvil: workspace packages without a resolved rust-version: $($missing -join ', '); declare one or select a toolchain explicitly" } + function ConvertTo-ComparableRustVersion([string] $value) { + if ($value -notmatch '^\s*(\d+)\.(\d+)(?:\.(\d+))?\s*$') { + throw "anvil: invalid resolved rust-version '$value'" + } + $patch = if ([string]::IsNullOrWhiteSpace($Matches[3])) { 0 } else { [int] $Matches[3] } + return [version]::new([int] $Matches[1], [int] $Matches[2], $patch) + } + + $rootVersion = ConvertTo-ComparableRustVersion $rootMsrv $newer = @( $members | - Where-Object { [version] ([string] $_.rust_version) -gt [version] $rootMsrv } | + Where-Object { (ConvertTo-ComparableRustVersion ([string] $_.rust_version)) -gt $rootVersion } | ForEach-Object { "$($_.name) ($($_.rust_version))" } ) if ($newer.Count -gt 0) { @@ -6480,7 +6485,7 @@ _anvil-resolve-stable action="resolve": if ($LASTEXITCODE -ne 0) { throw 'anvil: rustup toolchain list failed' } - if ($installed -notmatch "(?m)^$([regex]::Escape($toolchain.Value))(?:-|$)") { + if (($installed -join "`n") -notmatch "(?m)^$([regex]::Escape($toolchain.Value))(?:-|$)") { Write-Host "anvil: installing $label toolchain '$($toolchain.Value)'" rustup toolchain install $toolchain.Value --profile minimal if ($LASTEXITCODE -ne 0) { @@ -6529,7 +6534,7 @@ _anvil-resolve-stable action="resolve": if ($action -eq 'install') { if ($selection.Source -eq 'file') { - Push-Location $repoRoot + Push-Location -LiteralPath $repoRoot try { & rustc --version if ($LASTEXITCODE -ne 0) { @@ -6549,12 +6554,6 @@ _anvil-resolve-stable action="resolve": exit 0 } - if ($action -eq 'for-environment' -and $selection.Source -eq 'file') { - '' - } else { - $selection.Value - } - # Provision the selected stable toolchain. Repository toolchain files are # processed natively by rustup. This is the shared prerequisite for setup paths # that execute stable Cargo/Rust or install Cargo tools; Just deduplicates it @@ -6562,7 +6561,7 @@ _anvil-resolve-stable action="resolve": [group("anvil-setup")] anvil-toolchain-stable-install: (_anvil-resolve-stable "install") -# Report the selected stable rustc version for cloud cache keys. Provisioning +# Report the selected stable rustc version for GitHub cache keys. Provisioning # is a dependency so workflow surfaces invoke one setup-aware operation rather # than orchestrating the resolver directly. [private] @@ -7242,7 +7241,7 @@ if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { if ((Test-Path -LiteralPath (Join-Path $RepoRoot 'rust-toolchain') -PathType Leaf) -or (Test-Path -LiteralPath (Join-Path $RepoRoot 'rust-toolchain.toml') -PathType Leaf)) { - Push-Location $RepoRoot + Push-Location -LiteralPath $RepoRoot try { $rustupOutput = @(& rustup show active-toolchain 2>&1) if ($LASTEXITCODE -ne 0) { @@ -7256,7 +7255,7 @@ if ((Test-Path -LiteralPath (Join-Path $RepoRoot 'rust-toolchain') -PathType Lea Pop-Location } $resolved = [string] $active - $sourceMarker = $resolved.LastIndexOf(' (') + $sourceMarker = $resolved.IndexOf(' (') if ($sourceMarker -ge 0) { $resolved = $resolved.Substring(0, $sourceMarker) } diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap index 714f7b6d0..51db3d65a 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap @@ -6165,14 +6165,12 @@ anvil-tool-pwsh-validate-prereqs: # Rustup owns toolchain-file parsing, installation, and file options. Anvil only # handles explicit environment overrides and the root Cargo MSRV fallback. [script("pwsh", "-NoProfile")] -_anvil-resolve-stable action="resolve": +_anvil-resolve-stable action="install": $ErrorActionPreference = 'Stop' Set-StrictMode -Version 3 $action = '{{action}}' $validActions = @( - 'resolve', - 'for-environment', 'install', 'validate-workspace-msrv', 'msrv', @@ -6183,7 +6181,7 @@ _anvil-resolve-stable action="resolve": exit 2 } - $repoRoot = '{{justfile_directory()}}' + $repoRoot = '{{replace(justfile_directory(), "'", "''")}}' function Test-RootToolchainFile { return ( @@ -6242,9 +6240,18 @@ _anvil-resolve-stable action="resolve": throw "anvil: workspace packages without a resolved rust-version: $($missing -join ', '); declare one or select a toolchain explicitly" } + function ConvertTo-ComparableRustVersion([string] $value) { + if ($value -notmatch '^\s*(\d+)\.(\d+)(?:\.(\d+))?\s*$') { + throw "anvil: invalid resolved rust-version '$value'" + } + $patch = if ([string]::IsNullOrWhiteSpace($Matches[3])) { 0 } else { [int] $Matches[3] } + return [version]::new([int] $Matches[1], [int] $Matches[2], $patch) + } + + $rootVersion = ConvertTo-ComparableRustVersion $rootMsrv $newer = @( $members | - Where-Object { [version] ([string] $_.rust_version) -gt [version] $rootMsrv } | + Where-Object { (ConvertTo-ComparableRustVersion ([string] $_.rust_version)) -gt $rootVersion } | ForEach-Object { "$($_.name) ($($_.rust_version))" } ) if ($newer.Count -gt 0) { @@ -6281,7 +6288,7 @@ _anvil-resolve-stable action="resolve": if ($LASTEXITCODE -ne 0) { throw 'anvil: rustup toolchain list failed' } - if ($installed -notmatch "(?m)^$([regex]::Escape($toolchain.Value))(?:-|$)") { + if (($installed -join "`n") -notmatch "(?m)^$([regex]::Escape($toolchain.Value))(?:-|$)") { Write-Host "anvil: installing $label toolchain '$($toolchain.Value)'" rustup toolchain install $toolchain.Value --profile minimal if ($LASTEXITCODE -ne 0) { @@ -6330,7 +6337,7 @@ _anvil-resolve-stable action="resolve": if ($action -eq 'install') { if ($selection.Source -eq 'file') { - Push-Location $repoRoot + Push-Location -LiteralPath $repoRoot try { & rustc --version if ($LASTEXITCODE -ne 0) { @@ -6350,12 +6357,6 @@ _anvil-resolve-stable action="resolve": exit 0 } - if ($action -eq 'for-environment' -and $selection.Source -eq 'file') { - '' - } else { - $selection.Value - } - # Provision the selected stable toolchain. Repository toolchain files are # processed natively by rustup. This is the shared prerequisite for setup paths # that execute stable Cargo/Rust or install Cargo tools; Just deduplicates it @@ -6363,7 +6364,7 @@ _anvil-resolve-stable action="resolve": [group("anvil-setup")] anvil-toolchain-stable-install: (_anvil-resolve-stable "install") -# Report the selected stable rustc version for cloud cache keys. Provisioning +# Report the selected stable rustc version for GitHub cache keys. Provisioning # is a dependency so workflow surfaces invoke one setup-aware operation rather # than orchestrating the resolver directly. [private] @@ -7043,7 +7044,7 @@ if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { if ((Test-Path -LiteralPath (Join-Path $RepoRoot 'rust-toolchain') -PathType Leaf) -or (Test-Path -LiteralPath (Join-Path $RepoRoot 'rust-toolchain.toml') -PathType Leaf)) { - Push-Location $RepoRoot + Push-Location -LiteralPath $RepoRoot try { $rustupOutput = @(& rustup show active-toolchain 2>&1) if ($LASTEXITCODE -ne 0) { @@ -7057,7 +7058,7 @@ if ((Test-Path -LiteralPath (Join-Path $RepoRoot 'rust-toolchain') -PathType Lea Pop-Location } $resolved = [string] $active - $sourceMarker = $resolved.LastIndexOf(' (') + $sourceMarker = $resolved.IndexOf(' (') if ($sourceMarker -ge 0) { $resolved = $resolved.Substring(0, $sourceMarker) } diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap index 2bee1d427..afe542511 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap @@ -5021,14 +5021,12 @@ anvil-tool-pwsh-validate-prereqs: # Rustup owns toolchain-file parsing, installation, and file options. Anvil only # handles explicit environment overrides and the root Cargo MSRV fallback. [script("pwsh", "-NoProfile")] -_anvil-resolve-stable action="resolve": +_anvil-resolve-stable action="install": $ErrorActionPreference = 'Stop' Set-StrictMode -Version 3 $action = '{{action}}' $validActions = @( - 'resolve', - 'for-environment', 'install', 'validate-workspace-msrv', 'msrv', @@ -5039,7 +5037,7 @@ _anvil-resolve-stable action="resolve": exit 2 } - $repoRoot = '{{justfile_directory()}}' + $repoRoot = '{{replace(justfile_directory(), "'", "''")}}' function Test-RootToolchainFile { return ( @@ -5098,9 +5096,18 @@ _anvil-resolve-stable action="resolve": throw "anvil: workspace packages without a resolved rust-version: $($missing -join ', '); declare one or select a toolchain explicitly" } + function ConvertTo-ComparableRustVersion([string] $value) { + if ($value -notmatch '^\s*(\d+)\.(\d+)(?:\.(\d+))?\s*$') { + throw "anvil: invalid resolved rust-version '$value'" + } + $patch = if ([string]::IsNullOrWhiteSpace($Matches[3])) { 0 } else { [int] $Matches[3] } + return [version]::new([int] $Matches[1], [int] $Matches[2], $patch) + } + + $rootVersion = ConvertTo-ComparableRustVersion $rootMsrv $newer = @( $members | - Where-Object { [version] ([string] $_.rust_version) -gt [version] $rootMsrv } | + Where-Object { (ConvertTo-ComparableRustVersion ([string] $_.rust_version)) -gt $rootVersion } | ForEach-Object { "$($_.name) ($($_.rust_version))" } ) if ($newer.Count -gt 0) { @@ -5137,7 +5144,7 @@ _anvil-resolve-stable action="resolve": if ($LASTEXITCODE -ne 0) { throw 'anvil: rustup toolchain list failed' } - if ($installed -notmatch "(?m)^$([regex]::Escape($toolchain.Value))(?:-|$)") { + if (($installed -join "`n") -notmatch "(?m)^$([regex]::Escape($toolchain.Value))(?:-|$)") { Write-Host "anvil: installing $label toolchain '$($toolchain.Value)'" rustup toolchain install $toolchain.Value --profile minimal if ($LASTEXITCODE -ne 0) { @@ -5186,7 +5193,7 @@ _anvil-resolve-stable action="resolve": if ($action -eq 'install') { if ($selection.Source -eq 'file') { - Push-Location $repoRoot + Push-Location -LiteralPath $repoRoot try { & rustc --version if ($LASTEXITCODE -ne 0) { @@ -5206,12 +5213,6 @@ _anvil-resolve-stable action="resolve": exit 0 } - if ($action -eq 'for-environment' -and $selection.Source -eq 'file') { - '' - } else { - $selection.Value - } - # Provision the selected stable toolchain. Repository toolchain files are # processed natively by rustup. This is the shared prerequisite for setup paths # that execute stable Cargo/Rust or install Cargo tools; Just deduplicates it @@ -5219,7 +5220,7 @@ _anvil-resolve-stable action="resolve": [group("anvil-setup")] anvil-toolchain-stable-install: (_anvil-resolve-stable "install") -# Report the selected stable rustc version for cloud cache keys. Provisioning +# Report the selected stable rustc version for GitHub cache keys. Provisioning # is a dependency so workflow surfaces invoke one setup-aware operation rather # than orchestrating the resolver directly. [private] @@ -5899,7 +5900,7 @@ if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { if ((Test-Path -LiteralPath (Join-Path $RepoRoot 'rust-toolchain') -PathType Leaf) -or (Test-Path -LiteralPath (Join-Path $RepoRoot 'rust-toolchain.toml') -PathType Leaf)) { - Push-Location $RepoRoot + Push-Location -LiteralPath $RepoRoot try { $rustupOutput = @(& rustup show active-toolchain 2>&1) if ($LASTEXITCODE -ne 0) { @@ -5913,7 +5914,7 @@ if ((Test-Path -LiteralPath (Join-Path $RepoRoot 'rust-toolchain') -PathType Lea Pop-Location } $resolved = [string] $active - $sourceMarker = $resolved.LastIndexOf(' (') + $sourceMarker = $resolved.IndexOf(' (') if ($sourceMarker -ge 0) { $resolved = $resolved.Substring(0, $sourceMarker) } diff --git a/justfiles/anvil/tools.just b/justfiles/anvil/tools.just index 0d6e45f92..dc2409c73 100644 --- a/justfiles/anvil/tools.just +++ b/justfiles/anvil/tools.just @@ -176,14 +176,12 @@ anvil-tool-pwsh-validate-prereqs: # Rustup owns toolchain-file parsing, installation, and file options. Anvil only # handles explicit environment overrides and the root Cargo MSRV fallback. [script("pwsh", "-NoProfile")] -_anvil-resolve-stable action="resolve": +_anvil-resolve-stable action="install": $ErrorActionPreference = 'Stop' Set-StrictMode -Version 3 $action = '{{action}}' $validActions = @( - 'resolve', - 'for-environment', 'install', 'validate-workspace-msrv', 'msrv', @@ -194,7 +192,7 @@ _anvil-resolve-stable action="resolve": exit 2 } - $repoRoot = '{{justfile_directory()}}' + $repoRoot = '{{replace(justfile_directory(), "'", "''")}}' function Test-RootToolchainFile { return ( @@ -253,9 +251,18 @@ _anvil-resolve-stable action="resolve": throw "anvil: workspace packages without a resolved rust-version: $($missing -join ', '); declare one or select a toolchain explicitly" } + function ConvertTo-ComparableRustVersion([string] $value) { + if ($value -notmatch '^\s*(\d+)\.(\d+)(?:\.(\d+))?\s*$') { + throw "anvil: invalid resolved rust-version '$value'" + } + $patch = if ([string]::IsNullOrWhiteSpace($Matches[3])) { 0 } else { [int] $Matches[3] } + return [version]::new([int] $Matches[1], [int] $Matches[2], $patch) + } + + $rootVersion = ConvertTo-ComparableRustVersion $rootMsrv $newer = @( $members | - Where-Object { [version] ([string] $_.rust_version) -gt [version] $rootMsrv } | + Where-Object { (ConvertTo-ComparableRustVersion ([string] $_.rust_version)) -gt $rootVersion } | ForEach-Object { "$($_.name) ($($_.rust_version))" } ) if ($newer.Count -gt 0) { @@ -292,7 +299,7 @@ _anvil-resolve-stable action="resolve": if ($LASTEXITCODE -ne 0) { throw 'anvil: rustup toolchain list failed' } - if ($installed -notmatch "(?m)^$([regex]::Escape($toolchain.Value))(?:-|$)") { + if (($installed -join "`n") -notmatch "(?m)^$([regex]::Escape($toolchain.Value))(?:-|$)") { Write-Host "anvil: installing $label toolchain '$($toolchain.Value)'" rustup toolchain install $toolchain.Value --profile minimal if ($LASTEXITCODE -ne 0) { @@ -341,7 +348,7 @@ _anvil-resolve-stable action="resolve": if ($action -eq 'install') { if ($selection.Source -eq 'file') { - Push-Location $repoRoot + Push-Location -LiteralPath $repoRoot try { & rustc --version if ($LASTEXITCODE -ne 0) { @@ -361,12 +368,6 @@ _anvil-resolve-stable action="resolve": exit 0 } - if ($action -eq 'for-environment' -and $selection.Source -eq 'file') { - '' - } else { - $selection.Value - } - # Provision the selected stable toolchain. Repository toolchain files are # processed natively by rustup. This is the shared prerequisite for setup paths # that execute stable Cargo/Rust or install Cargo tools; Just deduplicates it @@ -374,7 +375,7 @@ _anvil-resolve-stable action="resolve": [group("anvil-setup")] anvil-toolchain-stable-install: (_anvil-resolve-stable "install") -# Report the selected stable rustc version for cloud cache keys. Provisioning +# Report the selected stable rustc version for GitHub cache keys. Provisioning # is a dependency so workflow surfaces invoke one setup-aware operation rather # than orchestrating the resolver directly. [private] diff --git a/justfiles/anvil/versions.just b/justfiles/anvil/versions.just index 1641ee8a3..16a523aaa 100644 --- a/justfiles/anvil/versions.just +++ b/justfiles/anvil/versions.just @@ -48,7 +48,7 @@ if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { if ((Test-Path -LiteralPath (Join-Path $RepoRoot 'rust-toolchain') -PathType Leaf) -or (Test-Path -LiteralPath (Join-Path $RepoRoot 'rust-toolchain.toml') -PathType Leaf)) { - Push-Location $RepoRoot + Push-Location -LiteralPath $RepoRoot try { $rustupOutput = @(& rustup show active-toolchain 2>&1) if ($LASTEXITCODE -ne 0) { @@ -62,7 +62,7 @@ if ((Test-Path -LiteralPath (Join-Path $RepoRoot 'rust-toolchain') -PathType Lea Pop-Location } $resolved = [string] $active - $sourceMarker = $resolved.LastIndexOf(' (') + $sourceMarker = $resolved.IndexOf(' (') if ($sourceMarker -ge 0) { $resolved = $resolved.Substring(0, $sourceMarker) } From 5aab9610435d6fce18d5188fcddb794ae9bbddf3 Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Mon, 31 Aug 2026 12:31:43 +0200 Subject: [PATCH 23/32] Preserve path-selected toolchain names Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: f7996e6f-5c20-4d7a-b2f7-690af95e557e --- .anvil.lock | 4 ++-- .../src/anvil/artifacts/justfile.rs | 18 ++++++++++-------- .../templates/justfiles/anvil/versions.just | 14 ++++++-------- crates/cargo-anvil/tests/impact.rs | 7 ++++++- .../snapshots/snapshots__ado_backend.snap | 14 ++++++-------- .../snapshots/snapshots__github_backend.snap | 14 ++++++-------- .../tests/snapshots/snapshots__local_only.snap | 14 ++++++-------- justfiles/anvil/versions.just | 14 ++++++-------- 8 files changed, 48 insertions(+), 51 deletions(-) diff --git a/.anvil.lock b/.anvil.lock index 566580d70..75434df9c 100644 --- a/.anvil.lock +++ b/.anvil.lock @@ -1,7 +1,7 @@ version = 1 tool = "anvil" tool_version = "0.5.0" -catalog_checksum = "sha256:c210df505425c2041a55fd4e857f552ce0646ebf01f5866d6834ecf9ec8b86d1" +catalog_checksum = "sha256:c729f558e395268c2b0532d6b35087e2997c8a3868d505892e8d87d2bcc35473" [[file]] path = ".anvil/container/Containerfile" @@ -265,7 +265,7 @@ checksum = "sha256:3f5d1ceb41d9b879a2944e59ee0f780cd1798ad7fa52956d717614180002d [[file]] path = "justfiles/anvil/versions.just" -checksum = "sha256:6b269db74cb0b38dfdd5b7392d3301a7e603ac0c62d592136eb237289272ddef" +checksum = "sha256:6d031b01aec542b93d698d236bcf559f5baeb8bf910a33b94fba41224cb76298" [[region]] host = ".delta.toml" diff --git a/crates/cargo-anvil/src/anvil/artifacts/justfile.rs b/crates/cargo-anvil/src/anvil/artifacts/justfile.rs index 3e17432c5..9568210e2 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/justfile.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/justfile.rs @@ -985,7 +985,7 @@ mod tests { fs::write(shim.path().join("cargo.cmd"), "@echo off\r\necho %*\r\nexit /b 0\r\n").expect("Cargo shim must be writable"); fs::write( shim.path().join("rustup.cmd"), - "@echo off\r\nif \"%1 %2\"==\"show active-toolchain\" echo C:\\toolchains\\test toolchain (overridden by 'checkout (fork)')\r\nexit /b 0\r\n", + "@echo off\r\nif \"%*\"==\"show active-toolchain --verbose\" echo C:\\Program Files ^(x86^)\\custom rust\r\nexit /b 0\r\n", ) .expect("rustup shim must be writable"); } @@ -999,7 +999,7 @@ mod tests { let rustup = shim.path().join("rustup"); fs::write( &rustup, - "#!/bin/sh\nif [ \"$*\" = 'show active-toolchain' ]; then echo \"/toolchains/test toolchain (overridden by 'checkout (fork)')\"; fi\nexit 0\n", + "#!/bin/sh\nif [ \"$*\" = 'show active-toolchain --verbose' ]; then echo '/toolchains/test (fork) toolchain'; fi\nexit 0\n", ) .expect("rustup shim must be writable"); fs::set_permissions(&rustup, fs::Permissions::from_mode(0o755)).expect("rustup shim must be executable"); @@ -1037,9 +1037,9 @@ mod tests { normalized_diagnostic(&output) ); let expected_file_toolchain = if cfg!(windows) { - "\"+C:\\toolchains\\test toolchain\" --version" + "\"+C:\\Program Files (x86)\\custom rust\" --version" } else { - "+/toolchains/test toolchain --version" + "+/toolchains/test (fork) toolchain --version" }; assert_eq!( String::from_utf8(output.stdout).expect("Cargo shim output must be UTF-8").trim(), @@ -1098,7 +1098,9 @@ mod tests { assert!( diagnostic.contains("rustup could not resolve") && diagnostic.contains("override") - && (diagnostic.contains("empty") || diagnostic.contains("error parsing")), + && (diagnostic.contains("empty") + || diagnostic.contains("could not parse") + || (diagnostic.contains("error") && diagnostic.contains("parsing"))), "unexpected resolver diagnostic: {diagnostic}" ); } @@ -1122,7 +1124,7 @@ mod tests { .expect("Cargo shim must be writable"); fs::write( shim.path().join("rustup.cmd"), - "@echo off\r\n>>\"%ANVIL_RUSTUP_LOG%\" echo %*\r\nif \"%1 %2\"==\"show active-toolchain\" echo 1.94-test-host (overridden by fixture)\r\nexit /b 0\r\n", + "@echo off\r\n>>\"%ANVIL_RUSTUP_LOG%\" echo %*\r\nif \"%*\"==\"show active-toolchain --verbose\" echo 1.94-test-host\r\nexit /b 0\r\n", ) .expect("rustup shim must be writable"); } @@ -1133,7 +1135,7 @@ mod tests { for (name, log_variable, status) in [("cargo", "ANVIL_CARGO_LOG", 1), ("rustup", "ANVIL_RUSTUP_LOG", 0)] { let executable = shim.path().join(name); let extra = if name == "rustup" { - "if [ \"$*\" = 'show active-toolchain' ]; then echo '1.94-test-host (overridden by fixture)'; fi\n" + "if [ \"$*\" = 'show active-toolchain --verbose' ]; then echo '1.94-test-host'; fi\n" } else { "" }; @@ -1197,7 +1199,7 @@ mod tests { .any(|line| line == "component add --toolchain 1.94-test-host clippy") ); assert!( - rustup_call.lines().any(|line| line == "show active-toolchain"), + rustup_call.lines().any(|line| line == "show active-toolchain --verbose"), "repository-selected component installation must ask rustup for the active toolchain" ); } diff --git a/crates/cargo-anvil/templates/justfiles/anvil/versions.just b/crates/cargo-anvil/templates/justfiles/anvil/versions.just index 16a523aaa..f984db8c7 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/versions.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/versions.just @@ -50,23 +50,21 @@ if ((Test-Path -LiteralPath (Join-Path $RepoRoot 'rust-toolchain') -PathType Lea (Test-Path -LiteralPath (Join-Path $RepoRoot 'rust-toolchain.toml') -PathType Leaf)) { Push-Location -LiteralPath $RepoRoot try { - $rustupOutput = @(& rustup show active-toolchain 2>&1) + $rustupOutput = @(& rustup show active-toolchain --verbose 2>&1) if ($LASTEXITCODE -ne 0) { throw "anvil: rustup could not resolve the repository toolchain file:`n$($rustupOutput -join "`n")" } - $active = $rustupOutput | Select-Object -Last 1 + # Verbose output puts the exact toolchain name on its own first line, + # avoiding ambiguity between spaces/parentheses in path toolchains and + # rustup's human-readable reason annotation. + $active = $rustupOutput | Select-Object -First 1 if ([string]::IsNullOrWhiteSpace([string] $active)) { throw 'anvil: rustup returned no active repository toolchain' } } finally { Pop-Location } - $resolved = [string] $active - $sourceMarker = $resolved.IndexOf(' (') - if ($sourceMarker -ge 0) { - $resolved = $resolved.Substring(0, $sourceMarker) - } - Write-Output ('+' + $resolved) + Write-Output ('+' + [string] $active) return } diff --git a/crates/cargo-anvil/tests/impact.rs b/crates/cargo-anvil/tests/impact.rs index aaf8565f5..fc695e359 100644 --- a/crates/cargo-anvil/tests/impact.rs +++ b/crates/cargo-anvil/tests/impact.rs @@ -1437,7 +1437,12 @@ fn snapshot_toolchain_probe(caller_toolchain: Option<&str>, toolchain_file: bool ( "rustup.ps1", "if (($args -contains 'show') -and ($args -contains 'active-toolchain')) {\n\ - \x20 Write-Output 'file-active-toolchain (overridden by rust-toolchain.toml)'\n\ + \x20 if ($args -contains '--verbose') {\n\ + \x20 Write-Output 'file-active-toolchain'\n\ + \x20 Write-Output 'active because: overridden by rust-toolchain.toml'\n\ + \x20 } else {\n\ + \x20 Write-Output 'file-active-toolchain (overridden by rust-toolchain.toml)'\n\ + \x20 }\n\ \x20 exit 0\n\ }\n\ exit 0\n", diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap index 2abee2715..72336d1d2 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap @@ -7243,23 +7243,21 @@ if ((Test-Path -LiteralPath (Join-Path $RepoRoot 'rust-toolchain') -PathType Lea (Test-Path -LiteralPath (Join-Path $RepoRoot 'rust-toolchain.toml') -PathType Leaf)) { Push-Location -LiteralPath $RepoRoot try { - $rustupOutput = @(& rustup show active-toolchain 2>&1) + $rustupOutput = @(& rustup show active-toolchain --verbose 2>&1) if ($LASTEXITCODE -ne 0) { throw "anvil: rustup could not resolve the repository toolchain file:`n$($rustupOutput -join "`n")" } - $active = $rustupOutput | Select-Object -Last 1 + # Verbose output puts the exact toolchain name on its own first line, + # avoiding ambiguity between spaces/parentheses in path toolchains and + # rustup's human-readable reason annotation. + $active = $rustupOutput | Select-Object -First 1 if ([string]::IsNullOrWhiteSpace([string] $active)) { throw 'anvil: rustup returned no active repository toolchain' } } finally { Pop-Location } - $resolved = [string] $active - $sourceMarker = $resolved.IndexOf(' (') - if ($sourceMarker -ge 0) { - $resolved = $resolved.Substring(0, $sourceMarker) - } - Write-Output ('+' + $resolved) + Write-Output ('+' + [string] $active) return } diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap index 51db3d65a..ace55506d 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap @@ -7046,23 +7046,21 @@ if ((Test-Path -LiteralPath (Join-Path $RepoRoot 'rust-toolchain') -PathType Lea (Test-Path -LiteralPath (Join-Path $RepoRoot 'rust-toolchain.toml') -PathType Leaf)) { Push-Location -LiteralPath $RepoRoot try { - $rustupOutput = @(& rustup show active-toolchain 2>&1) + $rustupOutput = @(& rustup show active-toolchain --verbose 2>&1) if ($LASTEXITCODE -ne 0) { throw "anvil: rustup could not resolve the repository toolchain file:`n$($rustupOutput -join "`n")" } - $active = $rustupOutput | Select-Object -Last 1 + # Verbose output puts the exact toolchain name on its own first line, + # avoiding ambiguity between spaces/parentheses in path toolchains and + # rustup's human-readable reason annotation. + $active = $rustupOutput | Select-Object -First 1 if ([string]::IsNullOrWhiteSpace([string] $active)) { throw 'anvil: rustup returned no active repository toolchain' } } finally { Pop-Location } - $resolved = [string] $active - $sourceMarker = $resolved.IndexOf(' (') - if ($sourceMarker -ge 0) { - $resolved = $resolved.Substring(0, $sourceMarker) - } - Write-Output ('+' + $resolved) + Write-Output ('+' + [string] $active) return } diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap index afe542511..ab56b7ee5 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap @@ -5902,23 +5902,21 @@ if ((Test-Path -LiteralPath (Join-Path $RepoRoot 'rust-toolchain') -PathType Lea (Test-Path -LiteralPath (Join-Path $RepoRoot 'rust-toolchain.toml') -PathType Leaf)) { Push-Location -LiteralPath $RepoRoot try { - $rustupOutput = @(& rustup show active-toolchain 2>&1) + $rustupOutput = @(& rustup show active-toolchain --verbose 2>&1) if ($LASTEXITCODE -ne 0) { throw "anvil: rustup could not resolve the repository toolchain file:`n$($rustupOutput -join "`n")" } - $active = $rustupOutput | Select-Object -Last 1 + # Verbose output puts the exact toolchain name on its own first line, + # avoiding ambiguity between spaces/parentheses in path toolchains and + # rustup's human-readable reason annotation. + $active = $rustupOutput | Select-Object -First 1 if ([string]::IsNullOrWhiteSpace([string] $active)) { throw 'anvil: rustup returned no active repository toolchain' } } finally { Pop-Location } - $resolved = [string] $active - $sourceMarker = $resolved.IndexOf(' (') - if ($sourceMarker -ge 0) { - $resolved = $resolved.Substring(0, $sourceMarker) - } - Write-Output ('+' + $resolved) + Write-Output ('+' + [string] $active) return } diff --git a/justfiles/anvil/versions.just b/justfiles/anvil/versions.just index 16a523aaa..f984db8c7 100644 --- a/justfiles/anvil/versions.just +++ b/justfiles/anvil/versions.just @@ -50,23 +50,21 @@ if ((Test-Path -LiteralPath (Join-Path $RepoRoot 'rust-toolchain') -PathType Lea (Test-Path -LiteralPath (Join-Path $RepoRoot 'rust-toolchain.toml') -PathType Leaf)) { Push-Location -LiteralPath $RepoRoot try { - $rustupOutput = @(& rustup show active-toolchain 2>&1) + $rustupOutput = @(& rustup show active-toolchain --verbose 2>&1) if ($LASTEXITCODE -ne 0) { throw "anvil: rustup could not resolve the repository toolchain file:`n$($rustupOutput -join "`n")" } - $active = $rustupOutput | Select-Object -Last 1 + # Verbose output puts the exact toolchain name on its own first line, + # avoiding ambiguity between spaces/parentheses in path toolchains and + # rustup's human-readable reason annotation. + $active = $rustupOutput | Select-Object -First 1 if ([string]::IsNullOrWhiteSpace([string] $active)) { throw 'anvil: rustup returned no active repository toolchain' } } finally { Pop-Location } - $resolved = [string] $active - $sourceMarker = $resolved.IndexOf(' (') - if ($sourceMarker -ge 0) { - $resolved = $resolved.Substring(0, $sourceMarker) - } - Write-Output ('+' + $resolved) + Write-Output ('+' + [string] $active) return } From a6592f76faa8ef55cf61a159bb4cf9256e86c97f Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Mon, 31 Aug 2026 14:08:46 +0200 Subject: [PATCH 24/32] Simplify stable toolchain selection Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: f7996e6f-5c20-4d7a-b2f7-690af95e557e --- .anvil.lock | 8 +- crates/cargo-anvil/docs/design/checks.md | 9 +- crates/cargo-anvil/docs/design/github.md | 10 +- crates/cargo-anvil/docs/design/local.md | 14 +-- .../src/anvil/artifacts/justfile.rs | 96 ++++++---------- .../templates/justfiles/anvil/impact.just | 25 ++--- .../templates/justfiles/anvil/tools.just | 61 +++++----- .../templates/justfiles/anvil/versions.just | 18 --- crates/cargo-anvil/tests/impact.rs | 22 ++-- crates/cargo-anvil/tests/recipe_contracts.rs | 4 +- .../snapshots/snapshots__ado_backend.snap | 104 +++++++----------- .../snapshots/snapshots__github_backend.snap | 104 +++++++----------- .../snapshots/snapshots__local_only.snap | 104 +++++++----------- justfiles/anvil/impact.just | 25 ++--- justfiles/anvil/tools.just | 61 +++++----- justfiles/anvil/versions.just | 18 --- 16 files changed, 268 insertions(+), 415 deletions(-) diff --git a/.anvil.lock b/.anvil.lock index 75434df9c..ea179c917 100644 --- a/.anvil.lock +++ b/.anvil.lock @@ -1,7 +1,7 @@ version = 1 tool = "anvil" tool_version = "0.5.0" -catalog_checksum = "sha256:c729f558e395268c2b0532d6b35087e2997c8a3868d505892e8d87d2bcc35473" +catalog_checksum = "sha256:155ac0ea6d212a1067c38cbbd85d8cba29a384c84c6ed338a499d0a763dd3382" [[file]] path = ".anvil/container/Containerfile" @@ -245,7 +245,7 @@ checksum = "sha256:6b71379310b986e9a44000ba9d072c3f8934397a0f7fb1bf27a31a7742ca7 [[file]] path = "justfiles/anvil/impact.just" -checksum = "sha256:efcee10ddda4d4dd7be46a36187e5bb56c05fb069776fba377a227556bccc2eb" +checksum = "sha256:0b3a5c96dd33384b4c86ac02b17396bc1a9fe9c82edbc516a2ceea7f4fe3833d" [[file]] path = "justfiles/anvil/mod.just" @@ -261,11 +261,11 @@ checksum = "sha256:5b2d91569f3fe87cd5f516623a01b5c7ae855ad587870ef9a2ae6d5619dd9 [[file]] path = "justfiles/anvil/tools.just" -checksum = "sha256:3f5d1ceb41d9b879a2944e59ee0f780cd1798ad7fa52956d717614180002db9b" +checksum = "sha256:efa6a28240fcc28a05b28acbca1191a38aed701a5f037dc81b91cc1c9b36ce4a" [[file]] path = "justfiles/anvil/versions.just" -checksum = "sha256:6d031b01aec542b93d698d236bcf559f5baeb8bf910a33b94fba41224cb76298" +checksum = "sha256:4dfafce9c45382c01e1e58119d290b4f904b3c77d4681e275ed9125530e458a7" [[region]] host = ".delta.toml" diff --git a/crates/cargo-anvil/docs/design/checks.md b/crates/cargo-anvil/docs/design/checks.md index 61bed0901..5162b0f68 100644 --- a/crates/cargo-anvil/docs/design/checks.md +++ b/crates/cargo-anvil/docs/design/checks.md @@ -165,10 +165,11 @@ that provided the strongest version of the check. Invocations shown without a pinned nightly or MSRV use the selected stable compiler. Their generated PowerShell recipes invoke Cargo directly with the -lazy optional stable argument: an explicit `+toolchain` for a caller override -or root MSRV, or the concrete toolchain that rustup resolves from a repository -toolchain file. The shared inline PowerShell array expression evaluates in that -same recipe process without a host-OS branch. Its repository root is fixed by +lazy optional stable argument. Caller-provided `RUSTUP_TOOLCHAIN` and a +repository-root toolchain file remain native rustup inputs and produce no +argument; only the root MSRV fallback produces an explicit `+toolchain`. The +shared inline PowerShell array expression evaluates in that same recipe process +without a host-OS branch. Its repository root is fixed by `justfile_directory()`, so checks that enter crate directories retain the same selection as setup. They do not route the main command through a nested Just recipe. diff --git a/crates/cargo-anvil/docs/design/github.md b/crates/cargo-anvil/docs/design/github.md index 94535d658..8d3b06963 100644 --- a/crates/cargo-anvil/docs/design/github.md +++ b/crates/cargo-anvil/docs/design/github.md @@ -846,10 +846,12 @@ The check → bucket mapping is in The setup action bootstraps cargo-binstall and Just using the runner environment, then calls `_anvil-stable-rustc-version`. That helper depends on `anvil-toolchain-stable-install`, so the setup graph provisions a missing -public selection and replays applicable file options before invoking `rustc` -directly with the lazy optional `+toolchain` argument. The resulting version -keys the cache; the workflow never invokes `_anvil-resolve-stable` itself and -does not publish a resolved `RUSTUP_TOOLCHAIN` through `GITHUB_ENV`. +public MSRV or lets rustup process a repository toolchain file before invoking +`rustc` directly with the lazy optional `+toolchain` argument. Native rustup +selectors produce no argument; only the root MSRV fallback is explicit. The +resulting version keys the cache; the workflow never invokes +`_anvil-resolve-stable` itself and does not publish a resolved +`RUSTUP_TOOLCHAIN` through `GITHUB_ENV`. Each stable recipe lazily selects the same optional argument and invokes Cargo or Rust directly in its current PowerShell process. The selector is an inline PowerShell array expression rather than an expression-time shell subprocess, so diff --git a/crates/cargo-anvil/docs/design/local.md b/crates/cargo-anvil/docs/design/local.md index e641430d4..fe2e3cd25 100644 --- a/crates/cargo-anvil/docs/design/local.md +++ b/crates/cargo-anvil/docs/design/local.md @@ -416,12 +416,10 @@ toolchain file is invalid and produces an actionable error. `versions.just` holds the selector as the lazy, non-exported `_anvil_stable_toolchain_args` value. Its value is a multiline PowerShell array expression, not a command for Just's expression-time `shell()` function. Just -interpolates the expression into an existing PowerShell recipe, where it -evaluates to one `+toolchain` string. For a repository toolchain file, it asks -`rustup show active-toolchain` from the repository root and uses rustup's -resolved concrete toolchain. For an override or MSRV fallback, it uses that -value directly. Toolchain names, including apostrophes, therefore remain -runtime string data rather than source text that needs escaping or reparsing. +interpolates the expression into an existing PowerShell recipe. A +caller-provided `RUSTUP_TOOLCHAIN` or repository-root toolchain file evaluates +to an empty array because rustup already consumes those inputs natively. Only +the synthesized root MSRV fallback evaluates to one `+toolchain` string. `RUSTUP_TOOLCHAIN` is an input to selection, never a globally exported output. The expression embeds an escaped PowerShell literal produced from `justfile_directory()`, matching the setup resolver's repository root. @@ -433,8 +431,8 @@ recipe process, for example `& cargo {{_anvil_stable_toolchain_args}} clippy ...`. Repository toolchain-file provisioning runs `rustc` from the repository root, so rustup natively owns the file's channel, path, profile, components, and targets. Default Clippy and -rustfmt setup then passes `--toolchain` for the resolved concrete toolchain, so -the component is installed on the compiler their checks probe. Nightly and +rustfmt setup likewise lets native selectors choose the component target, +adding `--toolchain` only for the synthesized MSRV fallback. Nightly and MSRV-specific checks keep their own explicit `+toolchain` arguments. No nested Just wrapper sits on the main Cargo/Rust command path. Selection has no OS-specific branch or host-shell quoting and does not change the adopter's diff --git a/crates/cargo-anvil/src/anvil/artifacts/justfile.rs b/crates/cargo-anvil/src/anvil/artifacts/justfile.rs index 9568210e2..e76bd2f2b 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/justfile.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/justfile.rs @@ -745,7 +745,7 @@ mod tests { VERSIONS_JUST .contains("_anvil_stable_toolchain_args := trim(\"@(& {\\n$RepoRoot = '\" + replace(justfile_directory(), \"'\", \"''\")") ); - assert!(VERSIONS_JUST.contains("Write-Output ('+' + $env:RUSTUP_TOOLCHAIN)")); + assert!(!VERSIONS_JUST.contains("Write-Output ('+' + $env:RUSTUP_TOOLCHAIN)")); assert!(!VERSIONS_JUST.contains("(Get-Location).Path")); assert!(!VERSIONS_JUST.contains("os_family()")); assert!(!VERSIONS_JUST.contains("shell(")); @@ -753,7 +753,7 @@ mod tests { assert!(TOOLS_JUST.contains("_anvil-resolve-stable action=\"install\":")); assert!(!TOOLS_JUST.contains("_anvil-with-stable")); assert!(TOOLS_JUST.contains("& cargo {{_anvil_stable_toolchain_args}}")); - assert!(VERSIONS_JUST.contains("rustup show active-toolchain")); + assert!(!VERSIONS_JUST.contains("rustup show active-toolchain")); assert!(TOOLS_JUST.contains("rustup component add --toolchain")); assert!(!TOOLS_JUST.contains("rustup target add --toolchain")); } @@ -969,8 +969,11 @@ mod tests { let root = temp.path(); assert_eq!(resolved(root, None), "@('+1.93')"); - assert_eq!(resolved(root, Some("custom-toolchain")), "@('+custom-toolchain')"); - assert_eq!(resolved(root, Some("team's-toolchain")), "@('+team''s-toolchain')"); + assert_eq!(resolved(root, Some("custom-toolchain")), "@()"); + assert_eq!(resolved(root, Some("team's-toolchain")), "@()"); + + fs::write(root.join("rust-toolchain.toml"), "[toolchain]\nchannel = \"1.94\"\n").expect("toolchain fixture must be writable"); + assert_eq!(resolved(root, None), "@()"); } #[test] @@ -983,11 +986,6 @@ mod tests { #[cfg(windows)] { fs::write(shim.path().join("cargo.cmd"), "@echo off\r\necho %*\r\nexit /b 0\r\n").expect("Cargo shim must be writable"); - fs::write( - shim.path().join("rustup.cmd"), - "@echo off\r\nif \"%*\"==\"show active-toolchain --verbose\" echo C:\\Program Files ^(x86^)\\custom rust\r\nexit /b 0\r\n", - ) - .expect("rustup shim must be writable"); } #[cfg(unix)] { @@ -996,13 +994,6 @@ mod tests { let cargo = shim.path().join("cargo"); fs::write(&cargo, "#!/bin/sh\nprintf '%s\\n' \"$*\"\nexit 0\n").expect("Cargo shim must be writable"); fs::set_permissions(&cargo, fs::Permissions::from_mode(0o755)).expect("Cargo shim must be executable"); - let rustup = shim.path().join("rustup"); - fs::write( - &rustup, - "#!/bin/sh\nif [ \"$*\" = 'show active-toolchain --verbose' ]; then echo '/toolchains/test (fork) toolchain'; fi\nexit 0\n", - ) - .expect("rustup shim must be writable"); - fs::set_permissions(&rustup, fs::Permissions::from_mode(0o755)).expect("rustup shim must be executable"); } let mut paths = vec![shim.path().to_path_buf()]; paths.extend(env::split_paths(&env::var_os("PATH").unwrap_or_default())); @@ -1021,7 +1012,7 @@ mod tests { ); assert_eq!( String::from_utf8(output.stdout).expect("Cargo shim output must be UTF-8").trim(), - "+team's-toolchain --version" + "--version" ); fs::write(temp.path().join("rust-toolchain.toml"), "[toolchain]\ncomponents = [\"clippy\"]\n") @@ -1036,14 +1027,9 @@ mod tests { "repository-selected Cargo command failed: {}", normalized_diagnostic(&output) ); - let expected_file_toolchain = if cfg!(windows) { - "\"+C:\\Program Files (x86)\\custom rust\" --version" - } else { - "+/toolchains/test (fork) toolchain --version" - }; assert_eq!( String::from_utf8(output.stdout).expect("Cargo shim output must be UTF-8").trim(), - expected_file_toolchain + "--version" ); fs::remove_file(temp.path().join("rust-toolchain.toml")).expect("toolchain fixture must be removable"); @@ -1083,29 +1069,6 @@ mod tests { ); } - #[test] - fn rejects_empty_legacy_toolchain_files_with_an_actionable_error() { - if !tools_available() { - return; - } - for contents in ["", " \r\n\t"] { - let temp = fixture("[workspace.package]\nrust-version = \"1.93\"\n"); - fs::write(temp.path().join("rust-toolchain"), contents).expect("toolchain fixture must be writable"); - - let output = run(temp.path(), &[], None); - assert!(!output.status.success(), "empty toolchain file must fail resolution"); - let diagnostic = normalized_diagnostic(&output); - assert!( - diagnostic.contains("rustup could not resolve") - && diagnostic.contains("override") - && (diagnostic.contains("empty") - || diagnostic.contains("could not parse") - || (diagnostic.contains("error") && diagnostic.contains("parsing"))), - "unexpected resolver diagnostic: {diagnostic}" - ); - } - } - #[test] fn installs_default_components_on_the_selected_stable_toolchain() { if !tools_available() { @@ -1124,7 +1087,7 @@ mod tests { .expect("Cargo shim must be writable"); fs::write( shim.path().join("rustup.cmd"), - "@echo off\r\n>>\"%ANVIL_RUSTUP_LOG%\" echo %*\r\nif \"%*\"==\"show active-toolchain --verbose\" echo 1.94-test-host\r\nexit /b 0\r\n", + "@echo off\r\n>>\"%ANVIL_RUSTUP_LOG%\" echo %*\r\nexit /b 0\r\n", ) .expect("rustup shim must be writable"); } @@ -1134,14 +1097,9 @@ mod tests { for (name, log_variable, status) in [("cargo", "ANVIL_CARGO_LOG", 1), ("rustup", "ANVIL_RUSTUP_LOG", 0)] { let executable = shim.path().join(name); - let extra = if name == "rustup" { - "if [ \"$*\" = 'show active-toolchain --verbose' ]; then echo '1.94-test-host'; fi\n" - } else { - "" - }; fs::write( &executable, - format!("#!/bin/sh\nprintf '%s\\n' \"$*\" >> \"${log_variable}\"\n{extra}exit {status}\n"), + format!("#!/bin/sh\nprintf '%s\\n' \"$*\" >> \"${log_variable}\"\nexit {status}\n"), ) .expect("component shim must be writable"); fs::set_permissions(&executable, fs::Permissions::from_mode(0o755)).expect("component shim must be executable"); @@ -1171,11 +1129,11 @@ mod tests { ); assert_eq!( fs::read_to_string(&cargo_log).expect("Cargo shim log must be readable").trim(), - "+custom-toolchain clippy --version" + "clippy --version" ); assert_eq!( fs::read_to_string(&rustup_log).expect("rustup shim log must be readable").trim(), - "component add --toolchain custom-toolchain clippy" + "component add clippy" ); fs::write(temp.path().join("rust-toolchain.toml"), "[toolchain]\nchannel = \"1.94\"\n") @@ -1190,17 +1148,29 @@ mod tests { ); assert_eq!( fs::read_to_string(&cargo_log).expect("Cargo shim log must be readable").trim(), - "+1.94-test-host clippy --version" + "clippy --version" ); - let rustup_call = fs::read_to_string(&rustup_log).expect("rustup shim log must be readable"); - assert!( - rustup_call - .lines() - .any(|line| line == "component add --toolchain 1.94-test-host clippy") + assert_eq!( + fs::read_to_string(&rustup_log).expect("rustup shim log must be readable").trim(), + "component add clippy" ); + + fs::remove_file(temp.path().join("rust-toolchain.toml")).expect("toolchain fixture must be removable"); + fs::write(&cargo_log, "").expect("Cargo shim log must be resettable"); + fs::write(&rustup_log, "").expect("rustup shim log must be resettable"); + let output = run_install(None); assert!( - rustup_call.lines().any(|line| line == "show active-toolchain --verbose"), - "repository-selected component installation must ask rustup for the active toolchain" + output.status.success(), + "MSRV-selected component installation failed: {}", + normalized_diagnostic(&output) + ); + assert_eq!( + fs::read_to_string(&cargo_log).expect("Cargo shim log must be readable").trim(), + "+1.93 clippy --version" + ); + assert_eq!( + fs::read_to_string(&rustup_log).expect("rustup shim log must be readable").trim(), + "component add --toolchain 1.93 clippy" ); } diff --git a/crates/cargo-anvil/templates/justfiles/anvil/impact.just b/crates/cargo-anvil/templates/justfiles/anvil/impact.just index 4f05394c8..c9e7db74e 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/impact.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/impact.just @@ -239,25 +239,20 @@ _anvil-impact-snapshot: # the selected compiler from the current checkout to override any # different toolchain file in the baseline worktree. Environment/MSRV # selections are already valid rustup overrides. For a selecting - # toolchain file, ask rustup for the concrete active identifier so - # `path` selections are not mistaken for named toolchains. + # toolchain file, use the selected rustc sysroot as an unambiguous path + # toolchain. $stableArgs = {{_anvil_stable_toolchain_args}} - $baselineToolchain = if ($stableArgs.Count -eq 1) { - $stableArgs[0].Substring(1) + $baselineToolchain = if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { + $env:RUSTUP_TOOLCHAIN.Trim() + } elseif ($stableArgs.Count -eq 1) { + ([string] $stableArgs[0]).Substring(1) } else { - $null - } - if ($null -eq $baselineToolchain) { - if (-not (Get-Command rustup -ErrorAction SilentlyContinue)) { - Write-Error 'anvil-impact: rustup is required to resolve the active repository toolchain for the baseline snapshot' - exit 1 - } - $active = (rustup show active-toolchain 2>$null) - if (($LASTEXITCODE -ne 0) -or -not $active) { - Write-Error 'anvil-impact: failed to resolve the active repository toolchain for the baseline snapshot' + $sysroot = @(& rustc --print sysroot) + if ($LASTEXITCODE -ne 0 -or $sysroot.Count -ne 1 -or [string]::IsNullOrWhiteSpace([string] $sysroot[0])) { + Write-Error 'anvil-impact: rustc could not report the current checkout stable toolchain sysroot' exit 1 } - $baselineToolchain = (($active | Select-Object -First 1) -split '\s+')[0] + ([string] $sysroot[0]).Trim() } # Temp-root precedence follows the runner-managed scratch dir on each diff --git a/crates/cargo-anvil/templates/justfiles/anvil/tools.just b/crates/cargo-anvil/templates/justfiles/anvil/tools.just index dc2409c73..194a7172c 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/tools.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/tools.just @@ -294,16 +294,16 @@ _anvil-resolve-stable action="install": return [pscustomobject]@{ Value = $msrv; Source = 'msrv'; Mapped = $false } } - function Install-Toolchain([object] $toolchain, [string] $label) { + function Install-Toolchain([string] $toolchain, [string] $label) { $installed = rustup toolchain list if ($LASTEXITCODE -ne 0) { throw 'anvil: rustup toolchain list failed' } - if (($installed -join "`n") -notmatch "(?m)^$([regex]::Escape($toolchain.Value))(?:-|$)") { - Write-Host "anvil: installing $label toolchain '$($toolchain.Value)'" - rustup toolchain install $toolchain.Value --profile minimal + if (($installed -join "`n") -notmatch "(?m)^$([regex]::Escape($toolchain))(?:-|$)") { + Write-Host "anvil: installing $label toolchain '$toolchain'" + rustup toolchain install $toolchain --profile minimal if ($LASTEXITCODE -ne 0) { - throw "anvil: failed to install $label toolchain '$($toolchain.Value)'" + throw "anvil: failed to install $label toolchain '$toolchain'" } } } @@ -329,7 +329,7 @@ _anvil-resolve-stable action="install": throw "anvil: mapped MSRV toolchain '$($msrvSelection.Value)' is unavailable; provision ANVIL_MSRV_TOOLCHAIN or unset it to let Anvil install the declared public MSRV" } } else { - Install-Toolchain $msrvSelection 'MSRV' + Install-Toolchain $msrvSelection.Value 'MSRV' } } else { $msrvSelection.Value @@ -337,37 +337,34 @@ _anvil-resolve-stable action="install": exit 0 } + if ($action -ne 'install') { + throw "anvil: unsupported stable resolver action '$action'" + } + Assert-CompleteInternalToolchainConfiguration - $selection = if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { - [pscustomobject]@{ Value = $env:RUSTUP_TOOLCHAIN; Source = 'environment' } - } elseif (Test-RootToolchainFile) { - [pscustomobject]@{ Value = $null; Source = 'file' } - } else { - [pscustomobject]@{ Value = Get-RootMsrv; Source = 'msrv' } + if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { + & cargo --version *> $null + if ($LASTEXITCODE -ne 0) { + throw "anvil: selected stable toolchain '$($env:RUSTUP_TOOLCHAIN)' is unavailable; provision RUSTUP_TOOLCHAIN or unset it" + } + exit 0 } - if ($action -eq 'install') { - if ($selection.Source -eq 'file') { - Push-Location -LiteralPath $repoRoot - try { - & rustc --version - if ($LASTEXITCODE -ne 0) { - throw 'anvil: rustup could not provision the repository toolchain file' - } - } finally { - Pop-Location - } - } elseif ($selection.Source -eq 'environment') { - & cargo "+$($selection.Value)" --version *> $null + if (Test-RootToolchainFile) { + Push-Location -LiteralPath $repoRoot + try { + & rustc --version if ($LASTEXITCODE -ne 0) { - throw "anvil: selected stable toolchain '$($selection.Value)' is unavailable; provision RUSTUP_TOOLCHAIN or unset it" + throw 'anvil: rustup could not provision the repository toolchain file' } - } else { - Install-Toolchain $selection 'stable' + } finally { + Pop-Location } exit 0 } + Install-Toolchain (Get-RootMsrv) 'stable' + # Provision the selected stable toolchain. Repository toolchain files are # processed natively by rustup. This is the shared prerequisite for setup paths # that execute stable Cargo/Rust or install Cargo tools; Just deduplicates it @@ -537,7 +534,8 @@ _check-toolchain toolchain: # the literal "default" (Anvil's selected stable toolchain) or a specific pinned # toolchain string (which must already be installed -- the per-component setup # recipes ensure this via a dependency on anvil--install). Explicit -# stable selections target `rustup component add --toolchain`. +# MSRV fallback selections target `rustup component add --toolchain`; native +# rustup selections use `rustup component add` without restating the selector. # # Probe-first: if the component is already available, skip the # `rustup component add`. This keeps the recipe idempotent and robust @@ -609,9 +607,12 @@ _install-component toolchain component: exit 0 } - if ($toolchain -eq 'default') { + if ($toolchain -eq 'default' -and $null -ne $selectedStableToolchain) { Write-Host "rustup component add --toolchain $selectedStableToolchain $component" rustup component add --toolchain $selectedStableToolchain $component + } elseif ($toolchain -eq 'default') { + Write-Host "rustup component add $component" + rustup component add $component } else { Write-Host "rustup component add --toolchain $toolchain $component" rustup component add --toolchain $toolchain $component diff --git a/crates/cargo-anvil/templates/justfiles/anvil/versions.just b/crates/cargo-anvil/templates/justfiles/anvil/versions.just index f984db8c7..91b42db6e 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/versions.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/versions.just @@ -42,29 +42,11 @@ $ErrorActionPreference = 'Stop' Set-StrictMode -Version 3 if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { - Write-Output ('+' + $env:RUSTUP_TOOLCHAIN) return } if ((Test-Path -LiteralPath (Join-Path $RepoRoot 'rust-toolchain') -PathType Leaf) -or (Test-Path -LiteralPath (Join-Path $RepoRoot 'rust-toolchain.toml') -PathType Leaf)) { - Push-Location -LiteralPath $RepoRoot - try { - $rustupOutput = @(& rustup show active-toolchain --verbose 2>&1) - if ($LASTEXITCODE -ne 0) { - throw "anvil: rustup could not resolve the repository toolchain file:`n$($rustupOutput -join "`n")" - } - # Verbose output puts the exact toolchain name on its own first line, - # avoiding ambiguity between spaces/parentheses in path toolchains and - # rustup's human-readable reason annotation. - $active = $rustupOutput | Select-Object -First 1 - if ([string]::IsNullOrWhiteSpace([string] $active)) { - throw 'anvil: rustup returned no active repository toolchain' - } - } finally { - Pop-Location - } - Write-Output ('+' + [string] $active) return } diff --git a/crates/cargo-anvil/tests/impact.rs b/crates/cargo-anvil/tests/impact.rs index fc695e359..0cdf2c155 100644 --- a/crates/cargo-anvil/tests/impact.rs +++ b/crates/cargo-anvil/tests/impact.rs @@ -1435,17 +1435,8 @@ fn snapshot_toolchain_probe(caller_toolchain: Option<&str>, toolchain_file: bool exit 0\n", ), ( - "rustup.ps1", - "if (($args -contains 'show') -and ($args -contains 'active-toolchain')) {\n\ - \x20 if ($args -contains '--verbose') {\n\ - \x20 Write-Output 'file-active-toolchain'\n\ - \x20 Write-Output 'active because: overridden by rust-toolchain.toml'\n\ - \x20 } else {\n\ - \x20 Write-Output 'file-active-toolchain (overridden by rust-toolchain.toml)'\n\ - \x20 }\n\ - \x20 exit 0\n\ - }\n\ - exit 0\n", + "rustc.ps1", + "if ($args -contains 'sysroot') { Write-Output '/toolchains/test (fork) toolchain' }\nexit 0\n", ), ]); @@ -1492,12 +1483,13 @@ fn impact_snapshot_uses_current_checkout_toolchain_for_both_trees() { "baseline and current snapshots must both use Anvil's selected MSRV" ); - // Case C: rustup resolves the current checkout's toolchain file once and - // the concrete identifier selects both snapshots. + // Case C: the baseline receives the current checkout's selected rustc + // sysroot as an unambiguous path toolchain. The current snapshot needs no + // override because it runs under the selecting repository file itself. let with_file = snapshot_toolchain_probe(None, true); assert_eq!( with_file, - vec!["file-active-toolchain".to_owned(), "file-active-toolchain".to_owned()], - "baseline and current snapshots must both use the current file's concrete active toolchain" + vec!["/toolchains/test (fork) toolchain".to_owned(), "".to_owned()], + "the baseline must inherit the current file selection while the current snapshot uses it natively" ); } diff --git a/crates/cargo-anvil/tests/recipe_contracts.rs b/crates/cargo-anvil/tests/recipe_contracts.rs index 4dbe8e7df..293154e06 100644 --- a/crates/cargo-anvil/tests/recipe_contracts.rs +++ b/crates/cargo-anvil/tests/recipe_contracts.rs @@ -213,7 +213,7 @@ fn assert_failed(output: &Output, context: &str) { } #[test] -fn stable_command_runs_directly_with_explicit_toolchain_arguments() { +fn stable_command_leaves_environment_toolchain_selection_native() { if !tools_available() { return; } @@ -246,7 +246,7 @@ fn stable_command_runs_directly_with_explicit_toolchain_arguments() { String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr) ); - assert_eq!(fs::read_to_string(args_log).unwrap().trim(), "+test-stable doc2readme --check"); + assert_eq!(fs::read_to_string(args_log).unwrap().trim(), "doc2readme --check"); assert_eq!( fs::canonicalize(fs::read_to_string(cwd_log).unwrap().trim()).unwrap(), fs::canonicalize(tmp.path()).unwrap(), diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap index 72336d1d2..809bfa984 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap @@ -5503,25 +5503,20 @@ _anvil-impact-snapshot: # the selected compiler from the current checkout to override any # different toolchain file in the baseline worktree. Environment/MSRV # selections are already valid rustup overrides. For a selecting - # toolchain file, ask rustup for the concrete active identifier so - # `path` selections are not mistaken for named toolchains. + # toolchain file, use the selected rustc sysroot as an unambiguous path + # toolchain. $stableArgs = {{_anvil_stable_toolchain_args}} - $baselineToolchain = if ($stableArgs.Count -eq 1) { - $stableArgs[0].Substring(1) + $baselineToolchain = if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { + $env:RUSTUP_TOOLCHAIN.Trim() + } elseif ($stableArgs.Count -eq 1) { + ([string] $stableArgs[0]).Substring(1) } else { - $null - } - if ($null -eq $baselineToolchain) { - if (-not (Get-Command rustup -ErrorAction SilentlyContinue)) { - Write-Error 'anvil-impact: rustup is required to resolve the active repository toolchain for the baseline snapshot' - exit 1 - } - $active = (rustup show active-toolchain 2>$null) - if (($LASTEXITCODE -ne 0) -or -not $active) { - Write-Error 'anvil-impact: failed to resolve the active repository toolchain for the baseline snapshot' + $sysroot = @(& rustc --print sysroot) + if ($LASTEXITCODE -ne 0 -or $sysroot.Count -ne 1 -or [string]::IsNullOrWhiteSpace([string] $sysroot[0])) { + Write-Error 'anvil-impact: rustc could not report the current checkout stable toolchain sysroot' exit 1 } - $baselineToolchain = (($active | Select-Object -First 1) -split '\s+')[0] + ([string] $sysroot[0]).Trim() } # Temp-root precedence follows the runner-managed scratch dir on each @@ -6480,16 +6475,16 @@ _anvil-resolve-stable action="install": return [pscustomobject]@{ Value = $msrv; Source = 'msrv'; Mapped = $false } } - function Install-Toolchain([object] $toolchain, [string] $label) { + function Install-Toolchain([string] $toolchain, [string] $label) { $installed = rustup toolchain list if ($LASTEXITCODE -ne 0) { throw 'anvil: rustup toolchain list failed' } - if (($installed -join "`n") -notmatch "(?m)^$([regex]::Escape($toolchain.Value))(?:-|$)") { - Write-Host "anvil: installing $label toolchain '$($toolchain.Value)'" - rustup toolchain install $toolchain.Value --profile minimal + if (($installed -join "`n") -notmatch "(?m)^$([regex]::Escape($toolchain))(?:-|$)") { + Write-Host "anvil: installing $label toolchain '$toolchain'" + rustup toolchain install $toolchain --profile minimal if ($LASTEXITCODE -ne 0) { - throw "anvil: failed to install $label toolchain '$($toolchain.Value)'" + throw "anvil: failed to install $label toolchain '$toolchain'" } } } @@ -6515,7 +6510,7 @@ _anvil-resolve-stable action="install": throw "anvil: mapped MSRV toolchain '$($msrvSelection.Value)' is unavailable; provision ANVIL_MSRV_TOOLCHAIN or unset it to let Anvil install the declared public MSRV" } } else { - Install-Toolchain $msrvSelection 'MSRV' + Install-Toolchain $msrvSelection.Value 'MSRV' } } else { $msrvSelection.Value @@ -6523,37 +6518,34 @@ _anvil-resolve-stable action="install": exit 0 } + if ($action -ne 'install') { + throw "anvil: unsupported stable resolver action '$action'" + } + Assert-CompleteInternalToolchainConfiguration - $selection = if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { - [pscustomobject]@{ Value = $env:RUSTUP_TOOLCHAIN; Source = 'environment' } - } elseif (Test-RootToolchainFile) { - [pscustomobject]@{ Value = $null; Source = 'file' } - } else { - [pscustomobject]@{ Value = Get-RootMsrv; Source = 'msrv' } + if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { + & cargo --version *> $null + if ($LASTEXITCODE -ne 0) { + throw "anvil: selected stable toolchain '$($env:RUSTUP_TOOLCHAIN)' is unavailable; provision RUSTUP_TOOLCHAIN or unset it" + } + exit 0 } - if ($action -eq 'install') { - if ($selection.Source -eq 'file') { - Push-Location -LiteralPath $repoRoot - try { - & rustc --version - if ($LASTEXITCODE -ne 0) { - throw 'anvil: rustup could not provision the repository toolchain file' - } - } finally { - Pop-Location - } - } elseif ($selection.Source -eq 'environment') { - & cargo "+$($selection.Value)" --version *> $null + if (Test-RootToolchainFile) { + Push-Location -LiteralPath $repoRoot + try { + & rustc --version if ($LASTEXITCODE -ne 0) { - throw "anvil: selected stable toolchain '$($selection.Value)' is unavailable; provision RUSTUP_TOOLCHAIN or unset it" + throw 'anvil: rustup could not provision the repository toolchain file' } - } else { - Install-Toolchain $selection 'stable' + } finally { + Pop-Location } exit 0 } + Install-Toolchain (Get-RootMsrv) 'stable' + # Provision the selected stable toolchain. Repository toolchain files are # processed natively by rustup. This is the shared prerequisite for setup paths # that execute stable Cargo/Rust or install Cargo tools; Just deduplicates it @@ -6723,7 +6715,8 @@ _check-toolchain toolchain: # the literal "default" (Anvil's selected stable toolchain) or a specific pinned # toolchain string (which must already be installed -- the per-component setup # recipes ensure this via a dependency on anvil--install). Explicit -# stable selections target `rustup component add --toolchain`. +# MSRV fallback selections target `rustup component add --toolchain`; native +# rustup selections use `rustup component add` without restating the selector. # # Probe-first: if the component is already available, skip the # `rustup component add`. This keeps the recipe idempotent and robust @@ -6795,9 +6788,12 @@ _install-component toolchain component: exit 0 } - if ($toolchain -eq 'default') { + if ($toolchain -eq 'default' -and $null -ne $selectedStableToolchain) { Write-Host "rustup component add --toolchain $selectedStableToolchain $component" rustup component add --toolchain $selectedStableToolchain $component + } elseif ($toolchain -eq 'default') { + Write-Host "rustup component add $component" + rustup component add $component } else { Write-Host "rustup component add --toolchain $toolchain $component" rustup component add --toolchain $toolchain $component @@ -7235,29 +7231,11 @@ $ErrorActionPreference = 'Stop' Set-StrictMode -Version 3 if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { - Write-Output ('+' + $env:RUSTUP_TOOLCHAIN) return } if ((Test-Path -LiteralPath (Join-Path $RepoRoot 'rust-toolchain') -PathType Leaf) -or (Test-Path -LiteralPath (Join-Path $RepoRoot 'rust-toolchain.toml') -PathType Leaf)) { - Push-Location -LiteralPath $RepoRoot - try { - $rustupOutput = @(& rustup show active-toolchain --verbose 2>&1) - if ($LASTEXITCODE -ne 0) { - throw "anvil: rustup could not resolve the repository toolchain file:`n$($rustupOutput -join "`n")" - } - # Verbose output puts the exact toolchain name on its own first line, - # avoiding ambiguity between spaces/parentheses in path toolchains and - # rustup's human-readable reason annotation. - $active = $rustupOutput | Select-Object -First 1 - if ([string]::IsNullOrWhiteSpace([string] $active)) { - throw 'anvil: rustup returned no active repository toolchain' - } - } finally { - Pop-Location - } - Write-Output ('+' + [string] $active) return } diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap index ace55506d..9d847656e 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap @@ -5306,25 +5306,20 @@ _anvil-impact-snapshot: # the selected compiler from the current checkout to override any # different toolchain file in the baseline worktree. Environment/MSRV # selections are already valid rustup overrides. For a selecting - # toolchain file, ask rustup for the concrete active identifier so - # `path` selections are not mistaken for named toolchains. + # toolchain file, use the selected rustc sysroot as an unambiguous path + # toolchain. $stableArgs = {{_anvil_stable_toolchain_args}} - $baselineToolchain = if ($stableArgs.Count -eq 1) { - $stableArgs[0].Substring(1) + $baselineToolchain = if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { + $env:RUSTUP_TOOLCHAIN.Trim() + } elseif ($stableArgs.Count -eq 1) { + ([string] $stableArgs[0]).Substring(1) } else { - $null - } - if ($null -eq $baselineToolchain) { - if (-not (Get-Command rustup -ErrorAction SilentlyContinue)) { - Write-Error 'anvil-impact: rustup is required to resolve the active repository toolchain for the baseline snapshot' - exit 1 - } - $active = (rustup show active-toolchain 2>$null) - if (($LASTEXITCODE -ne 0) -or -not $active) { - Write-Error 'anvil-impact: failed to resolve the active repository toolchain for the baseline snapshot' + $sysroot = @(& rustc --print sysroot) + if ($LASTEXITCODE -ne 0 -or $sysroot.Count -ne 1 -or [string]::IsNullOrWhiteSpace([string] $sysroot[0])) { + Write-Error 'anvil-impact: rustc could not report the current checkout stable toolchain sysroot' exit 1 } - $baselineToolchain = (($active | Select-Object -First 1) -split '\s+')[0] + ([string] $sysroot[0]).Trim() } # Temp-root precedence follows the runner-managed scratch dir on each @@ -6283,16 +6278,16 @@ _anvil-resolve-stable action="install": return [pscustomobject]@{ Value = $msrv; Source = 'msrv'; Mapped = $false } } - function Install-Toolchain([object] $toolchain, [string] $label) { + function Install-Toolchain([string] $toolchain, [string] $label) { $installed = rustup toolchain list if ($LASTEXITCODE -ne 0) { throw 'anvil: rustup toolchain list failed' } - if (($installed -join "`n") -notmatch "(?m)^$([regex]::Escape($toolchain.Value))(?:-|$)") { - Write-Host "anvil: installing $label toolchain '$($toolchain.Value)'" - rustup toolchain install $toolchain.Value --profile minimal + if (($installed -join "`n") -notmatch "(?m)^$([regex]::Escape($toolchain))(?:-|$)") { + Write-Host "anvil: installing $label toolchain '$toolchain'" + rustup toolchain install $toolchain --profile minimal if ($LASTEXITCODE -ne 0) { - throw "anvil: failed to install $label toolchain '$($toolchain.Value)'" + throw "anvil: failed to install $label toolchain '$toolchain'" } } } @@ -6318,7 +6313,7 @@ _anvil-resolve-stable action="install": throw "anvil: mapped MSRV toolchain '$($msrvSelection.Value)' is unavailable; provision ANVIL_MSRV_TOOLCHAIN or unset it to let Anvil install the declared public MSRV" } } else { - Install-Toolchain $msrvSelection 'MSRV' + Install-Toolchain $msrvSelection.Value 'MSRV' } } else { $msrvSelection.Value @@ -6326,37 +6321,34 @@ _anvil-resolve-stable action="install": exit 0 } + if ($action -ne 'install') { + throw "anvil: unsupported stable resolver action '$action'" + } + Assert-CompleteInternalToolchainConfiguration - $selection = if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { - [pscustomobject]@{ Value = $env:RUSTUP_TOOLCHAIN; Source = 'environment' } - } elseif (Test-RootToolchainFile) { - [pscustomobject]@{ Value = $null; Source = 'file' } - } else { - [pscustomobject]@{ Value = Get-RootMsrv; Source = 'msrv' } + if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { + & cargo --version *> $null + if ($LASTEXITCODE -ne 0) { + throw "anvil: selected stable toolchain '$($env:RUSTUP_TOOLCHAIN)' is unavailable; provision RUSTUP_TOOLCHAIN or unset it" + } + exit 0 } - if ($action -eq 'install') { - if ($selection.Source -eq 'file') { - Push-Location -LiteralPath $repoRoot - try { - & rustc --version - if ($LASTEXITCODE -ne 0) { - throw 'anvil: rustup could not provision the repository toolchain file' - } - } finally { - Pop-Location - } - } elseif ($selection.Source -eq 'environment') { - & cargo "+$($selection.Value)" --version *> $null + if (Test-RootToolchainFile) { + Push-Location -LiteralPath $repoRoot + try { + & rustc --version if ($LASTEXITCODE -ne 0) { - throw "anvil: selected stable toolchain '$($selection.Value)' is unavailable; provision RUSTUP_TOOLCHAIN or unset it" + throw 'anvil: rustup could not provision the repository toolchain file' } - } else { - Install-Toolchain $selection 'stable' + } finally { + Pop-Location } exit 0 } + Install-Toolchain (Get-RootMsrv) 'stable' + # Provision the selected stable toolchain. Repository toolchain files are # processed natively by rustup. This is the shared prerequisite for setup paths # that execute stable Cargo/Rust or install Cargo tools; Just deduplicates it @@ -6526,7 +6518,8 @@ _check-toolchain toolchain: # the literal "default" (Anvil's selected stable toolchain) or a specific pinned # toolchain string (which must already be installed -- the per-component setup # recipes ensure this via a dependency on anvil--install). Explicit -# stable selections target `rustup component add --toolchain`. +# MSRV fallback selections target `rustup component add --toolchain`; native +# rustup selections use `rustup component add` without restating the selector. # # Probe-first: if the component is already available, skip the # `rustup component add`. This keeps the recipe idempotent and robust @@ -6598,9 +6591,12 @@ _install-component toolchain component: exit 0 } - if ($toolchain -eq 'default') { + if ($toolchain -eq 'default' -and $null -ne $selectedStableToolchain) { Write-Host "rustup component add --toolchain $selectedStableToolchain $component" rustup component add --toolchain $selectedStableToolchain $component + } elseif ($toolchain -eq 'default') { + Write-Host "rustup component add $component" + rustup component add $component } else { Write-Host "rustup component add --toolchain $toolchain $component" rustup component add --toolchain $toolchain $component @@ -7038,29 +7034,11 @@ $ErrorActionPreference = 'Stop' Set-StrictMode -Version 3 if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { - Write-Output ('+' + $env:RUSTUP_TOOLCHAIN) return } if ((Test-Path -LiteralPath (Join-Path $RepoRoot 'rust-toolchain') -PathType Leaf) -or (Test-Path -LiteralPath (Join-Path $RepoRoot 'rust-toolchain.toml') -PathType Leaf)) { - Push-Location -LiteralPath $RepoRoot - try { - $rustupOutput = @(& rustup show active-toolchain --verbose 2>&1) - if ($LASTEXITCODE -ne 0) { - throw "anvil: rustup could not resolve the repository toolchain file:`n$($rustupOutput -join "`n")" - } - # Verbose output puts the exact toolchain name on its own first line, - # avoiding ambiguity between spaces/parentheses in path toolchains and - # rustup's human-readable reason annotation. - $active = $rustupOutput | Select-Object -First 1 - if ([string]::IsNullOrWhiteSpace([string] $active)) { - throw 'anvil: rustup returned no active repository toolchain' - } - } finally { - Pop-Location - } - Write-Output ('+' + [string] $active) return } diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap index ab56b7ee5..914940ccd 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap @@ -4162,25 +4162,20 @@ _anvil-impact-snapshot: # the selected compiler from the current checkout to override any # different toolchain file in the baseline worktree. Environment/MSRV # selections are already valid rustup overrides. For a selecting - # toolchain file, ask rustup for the concrete active identifier so - # `path` selections are not mistaken for named toolchains. + # toolchain file, use the selected rustc sysroot as an unambiguous path + # toolchain. $stableArgs = {{_anvil_stable_toolchain_args}} - $baselineToolchain = if ($stableArgs.Count -eq 1) { - $stableArgs[0].Substring(1) + $baselineToolchain = if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { + $env:RUSTUP_TOOLCHAIN.Trim() + } elseif ($stableArgs.Count -eq 1) { + ([string] $stableArgs[0]).Substring(1) } else { - $null - } - if ($null -eq $baselineToolchain) { - if (-not (Get-Command rustup -ErrorAction SilentlyContinue)) { - Write-Error 'anvil-impact: rustup is required to resolve the active repository toolchain for the baseline snapshot' - exit 1 - } - $active = (rustup show active-toolchain 2>$null) - if (($LASTEXITCODE -ne 0) -or -not $active) { - Write-Error 'anvil-impact: failed to resolve the active repository toolchain for the baseline snapshot' + $sysroot = @(& rustc --print sysroot) + if ($LASTEXITCODE -ne 0 -or $sysroot.Count -ne 1 -or [string]::IsNullOrWhiteSpace([string] $sysroot[0])) { + Write-Error 'anvil-impact: rustc could not report the current checkout stable toolchain sysroot' exit 1 } - $baselineToolchain = (($active | Select-Object -First 1) -split '\s+')[0] + ([string] $sysroot[0]).Trim() } # Temp-root precedence follows the runner-managed scratch dir on each @@ -5139,16 +5134,16 @@ _anvil-resolve-stable action="install": return [pscustomobject]@{ Value = $msrv; Source = 'msrv'; Mapped = $false } } - function Install-Toolchain([object] $toolchain, [string] $label) { + function Install-Toolchain([string] $toolchain, [string] $label) { $installed = rustup toolchain list if ($LASTEXITCODE -ne 0) { throw 'anvil: rustup toolchain list failed' } - if (($installed -join "`n") -notmatch "(?m)^$([regex]::Escape($toolchain.Value))(?:-|$)") { - Write-Host "anvil: installing $label toolchain '$($toolchain.Value)'" - rustup toolchain install $toolchain.Value --profile minimal + if (($installed -join "`n") -notmatch "(?m)^$([regex]::Escape($toolchain))(?:-|$)") { + Write-Host "anvil: installing $label toolchain '$toolchain'" + rustup toolchain install $toolchain --profile minimal if ($LASTEXITCODE -ne 0) { - throw "anvil: failed to install $label toolchain '$($toolchain.Value)'" + throw "anvil: failed to install $label toolchain '$toolchain'" } } } @@ -5174,7 +5169,7 @@ _anvil-resolve-stable action="install": throw "anvil: mapped MSRV toolchain '$($msrvSelection.Value)' is unavailable; provision ANVIL_MSRV_TOOLCHAIN or unset it to let Anvil install the declared public MSRV" } } else { - Install-Toolchain $msrvSelection 'MSRV' + Install-Toolchain $msrvSelection.Value 'MSRV' } } else { $msrvSelection.Value @@ -5182,37 +5177,34 @@ _anvil-resolve-stable action="install": exit 0 } + if ($action -ne 'install') { + throw "anvil: unsupported stable resolver action '$action'" + } + Assert-CompleteInternalToolchainConfiguration - $selection = if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { - [pscustomobject]@{ Value = $env:RUSTUP_TOOLCHAIN; Source = 'environment' } - } elseif (Test-RootToolchainFile) { - [pscustomobject]@{ Value = $null; Source = 'file' } - } else { - [pscustomobject]@{ Value = Get-RootMsrv; Source = 'msrv' } + if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { + & cargo --version *> $null + if ($LASTEXITCODE -ne 0) { + throw "anvil: selected stable toolchain '$($env:RUSTUP_TOOLCHAIN)' is unavailable; provision RUSTUP_TOOLCHAIN or unset it" + } + exit 0 } - if ($action -eq 'install') { - if ($selection.Source -eq 'file') { - Push-Location -LiteralPath $repoRoot - try { - & rustc --version - if ($LASTEXITCODE -ne 0) { - throw 'anvil: rustup could not provision the repository toolchain file' - } - } finally { - Pop-Location - } - } elseif ($selection.Source -eq 'environment') { - & cargo "+$($selection.Value)" --version *> $null + if (Test-RootToolchainFile) { + Push-Location -LiteralPath $repoRoot + try { + & rustc --version if ($LASTEXITCODE -ne 0) { - throw "anvil: selected stable toolchain '$($selection.Value)' is unavailable; provision RUSTUP_TOOLCHAIN or unset it" + throw 'anvil: rustup could not provision the repository toolchain file' } - } else { - Install-Toolchain $selection 'stable' + } finally { + Pop-Location } exit 0 } + Install-Toolchain (Get-RootMsrv) 'stable' + # Provision the selected stable toolchain. Repository toolchain files are # processed natively by rustup. This is the shared prerequisite for setup paths # that execute stable Cargo/Rust or install Cargo tools; Just deduplicates it @@ -5382,7 +5374,8 @@ _check-toolchain toolchain: # the literal "default" (Anvil's selected stable toolchain) or a specific pinned # toolchain string (which must already be installed -- the per-component setup # recipes ensure this via a dependency on anvil--install). Explicit -# stable selections target `rustup component add --toolchain`. +# MSRV fallback selections target `rustup component add --toolchain`; native +# rustup selections use `rustup component add` without restating the selector. # # Probe-first: if the component is already available, skip the # `rustup component add`. This keeps the recipe idempotent and robust @@ -5454,9 +5447,12 @@ _install-component toolchain component: exit 0 } - if ($toolchain -eq 'default') { + if ($toolchain -eq 'default' -and $null -ne $selectedStableToolchain) { Write-Host "rustup component add --toolchain $selectedStableToolchain $component" rustup component add --toolchain $selectedStableToolchain $component + } elseif ($toolchain -eq 'default') { + Write-Host "rustup component add $component" + rustup component add $component } else { Write-Host "rustup component add --toolchain $toolchain $component" rustup component add --toolchain $toolchain $component @@ -5894,29 +5890,11 @@ $ErrorActionPreference = 'Stop' Set-StrictMode -Version 3 if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { - Write-Output ('+' + $env:RUSTUP_TOOLCHAIN) return } if ((Test-Path -LiteralPath (Join-Path $RepoRoot 'rust-toolchain') -PathType Leaf) -or (Test-Path -LiteralPath (Join-Path $RepoRoot 'rust-toolchain.toml') -PathType Leaf)) { - Push-Location -LiteralPath $RepoRoot - try { - $rustupOutput = @(& rustup show active-toolchain --verbose 2>&1) - if ($LASTEXITCODE -ne 0) { - throw "anvil: rustup could not resolve the repository toolchain file:`n$($rustupOutput -join "`n")" - } - # Verbose output puts the exact toolchain name on its own first line, - # avoiding ambiguity between spaces/parentheses in path toolchains and - # rustup's human-readable reason annotation. - $active = $rustupOutput | Select-Object -First 1 - if ([string]::IsNullOrWhiteSpace([string] $active)) { - throw 'anvil: rustup returned no active repository toolchain' - } - } finally { - Pop-Location - } - Write-Output ('+' + [string] $active) return } diff --git a/justfiles/anvil/impact.just b/justfiles/anvil/impact.just index 4f05394c8..c9e7db74e 100644 --- a/justfiles/anvil/impact.just +++ b/justfiles/anvil/impact.just @@ -239,25 +239,20 @@ _anvil-impact-snapshot: # the selected compiler from the current checkout to override any # different toolchain file in the baseline worktree. Environment/MSRV # selections are already valid rustup overrides. For a selecting - # toolchain file, ask rustup for the concrete active identifier so - # `path` selections are not mistaken for named toolchains. + # toolchain file, use the selected rustc sysroot as an unambiguous path + # toolchain. $stableArgs = {{_anvil_stable_toolchain_args}} - $baselineToolchain = if ($stableArgs.Count -eq 1) { - $stableArgs[0].Substring(1) + $baselineToolchain = if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { + $env:RUSTUP_TOOLCHAIN.Trim() + } elseif ($stableArgs.Count -eq 1) { + ([string] $stableArgs[0]).Substring(1) } else { - $null - } - if ($null -eq $baselineToolchain) { - if (-not (Get-Command rustup -ErrorAction SilentlyContinue)) { - Write-Error 'anvil-impact: rustup is required to resolve the active repository toolchain for the baseline snapshot' - exit 1 - } - $active = (rustup show active-toolchain 2>$null) - if (($LASTEXITCODE -ne 0) -or -not $active) { - Write-Error 'anvil-impact: failed to resolve the active repository toolchain for the baseline snapshot' + $sysroot = @(& rustc --print sysroot) + if ($LASTEXITCODE -ne 0 -or $sysroot.Count -ne 1 -or [string]::IsNullOrWhiteSpace([string] $sysroot[0])) { + Write-Error 'anvil-impact: rustc could not report the current checkout stable toolchain sysroot' exit 1 } - $baselineToolchain = (($active | Select-Object -First 1) -split '\s+')[0] + ([string] $sysroot[0]).Trim() } # Temp-root precedence follows the runner-managed scratch dir on each diff --git a/justfiles/anvil/tools.just b/justfiles/anvil/tools.just index dc2409c73..194a7172c 100644 --- a/justfiles/anvil/tools.just +++ b/justfiles/anvil/tools.just @@ -294,16 +294,16 @@ _anvil-resolve-stable action="install": return [pscustomobject]@{ Value = $msrv; Source = 'msrv'; Mapped = $false } } - function Install-Toolchain([object] $toolchain, [string] $label) { + function Install-Toolchain([string] $toolchain, [string] $label) { $installed = rustup toolchain list if ($LASTEXITCODE -ne 0) { throw 'anvil: rustup toolchain list failed' } - if (($installed -join "`n") -notmatch "(?m)^$([regex]::Escape($toolchain.Value))(?:-|$)") { - Write-Host "anvil: installing $label toolchain '$($toolchain.Value)'" - rustup toolchain install $toolchain.Value --profile minimal + if (($installed -join "`n") -notmatch "(?m)^$([regex]::Escape($toolchain))(?:-|$)") { + Write-Host "anvil: installing $label toolchain '$toolchain'" + rustup toolchain install $toolchain --profile minimal if ($LASTEXITCODE -ne 0) { - throw "anvil: failed to install $label toolchain '$($toolchain.Value)'" + throw "anvil: failed to install $label toolchain '$toolchain'" } } } @@ -329,7 +329,7 @@ _anvil-resolve-stable action="install": throw "anvil: mapped MSRV toolchain '$($msrvSelection.Value)' is unavailable; provision ANVIL_MSRV_TOOLCHAIN or unset it to let Anvil install the declared public MSRV" } } else { - Install-Toolchain $msrvSelection 'MSRV' + Install-Toolchain $msrvSelection.Value 'MSRV' } } else { $msrvSelection.Value @@ -337,37 +337,34 @@ _anvil-resolve-stable action="install": exit 0 } + if ($action -ne 'install') { + throw "anvil: unsupported stable resolver action '$action'" + } + Assert-CompleteInternalToolchainConfiguration - $selection = if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { - [pscustomobject]@{ Value = $env:RUSTUP_TOOLCHAIN; Source = 'environment' } - } elseif (Test-RootToolchainFile) { - [pscustomobject]@{ Value = $null; Source = 'file' } - } else { - [pscustomobject]@{ Value = Get-RootMsrv; Source = 'msrv' } + if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { + & cargo --version *> $null + if ($LASTEXITCODE -ne 0) { + throw "anvil: selected stable toolchain '$($env:RUSTUP_TOOLCHAIN)' is unavailable; provision RUSTUP_TOOLCHAIN or unset it" + } + exit 0 } - if ($action -eq 'install') { - if ($selection.Source -eq 'file') { - Push-Location -LiteralPath $repoRoot - try { - & rustc --version - if ($LASTEXITCODE -ne 0) { - throw 'anvil: rustup could not provision the repository toolchain file' - } - } finally { - Pop-Location - } - } elseif ($selection.Source -eq 'environment') { - & cargo "+$($selection.Value)" --version *> $null + if (Test-RootToolchainFile) { + Push-Location -LiteralPath $repoRoot + try { + & rustc --version if ($LASTEXITCODE -ne 0) { - throw "anvil: selected stable toolchain '$($selection.Value)' is unavailable; provision RUSTUP_TOOLCHAIN or unset it" + throw 'anvil: rustup could not provision the repository toolchain file' } - } else { - Install-Toolchain $selection 'stable' + } finally { + Pop-Location } exit 0 } + Install-Toolchain (Get-RootMsrv) 'stable' + # Provision the selected stable toolchain. Repository toolchain files are # processed natively by rustup. This is the shared prerequisite for setup paths # that execute stable Cargo/Rust or install Cargo tools; Just deduplicates it @@ -537,7 +534,8 @@ _check-toolchain toolchain: # the literal "default" (Anvil's selected stable toolchain) or a specific pinned # toolchain string (which must already be installed -- the per-component setup # recipes ensure this via a dependency on anvil--install). Explicit -# stable selections target `rustup component add --toolchain`. +# MSRV fallback selections target `rustup component add --toolchain`; native +# rustup selections use `rustup component add` without restating the selector. # # Probe-first: if the component is already available, skip the # `rustup component add`. This keeps the recipe idempotent and robust @@ -609,9 +607,12 @@ _install-component toolchain component: exit 0 } - if ($toolchain -eq 'default') { + if ($toolchain -eq 'default' -and $null -ne $selectedStableToolchain) { Write-Host "rustup component add --toolchain $selectedStableToolchain $component" rustup component add --toolchain $selectedStableToolchain $component + } elseif ($toolchain -eq 'default') { + Write-Host "rustup component add $component" + rustup component add $component } else { Write-Host "rustup component add --toolchain $toolchain $component" rustup component add --toolchain $toolchain $component diff --git a/justfiles/anvil/versions.just b/justfiles/anvil/versions.just index f984db8c7..91b42db6e 100644 --- a/justfiles/anvil/versions.just +++ b/justfiles/anvil/versions.just @@ -42,29 +42,11 @@ $ErrorActionPreference = 'Stop' Set-StrictMode -Version 3 if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { - Write-Output ('+' + $env:RUSTUP_TOOLCHAIN) return } if ((Test-Path -LiteralPath (Join-Path $RepoRoot 'rust-toolchain') -PathType Leaf) -or (Test-Path -LiteralPath (Join-Path $RepoRoot 'rust-toolchain.toml') -PathType Leaf)) { - Push-Location -LiteralPath $RepoRoot - try { - $rustupOutput = @(& rustup show active-toolchain --verbose 2>&1) - if ($LASTEXITCODE -ne 0) { - throw "anvil: rustup could not resolve the repository toolchain file:`n$($rustupOutput -join "`n")" - } - # Verbose output puts the exact toolchain name on its own first line, - # avoiding ambiguity between spaces/parentheses in path toolchains and - # rustup's human-readable reason annotation. - $active = $rustupOutput | Select-Object -First 1 - if ([string]::IsNullOrWhiteSpace([string] $active)) { - throw 'anvil: rustup returned no active repository toolchain' - } - } finally { - Pop-Location - } - Write-Output ('+' + [string] $active) return } From 0f9ffee126fed3e70982a11e413a1a39568b0957 Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Tue, 1 Sep 2026 12:01:03 +0200 Subject: [PATCH 25/32] Simplify cloud tool cache setup Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: f7996e6f-5c20-4d7a-b2f7-690af95e557e --- .anvil.lock | 6 +- .github/actions/anvil-setup/action.yml | 80 +++++---------- crates/cargo-anvil/docs/design/README.md | 10 +- crates/cargo-anvil/docs/design/ado.md | 68 ++++++------- crates/cargo-anvil/docs/design/checks.md | 17 ++-- crates/cargo-anvil/docs/design/github.md | 39 ++++---- crates/cargo-anvil/docs/design/local.md | 38 ++++--- crates/cargo-anvil/src/anvil/artifacts/ado.rs | 20 ++-- .../cargo-anvil/src/anvil/artifacts/github.rs | 17 +++- .../src/anvil/artifacts/justfile.rs | 5 - .../cargo-anvil/templates/ado/steps/setup.yml | 34 ++++--- .../templates/github/setup-action.yml | 80 +++++---------- .../templates/justfiles/anvil/tools.just | 18 ---- .../snapshots/snapshots__ado_backend.snap | 52 ++++------ .../snapshots/snapshots__github_backend.snap | 98 +++++-------------- .../snapshots/snapshots__local_only.snap | 18 ---- justfiles/anvil/tools.just | 18 ---- 17 files changed, 232 insertions(+), 386 deletions(-) diff --git a/.anvil.lock b/.anvil.lock index 1f9bd1d43..707ab2790 100644 --- a/.anvil.lock +++ b/.anvil.lock @@ -1,7 +1,7 @@ version = 1 tool = "anvil" tool_version = "0.5.0" -catalog_checksum = "sha256:b2f2262361f694c99ca7f41133e9b4f0e990cec7fc7924027b56f24280d29ab7" +catalog_checksum = "sha256:e03fe02f75e271234fd24bb3a2d20536d1bb077845661e3547bee1270a8143d0" [[file]] path = ".anvil/container/Containerfile" @@ -49,7 +49,7 @@ checksum = "sha256:ff8def6c0786b6e146c4b633dfe38cb9b8ede398345516cca32bbcd558608 [[file]] path = ".github/actions/anvil-setup/action.yml" -checksum = "sha256:259d59fb9381b5e29fe2f0a123e717ba62aaecd25955906fd943f385eca22067" +checksum = "sha256:898f28eec0c0e0006dd5b433a7cfedd31188e41a06d27be1b64d51f5c28605c2" [[file]] path = ".github/actions/anvil-setup/just-problem-matcher.json" @@ -261,7 +261,7 @@ checksum = "sha256:5b2d91569f3fe87cd5f516623a01b5c7ae855ad587870ef9a2ae6d5619dd9 [[file]] path = "justfiles/anvil/tools.just" -checksum = "sha256:efa6a28240fcc28a05b28acbca1191a38aed701a5f037dc81b91cc1c9b36ce4a" +checksum = "sha256:91f23677f01ef611cda076d8ceb58ee320d9a8827821b880c94a66456427a914" [[file]] path = "justfiles/anvil/versions.just" diff --git a/.github/actions/anvil-setup/action.yml b/.github/actions/anvil-setup/action.yml index 2b4c47a5c..4ad8d0b91 100644 --- a/.github/actions/anvil-setup/action.yml +++ b/.github/actions/anvil-setup/action.yml @@ -13,8 +13,8 @@ inputs: - "" (default): install the full catalog via `just anvil-setup` -- use for local "give me everything" flows. - - "none": skip tool installation entirely; just bootstrap the - rust toolchain + just + binstall + cache. Used by + - "none": skip tool installation entirely; just restore the + Cargo cache and bootstrap just + binstall. Used by anvil-impact, which installs only cargo-delta afterwards. - a name containing only lowercase letters, digits, and hyphens: install only that group's prerequisites via @@ -77,68 +77,29 @@ runs: } "Free after cleanup (GiB): $(Get-FreeDiskGiB)" - # Stable provisioning is part of the generated Just setup graph, so - # bootstrap Just before asking it for the selected compiler version. - # cargo-binstall keeps this cold path fast. - - name: Install cargo-binstall - uses: cargo-bins/cargo-binstall@v1.21.0 # immutable release, the tag cannot be moved - - - name: Install just - shell: bash - run: | - if ! command -v just >/dev/null 2>&1 ; then - cargo binstall --no-confirm --locked just || cargo install --locked just - fi - - - name: Provision stable toolchain and capture version - id: rustc-version - shell: pwsh -NoProfile -Command ". '{0}'" - run: | - $version = (& just _anvil-stable-rustc-version | Select-Object -Last 1) - if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($version)) { - throw 'anvil: failed to provision the selected stable toolchain' - } - $version = $version.Trim() - "version=$version" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append + # Restore before bootstrapping Just so a warm job can reuse the cached + # executable. Repository toolchain files intentionally define a fresh + # cache generation, bounding registry growth without invalidating on + # routine Cargo.toml or Cargo.lock edits. - name: Restore cargo cache id: cargo-cache uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: - # Key on OS + arch + rustc version + lockfile hashes + catalog - # hash so a Rust toolchain bump, a cross-arch matrix leg, or a - # tool-versions update all invalidate the cache cleanly. - # runner.arch resolves to X64 / ARM64 / X86, which keeps the - # x86_64 and aarch64 legs of the same OS from colliding on - # arch-incompatible cached binaries in ~/.cargo/bin. - # - # ${{ github.job }} discriminates by workflow-job-id (`pr-fast`, - # `pr-test`, `pr-slow`, etc.) so concurrent matrix legs that - # share OS+arch (e.g. pr-fast linux + pr-test linux) don't race - # on the same cache key. Without this, the first-to-finish job - # reserves the key, the others get "Unable to reserve cache: - # another job may be creating this cache" and silently skip - # the save -- leaving the cache empty forever. The restore-keys - # fall back across jobs so the install work is still shared - # across sibling legs on subsequent runs. - key: anvil-v1-${{ runner.os }}-${{ runner.arch }}-rust${{ steps.rustc-version.outputs.version }}-${{ hashFiles('Cargo.toml', 'Cargo.lock', '.cargo/config.toml', 'rust-toolchain', 'rust-toolchain.toml', 'justfiles/anvil/versions.just') }}-${{ github.job }} + # The job suffix prevents concurrent matrix jobs from racing to save + # one key. The prefix shares a completed cache across sibling jobs. + # There is deliberately no fallback across toolchain/catalog changes: + # those inputs are the cache-pruning boundary. + key: anvil-v2-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('.cargo/config.toml', 'rust-toolchain', 'rust-toolchain.toml', 'justfiles/anvil/versions.just') }}-${{ github.job }} restore-keys: | - anvil-v1-${{ runner.os }}-${{ runner.arch }}-rust${{ steps.rustc-version.outputs.version }}-${{ hashFiles('Cargo.toml', 'Cargo.lock', '.cargo/config.toml', 'rust-toolchain', 'rust-toolchain.toml', 'justfiles/anvil/versions.just') }}- - anvil-v1-${{ runner.os }}-${{ runner.arch }}-rust${{ steps.rustc-version.outputs.version }}- - anvil-v1-${{ runner.os }}-${{ runner.arch }}- + anvil-v2-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('.cargo/config.toml', 'rust-toolchain', 'rust-toolchain.toml', 'justfiles/anvil/versions.just') }}- # `.crates.toml` and `.crates2.json` track which cargo-installed # tools and versions live in ~/.cargo/bin/. Without them in the # cache, `cargo install --list` and (downstream) the # tool-install recipes' early-skip on "installed >= pin" don't - # see the cached binaries — every run then tries to reinstall - # on top of them and fails with "binary X already exists in - # destination". + # see the cached binaries. # - # target/ (the compilation output dir) is deliberately NOT cached here: - # per the GitHub backend design's caching policy, multi-GB per-job - # target/ trees dwarf the high-value tool cache under the repo cache - # quota, give only modest recompile savings once tools are cached, and - # could overwrite the downloaded target/anvil/impact/ cache. Only the - # ~/.cargo tool + registry state below is cached. + # target/ is deliberately excluded: per-job target trees dwarf the + # tool cache and could overwrite the downloaded impact cache. path: | ~/.cargo/registry/cache/ ~/.cargo/registry/index/ @@ -146,6 +107,17 @@ runs: ~/.cargo/.crates.toml ~/.cargo/.crates2.json + # cargo-binstall keeps the cold bootstrap path fast. + - name: Install cargo-binstall + uses: cargo-bins/cargo-binstall@v1.21.0 # immutable release, the tag cannot be moved + + - name: Install just + shell: bash + run: | + if ! command -v just >/dev/null 2>&1 ; then + cargo binstall --no-confirm --locked just || cargo install --locked just + fi + # The catalog recipe `anvil-setup` (or `anvil--setup` # when a group is specified via the `group` input) uses the Just # bootstrapped above, then handles everything else. The setup recipes are idempotent and diff --git a/crates/cargo-anvil/docs/design/README.md b/crates/cargo-anvil/docs/design/README.md index 7c56dbf5c..bec648e7c 100644 --- a/crates/cargo-anvil/docs/design/README.md +++ b/crates/cargo-anvil/docs/design/README.md @@ -12,7 +12,7 @@ user-visible shape of the tool. Detail lives in companion documents: - [updates.md](./updates.md) — the drift-detection and update algorithm; opt-out semantics. - [extensibility.md](./extensibility.md) — how downstream tools ship their own brand + catalog. - [github.md](./github.md) — GitHub Actions emission, example workflows, impact wiring. -- [ado.md](./ado.md) — Azure DevOps Pipelines emission, 1ESPT/msrustup composition. +- [ado.md](./ado.md) — Azure DevOps Pipelines emission and compliance-template composition. - [containers.md](./containers.md) — the opt-in, local-only container backend for running any `anvil-*` recipe in a pinned Linux image (Linux-on-Windows parity, distro pinning). - [../implementation.md](../implementation.md) — internal implementation guidance. @@ -27,12 +27,12 @@ implemented six different ways: | Repo | cloud workflows | Justfile shape | Toolchain | Notable specifics | |------|----|----------------|-----------|-------------------| -| `oxidizer` | ADO 1ESPT (`SubstratePT`) | 500-line monolith + `just_mutants.just` | `ms-prod-1.93` | Stage flags (`enableStages`), `cargo-aprz`, stable-API checks | +| `oxidizer` | ADO 1ESPT (`SubstratePT`) | 500-line monolith + `just_mutants.just` | caller-provisioned | Stage flags (`enableStages`), `cargo-aprz`, stable-API checks | | `oxidizer-github` | GitHub Actions | Modular `justfiles/{basic,coverage,format,setup,spelling}.just` + `constants.env` | `1.93` | `cargo-delta` impact-scoped builds, sticky semver comments, composite `setup` action | -| `ox-tools` | ADO (CloudBuild + classic) | none in worktree | `ms-prod-1.92` | NuGet/MSBuild scaffolding, internal templates | +| `ox-tools` | ADO (CloudBuild + classic) | none in worktree | caller-provisioned | NuGet/MSBuild scaffolding, internal templates | | `ox-tools-gh` | GitHub Actions | Same modular shape as `oxidizer-github` | `1.93` | Mirror surface to OSS oxidizer | -| `assistants-oxide` | ADO 1ESPT (custom `rust/`) | Monolith + `.just/tds.just` | `ms-prod-1.93` | Symcrypt setup steps, NuGet publish stage | -| `ox-docs` | ADO classic | Monolith | `ms-prod-1.88` | Mixed C#/.NET + Rust, mdbook/docfx | +| `assistants-oxide` | ADO 1ESPT (custom `rust/`) | Monolith + `.just/tds.just` | caller-provisioned | Symcrypt setup steps, NuGet publish stage | +| `ox-docs` | ADO classic | Monolith | caller-provisioned | Mixed C#/.NET + Rust, mdbook/docfx | The same logical checks (clippy, fmt, deny, miri, mutants, coverage, hack feature-powerset, udeps, semver, spellcheck, license headers, doc/doctest, careful, audit, ensure-no-cyclic-deps, diff --git a/crates/cargo-anvil/docs/design/ado.md b/crates/cargo-anvil/docs/design/ado.md index 97f36b20b..415dbdfaa 100644 --- a/crates/cargo-anvil/docs/design/ado.md +++ b/crates/cargo-anvil/docs/design/ado.md @@ -373,11 +373,11 @@ x86_64 jobs and per-OS affected-package impact sets as `pr_test`, and invokes `anvil-pr-msrv`. The stage consumes the per-OS impact artifact like the other PR groups; when the root manifest declares no MSRV, the recipe exits successfully without running tests. -Internal pipelines set `RUSTUP_TOOLCHAIN` to their provisioned stable compiler -and `ANVIL_MSRV_TOOLCHAIN` to the provisioned `ms-prod-*` equivalent of the -declared MSRV. Supplying only the MSRV mapping is rejected because stable checks -would otherwise fall through to a public channel. Public pipelines leave both -unset and let Anvil install the declared MSRV through rustup. +Callers may set `RUSTUP_TOOLCHAIN` to an already-provisioned stable compiler and +`ANVIL_MSRV_TOOLCHAIN` to an already-provisioned compiler for the dedicated +MSRV run. Supplying only the MSRV mapping is rejected because stable checks +would otherwise select an unrelated compiler. When the mapping is unset, +Anvil installs the declared MSRV through rustup. ### 4.1 Per-job wrapper (`steps/job.yml`) — the 1ESPT extensibility point @@ -710,19 +710,17 @@ takes a single `group` parameter that controls which recipes run: - empty (default): runs `just anvil-setup` -- the full catalog. Use for "give me everything" flows. -- `none`: skips the group/full tool fan-out after stable provisioning. Used by - `impact.yml`, which only needs `cargo-delta` and installs it itself - afterwards. +- `none`: skips the group/full tool fan-out. Used by `impact.yml`, which only + needs `cargo-delta` and installs it itself afterwards. - any other value (e.g. `pr-fast`, `scheduled-advisories`): runs `just anvil--setup` -- only the tools, components, and toolchains that group actually needs. Every per-group step template (`.pipelines/anvil/steps/.yml`) passes its own group name here, so a `pr-fast` matrix leg never installs cargo-mutants. -The template does not install Rust; it expects `cargo` on PATH -- provided by the -user's msrustup step in 1ESPT pipelines or by a previous step in OSS pipelines -(see §6). ADO uses the default `install` backend (source builds) because -`cargo-binstall` has unresolved compliance issues for internal ADO pipelines. +The template expects the caller to provide the Rust/rustup bootstrap and +`cargo` on PATH (see §6). ADO uses the default `install` backend (source +builds) so adopters do not need to approve a binary-installation service. `impact.yml` invokes `setup.yml` with `group: none`, then installs `cargo-delta` via `anvil-tool-cargo-delta-install` and runs the shared **`just anvil-impact`** @@ -748,9 +746,9 @@ mechanics are in [local.md §4](./local.md#4-impact-scoping-via-the-anvil-impact The user's root pipeline or compliance template installs the Rust/rustup bootstrap before the Anvil stages run. The generated setup step restores Cargo -home before bootstrapping Just, then calls `anvil-toolchain-stable-install` to -provision a public selection or prepare an externally provisioned internal -selection. The template never invokes `_anvil-resolve-stable` itself and does +home before bootstrapping Just. Its selected catalog setup recipe reaches +`anvil-toolchain-stable-install` before any stable Cargo or Rust use. The +template never invokes `_anvil-resolve-stable` itself and does not publish a resolved `RUSTUP_TOOLCHAIN` to subsequent steps. Each stable recipe evaluates the inline PowerShell array expression and invokes Cargo or Rust directly in its current PowerShell process. Selection has no expression-time @@ -758,38 +756,34 @@ subprocess, host-OS branch, or change to the adopter's configured shell. A selecting toolchain file remains unoverridden so rustup processes the complete file. -Selection follows the shared local contract: an existing `RUSTUP_TOOLCHAIN`, then a -root toolchain file with `channel` or `path`, then the root MSRV. Internal pipelines -set `RUSTUP_TOOLCHAIN=ms-prod-*` so it overrides a public toolchain file or MSRV, -while their existing installer-specific setting continues to tell msrustup which -toolchain to provision. Current msrustup honors the standard override precedence. -Setup reapplies explicit components and targets from a suppressed root toolchain -file to the provisioned compiler. Unavailable options warn and are skipped, matching -rustup's native toolchain-file behavior. - -In 1ESPT compliance pipelines, msrustup still provisions the selected -`RUSTUP_TOOLCHAIN`; the Anvil setup primitive treats that caller override as -external and only reapplies applicable repository file options. Mapping a -public MSRV to an internal `ms-prod-*` channel remains a root-pipeline policy. -In OSS/non-1ESPT pipelines, the user provides rustup (for example with -`RustInstaller@1`) and Anvil installs a missing public MSRV selection through -the same setup primitive. +Selection follows the shared local contract: an existing `RUSTUP_TOOLCHAIN`, +then a root toolchain file with `channel` or `path`, then the root MSRV. A +caller-provided override takes precedence over repository files and is treated +as an externally provisioned selection. Anvil does not parse repository +toolchain files or reapply options from a file suppressed by that override. +Without an override, rustup processes the complete repository toolchain file +natively. Without either native selector, Anvil installs the root MSRV. + +`ANVIL_MSRV_TOOLCHAIN` may map the dedicated MSRV run to a separately +provisioned toolchain. It does not select the compiler used by ordinary stable +checks and therefore must be paired with a stable selector. Shared Cargo-tool/default-component setup and stable-only leaf setup all depend on `anvil-toolchain-stable-install`; Just deduplicates it for group/full fan-out. `anvil-tool-rustc-validate-prereqs` remains read-only and validates that `rustc` is available plus the workspace MSRV compatibility rule. For nightly-requiring checks, the matching toolchain-validate-prereqs recipe fails -with a suggestion to ask the team's pipeline owner to add that dated nightly -to msrustup. +with a suggestion to ask the pipeline owner to provision that dated nightly. ## 7. Caching `setup.yml` restores Cargo home before bootstrapping Just so a warm job does not compile Just before reaching the cache. The ADO `Cache@2` key uses agent OS and -architecture plus hashes of the root Cargo manifests, Cargo configuration, -toolchain files, and `versions.just`. The selected compiler version is omitted -because rustup toolchains are outside Cargo home. +architecture plus Cargo configuration, the repository toolchain-file +fingerprint, and `versions.just`. Toolchain-file changes deliberately start a +fresh cache generation to bound registry growth. Routine `Cargo.toml`, +`Cargo.lock`, and compiler-version changes do not invalidate standalone cached +tools. Rustup toolchains themselves are outside Cargo home. The cache covers: @@ -847,7 +841,7 @@ For repos with an existing 1ESPT-extending pipeline, adopting anvil is increment anvil's owned templates compose cleanly with the 1ESPT `enableStages` flag system: each group is its own job inside the `ANVIL_pr` stage, so 1ESPT can gate or split them as -needed. The pre-existing repo-specific compliance steps (msrustup, NuGet pushes, signing, +needed. The pre-existing repo-specific compliance steps (provisioning, signing, …) keep running alongside the anvil stage. anvil does not own the pipeline's shape — it just contributes a stage. diff --git a/crates/cargo-anvil/docs/design/checks.md b/crates/cargo-anvil/docs/design/checks.md index 06f9074f0..43f40bca7 100644 --- a/crates/cargo-anvil/docs/design/checks.md +++ b/crates/cargo-anvil/docs/design/checks.md @@ -233,12 +233,11 @@ The group uses the same OS/architecture matrix and per-OS impact sets as `pr-tes so cfg-gated targets and dependencies are exercised under the MSRV. It runs in parallel with the other PR groups. When no root MSRV exists, the recipe exits successfully without running tests. -`ANVIL_MSRV_TOOLCHAIN` may map the public MSRV to an already-provisioned -internal toolchain. Internal environments pair it with `RUSTUP_TOOLCHAIN` (or a -selecting repository toolchain file) for stable checks; a mapping by itself is -rejected rather than allowing stable checks to fall through to a public -channel. When the mapping is unset, setup installs the declared MSRV through -rustup. +`ANVIL_MSRV_TOOLCHAIN` may map the declared MSRV to an already-provisioned +toolchain. Callers pair it with `RUSTUP_TOOLCHAIN` (or a selecting repository +toolchain file) for stable checks; a mapping by itself is rejected rather than +allowing stable checks to select an unrelated compiler. When the mapping is +unset, setup installs the declared MSRV through rustup. #### `pr-runtime-analysis` (stricter-runtime correctness) @@ -281,9 +280,9 @@ publish a full-coverage snapshot for the current state of `main`. These checks share a property: their outcome can change without a commit to this repo. `deny`/`audit`/`aprz` consult external databases (RustSec advisory DB, license registries, Azure risk indices). `clippy` reflects whatever lint set ships with the currently-installed -toolchain -- even when `rust-toolchain.toml` is pinned, repos using floating channels -(`stable`, or msrustup channel pointers like `ms-prod-1.93`) can pick up new lints when the -pointer is bumped upstream. Re-running these on the scheduled tier turns "something landed +toolchain -- even when `rust-toolchain.toml` is pinned, repositories using a +floating channel such as `stable` can pick up new lints when the channel moves. +Re-running these on the scheduled tier turns "something landed upstream yesterday" into a tracked failure rather than an invisible regression discovered next time someone opens an unrelated PR. diff --git a/crates/cargo-anvil/docs/design/github.md b/crates/cargo-anvil/docs/design/github.md index 8d3b06963..36a354330 100644 --- a/crates/cargo-anvil/docs/design/github.md +++ b/crates/cargo-anvil/docs/design/github.md @@ -742,9 +742,8 @@ then invokes the requested catalog setup recipe. Its - empty (default): runs `just anvil-setup binstall` -- the full catalog. Use for local "give me everything" flows. -- `none`: skips the group/full tool fan-out after stable provisioning. Used by - `anvil-impact`, which only needs `cargo-delta` and installs it itself - afterwards. +- `none`: skips the group/full tool fan-out. Used by `anvil-impact`, which only + needs `cargo-delta` and installs it itself afterwards. - any other value (e.g. `pr-fast`, `scheduled-advisories`): runs `just anvil--setup binstall` -- only the tools, components, and toolchains that group actually needs. Ordinary group names contain only @@ -843,15 +842,12 @@ The check → bucket mapping is in ## 7. Rust toolchain -The setup action bootstraps cargo-binstall and Just using the runner -environment, then calls `_anvil-stable-rustc-version`. That helper depends on -`anvil-toolchain-stable-install`, so the setup graph provisions a missing -public MSRV or lets rustup process a repository toolchain file before invoking -`rustc` directly with the lazy optional `+toolchain` argument. Native rustup -selectors produce no argument; only the root MSRV fallback is explicit. The -resulting version keys the cache; the workflow never invokes -`_anvil-resolve-stable` itself and does not publish a resolved -`RUSTUP_TOOLCHAIN` through `GITHUB_ENV`. +The setup action restores Cargo home, bootstraps cargo-binstall and Just, and +then invokes the selected catalog setup recipe. That setup graph reaches +`anvil-toolchain-stable-install` before stable Cargo or Rust is used, so it +provisions a missing root MSRV or lets rustup process a repository toolchain +file. The workflow never invokes `_anvil-resolve-stable` itself and does not +publish a resolved `RUSTUP_TOOLCHAIN` through `GITHUB_ENV`. Each stable recipe lazily selects the same optional argument and invokes Cargo or Rust directly in its current PowerShell process. The selector is an inline PowerShell array expression rather than an expression-time shell subprocess, so @@ -865,10 +861,9 @@ Selection follows the shared local contract: an existing `RUSTUP_TOOLCHAIN`, the root toolchain file with `channel` or `path`, then the root MSRV. There is no runner-default fallback. GH-hosted runners provide the rustup proxy used to install a selected public channel. A repository that supplies a path toolchain must provision -that path before the Anvil action runs. When an environment override or MSRV fallback -suppresses a root toolchain file, setup reapplies its explicit components and targets -to the selected compiler; unavailable options produce warnings as they do when rustup -processes the file natively. +that path before the Anvil action runs. A caller-provided environment override +takes precedence over repository files. Anvil does not parse a repository +toolchain file or reapply options from a file suppressed by that override. On self-hosted runners or pre-baked images without rustup, the user provisions the selected toolchain before the `uses:` of the reusable workflow: @@ -896,10 +891,14 @@ installed `rustc` and workspace MSRV contract at check time. ## 8. Caching The `anvil-setup` composite action computes a cache key from runner OS and -architecture, the actual `rustc --version`, hashes of `Cargo.lock`, -`.cargo/config.toml`, `rust-toolchain.toml`, and `versions.just`, plus the workflow -job ID. Job discrimination prevents concurrent jobs from racing to save one key; -prefix restore keys still share prior installs across jobs. +architecture plus hashes of `.cargo/config.toml`, either supported repository +toolchain file, and `versions.just`, followed by the workflow job ID. +Toolchain-file or catalog changes deliberately start a fresh cache generation +to bound registry growth; there is no restore fallback across those boundaries. +Routine `Cargo.toml`, `Cargo.lock`, and compiler-version changes do not +invalidate standalone cached tools. Job discrimination prevents concurrent +jobs from racing to save one key, while the fingerprint prefix shares prior +installs across jobs within the same cache generation. The cache covers: diff --git a/crates/cargo-anvil/docs/design/local.md b/crates/cargo-anvil/docs/design/local.md index fe2e3cd25..a44b26bc8 100644 --- a/crates/cargo-anvil/docs/design/local.md +++ b/crates/cargo-anvil/docs/design/local.md @@ -342,8 +342,8 @@ install recipe can run: Trade-off acknowledged: `cargo install --locked` is slow on a cold cache (several minutes for the full catalog). It is also the most reliable mechanism in restricted networks. Caching (via the GH cache action and the ADO pipeline workspace cache) is -configured by the setup action/template to key on repository manifests, -toolchain configuration, and `versions.just`. See +configured by the setup action/template to key on platform, toolchain +configuration, Cargo configuration, and `versions.just`. See [github.md](./github.md#caching) and [ado.md](./ado.md#caching). #### 3.3.1 System-level prerequisites @@ -389,8 +389,8 @@ predates the catalog minimum, with a one-line hint pointing at the matching Anvil selects one deterministic toolchain for every check that otherwise uses the ambient stable toolchain. The selection order is: -1. `RUSTUP_TOOLCHAIN`, when the caller already set it. This is the standard rustup - override and lets internal pipelines select an installed `ms-prod-*` toolchain. +1. `RUSTUP_TOOLCHAIN`, when the caller already set it. This is rustup's standard + override for selecting an already available toolchain. 2. A root `rust-toolchain` or `rust-toolchain.toml`, delegated to rustup without Anvil parsing or modifying the file. 3. The root package or `[workspace.package]` `rust-version`, treated as the @@ -406,12 +406,12 @@ floor should declare root `[workspace.package].rust-version` (or package `rust-version`) and have every workspace member inherit it or declare an equal or lower minimum. A repository with missing package MSRVs, or a member minimum newer than the root, must instead correct the root floor or add a root toolchain -file to select the single compiler used by catalog checks. Internal builds may -set `RUSTUP_TOOLCHAIN` to a provisioned toolchain -rather than adding a public compiler pin. Setting `ANVIL_MSRV_TOOLCHAIN` -without that stable override (and without a selecting repository toolchain -file) is rejected as incomplete internal configuration. An empty root -toolchain file is invalid and produces an actionable error. +file to select the single compiler used by catalog checks. Callers may set +`RUSTUP_TOOLCHAIN` to an already provisioned toolchain rather than adding a +repository compiler pin. Setting `ANVIL_MSRV_TOOLCHAIN` without that stable +override (and without a selecting repository toolchain file) is rejected as +incomplete configuration. An empty root toolchain file is invalid and produces +an actionable error. `versions.just` holds the selector as the lazy, non-exported `_anvil_stable_toolchain_args` value. Its value is a multiline PowerShell array @@ -441,15 +441,13 @@ environment. The public setup primitive `anvil-toolchain-stable-install` owns stable provisioning. Rustup processes repository toolchain files directly; the private -`_anvil-resolve-stable` implementation handles public MSRV installation and -internal mapping. Leaf setup recipes reach the primitive directly or through +`_anvil-resolve-stable` implementation handles MSRV installation and an +optional separately provisioned MSRV selector. Leaf setup recipes reach the primitive directly or through the shared Cargo-tool/default-component installers; group, tier, and -`anvil-setup` recipes inherit and deduplicate that dependency. GitHub version -capture uses the private `_anvil-stable-rustc-version` recipe, whose dependency -provisions stable before it invokes `rustc` directly with the lazy argument. -Workflows and container construction therefore invoke setup operations rather -than orchestrating `_anvil-resolve-stable` actions. Neither helper wraps a -Cargo/Rust command that performs a check, and no standalone script is involved. +`anvil-setup` recipes inherit and deduplicate that dependency. Workflows and +container construction invoke setup operations rather than orchestrating +`_anvil-resolve-stable` actions. The helper does not wrap a Cargo/Rust command +that performs a check, and no standalone script is involved. When selection falls back to the root MSRV, Anvil reads the root manifest just far enough to bootstrap that compiler, then prerequisite validation reads @@ -466,8 +464,8 @@ for this uncommon case. When the root manifest declares an MSRV, `anvil-msrv-test` runs affected-package unit and integration tests under that compiler with all features and with default features. A selecting toolchain file does not suppress this minimum-version run. -`ANVIL_MSRV_TOOLCHAIN` optionally maps the declared MSRV to a provisioned internal -toolchain. Without a root MSRV the recipe is a no-op. +`ANVIL_MSRV_TOOLCHAIN` optionally maps the declared MSRV to a separately +provisioned toolchain. Without a root MSRV the recipe is a no-op. `anvil-tool-rustc-validate-prereqs` verifies that `rustc` is available and enforces the workspace MSRV compatibility rule. Stable setup separately diff --git a/crates/cargo-anvil/src/anvil/artifacts/ado.rs b/crates/cargo-anvil/src/anvil/artifacts/ado.rs index 3db58af09..8a29610ee 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/ado.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/ado.rs @@ -214,24 +214,28 @@ mod tests { fn setup_step_takes_group_parameter_and_dispatches() { assert!(SETUP_STEP.contains("name: group")); assert!(SETUP_STEP.contains("just anvil-setup")); - assert!(SETUP_STEP.contains("just anvil-toolchain-stable-install")); + assert!(!SETUP_STEP.contains("just anvil-toolchain-stable-install")); assert!(!SETUP_STEP.contains("_anvil-resolve-stable")); assert!(SETUP_STEP.contains("just anvil-${{ parameters.group }}-setup")); assert!(SETUP_STEP.contains("eq(parameters.group, 'none')")); + assert!(SETUP_STEP.contains("anvil_toolchain_fingerprint")); + assert!(!SETUP_STEP.contains("Cargo.toml | Cargo.lock")); + assert!(SETUP_STEP.contains("'rust-toolchain.toml'")); + assert!(!SETUP_STEP.contains("restoreKeys:")); + let fingerprint = SETUP_STEP + .find("anvil setup (fingerprint repository toolchain)") + .expect("setup must fingerprint optional repository toolchain files"); let cache_restore = SETUP_STEP .find("anvil setup (cache cargo home)") .expect("setup must restore Cargo home"); let just_bootstrap = SETUP_STEP.find("anvil setup (install just)").expect("setup must bootstrap Just"); - let stable_provisioning = SETUP_STEP - .find("just anvil-toolchain-stable-install") - .expect("setup must provision the selected stable compiler"); assert!( - cache_restore < just_bootstrap, - "Cargo home must be restored before Just is bootstrapped" + fingerprint < cache_restore, + "optional toolchain files must be fingerprinted before cache restore" ); assert!( - just_bootstrap < stable_provisioning, - "Just must be bootstrapped before setup-driven stable provisioning" + cache_restore < just_bootstrap, + "Cargo home must be restored before Just is bootstrapped" ); } diff --git a/crates/cargo-anvil/src/anvil/artifacts/github.rs b/crates/cargo-anvil/src/anvil/artifacts/github.rs index 0975a0b74..04c855840 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/github.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/github.rs @@ -165,9 +165,24 @@ mod tests { fn setup_action_takes_group_input_and_dispatches() { assert!(SETUP_ACTION.contains("group:")); assert!(SETUP_ACTION.contains("just anvil-setup binstall")); - assert!(SETUP_ACTION.contains("just _anvil-stable-rustc-version")); assert!(!SETUP_ACTION.contains("_anvil-resolve-stable")); assert!(!SETUP_ACTION.contains("just anvil-toolchain-stable-install")); + assert!(!SETUP_ACTION.contains("rustc-version")); + assert!(!SETUP_ACTION.contains("hashFiles('Cargo.toml'")); + assert!(!SETUP_ACTION.contains("'Cargo.lock'")); + assert!(SETUP_ACTION.contains("'rust-toolchain.toml'")); + let cache_restore = SETUP_ACTION + .find("name: Restore cargo cache") + .expect("setup must restore Cargo home"); + let just_bootstrap = SETUP_ACTION.find("name: Install just").expect("setup must bootstrap Just"); + let catalog_setup = SETUP_ACTION + .find("name: Install anvil toolchains + tools") + .expect("setup must dispatch catalog setup"); + assert!( + cache_restore < just_bootstrap, + "Cargo home must be restored before Just is bootstrapped" + ); + assert!(just_bootstrap < catalog_setup, "Just must be bootstrapped before catalog setup"); assert!(SETUP_ACTION.contains("ANVIL_GROUP: ${{ inputs.group }}")); assert!(SETUP_ACTION.contains("just \"anvil-$ANVIL_GROUP-setup\" binstall")); assert!(SETUP_ACTION.contains(r"^[a-z0-9-]+$")); diff --git a/crates/cargo-anvil/src/anvil/artifacts/justfile.rs b/crates/cargo-anvil/src/anvil/artifacts/justfile.rs index 15d7b3b4a..4554d60cf 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/justfile.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/justfile.rs @@ -787,11 +787,6 @@ mod tests { "default component '{component}' must provision stable first" ); } - assert!( - TOOLS_JUST.contains("_anvil-stable-rustc-version: anvil-toolchain-stable-install"), - "cloud version capture must use the setup dependency graph" - ); - let checks = all_check_bodies(); for check in ["bench", "doc-build", "doc-test", "examples", "loom"] { let setup = format!("anvil-{check}-setup installer=\"install\": anvil-toolchain-stable-install"); diff --git a/crates/cargo-anvil/templates/ado/steps/setup.yml b/crates/cargo-anvil/templates/ado/steps/setup.yml index b949ee078..89d2aa908 100644 --- a/crates/cargo-anvil/templates/ado/steps/setup.yml +++ b/crates/cargo-anvil/templates/ado/steps/setup.yml @@ -8,9 +8,9 @@ # group: which anvil group to install setup for. Special values: # - "" (default): install the full catalog via # `just anvil-setup` -- use for "give me everything" flows. -# - "none": skip tool installation entirely; just bootstrap the -# rust cache + libclang + just. Used by impact.yml, which installs -# only cargo-delta afterwards. +# - "none": skip tool installation entirely; just restore the +# Cargo cache and bootstrap libclang + just. Used by impact.yml, +# which installs only cargo-delta afterwards. # - anything else (e.g. "pr-fast"): install only that group's # prerequisites via `just anvil--setup`. parameters: @@ -18,15 +18,30 @@ parameters: type: string default: '' steps: + # Toolchain changes deliberately start a fresh Cargo-home cache. Besides + # bounding registry growth, this gives repositories a predictable cache + # reset point without coupling cache identity to routine manifest edits. + # Compute the digest explicitly because either supported toolchain file is + # optional and Cache@2 rejects missing file-path key segments. + - pwsh: | + $parts = @( + foreach ($name in @('rust-toolchain', 'rust-toolchain.toml')) { + if (Test-Path -LiteralPath $name -PathType Leaf) { + "$name=$((Get-FileHash -LiteralPath $name -Algorithm SHA256).Hash.ToLowerInvariant())" + } + } + ) + $fingerprint = if ($parts.Count -eq 0) { 'none' } else { $parts -join '-' } + Write-Host "##vso[task.setvariable variable=anvil_toolchain_fingerprint]$fingerprint" + displayName: anvil setup (fingerprint repository toolchain) + # Restore Cargo's registry and installed binaries before bootstrapping Just, # so warm ADO jobs do not compile Just again before reaching the cache. # Rustup toolchains are not stored under Cargo home, so the selected compiler # version does not affect this cache's contents. - task: Cache@2 inputs: - key: 'anvil-v2 | "$(Agent.OS)" | "$(Agent.OSArchitecture)" | Cargo.toml | Cargo.lock | .cargo/config.toml | rust-toolchain | rust-toolchain.toml | justfiles/anvil/versions.just' - restoreKeys: | - anvil-v2 | "$(Agent.OS)" | "$(Agent.OSArchitecture)" + key: 'anvil-v3 | "$(Agent.OS)" | "$(Agent.OSArchitecture)" | "$(anvil_toolchain_fingerprint)" | .cargo/config.toml | justfiles/anvil/versions.just' path: | $(HOME)/.cargo/registry/cache/ $(HOME)/.cargo/registry/index/ @@ -41,13 +56,6 @@ steps: fi displayName: anvil setup (install just) - - pwsh: | - & just anvil-toolchain-stable-install - if ($LASTEXITCODE -ne 0) { - throw 'anvil: failed to provision the selected stable toolchain' - } - displayName: anvil setup (provision stable toolchain) - # Hand everything else off to the catalog recipe. Idempotent. ADO # uses the default `install` backend (source builds) because # cargo-binstall has unresolved compliance issues for internal ADO diff --git a/crates/cargo-anvil/templates/github/setup-action.yml b/crates/cargo-anvil/templates/github/setup-action.yml index 2b4c47a5c..4ad8d0b91 100644 --- a/crates/cargo-anvil/templates/github/setup-action.yml +++ b/crates/cargo-anvil/templates/github/setup-action.yml @@ -13,8 +13,8 @@ inputs: - "" (default): install the full catalog via `just anvil-setup` -- use for local "give me everything" flows. - - "none": skip tool installation entirely; just bootstrap the - rust toolchain + just + binstall + cache. Used by + - "none": skip tool installation entirely; just restore the + Cargo cache and bootstrap just + binstall. Used by anvil-impact, which installs only cargo-delta afterwards. - a name containing only lowercase letters, digits, and hyphens: install only that group's prerequisites via @@ -77,68 +77,29 @@ runs: } "Free after cleanup (GiB): $(Get-FreeDiskGiB)" - # Stable provisioning is part of the generated Just setup graph, so - # bootstrap Just before asking it for the selected compiler version. - # cargo-binstall keeps this cold path fast. - - name: Install cargo-binstall - uses: cargo-bins/cargo-binstall@v1.21.0 # immutable release, the tag cannot be moved - - - name: Install just - shell: bash - run: | - if ! command -v just >/dev/null 2>&1 ; then - cargo binstall --no-confirm --locked just || cargo install --locked just - fi - - - name: Provision stable toolchain and capture version - id: rustc-version - shell: pwsh -NoProfile -Command ". '{0}'" - run: | - $version = (& just _anvil-stable-rustc-version | Select-Object -Last 1) - if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($version)) { - throw 'anvil: failed to provision the selected stable toolchain' - } - $version = $version.Trim() - "version=$version" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append + # Restore before bootstrapping Just so a warm job can reuse the cached + # executable. Repository toolchain files intentionally define a fresh + # cache generation, bounding registry growth without invalidating on + # routine Cargo.toml or Cargo.lock edits. - name: Restore cargo cache id: cargo-cache uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: - # Key on OS + arch + rustc version + lockfile hashes + catalog - # hash so a Rust toolchain bump, a cross-arch matrix leg, or a - # tool-versions update all invalidate the cache cleanly. - # runner.arch resolves to X64 / ARM64 / X86, which keeps the - # x86_64 and aarch64 legs of the same OS from colliding on - # arch-incompatible cached binaries in ~/.cargo/bin. - # - # ${{ github.job }} discriminates by workflow-job-id (`pr-fast`, - # `pr-test`, `pr-slow`, etc.) so concurrent matrix legs that - # share OS+arch (e.g. pr-fast linux + pr-test linux) don't race - # on the same cache key. Without this, the first-to-finish job - # reserves the key, the others get "Unable to reserve cache: - # another job may be creating this cache" and silently skip - # the save -- leaving the cache empty forever. The restore-keys - # fall back across jobs so the install work is still shared - # across sibling legs on subsequent runs. - key: anvil-v1-${{ runner.os }}-${{ runner.arch }}-rust${{ steps.rustc-version.outputs.version }}-${{ hashFiles('Cargo.toml', 'Cargo.lock', '.cargo/config.toml', 'rust-toolchain', 'rust-toolchain.toml', 'justfiles/anvil/versions.just') }}-${{ github.job }} + # The job suffix prevents concurrent matrix jobs from racing to save + # one key. The prefix shares a completed cache across sibling jobs. + # There is deliberately no fallback across toolchain/catalog changes: + # those inputs are the cache-pruning boundary. + key: anvil-v2-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('.cargo/config.toml', 'rust-toolchain', 'rust-toolchain.toml', 'justfiles/anvil/versions.just') }}-${{ github.job }} restore-keys: | - anvil-v1-${{ runner.os }}-${{ runner.arch }}-rust${{ steps.rustc-version.outputs.version }}-${{ hashFiles('Cargo.toml', 'Cargo.lock', '.cargo/config.toml', 'rust-toolchain', 'rust-toolchain.toml', 'justfiles/anvil/versions.just') }}- - anvil-v1-${{ runner.os }}-${{ runner.arch }}-rust${{ steps.rustc-version.outputs.version }}- - anvil-v1-${{ runner.os }}-${{ runner.arch }}- + anvil-v2-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('.cargo/config.toml', 'rust-toolchain', 'rust-toolchain.toml', 'justfiles/anvil/versions.just') }}- # `.crates.toml` and `.crates2.json` track which cargo-installed # tools and versions live in ~/.cargo/bin/. Without them in the # cache, `cargo install --list` and (downstream) the # tool-install recipes' early-skip on "installed >= pin" don't - # see the cached binaries — every run then tries to reinstall - # on top of them and fails with "binary X already exists in - # destination". + # see the cached binaries. # - # target/ (the compilation output dir) is deliberately NOT cached here: - # per the GitHub backend design's caching policy, multi-GB per-job - # target/ trees dwarf the high-value tool cache under the repo cache - # quota, give only modest recompile savings once tools are cached, and - # could overwrite the downloaded target/anvil/impact/ cache. Only the - # ~/.cargo tool + registry state below is cached. + # target/ is deliberately excluded: per-job target trees dwarf the + # tool cache and could overwrite the downloaded impact cache. path: | ~/.cargo/registry/cache/ ~/.cargo/registry/index/ @@ -146,6 +107,17 @@ runs: ~/.cargo/.crates.toml ~/.cargo/.crates2.json + # cargo-binstall keeps the cold bootstrap path fast. + - name: Install cargo-binstall + uses: cargo-bins/cargo-binstall@v1.21.0 # immutable release, the tag cannot be moved + + - name: Install just + shell: bash + run: | + if ! command -v just >/dev/null 2>&1 ; then + cargo binstall --no-confirm --locked just || cargo install --locked just + fi + # The catalog recipe `anvil-setup` (or `anvil--setup` # when a group is specified via the `group` input) uses the Just # bootstrapped above, then handles everything else. The setup recipes are idempotent and diff --git a/crates/cargo-anvil/templates/justfiles/anvil/tools.just b/crates/cargo-anvil/templates/justfiles/anvil/tools.just index 194a7172c..85b54c06b 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/tools.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/tools.just @@ -372,24 +372,6 @@ _anvil-resolve-stable action="install": [group("anvil-setup")] anvil-toolchain-stable-install: (_anvil-resolve-stable "install") -# Report the selected stable rustc version for GitHub cache keys. Provisioning -# is a dependency so workflow surfaces invoke one setup-aware operation rather -# than orchestrating the resolver directly. -[private] -[script("pwsh", "-NoProfile")] -_anvil-stable-rustc-version: anvil-toolchain-stable-install - $versionLine = (& rustc {{_anvil_stable_toolchain_args}} --version | Select-Object -Last 1) - if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($versionLine)) { - Write-Error 'anvil: failed to read the selected stable rustc version' - exit 1 - } - $parts = ([string] $versionLine).Split() - if ($parts.Count -lt 2) { - Write-Error "anvil: unexpected rustc version output: $versionLine" - exit 1 - } - Write-Output $parts[1] - # _install-tool-core: install a cargo subcommand at the catalog version, # or no-op if it is already installed at or above that version. The # `installer` parameter selects between: diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap index c52df3292..8adbdd8ea 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap @@ -2456,9 +2456,9 @@ steps: # group: which anvil group to install setup for. Special values: # - "" (default): install the full catalog via # `just anvil-setup` -- use for "give me everything" flows. -# - "none": skip tool installation entirely; just bootstrap the -# rust cache + libclang + just. Used by impact.yml, which installs -# only cargo-delta afterwards. +# - "none": skip tool installation entirely; just restore the +# Cargo cache and bootstrap libclang + just. Used by impact.yml, +# which installs only cargo-delta afterwards. # - anything else (e.g. "pr-fast"): install only that group's # prerequisites via `just anvil--setup`. parameters: @@ -2466,15 +2466,30 @@ parameters: type: string default: '' steps: + # Toolchain changes deliberately start a fresh Cargo-home cache. Besides + # bounding registry growth, this gives repositories a predictable cache + # reset point without coupling cache identity to routine manifest edits. + # Compute the digest explicitly because either supported toolchain file is + # optional and Cache@2 rejects missing file-path key segments. + - pwsh: | + $parts = @( + foreach ($name in @('rust-toolchain', 'rust-toolchain.toml')) { + if (Test-Path -LiteralPath $name -PathType Leaf) { + "$name=$((Get-FileHash -LiteralPath $name -Algorithm SHA256).Hash.ToLowerInvariant())" + } + } + ) + $fingerprint = if ($parts.Count -eq 0) { 'none' } else { $parts -join '-' } + Write-Host "##vso[task.setvariable variable=anvil_toolchain_fingerprint]$fingerprint" + displayName: anvil setup (fingerprint repository toolchain) + # Restore Cargo's registry and installed binaries before bootstrapping Just, # so warm ADO jobs do not compile Just again before reaching the cache. # Rustup toolchains are not stored under Cargo home, so the selected compiler # version does not affect this cache's contents. - task: Cache@2 inputs: - key: 'anvil-v2 | "$(Agent.OS)" | "$(Agent.OSArchitecture)" | Cargo.toml | Cargo.lock | .cargo/config.toml | rust-toolchain | rust-toolchain.toml | justfiles/anvil/versions.just' - restoreKeys: | - anvil-v2 | "$(Agent.OS)" | "$(Agent.OSArchitecture)" + key: 'anvil-v3 | "$(Agent.OS)" | "$(Agent.OSArchitecture)" | "$(anvil_toolchain_fingerprint)" | .cargo/config.toml | justfiles/anvil/versions.just' path: | $(HOME)/.cargo/registry/cache/ $(HOME)/.cargo/registry/index/ @@ -2489,13 +2504,6 @@ steps: fi displayName: anvil setup (install just) - - pwsh: | - & just anvil-toolchain-stable-install - if ($LASTEXITCODE -ne 0) { - throw 'anvil: failed to provision the selected stable toolchain' - } - displayName: anvil setup (provision stable toolchain) - # Hand everything else off to the catalog recipe. Idempotent. ADO # uses the default `install` backend (source builds) because # cargo-binstall has unresolved compliance issues for internal ADO @@ -6571,24 +6579,6 @@ _anvil-resolve-stable action="install": [group("anvil-setup")] anvil-toolchain-stable-install: (_anvil-resolve-stable "install") -# Report the selected stable rustc version for GitHub cache keys. Provisioning -# is a dependency so workflow surfaces invoke one setup-aware operation rather -# than orchestrating the resolver directly. -[private] -[script("pwsh", "-NoProfile")] -_anvil-stable-rustc-version: anvil-toolchain-stable-install - $versionLine = (& rustc {{_anvil_stable_toolchain_args}} --version | Select-Object -Last 1) - if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($versionLine)) { - Write-Error 'anvil: failed to read the selected stable rustc version' - exit 1 - } - $parts = ([string] $versionLine).Split() - if ($parts.Count -lt 2) { - Write-Error "anvil: unexpected rustc version output: $versionLine" - exit 1 - } - Write-Output $parts[1] - # _install-tool-core: install a cargo subcommand at the catalog version, # or no-op if it is already installed at or above that version. The # `installer` parameter selects between: diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap index 5cf4fb2df..2426a53ff 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap @@ -1538,8 +1538,8 @@ inputs: - "" (default): install the full catalog via `just anvil-setup` -- use for local "give me everything" flows. - - "none": skip tool installation entirely; just bootstrap the - rust toolchain + just + binstall + cache. Used by + - "none": skip tool installation entirely; just restore the + Cargo cache and bootstrap just + binstall. Used by anvil-impact, which installs only cargo-delta afterwards. - a name containing only lowercase letters, digits, and hyphens: install only that group's prerequisites via @@ -1602,68 +1602,29 @@ runs: } "Free after cleanup (GiB): $(Get-FreeDiskGiB)" - # Stable provisioning is part of the generated Just setup graph, so - # bootstrap Just before asking it for the selected compiler version. - # cargo-binstall keeps this cold path fast. - - name: Install cargo-binstall - uses: cargo-bins/cargo-binstall@v1.21.0 # immutable release, the tag cannot be moved - - - name: Install just - shell: bash - run: | - if ! command -v just >/dev/null 2>&1 ; then - cargo binstall --no-confirm --locked just || cargo install --locked just - fi - - - name: Provision stable toolchain and capture version - id: rustc-version - shell: pwsh -NoProfile -Command ". '{0}'" - run: | - $version = (& just _anvil-stable-rustc-version | Select-Object -Last 1) - if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($version)) { - throw 'anvil: failed to provision the selected stable toolchain' - } - $version = $version.Trim() - "version=$version" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append + # Restore before bootstrapping Just so a warm job can reuse the cached + # executable. Repository toolchain files intentionally define a fresh + # cache generation, bounding registry growth without invalidating on + # routine Cargo.toml or Cargo.lock edits. - name: Restore cargo cache id: cargo-cache uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: - # Key on OS + arch + rustc version + lockfile hashes + catalog - # hash so a Rust toolchain bump, a cross-arch matrix leg, or a - # tool-versions update all invalidate the cache cleanly. - # runner.arch resolves to X64 / ARM64 / X86, which keeps the - # x86_64 and aarch64 legs of the same OS from colliding on - # arch-incompatible cached binaries in ~/.cargo/bin. - # - # ${{ github.job }} discriminates by workflow-job-id (`pr-fast`, - # `pr-test`, `pr-slow`, etc.) so concurrent matrix legs that - # share OS+arch (e.g. pr-fast linux + pr-test linux) don't race - # on the same cache key. Without this, the first-to-finish job - # reserves the key, the others get "Unable to reserve cache: - # another job may be creating this cache" and silently skip - # the save -- leaving the cache empty forever. The restore-keys - # fall back across jobs so the install work is still shared - # across sibling legs on subsequent runs. - key: anvil-v1-${{ runner.os }}-${{ runner.arch }}-rust${{ steps.rustc-version.outputs.version }}-${{ hashFiles('Cargo.toml', 'Cargo.lock', '.cargo/config.toml', 'rust-toolchain', 'rust-toolchain.toml', 'justfiles/anvil/versions.just') }}-${{ github.job }} + # The job suffix prevents concurrent matrix jobs from racing to save + # one key. The prefix shares a completed cache across sibling jobs. + # There is deliberately no fallback across toolchain/catalog changes: + # those inputs are the cache-pruning boundary. + key: anvil-v2-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('.cargo/config.toml', 'rust-toolchain', 'rust-toolchain.toml', 'justfiles/anvil/versions.just') }}-${{ github.job }} restore-keys: | - anvil-v1-${{ runner.os }}-${{ runner.arch }}-rust${{ steps.rustc-version.outputs.version }}-${{ hashFiles('Cargo.toml', 'Cargo.lock', '.cargo/config.toml', 'rust-toolchain', 'rust-toolchain.toml', 'justfiles/anvil/versions.just') }}- - anvil-v1-${{ runner.os }}-${{ runner.arch }}-rust${{ steps.rustc-version.outputs.version }}- - anvil-v1-${{ runner.os }}-${{ runner.arch }}- + anvil-v2-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('.cargo/config.toml', 'rust-toolchain', 'rust-toolchain.toml', 'justfiles/anvil/versions.just') }}- # `.crates.toml` and `.crates2.json` track which cargo-installed # tools and versions live in ~/.cargo/bin/. Without them in the # cache, `cargo install --list` and (downstream) the # tool-install recipes' early-skip on "installed >= pin" don't - # see the cached binaries — every run then tries to reinstall - # on top of them and fails with "binary X already exists in - # destination". + # see the cached binaries. # - # target/ (the compilation output dir) is deliberately NOT cached here: - # per the GitHub backend design's caching policy, multi-GB per-job - # target/ trees dwarf the high-value tool cache under the repo cache - # quota, give only modest recompile savings once tools are cached, and - # could overwrite the downloaded target/anvil/impact/ cache. Only the - # ~/.cargo tool + registry state below is cached. + # target/ is deliberately excluded: per-job target trees dwarf the + # tool cache and could overwrite the downloaded impact cache. path: | ~/.cargo/registry/cache/ ~/.cargo/registry/index/ @@ -1671,6 +1632,17 @@ runs: ~/.cargo/.crates.toml ~/.cargo/.crates2.json + # cargo-binstall keeps the cold bootstrap path fast. + - name: Install cargo-binstall + uses: cargo-bins/cargo-binstall@v1.21.0 # immutable release, the tag cannot be moved + + - name: Install just + shell: bash + run: | + if ! command -v just >/dev/null 2>&1 ; then + cargo binstall --no-confirm --locked just || cargo install --locked just + fi + # The catalog recipe `anvil-setup` (or `anvil--setup` # when a group is specified via the `group` input) uses the Just # bootstrapped above, then handles everything else. The setup recipes are idempotent and @@ -6374,24 +6346,6 @@ _anvil-resolve-stable action="install": [group("anvil-setup")] anvil-toolchain-stable-install: (_anvil-resolve-stable "install") -# Report the selected stable rustc version for GitHub cache keys. Provisioning -# is a dependency so workflow surfaces invoke one setup-aware operation rather -# than orchestrating the resolver directly. -[private] -[script("pwsh", "-NoProfile")] -_anvil-stable-rustc-version: anvil-toolchain-stable-install - $versionLine = (& rustc {{_anvil_stable_toolchain_args}} --version | Select-Object -Last 1) - if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($versionLine)) { - Write-Error 'anvil: failed to read the selected stable rustc version' - exit 1 - } - $parts = ([string] $versionLine).Split() - if ($parts.Count -lt 2) { - Write-Error "anvil: unexpected rustc version output: $versionLine" - exit 1 - } - Write-Output $parts[1] - # _install-tool-core: install a cargo subcommand at the catalog version, # or no-op if it is already installed at or above that version. The # `installer` parameter selects between: diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap index 66578b989..e512bb4c7 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap @@ -5230,24 +5230,6 @@ _anvil-resolve-stable action="install": [group("anvil-setup")] anvil-toolchain-stable-install: (_anvil-resolve-stable "install") -# Report the selected stable rustc version for GitHub cache keys. Provisioning -# is a dependency so workflow surfaces invoke one setup-aware operation rather -# than orchestrating the resolver directly. -[private] -[script("pwsh", "-NoProfile")] -_anvil-stable-rustc-version: anvil-toolchain-stable-install - $versionLine = (& rustc {{_anvil_stable_toolchain_args}} --version | Select-Object -Last 1) - if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($versionLine)) { - Write-Error 'anvil: failed to read the selected stable rustc version' - exit 1 - } - $parts = ([string] $versionLine).Split() - if ($parts.Count -lt 2) { - Write-Error "anvil: unexpected rustc version output: $versionLine" - exit 1 - } - Write-Output $parts[1] - # _install-tool-core: install a cargo subcommand at the catalog version, # or no-op if it is already installed at or above that version. The # `installer` parameter selects between: diff --git a/justfiles/anvil/tools.just b/justfiles/anvil/tools.just index 194a7172c..85b54c06b 100644 --- a/justfiles/anvil/tools.just +++ b/justfiles/anvil/tools.just @@ -372,24 +372,6 @@ _anvil-resolve-stable action="install": [group("anvil-setup")] anvil-toolchain-stable-install: (_anvil-resolve-stable "install") -# Report the selected stable rustc version for GitHub cache keys. Provisioning -# is a dependency so workflow surfaces invoke one setup-aware operation rather -# than orchestrating the resolver directly. -[private] -[script("pwsh", "-NoProfile")] -_anvil-stable-rustc-version: anvil-toolchain-stable-install - $versionLine = (& rustc {{_anvil_stable_toolchain_args}} --version | Select-Object -Last 1) - if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($versionLine)) { - Write-Error 'anvil: failed to read the selected stable rustc version' - exit 1 - } - $parts = ([string] $versionLine).Split() - if ($parts.Count -lt 2) { - Write-Error "anvil: unexpected rustc version output: $versionLine" - exit 1 - } - Write-Output $parts[1] - # _install-tool-core: install a cargo subcommand at the catalog version, # or no-op if it is already installed at or above that version. The # `installer` parameter selects between: From 1957f197f1301c623a835353c226201f669a2483 Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Tue, 1 Sep 2026 12:49:15 +0200 Subject: [PATCH 26/32] Address stable toolchain review feedback Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: f7996e6f-5c20-4d7a-b2f7-690af95e557e --- .anvil.lock | 16 +- .github/actions/anvil-setup/action.yml | 2 +- crates/cargo-anvil/README.md | 18 +- crates/cargo-anvil/docs/design/README.md | 22 +- crates/cargo-anvil/docs/design/ado.md | 79 ++-- crates/cargo-anvil/docs/design/checks.md | 69 ++-- crates/cargo-anvil/docs/design/containers.md | 6 +- crates/cargo-anvil/docs/design/github.md | 94 ++--- crates/cargo-anvil/docs/design/local.md | 102 ++--- crates/cargo-anvil/docs/implementation.md | 53 +++ crates/cargo-anvil/src/anvil/artifacts/ado.rs | 41 ++- .../cargo-anvil/src/anvil/artifacts/github.rs | 2 + .../src/anvil/artifacts/justfile.rs | 261 ++++++++----- crates/cargo-anvil/src/lib.rs | 16 +- .../cargo-anvil/templates/ado/steps/group.yml | 31 +- .../templates/ado/steps/pr-title-env.yml | 2 + .../templates/ado/steps/pr-title-step.yml | 24 ++ .../templates/github/setup-action.yml | 2 +- .../justfiles/anvil/checks/msrv-test.just | 12 +- .../justfiles/anvil/checks/readme-check.just | 5 +- .../justfiles/anvil/checks/semver-check.just | 5 +- .../justfiles/anvil/groups/pr-slow.just | 11 +- .../templates/justfiles/anvil/tools.just | 48 ++- .../templates/justfiles/anvil/versions.just | 3 + crates/cargo-anvil/tests/recipe_contracts.rs | 79 +++- .../snapshots/snapshots__ado_backend.snap | 347 ++++-------------- .../snapshots/snapshots__github_backend.snap | 86 +++-- .../snapshots/snapshots__local_only.snap | 84 +++-- justfiles/anvil/checks/msrv-test.just | 12 +- justfiles/anvil/checks/readme-check.just | 5 +- justfiles/anvil/checks/semver-check.just | 5 +- justfiles/anvil/groups/pr-slow.just | 11 +- justfiles/anvil/tools.just | 48 ++- justfiles/anvil/versions.just | 3 + 34 files changed, 865 insertions(+), 739 deletions(-) create mode 100644 crates/cargo-anvil/templates/ado/steps/pr-title-env.yml create mode 100644 crates/cargo-anvil/templates/ado/steps/pr-title-step.yml diff --git a/.anvil.lock b/.anvil.lock index 43a7a2eac..1b42d4591 100644 --- a/.anvil.lock +++ b/.anvil.lock @@ -1,7 +1,7 @@ version = 1 tool = "anvil" tool_version = "0.6.0" -catalog_checksum = "sha256:2bc31e569ea3ac1bf82f45bdf75246cf80faecf2ffe3824c6e5eea659cd9454d" +catalog_checksum = "sha256:7382ec5160cdd18e1ac7430ec906473bb730bff8d858a0ea21a632196626281e" [[file]] path = ".anvil/container/Dockerfile.dockerignore" @@ -21,7 +21,7 @@ checksum = "sha256:ff8def6c0786b6e146c4b633dfe38cb9b8ede398345516cca32bbcd558608 [[file]] path = ".github/actions/anvil-setup/action.yml" -checksum = "sha256:898f28eec0c0e0006dd5b433a7cfedd31188e41a06d27be1b64d51f5c28605c2" +checksum = "sha256:e1029f7ff95a5966184b4c04ccbed04ee8ba93bfc93bc9ac307d3dcef31374fb" [[file]] path = ".github/actions/anvil-setup/just-problem-matcher.json" @@ -137,7 +137,7 @@ checksum = "sha256:9e9d0cbfef1e1e1586c2af4e9c387719c2f017ea1203d326baacc3ae25df8 [[file]] path = "justfiles/anvil/checks/msrv-test.just" -checksum = "sha256:968a0cf65a319626c3ed61f75e1d363608856664d105850be281e91f8ecf3537" +checksum = "sha256:9c25b140dcaa4b38db7701fd627dd9a2d5ad5f9bb83d4490c146058bf4328cc8" [[file]] path = "justfiles/anvil/checks/mutants-diff.just" @@ -153,11 +153,11 @@ checksum = "sha256:28734aa77526ca5c53d89a32c3e20ae6b42d2032c73ab6a1dbc9831f25cb0 [[file]] path = "justfiles/anvil/checks/readme-check.just" -checksum = "sha256:777b6b7594b299452dc97f949937fa1a521e06de5cebbe643267aabbb41fc6bb" +checksum = "sha256:4993e662875561ddf976ec94c69ec0d647a5c957bfb8f5174f79162a640ed892" [[file]] path = "justfiles/anvil/checks/semver-check.just" -checksum = "sha256:6fdb13161d3d0e6ac639472d1aa2087fd135634e7272276a501f1f946a555075" +checksum = "sha256:be0430a53007301db00602d4bc9296f51ad9b2f011583084d069c51b30939b74" [[file]] path = "justfiles/anvil/checks/spellcheck.just" @@ -189,7 +189,7 @@ checksum = "sha256:9a94b6ae2d212f91d324e401ebdb2238805f3450d95d4d45df791d7f1679a [[file]] path = "justfiles/anvil/groups/pr-slow.just" -checksum = "sha256:5d8bb4b73ce14ec76477f90b754c47904054e1497471a21d3d84b0ee6a4676d0" +checksum = "sha256:c3281f2044494dcfcfa7544b022ce3d751444241d93d3e8f136669d72d102528" [[file]] path = "justfiles/anvil/groups/pr-test.just" @@ -229,11 +229,11 @@ checksum = "sha256:00453a12cbb34811ee6a2c083dade5f6198575e3b0610f49e4743366326cd [[file]] path = "justfiles/anvil/tools.just" -checksum = "sha256:e892e09dcedea51d0c62ab4ab2541e0d036ea15816782088d2f306b800de80ef" +checksum = "sha256:05649a470de4226b503d541a5f6ea35a8f3075de6f4e7349075408a7efb8c036" [[file]] path = "justfiles/anvil/versions.just" -checksum = "sha256:13eccb8202ddc168717d090d57bcbb257b11084e6f47361b60f634c2c8185046" +checksum = "sha256:ae6676fb883aa58adb0995384adef477957ad7df10fc43439517cfb617fd92d1" [[region]] host = ".anvil/container/Dockerfile" diff --git a/.github/actions/anvil-setup/action.yml b/.github/actions/anvil-setup/action.yml index 4ad8d0b91..0c5c0077a 100644 --- a/.github/actions/anvil-setup/action.yml +++ b/.github/actions/anvil-setup/action.yml @@ -119,7 +119,7 @@ runs: fi # The catalog recipe `anvil-setup` (or `anvil--setup` - # when a group is specified via the `group` input) uses the Just + # when a group is specified via the `group` input) uses the Just executable # bootstrapped above, then handles everything else. The setup recipes are idempotent and # short-circuit on tools already installed at or above the pinned # version, so re-running on cache-hit runs is cheap. diff --git a/crates/cargo-anvil/README.md b/crates/cargo-anvil/README.md index 118692de1..d3eb1fb20 100644 --- a/crates/cargo-anvil/README.md +++ b/crates/cargo-anvil/README.md @@ -77,6 +77,14 @@ Flags: `catalog:` checksum — a `sha256` over the whole compiled-in catalog — so two builds at the same version but different catalogs are distinguishable. +Before running ordinary checks, provide a deterministic Rust selection: +a caller-set `RUSTUP_TOOLCHAIN`, either root `rust-toolchain` file spelling, +or a root `[workspace.package].rust-version` / `[package].rust-version`. +Anvil passes no explicit selector for the environment or file cases so +rustup handles them natively; only the root MSRV becomes `+`. +Repositories with none of these sources fail instead of inheriting the +runner’s ambient compiler. + ### Daily driver After the first run, your daily workflow is plain `just`: @@ -266,11 +274,11 @@ the tables below map each check to the group that runs it, link each check to its tool’s documentation, and note anything anvil-specific. **PR tier** (`anvil-pr`) — runs on every pull request, impact-scoped -both locally and in cloud workflows. `pr-fast` is one job, while the four -`pr-slow` sub-groups run as independent parallel jobs per OS leg: +both locally and in cloud workflows. `pr-fast` is one job, while the +`pr-slow` groups run as independent parallel jobs per OS leg:
JobSub-groupCheckNotes
- + @@ -290,7 +298,7 @@ both locally and in cloud workflows. `pr-fast` is one job, while the four - + @@ -483,7 +491,7 @@ And `docs/verification.md` for the continuous-validation strategy. This crate was developed as part of The Oxidizer Project. Browse this crate's source code. - [__cargo_doc2readme_dependencies_info]: ggGmYW0CYXZlMC43LjNhdIQbFhzZ8rzWNNYbuRaDSGWynFgbH4PMdoT7GNcbVwNPtPjAhvFhYvRhcoQbKUQeMMosXtQb0hCXAl_zicgbTfhCfds-TfwbDvJtFhWU1qxhZIGDa2NhcmdvLWFudmlsZTAuNi4wa2NhcmdvX2Fudmls + [__cargo_doc2readme_dependencies_info]: ggGmYW0CYXZlMC43LjNhdIQbFhzZ8rzWNNYbuRaDSGWynFgbH4PMdoT7GNcbVwNPtPjAhvFhYvRhcoQbLvVGTNtetQUbnp9vX0Ew7_gbkZEyxfXZXyMbltL72AXa-o1hZIGDa2NhcmdvLWFudmlsZTAuNi4wa2NhcmdvX2Fudmls [__link0]: https://crates.io/crates/cargo-delta [__link1]: https://docs.rs/cargo-anvil/0.6.0/cargo_anvil/?search=artifacts::container [__link10]: https://docs.rs/cargo-anvil/0.6.0/cargo_anvil/?search=artifacts diff --git a/crates/cargo-anvil/docs/design/README.md b/crates/cargo-anvil/docs/design/README.md index e6eab37ce..205747376 100644 --- a/crates/cargo-anvil/docs/design/README.md +++ b/crates/cargo-anvil/docs/design/README.md @@ -58,11 +58,15 @@ missed, and onboarding new Rust repos requires copying-and-praying. full tier are all reproducible locally with a single `just` invocation, using the exact same arguments cloud workflows uses. The three commands `just anvil-pr`, `just anvil-scheduled`, and `just anvil-full` (= pr + scheduled) are first-class local entry points. -6. **Plain-cargo fallback**: a developer with only `cargo` installed (no `just`, no +6. **Deterministic ordinary-check compiler selection**: use the caller's + `RUSTUP_TOOLCHAIN`, otherwise defer to either root toolchain-file spelling, + otherwise use the root manifest MSRV explicitly. Never inherit a + runner-default compiler when none of those sources exists. +7. **Plain-cargo fallback**: a developer with only `cargo` installed (no `just`, no `cargo-anvil`) can still build and run tests. -7. **Friendly updates**: the tool detects, per file and per managed region, whether the user has +8. **Friendly updates**: the tool detects, per file and per managed region, whether the user has modified it, and updates only the unmodified bits. -8. **Open source**: the crate ships from `github.com/microsoft/ox-tools` and publishes to +9. **Open source**: the crate ships from `github.com/microsoft/ox-tools` and publishes to crates.io. The binary contains no Microsoft-internal dependencies; everything it can install on the user's behalf comes from crates.io. @@ -78,6 +82,9 @@ missed, and onboarding new Rust repos requires copying-and-praying. existing override, a repository toolchain file, or the repository MSRV. GitHub setup and container construction install a missing public channel through rustup; caller-provisioned and path toolchains remain the execution environment's responsibility. +- Provisioning a separate compiler for Cargo-installed tools or stable analyzers. They use the + same repository-selected compiler as ordinary checks; nightly-only checks retain their + catalog pins. - Managing exact tool versions on the user's behalf — we enforce minimums only. See [local.md §3](./local.md#3-tool-versions-and-installation). - Hosting a service. The tool is a CLI binary; updates ship via crates.io. @@ -307,12 +314,11 @@ Detail on each host: users add their own attribute rules outside the region. - **`rust-toolchain`**, **`rust-toolchain.toml`**, and **`.cargo/config.toml`** — never touched. Toolchain files are optional read-only inputs to stable-toolchain selection. - When neither selects a toolchain, Anvil selects the root manifest's + When neither root spelling exists, Anvil selects the root manifest's `[workspace.package].rust-version` or `[package].rust-version`. GitHub setup installs a - missing public selection through rustup. ADO pipelines provision their selected internal - toolchain before Anvil runs. The caller's `RUSTUP_TOOLCHAIN` is an input override only; - generated recipes apply resolved selection per stable command rather than exporting it - globally. + missing public selection through rustup. ADO callers provide their Rust bootstrap before + Anvil runs. The caller's `RUSTUP_TOOLCHAIN` is an input override only and is inherited by + child commands rather than rewritten or exported by Anvil. The tool's persistent state lives in `.anvil.lock` at the repo root — the sidecar manifest tracking last-rendered checksums per owned file and per managed region. See diff --git a/crates/cargo-anvil/docs/design/ado.md b/crates/cargo-anvil/docs/design/ado.md index ba5f9a8d8..135df73a4 100644 --- a/crates/cargo-anvil/docs/design/ado.md +++ b/crates/cargo-anvil/docs/design/ado.md @@ -372,12 +372,10 @@ The PR template also contains a `pr_msrv` stage. It runs in parallel with x86_64 jobs and per-OS affected-package impact sets as `pr_test`, and invokes `anvil-pr-msrv`. The stage consumes the per-OS impact artifact like the other PR groups; when the root manifest declares no MSRV, the recipe exits successfully -without running tests. -Callers may set `RUSTUP_TOOLCHAIN` to an already-provisioned stable compiler and -`ANVIL_MSRV_TOOLCHAIN` to an already-provisioned compiler for the dedicated -MSRV run. Supplying only the MSRV mapping is rejected because stable checks -would otherwise select an unrelated compiler. When the mapping is unset, -Anvil installs the declared MSRV through rustup. +after reporting that it skipped the test. Otherwise it runs affected-package +`cargo test --all-targets` in all-features and default-features configurations. +Ordinary stable checks honor a caller-provided `RUSTUP_TOOLCHAIN`. The dedicated +MSRV setup ensures the declared root MSRV is available through rustup. ### 4.1 Per-job wrapper (`steps/job.yml`) — the 1ESPT extensibility point @@ -674,13 +672,11 @@ steps: # probed from the cacheable impact.state marker. See §4.3. ``` -The only per-group parameters are the PR-context strings a group's checks consume: - -| Template | Parameters | -|---------------------------|-------------------------------------------------------------------------| -| `pr-fast.yml` | `prTitle` (resolved from the REST API; ADO has no PR-title variable) | -| `pr-mutants.yml` | `prBaseRef` (default `$(System.PullRequest.TargetBranch)`) | -| `pr-test.yml`, `pr-runtime-analysis.yml`, `scheduled-*.yml` | — | +The generated per-group step templates take no parameters. Only `pr-fast.yml` +performs the PR-title REST lookup and injects `PR_TITLE`; groups that do not run +`anvil-pr-title`, including `pr-msrv`, have no title API or OAuth-token +dependency. The mutation recipe reads its base ref from ADO's predefined +pull-request environment. `$(System.PullRequest.*)` are auto-populated by ADO on PR build-validation runs. No manual web-UI wiring is needed. @@ -702,10 +698,9 @@ download to get scoping. ### `setup.yml` and `impact.yml` -`setup.yml` is a step template that installs `just` -(`cargo install just --locked`), provisions or prepares the selected stable -compiler through the setup dependency graph while capturing its cache-key -version, and then invokes the requested catalog setup recipe. It +`setup.yml` is a step template that restores Cargo home, bootstraps Just, and +then invokes the requested catalog setup recipe, whose prerequisites provision +the selected compiler and tools. It takes a single `group` parameter that controls which recipes run: - empty (default): runs `just anvil-setup` -- the full catalog. Use for "give @@ -745,35 +740,27 @@ mechanics are in [local.md §4](./local.md#4-impact-scoping-via-the-anvil-impact ## 6. Rust toolchain The user's root pipeline or compliance template installs the Rust/rustup -bootstrap before the Anvil stages run. The generated setup step restores Cargo -home before bootstrapping Just. Its selected catalog setup recipe reaches -`anvil-toolchain-stable-install` before any stable Cargo or Rust use. The -template never invokes `_anvil-resolve-stable` itself and does -not publish a resolved `RUSTUP_TOOLCHAIN` to subsequent steps. Each stable recipe -evaluates the inline PowerShell array expression and invokes Cargo or Rust -directly in its current PowerShell process. Selection has no expression-time -subprocess, host-OS branch, or change to the adopter's configured shell. A -selecting toolchain file remains unoverridden so rustup processes the complete -file. - -Selection follows the shared local contract: an existing `RUSTUP_TOOLCHAIN`, -then a root toolchain file with `channel` or `path`, then the root MSRV. A -caller-provided override takes precedence over repository files and is treated -as an externally provisioned selection. Anvil does not parse repository -toolchain files or reapply options from a file suppressed by that override. -Without an override, rustup processes the complete repository toolchain file -natively. Without either native selector, Anvil installs the root MSRV. - -`ANVIL_MSRV_TOOLCHAIN` may map the dedicated MSRV run to a separately -provisioned toolchain. It does not select the compiler used by ordinary stable -checks and therefore must be paired with a stable selector. - -Shared Cargo-tool/default-component setup and stable-only leaf setup all depend -on `anvil-toolchain-stable-install`; Just deduplicates it for group/full -fan-out. `anvil-tool-rustc-validate-prereqs` remains read-only and validates -that `rustc` is available plus the workspace MSRV compatibility rule. For -nightly-requiring checks, the matching toolchain-validate-prereqs recipe fails -with a suggestion to ask the pipeline owner to provision that dated nightly. +bootstrap before the Anvil stages run. Selection follows the shared local +contract: + +1. inherit an existing caller-provided `RUSTUP_TOOLCHAIN` unchanged; +2. when either root `rust-toolchain` spelling exists, pass no explicit selector + and let rustup process toolchain files natively; +3. otherwise pass the root manifest MSRV explicitly as `+`. + +There is no agent-default fallback. With no environment override, root +toolchain file, or root MSRV, setup and checks fail. Anvil does not parse +repository toolchain files or replay options from a file suppressed by the +environment override. Native file processing follows rustup's lookup from each +Cargo or Rust command's working directory. + +The generated setup step restores Cargo home before bootstrapping Just, then +invokes the selected catalog setup recipe. Setup ensures the selected compiler +is available before stable Cargo or Rust use and does not publish a rewritten +`RUSTUP_TOOLCHAIN` to later steps. Prerequisite validation remains read-only: +for a root-MSRV fallback, it requires rustup and verifies the exact toolchain is +already installed before running Cargo metadata. Nightly-requiring checks still +fail with a provisioning hint when their dated catalog toolchain is absent. ## 7. Caching diff --git a/crates/cargo-anvil/docs/design/checks.md b/crates/cargo-anvil/docs/design/checks.md index 899acf818..7ccb24994 100644 --- a/crates/cargo-anvil/docs/design/checks.md +++ b/crates/cargo-anvil/docs/design/checks.md @@ -108,26 +108,26 @@ flowchart LR (Tier nodes are the user-facing entry points; group nodes are the unit of cloud workflows parallelization; check nodes are the individual `anvil-` recipes. -The four `pr-slow` workloads run as parallel cloud-workflow jobs/stages rather -than sequentially. Locally, `just anvil-pr-slow` invokes the four sub-recipes in -order, and `just anvil` is an alias for `just anvil-pr`.) +The groups collected by the `pr-slow` umbrella run as parallel cloud-workflow +jobs/stages. Locally, `just anvil-pr-slow` invokes those groups in order, and +`just anvil` is an alias for `just anvil-pr`.) -### PR tier (5 groups) +### PR tier | Group | OS scope | Purpose | |--------------------|---------------------------------------|----------------------------------------------------------------------------------------------------------------------| | `pr-fast` | Linux x86_64 + Windows x86_64 + Linux aarch64 + Windows aarch64 (GH) / Linux x86_64 + Windows x86_64 (ADO) | All static analysis: clippy, `udeps`, `semver-check`, `external-types`, plus the text/metadata checks (fmt, license-headers, ...). Cross-OS because clippy, doc-build, udeps, semver-check, and external-types all compile per host target. Text/metadata checks run on every leg too; the redundancy cost is negligible compared to a separate job's setup overhead. | | `pr-test` | Same default as `pr-fast` | Tests + coverage: `llvm-cov` (instrumented `nextest`), `doc-test`, `examples`. Coverage is uploaded once from the canonical x86_64 Linux leg. | -| `pr-msrv` | Same default as `pr-test` | Affected-package unit and integration tests under the declared MSRV, with all features and default features. The recipe is a no-op when no root MSRV is declared. | +| `pr-msrv` | Same default as `pr-test` | Affected-package all-target tests under the declared MSRV, in all-features and default-features configurations. The recipe is a no-op when no root MSRV is declared. | | `pr-runtime-analysis` | Same default as `pr-fast` | Stricter-runtime correctness: `miri`, `careful`, `loom` (concurrency model checking), `bolero` (short-duration fuzzing smoke). Impact-scoped to the affected set so wall-clock is proportional to the PR's blast radius; the cheap checks (loom/bolero) self-skip when no affected crate ships their harness. | | `pr-mutants` | Linux x86_64 + Windows x86_64 + Linux aarch64 (GH) / Linux x86_64 + Windows x86_64 (ADO) | Diff-scoped mutation testing (`mutants --in-diff`). The recipe self-skips on `aarch64-pc-windows-msvc` (cargo-mutants doesn't build there), so the GH windows-arm leg is a no-op rather than a job failure. | -The four `pr-slow*` groups are independent: failures in `pr-test` don't block +The `pr-slow` groups are independent: failures in `pr-test` don't block `pr-msrv`, `pr-runtime-analysis`, or `pr-mutants`, and overall PR wall-clock is their maximum rather than their sum. Locally, `just anvil-pr-slow` is an umbrella -recipe that runs all four sub-recipes sequentially. +recipe that invokes those groups sequentially. -### scheduled tier (4 groups) +### scheduled tier | Group | OS scope | Purpose | |----------------------|---------------------------|----------------------------------------------------------------------------------------------------------------------------------------| @@ -148,7 +148,7 @@ backend-specific knobs ([github.md §4](./github.md#4-owned-reusable-workflows) the per-leg runner-label inputs and forking the workflow when the matrix shape itself needs to change, [ado.md §4](./ado.md#4-owned-stages-templates) for `linuxPool`/`windowsPool`). -Locally there is no OS matrix; `just anvil-pr-slow` (the umbrella recipe) runs the four sub-recipes in sequence against whatever OS the +Locally there is no OS matrix; `just anvil-pr-slow` (the umbrella recipe) invokes its groups in sequence against whatever OS the developer is on. See [README.md §8.3](./README.md#83-cross-os-test-matrices) for the overall rationale. @@ -164,19 +164,13 @@ The cell format is `cargo invocation (short rationale)`. "Source" cites the surv that provided the strongest version of the check. Invocations shown without a pinned nightly or MSRV use the selected stable -compiler. Their generated PowerShell recipes invoke Cargo directly with the -lazy optional stable argument. Caller-provided `RUSTUP_TOOLCHAIN` and a -repository-root toolchain file remain native rustup inputs and produce no -argument; only the root MSRV fallback produces an explicit `+toolchain`. The -shared inline PowerShell array expression evaluates in that same recipe process -without a host-OS branch. Its repository root is fixed by -`justfile_directory()`, so checks that enter crate directories retain the same -selection as setup. They do not route the main command through a nested Just -recipe. -Their paired setup recipes reach `anvil-toolchain-stable-install` directly or -through the shared Cargo-tool/default-component installer before any stable -command runs. Paired `*-validate-prereqs` recipes remain read-only and never -depend on installation. +compiler. Caller-provided `RUSTUP_TOOLCHAIN` remains a native rustup input and +is inherited unchanged by child commands. The presence of either root +toolchain-file spelling suppresses an explicit selector so rustup can process +the file natively at each command's working directory. Only the root MSRV +fallback produces an explicit `+toolchain`; with no source, the command fails. +Setup makes the selected compiler available before stable Cargo or Rust runs, +while paired prerequisite validation remains read-only. ### `pr-fast` @@ -198,12 +192,12 @@ depend on installation. | `semver-check` | `cargo semver-checks --baseline-rev ` per affected publishable library crate. Crates with `publish = false` and bin-only crates are skipped. The PR target is the baseline. Exit 100 is a completed check with deny-level findings; exit 101 or another nonzero status means the comparison was inconclusive. Both outcomes write `target/anvil/comments/semver.md` and remain advisory, matching the repository's native `semver` job (`continue-on-error: true`). Proven rename and bin→lib transitions with no comparable baseline, and dependencies proven to be yanked only in the checked-out baseline tree, are skipped without a comment. Anvil preflight failures such as invalid current-workspace metadata or an unavailable baseline ref still fail because the recipe cannot establish what to compare. | oxidizer-github | | `external-types` | `cargo + check-external-types --manifest-path` per library crate (per-manifest because the tool has no `--workspace`/`--package`; bin-only crates have no public API surface and are skipped). Setup installs the catalog version but validation accepts newer installed tools. The selected nightly is tested with the catalog version; an incompatible newer tool fails closed with a tool/nightly compatibility diagnostic rather than silently selecting a different schema. | oxidizer-github | -### `pr-slow` +### `pr-slow` umbrella -The PR-tier slow checks are split into four independent cloud-workflow-visible groups — +The PR-tier slow checks are split into independent cloud-workflow-visible groups — `pr-test`, `pr-msrv`, `pr-runtime-analysis`, `pr-mutants` — that each run as their own job (GitHub) or stage (ADO) in parallel. An umbrella `anvil-pr-slow` recipe is also provided in -`groups.just` for local use; it invokes the four sub-recipes sequentially so +`groups.just` for local use; it invokes those groups sequentially so adopters can type one command to run "everything slow" without needing the cloud workflow matrix overhead. @@ -215,29 +209,28 @@ matrix overhead. | `doc-test` | Two cargo-test runs over the same affected set: `cargo test --doc --workspace --all-features --locked` and `cargo test --doc --workspace --locked` (default features). Running both catches doctests that only compile under one feature configuration (oxidizer-github runs both). nextest does not run doctests, so this stays a separate cargo-test invocation. | oxidizer, oxidizer-github | | `examples` | `cargo build --workspace --examples --all-features --locked` -- verifies that example targets compile. Running each example is intentionally not part of the check (examples are not test scaffolding; their runtime behavior isn't part of what we gate on). | oxidizer, oxidizer-github | -This is the same set of checks that used to live in the standalone `pr-test` group; merging into `pr-test` removes one cloud-workflow job from the matrix without changing what runs. - #### `pr-msrv` (minimum-version tests) -When the root manifest declares an MSRV, Anvil runs affected-package unit and -integration tests under that compiler with both all features and default features. -This is the ordinary test-suite execution at the minimum supported compiler; +When the root manifest declares an MSRV, Anvil runs `cargo test --all-targets` +for affected packages under that compiler. Cargo's all-target set covers +library and binary unit tests, integration tests, examples, and benches as test +targets. Anvil runs exactly two feature configurations: `--all-features` and +the default features. It does not add a `--no-default-features` pass; such a +pass can exercise feature-negative code, but it is outside the current policy. +This is the all-target test execution at the minimum supported compiler; `pr-test` runs the same affected suite through coverage instrumentation on the catalog nightly. Other checks that use the selected stable compiler do not execute -this unit/integration suite, so an MSRV fallback does not make `pr-msrv` a duplicate. +this all-target suite, so an MSRV fallback does not make `pr-msrv` a duplicate. A selecting toolchain file does not suppress the MSRV run, even when it selects the same compiler, because the MSRV group is the authoritative minimum-version test result. The group uses the same OS/architecture matrix and per-OS impact sets as `pr-test` so cfg-gated targets and dependencies are exercised under the MSRV. It runs in -parallel with the other PR groups. When no root MSRV exists, the recipe exits -successfully without running tests. -`ANVIL_MSRV_TOOLCHAIN` may map the declared MSRV to an already-provisioned -toolchain. Callers pair it with `RUSTUP_TOOLCHAIN` (or a selecting repository -toolchain file) for stable checks; a mapping by itself is rejected rather than -allowing stable checks to select an unrelated compiler. When the mapping is -unset, setup installs the declared MSRV through rustup. +parallel with the other PR groups. When no root MSRV exists, +`anvil-msrv-test` reports the skip and exits successfully without running tests. +Setup installs the declared MSRV through rustup when it is not already +available. #### `pr-runtime-analysis` (stricter-runtime correctness) diff --git a/crates/cargo-anvil/docs/design/containers.md b/crates/cargo-anvil/docs/design/containers.md index 90d9124ab..b4753c5be 100644 --- a/crates/cargo-anvil/docs/design/containers.md +++ b/crates/cargo-anvil/docs/design/containers.md @@ -53,6 +53,10 @@ Both are addressed by executing the recipe unchanged inside an image built from bodies are identical in either mode, and cloud workflows are unaffected: they run the same recipes natively on their own agents. The image is pinned to resemble that environment, not to reproduce it. +Image construction has a deliberately stricter compiler contract than native execution: the repository must own +`rust-toolchain.toml`. The build context admits that file but not `rust-toolchain`, and the build does not inherit the +host's `RUSTUP_TOOLCHAIN`; rustup therefore selects the image compiler from the repository-owned TOML file. + ## 2. Command surface `anvil-container` takes the argv to run inside the image; nothing is routed there automatically, so everything @@ -74,7 +78,7 @@ through the environment instead. | `just anvil-container-status` | Print the engine, working directory, image reference, and whether it is present. Never builds or pulls. | | `just anvil-container-down` | Remove this repository's cache volumes. The image is retained. | -All four are annotated `[group("anvil-container")]` and appear as one cluster in `just --groups`. Each repeats a +These recipes are annotated `[group("anvil-container")]` and appear as one cluster in `just --groups`. Each repeats a one-line summary immediately above its attributes, because `just --list` takes the last comment line before them as the description and would otherwise print the tail of a rationale paragraph as a fragment. diff --git a/crates/cargo-anvil/docs/design/github.md b/crates/cargo-anvil/docs/design/github.md index 36a354330..3759ff4ce 100644 --- a/crates/cargo-anvil/docs/design/github.md +++ b/crates/cargo-anvil/docs/design/github.md @@ -301,7 +301,8 @@ action in `consume` mode; which tiers a group's checks actually consume from tha is the catalog's concern, not the wiring layer's. Moving a check between groups never changes the reusable workflow. -Approximate shape (anvil writes this verbatim; users never edit it): +Representative emitted shape (the values shown are executable; repeated jobs +and steps called out in comments are omitted for brevity): ```yaml # .github/workflows/anvil-pr-impl.yml (owned by cargo-anvil) @@ -321,13 +322,13 @@ jobs: impact-linux: runs-on: ${{ inputs.linux_runner }} steps: - - uses: actions/checkout + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: { fetch-depth: 0 } - uses: ./.github/actions/anvil-impact # runs `just anvil-impact` + upload-artifact anvil-impact-Linux impact-windows: runs-on: ${{ inputs.windows_runner }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: { fetch-depth: 0 } - uses: ./.github/actions/anvil-impact # uploads anvil-impact-Windows @@ -343,12 +344,12 @@ jobs: || matrix.os == 'linux-arm' && inputs.linux_arm_runner || inputs.windows_arm_runner }} steps: - - uses: actions/checkout + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: { fetch-depth: 0 } # semver-check needs origin/ resolvable for --baseline-rev # Download the impact cache computed on this leg's OS into # target/anvil/impact/ (arm reuses its OS-family artifact). pr-test / # pr-runtime-analysis / pr-mutants do the identical download. - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: anvil-impact-${{ startsWith(matrix.os, 'linux') && 'Linux' || 'Windows' }} path: target/anvil/impact @@ -373,9 +374,9 @@ jobs: || matrix.os == 'linux-arm' && inputs.linux_arm_runner || inputs.windows_arm_runner }} steps: - - uses: actions/checkout + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 # preceded by the same per-OS download-artifact step as pr-fast - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: anvil-impact-${{ startsWith(matrix.os, 'linux') && 'Linux' || 'Windows' }} path: target/anvil/impact @@ -392,10 +393,13 @@ jobs: fail-fast: false matrix: os: [linux, windows, linux-arm, windows-arm] - runs-on: # selected from matrix.os and the runner-label inputs + runs-on: ${{ matrix.os == 'linux' && inputs.linux_runner + || matrix.os == 'windows' && inputs.windows_runner + || matrix.os == 'linux-arm' && inputs.linux_arm_runner + || inputs.windows_arm_runner }} steps: - - uses: actions/checkout - - uses: actions/download-artifact@v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: anvil-impact-${{ startsWith(matrix.os, 'linux') && 'Linux' || 'Windows' }} path: target/anvil/impact @@ -403,6 +407,7 @@ jobs: with: group: pr-msrv impact_mode: consume + free-disk-space: true # pr-runtime-analysis (miri + careful) and pr-mutants (mutants) follow the # multi-OS shape; pr-mutants additionally sets `env: BASE_REF` for @@ -422,9 +427,9 @@ the OS axis is almost certainly making other changes too. The MSRV group uses the same four-leg matrix as `pr-test`. Minimum-version breaks can be confined to cfg-gated OS or architecture code, so a single Linux execution would not establish the supported configuration contract. Running the group in -parallel keeps the extra test pass from extending `pr-test` serially. The Linux -impact job exposes whether the root manifest declares an MSRV, so GitHub skips the -matrix before setup when no minimum version is declared. +parallel keeps the extra test pass from extending `pr-test` serially. GitHub always +creates the matrix; the generated recipe exits successfully at recipe level when no +root MSRV is declared. The pr-* jobs gate on the impact jobs *succeeding*: their `needs: [impact-linux, impact-windows]` uses GitHub's default behavior, so if an impact job fails the pr-* @@ -458,7 +463,7 @@ caller anvil-scheduled.yml ├─ scheduled-runtime-analysis (Linux/Windows × x64/ARM64) ├─ scheduled-exhaustive (Linux/Windows x64) └─ publish-failure - needs: all four scheduled groups + needs: all scheduled groups condition: at least one failure and publication not disabled job override: issues:write only ``` @@ -734,10 +739,9 @@ result, and log link without check-suite attribution. ### `anvil-setup` -`anvil-setup` is a composite action that installs `just` -(`cargo install just --locked`), provisions the selected stable compiler -through the setup dependency graph while capturing its cache-key version, and -then invokes the requested catalog setup recipe. Its +`anvil-setup` is a composite action that restores Cargo home, bootstraps +cargo-binstall and Just, then invokes the requested catalog setup recipe, whose +prerequisites provision the selected compiler and tools. Its `group` input controls which recipes run: - empty (default): runs `just anvil-setup binstall` -- the full catalog. Use @@ -766,8 +770,8 @@ Its optional `free-disk-space` input defaults to `false`. When enabled on a GitHub-hosted runner, it removes pre-installed toolchains that anvil's Rust checks do not use: Android, Haskell/GHC, Swift and browser drivers on Linux; Android and Haskell/GHC on Windows. This reclaims approximately 18 GB on Linux and 17 GB on -Windows. It is a no-op on macOS and self-hosted runners. The generated reusable -workflows explicitly enable this input only for `pr-test` and `scheduled-test`, +Windows. It is a no-op on macOS and self-hosted runners. The generated reusable +workflows explicitly enable this input only for `pr-test`, `pr-msrv`, and `scheduled-test`, mirroring the testing-job integration in [microsoft/oxidizer#583](https://github.com/microsoft/oxidizer/pull/583). Other groups retain the action's disabled default. @@ -842,28 +846,26 @@ The check → bucket mapping is in ## 7. Rust toolchain +Selection follows the shared local contract: + +1. inherit an existing caller-provided `RUSTUP_TOOLCHAIN` unchanged; +2. when either root `rust-toolchain` spelling exists, pass no explicit selector + and let rustup process toolchain files natively; +3. otherwise pass the root manifest MSRV explicitly as `+`. + +There is no runner-default fallback. With no environment override, root +toolchain file, or root MSRV, setup and checks fail. Anvil never parses a +toolchain file or replays options from a file suppressed by +`RUSTUP_TOOLCHAIN`. Because file selection remains native, rustup applies its +normal lookup from each Cargo or Rust command's working directory. + The setup action restores Cargo home, bootstraps cargo-binstall and Just, and -then invokes the selected catalog setup recipe. That setup graph reaches -`anvil-toolchain-stable-install` before stable Cargo or Rust is used, so it -provisions a missing root MSRV or lets rustup process a repository toolchain -file. The workflow never invokes `_anvil-resolve-stable` itself and does not -publish a resolved `RUSTUP_TOOLCHAIN` through `GITHUB_ENV`. -Each stable recipe lazily selects the same optional argument and invokes Cargo -or Rust directly in its current PowerShell process. The selector is an inline -PowerShell array expression rather than an expression-time shell subprocess, so -it has no host-OS branch and does not alter the adopter's configured shell. -Selecting toolchain files remain unoverridden so rustup processes them natively. -All matrix legs therefore use the same repository selection instead of the -different stable versions that runner images may carry, without changing -unrelated recipe environments. - -Selection follows the shared local contract: an existing `RUSTUP_TOOLCHAIN`, then a -root toolchain file with `channel` or `path`, then the root MSRV. There is no -runner-default fallback. GH-hosted runners provide the rustup proxy used to install a -selected public channel. A repository that supplies a path toolchain must provision -that path before the Anvil action runs. A caller-provided environment override -takes precedence over repository files. Anvil does not parse a repository -toolchain file or reapply options from a file suppressed by that override. +then invokes the selected catalog setup recipe. Setup ensures the selected +compiler is available before stable Cargo or Rust runs. GH-hosted runners +provide the rustup proxy used to install a missing public MSRV or process a +repository toolchain file. A caller-provided or path toolchain remains the +runner owner's provisioning responsibility. Setup does not publish a rewritten +`RUSTUP_TOOLCHAIN` through `GITHUB_ENV`. On self-hosted runners or pre-baked images without rustup, the user provisions the selected toolchain before the `uses:` of the reusable workflow: @@ -881,12 +883,10 @@ users that need additional preparation take ownership of the generated reusable implementation workflow and add the preparation there. The generated composite actions remain implementation details rather than a separately supported API. -Setup recipes that need stable Rust depend on -`anvil-toolchain-stable-install`; shared Cargo-tool installation and -default-component installation carry the same prerequisite. Just deduplicates -it across each group/full setup invocation. -`anvil-tool-rustc-validate-prereqs` remains read-only and validates the -installed `rustc` and workspace MSRV contract at check time. +Prerequisite validation remains read-only. For the root-MSRV fallback, it +requires rustup and verifies the exact MSRV toolchain is already installed +before running Cargo metadata, preventing validation from auto-installing a +compiler. ## 8. Caching diff --git a/crates/cargo-anvil/docs/design/local.md b/crates/cargo-anvil/docs/design/local.md index 42e756f80..f2aecb668 100644 --- a/crates/cargo-anvil/docs/design/local.md +++ b/crates/cargo-anvil/docs/design/local.md @@ -45,7 +45,7 @@ repo/ │ │ `*-validate-prereqs` (anvil-pr-fast, anvil-pr-test, │ │ anvil-pr-msrv, anvil-pr-runtime-analysis, anvil-pr-mutants, │ │ anvil-scheduled-test, …). `anvil-pr-slow` is a -│ │ convenience umbrella over the four pr-slow sub-groups. +│ │ convenience umbrella over the pr-slow groups. │ ├── container.just containerized execution (`anvil-container`). See containers.md. │ ├── tiers.just tier aggregators (anvil-pr, anvil-scheduled, anvil-full). │ ├── tools.just tool/component/toolchain install + validate-prereqs recipes, @@ -132,10 +132,10 @@ namespaces are kept disjoint by naming choice: no check is named `- any tier × group combination (e.g. the coverage-instrumented test check is named `llvm-cov`, not `test`, so that group names like `anvil-pr-test` unambiguously refer to a group recipe). -The `pr-slow` work is split into four independent cloud-workflow-visible sub-groups +The `pr-slow` work is split into independent cloud-workflow-visible groups (`pr-test`, `pr-msrv`, `pr-runtime-analysis`, `pr-mutants`) so they run as parallel cloud-workflow jobs/stages. A convenience umbrella `anvil-pr-slow` recipe is also provided for local -use; it invokes the four sub-recipes sequentially. `pr-mutants` (mutants) is +use; it invokes those groups sequentially. `pr-mutants` (mutants) is diff-scoped against the PR base; `scheduled-exhaustive` runs the full-workspace mutants recipe: @@ -394,58 +394,33 @@ ambient stable toolchain. The selection order is: 3. The root package or `[workspace.package]` `rust-version`, treated as the repository MSRV. -There is no runner-default fallback. A repository with neither a selecting toolchain +There is no runner-default fallback. A repository with neither a root toolchain file nor a root MSRV fails with an actionable setup error rather than inheriting a compiler that can change with the machine image. -This is a breaking migration for adopters that previously relied on the runner's -ambient stable compiler. Before regenerating, a repository with one compatibility -floor should declare root `[workspace.package].rust-version` (or package +Repositories with one compatibility floor should declare root +`[workspace.package].rust-version` (or package `rust-version`) and have every workspace member inherit it or declare an equal -or lower minimum. A repository with missing package MSRVs, or a member minimum -newer than the root, must instead correct the root floor or add a root toolchain -file to select the single compiler used by catalog checks. Callers may set -`RUSTUP_TOOLCHAIN` to an already provisioned toolchain rather than adding a -repository compiler pin. Setting `ANVIL_MSRV_TOOLCHAIN` without that stable -override (and without a selecting repository toolchain file) is rejected as -incomplete configuration. An empty root toolchain file is invalid and produces -an actionable error. - -`versions.just` holds the selector as the lazy, non-exported -`_anvil_stable_toolchain_args` value. Its value is a multiline PowerShell array -expression, not a command for Just's expression-time `shell()` function. Just -interpolates the expression into an existing PowerShell recipe. A -caller-provided `RUSTUP_TOOLCHAIN` or repository-root toolchain file evaluates -to an empty array because rustup already consumes those inputs natively. Only -the synthesized root MSRV fallback evaluates to one `+toolchain` string. -`RUSTUP_TOOLCHAIN` is an input to selection, never a globally exported output. -The expression embeds an escaped PowerShell literal produced from -`justfile_directory()`, matching the setup resolver's repository root. -Recipe-local `Set-Location` calls and Just's `--working-directory` option -therefore cannot redirect selection to a nested manifest. - -Each stable recipe executes Cargo or Rust directly in its current PowerShell -recipe process, for example -`& cargo {{_anvil_stable_toolchain_args}} clippy ...`. Repository toolchain-file -provisioning runs `rustc` from the repository root, so rustup natively owns the -file's channel, path, profile, components, and targets. Default Clippy and -rustfmt setup likewise lets native selectors choose the component target, -adding `--toolchain` only for the synthesized MSRV fallback. Nightly and -MSRV-specific checks keep their own explicit `+toolchain` arguments. No nested -Just wrapper sits on the main Cargo/Rust command path. Selection has no -OS-specific branch or host-shell quoting and does not change the adopter's -configured shell. Unrelated recipes and raw commands retain the caller's normal -environment. - -The public setup primitive `anvil-toolchain-stable-install` owns stable -provisioning. Rustup processes repository toolchain files directly; the private -`_anvil-resolve-stable` implementation handles MSRV installation and an -optional separately provisioned MSRV selector. Leaf setup recipes reach the primitive directly or through -the shared Cargo-tool/default-component installers; group, tier, and -`anvil-setup` recipes inherit and deduplicate that dependency. Workflows and -container construction invoke setup operations rather than orchestrating -`_anvil-resolve-stable` actions. The helper does not wrap a Cargo/Rust command -that performs a check, and no standalone script is involved. +or lower minimum. Repositories with missing package MSRVs, or a member minimum +newer than the root, correct the root floor or add a root toolchain file to +select one compiler for catalog checks. Callers can instead set +`RUSTUP_TOOLCHAIN` to an already provisioned toolchain. + +A caller-provided `RUSTUP_TOOLCHAIN` and the presence of either root +toolchain-file spelling both produce no explicit `+toolchain` argument. The +environment value is inherited unchanged. In the file case, rustup owns all +parsing and applies its native lookup from each Cargo or Rust command's actual +working directory; Anvil does not parse the root file, replay options from it, +or promise isolation from nested toolchain files. Only the root MSRV fallback +produces an explicit `+` argument. + +Setup ensures that the selected stable compiler is available before stable +Cargo or Rust use. Repository toolchain files are processed by rustup; a +missing root-MSRV toolchain is installed explicitly. Prerequisite validation is +read-only: before running workspace metadata under the root MSRV, it requires +rustup and verifies that exact toolchain is already installed. A missing +toolchain fails with the stable-setup recipe as the corrective action instead +of allowing Cargo to auto-install it. When selection falls back to the root MSRV, Anvil reads the root manifest just far enough to bootstrap that compiler, then prerequisite validation reads @@ -459,20 +434,21 @@ correct the declarations or choose a single catalog toolchain explicitly with a selecting toolchain file. Anvil does not build a per-package toolchain matrix for this uncommon case. +Cargo-installed tools and stable analyzers use this same repository-selected +compiler. Anvil does not provision a separate tooling compiler; checks that +require nightly continue to use their catalog-pinned nightly. + When the root manifest declares an MSRV, `anvil-msrv-test` runs affected-package -unit and integration tests under that compiler with all features and with default -features. A selecting toolchain file does not suppress this minimum-version run. -`ANVIL_MSRV_TOOLCHAIN` optionally maps the declared MSRV to a separately -provisioned toolchain. Without a root MSRV the recipe is a no-op. +`cargo test --all-targets` in all-features and default-features configurations +under that compiler. This includes library and binary unit tests, integration +tests, examples, and benches as test targets. It does not add a +`--no-default-features` pass. A root toolchain file does not suppress this +minimum-version run. Without a root MSRV the recipe is a no-op. `anvil-tool-rustc-validate-prereqs` verifies that `rustc` is available and -enforces the workspace MSRV compatibility rule. Stable setup separately -provisions the selected compiler. Per-check toolchain requirements (for -example, miri, careful, and udeps -need nightly) remain enforced by the matching -`anvil-toolchain--validate-prereqs` recipe. Explicit `cargo +toolchain` -arguments retain rustup's higher precedence, so these checks continue to use the -catalog's dated nightly pins. +enforces the workspace MSRV compatibility rule. Per-check toolchain +requirements (for example, miri, careful, and udeps need nightly) remain +enforced by their matching prerequisite validation. ### 3.6 Nightly pinning @@ -638,7 +614,7 @@ dependency. This is exactly how the **scheduled** and **full** tiers stay full-w `ANVIL_IMPACT=off` before invoking the private `_anvil-` recipe, so the whole dependency tree runs unscoped. The export lives in the wrapper because a dependency-only tier recipe cannot set env for its own dependencies — they run before its -body. The four scheduled *groups* (`anvil-scheduled-test`, …) wrap the same way, so a +body. The scheduled *groups* (`anvil-scheduled-test`, …) wrap the same way, so a scheduled group invoked directly is full-workspace too. `ANVIL_IMPACT` is a strict tri-state — `off`, `consume`, or unset. Any other value makes diff --git a/crates/cargo-anvil/docs/implementation.md b/crates/cargo-anvil/docs/implementation.md index 005554372..cc2145400 100644 --- a/crates/cargo-anvil/docs/implementation.md +++ b/crates/cargo-anvil/docs/implementation.md @@ -44,6 +44,59 @@ The canonical recipe owns this mapping, baseline availability checks, and per-package execution. Focused contract tests cover every publication form, while generated copies and snapshots detect drift from the template. +## Stable toolchain selection and setup + +The stable selector has two canonical implementation sites because it must run +before Cargo can parse the root manifest. `versions.just` owns the lazy +per-command argument expression; `tools.just` owns provisioning, workspace-MSRV +validation, and the dedicated MSRV-test selection. Their small line-oriented +root-manifest scanners intentionally duplicate the same accepted syntax and +`workspace.package`-before-`package` precedence. Changes to one scanner must +update the other and their focused resolver tests in the same patch. + +Both implementations enforce the same ordinary-check decision table: + +1. a nonempty caller `RUSTUP_TOOLCHAIN` emits no explicit argument; +2. the presence of either root toolchain-file spelling emits no explicit + argument; +3. otherwise the root MSRV emits `+`; +4. absence of every source fails. + +The empty argument in the first two cases is load-bearing. It preserves +rustup's native environment and working-directory-sensitive toolchain-file +behavior, including profiles, components, targets, and path channels, without +Anvil parsing or replaying suppressed options. + +`_anvil-resolve-stable` owns the setup actions. The optional +`ANVIL_MSRV_TOOLCHAIN` maps only the dedicated MSRV run and is rejected as an +unpaired stable configuration when no ordinary stable selector exists. +Workspace-MSRV validation is read-only: it confirms rustup is present and the +exact root-MSRV toolchain is installed before invoking metadata, so Cargo +cannot auto-install a compiler during validation. Installation uses the same +anchored toolchain-list match and emits a dedicated rustup bootstrap diagnostic +when the executable is absent. + +Setup dependencies, rather than the cloud templates, route provisioning. +Cargo-tool installers, default-component installers, and stable-only setup +leaves depend on `anvil-toolchain-stable-install`; group and tier fan-out lets +Just deduplicate that prerequisite. GitHub and ADO setup restore Cargo home, +bootstrap Just, and invoke the selected catalog setup recipe. Neither backend +adds a duplicate standalone stable-provisioning step. + +Cloud Cargo-home keys include `.cargo/config.toml`, both repository +toolchain-file spellings, and `versions.just`. They deliberately exclude +`Cargo.toml`, `Cargo.lock`, and the resolved compiler version because cached +Cargo-installed tools do not depend on routine workspace dependency changes, +while toolchain files and catalog pins define the cache-pruning boundary. +Rustup toolchains live outside Cargo home. + +Canonical recipes and workflow fragments live under `templates/`. The artifact +registry embeds those files, renderer tests pin conditional substitutions such +as ADO's `pr-fast`-only title lookup, and snapshot tests pin complete emitted +backends. After a canonical change, run cargo-anvil against this repository to +refresh owned mirrors and `.anvil.lock`; regenerate crate README content from +`src/lib.rs`, never by editing `README.md`. + ## Impact scoping and tier routing Impact scoping lets a clean PR run each check against only the cargo packages its committed diff --git a/crates/cargo-anvil/src/anvil/artifacts/ado.rs b/crates/cargo-anvil/src/anvil/artifacts/ado.rs index 8a29610ee..7d9b7247d 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/ado.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/ado.rs @@ -62,22 +62,36 @@ const GROUPS: &[&str] = &[ /// the group name at emit time. const GROUP_STEP_TEMPLATE: &str = include_str!("../../../templates/ado/steps/group.yml"); +/// Optional ADO title lookup inserted only into the `pr-fast` group. +const PR_TITLE_STEP_TEMPLATE: &str = include_str!("../../../templates/ado/steps/pr-title-step.yml"); + +/// Optional `PR_TITLE` environment entry inserted only into `pr-fast`. +const PR_TITLE_ENV_TEMPLATE: &str = include_str!("../../../templates/ado/steps/pr-title-env.yml"); + /// Placeholder token the per-group template uses for the group name. const GROUP_PLACEHOLDER: &str = "__GROUP__"; /// Placeholder token the per-group template uses for the impact-mode selection. const IMPACT_MODE_PLACEHOLDER: &str = "__IMPACT_MODE__"; +/// Placeholder token for the optional PR-title lookup step. +const PR_TITLE_STEP_PLACEHOLDER: &str = "__PR_TITLE_STEP__"; + +/// Placeholder token for the optional `PR_TITLE` environment entry. +const PR_TITLE_ENV_PLACEHOLDER: &str = "__PR_TITLE_ENV__"; + /// Render the step template for one group. #[must_use] fn render_group_step(group: &str) -> String { - // Substitute the group name first, then replace the single impact-mode - // token in place -- reusing the already-allocated buffer instead of - // allocating a second full-template String for the second substitution. let mut rendered = GROUP_STEP_TEMPLATE.replace(GROUP_PLACEHOLDER, group); - if let Some(pos) = rendered.find(IMPACT_MODE_PLACEHOLDER) { - rendered.replace_range(pos..pos + IMPACT_MODE_PLACEHOLDER.len(), impact_mode(group)); - } + rendered = rendered.replace(IMPACT_MODE_PLACEHOLDER, impact_mode(group)); + let (title_step, title_env) = if group == "pr-fast" { + (PR_TITLE_STEP_TEMPLATE.trim_end(), PR_TITLE_ENV_TEMPLATE.trim_end()) + } else { + ("", "") + }; + rendered = rendered.replace(PR_TITLE_STEP_PLACEHOLDER, title_step); + rendered = rendered.replace(PR_TITLE_ENV_PLACEHOLDER, title_env); rendered } @@ -304,7 +318,7 @@ mod tests { } #[test] - fn render_group_step_shares_impact_via_cache_not_env() { + fn render_pr_fast_group_shares_impact_and_resolves_title() { let body = render_group_step("pr-fast"); assert!(body.contains("just anvil-pr-fast")); // The impact set is shared as a downloaded artifact, so the group step @@ -325,13 +339,22 @@ mod tests { !body.contains("[ -f target/anvil/impact/impact.state ]"), "PR group must not gate its mode on a runtime marker-file probe" ); - // PR_TITLE is resolved from the REST API (ADO has no PR-title - // predefined variable) and threaded via the PR_TITLE pipeline var. + // Only pr-fast needs the PR title, which ADO does not expose as a + // predefined variable. assert!(body.contains("PR_TITLE: $(PR_TITLE)")); assert!(body.contains("setvariable variable=PR_TITLE")); + assert!(body.contains("Invoke-RestMethod")); assert!(!body.contains("PR_TITLE: $(System.PullRequest.Title)")); } + #[test] + fn render_pr_msrv_group_has_no_title_api_or_token_dependency() { + let body = render_group_step("pr-msrv"); + for needle in ["PR_TITLE", "Invoke-RestMethod", "SYSTEM_ACCESSTOKEN", "System.AccessToken"] { + assert!(!body.contains(needle), "pr-msrv must not contain '{needle}'"); + } + } + #[test] fn scheduled_group_step_forces_impact_off_unconditionally() { // The scheduled tier always validates the full workspace. Its impact diff --git a/crates/cargo-anvil/src/anvil/artifacts/github.rs b/crates/cargo-anvil/src/anvil/artifacts/github.rs index 04c855840..6eaa33e45 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/github.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/github.rs @@ -405,6 +405,8 @@ export -f just ); } assert!(PR_IMPL_WORKFLOW.contains("needs: [impact-linux, impact-windows]")); + // The matrix is intentionally always present; the generated MSRV + // recipe owns the successful no-MSRV no-op. assert!(!PR_IMPL_WORKFLOW.contains("msrv_test_required")); assert!(PR_IMPL_WORKFLOW.contains("Check Group: MSRV Tests (${{ matrix.os }})")); assert!(PR_IMPL_WORKFLOW.contains("os: [linux, windows, linux-arm, windows-arm]")); diff --git a/crates/cargo-anvil/src/anvil/artifacts/justfile.rs b/crates/cargo-anvil/src/anvil/artifacts/justfile.rs index 56313f08e..7d5a4e7af 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/justfile.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/justfile.rs @@ -525,14 +525,15 @@ mod tests { "_anvil-base-ref", "git rev-parse --verify \"$base^{commit}\"", "git cat-file -e $baselineManifest", - "cargo {{_anvil_stable_toolchain_args}} semver-checks --package $p --baseline-rev $base", + "$stableArgs = {{_anvil_stable_toolchain_args}}", + "cargo @stableArgs semver-checks --package $p --baseline-rev $base", ] { assert!(body.contains(needle), "semver check template missing '{needle}'"); } } #[test] - fn groups_just_template_includes_all_groups_and_pr_slow_sub_recipes() { + fn groups_just_template_includes_all_groups_and_pr_slow_groups() { let groups = all_group_bodies(); for needle in [ "anvil-pr-fast:", @@ -781,6 +782,7 @@ mod tests { #[test] 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) { let invokes_cargo = line.starts_with("cargo ") || line.starts_with("& cargo ") @@ -795,7 +797,8 @@ mod tests { assert!( toolchain.starts_with("'+") || toolchain.starts_with("\"+") - || toolchain.starts_with("{{_anvil_stable_toolchain_args}}"), + || toolchain.starts_with("{{_anvil_stable_toolchain_args}}") + || (caches_stable_args && toolchain.starts_with("@stableArgs")), "{path} invokes Cargo without an explicit toolchain selection: {line}" ); } @@ -803,8 +806,70 @@ mod tests { } } + #[test] + fn mod_just_imports_siblings_and_defines_alias() { + for needle in [ + "import 'helpers.just'", + "import 'impact.just'", + "import 'checks/fmt.just'", + "import 'checks/miri.just'", + // Optional: a fork can drop the container backend with + // `without_artifact` and the tree still parses. + "import? 'container.just'", + "import 'groups/pr-fast.just'", + "import 'groups/scheduled-exhaustive.just'", + "import 'tiers.just'", + "import 'tools.just'", + "import 'versions.just'", + "alias anvil := anvil-pr", + ] { + assert!(MOD_JUST.contains(needle), "mod.just missing '{needle}'"); + } + // Every split recipe file must be imported by mod.just. + for (path, _) in CHECK_FILES.iter().chain(GROUP_FILES.iter()) { + let import = format!( + "import '{}'", + path.strip_prefix("justfiles/anvil/").expect("path under justfiles/anvil/") + ); + assert!(MOD_JUST.contains(&import), "mod.just missing '{import}'"); + } + } + + #[test] + fn versions_just_defines_both_nightly_pins() { + assert!(VERSIONS_JUST.contains("rust_nightly :="), "versions.just missing rust_nightly"); + assert!( + VERSIONS_JUST.contains("rust_nightly_external_types :="), + "versions.just missing rust_nightly_external_types" + ); + } + + #[test] + fn checks_just_has_no_floating_nightly_invocations() { + let checks = all_check_bodies(); + for line in checks.lines() { + let stripped = line.split('#').next().unwrap_or(""); + assert!( + !stripped.contains("+nightly "), + "checks tree has a floating '+nightly' invocation: {line}" + ); + assert!( + !stripped.contains("'+nightly'"), + "checks tree has a floating '+nightly' invocation: {line}" + ); + } + } + + #[test] + fn justfile_imports_body_is_a_single_import_line() { + let body = JUSTFILE_IMPORTS_BODY.trim(); + assert_eq!(body, "import 'justfiles/anvil/mod.just'"); + } + #[cfg(not(miri))] mod stable_toolchain_resolver_tests { + #[cfg(unix)] + use std::os::unix::fs::PermissionsExt; use std::path::Path; use std::process::{Command, Output}; use std::{env, fs}; @@ -813,6 +878,15 @@ mod tests { use super::{TOOLS_JUST, VERSIONS_JUST}; + #[derive(Clone, Copy)] + enum ResolverOperation { + StableArgs, + InstallIfMissing, + ValidateWorkspaceMsrv, + MsrvToolchain, + InstallMsrvIfNeeded, + } + fn fixture(cargo_toml: &str) -> TempDir { let temp = Builder::new() .prefix("anvil repo's [copy] (fork) ") @@ -865,26 +939,30 @@ mod tests { command } - fn run(root: &Path, args: &[&str], rustup_toolchain: Option<&str>) -> Output { + fn command_for_operation(root: &Path, operation: ResolverOperation) -> Command { let mut command = command(root); - match args { - [] => { + match operation { + ResolverOperation::StableArgs => { command.arg("_anvil-test-stable-args"); } - ["-InstallIfMissing"] => { + ResolverOperation::InstallIfMissing => { command.arg("anvil-toolchain-stable-install"); } - ["-ValidateWorkspaceMsrv"] => { + ResolverOperation::ValidateWorkspaceMsrv => { command.args(["_anvil-resolve-stable", "validate-workspace-msrv"]); } - ["-MsrvToolchain"] => { + ResolverOperation::MsrvToolchain => { command.args(["_anvil-resolve-stable", "msrv"]); } - ["-InstallMsrvIfNeeded"] => { + ResolverOperation::InstallMsrvIfNeeded => { command.args(["_anvil-resolve-stable", "install-msrv"]); } - other => panic!("unsupported resolver arguments in test: {other:?}"), } + command + } + + fn run(root: &Path, operation: ResolverOperation, rustup_toolchain: Option<&str>) -> Output { + let mut command = command_for_operation(root, operation); match rustup_toolchain { Some(value) => { command.env("RUSTUP_TOOLCHAIN", value); @@ -899,7 +977,7 @@ mod tests { } fn resolved(root: &Path, rustup_toolchain: Option<&str>) -> String { - let output = run(root, &[], rustup_toolchain); + let output = run(root, ResolverOperation::StableArgs, rustup_toolchain); assert!( output.status.success(), "resolver failed: {}", @@ -922,6 +1000,16 @@ mod tests { .join(" ") } + fn path_without_rustup() -> std::ffi::OsString { + let path = env::var_os("PATH").unwrap_or_default(); + let paths = env::split_paths(&path).filter(|directory| { + !["rustup", "rustup.exe", "rustup.cmd", "rustup.ps1"] + .iter() + .any(|name| directory.join(name).is_file()) + }); + env::join_paths(paths).expect("PATH without rustup must be valid") + } + #[test] fn honors_environment_files_and_msrv_in_precedence_order() { if !tools_available() { @@ -951,20 +1039,18 @@ mod tests { } #[cfg(unix)] { - use std::os::unix::fs::PermissionsExt; - let cargo = shim.path().join("cargo"); fs::write(&cargo, "#!/bin/sh\nprintf '%s\\n' \"$*\"\nexit 0\n").expect("Cargo shim must be writable"); fs::set_permissions(&cargo, fs::Permissions::from_mode(0o755)).expect("Cargo shim must be executable"); } let mut paths = vec![shim.path().to_path_buf()]; paths.extend(env::split_paths(&env::var_os("PATH").unwrap_or_default())); - let path = env::join_paths(paths).expect("shim PATH must be valid"); + let paths = env::join_paths(paths).expect("shim PATH must be valid"); let output = command(temp.path()) .arg("_anvil-test-stable-command") .env("RUSTUP_TOOLCHAIN", "team's-toolchain") - .env("PATH", &path) + .env("PATH", &paths) .output() .expect("direct stable Cargo command must run"); assert!( @@ -981,7 +1067,7 @@ mod tests { .expect("toolchain fixture must be writable"); let output = command(temp.path()) .arg("_anvil-test-stable-command") - .env("PATH", &path) + .env("PATH", &paths) .output() .expect("repository-selected Cargo command must run"); assert!( @@ -1002,7 +1088,7 @@ mod tests { .args(["--working-directory"]) .arg(&nested) .arg("_anvil-test-stable-command") - .env("PATH", &path) + .env("PATH", &paths) .output() .expect("stable Cargo command with an explicit working directory must run"); assert!( @@ -1017,7 +1103,7 @@ mod tests { let output = command(temp.path()) .arg("_anvil-test-stable-command-after-chdir") - .env("PATH", path) + .env("PATH", paths) .output() .expect("stable Cargo command after Set-Location must run"); assert!( @@ -1055,8 +1141,6 @@ mod tests { } #[cfg(unix)] { - use std::os::unix::fs::PermissionsExt; - for (name, log_variable, status) in [("cargo", "ANVIL_CARGO_LOG", 1), ("rustup", "ANVIL_RUSTUP_LOG", 0)] { let executable = shim.path().join(name); fs::write( @@ -1069,14 +1153,14 @@ mod tests { } let mut paths = vec![shim.path().to_path_buf()]; paths.extend(env::split_paths(&env::var_os("PATH").unwrap_or_default())); - let path = env::join_paths(paths).expect("shim PATH must be valid"); + let paths = env::join_paths(paths).expect("shim PATH must be valid"); let run_install = |rustup_toolchain: Option<&str>| { let mut command = command(temp.path()); command .args(["_install-component", "default", "clippy"]) .env("ANVIL_CARGO_LOG", &cargo_log) .env("ANVIL_RUSTUP_LOG", &rustup_log) - .env("PATH", &path); + .env("PATH", &paths); if let Some(value) = rustup_toolchain { command.env("RUSTUP_TOOLCHAIN", value); } @@ -1142,7 +1226,7 @@ mod tests { return; } let missing = fixture("[workspace]\nresolver = \"2\"\n"); - let output = run(missing.path(), &[], None); + let output = run(missing.path(), ResolverOperation::StableArgs, None); assert!(!output.status.success()); let diagnostic = normalized_diagnostic(&output); assert!( @@ -1183,23 +1267,46 @@ mod tests { ), ) .expect("Cargo shim must be writable"); + fs::write( + shim.path().join("rustup.ps1"), + concat!( + "if (($args -contains 'toolchain') -and ($args -contains 'list')) {\n", + " $env:ANVIL_TEST_INSTALLED_TOOLCHAINS -split ';' | Where-Object { $_ }\n", + "}\n", + "exit 0\n", + ), + ) + .expect("rustup shim must be writable"); let mut paths = vec![shim.path().to_path_buf()]; paths.extend(env::split_paths(&env::var_os("PATH").unwrap_or_default())); - let path = env::join_paths(paths).expect("shim PATH must be valid"); - let run_validation = |b_msrv: &str, rustup_toolchain: Option<&str>| { + let paths = env::join_paths(paths).expect("shim PATH must be valid"); + let run_validation = |b_msrv: &str, rustup_toolchain: Option<&str>, installed: &str| { let mut command = command(workspace.path()); command .args(["_anvil-resolve-stable", "validate-workspace-msrv"]) .env("ANVIL_TEST_B_MSRV", b_msrv) + .env("ANVIL_TEST_INSTALLED_TOOLCHAINS", installed) .env("ANVIL_TEST_LOG", &log) - .env("PATH", &path); + .env("PATH", &paths); if let Some(value) = rustup_toolchain { command.env("RUSTUP_TOOLCHAIN", value); } command.output().expect("workspace MSRV validation must run") }; - let output = run_validation("1.93", None); + let output = run_validation("1.92", None, "stable-x86_64-pc-windows-msvc"); + assert!(!output.status.success(), "validation must not auto-install a missing root MSRV"); + let diagnostic = normalized_diagnostic(&output); + assert!( + diagnostic.contains("root MSRV toolchain '1.92' is not installed") && diagnostic.contains("anvil-toolchain-stable-install"), + "unexpected missing-toolchain diagnostic: {diagnostic}" + ); + assert!( + fs::read_to_string(&log).unwrap_or_default().is_empty(), + "Cargo metadata must not run before the root MSRV is known to be installed" + ); + + let output = run_validation("1.93", None, "1.92-x86_64-pc-windows-msvc"); assert!(!output.status.success()); let diagnostic = normalized_diagnostic(&output); assert!( @@ -1207,7 +1314,7 @@ mod tests { "unexpected resolver diagnostic: {diagnostic}" ); - let output = run_validation("1.92.0", None); + let output = run_validation("1.92.0", None, "1.92-x86_64-pc-windows-msvc"); assert!( output.status.success(), "member MSRVs at or below the root must be compatible: {}", @@ -1219,10 +1326,30 @@ mod tests { "workspace compatibility metadata must use the root MSRV toolchain:\n{calls}" ); - let output = run_validation("1.93", Some("explicit-toolchain")); + let output = run_validation("1.93", Some("explicit-toolchain"), ""); assert!(output.status.success(), "explicit override must permit differing MSRVs"); } + #[test] + fn diagnoses_missing_rustup_before_installing_a_toolchain() { + if !tools_available() { + return; + } + let temp = fixture("[workspace.package]\nrust-version = \"1.93\"\n"); + let output = command_for_operation(temp.path(), ResolverOperation::InstallIfMissing) + .env("PATH", path_without_rustup()) + .output() + .expect("stable setup must run far enough to diagnose missing rustup"); + assert!(!output.status.success(), "stable setup without rustup must fail"); + let diagnostic = normalized_diagnostic(&output); + assert!( + diagnostic.contains("rustup not found") + && diagnostic.contains("https://rustup.rs") + && diagnostic.contains("ensure it is on PATH"), + "unexpected missing-rustup diagnostic: {diagnostic}" + ); + } + #[test] fn rejects_an_unpaired_internal_msrv_mapping_for_stable_selection() { if !tools_available() { @@ -1347,7 +1474,12 @@ mod tests { let no_msrv = fixture("[workspace]\nresolver = \"2\"\n"); fs::write(no_msrv.path().join("rust-toolchain.toml"), "[toolchain]\nchannel = \"1.94\"\n") .expect("toolchain fixture must be writable"); - let output = run(no_msrv.path(), &["-MsrvToolchain"], None); + let output = run(no_msrv.path(), ResolverOperation::InstallMsrvIfNeeded, None); + assert!( + output.status.success(), + "a repository without a declared MSRV must not provision an MSRV" + ); + let output = run(no_msrv.path(), ResolverOperation::MsrvToolchain, None); assert!( output.status.success(), "a repository without a declared MSRV must skip the MSRV test" @@ -1361,8 +1493,13 @@ mod tests { let temp = fixture("[workspace.package]\nrust-version = \"1.93\"\n"); let root = temp.path(); + let output = run(root, ResolverOperation::ValidateWorkspaceMsrv, Some("explicit-toolchain")); + assert!( + output.status.success(), + "an explicit stable override must bypass root-MSRV workspace validation" + ); let resolve_msrv = |root: &Path| { - let output = run(root, &["-MsrvToolchain"], None); + let output = run(root, ResolverOperation::MsrvToolchain, None); assert!( output.status.success(), "MSRV resolution failed: {}", @@ -1400,64 +1537,4 @@ mod tests { assert_eq!(resolve_msrv(root), "1.93"); } } - - #[test] - fn mod_just_imports_siblings_and_defines_alias() { - for needle in [ - "import 'helpers.just'", - "import 'impact.just'", - "import 'checks/fmt.just'", - "import 'checks/miri.just'", - // Optional: a fork can drop the container backend with - // `without_artifact` and the tree still parses. - "import? 'container.just'", - "import 'groups/pr-fast.just'", - "import 'groups/scheduled-exhaustive.just'", - "import 'tiers.just'", - "import 'tools.just'", - "import 'versions.just'", - "alias anvil := anvil-pr", - ] { - assert!(MOD_JUST.contains(needle), "mod.just missing '{needle}'"); - } - // Every split recipe file must be imported by mod.just. - for (path, _) in CHECK_FILES.iter().chain(GROUP_FILES.iter()) { - let import = format!( - "import '{}'", - path.strip_prefix("justfiles/anvil/").expect("path under justfiles/anvil/") - ); - assert!(MOD_JUST.contains(&import), "mod.just missing '{import}'"); - } - } - - #[test] - fn versions_just_defines_both_nightly_pins() { - assert!(VERSIONS_JUST.contains("rust_nightly :="), "versions.just missing rust_nightly"); - assert!( - VERSIONS_JUST.contains("rust_nightly_external_types :="), - "versions.just missing rust_nightly_external_types" - ); - } - - #[test] - fn checks_just_has_no_floating_nightly_invocations() { - let checks = all_check_bodies(); - for line in checks.lines() { - let stripped = line.split('#').next().unwrap_or(""); - assert!( - !stripped.contains("+nightly "), - "checks tree has a floating '+nightly' invocation: {line}" - ); - assert!( - !stripped.contains("'+nightly'"), - "checks tree has a floating '+nightly' invocation: {line}" - ); - } - } - - #[test] - fn justfile_imports_body_is_a_single_import_line() { - let body = JUSTFILE_IMPORTS_BODY.trim(); - assert_eq!(body, "import 'justfiles/anvil/mod.just'"); - } } diff --git a/crates/cargo-anvil/src/lib.rs b/crates/cargo-anvil/src/lib.rs index e2a58a4c0..31fd78811 100644 --- a/crates/cargo-anvil/src/lib.rs +++ b/crates/cargo-anvil/src/lib.rs @@ -78,6 +78,14 @@ //! `catalog:` checksum — a `sha256` over the whole compiled-in catalog — so //! two builds at the same version but different catalogs are distinguishable. //! +//! Before running ordinary checks, provide a deterministic Rust selection: +//! a caller-set `RUSTUP_TOOLCHAIN`, either root `rust-toolchain` file spelling, +//! or a root `[workspace.package].rust-version` / `[package].rust-version`. +//! Anvil passes no explicit selector for the environment or file cases so +//! rustup handles them natively; only the root MSRV becomes `+`. +//! Repositories with none of these sources fail instead of inheriting the +//! runner's ambient compiler. +//! //! ## Daily driver //! //! After the first run, your daily workflow is plain `just`: @@ -267,11 +275,11 @@ //! check to its tool's documentation, and note anything anvil-specific. //! //! **PR tier** (`anvil-pr`) — runs on every pull request, impact-scoped -//! both locally and in cloud workflows. `pr-fast` is one job, while the four -//! `pr-slow` sub-groups run as independent parallel jobs per OS leg: +//! both locally and in cloud workflows. `pr-fast` is one job, while the +//! `pr-slow` groups run as independent parallel jobs per OS leg: //! //!
JobSub-groupCheckNotes
UmbrellaGroupCheckNotes
pr-fastfmtpredefined configuration with nightly features
clippypredefined lints
pr-slowpr-testllvm-covdual feature-config; gated by cargo-coverage-gate
doc-testruns both feature configs
examplescompile-only
pr-msrvmsrv-testdual feature-config tests under the declared MSRV
pr-msrvmsrv-testdual feature-config, all-target tests under the declared MSRV
pr-runtime-analysismirilibtest, not nextest
carefulself-cleans on a toolchain bump
loomopt-in targets only
-//! +//! //! //! //! @@ -291,7 +299,7 @@ //! //! //! -//! +//! //! //! //! diff --git a/crates/cargo-anvil/templates/ado/steps/group.yml b/crates/cargo-anvil/templates/ado/steps/group.yml index 173e18946..335afcfc6 100644 --- a/crates/cargo-anvil/templates/ado/steps/group.yml +++ b/crates/cargo-anvil/templates/ado/steps/group.yml @@ -16,33 +16,7 @@ steps: - template: setup.yml parameters: group: __GROUP__ - # ADO has no PR-title predefined variable: `System.PullRequest.Title` - # does NOT exist, so `$(System.PullRequest.Title)` would expand to the - # literal unexpanded macro text (non-empty) and make anvil-pr-title - # validate that string. Resolve the real title from the REST API on PR - # builds and publish it as the PR_TITLE pipeline variable. Only - # anvil-pr-title (in pr-fast) consults it; other groups ignore it, but we - # resolve uniformly to keep group.yml the same across groups. Non-PR builds - # skip; a known PR whose metadata cannot be retrieved fails closed. - - pwsh: | - $ErrorActionPreference = 'Stop' - $prId = $env:SYSTEM_PULLREQUEST_PULLREQUESTID - if (-not $prId) { - Write-Host 'anvil: not a PR build; leaving PR_TITLE empty' - Write-Host '##vso[task.setvariable variable=PR_TITLE]' - exit 0 - } - $uri = "$($env:SYSTEM_COLLECTIONURI)$($env:SYSTEM_TEAMPROJECTID)/_apis/git/repositories/$($env:BUILD_REPOSITORY_ID)/pullRequests/${prId}?api-version=7.0" - try { - $resp = Invoke-RestMethod -Uri $uri -Headers @{ Authorization = "Bearer $($env:SYSTEM_ACCESSTOKEN)" } - Write-Host "##vso[task.setvariable variable=PR_TITLE]$($resp.title)" - } catch { - Write-Error "anvil: could not resolve PR title for PR $prId ($_). Ensure the job can access the repository OAuth token." - exit 1 - } - displayName: anvil-__GROUP__ (resolve PR title) - env: - SYSTEM_ACCESSTOKEN: $(System.AccessToken) +__PR_TITLE_STEP__ - bash: | set -euo pipefail # The impact mode is fixed by group class at emit time -- never probed from a @@ -54,8 +28,7 @@ steps: just anvil-__GROUP__ displayName: anvil-__GROUP__ env: - # Resolved by the preceding step; empty only outside PR builds. - PR_TITLE: $(PR_TITLE) +__PR_TITLE_ENV__ # Disable cargo incremental compilation in CI: the incremental dir is # not part of the anvil-setup cache (see setup.yml), so generating it is # pure cost with no cross-run benefit. Mirrors the GitHub impl workflows' diff --git a/crates/cargo-anvil/templates/ado/steps/pr-title-env.yml b/crates/cargo-anvil/templates/ado/steps/pr-title-env.yml new file mode 100644 index 000000000..007650084 --- /dev/null +++ b/crates/cargo-anvil/templates/ado/steps/pr-title-env.yml @@ -0,0 +1,2 @@ + # Resolved by the preceding pr-fast step; empty only outside PR builds. + PR_TITLE: $(PR_TITLE) diff --git a/crates/cargo-anvil/templates/ado/steps/pr-title-step.yml b/crates/cargo-anvil/templates/ado/steps/pr-title-step.yml new file mode 100644 index 000000000..e66e4c2f1 --- /dev/null +++ b/crates/cargo-anvil/templates/ado/steps/pr-title-step.yml @@ -0,0 +1,24 @@ + # ADO has no PR-title predefined variable: `System.PullRequest.Title` + # does NOT exist, so `$(System.PullRequest.Title)` would expand to literal + # macro text. Resolve the title only for pr-fast, which owns anvil-pr-title. + # Non-PR builds skip; a known PR whose metadata cannot be retrieved fails + # closed. + - pwsh: | + $ErrorActionPreference = 'Stop' + $prId = $env:SYSTEM_PULLREQUEST_PULLREQUESTID + if (-not $prId) { + Write-Host 'anvil: not a PR build; leaving PR_TITLE empty' + Write-Host '##vso[task.setvariable variable=PR_TITLE]' + exit 0 + } + $uri = "$($env:SYSTEM_COLLECTIONURI)$($env:SYSTEM_TEAMPROJECTID)/_apis/git/repositories/$($env:BUILD_REPOSITORY_ID)/pullRequests/${prId}?api-version=7.0" + try { + $resp = Invoke-RestMethod -Uri $uri -Headers @{ Authorization = "Bearer $env:SYSTEM_ACCESSTOKEN" } + Write-Host "##vso[task.setvariable variable=PR_TITLE]$($resp.title)" + } catch { + Write-Error "anvil: could not resolve PR title for PR $prId ($_). Ensure the job can access the repository OAuth token." + exit 1 + } + displayName: anvil-pr-fast (resolve PR title) + env: + SYSTEM_ACCESSTOKEN: $(System.AccessToken) diff --git a/crates/cargo-anvil/templates/github/setup-action.yml b/crates/cargo-anvil/templates/github/setup-action.yml index 4ad8d0b91..0c5c0077a 100644 --- a/crates/cargo-anvil/templates/github/setup-action.yml +++ b/crates/cargo-anvil/templates/github/setup-action.yml @@ -119,7 +119,7 @@ runs: fi # The catalog recipe `anvil-setup` (or `anvil--setup` - # when a group is specified via the `group` input) uses the Just + # when a group is specified via the `group` input) uses the Just executable # bootstrapped above, then handles everything else. The setup recipes are idempotent and # short-circuit on tools already installed at or above the pinned # version, so re-running on cache-hit runs is cheap. diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/msrv-test.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/msrv-test.just index 362e470c8..f95b38cff 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/msrv-test.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/msrv-test.just @@ -11,8 +11,13 @@ anvil-msrv-test: anvil-msrv-test-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include affected) + $impactExit = $LASTEXITCODE + if ($impactExit -ne 0) { exit $impactExit } if ($include -eq '--skip') { Write-Host 'anvil-msrv-test: no affected packages; skipping'; exit 0 } - $toolchain = (& "{{ just_executable() }}" _anvil-resolve-stable msrv | Out-String).Trim() + $toolchainOutput = & "{{ just_executable() }}" _anvil-resolve-stable msrv | Out-String + $resolverExit = $LASTEXITCODE + if ($resolverExit -ne 0) { exit $resolverExit } + $toolchain = $toolchainOutput.Trim() if ([string]::IsNullOrWhiteSpace($toolchain)) { Write-Host 'anvil-msrv-test: no root MSRV declared; skipping' exit 0 @@ -34,7 +39,10 @@ anvil-msrv-test-setup installer="install": [group("anvil-setup")] [script("pwsh", "-NoProfile")] anvil-msrv-test-validate-prereqs: - $toolchain = (& "{{ just_executable() }}" _anvil-resolve-stable msrv | Out-String).Trim() + $toolchainOutput = & "{{ just_executable() }}" _anvil-resolve-stable msrv | Out-String + $resolverExit = $LASTEXITCODE + if ($resolverExit -ne 0) { exit $resolverExit } + $toolchain = $toolchainOutput.Trim() if ([string]::IsNullOrWhiteSpace($toolchain)) { exit 0 } & cargo "+$toolchain" --version if ($LASTEXITCODE -ne 0) { diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/readme-check.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/readme-check.just index 7eed9bbb1..7ae42342d 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/readme-check.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/readme-check.just @@ -37,6 +37,7 @@ [script("pwsh", "-NoProfile")] _anvil-readme mode: anvil-readme-check-validate-prereqs $ErrorActionPreference = 'Stop' + $stableArgs = {{_anvil_stable_toolchain_args}} $action = '{{ mode }}' if ($action -notin @('generate', 'check')) { [Console]::Error.WriteLine("anvil-readme: invalid mode '$action'; expected 'generate' or 'check'") @@ -55,7 +56,7 @@ _anvil-readme mode: anvil-readme-check-validate-prereqs # Unscoped: a change to the workspace-level README template is a repo-level # input cargo-delta cannot map to a package, so this check always runs the # full eligible-crate set rather than an impact subset. - $metadataJson = & cargo {{_anvil_stable_toolchain_args}} metadata --no-deps --format-version 1 + $metadataJson = & cargo @stableArgs metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error "${prefix}: cargo metadata failed" exit $LASTEXITCODE @@ -101,7 +102,7 @@ _anvil-readme mode: anvil-readme-check-validate-prereqs $args = @('doc2readme', $target) if ($check) { $args += '--check' } if ($relTemplate) { $args += @('--template', $relTemplate) } - & cargo {{_anvil_stable_toolchain_args}} @args + & cargo @stableArgs @args if ($LASTEXITCODE -ne 0) { $hadFailure = $true } } finally { Pop-Location diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/semver-check.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/semver-check.just index 482fd7053..5dbd16128 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/semver-check.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/semver-check.just @@ -49,6 +49,7 @@ [script("pwsh", "-NoProfile")] anvil-semver-check: anvil-semver-check-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' + $stableArgs = {{_anvil_stable_toolchain_args}} $include = (& "{{ just_executable() }}" _anvil-impact-include affected) $commentFile = 'target/anvil/comments/semver.md' if ($include -eq '--skip') { @@ -68,7 +69,7 @@ anvil-semver-check: anvil-semver-check-validate-prereqs anvil-impact # null, `publish = false` as an empty array, and named-registry restrictions # as a nonempty array. Only the empty array is non-publishable. $libPkgs = @{} - $metadataJson = & cargo {{_anvil_stable_toolchain_args}} metadata --no-deps --format-version 1 + $metadataJson = & cargo @stableArgs metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-semver-check: cargo metadata failed' exit $LASTEXITCODE @@ -146,7 +147,7 @@ anvil-semver-check: anvil-semver-check-validate-prereqs anvil-impact } Write-Host "anvil-semver-check: $p (baseline $base)" - $outputLines = @(& cargo {{_anvil_stable_toolchain_args}} semver-checks --package $p --baseline-rev $base 2>&1) + $outputLines = @(& cargo @stableArgs semver-checks --package $p --baseline-rev $base 2>&1) $semverExit = $LASTEXITCODE $output = ($outputLines | Out-String) if ($semverExit -eq 100) { diff --git a/crates/cargo-anvil/templates/justfiles/anvil/groups/pr-slow.just b/crates/cargo-anvil/templates/justfiles/anvil/groups/pr-slow.just index 3ac2c01f5..ddce5d420 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/groups/pr-slow.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/groups/pr-slow.just @@ -7,20 +7,19 @@ # See https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/checks.md # pr-slow is the local umbrella for everything that takes more than ~30s -# per crate (tests, MSRV tests, stricter runtimes, mutation testing). It's -# split into four sub-recipes so individual concerns can be invoked locally -# without dragging the others along: +# per crate (tests, MSRV tests, stricter runtimes, mutation testing). Its +# groups can be invoked individually without invoking the other groups: # # pr-test: tests + coverage -# pr-msrv: unit and integration tests under the declared MSRV +# pr-msrv: all-target tests under the declared MSRV # pr-runtime-analysis: stricter-runtime correctness (miri, careful) # pr-mutants: mutation testing # -# Cloud workflows run the four sub-groups as independent parallel jobs. +# Cloud workflows run these groups as independent parallel jobs. # The umbrella runs them sequentially only when invoked locally: # # $ just anvil-pr-test # tests + coverage only -# $ just anvil-pr-msrv # MSRV unit and integration tests only +# $ just anvil-pr-msrv # MSRV all-target tests only # $ just anvil-pr-runtime-analysis # miri + careful only # $ just anvil-pr-mutants # mutants only diff --git a/crates/cargo-anvil/templates/justfiles/anvil/tools.just b/crates/cargo-anvil/templates/justfiles/anvil/tools.just index 6c78928fb..beec3a4f2 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/tools.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/tools.just @@ -135,7 +135,7 @@ anvil-tool-cargo-spellcheck-source-deps-check: # pwsh is installed via the platform's package manager. The validate recipes # are read-only and only surface a friendly error if a prerequisite is absent. -# Validate that rustc is available. +# Validate that rustc is available and workspace MSRVs are compatible. [group("anvil-setup")] [script("pwsh", "-NoProfile")] anvil-tool-rustc-validate-prereqs: @@ -207,6 +207,10 @@ _anvil-resolve-stable action="install": throw "anvil: Cargo.toml not found at repository root '$repoRoot'" } + # This bootstrap scan must choose Cargo before Cargo is available to + # parse the manifest. Keep its workspace.package-before-package + # precedence and accepted syntax synchronized with the selector in + # versions.just. $values = @{} $section = '' foreach ($line in (Get-Content -LiteralPath $manifestPath)) { @@ -233,9 +237,28 @@ _anvil-resolve-stable action="install": throw 'anvil: no selecting rust-toolchain(.toml) and no root [workspace.package] or [package] rust-version; declare an MSRV or select a toolchain explicitly' } + function Get-InstalledToolchains { + if (-not (Get-Command rustup -ErrorAction SilentlyContinue)) { + throw 'anvil: rustup not found. Install rustup from https://rustup.rs and ensure it is on PATH' + } + $installed = @(rustup toolchain list) + if ($LASTEXITCODE -ne 0) { + throw 'anvil: rustup toolchain list failed' + } + return $installed + } + + function Test-ToolchainInstalled([string] $toolchain) { + $installed = Get-InstalledToolchains + return (($installed -join "`n") -match "(?m)^$([regex]::Escape($toolchain))(?:-|$)") + } + function Assert-WorkspaceMsrvCompatibility { $manifestPath = Join-Path $repoRoot 'Cargo.toml' $rootMsrv = Get-RootMsrv + if (-not (Test-ToolchainInstalled $rootMsrv)) { + throw "anvil: root MSRV toolchain '$rootMsrv' is not installed; run 'just anvil-toolchain-stable-install' before validating prerequisites" + } $metadataText = (& cargo "+$rootMsrv" metadata --manifest-path $manifestPath --format-version 1 --no-deps 2>&1 | Out-String) if ($LASTEXITCODE -ne 0) { throw "anvil: cargo metadata failed while validating workspace MSRVs:`n$metadataText" @@ -255,6 +278,8 @@ _anvil-resolve-stable action="install": if ($value -notmatch '^\s*(\d+)\.(\d+)(?:\.(\d+))?\s*$') { throw "anvil: invalid resolved rust-version '$value'" } + # Cargo treats 1.x and 1.x.0 as the same floor, while + # System.Version sorts an unspecified build component differently. $patch = if ([string]::IsNullOrWhiteSpace($Matches[3])) { 0 } else { [int] $Matches[3] } return [version]::new([int] $Matches[1], [int] $Matches[2], $patch) } @@ -287,19 +312,14 @@ _anvil-resolve-stable action="install": if (-not [string]::IsNullOrWhiteSpace($env:ANVIL_MSRV_TOOLCHAIN)) { return [pscustomobject]@{ Value = $env:ANVIL_MSRV_TOOLCHAIN - Source = 'mapped-msrv' Mapped = $true } } - return [pscustomobject]@{ Value = $msrv; Source = 'msrv'; Mapped = $false } + return [pscustomobject]@{ Value = $msrv; Mapped = $false } } function Install-Toolchain([string] $toolchain, [string] $label) { - $installed = rustup toolchain list - if ($LASTEXITCODE -ne 0) { - throw 'anvil: rustup toolchain list failed' - } - if (($installed -join "`n") -notmatch "(?m)^$([regex]::Escape($toolchain))(?:-|$)") { + if (-not (Test-ToolchainInstalled $toolchain)) { Write-Host "anvil: installing $label toolchain '$toolchain'" rustup toolchain install $toolchain --profile minimal if ($LASTEXITCODE -ne 0) { @@ -337,10 +357,6 @@ _anvil-resolve-stable action="install": exit 0 } - if ($action -ne 'install') { - throw "anvil: unsupported stable resolver action '$action'" - } - Assert-CompleteInternalToolchainConfiguration if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { & cargo --version *> $null @@ -365,10 +381,10 @@ _anvil-resolve-stable action="install": Install-Toolchain (Get-RootMsrv) 'stable' -# Provision the selected stable toolchain. Repository toolchain files are -# processed natively by rustup. This is the shared prerequisite for setup paths -# that execute stable Cargo/Rust or install Cargo tools; Just deduplicates it -# within each setup invocation. +# Setup paths that execute stable Cargo/Rust or install Cargo tools share this +# dependency, which Just deduplicates within one setup invocation. + +# Ensure the selected stable toolchain is available while preserving native repository toolchain-file behavior. [group("anvil-setup")] anvil-toolchain-stable-install: (_anvil-resolve-stable "install") diff --git a/crates/cargo-anvil/templates/justfiles/anvil/versions.just b/crates/cargo-anvil/templates/justfiles/anvil/versions.just index c81fb01ac..f330945cf 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/versions.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/versions.just @@ -59,6 +59,9 @@ if (-not (Test-Path -LiteralPath $manifestPath -PathType Leaf)) { throw "anvil: Cargo.toml not found at repository root '$RepoRoot'" } +# This bootstrap scan must choose Cargo before Cargo is available to parse the +# manifest. Keep its workspace.package-before-package precedence and accepted +# syntax synchronized with Get-RootMsrv in tools.just. $values = @{} $section = '' foreach ($line in (Get-Content -LiteralPath $manifestPath)) { diff --git a/crates/cargo-anvil/tests/recipe_contracts.rs b/crates/cargo-anvil/tests/recipe_contracts.rs index ff4053a55..45f13217a 100644 --- a/crates/cargo-anvil/tests/recipe_contracts.rs +++ b/crates/cargo-anvil/tests/recipe_contracts.rs @@ -23,6 +23,7 @@ const BOLERO: &str = include_str!("../templates/justfiles/anvil/checks/bolero.ju const FMT: &str = include_str!("../templates/justfiles/anvil/checks/fmt.just"); const LLVM_COV: &str = include_str!("../templates/justfiles/anvil/checks/llvm-cov.just"); const LOOM: &str = include_str!("../templates/justfiles/anvil/checks/loom.just"); +const MSRV_TEST: &str = include_str!("../templates/justfiles/anvil/checks/msrv-test.just"); const SEMVER: &str = include_str!("../templates/justfiles/anvil/checks/semver-check.just"); const EXTERNAL_TYPES: &str = include_str!("../templates/justfiles/anvil/checks/external-types.just"); const TOOLS: &str = include_str!("../templates/justfiles/anvil/tools.just"); @@ -80,6 +81,9 @@ if ($env:FAKE_CARGO_LOG) { if ($env:FAKE_CARGO_CWD_LOG) { Add-Content -LiteralPath $env:FAKE_CARGO_CWD_LOG -Value (Get-Location).Path } +if ($env:FAKE_CARGO_TOOLCHAIN_LOG) { + Add-Content -LiteralPath $env:FAKE_CARGO_TOOLCHAIN_LOG -Value $env:RUSTUP_TOOLCHAIN +} if ($args -contains 'each') { exit [int]$env:FAKE_EACH_EXIT } @@ -253,16 +257,12 @@ fn path_with_fake_bin(root: &Path) -> OsString { } fn run_just(root: &Path, arguments: &[&str], environment: &[(&str, &OsStr)]) -> Output { - run_just_from(root, root, arguments, environment) -} - -fn run_just_from(root: &Path, current_dir: &Path, arguments: &[&str], environment: &[(&str, &OsStr)]) -> Output { let mut command = Command::new("just"); command .arg("--justfile") .arg(root.join("Justfile")) .args(arguments) - .current_dir(current_dir); + .current_dir(root); command.env("PATH", path_with_fake_bin(root)); command.env("FAKE_WORKSPACE_ROOT", root); // A fixture is a scratch workspace, so it must not inherit the impact @@ -311,6 +311,7 @@ fn stable_command_leaves_environment_toolchain_selection_native() { write(&justfile_path, &justfile); let args_log = tmp.path().join("cargo-args.log"); let cwd_log = tmp.path().join("cargo-cwd.log"); + let toolchain_log = tmp.path().join("cargo-toolchain.log"); let output = run_just( tmp.path(), @@ -318,6 +319,7 @@ fn stable_command_leaves_environment_toolchain_selection_native() { &[ ("FAKE_CARGO_LOG", args_log.as_os_str()), ("FAKE_CARGO_CWD_LOG", cwd_log.as_os_str()), + ("FAKE_CARGO_TOOLCHAIN_LOG", toolchain_log.as_os_str()), ("RUSTUP_TOOLCHAIN", OsStr::new("test-stable")), ], ); @@ -334,6 +336,73 @@ fn stable_command_leaves_environment_toolchain_selection_native() { fs::canonicalize(tmp.path()).unwrap(), "the command must run directly in its recipe process" ); + assert_eq!(fs::read_to_string(toolchain_log).unwrap().trim(), "test-stable"); +} + +#[test] +fn msrv_test_propagates_nested_just_failures() { + if !tools_available() { + return; + } + let helper_recipes = "\n[script(\"pwsh\", \"-NoProfile\")]\n\ + _anvil-impact-include tier:\n\ + \x20 if ($env:FAKE_IMPACT_HELPER_OUTPUT) { Write-Output $env:FAKE_IMPACT_HELPER_OUTPUT }\n\ + \x20 exit [int]$env:FAKE_IMPACT_HELPER_EXIT\n\ + \n[script(\"pwsh\", \"-NoProfile\")]\n\ + _anvil-resolve-stable action:\n\ + \x20 if ($env:FAKE_RESOLVER_OUTPUT) { Write-Output $env:FAKE_RESOLVER_OUTPUT }\n\ + \x20 exit [int]$env:FAKE_RESOLVER_EXIT\n"; + + let validation_tmp = fixture(&[("msrv-test.just", MSRV_TEST)], &["anvil-impact"]); + let validation_justfile = validation_tmp.path().join("Justfile"); + let mut validation_body = fs::read_to_string(&validation_justfile).unwrap(); + validation_body.push_str(helper_recipes); + write(&validation_justfile, &validation_body); + let validation_failure = run_just( + validation_tmp.path(), + &["anvil-msrv-test-validate-prereqs"], + &[("FAKE_RESOLVER_EXIT", OsStr::new("30"))], + ); + assert_failed(&validation_failure, "failed prerequisite MSRV resolver"); + + let tmp = fixture( + &[("msrv-test.just", MSRV_TEST)], + &["anvil-msrv-test-validate-prereqs", "anvil-impact"], + ); + let justfile_path = tmp.path().join("Justfile"); + let mut justfile = fs::read_to_string(&justfile_path).unwrap(); + justfile.push_str(helper_recipes); + write(&justfile_path, &justfile); + let cargo_log = tmp.path().join("cargo.log"); + + let impact_failure = run_just( + tmp.path(), + &["anvil-msrv-test"], + &[ + ("FAKE_IMPACT_HELPER_EXIT", OsStr::new("31")), + ("FAKE_CARGO_LOG", cargo_log.as_os_str()), + ], + ); + assert_failed(&impact_failure, "failed impact helper"); + assert!( + fs::read_to_string(&cargo_log).unwrap_or_default().is_empty(), + "Cargo must not run after impact helper failure" + ); + + let resolver_failure = run_just( + tmp.path(), + &["anvil-msrv-test"], + &[ + ("FAKE_IMPACT_HELPER_EXIT", OsStr::new("0")), + ("FAKE_RESOLVER_EXIT", OsStr::new("32")), + ("FAKE_CARGO_LOG", cargo_log.as_os_str()), + ], + ); + assert_failed(&resolver_failure, "failed MSRV resolver"); + assert!( + fs::read_to_string(cargo_log).unwrap_or_default().is_empty(), + "Cargo must not run after resolver failure" + ); } #[test] diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap index 6627749db..bd94ae970 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap @@ -847,13 +847,10 @@ steps: parameters: group: pr-fast # ADO has no PR-title predefined variable: `System.PullRequest.Title` - # does NOT exist, so `$(System.PullRequest.Title)` would expand to the - # literal unexpanded macro text (non-empty) and make anvil-pr-title - # validate that string. Resolve the real title from the REST API on PR - # builds and publish it as the PR_TITLE pipeline variable. Only - # anvil-pr-title (in pr-fast) consults it; other groups ignore it, but we - # resolve uniformly to keep group.yml the same across groups. Non-PR builds - # skip; a known PR whose metadata cannot be retrieved fails closed. + # does NOT exist, so `$(System.PullRequest.Title)` would expand to literal + # macro text. Resolve the title only for pr-fast, which owns anvil-pr-title. + # Non-PR builds skip; a known PR whose metadata cannot be retrieved fails + # closed. - pwsh: | $ErrorActionPreference = 'Stop' $prId = $env:SYSTEM_PULLREQUEST_PULLREQUESTID @@ -864,7 +861,7 @@ steps: } $uri = "$($env:SYSTEM_COLLECTIONURI)$($env:SYSTEM_TEAMPROJECTID)/_apis/git/repositories/$($env:BUILD_REPOSITORY_ID)/pullRequests/${prId}?api-version=7.0" try { - $resp = Invoke-RestMethod -Uri $uri -Headers @{ Authorization = "Bearer $($env:SYSTEM_ACCESSTOKEN)" } + $resp = Invoke-RestMethod -Uri $uri -Headers @{ Authorization = "Bearer $env:SYSTEM_ACCESSTOKEN" } Write-Host "##vso[task.setvariable variable=PR_TITLE]$($resp.title)" } catch { Write-Error "anvil: could not resolve PR title for PR $prId ($_). Ensure the job can access the repository OAuth token." @@ -884,7 +881,7 @@ steps: just anvil-pr-fast displayName: anvil-pr-fast env: - # Resolved by the preceding step; empty only outside PR builds. + # Resolved by the preceding pr-fast step; empty only outside PR builds. PR_TITLE: $(PR_TITLE) # Disable cargo incremental compilation in CI: the incremental dir is # not part of the anvil-setup cache (see setup.yml), so generating it is @@ -911,33 +908,7 @@ steps: - template: setup.yml parameters: group: pr-msrv - # ADO has no PR-title predefined variable: `System.PullRequest.Title` - # does NOT exist, so `$(System.PullRequest.Title)` would expand to the - # literal unexpanded macro text (non-empty) and make anvil-pr-title - # validate that string. Resolve the real title from the REST API on PR - # builds and publish it as the PR_TITLE pipeline variable. Only - # anvil-pr-title (in pr-fast) consults it; other groups ignore it, but we - # resolve uniformly to keep group.yml the same across groups. Non-PR builds - # skip; a known PR whose metadata cannot be retrieved fails closed. - - pwsh: | - $ErrorActionPreference = 'Stop' - $prId = $env:SYSTEM_PULLREQUEST_PULLREQUESTID - if (-not $prId) { - Write-Host 'anvil: not a PR build; leaving PR_TITLE empty' - Write-Host '##vso[task.setvariable variable=PR_TITLE]' - exit 0 - } - $uri = "$($env:SYSTEM_COLLECTIONURI)$($env:SYSTEM_TEAMPROJECTID)/_apis/git/repositories/$($env:BUILD_REPOSITORY_ID)/pullRequests/${prId}?api-version=7.0" - try { - $resp = Invoke-RestMethod -Uri $uri -Headers @{ Authorization = "Bearer $($env:SYSTEM_ACCESSTOKEN)" } - Write-Host "##vso[task.setvariable variable=PR_TITLE]$($resp.title)" - } catch { - Write-Error "anvil: could not resolve PR title for PR $prId ($_). Ensure the job can access the repository OAuth token." - exit 1 - } - displayName: anvil-pr-msrv (resolve PR title) - env: - SYSTEM_ACCESSTOKEN: $(System.AccessToken) + - bash: | set -euo pipefail # The impact mode is fixed by group class at emit time -- never probed from a @@ -949,8 +920,7 @@ steps: just anvil-pr-msrv displayName: anvil-pr-msrv env: - # Resolved by the preceding step; empty only outside PR builds. - PR_TITLE: $(PR_TITLE) + # Disable cargo incremental compilation in CI: the incremental dir is # not part of the anvil-setup cache (see setup.yml), so generating it is # pure cost with no cross-run benefit. Mirrors the GitHub impl workflows' @@ -976,33 +946,7 @@ steps: - template: setup.yml parameters: group: pr-mutants - # ADO has no PR-title predefined variable: `System.PullRequest.Title` - # does NOT exist, so `$(System.PullRequest.Title)` would expand to the - # literal unexpanded macro text (non-empty) and make anvil-pr-title - # validate that string. Resolve the real title from the REST API on PR - # builds and publish it as the PR_TITLE pipeline variable. Only - # anvil-pr-title (in pr-fast) consults it; other groups ignore it, but we - # resolve uniformly to keep group.yml the same across groups. Non-PR builds - # skip; a known PR whose metadata cannot be retrieved fails closed. - - pwsh: | - $ErrorActionPreference = 'Stop' - $prId = $env:SYSTEM_PULLREQUEST_PULLREQUESTID - if (-not $prId) { - Write-Host 'anvil: not a PR build; leaving PR_TITLE empty' - Write-Host '##vso[task.setvariable variable=PR_TITLE]' - exit 0 - } - $uri = "$($env:SYSTEM_COLLECTIONURI)$($env:SYSTEM_TEAMPROJECTID)/_apis/git/repositories/$($env:BUILD_REPOSITORY_ID)/pullRequests/${prId}?api-version=7.0" - try { - $resp = Invoke-RestMethod -Uri $uri -Headers @{ Authorization = "Bearer $($env:SYSTEM_ACCESSTOKEN)" } - Write-Host "##vso[task.setvariable variable=PR_TITLE]$($resp.title)" - } catch { - Write-Error "anvil: could not resolve PR title for PR $prId ($_). Ensure the job can access the repository OAuth token." - exit 1 - } - displayName: anvil-pr-mutants (resolve PR title) - env: - SYSTEM_ACCESSTOKEN: $(System.AccessToken) + - bash: | set -euo pipefail # The impact mode is fixed by group class at emit time -- never probed from a @@ -1014,8 +958,7 @@ steps: just anvil-pr-mutants displayName: anvil-pr-mutants env: - # Resolved by the preceding step; empty only outside PR builds. - PR_TITLE: $(PR_TITLE) + # Disable cargo incremental compilation in CI: the incremental dir is # not part of the anvil-setup cache (see setup.yml), so generating it is # pure cost with no cross-run benefit. Mirrors the GitHub impl workflows' @@ -1041,33 +984,7 @@ steps: - template: setup.yml parameters: group: pr-runtime-analysis - # ADO has no PR-title predefined variable: `System.PullRequest.Title` - # does NOT exist, so `$(System.PullRequest.Title)` would expand to the - # literal unexpanded macro text (non-empty) and make anvil-pr-title - # validate that string. Resolve the real title from the REST API on PR - # builds and publish it as the PR_TITLE pipeline variable. Only - # anvil-pr-title (in pr-fast) consults it; other groups ignore it, but we - # resolve uniformly to keep group.yml the same across groups. Non-PR builds - # skip; a known PR whose metadata cannot be retrieved fails closed. - - pwsh: | - $ErrorActionPreference = 'Stop' - $prId = $env:SYSTEM_PULLREQUEST_PULLREQUESTID - if (-not $prId) { - Write-Host 'anvil: not a PR build; leaving PR_TITLE empty' - Write-Host '##vso[task.setvariable variable=PR_TITLE]' - exit 0 - } - $uri = "$($env:SYSTEM_COLLECTIONURI)$($env:SYSTEM_TEAMPROJECTID)/_apis/git/repositories/$($env:BUILD_REPOSITORY_ID)/pullRequests/${prId}?api-version=7.0" - try { - $resp = Invoke-RestMethod -Uri $uri -Headers @{ Authorization = "Bearer $($env:SYSTEM_ACCESSTOKEN)" } - Write-Host "##vso[task.setvariable variable=PR_TITLE]$($resp.title)" - } catch { - Write-Error "anvil: could not resolve PR title for PR $prId ($_). Ensure the job can access the repository OAuth token." - exit 1 - } - displayName: anvil-pr-runtime-analysis (resolve PR title) - env: - SYSTEM_ACCESSTOKEN: $(System.AccessToken) + - bash: | set -euo pipefail # The impact mode is fixed by group class at emit time -- never probed from a @@ -1079,8 +996,7 @@ steps: just anvil-pr-runtime-analysis displayName: anvil-pr-runtime-analysis env: - # Resolved by the preceding step; empty only outside PR builds. - PR_TITLE: $(PR_TITLE) + # Disable cargo incremental compilation in CI: the incremental dir is # not part of the anvil-setup cache (see setup.yml), so generating it is # pure cost with no cross-run benefit. Mirrors the GitHub impl workflows' @@ -1106,33 +1022,7 @@ steps: - template: setup.yml parameters: group: pr-test - # ADO has no PR-title predefined variable: `System.PullRequest.Title` - # does NOT exist, so `$(System.PullRequest.Title)` would expand to the - # literal unexpanded macro text (non-empty) and make anvil-pr-title - # validate that string. Resolve the real title from the REST API on PR - # builds and publish it as the PR_TITLE pipeline variable. Only - # anvil-pr-title (in pr-fast) consults it; other groups ignore it, but we - # resolve uniformly to keep group.yml the same across groups. Non-PR builds - # skip; a known PR whose metadata cannot be retrieved fails closed. - - pwsh: | - $ErrorActionPreference = 'Stop' - $prId = $env:SYSTEM_PULLREQUEST_PULLREQUESTID - if (-not $prId) { - Write-Host 'anvil: not a PR build; leaving PR_TITLE empty' - Write-Host '##vso[task.setvariable variable=PR_TITLE]' - exit 0 - } - $uri = "$($env:SYSTEM_COLLECTIONURI)$($env:SYSTEM_TEAMPROJECTID)/_apis/git/repositories/$($env:BUILD_REPOSITORY_ID)/pullRequests/${prId}?api-version=7.0" - try { - $resp = Invoke-RestMethod -Uri $uri -Headers @{ Authorization = "Bearer $($env:SYSTEM_ACCESSTOKEN)" } - Write-Host "##vso[task.setvariable variable=PR_TITLE]$($resp.title)" - } catch { - Write-Error "anvil: could not resolve PR title for PR $prId ($_). Ensure the job can access the repository OAuth token." - exit 1 - } - displayName: anvil-pr-test (resolve PR title) - env: - SYSTEM_ACCESSTOKEN: $(System.AccessToken) + - bash: | set -euo pipefail # The impact mode is fixed by group class at emit time -- never probed from a @@ -1144,8 +1034,7 @@ steps: just anvil-pr-test displayName: anvil-pr-test env: - # Resolved by the preceding step; empty only outside PR builds. - PR_TITLE: $(PR_TITLE) + # Disable cargo incremental compilation in CI: the incremental dir is # not part of the anvil-setup cache (see setup.yml), so generating it is # pure cost with no cross-run benefit. Mirrors the GitHub impl workflows' @@ -1171,33 +1060,7 @@ steps: - template: setup.yml parameters: group: scheduled-advisories - # ADO has no PR-title predefined variable: `System.PullRequest.Title` - # does NOT exist, so `$(System.PullRequest.Title)` would expand to the - # literal unexpanded macro text (non-empty) and make anvil-pr-title - # validate that string. Resolve the real title from the REST API on PR - # builds and publish it as the PR_TITLE pipeline variable. Only - # anvil-pr-title (in pr-fast) consults it; other groups ignore it, but we - # resolve uniformly to keep group.yml the same across groups. Non-PR builds - # skip; a known PR whose metadata cannot be retrieved fails closed. - - pwsh: | - $ErrorActionPreference = 'Stop' - $prId = $env:SYSTEM_PULLREQUEST_PULLREQUESTID - if (-not $prId) { - Write-Host 'anvil: not a PR build; leaving PR_TITLE empty' - Write-Host '##vso[task.setvariable variable=PR_TITLE]' - exit 0 - } - $uri = "$($env:SYSTEM_COLLECTIONURI)$($env:SYSTEM_TEAMPROJECTID)/_apis/git/repositories/$($env:BUILD_REPOSITORY_ID)/pullRequests/${prId}?api-version=7.0" - try { - $resp = Invoke-RestMethod -Uri $uri -Headers @{ Authorization = "Bearer $($env:SYSTEM_ACCESSTOKEN)" } - Write-Host "##vso[task.setvariable variable=PR_TITLE]$($resp.title)" - } catch { - Write-Error "anvil: could not resolve PR title for PR $prId ($_). Ensure the job can access the repository OAuth token." - exit 1 - } - displayName: anvil-scheduled-advisories (resolve PR title) - env: - SYSTEM_ACCESSTOKEN: $(System.AccessToken) + - bash: | set -euo pipefail # The impact mode is fixed by group class at emit time -- never probed from a @@ -1209,8 +1072,7 @@ steps: just anvil-scheduled-advisories displayName: anvil-scheduled-advisories env: - # Resolved by the preceding step; empty only outside PR builds. - PR_TITLE: $(PR_TITLE) + # Disable cargo incremental compilation in CI: the incremental dir is # not part of the anvil-setup cache (see setup.yml), so generating it is # pure cost with no cross-run benefit. Mirrors the GitHub impl workflows' @@ -1236,33 +1098,7 @@ steps: - template: setup.yml parameters: group: scheduled-exhaustive - # ADO has no PR-title predefined variable: `System.PullRequest.Title` - # does NOT exist, so `$(System.PullRequest.Title)` would expand to the - # literal unexpanded macro text (non-empty) and make anvil-pr-title - # validate that string. Resolve the real title from the REST API on PR - # builds and publish it as the PR_TITLE pipeline variable. Only - # anvil-pr-title (in pr-fast) consults it; other groups ignore it, but we - # resolve uniformly to keep group.yml the same across groups. Non-PR builds - # skip; a known PR whose metadata cannot be retrieved fails closed. - - pwsh: | - $ErrorActionPreference = 'Stop' - $prId = $env:SYSTEM_PULLREQUEST_PULLREQUESTID - if (-not $prId) { - Write-Host 'anvil: not a PR build; leaving PR_TITLE empty' - Write-Host '##vso[task.setvariable variable=PR_TITLE]' - exit 0 - } - $uri = "$($env:SYSTEM_COLLECTIONURI)$($env:SYSTEM_TEAMPROJECTID)/_apis/git/repositories/$($env:BUILD_REPOSITORY_ID)/pullRequests/${prId}?api-version=7.0" - try { - $resp = Invoke-RestMethod -Uri $uri -Headers @{ Authorization = "Bearer $($env:SYSTEM_ACCESSTOKEN)" } - Write-Host "##vso[task.setvariable variable=PR_TITLE]$($resp.title)" - } catch { - Write-Error "anvil: could not resolve PR title for PR $prId ($_). Ensure the job can access the repository OAuth token." - exit 1 - } - displayName: anvil-scheduled-exhaustive (resolve PR title) - env: - SYSTEM_ACCESSTOKEN: $(System.AccessToken) + - bash: | set -euo pipefail # The impact mode is fixed by group class at emit time -- never probed from a @@ -1274,8 +1110,7 @@ steps: just anvil-scheduled-exhaustive displayName: anvil-scheduled-exhaustive env: - # Resolved by the preceding step; empty only outside PR builds. - PR_TITLE: $(PR_TITLE) + # Disable cargo incremental compilation in CI: the incremental dir is # not part of the anvil-setup cache (see setup.yml), so generating it is # pure cost with no cross-run benefit. Mirrors the GitHub impl workflows' @@ -1301,33 +1136,7 @@ steps: - template: setup.yml parameters: group: scheduled-runtime-analysis - # ADO has no PR-title predefined variable: `System.PullRequest.Title` - # does NOT exist, so `$(System.PullRequest.Title)` would expand to the - # literal unexpanded macro text (non-empty) and make anvil-pr-title - # validate that string. Resolve the real title from the REST API on PR - # builds and publish it as the PR_TITLE pipeline variable. Only - # anvil-pr-title (in pr-fast) consults it; other groups ignore it, but we - # resolve uniformly to keep group.yml the same across groups. Non-PR builds - # skip; a known PR whose metadata cannot be retrieved fails closed. - - pwsh: | - $ErrorActionPreference = 'Stop' - $prId = $env:SYSTEM_PULLREQUEST_PULLREQUESTID - if (-not $prId) { - Write-Host 'anvil: not a PR build; leaving PR_TITLE empty' - Write-Host '##vso[task.setvariable variable=PR_TITLE]' - exit 0 - } - $uri = "$($env:SYSTEM_COLLECTIONURI)$($env:SYSTEM_TEAMPROJECTID)/_apis/git/repositories/$($env:BUILD_REPOSITORY_ID)/pullRequests/${prId}?api-version=7.0" - try { - $resp = Invoke-RestMethod -Uri $uri -Headers @{ Authorization = "Bearer $($env:SYSTEM_ACCESSTOKEN)" } - Write-Host "##vso[task.setvariable variable=PR_TITLE]$($resp.title)" - } catch { - Write-Error "anvil: could not resolve PR title for PR $prId ($_). Ensure the job can access the repository OAuth token." - exit 1 - } - displayName: anvil-scheduled-runtime-analysis (resolve PR title) - env: - SYSTEM_ACCESSTOKEN: $(System.AccessToken) + - bash: | set -euo pipefail # The impact mode is fixed by group class at emit time -- never probed from a @@ -1339,8 +1148,7 @@ steps: just anvil-scheduled-runtime-analysis displayName: anvil-scheduled-runtime-analysis env: - # Resolved by the preceding step; empty only outside PR builds. - PR_TITLE: $(PR_TITLE) + # Disable cargo incremental compilation in CI: the incremental dir is # not part of the anvil-setup cache (see setup.yml), so generating it is # pure cost with no cross-run benefit. Mirrors the GitHub impl workflows' @@ -1366,33 +1174,7 @@ steps: - template: setup.yml parameters: group: scheduled-test - # ADO has no PR-title predefined variable: `System.PullRequest.Title` - # does NOT exist, so `$(System.PullRequest.Title)` would expand to the - # literal unexpanded macro text (non-empty) and make anvil-pr-title - # validate that string. Resolve the real title from the REST API on PR - # builds and publish it as the PR_TITLE pipeline variable. Only - # anvil-pr-title (in pr-fast) consults it; other groups ignore it, but we - # resolve uniformly to keep group.yml the same across groups. Non-PR builds - # skip; a known PR whose metadata cannot be retrieved fails closed. - - pwsh: | - $ErrorActionPreference = 'Stop' - $prId = $env:SYSTEM_PULLREQUEST_PULLREQUESTID - if (-not $prId) { - Write-Host 'anvil: not a PR build; leaving PR_TITLE empty' - Write-Host '##vso[task.setvariable variable=PR_TITLE]' - exit 0 - } - $uri = "$($env:SYSTEM_COLLECTIONURI)$($env:SYSTEM_TEAMPROJECTID)/_apis/git/repositories/$($env:BUILD_REPOSITORY_ID)/pullRequests/${prId}?api-version=7.0" - try { - $resp = Invoke-RestMethod -Uri $uri -Headers @{ Authorization = "Bearer $($env:SYSTEM_ACCESSTOKEN)" } - Write-Host "##vso[task.setvariable variable=PR_TITLE]$($resp.title)" - } catch { - Write-Error "anvil: could not resolve PR title for PR $prId ($_). Ensure the job can access the repository OAuth token." - exit 1 - } - displayName: anvil-scheduled-test (resolve PR title) - env: - SYSTEM_ACCESSTOKEN: $(System.AccessToken) + - bash: | set -euo pipefail # The impact mode is fixed by group class at emit time -- never probed from a @@ -1404,8 +1186,7 @@ steps: just anvil-scheduled-test displayName: anvil-scheduled-test env: - # Resolved by the preceding step; empty only outside PR builds. - PR_TITLE: $(PR_TITLE) + # Disable cargo incremental compilation in CI: the incremental dir is # not part of the anvil-setup cache (see setup.yml), so generating it is # pure cost with no cross-run benefit. Mirrors the GitHub impl workflows' @@ -2996,8 +2777,13 @@ anvil-miri-validate-prereqs: anvil-component-nightly-miri-validate-prereqs anvil anvil-msrv-test: anvil-msrv-test-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include affected) + $impactExit = $LASTEXITCODE + if ($impactExit -ne 0) { exit $impactExit } if ($include -eq '--skip') { Write-Host 'anvil-msrv-test: no affected packages; skipping'; exit 0 } - $toolchain = (& "{{ just_executable() }}" _anvil-resolve-stable msrv | Out-String).Trim() + $toolchainOutput = & "{{ just_executable() }}" _anvil-resolve-stable msrv | Out-String + $resolverExit = $LASTEXITCODE + if ($resolverExit -ne 0) { exit $resolverExit } + $toolchain = $toolchainOutput.Trim() if ([string]::IsNullOrWhiteSpace($toolchain)) { Write-Host 'anvil-msrv-test: no root MSRV declared; skipping' exit 0 @@ -3019,7 +2805,10 @@ anvil-msrv-test-setup installer="install": [group("anvil-setup")] [script("pwsh", "-NoProfile")] anvil-msrv-test-validate-prereqs: - $toolchain = (& "{{ just_executable() }}" _anvil-resolve-stable msrv | Out-String).Trim() + $toolchainOutput = & "{{ just_executable() }}" _anvil-resolve-stable msrv | Out-String + $resolverExit = $LASTEXITCODE + if ($resolverExit -ne 0) { exit $resolverExit } + $toolchain = $toolchainOutput.Trim() if ([string]::IsNullOrWhiteSpace($toolchain)) { exit 0 } & cargo "+$toolchain" --version if ($LASTEXITCODE -ne 0) { @@ -3256,6 +3045,7 @@ anvil-pr-title-validate-prereqs: anvil-tool-pwsh-validate-prereqs [script("pwsh", "-NoProfile")] _anvil-readme mode: anvil-readme-check-validate-prereqs $ErrorActionPreference = 'Stop' + $stableArgs = {{_anvil_stable_toolchain_args}} $action = '{{ mode }}' if ($action -notin @('generate', 'check')) { [Console]::Error.WriteLine("anvil-readme: invalid mode '$action'; expected 'generate' or 'check'") @@ -3274,7 +3064,7 @@ _anvil-readme mode: anvil-readme-check-validate-prereqs # Unscoped: a change to the workspace-level README template is a repo-level # input cargo-delta cannot map to a package, so this check always runs the # full eligible-crate set rather than an impact subset. - $metadataJson = & cargo {{_anvil_stable_toolchain_args}} metadata --no-deps --format-version 1 + $metadataJson = & cargo @stableArgs metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error "${prefix}: cargo metadata failed" exit $LASTEXITCODE @@ -3320,7 +3110,7 @@ _anvil-readme mode: anvil-readme-check-validate-prereqs $args = @('doc2readme', $target) if ($check) { $args += '--check' } if ($relTemplate) { $args += @('--template', $relTemplate) } - & cargo {{_anvil_stable_toolchain_args}} @args + & cargo @stableArgs @args if ($LASTEXITCODE -ne 0) { $hadFailure = $true } } finally { Pop-Location @@ -3391,6 +3181,7 @@ anvil-readme-check-validate-prereqs: anvil-tool-cargo-doc2readme-validate-prereq [script("pwsh", "-NoProfile")] anvil-semver-check: anvil-semver-check-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' + $stableArgs = {{_anvil_stable_toolchain_args}} $include = (& "{{ just_executable() }}" _anvil-impact-include affected) $commentFile = 'target/anvil/comments/semver.md' if ($include -eq '--skip') { @@ -3410,7 +3201,7 @@ anvil-semver-check: anvil-semver-check-validate-prereqs anvil-impact # null, `publish = false` as an empty array, and named-registry restrictions # as a nonempty array. Only the empty array is non-publishable. $libPkgs = @{} - $metadataJson = & cargo {{_anvil_stable_toolchain_args}} metadata --no-deps --format-version 1 + $metadataJson = & cargo @stableArgs metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-semver-check: cargo metadata failed' exit $LASTEXITCODE @@ -3488,7 +3279,7 @@ anvil-semver-check: anvil-semver-check-validate-prereqs anvil-impact } Write-Host "anvil-semver-check: $p (baseline $base)" - $outputLines = @(& cargo {{_anvil_stable_toolchain_args}} semver-checks --package $p --baseline-rev $base 2>&1) + $outputLines = @(& cargo @stableArgs semver-checks --package $p --baseline-rev $base 2>&1) $semverExit = $LASTEXITCODE $output = ($outputLines | Out-String) if ($semverExit -eq 100) { @@ -5019,20 +4810,19 @@ anvil-pr-runtime-analysis-validate-prereqs: \ # See https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/checks.md # pr-slow is the local umbrella for everything that takes more than ~30s -# per crate (tests, MSRV tests, stricter runtimes, mutation testing). It's -# split into four sub-recipes so individual concerns can be invoked locally -# without dragging the others along: +# per crate (tests, MSRV tests, stricter runtimes, mutation testing). Its +# groups can be invoked individually without invoking the other groups: # # pr-test: tests + coverage -# pr-msrv: unit and integration tests under the declared MSRV +# pr-msrv: all-target tests under the declared MSRV # pr-runtime-analysis: stricter-runtime correctness (miri, careful) # pr-mutants: mutation testing # -# Cloud workflows run the four sub-groups as independent parallel jobs. +# Cloud workflows run these groups as independent parallel jobs. # The umbrella runs them sequentially only when invoked locally: # # $ just anvil-pr-test # tests + coverage only -# $ just anvil-pr-msrv # MSRV unit and integration tests only +# $ just anvil-pr-msrv # MSRV all-target tests only # $ just anvil-pr-runtime-analysis # miri + careful only # $ just anvil-pr-mutants # mutants only @@ -6392,7 +6182,7 @@ anvil-tool-cargo-spellcheck-source-deps-check: # pwsh is installed via the platform's package manager. The validate recipes # are read-only and only surface a friendly error if a prerequisite is absent. -# Validate that rustc is available. +# Validate that rustc is available and workspace MSRVs are compatible. [group("anvil-setup")] [script("pwsh", "-NoProfile")] anvil-tool-rustc-validate-prereqs: @@ -6464,6 +6254,10 @@ _anvil-resolve-stable action="install": throw "anvil: Cargo.toml not found at repository root '$repoRoot'" } + # This bootstrap scan must choose Cargo before Cargo is available to + # parse the manifest. Keep its workspace.package-before-package + # precedence and accepted syntax synchronized with the selector in + # versions.just. $values = @{} $section = '' foreach ($line in (Get-Content -LiteralPath $manifestPath)) { @@ -6490,9 +6284,28 @@ _anvil-resolve-stable action="install": throw 'anvil: no selecting rust-toolchain(.toml) and no root [workspace.package] or [package] rust-version; declare an MSRV or select a toolchain explicitly' } + function Get-InstalledToolchains { + if (-not (Get-Command rustup -ErrorAction SilentlyContinue)) { + throw 'anvil: rustup not found. Install rustup from https://rustup.rs and ensure it is on PATH' + } + $installed = @(rustup toolchain list) + if ($LASTEXITCODE -ne 0) { + throw 'anvil: rustup toolchain list failed' + } + return $installed + } + + function Test-ToolchainInstalled([string] $toolchain) { + $installed = Get-InstalledToolchains + return (($installed -join "`n") -match "(?m)^$([regex]::Escape($toolchain))(?:-|$)") + } + function Assert-WorkspaceMsrvCompatibility { $manifestPath = Join-Path $repoRoot 'Cargo.toml' $rootMsrv = Get-RootMsrv + if (-not (Test-ToolchainInstalled $rootMsrv)) { + throw "anvil: root MSRV toolchain '$rootMsrv' is not installed; run 'just anvil-toolchain-stable-install' before validating prerequisites" + } $metadataText = (& cargo "+$rootMsrv" metadata --manifest-path $manifestPath --format-version 1 --no-deps 2>&1 | Out-String) if ($LASTEXITCODE -ne 0) { throw "anvil: cargo metadata failed while validating workspace MSRVs:`n$metadataText" @@ -6512,6 +6325,8 @@ _anvil-resolve-stable action="install": if ($value -notmatch '^\s*(\d+)\.(\d+)(?:\.(\d+))?\s*$') { throw "anvil: invalid resolved rust-version '$value'" } + # Cargo treats 1.x and 1.x.0 as the same floor, while + # System.Version sorts an unspecified build component differently. $patch = if ([string]::IsNullOrWhiteSpace($Matches[3])) { 0 } else { [int] $Matches[3] } return [version]::new([int] $Matches[1], [int] $Matches[2], $patch) } @@ -6544,19 +6359,14 @@ _anvil-resolve-stable action="install": if (-not [string]::IsNullOrWhiteSpace($env:ANVIL_MSRV_TOOLCHAIN)) { return [pscustomobject]@{ Value = $env:ANVIL_MSRV_TOOLCHAIN - Source = 'mapped-msrv' Mapped = $true } } - return [pscustomobject]@{ Value = $msrv; Source = 'msrv'; Mapped = $false } + return [pscustomobject]@{ Value = $msrv; Mapped = $false } } function Install-Toolchain([string] $toolchain, [string] $label) { - $installed = rustup toolchain list - if ($LASTEXITCODE -ne 0) { - throw 'anvil: rustup toolchain list failed' - } - if (($installed -join "`n") -notmatch "(?m)^$([regex]::Escape($toolchain))(?:-|$)") { + if (-not (Test-ToolchainInstalled $toolchain)) { Write-Host "anvil: installing $label toolchain '$toolchain'" rustup toolchain install $toolchain --profile minimal if ($LASTEXITCODE -ne 0) { @@ -6594,10 +6404,6 @@ _anvil-resolve-stable action="install": exit 0 } - if ($action -ne 'install') { - throw "anvil: unsupported stable resolver action '$action'" - } - Assert-CompleteInternalToolchainConfiguration if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { & cargo --version *> $null @@ -6622,10 +6428,10 @@ _anvil-resolve-stable action="install": Install-Toolchain (Get-RootMsrv) 'stable' -# Provision the selected stable toolchain. Repository toolchain files are -# processed natively by rustup. This is the shared prerequisite for setup paths -# that execute stable Cargo/Rust or install Cargo tools; Just deduplicates it -# within each setup invocation. +# Setup paths that execute stable Cargo/Rust or install Cargo tools share this +# dependency, which Just deduplicates within one setup invocation. + +# Ensure the selected stable toolchain is available while preserving native repository toolchain-file behavior. [group("anvil-setup")] anvil-toolchain-stable-install: (_anvil-resolve-stable "install") @@ -7314,6 +7120,9 @@ if (-not (Test-Path -LiteralPath $manifestPath -PathType Leaf)) { throw "anvil: Cargo.toml not found at repository root '$RepoRoot'" } +# This bootstrap scan must choose Cargo before Cargo is available to parse the +# manifest. Keep its workspace.package-before-package precedence and accepted +# syntax synchronized with Get-RootMsrv in tools.just. $values = @{} $section = '' foreach ($line in (Get-Content -LiteralPath $manifestPath)) { diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap index 4fef0b768..715f2e3c5 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap @@ -611,7 +611,7 @@ runs: fi # The catalog recipe `anvil-setup` (or `anvil--setup` - # when a group is specified via the `group` input) uses the Just + # when a group is specified via the `group` input) uses the Just executable # bootstrapped above, then handles everything else. The setup recipes are idempotent and # short-circuit on tools already installed at or above the pinned # version, so re-running on cache-hit runs is cheap. @@ -2763,8 +2763,13 @@ anvil-miri-validate-prereqs: anvil-component-nightly-miri-validate-prereqs anvil anvil-msrv-test: anvil-msrv-test-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include affected) + $impactExit = $LASTEXITCODE + if ($impactExit -ne 0) { exit $impactExit } if ($include -eq '--skip') { Write-Host 'anvil-msrv-test: no affected packages; skipping'; exit 0 } - $toolchain = (& "{{ just_executable() }}" _anvil-resolve-stable msrv | Out-String).Trim() + $toolchainOutput = & "{{ just_executable() }}" _anvil-resolve-stable msrv | Out-String + $resolverExit = $LASTEXITCODE + if ($resolverExit -ne 0) { exit $resolverExit } + $toolchain = $toolchainOutput.Trim() if ([string]::IsNullOrWhiteSpace($toolchain)) { Write-Host 'anvil-msrv-test: no root MSRV declared; skipping' exit 0 @@ -2786,7 +2791,10 @@ anvil-msrv-test-setup installer="install": [group("anvil-setup")] [script("pwsh", "-NoProfile")] anvil-msrv-test-validate-prereqs: - $toolchain = (& "{{ just_executable() }}" _anvil-resolve-stable msrv | Out-String).Trim() + $toolchainOutput = & "{{ just_executable() }}" _anvil-resolve-stable msrv | Out-String + $resolverExit = $LASTEXITCODE + if ($resolverExit -ne 0) { exit $resolverExit } + $toolchain = $toolchainOutput.Trim() if ([string]::IsNullOrWhiteSpace($toolchain)) { exit 0 } & cargo "+$toolchain" --version if ($LASTEXITCODE -ne 0) { @@ -3023,6 +3031,7 @@ anvil-pr-title-validate-prereqs: anvil-tool-pwsh-validate-prereqs [script("pwsh", "-NoProfile")] _anvil-readme mode: anvil-readme-check-validate-prereqs $ErrorActionPreference = 'Stop' + $stableArgs = {{_anvil_stable_toolchain_args}} $action = '{{ mode }}' if ($action -notin @('generate', 'check')) { [Console]::Error.WriteLine("anvil-readme: invalid mode '$action'; expected 'generate' or 'check'") @@ -3041,7 +3050,7 @@ _anvil-readme mode: anvil-readme-check-validate-prereqs # Unscoped: a change to the workspace-level README template is a repo-level # input cargo-delta cannot map to a package, so this check always runs the # full eligible-crate set rather than an impact subset. - $metadataJson = & cargo {{_anvil_stable_toolchain_args}} metadata --no-deps --format-version 1 + $metadataJson = & cargo @stableArgs metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error "${prefix}: cargo metadata failed" exit $LASTEXITCODE @@ -3087,7 +3096,7 @@ _anvil-readme mode: anvil-readme-check-validate-prereqs $args = @('doc2readme', $target) if ($check) { $args += '--check' } if ($relTemplate) { $args += @('--template', $relTemplate) } - & cargo {{_anvil_stable_toolchain_args}} @args + & cargo @stableArgs @args if ($LASTEXITCODE -ne 0) { $hadFailure = $true } } finally { Pop-Location @@ -3158,6 +3167,7 @@ anvil-readme-check-validate-prereqs: anvil-tool-cargo-doc2readme-validate-prereq [script("pwsh", "-NoProfile")] anvil-semver-check: anvil-semver-check-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' + $stableArgs = {{_anvil_stable_toolchain_args}} $include = (& "{{ just_executable() }}" _anvil-impact-include affected) $commentFile = 'target/anvil/comments/semver.md' if ($include -eq '--skip') { @@ -3177,7 +3187,7 @@ anvil-semver-check: anvil-semver-check-validate-prereqs anvil-impact # null, `publish = false` as an empty array, and named-registry restrictions # as a nonempty array. Only the empty array is non-publishable. $libPkgs = @{} - $metadataJson = & cargo {{_anvil_stable_toolchain_args}} metadata --no-deps --format-version 1 + $metadataJson = & cargo @stableArgs metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-semver-check: cargo metadata failed' exit $LASTEXITCODE @@ -3255,7 +3265,7 @@ anvil-semver-check: anvil-semver-check-validate-prereqs anvil-impact } Write-Host "anvil-semver-check: $p (baseline $base)" - $outputLines = @(& cargo {{_anvil_stable_toolchain_args}} semver-checks --package $p --baseline-rev $base 2>&1) + $outputLines = @(& cargo @stableArgs semver-checks --package $p --baseline-rev $base 2>&1) $semverExit = $LASTEXITCODE $output = ($outputLines | Out-String) if ($semverExit -eq 100) { @@ -4786,20 +4796,19 @@ anvil-pr-runtime-analysis-validate-prereqs: \ # See https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/checks.md # pr-slow is the local umbrella for everything that takes more than ~30s -# per crate (tests, MSRV tests, stricter runtimes, mutation testing). It's -# split into four sub-recipes so individual concerns can be invoked locally -# without dragging the others along: +# per crate (tests, MSRV tests, stricter runtimes, mutation testing). Its +# groups can be invoked individually without invoking the other groups: # # pr-test: tests + coverage -# pr-msrv: unit and integration tests under the declared MSRV +# pr-msrv: all-target tests under the declared MSRV # pr-runtime-analysis: stricter-runtime correctness (miri, careful) # pr-mutants: mutation testing # -# Cloud workflows run the four sub-groups as independent parallel jobs. +# Cloud workflows run these groups as independent parallel jobs. # The umbrella runs them sequentially only when invoked locally: # # $ just anvil-pr-test # tests + coverage only -# $ just anvil-pr-msrv # MSRV unit and integration tests only +# $ just anvil-pr-msrv # MSRV all-target tests only # $ just anvil-pr-runtime-analysis # miri + careful only # $ just anvil-pr-mutants # mutants only @@ -6159,7 +6168,7 @@ anvil-tool-cargo-spellcheck-source-deps-check: # pwsh is installed via the platform's package manager. The validate recipes # are read-only and only surface a friendly error if a prerequisite is absent. -# Validate that rustc is available. +# Validate that rustc is available and workspace MSRVs are compatible. [group("anvil-setup")] [script("pwsh", "-NoProfile")] anvil-tool-rustc-validate-prereqs: @@ -6231,6 +6240,10 @@ _anvil-resolve-stable action="install": throw "anvil: Cargo.toml not found at repository root '$repoRoot'" } + # This bootstrap scan must choose Cargo before Cargo is available to + # parse the manifest. Keep its workspace.package-before-package + # precedence and accepted syntax synchronized with the selector in + # versions.just. $values = @{} $section = '' foreach ($line in (Get-Content -LiteralPath $manifestPath)) { @@ -6257,9 +6270,28 @@ _anvil-resolve-stable action="install": throw 'anvil: no selecting rust-toolchain(.toml) and no root [workspace.package] or [package] rust-version; declare an MSRV or select a toolchain explicitly' } + function Get-InstalledToolchains { + if (-not (Get-Command rustup -ErrorAction SilentlyContinue)) { + throw 'anvil: rustup not found. Install rustup from https://rustup.rs and ensure it is on PATH' + } + $installed = @(rustup toolchain list) + if ($LASTEXITCODE -ne 0) { + throw 'anvil: rustup toolchain list failed' + } + return $installed + } + + function Test-ToolchainInstalled([string] $toolchain) { + $installed = Get-InstalledToolchains + return (($installed -join "`n") -match "(?m)^$([regex]::Escape($toolchain))(?:-|$)") + } + function Assert-WorkspaceMsrvCompatibility { $manifestPath = Join-Path $repoRoot 'Cargo.toml' $rootMsrv = Get-RootMsrv + if (-not (Test-ToolchainInstalled $rootMsrv)) { + throw "anvil: root MSRV toolchain '$rootMsrv' is not installed; run 'just anvil-toolchain-stable-install' before validating prerequisites" + } $metadataText = (& cargo "+$rootMsrv" metadata --manifest-path $manifestPath --format-version 1 --no-deps 2>&1 | Out-String) if ($LASTEXITCODE -ne 0) { throw "anvil: cargo metadata failed while validating workspace MSRVs:`n$metadataText" @@ -6279,6 +6311,8 @@ _anvil-resolve-stable action="install": if ($value -notmatch '^\s*(\d+)\.(\d+)(?:\.(\d+))?\s*$') { throw "anvil: invalid resolved rust-version '$value'" } + # Cargo treats 1.x and 1.x.0 as the same floor, while + # System.Version sorts an unspecified build component differently. $patch = if ([string]::IsNullOrWhiteSpace($Matches[3])) { 0 } else { [int] $Matches[3] } return [version]::new([int] $Matches[1], [int] $Matches[2], $patch) } @@ -6311,19 +6345,14 @@ _anvil-resolve-stable action="install": if (-not [string]::IsNullOrWhiteSpace($env:ANVIL_MSRV_TOOLCHAIN)) { return [pscustomobject]@{ Value = $env:ANVIL_MSRV_TOOLCHAIN - Source = 'mapped-msrv' Mapped = $true } } - return [pscustomobject]@{ Value = $msrv; Source = 'msrv'; Mapped = $false } + return [pscustomobject]@{ Value = $msrv; Mapped = $false } } function Install-Toolchain([string] $toolchain, [string] $label) { - $installed = rustup toolchain list - if ($LASTEXITCODE -ne 0) { - throw 'anvil: rustup toolchain list failed' - } - if (($installed -join "`n") -notmatch "(?m)^$([regex]::Escape($toolchain))(?:-|$)") { + if (-not (Test-ToolchainInstalled $toolchain)) { Write-Host "anvil: installing $label toolchain '$toolchain'" rustup toolchain install $toolchain --profile minimal if ($LASTEXITCODE -ne 0) { @@ -6361,10 +6390,6 @@ _anvil-resolve-stable action="install": exit 0 } - if ($action -ne 'install') { - throw "anvil: unsupported stable resolver action '$action'" - } - Assert-CompleteInternalToolchainConfiguration if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { & cargo --version *> $null @@ -6389,10 +6414,10 @@ _anvil-resolve-stable action="install": Install-Toolchain (Get-RootMsrv) 'stable' -# Provision the selected stable toolchain. Repository toolchain files are -# processed natively by rustup. This is the shared prerequisite for setup paths -# that execute stable Cargo/Rust or install Cargo tools; Just deduplicates it -# within each setup invocation. +# Setup paths that execute stable Cargo/Rust or install Cargo tools share this +# dependency, which Just deduplicates within one setup invocation. + +# Ensure the selected stable toolchain is available while preserving native repository toolchain-file behavior. [group("anvil-setup")] anvil-toolchain-stable-install: (_anvil-resolve-stable "install") @@ -7081,6 +7106,9 @@ if (-not (Test-Path -LiteralPath $manifestPath -PathType Leaf)) { throw "anvil: Cargo.toml not found at repository root '$RepoRoot'" } +# This bootstrap scan must choose Cargo before Cargo is available to parse the +# manifest. Keep its workspace.package-before-package precedence and accepted +# syntax synchronized with Get-RootMsrv in tools.just. $values = @{} $section = '' foreach ($line in (Get-Content -LiteralPath $manifestPath)) { diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap index 62c3e1860..f744aed41 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap @@ -1647,8 +1647,13 @@ anvil-miri-validate-prereqs: anvil-component-nightly-miri-validate-prereqs anvil anvil-msrv-test: anvil-msrv-test-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include affected) + $impactExit = $LASTEXITCODE + if ($impactExit -ne 0) { exit $impactExit } if ($include -eq '--skip') { Write-Host 'anvil-msrv-test: no affected packages; skipping'; exit 0 } - $toolchain = (& "{{ just_executable() }}" _anvil-resolve-stable msrv | Out-String).Trim() + $toolchainOutput = & "{{ just_executable() }}" _anvil-resolve-stable msrv | Out-String + $resolverExit = $LASTEXITCODE + if ($resolverExit -ne 0) { exit $resolverExit } + $toolchain = $toolchainOutput.Trim() if ([string]::IsNullOrWhiteSpace($toolchain)) { Write-Host 'anvil-msrv-test: no root MSRV declared; skipping' exit 0 @@ -1670,7 +1675,10 @@ anvil-msrv-test-setup installer="install": [group("anvil-setup")] [script("pwsh", "-NoProfile")] anvil-msrv-test-validate-prereqs: - $toolchain = (& "{{ just_executable() }}" _anvil-resolve-stable msrv | Out-String).Trim() + $toolchainOutput = & "{{ just_executable() }}" _anvil-resolve-stable msrv | Out-String + $resolverExit = $LASTEXITCODE + if ($resolverExit -ne 0) { exit $resolverExit } + $toolchain = $toolchainOutput.Trim() if ([string]::IsNullOrWhiteSpace($toolchain)) { exit 0 } & cargo "+$toolchain" --version if ($LASTEXITCODE -ne 0) { @@ -1907,6 +1915,7 @@ anvil-pr-title-validate-prereqs: anvil-tool-pwsh-validate-prereqs [script("pwsh", "-NoProfile")] _anvil-readme mode: anvil-readme-check-validate-prereqs $ErrorActionPreference = 'Stop' + $stableArgs = {{_anvil_stable_toolchain_args}} $action = '{{ mode }}' if ($action -notin @('generate', 'check')) { [Console]::Error.WriteLine("anvil-readme: invalid mode '$action'; expected 'generate' or 'check'") @@ -1925,7 +1934,7 @@ _anvil-readme mode: anvil-readme-check-validate-prereqs # Unscoped: a change to the workspace-level README template is a repo-level # input cargo-delta cannot map to a package, so this check always runs the # full eligible-crate set rather than an impact subset. - $metadataJson = & cargo {{_anvil_stable_toolchain_args}} metadata --no-deps --format-version 1 + $metadataJson = & cargo @stableArgs metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error "${prefix}: cargo metadata failed" exit $LASTEXITCODE @@ -1971,7 +1980,7 @@ _anvil-readme mode: anvil-readme-check-validate-prereqs $args = @('doc2readme', $target) if ($check) { $args += '--check' } if ($relTemplate) { $args += @('--template', $relTemplate) } - & cargo {{_anvil_stable_toolchain_args}} @args + & cargo @stableArgs @args if ($LASTEXITCODE -ne 0) { $hadFailure = $true } } finally { Pop-Location @@ -2042,6 +2051,7 @@ anvil-readme-check-validate-prereqs: anvil-tool-cargo-doc2readme-validate-prereq [script("pwsh", "-NoProfile")] anvil-semver-check: anvil-semver-check-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' + $stableArgs = {{_anvil_stable_toolchain_args}} $include = (& "{{ just_executable() }}" _anvil-impact-include affected) $commentFile = 'target/anvil/comments/semver.md' if ($include -eq '--skip') { @@ -2061,7 +2071,7 @@ anvil-semver-check: anvil-semver-check-validate-prereqs anvil-impact # null, `publish = false` as an empty array, and named-registry restrictions # as a nonempty array. Only the empty array is non-publishable. $libPkgs = @{} - $metadataJson = & cargo {{_anvil_stable_toolchain_args}} metadata --no-deps --format-version 1 + $metadataJson = & cargo @stableArgs metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-semver-check: cargo metadata failed' exit $LASTEXITCODE @@ -2139,7 +2149,7 @@ anvil-semver-check: anvil-semver-check-validate-prereqs anvil-impact } Write-Host "anvil-semver-check: $p (baseline $base)" - $outputLines = @(& cargo {{_anvil_stable_toolchain_args}} semver-checks --package $p --baseline-rev $base 2>&1) + $outputLines = @(& cargo @stableArgs semver-checks --package $p --baseline-rev $base 2>&1) $semverExit = $LASTEXITCODE $output = ($outputLines | Out-String) if ($semverExit -eq 100) { @@ -3670,20 +3680,19 @@ anvil-pr-runtime-analysis-validate-prereqs: \ # See https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/checks.md # pr-slow is the local umbrella for everything that takes more than ~30s -# per crate (tests, MSRV tests, stricter runtimes, mutation testing). It's -# split into four sub-recipes so individual concerns can be invoked locally -# without dragging the others along: +# per crate (tests, MSRV tests, stricter runtimes, mutation testing). Its +# groups can be invoked individually without invoking the other groups: # # pr-test: tests + coverage -# pr-msrv: unit and integration tests under the declared MSRV +# pr-msrv: all-target tests under the declared MSRV # pr-runtime-analysis: stricter-runtime correctness (miri, careful) # pr-mutants: mutation testing # -# Cloud workflows run the four sub-groups as independent parallel jobs. +# Cloud workflows run these groups as independent parallel jobs. # The umbrella runs them sequentially only when invoked locally: # # $ just anvil-pr-test # tests + coverage only -# $ just anvil-pr-msrv # MSRV unit and integration tests only +# $ just anvil-pr-msrv # MSRV all-target tests only # $ just anvil-pr-runtime-analysis # miri + careful only # $ just anvil-pr-mutants # mutants only @@ -5043,7 +5052,7 @@ anvil-tool-cargo-spellcheck-source-deps-check: # pwsh is installed via the platform's package manager. The validate recipes # are read-only and only surface a friendly error if a prerequisite is absent. -# Validate that rustc is available. +# Validate that rustc is available and workspace MSRVs are compatible. [group("anvil-setup")] [script("pwsh", "-NoProfile")] anvil-tool-rustc-validate-prereqs: @@ -5115,6 +5124,10 @@ _anvil-resolve-stable action="install": throw "anvil: Cargo.toml not found at repository root '$repoRoot'" } + # This bootstrap scan must choose Cargo before Cargo is available to + # parse the manifest. Keep its workspace.package-before-package + # precedence and accepted syntax synchronized with the selector in + # versions.just. $values = @{} $section = '' foreach ($line in (Get-Content -LiteralPath $manifestPath)) { @@ -5141,9 +5154,28 @@ _anvil-resolve-stable action="install": throw 'anvil: no selecting rust-toolchain(.toml) and no root [workspace.package] or [package] rust-version; declare an MSRV or select a toolchain explicitly' } + function Get-InstalledToolchains { + if (-not (Get-Command rustup -ErrorAction SilentlyContinue)) { + throw 'anvil: rustup not found. Install rustup from https://rustup.rs and ensure it is on PATH' + } + $installed = @(rustup toolchain list) + if ($LASTEXITCODE -ne 0) { + throw 'anvil: rustup toolchain list failed' + } + return $installed + } + + function Test-ToolchainInstalled([string] $toolchain) { + $installed = Get-InstalledToolchains + return (($installed -join "`n") -match "(?m)^$([regex]::Escape($toolchain))(?:-|$)") + } + function Assert-WorkspaceMsrvCompatibility { $manifestPath = Join-Path $repoRoot 'Cargo.toml' $rootMsrv = Get-RootMsrv + if (-not (Test-ToolchainInstalled $rootMsrv)) { + throw "anvil: root MSRV toolchain '$rootMsrv' is not installed; run 'just anvil-toolchain-stable-install' before validating prerequisites" + } $metadataText = (& cargo "+$rootMsrv" metadata --manifest-path $manifestPath --format-version 1 --no-deps 2>&1 | Out-String) if ($LASTEXITCODE -ne 0) { throw "anvil: cargo metadata failed while validating workspace MSRVs:`n$metadataText" @@ -5163,6 +5195,8 @@ _anvil-resolve-stable action="install": if ($value -notmatch '^\s*(\d+)\.(\d+)(?:\.(\d+))?\s*$') { throw "anvil: invalid resolved rust-version '$value'" } + # Cargo treats 1.x and 1.x.0 as the same floor, while + # System.Version sorts an unspecified build component differently. $patch = if ([string]::IsNullOrWhiteSpace($Matches[3])) { 0 } else { [int] $Matches[3] } return [version]::new([int] $Matches[1], [int] $Matches[2], $patch) } @@ -5195,19 +5229,14 @@ _anvil-resolve-stable action="install": if (-not [string]::IsNullOrWhiteSpace($env:ANVIL_MSRV_TOOLCHAIN)) { return [pscustomobject]@{ Value = $env:ANVIL_MSRV_TOOLCHAIN - Source = 'mapped-msrv' Mapped = $true } } - return [pscustomobject]@{ Value = $msrv; Source = 'msrv'; Mapped = $false } + return [pscustomobject]@{ Value = $msrv; Mapped = $false } } function Install-Toolchain([string] $toolchain, [string] $label) { - $installed = rustup toolchain list - if ($LASTEXITCODE -ne 0) { - throw 'anvil: rustup toolchain list failed' - } - if (($installed -join "`n") -notmatch "(?m)^$([regex]::Escape($toolchain))(?:-|$)") { + if (-not (Test-ToolchainInstalled $toolchain)) { Write-Host "anvil: installing $label toolchain '$toolchain'" rustup toolchain install $toolchain --profile minimal if ($LASTEXITCODE -ne 0) { @@ -5245,10 +5274,6 @@ _anvil-resolve-stable action="install": exit 0 } - if ($action -ne 'install') { - throw "anvil: unsupported stable resolver action '$action'" - } - Assert-CompleteInternalToolchainConfiguration if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { & cargo --version *> $null @@ -5273,10 +5298,10 @@ _anvil-resolve-stable action="install": Install-Toolchain (Get-RootMsrv) 'stable' -# Provision the selected stable toolchain. Repository toolchain files are -# processed natively by rustup. This is the shared prerequisite for setup paths -# that execute stable Cargo/Rust or install Cargo tools; Just deduplicates it -# within each setup invocation. +# Setup paths that execute stable Cargo/Rust or install Cargo tools share this +# dependency, which Just deduplicates within one setup invocation. + +# Ensure the selected stable toolchain is available while preserving native repository toolchain-file behavior. [group("anvil-setup")] anvil-toolchain-stable-install: (_anvil-resolve-stable "install") @@ -5965,6 +5990,9 @@ if (-not (Test-Path -LiteralPath $manifestPath -PathType Leaf)) { throw "anvil: Cargo.toml not found at repository root '$RepoRoot'" } +# This bootstrap scan must choose Cargo before Cargo is available to parse the +# manifest. Keep its workspace.package-before-package precedence and accepted +# syntax synchronized with Get-RootMsrv in tools.just. $values = @{} $section = '' foreach ($line in (Get-Content -LiteralPath $manifestPath)) { diff --git a/justfiles/anvil/checks/msrv-test.just b/justfiles/anvil/checks/msrv-test.just index 362e470c8..f95b38cff 100644 --- a/justfiles/anvil/checks/msrv-test.just +++ b/justfiles/anvil/checks/msrv-test.just @@ -11,8 +11,13 @@ anvil-msrv-test: anvil-msrv-test-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include affected) + $impactExit = $LASTEXITCODE + if ($impactExit -ne 0) { exit $impactExit } if ($include -eq '--skip') { Write-Host 'anvil-msrv-test: no affected packages; skipping'; exit 0 } - $toolchain = (& "{{ just_executable() }}" _anvil-resolve-stable msrv | Out-String).Trim() + $toolchainOutput = & "{{ just_executable() }}" _anvil-resolve-stable msrv | Out-String + $resolverExit = $LASTEXITCODE + if ($resolverExit -ne 0) { exit $resolverExit } + $toolchain = $toolchainOutput.Trim() if ([string]::IsNullOrWhiteSpace($toolchain)) { Write-Host 'anvil-msrv-test: no root MSRV declared; skipping' exit 0 @@ -34,7 +39,10 @@ anvil-msrv-test-setup installer="install": [group("anvil-setup")] [script("pwsh", "-NoProfile")] anvil-msrv-test-validate-prereqs: - $toolchain = (& "{{ just_executable() }}" _anvil-resolve-stable msrv | Out-String).Trim() + $toolchainOutput = & "{{ just_executable() }}" _anvil-resolve-stable msrv | Out-String + $resolverExit = $LASTEXITCODE + if ($resolverExit -ne 0) { exit $resolverExit } + $toolchain = $toolchainOutput.Trim() if ([string]::IsNullOrWhiteSpace($toolchain)) { exit 0 } & cargo "+$toolchain" --version if ($LASTEXITCODE -ne 0) { diff --git a/justfiles/anvil/checks/readme-check.just b/justfiles/anvil/checks/readme-check.just index 7eed9bbb1..7ae42342d 100644 --- a/justfiles/anvil/checks/readme-check.just +++ b/justfiles/anvil/checks/readme-check.just @@ -37,6 +37,7 @@ [script("pwsh", "-NoProfile")] _anvil-readme mode: anvil-readme-check-validate-prereqs $ErrorActionPreference = 'Stop' + $stableArgs = {{_anvil_stable_toolchain_args}} $action = '{{ mode }}' if ($action -notin @('generate', 'check')) { [Console]::Error.WriteLine("anvil-readme: invalid mode '$action'; expected 'generate' or 'check'") @@ -55,7 +56,7 @@ _anvil-readme mode: anvil-readme-check-validate-prereqs # Unscoped: a change to the workspace-level README template is a repo-level # input cargo-delta cannot map to a package, so this check always runs the # full eligible-crate set rather than an impact subset. - $metadataJson = & cargo {{_anvil_stable_toolchain_args}} metadata --no-deps --format-version 1 + $metadataJson = & cargo @stableArgs metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error "${prefix}: cargo metadata failed" exit $LASTEXITCODE @@ -101,7 +102,7 @@ _anvil-readme mode: anvil-readme-check-validate-prereqs $args = @('doc2readme', $target) if ($check) { $args += '--check' } if ($relTemplate) { $args += @('--template', $relTemplate) } - & cargo {{_anvil_stable_toolchain_args}} @args + & cargo @stableArgs @args if ($LASTEXITCODE -ne 0) { $hadFailure = $true } } finally { Pop-Location diff --git a/justfiles/anvil/checks/semver-check.just b/justfiles/anvil/checks/semver-check.just index 482fd7053..5dbd16128 100644 --- a/justfiles/anvil/checks/semver-check.just +++ b/justfiles/anvil/checks/semver-check.just @@ -49,6 +49,7 @@ [script("pwsh", "-NoProfile")] anvil-semver-check: anvil-semver-check-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' + $stableArgs = {{_anvil_stable_toolchain_args}} $include = (& "{{ just_executable() }}" _anvil-impact-include affected) $commentFile = 'target/anvil/comments/semver.md' if ($include -eq '--skip') { @@ -68,7 +69,7 @@ anvil-semver-check: anvil-semver-check-validate-prereqs anvil-impact # null, `publish = false` as an empty array, and named-registry restrictions # as a nonempty array. Only the empty array is non-publishable. $libPkgs = @{} - $metadataJson = & cargo {{_anvil_stable_toolchain_args}} metadata --no-deps --format-version 1 + $metadataJson = & cargo @stableArgs metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-semver-check: cargo metadata failed' exit $LASTEXITCODE @@ -146,7 +147,7 @@ anvil-semver-check: anvil-semver-check-validate-prereqs anvil-impact } Write-Host "anvil-semver-check: $p (baseline $base)" - $outputLines = @(& cargo {{_anvil_stable_toolchain_args}} semver-checks --package $p --baseline-rev $base 2>&1) + $outputLines = @(& cargo @stableArgs semver-checks --package $p --baseline-rev $base 2>&1) $semverExit = $LASTEXITCODE $output = ($outputLines | Out-String) if ($semverExit -eq 100) { diff --git a/justfiles/anvil/groups/pr-slow.just b/justfiles/anvil/groups/pr-slow.just index 3ac2c01f5..ddce5d420 100644 --- a/justfiles/anvil/groups/pr-slow.just +++ b/justfiles/anvil/groups/pr-slow.just @@ -7,20 +7,19 @@ # See https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/checks.md # pr-slow is the local umbrella for everything that takes more than ~30s -# per crate (tests, MSRV tests, stricter runtimes, mutation testing). It's -# split into four sub-recipes so individual concerns can be invoked locally -# without dragging the others along: +# per crate (tests, MSRV tests, stricter runtimes, mutation testing). Its +# groups can be invoked individually without invoking the other groups: # # pr-test: tests + coverage -# pr-msrv: unit and integration tests under the declared MSRV +# pr-msrv: all-target tests under the declared MSRV # pr-runtime-analysis: stricter-runtime correctness (miri, careful) # pr-mutants: mutation testing # -# Cloud workflows run the four sub-groups as independent parallel jobs. +# Cloud workflows run these groups as independent parallel jobs. # The umbrella runs them sequentially only when invoked locally: # # $ just anvil-pr-test # tests + coverage only -# $ just anvil-pr-msrv # MSRV unit and integration tests only +# $ just anvil-pr-msrv # MSRV all-target tests only # $ just anvil-pr-runtime-analysis # miri + careful only # $ just anvil-pr-mutants # mutants only diff --git a/justfiles/anvil/tools.just b/justfiles/anvil/tools.just index 6c78928fb..beec3a4f2 100644 --- a/justfiles/anvil/tools.just +++ b/justfiles/anvil/tools.just @@ -135,7 +135,7 @@ anvil-tool-cargo-spellcheck-source-deps-check: # pwsh is installed via the platform's package manager. The validate recipes # are read-only and only surface a friendly error if a prerequisite is absent. -# Validate that rustc is available. +# Validate that rustc is available and workspace MSRVs are compatible. [group("anvil-setup")] [script("pwsh", "-NoProfile")] anvil-tool-rustc-validate-prereqs: @@ -207,6 +207,10 @@ _anvil-resolve-stable action="install": throw "anvil: Cargo.toml not found at repository root '$repoRoot'" } + # This bootstrap scan must choose Cargo before Cargo is available to + # parse the manifest. Keep its workspace.package-before-package + # precedence and accepted syntax synchronized with the selector in + # versions.just. $values = @{} $section = '' foreach ($line in (Get-Content -LiteralPath $manifestPath)) { @@ -233,9 +237,28 @@ _anvil-resolve-stable action="install": throw 'anvil: no selecting rust-toolchain(.toml) and no root [workspace.package] or [package] rust-version; declare an MSRV or select a toolchain explicitly' } + function Get-InstalledToolchains { + if (-not (Get-Command rustup -ErrorAction SilentlyContinue)) { + throw 'anvil: rustup not found. Install rustup from https://rustup.rs and ensure it is on PATH' + } + $installed = @(rustup toolchain list) + if ($LASTEXITCODE -ne 0) { + throw 'anvil: rustup toolchain list failed' + } + return $installed + } + + function Test-ToolchainInstalled([string] $toolchain) { + $installed = Get-InstalledToolchains + return (($installed -join "`n") -match "(?m)^$([regex]::Escape($toolchain))(?:-|$)") + } + function Assert-WorkspaceMsrvCompatibility { $manifestPath = Join-Path $repoRoot 'Cargo.toml' $rootMsrv = Get-RootMsrv + if (-not (Test-ToolchainInstalled $rootMsrv)) { + throw "anvil: root MSRV toolchain '$rootMsrv' is not installed; run 'just anvil-toolchain-stable-install' before validating prerequisites" + } $metadataText = (& cargo "+$rootMsrv" metadata --manifest-path $manifestPath --format-version 1 --no-deps 2>&1 | Out-String) if ($LASTEXITCODE -ne 0) { throw "anvil: cargo metadata failed while validating workspace MSRVs:`n$metadataText" @@ -255,6 +278,8 @@ _anvil-resolve-stable action="install": if ($value -notmatch '^\s*(\d+)\.(\d+)(?:\.(\d+))?\s*$') { throw "anvil: invalid resolved rust-version '$value'" } + # Cargo treats 1.x and 1.x.0 as the same floor, while + # System.Version sorts an unspecified build component differently. $patch = if ([string]::IsNullOrWhiteSpace($Matches[3])) { 0 } else { [int] $Matches[3] } return [version]::new([int] $Matches[1], [int] $Matches[2], $patch) } @@ -287,19 +312,14 @@ _anvil-resolve-stable action="install": if (-not [string]::IsNullOrWhiteSpace($env:ANVIL_MSRV_TOOLCHAIN)) { return [pscustomobject]@{ Value = $env:ANVIL_MSRV_TOOLCHAIN - Source = 'mapped-msrv' Mapped = $true } } - return [pscustomobject]@{ Value = $msrv; Source = 'msrv'; Mapped = $false } + return [pscustomobject]@{ Value = $msrv; Mapped = $false } } function Install-Toolchain([string] $toolchain, [string] $label) { - $installed = rustup toolchain list - if ($LASTEXITCODE -ne 0) { - throw 'anvil: rustup toolchain list failed' - } - if (($installed -join "`n") -notmatch "(?m)^$([regex]::Escape($toolchain))(?:-|$)") { + if (-not (Test-ToolchainInstalled $toolchain)) { Write-Host "anvil: installing $label toolchain '$toolchain'" rustup toolchain install $toolchain --profile minimal if ($LASTEXITCODE -ne 0) { @@ -337,10 +357,6 @@ _anvil-resolve-stable action="install": exit 0 } - if ($action -ne 'install') { - throw "anvil: unsupported stable resolver action '$action'" - } - Assert-CompleteInternalToolchainConfiguration if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { & cargo --version *> $null @@ -365,10 +381,10 @@ _anvil-resolve-stable action="install": Install-Toolchain (Get-RootMsrv) 'stable' -# Provision the selected stable toolchain. Repository toolchain files are -# processed natively by rustup. This is the shared prerequisite for setup paths -# that execute stable Cargo/Rust or install Cargo tools; Just deduplicates it -# within each setup invocation. +# Setup paths that execute stable Cargo/Rust or install Cargo tools share this +# dependency, which Just deduplicates within one setup invocation. + +# Ensure the selected stable toolchain is available while preserving native repository toolchain-file behavior. [group("anvil-setup")] anvil-toolchain-stable-install: (_anvil-resolve-stable "install") diff --git a/justfiles/anvil/versions.just b/justfiles/anvil/versions.just index c81fb01ac..f330945cf 100644 --- a/justfiles/anvil/versions.just +++ b/justfiles/anvil/versions.just @@ -59,6 +59,9 @@ if (-not (Test-Path -LiteralPath $manifestPath -PathType Leaf)) { throw "anvil: Cargo.toml not found at repository root '$RepoRoot'" } +# This bootstrap scan must choose Cargo before Cargo is available to parse the +# manifest. Keep its workspace.package-before-package precedence and accepted +# syntax synchronized with Get-RootMsrv in tools.just. $values = @{} $section = '' foreach ($line in (Get-Content -LiteralPath $manifestPath)) { From 4ef6a09ebdc1813f2606fdc186879b4b78cb7e21 Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Tue, 1 Sep 2026 12:55:12 +0200 Subject: [PATCH 27/32] Fix missing-rustup test on Linux Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: f7996e6f-5c20-4d7a-b2f7-690af95e557e --- crates/cargo-anvil/src/anvil/artifacts/justfile.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/crates/cargo-anvil/src/anvil/artifacts/justfile.rs b/crates/cargo-anvil/src/anvil/artifacts/justfile.rs index 7d5a4e7af..a0d7ea301 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/justfile.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/justfile.rs @@ -870,7 +870,7 @@ mod tests { mod stable_toolchain_resolver_tests { #[cfg(unix)] use std::os::unix::fs::PermissionsExt; - use std::path::Path; + use std::path::{Path, PathBuf}; use std::process::{Command, Output}; use std::{env, fs}; @@ -928,8 +928,16 @@ mod tests { Command::new("just").arg("--version").output().is_ok() && Command::new("pwsh").arg("--version").output().is_ok() } + fn tool_path(name: &str) -> Option { + let executable = format!("{name}{}", env::consts::EXE_SUFFIX); + env::split_paths(&env::var_os("PATH").unwrap_or_default()) + .map(|directory| directory.join(&executable)) + .find(|candidate| candidate.is_file()) + } + fn command(root: &Path) -> Command { - let mut command = Command::new("just"); + let just = tool_path("just").expect("resolver tests call command only after tools_available confirms Just is on PATH"); + let mut command = Command::new(just); command .args(["--justfile"]) .arg(root.join("Justfile")) From 1f4643189fd0f9f5a4d935cbda468916d5cf3c0c Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Tue, 1 Sep 2026 13:08:34 +0200 Subject: [PATCH 28/32] Correct installer helper diagnostic Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: f7996e6f-5c20-4d7a-b2f7-690af95e557e --- .anvil.lock | 4 ++-- crates/cargo-anvil/templates/justfiles/anvil/tools.just | 2 +- .../cargo-anvil/tests/snapshots/snapshots__ado_backend.snap | 2 +- .../tests/snapshots/snapshots__github_backend.snap | 2 +- crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap | 2 +- justfiles/anvil/tools.just | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.anvil.lock b/.anvil.lock index 1b42d4591..89767c8f2 100644 --- a/.anvil.lock +++ b/.anvil.lock @@ -1,7 +1,7 @@ version = 1 tool = "anvil" tool_version = "0.6.0" -catalog_checksum = "sha256:7382ec5160cdd18e1ac7430ec906473bb730bff8d858a0ea21a632196626281e" +catalog_checksum = "sha256:46491075d60cc318a3c0326eac93849323fe94347ea0987cae8cfff715c993c3" [[file]] path = ".anvil/container/Dockerfile.dockerignore" @@ -229,7 +229,7 @@ checksum = "sha256:00453a12cbb34811ee6a2c083dade5f6198575e3b0610f49e4743366326cd [[file]] path = "justfiles/anvil/tools.just" -checksum = "sha256:05649a470de4226b503d541a5f6ea35a8f3075de6f4e7349075408a7efb8c036" +checksum = "sha256:131db48c69afd2e2a360f735c5638b38683b11b21d3c73f2611f7bccef4d4234" [[file]] path = "justfiles/anvil/versions.just" diff --git a/crates/cargo-anvil/templates/justfiles/anvil/tools.just b/crates/cargo-anvil/templates/justfiles/anvil/tools.just index beec3a4f2..c05bad9b0 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/tools.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/tools.just @@ -406,7 +406,7 @@ _install-tool-core name version installer source_prereq="": $sourcePrereq = '{{source_prereq}}' if ($installer -ne 'install' -and $installer -ne 'binstall') { - Write-Error "_install-tool: unknown installer '$installer' (expected 'install' or 'binstall')" + Write-Error "_install-tool-core: unknown installer '$installer' (expected 'install' or 'binstall')" exit 2 } diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap index bd94ae970..9b74b6418 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap @@ -6453,7 +6453,7 @@ _install-tool-core name version installer source_prereq="": $sourcePrereq = '{{source_prereq}}' if ($installer -ne 'install' -and $installer -ne 'binstall') { - Write-Error "_install-tool: unknown installer '$installer' (expected 'install' or 'binstall')" + Write-Error "_install-tool-core: unknown installer '$installer' (expected 'install' or 'binstall')" exit 2 } diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap index 715f2e3c5..fe1a4dedb 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap @@ -6439,7 +6439,7 @@ _install-tool-core name version installer source_prereq="": $sourcePrereq = '{{source_prereq}}' if ($installer -ne 'install' -and $installer -ne 'binstall') { - Write-Error "_install-tool: unknown installer '$installer' (expected 'install' or 'binstall')" + Write-Error "_install-tool-core: unknown installer '$installer' (expected 'install' or 'binstall')" exit 2 } diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap index f744aed41..ff4197f5d 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap @@ -5323,7 +5323,7 @@ _install-tool-core name version installer source_prereq="": $sourcePrereq = '{{source_prereq}}' if ($installer -ne 'install' -and $installer -ne 'binstall') { - Write-Error "_install-tool: unknown installer '$installer' (expected 'install' or 'binstall')" + Write-Error "_install-tool-core: unknown installer '$installer' (expected 'install' or 'binstall')" exit 2 } diff --git a/justfiles/anvil/tools.just b/justfiles/anvil/tools.just index beec3a4f2..c05bad9b0 100644 --- a/justfiles/anvil/tools.just +++ b/justfiles/anvil/tools.just @@ -406,7 +406,7 @@ _install-tool-core name version installer source_prereq="": $sourcePrereq = '{{source_prereq}}' if ($installer -ne 'install' -and $installer -ne 'binstall') { - Write-Error "_install-tool: unknown installer '$installer' (expected 'install' or 'binstall')" + Write-Error "_install-tool-core: unknown installer '$installer' (expected 'install' or 'binstall')" exit 2 } From a6ab86d7e068455fac1b03adb909fca4c98694bc Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Tue, 1 Sep 2026 14:07:27 +0200 Subject: [PATCH 29/32] Keep tool validation read-only Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: f7996e6f-5c20-4d7a-b2f7-690af95e557e --- .anvil.lock | 4 ++-- .../src/anvil/artifacts/justfile.rs | 22 +++++++++++++++++++ .../templates/justfiles/anvil/tools.just | 9 ++++++-- .../snapshots/snapshots__ado_backend.snap | 9 ++++++-- .../snapshots/snapshots__github_backend.snap | 9 ++++++-- .../snapshots/snapshots__local_only.snap | 9 ++++++-- justfiles/anvil/tools.just | 9 ++++++-- 7 files changed, 59 insertions(+), 12 deletions(-) diff --git a/.anvil.lock b/.anvil.lock index 89767c8f2..d9b0cf956 100644 --- a/.anvil.lock +++ b/.anvil.lock @@ -1,7 +1,7 @@ version = 1 tool = "anvil" tool_version = "0.6.0" -catalog_checksum = "sha256:46491075d60cc318a3c0326eac93849323fe94347ea0987cae8cfff715c993c3" +catalog_checksum = "sha256:c3e52edf9ec4d5d0a4b7b944b53fa630177fccbd4408346f80b397d98c99658b" [[file]] path = ".anvil/container/Dockerfile.dockerignore" @@ -229,7 +229,7 @@ checksum = "sha256:00453a12cbb34811ee6a2c083dade5f6198575e3b0610f49e4743366326cd [[file]] path = "justfiles/anvil/tools.just" -checksum = "sha256:131db48c69afd2e2a360f735c5638b38683b11b21d3c73f2611f7bccef4d4234" +checksum = "sha256:b7fd4153a07850a50e72466e939b3de72c660f79888142102df7d399beaab978" [[file]] path = "justfiles/anvil/versions.just" diff --git a/crates/cargo-anvil/src/anvil/artifacts/justfile.rs b/crates/cargo-anvil/src/anvil/artifacts/justfile.rs index a0d7ea301..3b79573d8 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/justfile.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/justfile.rs @@ -765,6 +765,10 @@ mod tests { !checks.contains("validate-prereqs: anvil-toolchain-stable-install"), "validate-prereqs recipes must never install stable" ); + assert!( + TOOLS_JUST.contains("_check-tool name version: anvil-tool-rustc-validate-prereqs"), + "Cargo-tool validation must confirm the stable selection without provisioning it" + ); let groups = all_group_bodies(); assert!( @@ -1451,6 +1455,24 @@ mod tests { ); assert!(cargo_calls.is_empty(), "public MSRV availability is checked through rustup"); + let (output, rustup_calls, cargo_calls) = run_install( + temp.path(), + None, + "nightly-2026-01-01-x86_64-pc-windows-msvc\n1.93.0-x86_64-pc-windows-msvc", + 0, + ); + assert!( + output.status.success(), + "a patch-normalized installed MSRV must satisfy an unpatched selection: {}", + normalized_diagnostic(&output) + ); + assert!(rustup_calls.contains("toolchain list")); + assert!( + !rustup_calls.contains("toolchain install"), + "an equivalent patch-normalized MSRV must not be reinstalled" + ); + assert!(cargo_calls.is_empty(), "public MSRV availability is checked through rustup"); + let (output, rustup_calls, cargo_calls) = run_install(temp.path(), Some("ms-prod-1.93"), "", 0); assert!( output.status.success(), diff --git a/crates/cargo-anvil/templates/justfiles/anvil/tools.just b/crates/cargo-anvil/templates/justfiles/anvil/tools.just index c05bad9b0..822b07c91 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/tools.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/tools.just @@ -250,7 +250,12 @@ _anvil-resolve-stable action="install": function Test-ToolchainInstalled([string] $toolchain) { $installed = Get-InstalledToolchains - return (($installed -join "`n") -match "(?m)^$([regex]::Escape($toolchain))(?:-|$)") + $pattern = if ($toolchain -match '^(\d+)\.(\d+)$') { + "(?m)^$([regex]::Escape($Matches[1]))\.$([regex]::Escape($Matches[2]))(?:\.0)?(?:-|$)" + } else { + "(?m)^$([regex]::Escape($toolchain))(?:-|$)" + } + return (($installed -join "`n") -match $pattern) } function Assert-WorkspaceMsrvCompatibility { @@ -470,7 +475,7 @@ _install-tool name version installer source_prereq="": anvil-toolchain-stable-in # _check-tool: verify a cargo subcommand is installed at or above the # pinned version. Errors with an install hint on missing or too-old. [script("pwsh", "-NoProfile")] -_check-tool name version: +_check-tool name version: anvil-tool-rustc-validate-prereqs $ErrorActionPreference = 'Stop' $name = '{{name}}' $version = '{{version}}' diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap index 9b74b6418..373fa8722 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap @@ -6297,7 +6297,12 @@ _anvil-resolve-stable action="install": function Test-ToolchainInstalled([string] $toolchain) { $installed = Get-InstalledToolchains - return (($installed -join "`n") -match "(?m)^$([regex]::Escape($toolchain))(?:-|$)") + $pattern = if ($toolchain -match '^(\d+)\.(\d+)$') { + "(?m)^$([regex]::Escape($Matches[1]))\.$([regex]::Escape($Matches[2]))(?:\.0)?(?:-|$)" + } else { + "(?m)^$([regex]::Escape($toolchain))(?:-|$)" + } + return (($installed -join "`n") -match $pattern) } function Assert-WorkspaceMsrvCompatibility { @@ -6517,7 +6522,7 @@ _install-tool name version installer source_prereq="": anvil-toolchain-stable-in # _check-tool: verify a cargo subcommand is installed at or above the # pinned version. Errors with an install hint on missing or too-old. [script("pwsh", "-NoProfile")] -_check-tool name version: +_check-tool name version: anvil-tool-rustc-validate-prereqs $ErrorActionPreference = 'Stop' $name = '{{name}}' $version = '{{version}}' diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap index fe1a4dedb..f92e66c61 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap @@ -6283,7 +6283,12 @@ _anvil-resolve-stable action="install": function Test-ToolchainInstalled([string] $toolchain) { $installed = Get-InstalledToolchains - return (($installed -join "`n") -match "(?m)^$([regex]::Escape($toolchain))(?:-|$)") + $pattern = if ($toolchain -match '^(\d+)\.(\d+)$') { + "(?m)^$([regex]::Escape($Matches[1]))\.$([regex]::Escape($Matches[2]))(?:\.0)?(?:-|$)" + } else { + "(?m)^$([regex]::Escape($toolchain))(?:-|$)" + } + return (($installed -join "`n") -match $pattern) } function Assert-WorkspaceMsrvCompatibility { @@ -6503,7 +6508,7 @@ _install-tool name version installer source_prereq="": anvil-toolchain-stable-in # _check-tool: verify a cargo subcommand is installed at or above the # pinned version. Errors with an install hint on missing or too-old. [script("pwsh", "-NoProfile")] -_check-tool name version: +_check-tool name version: anvil-tool-rustc-validate-prereqs $ErrorActionPreference = 'Stop' $name = '{{name}}' $version = '{{version}}' diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap index ff4197f5d..8fa0032c7 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap @@ -5167,7 +5167,12 @@ _anvil-resolve-stable action="install": function Test-ToolchainInstalled([string] $toolchain) { $installed = Get-InstalledToolchains - return (($installed -join "`n") -match "(?m)^$([regex]::Escape($toolchain))(?:-|$)") + $pattern = if ($toolchain -match '^(\d+)\.(\d+)$') { + "(?m)^$([regex]::Escape($Matches[1]))\.$([regex]::Escape($Matches[2]))(?:\.0)?(?:-|$)" + } else { + "(?m)^$([regex]::Escape($toolchain))(?:-|$)" + } + return (($installed -join "`n") -match $pattern) } function Assert-WorkspaceMsrvCompatibility { @@ -5387,7 +5392,7 @@ _install-tool name version installer source_prereq="": anvil-toolchain-stable-in # _check-tool: verify a cargo subcommand is installed at or above the # pinned version. Errors with an install hint on missing or too-old. [script("pwsh", "-NoProfile")] -_check-tool name version: +_check-tool name version: anvil-tool-rustc-validate-prereqs $ErrorActionPreference = 'Stop' $name = '{{name}}' $version = '{{version}}' diff --git a/justfiles/anvil/tools.just b/justfiles/anvil/tools.just index c05bad9b0..822b07c91 100644 --- a/justfiles/anvil/tools.just +++ b/justfiles/anvil/tools.just @@ -250,7 +250,12 @@ _anvil-resolve-stable action="install": function Test-ToolchainInstalled([string] $toolchain) { $installed = Get-InstalledToolchains - return (($installed -join "`n") -match "(?m)^$([regex]::Escape($toolchain))(?:-|$)") + $pattern = if ($toolchain -match '^(\d+)\.(\d+)$') { + "(?m)^$([regex]::Escape($Matches[1]))\.$([regex]::Escape($Matches[2]))(?:\.0)?(?:-|$)" + } else { + "(?m)^$([regex]::Escape($toolchain))(?:-|$)" + } + return (($installed -join "`n") -match $pattern) } function Assert-WorkspaceMsrvCompatibility { @@ -470,7 +475,7 @@ _install-tool name version installer source_prereq="": anvil-toolchain-stable-in # _check-tool: verify a cargo subcommand is installed at or above the # pinned version. Errors with an install hint on missing or too-old. [script("pwsh", "-NoProfile")] -_check-tool name version: +_check-tool name version: anvil-tool-rustc-validate-prereqs $ErrorActionPreference = 'Stop' $name = '{{name}}' $version = '{{version}}' From 7f44cf8fd47d08ed5822e4fe32768ee94b21416f Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Tue, 1 Sep 2026 14:15:55 +0200 Subject: [PATCH 30/32] Narrow read-only tool validation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: f7996e6f-5c20-4d7a-b2f7-690af95e557e --- .anvil.lock | 4 ++-- .../cargo-anvil/src/anvil/artifacts/justfile.rs | 4 ++-- .../templates/justfiles/anvil/tools.just | 17 ++++++++++++++--- .../tests/snapshots/snapshots__ado_backend.snap | 17 ++++++++++++++--- .../snapshots/snapshots__github_backend.snap | 17 ++++++++++++++--- .../tests/snapshots/snapshots__local_only.snap | 17 ++++++++++++++--- justfiles/anvil/tools.just | 17 ++++++++++++++--- 7 files changed, 74 insertions(+), 19 deletions(-) diff --git a/.anvil.lock b/.anvil.lock index d9b0cf956..c47c9b419 100644 --- a/.anvil.lock +++ b/.anvil.lock @@ -1,7 +1,7 @@ version = 1 tool = "anvil" tool_version = "0.6.0" -catalog_checksum = "sha256:c3e52edf9ec4d5d0a4b7b944b53fa630177fccbd4408346f80b397d98c99658b" +catalog_checksum = "sha256:a9ce8db5a4ef9eefe2e68d3cd031939ec52d0255f972abbf35f2a20d658cce42" [[file]] path = ".anvil/container/Dockerfile.dockerignore" @@ -229,7 +229,7 @@ checksum = "sha256:00453a12cbb34811ee6a2c083dade5f6198575e3b0610f49e4743366326cd [[file]] path = "justfiles/anvil/tools.just" -checksum = "sha256:b7fd4153a07850a50e72466e939b3de72c660f79888142102df7d399beaab978" +checksum = "sha256:2cd024965be6eeff05c6f461da820eba3f63a25ca9ba51442b7c866a3e100ccc" [[file]] path = "justfiles/anvil/versions.just" diff --git a/crates/cargo-anvil/src/anvil/artifacts/justfile.rs b/crates/cargo-anvil/src/anvil/artifacts/justfile.rs index 3b79573d8..a25783b38 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/justfile.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/justfile.rs @@ -766,8 +766,8 @@ mod tests { "validate-prereqs recipes must never install stable" ); assert!( - TOOLS_JUST.contains("_check-tool name version: anvil-tool-rustc-validate-prereqs"), - "Cargo-tool validation must confirm the stable selection without provisioning it" + TOOLS_JUST.contains("$env:RUSTUP_AUTO_INSTALL = '0'"), + "Cargo-tool validation must disable rustup auto-installation" ); let groups = all_group_bodies(); diff --git a/crates/cargo-anvil/templates/justfiles/anvil/tools.just b/crates/cargo-anvil/templates/justfiles/anvil/tools.just index 822b07c91..554e3fb98 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/tools.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/tools.just @@ -475,15 +475,26 @@ _install-tool name version installer source_prereq="": anvil-toolchain-stable-in # _check-tool: verify a cargo subcommand is installed at or above the # pinned version. Errors with an install hint on missing or too-old. [script("pwsh", "-NoProfile")] -_check-tool name version: anvil-tool-rustc-validate-prereqs +_check-tool name version: $ErrorActionPreference = 'Stop' $name = '{{name}}' $version = '{{version}}' $installed = $null $pattern = '^' + [regex]::Escape($name) + ' v(\S+):' - foreach ($line in (& cargo {{_anvil_stable_toolchain_args}} install --list 2>$null)) { - if ($line -match $pattern) { $installed = $Matches[1]; break } + $previousAutoInstall = $env:RUSTUP_AUTO_INSTALL + try { + # Validation must not let the rustup proxy provision a missing selection. + $env:RUSTUP_AUTO_INSTALL = '0' + foreach ($line in (& cargo {{_anvil_stable_toolchain_args}} install --list 2>$null)) { + if ($line -match $pattern) { $installed = $Matches[1]; break } + } + } finally { + if ($null -eq $previousAutoInstall) { + Remove-Item Env:RUSTUP_AUTO_INSTALL -ErrorAction SilentlyContinue + } else { + $env:RUSTUP_AUTO_INSTALL = $previousAutoInstall + } } if (-not $installed) { Write-Error "anvil: required tool '$name' not found. Install: cargo install --locked --version =$version $name" diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap index 373fa8722..fb5ae2064 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap @@ -6522,15 +6522,26 @@ _install-tool name version installer source_prereq="": anvil-toolchain-stable-in # _check-tool: verify a cargo subcommand is installed at or above the # pinned version. Errors with an install hint on missing or too-old. [script("pwsh", "-NoProfile")] -_check-tool name version: anvil-tool-rustc-validate-prereqs +_check-tool name version: $ErrorActionPreference = 'Stop' $name = '{{name}}' $version = '{{version}}' $installed = $null $pattern = '^' + [regex]::Escape($name) + ' v(\S+):' - foreach ($line in (& cargo {{_anvil_stable_toolchain_args}} install --list 2>$null)) { - if ($line -match $pattern) { $installed = $Matches[1]; break } + $previousAutoInstall = $env:RUSTUP_AUTO_INSTALL + try { + # Validation must not let the rustup proxy provision a missing selection. + $env:RUSTUP_AUTO_INSTALL = '0' + foreach ($line in (& cargo {{_anvil_stable_toolchain_args}} install --list 2>$null)) { + if ($line -match $pattern) { $installed = $Matches[1]; break } + } + } finally { + if ($null -eq $previousAutoInstall) { + Remove-Item Env:RUSTUP_AUTO_INSTALL -ErrorAction SilentlyContinue + } else { + $env:RUSTUP_AUTO_INSTALL = $previousAutoInstall + } } if (-not $installed) { Write-Error "anvil: required tool '$name' not found. Install: cargo install --locked --version =$version $name" diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap index f92e66c61..8754d3173 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap @@ -6508,15 +6508,26 @@ _install-tool name version installer source_prereq="": anvil-toolchain-stable-in # _check-tool: verify a cargo subcommand is installed at or above the # pinned version. Errors with an install hint on missing or too-old. [script("pwsh", "-NoProfile")] -_check-tool name version: anvil-tool-rustc-validate-prereqs +_check-tool name version: $ErrorActionPreference = 'Stop' $name = '{{name}}' $version = '{{version}}' $installed = $null $pattern = '^' + [regex]::Escape($name) + ' v(\S+):' - foreach ($line in (& cargo {{_anvil_stable_toolchain_args}} install --list 2>$null)) { - if ($line -match $pattern) { $installed = $Matches[1]; break } + $previousAutoInstall = $env:RUSTUP_AUTO_INSTALL + try { + # Validation must not let the rustup proxy provision a missing selection. + $env:RUSTUP_AUTO_INSTALL = '0' + foreach ($line in (& cargo {{_anvil_stable_toolchain_args}} install --list 2>$null)) { + if ($line -match $pattern) { $installed = $Matches[1]; break } + } + } finally { + if ($null -eq $previousAutoInstall) { + Remove-Item Env:RUSTUP_AUTO_INSTALL -ErrorAction SilentlyContinue + } else { + $env:RUSTUP_AUTO_INSTALL = $previousAutoInstall + } } if (-not $installed) { Write-Error "anvil: required tool '$name' not found. Install: cargo install --locked --version =$version $name" diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap index 8fa0032c7..0a5e9dff6 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap @@ -5392,15 +5392,26 @@ _install-tool name version installer source_prereq="": anvil-toolchain-stable-in # _check-tool: verify a cargo subcommand is installed at or above the # pinned version. Errors with an install hint on missing or too-old. [script("pwsh", "-NoProfile")] -_check-tool name version: anvil-tool-rustc-validate-prereqs +_check-tool name version: $ErrorActionPreference = 'Stop' $name = '{{name}}' $version = '{{version}}' $installed = $null $pattern = '^' + [regex]::Escape($name) + ' v(\S+):' - foreach ($line in (& cargo {{_anvil_stable_toolchain_args}} install --list 2>$null)) { - if ($line -match $pattern) { $installed = $Matches[1]; break } + $previousAutoInstall = $env:RUSTUP_AUTO_INSTALL + try { + # Validation must not let the rustup proxy provision a missing selection. + $env:RUSTUP_AUTO_INSTALL = '0' + foreach ($line in (& cargo {{_anvil_stable_toolchain_args}} install --list 2>$null)) { + if ($line -match $pattern) { $installed = $Matches[1]; break } + } + } finally { + if ($null -eq $previousAutoInstall) { + Remove-Item Env:RUSTUP_AUTO_INSTALL -ErrorAction SilentlyContinue + } else { + $env:RUSTUP_AUTO_INSTALL = $previousAutoInstall + } } if (-not $installed) { Write-Error "anvil: required tool '$name' not found. Install: cargo install --locked --version =$version $name" diff --git a/justfiles/anvil/tools.just b/justfiles/anvil/tools.just index 822b07c91..554e3fb98 100644 --- a/justfiles/anvil/tools.just +++ b/justfiles/anvil/tools.just @@ -475,15 +475,26 @@ _install-tool name version installer source_prereq="": anvil-toolchain-stable-in # _check-tool: verify a cargo subcommand is installed at or above the # pinned version. Errors with an install hint on missing or too-old. [script("pwsh", "-NoProfile")] -_check-tool name version: anvil-tool-rustc-validate-prereqs +_check-tool name version: $ErrorActionPreference = 'Stop' $name = '{{name}}' $version = '{{version}}' $installed = $null $pattern = '^' + [regex]::Escape($name) + ' v(\S+):' - foreach ($line in (& cargo {{_anvil_stable_toolchain_args}} install --list 2>$null)) { - if ($line -match $pattern) { $installed = $Matches[1]; break } + $previousAutoInstall = $env:RUSTUP_AUTO_INSTALL + try { + # Validation must not let the rustup proxy provision a missing selection. + $env:RUSTUP_AUTO_INSTALL = '0' + foreach ($line in (& cargo {{_anvil_stable_toolchain_args}} install --list 2>$null)) { + if ($line -match $pattern) { $installed = $Matches[1]; break } + } + } finally { + if ($null -eq $previousAutoInstall) { + Remove-Item Env:RUSTUP_AUTO_INSTALL -ErrorAction SilentlyContinue + } else { + $env:RUSTUP_AUTO_INSTALL = $previousAutoInstall + } } if (-not $installed) { Write-Error "anvil: required tool '$name' not found. Install: cargo install --locked --version =$version $name" From e2d7d24500eb6b40b3f35c6449f3bb4c5576c4c9 Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Tue, 1 Sep 2026 14:18:00 +0200 Subject: [PATCH 31/32] Require rustup for native selections Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: f7996e6f-5c20-4d7a-b2f7-690af95e557e --- .anvil.lock | 4 ++-- .../src/anvil/artifacts/justfile.rs | 23 +++++++++++++++++++ .../templates/justfiles/anvil/tools.just | 7 +++++- .../snapshots/snapshots__ado_backend.snap | 7 +++++- .../snapshots/snapshots__github_backend.snap | 7 +++++- .../snapshots/snapshots__local_only.snap | 7 +++++- justfiles/anvil/tools.just | 7 +++++- 7 files changed, 55 insertions(+), 7 deletions(-) diff --git a/.anvil.lock b/.anvil.lock index c47c9b419..21f7d863b 100644 --- a/.anvil.lock +++ b/.anvil.lock @@ -1,7 +1,7 @@ version = 1 tool = "anvil" tool_version = "0.6.0" -catalog_checksum = "sha256:a9ce8db5a4ef9eefe2e68d3cd031939ec52d0255f972abbf35f2a20d658cce42" +catalog_checksum = "sha256:c9aaa0e99670b91553957b4438043e07bef190eb6402b6f36bfa2a6c980589a1" [[file]] path = ".anvil/container/Dockerfile.dockerignore" @@ -229,7 +229,7 @@ checksum = "sha256:00453a12cbb34811ee6a2c083dade5f6198575e3b0610f49e4743366326cd [[file]] path = "justfiles/anvil/tools.just" -checksum = "sha256:2cd024965be6eeff05c6f461da820eba3f63a25ca9ba51442b7c866a3e100ccc" +checksum = "sha256:ba82875d0b6bfeae424a51570340f575fbb4ecb9d365c3cc2d67d99bbc3f830e" [[file]] path = "justfiles/anvil/versions.just" diff --git a/crates/cargo-anvil/src/anvil/artifacts/justfile.rs b/crates/cargo-anvil/src/anvil/artifacts/justfile.rs index a25783b38..2f3d9ea61 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/justfile.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/justfile.rs @@ -1360,6 +1360,29 @@ mod tests { && diagnostic.contains("ensure it is on PATH"), "unexpected missing-rustup diagnostic: {diagnostic}" ); + + let output = command_for_operation(temp.path(), ResolverOperation::InstallIfMissing) + .env("RUSTUP_TOOLCHAIN", "selected-stable") + .env("PATH", path_without_rustup()) + .output() + .expect("environment-selected setup must diagnose missing rustup"); + assert!(!output.status.success(), "environment selection without rustup must fail"); + assert!( + normalized_diagnostic(&output).contains("rustup not found"), + "environment selection must require rustup" + ); + + fs::write(temp.path().join("rust-toolchain.toml"), "[toolchain]\nchannel = \"1.93\"\n") + .expect("toolchain fixture must be writable"); + let output = command_for_operation(temp.path(), ResolverOperation::InstallIfMissing) + .env("PATH", path_without_rustup()) + .output() + .expect("file-selected setup must diagnose missing rustup"); + assert!(!output.status.success(), "toolchain-file selection without rustup must fail"); + assert!( + normalized_diagnostic(&output).contains("rustup not found"), + "toolchain-file selection must require rustup" + ); } #[test] diff --git a/crates/cargo-anvil/templates/justfiles/anvil/tools.just b/crates/cargo-anvil/templates/justfiles/anvil/tools.just index 554e3fb98..697c48173 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/tools.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/tools.just @@ -336,7 +336,10 @@ _anvil-resolve-stable action="install": if ($action -eq 'validate-workspace-msrv') { Assert-CompleteInternalToolchainConfiguration $skipWorkspaceMsrvValidation = -not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN) - if (-not $skipWorkspaceMsrvValidation -and -not (Test-RootToolchainFile)) { + $hasRootToolchainFile = Test-RootToolchainFile + if ($skipWorkspaceMsrvValidation -or $hasRootToolchainFile) { + Get-InstalledToolchains | Out-Null + } else { Assert-WorkspaceMsrvCompatibility } exit 0 @@ -364,6 +367,7 @@ _anvil-resolve-stable action="install": Assert-CompleteInternalToolchainConfiguration if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { + Get-InstalledToolchains | Out-Null & cargo --version *> $null if ($LASTEXITCODE -ne 0) { throw "anvil: selected stable toolchain '$($env:RUSTUP_TOOLCHAIN)' is unavailable; provision RUSTUP_TOOLCHAIN or unset it" @@ -372,6 +376,7 @@ _anvil-resolve-stable action="install": } if (Test-RootToolchainFile) { + Get-InstalledToolchains | Out-Null Push-Location -LiteralPath $repoRoot try { & rustc --version diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap index fb5ae2064..bf6b03a21 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap @@ -6383,7 +6383,10 @@ _anvil-resolve-stable action="install": if ($action -eq 'validate-workspace-msrv') { Assert-CompleteInternalToolchainConfiguration $skipWorkspaceMsrvValidation = -not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN) - if (-not $skipWorkspaceMsrvValidation -and -not (Test-RootToolchainFile)) { + $hasRootToolchainFile = Test-RootToolchainFile + if ($skipWorkspaceMsrvValidation -or $hasRootToolchainFile) { + Get-InstalledToolchains | Out-Null + } else { Assert-WorkspaceMsrvCompatibility } exit 0 @@ -6411,6 +6414,7 @@ _anvil-resolve-stable action="install": Assert-CompleteInternalToolchainConfiguration if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { + Get-InstalledToolchains | Out-Null & cargo --version *> $null if ($LASTEXITCODE -ne 0) { throw "anvil: selected stable toolchain '$($env:RUSTUP_TOOLCHAIN)' is unavailable; provision RUSTUP_TOOLCHAIN or unset it" @@ -6419,6 +6423,7 @@ _anvil-resolve-stable action="install": } if (Test-RootToolchainFile) { + Get-InstalledToolchains | Out-Null Push-Location -LiteralPath $repoRoot try { & rustc --version diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap index 8754d3173..4403f1d7b 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap @@ -6369,7 +6369,10 @@ _anvil-resolve-stable action="install": if ($action -eq 'validate-workspace-msrv') { Assert-CompleteInternalToolchainConfiguration $skipWorkspaceMsrvValidation = -not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN) - if (-not $skipWorkspaceMsrvValidation -and -not (Test-RootToolchainFile)) { + $hasRootToolchainFile = Test-RootToolchainFile + if ($skipWorkspaceMsrvValidation -or $hasRootToolchainFile) { + Get-InstalledToolchains | Out-Null + } else { Assert-WorkspaceMsrvCompatibility } exit 0 @@ -6397,6 +6400,7 @@ _anvil-resolve-stable action="install": Assert-CompleteInternalToolchainConfiguration if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { + Get-InstalledToolchains | Out-Null & cargo --version *> $null if ($LASTEXITCODE -ne 0) { throw "anvil: selected stable toolchain '$($env:RUSTUP_TOOLCHAIN)' is unavailable; provision RUSTUP_TOOLCHAIN or unset it" @@ -6405,6 +6409,7 @@ _anvil-resolve-stable action="install": } if (Test-RootToolchainFile) { + Get-InstalledToolchains | Out-Null Push-Location -LiteralPath $repoRoot try { & rustc --version diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap index 0a5e9dff6..2cb16af5d 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap @@ -5253,7 +5253,10 @@ _anvil-resolve-stable action="install": if ($action -eq 'validate-workspace-msrv') { Assert-CompleteInternalToolchainConfiguration $skipWorkspaceMsrvValidation = -not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN) - if (-not $skipWorkspaceMsrvValidation -and -not (Test-RootToolchainFile)) { + $hasRootToolchainFile = Test-RootToolchainFile + if ($skipWorkspaceMsrvValidation -or $hasRootToolchainFile) { + Get-InstalledToolchains | Out-Null + } else { Assert-WorkspaceMsrvCompatibility } exit 0 @@ -5281,6 +5284,7 @@ _anvil-resolve-stable action="install": Assert-CompleteInternalToolchainConfiguration if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { + Get-InstalledToolchains | Out-Null & cargo --version *> $null if ($LASTEXITCODE -ne 0) { throw "anvil: selected stable toolchain '$($env:RUSTUP_TOOLCHAIN)' is unavailable; provision RUSTUP_TOOLCHAIN or unset it" @@ -5289,6 +5293,7 @@ _anvil-resolve-stable action="install": } if (Test-RootToolchainFile) { + Get-InstalledToolchains | Out-Null Push-Location -LiteralPath $repoRoot try { & rustc --version diff --git a/justfiles/anvil/tools.just b/justfiles/anvil/tools.just index 554e3fb98..697c48173 100644 --- a/justfiles/anvil/tools.just +++ b/justfiles/anvil/tools.just @@ -336,7 +336,10 @@ _anvil-resolve-stable action="install": if ($action -eq 'validate-workspace-msrv') { Assert-CompleteInternalToolchainConfiguration $skipWorkspaceMsrvValidation = -not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN) - if (-not $skipWorkspaceMsrvValidation -and -not (Test-RootToolchainFile)) { + $hasRootToolchainFile = Test-RootToolchainFile + if ($skipWorkspaceMsrvValidation -or $hasRootToolchainFile) { + Get-InstalledToolchains | Out-Null + } else { Assert-WorkspaceMsrvCompatibility } exit 0 @@ -364,6 +367,7 @@ _anvil-resolve-stable action="install": Assert-CompleteInternalToolchainConfiguration if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { + Get-InstalledToolchains | Out-Null & cargo --version *> $null if ($LASTEXITCODE -ne 0) { throw "anvil: selected stable toolchain '$($env:RUSTUP_TOOLCHAIN)' is unavailable; provision RUSTUP_TOOLCHAIN or unset it" @@ -372,6 +376,7 @@ _anvil-resolve-stable action="install": } if (Test-RootToolchainFile) { + Get-InstalledToolchains | Out-Null Push-Location -LiteralPath $repoRoot try { & rustc --version From 4331a220c1c9988053a1f136c48df95f759565f5 Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Tue, 1 Sep 2026 15:21:02 +0200 Subject: [PATCH 32/32] Match exact rustup toolchain selectors Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: f7996e6f-5c20-4d7a-b2f7-690af95e557e --- .anvil.lock | 4 ++-- crates/cargo-anvil/src/anvil/artifacts/justfile.rs | 7 +++---- crates/cargo-anvil/templates/justfiles/anvil/tools.just | 7 +------ .../tests/snapshots/snapshots__ado_backend.snap | 7 +------ .../tests/snapshots/snapshots__github_backend.snap | 7 +------ .../cargo-anvil/tests/snapshots/snapshots__local_only.snap | 7 +------ justfiles/anvil/tools.just | 7 +------ 7 files changed, 10 insertions(+), 36 deletions(-) diff --git a/.anvil.lock b/.anvil.lock index 21f7d863b..a127283c7 100644 --- a/.anvil.lock +++ b/.anvil.lock @@ -1,7 +1,7 @@ version = 1 tool = "anvil" tool_version = "0.6.0" -catalog_checksum = "sha256:c9aaa0e99670b91553957b4438043e07bef190eb6402b6f36bfa2a6c980589a1" +catalog_checksum = "sha256:411e8b3d247721c856c6a8a69f7b388711159325472ab687eff9c40bdd9727b0" [[file]] path = ".anvil/container/Dockerfile.dockerignore" @@ -229,7 +229,7 @@ checksum = "sha256:00453a12cbb34811ee6a2c083dade5f6198575e3b0610f49e4743366326cd [[file]] path = "justfiles/anvil/tools.just" -checksum = "sha256:ba82875d0b6bfeae424a51570340f575fbb4ecb9d365c3cc2d67d99bbc3f830e" +checksum = "sha256:48b887481a7eb8dc7615367347c93b8a02dd7baafb0ef80ece883f03600285dd" [[file]] path = "justfiles/anvil/versions.just" diff --git a/crates/cargo-anvil/src/anvil/artifacts/justfile.rs b/crates/cargo-anvil/src/anvil/artifacts/justfile.rs index 2f3d9ea61..7af410639 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/justfile.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/justfile.rs @@ -1486,13 +1486,12 @@ mod tests { ); assert!( output.status.success(), - "a patch-normalized installed MSRV must satisfy an unpatched selection: {}", + "distinct patch-qualified installation must not prevent provisioning the selected MSRV: {}", normalized_diagnostic(&output) ); - assert!(rustup_calls.contains("toolchain list")); assert!( - !rustup_calls.contains("toolchain install"), - "an equivalent patch-normalized MSRV must not be reinstalled" + rustup_calls.contains("toolchain install 1.93 --profile minimal"), + "the exact 1.93 selector must be installed when only 1.93.0 exists" ); assert!(cargo_calls.is_empty(), "public MSRV availability is checked through rustup"); diff --git a/crates/cargo-anvil/templates/justfiles/anvil/tools.just b/crates/cargo-anvil/templates/justfiles/anvil/tools.just index 697c48173..8e942948d 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/tools.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/tools.just @@ -250,12 +250,7 @@ _anvil-resolve-stable action="install": function Test-ToolchainInstalled([string] $toolchain) { $installed = Get-InstalledToolchains - $pattern = if ($toolchain -match '^(\d+)\.(\d+)$') { - "(?m)^$([regex]::Escape($Matches[1]))\.$([regex]::Escape($Matches[2]))(?:\.0)?(?:-|$)" - } else { - "(?m)^$([regex]::Escape($toolchain))(?:-|$)" - } - return (($installed -join "`n") -match $pattern) + return (($installed -join "`n") -match "(?m)^$([regex]::Escape($toolchain))(?:-|$)") } function Assert-WorkspaceMsrvCompatibility { diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap index bf6b03a21..e8063b707 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap @@ -6297,12 +6297,7 @@ _anvil-resolve-stable action="install": function Test-ToolchainInstalled([string] $toolchain) { $installed = Get-InstalledToolchains - $pattern = if ($toolchain -match '^(\d+)\.(\d+)$') { - "(?m)^$([regex]::Escape($Matches[1]))\.$([regex]::Escape($Matches[2]))(?:\.0)?(?:-|$)" - } else { - "(?m)^$([regex]::Escape($toolchain))(?:-|$)" - } - return (($installed -join "`n") -match $pattern) + return (($installed -join "`n") -match "(?m)^$([regex]::Escape($toolchain))(?:-|$)") } function Assert-WorkspaceMsrvCompatibility { diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap index 4403f1d7b..45d27cafe 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap @@ -6283,12 +6283,7 @@ _anvil-resolve-stable action="install": function Test-ToolchainInstalled([string] $toolchain) { $installed = Get-InstalledToolchains - $pattern = if ($toolchain -match '^(\d+)\.(\d+)$') { - "(?m)^$([regex]::Escape($Matches[1]))\.$([regex]::Escape($Matches[2]))(?:\.0)?(?:-|$)" - } else { - "(?m)^$([regex]::Escape($toolchain))(?:-|$)" - } - return (($installed -join "`n") -match $pattern) + return (($installed -join "`n") -match "(?m)^$([regex]::Escape($toolchain))(?:-|$)") } function Assert-WorkspaceMsrvCompatibility { diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap index 2cb16af5d..dd2197ec7 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap @@ -5167,12 +5167,7 @@ _anvil-resolve-stable action="install": function Test-ToolchainInstalled([string] $toolchain) { $installed = Get-InstalledToolchains - $pattern = if ($toolchain -match '^(\d+)\.(\d+)$') { - "(?m)^$([regex]::Escape($Matches[1]))\.$([regex]::Escape($Matches[2]))(?:\.0)?(?:-|$)" - } else { - "(?m)^$([regex]::Escape($toolchain))(?:-|$)" - } - return (($installed -join "`n") -match $pattern) + return (($installed -join "`n") -match "(?m)^$([regex]::Escape($toolchain))(?:-|$)") } function Assert-WorkspaceMsrvCompatibility { diff --git a/justfiles/anvil/tools.just b/justfiles/anvil/tools.just index 697c48173..8e942948d 100644 --- a/justfiles/anvil/tools.just +++ b/justfiles/anvil/tools.just @@ -250,12 +250,7 @@ _anvil-resolve-stable action="install": function Test-ToolchainInstalled([string] $toolchain) { $installed = Get-InstalledToolchains - $pattern = if ($toolchain -match '^(\d+)\.(\d+)$') { - "(?m)^$([regex]::Escape($Matches[1]))\.$([regex]::Escape($Matches[2]))(?:\.0)?(?:-|$)" - } else { - "(?m)^$([regex]::Escape($toolchain))(?:-|$)" - } - return (($installed -join "`n") -match $pattern) + return (($installed -join "`n") -match "(?m)^$([regex]::Escape($toolchain))(?:-|$)") } function Assert-WorkspaceMsrvCompatibility {
JobSub-groupCheckNotes
UmbrellaGroupCheckNotes
pr-fastfmtpredefined configuration with nightly features
clippypredefined lints
pr-slowpr-testllvm-covdual feature-config; gated by cargo-coverage-gate
doc-testruns both feature configs
examplescompile-only
pr-msrvmsrv-testdual feature-config tests under the declared MSRV
pr-msrvmsrv-testdual feature-config, all-target tests under the declared MSRV
pr-runtime-analysismirilibtest, not nextest
carefulself-cleans on a toolchain bump
loomopt-in targets only