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/21] 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 e3aa0ecd..4c251b93 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 fa68b18f..dc4a709e 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 6566e657..6aca8568 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 dfc6a53f..c95dd2c7 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 e0ed8105..f399e349 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 00000000..e0351d6b --- /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 9b09eca5..ed6fa244 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 7643b794..17867d9f 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 54c4b653..74f7eff1 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 df3e7059..b2bb240b 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 1499d446..dbdcd14c 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 35192935..58db6e6e 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 69db4dbd..fa19c70e 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 e68fcdb9..d01b6990 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 7b8d9d29..765e7283 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 45d0fad9..7d6d0e99 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 696c9309..16ab49f2 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 11327a34..f1c7353b 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 fa68b18f..dc4a709e 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 6566e657..6aca8568 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 dfc6a53f..c95dd2c7 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 e0ed8105..f399e349 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 00000000..e0351d6b --- /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 9b09eca5..ed6fa244 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 cb6aab14..1de4bcaf 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 564a3b90..d33e5aa0 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 91e18b89..9400b085 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 35d40084..933c3e94 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 7d6f37c4..8a62346f 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 0fe700aa..79ac557a 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 078a7888..807f625c 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 cb6aab14..1de4bcaf 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 564a3b90..d33e5aa0 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/21] 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 4c251b93..c1a87849 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 e0351d6b..df8cbfd3 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 705fd40e..4c1f8b14 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 ed6fa244..6105a4a2 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 7ad5cd5d..58ea25ad 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 17867d9f..16129d6a 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 74f7eff1..8d484bd9 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 b2bb240b..e42a9db4 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 69d2be0a..6427b449 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 58db6e6e..27eeb5d1 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 fa19c70e..24418b14 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 a62b7459..b21bc358 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 963df7e5..c67b3c45 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 765e7283..84847048 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 16ab49f2..d596bb1e 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 cb03b78e..32bc0436 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 6e1ac2f8..dbd2588f 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 2331ee9c..d808571f 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 f1c7353b..04b699e9 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 e0351d6b..df8cbfd3 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 705fd40e..4c1f8b14 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 7ad5cd5d..58ea25ad 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 ed6fa244..6105a4a2 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 00000000..0e488923 --- /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 00000000..1c25aa3f --- /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 75ad63c1..f063a49e 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 710bead6..456c9e12 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 8a62346f..e7b3c68d 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 79ac557a..5784c35f 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 807f625c..a92f2adb 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 00000000..0e488923 --- /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 00000000..1c25aa3f --- /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 75ad63c1..f063a49e 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 710bead6..456c9e12 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/21] 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 c1a87849..c172e2d4 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 df8cbfd3..736f8d08 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 4c1f8b14..37610ebb 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 58ea25ad..102256f6 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 16129d6a..bbd3a890 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 8d484bd9..f6741ae5 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 e42a9db4..85479852 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 6427b449..9ae4df66 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 27eeb5d1..db9e0049 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 24418b14..6062c97b 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 b21bc358..df48f063 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 c67b3c45..449591e1 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 84847048..a99d2d0d 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 d596bb1e..de8af6ad 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 dbd2588f..a5ecf90f 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 d808571f..f0c34ea7 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 df8cbfd3..736f8d08 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 4c1f8b14..37610ebb 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 58ea25ad..102256f6 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 0e488923..491701f5 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 e7b3c68d..f3159bc0 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 5784c35f..682e4212 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 a92f2adb..4a2b3980 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 0e488923..491701f5 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/21] 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 a99d2d0d..bf9f4091 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/21] 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 bf9f4091..4b256012 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/21] 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 4b256012..3b807b0f 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/21] 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 0d267dd7..d32471a3 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/21] 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 e8b36e85..c6f29823 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 09ced2d8..a2c3fe46 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 bff74e6d..05a93117 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 43b0af6b..e034b62c 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 b60b0316..af236bed 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 50dd19f6..719cc9c6 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 9594b5ea..ba1a4120 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 f5e15d79..e402fa06 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 02f56043..a6643e5b 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 30fc0bb1..6b73917d 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 ea86bad1..a8853f07 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 09ced2d8..a2c3fe46 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 1c25aa3f..d35d01ac 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 f063a49e..3ac2c01f 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 d32471a3..9cec814d 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 83580c5a..2cb08455 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 95366594..8255a007 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 1e6c5a4c..4d5172ce 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 18412bde..def43d97 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 1c25aa3f..d35d01ac 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 f063a49e..3ac2c01f 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/21] 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 a6643e5b..fd9e15e4 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/21] 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 fd9e15e4..2719db5b 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/21] 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 c6f29823..3160a4b4 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 dc4a709e..43f3685a 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 a2c3fe46..7c8b1b36 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 30f7402c..051fbc3f 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 e034b62c..d8e80d90 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 af236bed..a7276d24 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 c2ef6707..4f3987c3 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 ba1a4120..37477ed0 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 e402fa06..7b6ca823 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 2719db5b..eb0b393e 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 0e64a72e..9121d96b 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 dc4a709e..43f3685a 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 a2c3fe46..7c8b1b36 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 30f7402c..051fbc3f 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 1627c3a5..897ec37c 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 20de2046..d0aef319 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 2f4d42f7..7f8ca006 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 37f7d21b..306bb9ee 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 be987140..c937a5dc 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 ea604935..c4268043 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 a0af1ccb..8de8ad7a 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 c9167c91..5b9a8eb5 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 c52b613a..17d09830 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 9250d41a..b2933616 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 1954ae4a..1480f48d 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 48ef3e7c..e74beab7 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 c5668204..9def94fa 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 698b1115..47ab115f 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 33c0593d..87287a42 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 989f57b4..7f489f48 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 ab39d671..fbace94e 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 0f88aba6..4cc8441b 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 6e1ca1dd..362e470c 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 25c98ba9..e3c3c423 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 a914f1eb..4009e757 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 1d258e25..615fa443 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 4f7112b6..7cbac0ac 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 15e734b4..805d43cc 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 42d74502..56223c8a 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 1de4bcaf..54ef872d 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 888224ea..e2441b62 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 9cec814d..40505993 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 31190358..5b7ea9c1 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 8255a007..fedacb96 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 4d5172ce..bb2ef7d3 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 def43d97..32a30aa6 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 1627c3a5..897ec37c 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 20de2046..d0aef319 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 2f4d42f7..7f8ca006 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 37f7d21b..306bb9ee 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 be987140..c937a5dc 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 ea604935..c4268043 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 a0af1ccb..8de8ad7a 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 c9167c91..5b9a8eb5 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 c52b613a..17d09830 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 9250d41a..b2933616 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 1954ae4a..1480f48d 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 48ef3e7c..e74beab7 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 c5668204..9def94fa 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 698b1115..47ab115f 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 33c0593d..87287a42 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 989f57b4..7f489f48 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 ab39d671..fbace94e 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 0f88aba6..4cc8441b 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 6e1ca1dd..362e470c 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 25c98ba9..e3c3c423 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 a914f1eb..4009e757 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 1d258e25..615fa443 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 4f7112b6..7cbac0ac 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 15e734b4..805d43cc 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 42d74502..56223c8a 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 1de4bcaf..54ef872d 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 888224ea..e2441b62 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/21] 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 3160a4b4..b83469fe 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 7c8b1b36..e38de267 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 a7276d24..c52a0e3a 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 719cc9c6..9a2eabf3 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 4f3987c3..bd4cde05 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 7b6ca823..b62c6e41 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 eb0b393e..319e91e2 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 7c8b1b36..e38de267 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 fedacb96..c45d4041 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 bb2ef7d3..b3fb6f95 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 32a30aa6..3b0e90e1 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/21] 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 b83469fe..76b93525 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 051fbc3f..20c55562 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 37477ed0..0659de36 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 051fbc3f..20c55562 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 b3fb6f95..11048e11 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/21] 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 319e91e2..a445082c 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/21] 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 76b93525..c678bcfe 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 54ef872d..083a77be 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 5b7ea9c1..5d8f288f 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 54ef872d..083a77be 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/21] 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 c678bcfe..c843b1df 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 083a77be..218dc28f 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 c45d4041..1f011e5f 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 11048e11..a3d134bf 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 3b0e90e1..e98115c2 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 083a77be..218dc28f 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/21] 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 5d8f288f..3c977349 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/21] 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 c843b1df..3704d403 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 6aca8568..c242cb21 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 c95dd2c7..3ca2e859 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 f399e349..2dabc348 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 e38de267..00000000 --- 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 20c55562..cf3ad086 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 d8e80d90..7c56dbf5 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 c52a0e3a..765bd0d2 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 9a2eabf3..c0ad4447 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 bd4cde05..2c7a75ec 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 0659de36..873353fb 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 b62c6e41..eaf3b2c6 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 d01b6990..7cbdcab1 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 a445082c..0ac0af58 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 6b73917d..34f6bff1 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 9121d96b..ad3c6cbc 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 6aca8568..c242cb21 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 c95dd2c7..3ca2e859 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 f399e349..2dabc348 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 e38de267..00000000 --- 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 20c55562..cf3ad086 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 897ec37c..90c5b428 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 d0aef319..a43c5de0 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 7f8ca006..0158612e 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 306bb9ee..8ba438b3 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 c4268043..0377fb4d 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 8de8ad7a..dcf5d192 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 5b9a8eb5..a0354f2c 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 17d09830..c798669b 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 b2933616..c02ad20b 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 1480f48d..97f95976 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 e74beab7..57f36017 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 9def94fa..cee4d24d 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 47ab115f..de3b11ef 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 87287a42..d659a3d4 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 7f489f48..fb794c32 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 fbace94e..1096d274 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 4cc8441b..5ddd8260 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 e3c3c423..565edeb2 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 4009e757..e3e32a16 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 615fa443..aea18641 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 7cbac0ac..6c68adfb 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 805d43cc..cdf2aef9 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 56223c8a..4f05394c 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 218dc28f..41b9336f 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 e2441b62..70a33d8b 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 933c3e94..dcd7e84c 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 40505993..2aa59feb 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 3c977349..4dbe8e7d 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 1f011e5f..eaf98382 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 a3d134bf..79f61e72 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 e98115c2..f1ef4881 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 897ec37c..90c5b428 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 d0aef319..a43c5de0 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 7f8ca006..0158612e 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 306bb9ee..8ba438b3 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 c4268043..0377fb4d 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 8de8ad7a..dcf5d192 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 5b9a8eb5..a0354f2c 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 17d09830..c798669b 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 b2933616..c02ad20b 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 1480f48d..97f95976 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 e74beab7..57f36017 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 9def94fa..cee4d24d 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 47ab115f..de3b11ef 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 87287a42..d659a3d4 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 7f489f48..fb794c32 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 fbace94e..1096d274 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 4cc8441b..5ddd8260 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 e3c3c423..565edeb2 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 4009e757..e3e32a16 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 615fa443..aea18641 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 7cbac0ac..6c68adfb 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 805d43cc..cdf2aef9 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 56223c8a..4f05394c 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 218dc28f..41b9336f 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 e2441b62..70a33d8b 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/21] 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 3704d403..e01d0bf9 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 70a33d8b..ae8e9bda 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 70a33d8b..ae8e9bda 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/21] 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 e01d0bf9..2c0dfb63 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 43f3685a..5709437b 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 cf3ad086..2b4c47a5 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 765bd0d2..54442b64 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 c0ad4447..d33b7081 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 2c7a75ec..8ddeb62d 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 873353fb..94535d65 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 eaf3b2c6..ab0452f7 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 0bf61f64..99958e1d 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 7cbdcab1..3ed8e30e 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 92fcb078..0975a0b7 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 0ac0af58..e416bc0e 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 ad3c6cbc..c7352dd5 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 43f3685a..5709437b 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 cf3ad086..2b4c47a5 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 0158612e..b26880a8 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 c02ad20b..a8edeef7 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 97f95976..ca6ade8c 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 de3b11ef..fe447b7f 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 5ddd8260..845381bc 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 41b9336f..31f8537d 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 ae8e9bda..e4d7ef5a 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 eaf98382..2631249a 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 79f61e72..24bc62a5 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 f1ef4881..da4cf180 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 0158612e..b26880a8 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 c02ad20b..a8edeef7 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 97f95976..ca6ade8c 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 de3b11ef..fe447b7f 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 5ddd8260..845381bc 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 41b9336f..31f8537d 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 ae8e9bda..e4d7ef5a 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/21] 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 2c0dfb63..11e2ef92 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 d33b7081..61bed090 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 ab0452f7..638a5437 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 e416bc0e..e1576954 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 31f8537d..0d6e45f9 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 e4d7ef5a..1641ee8a 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 2aa59feb..aaf8565f 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 2631249a..ed5ab45a 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 24bc62a5..714f7b6d 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 da4cf180..2bee1d42 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 31f8537d..0d6e45f9 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 e4d7ef5a..1641ee8a 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) }) ''')
JobSub-groupCheckNotes