diff --git a/.anvil.lock b/.anvil.lock index 87055558..11e2ef92 100644 --- a/.anvil.lock +++ b/.anvil.lock @@ -1,15 +1,15 @@ version = 1 tool = "anvil" tool_version = "0.5.0" -catalog_checksum = "sha256:b9e76e5dab6d2cd8cd2eaea04bd1b4457ea8839a688795981dc4cc9a415f8092" +catalog_checksum = "sha256:bea05c54e887000f8461f5bdd8f86375fc94cd481942b6c7b8ab2674d64164c6" [[file]] path = ".anvil/container/Containerfile" -checksum = "sha256:4c343282b4e773978ff29b7d39e3a0955975bf64ad3e63afe1aa53e51be42916" +checksum = "sha256:317a2bfede7c6679750dc37a5b9828419219a86e98f8568cb9106c1460677271" [[file]] path = ".anvil/container/Containerfile.dockerignore" -checksum = "sha256:359c2bd70d599ff1e5674eae2af5bb35f344f78e642da7024cfdc6efc5d345da" +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:8cdd2dd9cdfb037d768802e4dda7d1156f626dc711663e6bc39fbb86d1ce2e04" +checksum = "sha256:ce553ba67b21ed670db307f7ac87c95941122ece1c0bd94b1e37b83227d1ace7" [[file]] path = ".anvil/container/image-id.sh" -checksum = "sha256:b526a643e42902dd1e8acff65529fb63741760f72c14b123e9747394158faf53" +checksum = "sha256:f71671ec0f8c2ec2e17328a5aab8172744bed6f0b3a312df2bc2bd60d6460ef6" [[file]] path = ".anvil/container/run-in-container.ps1" @@ -49,7 +49,7 @@ checksum = "sha256:ff8def6c0786b6e146c4b633dfe38cb9b8ede398345516cca32bbcd558608 [[file]] path = ".github/actions/anvil-setup/action.yml" -checksum = "sha256:3df22c126c79170d39c515a06b597a0c20a05a5803eacd38326f0fa63bea846b" +checksum = "sha256:259d59fb9381b5e29fe2f0a123e717ba62aaecd25955906fd943f385eca22067" [[file]] path = ".github/actions/anvil-setup/just-problem-matcher.json" @@ -57,7 +57,7 @@ checksum = "sha256:ea44d5e1a2cb1471cf2cef05eceab846d8afa45cf691be800f21fec1218da [[file]] path = ".github/workflows/anvil-pr-impl.yml" -checksum = "sha256:518d267b6def1d2549800df52fbe32c5f0848b2b2d9c00d9480b86e07b54ee3a" +checksum = "sha256:f69ff8d422152ec5b4e59d5905316be02ea5b054b8b16752e87aac3ae290a579" [[file]] path = ".github/workflows/anvil-pr.yml" @@ -73,63 +73,63 @@ checksum = "sha256:d4d3bd645a5586e9a1cc3a5fc27e38c93e59b1e29f83ede0ccd610273eec0 [[file]] path = "justfiles/anvil/checks/aprz.just" -checksum = "sha256:62d8334b55b0ba3b221550037dec987d9749e773f40df6c0cd18f08164c095bf" +checksum = "sha256:079b0bf8a3e97745d8ac1fd7986056b7787af778ef00e491786572ae5166d810" [[file]] path = "justfiles/anvil/checks/audit.just" -checksum = "sha256:54abf96a320bb4b35a3c0ddf2f30b0f4a30e0673e482ca3a71242fa383536415" +checksum = "sha256:ef33da620683628208b4c5ad51f96f5f6424fe1d56c2d0475487228b1f1aec23" [[file]] path = "justfiles/anvil/checks/bench.just" -checksum = "sha256:50f04b4ea6c99df8ad7434d320f34dccdb6db6a77b83e3090e35de0ca3a15f83" +checksum = "sha256:850793443ada2151a895ee2728e81f03da5430facde16522bcc2e0c314a28577" [[file]] path = "justfiles/anvil/checks/bolero.just" -checksum = "sha256:003032f39c781851b880c1d7e63b93573f5252cca9f785b98b04dfa2e858aff3" +checksum = "sha256:a67f556d46842487dc0f5851bb5ac634245eab6c2cfac32c6f6f0599b2ddab1d" [[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:4eca78f197c2cf428b693c49cf0af4afb735b9c6caa8c83a71e5f5fd312a9bc1" [[file]] path = "justfiles/anvil/checks/cargo-sort.just" -checksum = "sha256:2652ae52153eebe3fdb1dfd553aa65cab84d11054d5314470d55774a92a742b3" +checksum = "sha256:ea48b494699b6d6a71acd7070c96e28ab8b8b8ee74049de82afe922540508d75" [[file]] path = "justfiles/anvil/checks/clippy.just" -checksum = "sha256:64e8f11ed1a308797e5599117ab6152d0ca3a1dfb55e6a4cfdd42d35de13e1e2" +checksum = "sha256:945a1bf3bb1372b2bfbbfec00d520a97da56ebc5575b1e6292c421b52cc77243" [[file]] path = "justfiles/anvil/checks/deny.just" -checksum = "sha256:4bb4bc0822612e2d2a43689e3f5c7f1190f59ef3db201fad7f3332725ceaef16" +checksum = "sha256:c5604e8cb0eaa2baf4e2e2c70032d037d21873090375ffcf1ee99983965f6635" [[file]] path = "justfiles/anvil/checks/doc-build.just" -checksum = "sha256:57bf3ce93d3e7b947cb203f85b572531b86706ed2437ce3098f086745bf5d111" +checksum = "sha256:9e187f61cb1ef9d11fab2e402af8a882b77f6ed17ca332afb9ae0b1d2112e95b" [[file]] path = "justfiles/anvil/checks/doc-test.just" -checksum = "sha256:e923667a5ceb376232db02ec41824d21f2fccffd041565109f7740c001c1c23d" +checksum = "sha256:0e54c767de8e57a523a8b1b589a88ab6aca3169ac97d157c42d509d429f4b024" [[file]] path = "justfiles/anvil/checks/ensure-no-cyclic-deps.just" -checksum = "sha256:7351afd07f020f04fa6c7c82d4740ef32cbb1b648550f0a5ff306af6a5d58588" +checksum = "sha256:2d19930c33a93b49d0c9e9672a31d85e1ad4fb92faecc0fb599803139b11c18f" [[file]] path = "justfiles/anvil/checks/ensure-no-default-features.just" -checksum = "sha256:25c61d31ea76c5e65b5c3caf744ddc9b49f6f9cdf9717b012d1d82cebfe555b9" +checksum = "sha256:38e089afe176aea92c93ac6758154b206b6fc8940931035c0d059449fa824d6c" [[file]] path = "justfiles/anvil/checks/examples.just" -checksum = "sha256:0e84a7b43ff07608c624ece03d46ddd58c629ebd5f6a5684f3b43b7190026e0f" +checksum = "sha256:8d01fa3e303a9f2b768213636f12c68cc8b41932b68549239e9f10d3be43507b" [[file]] path = "justfiles/anvil/checks/external-types.just" -checksum = "sha256:d003f1d82898706c136f42c3156d5ecc8f288c18f125c964b9dca1d24fa93d67" +checksum = "sha256:7e67d90a9d8bd8cd5c8adb68baf41fdd4625bff4d425b5bf18b75baee62892a8" [[file]] path = "justfiles/anvil/checks/fmt.just" @@ -137,15 +137,15 @@ checksum = "sha256:967b68ef3ed3e0dcac2a1ea22ef92c66a7db802bda718f6e5ce39f529ed8e [[file]] path = "justfiles/anvil/checks/license-headers.just" -checksum = "sha256:ab9ef6c3b50475ff3a8ab873b319f1ec04c456275d566ce323a76730be3ad0fc" +checksum = "sha256:7aa0560dc9088b33e80cd55aee0a25090cbd3d3610652afd3734d0ca52ee7b47" [[file]] path = "justfiles/anvil/checks/llvm-cov.just" -checksum = "sha256:689f7d5b1eaea32672f8d0b9fff0994cc03bc56fb8ef282a4286f930fc503725" +checksum = "sha256:cb23283ac3d377b1219c904f4de93b05b437b48c93629fec617307914bba6296" [[file]] path = "justfiles/anvil/checks/loom.just" -checksum = "sha256:4bf509bcca3507aafd9848ce19ea3691ab2cd77de05b344e042f3ba2c300e35b" +checksum = "sha256:7d7cb6e9845a97be18dbe93d8b04860386dc43a5f2033816c3224aa460c5aa63" [[file]] path = "justfiles/anvil/checks/miri-race-coverage.just" @@ -163,13 +163,17 @@ checksum = "sha256:c82f586402f2cb05397a40eec6fe95c7590df2cc52f0dd9da6879de085175 path = "justfiles/anvil/checks/miri.just" checksum = "sha256:9e9d0cbfef1e1e1586c2af4e9c387719c2f017ea1203d326baacc3ae25df80e2" +[[file]] +path = "justfiles/anvil/checks/msrv-test.just" +checksum = "sha256:968a0cf65a319626c3ed61f75e1d363608856664d105850be281e91f8ecf3537" + [[file]] path = "justfiles/anvil/checks/mutants-diff.just" -checksum = "sha256:cb9721c1cc94161ea8bc20ccd94209daa23d83d1beda9f3c0969e2031a135022" +checksum = "sha256:7b7ae9b1c303d29cb26bc70d362b5c51e3f6fbde01bdbc442da27cf53f315018" [[file]] path = "justfiles/anvil/checks/mutants-full.just" -checksum = "sha256:9792344e6696f5c63f30fdccb5ef90a3598c56adb7561cec33f074c84ce6f89b" +checksum = "sha256:604dfb6aa2742695ff638a7d2db6811a1bcbf27da73660027baacc86b2fe1509" [[file]] path = "justfiles/anvil/checks/pr-title.just" @@ -177,15 +181,15 @@ checksum = "sha256:28734aa77526ca5c53d89a32c3e20ae6b42d2032c73ab6a1dbc9831f25cb0 [[file]] path = "justfiles/anvil/checks/readme-check.just" -checksum = "sha256:d346399f288570066e53fd123baf05f7d4a57f17da8ee687a6d881204c5bae12" +checksum = "sha256:ce5311a8d091bb8d9cfa631a8d5753546a240d4fca55daab4d1b1bacb7c1f2a9" [[file]] path = "justfiles/anvil/checks/semver-check.just" -checksum = "sha256:1b8b67f8c054c8da03d43b746f28c20f5366996e3dea3c21cf25d04a6496a072" +checksum = "sha256:7047eb4e98f7f1fddf6a734afd0e8b1e3b4e1747b9992a0cd4c9be5892190f2c" [[file]] path = "justfiles/anvil/checks/spellcheck.just" -checksum = "sha256:8f47518b756607402a7b3652762d5d37cff5d91eecd1052053176eedd65a9958" +checksum = "sha256:2b2063f7e494dba51f805534bce5235132be1eeabde3a118d42d87ce49d51fd6" [[file]] path = "justfiles/anvil/checks/udeps.just" @@ -199,6 +203,10 @@ checksum = "sha256:cbec6450a64f800963ea5ba9d2eedfa4e90d91d441307f18d109629f84912 path = "justfiles/anvil/groups/pr-fast.just" checksum = "sha256:29a219b7d8b2eaa605b0444838f05ffc7538ab8bccc43d1613c555f7562a731f" +[[file]] +path = "justfiles/anvil/groups/pr-msrv.just" +checksum = "sha256:5c862a1c7775742377e75d6848650070d11e9ce3facd9ab304b03dbca1c4b1d2" + [[file]] path = "justfiles/anvil/groups/pr-mutants.just" checksum = "sha256:19a5fb032f680eee2d2e4eca56dece0ced80ac15bd2332b8135915d884f804a1" @@ -209,7 +217,7 @@ checksum = "sha256:9a94b6ae2d212f91d324e401ebdb2238805f3450d95d4d45df791d7f1679a [[file]] path = "justfiles/anvil/groups/pr-slow.just" -checksum = "sha256:8b7c490cb6eb20c31549ef71eb75eac86ae145c8c6380ee1f590ccd81eec48ea" +checksum = "sha256:5d8bb4b73ce14ec76477f90b754c47904054e1497471a21d3d84b0ee6a4676d0" [[file]] path = "justfiles/anvil/groups/pr-test.just" @@ -237,11 +245,11 @@ checksum = "sha256:6b71379310b986e9a44000ba9d072c3f8934397a0f7fb1bf27a31a7742ca7 [[file]] path = "justfiles/anvil/impact.just" -checksum = "sha256:5714a138135154b91b234f6bad06ec3ade0e5780f761d631c1eca92b6010e5ad" +checksum = "sha256:efcee10ddda4d4dd7be46a36187e5bb56c05fb069776fba377a227556bccc2eb" [[file]] path = "justfiles/anvil/mod.just" -checksum = "sha256:76ded6ee071fc3ba1b2b38ca97c0b2d15ab75db20ceda44c57436e5cd249ae0b" +checksum = "sha256:c3f88aa70698bb7b9c911d395293f2d66cbb84bd2df0e9a04faec0a6cb183564" [[file]] path = "justfiles/anvil/runner.just" @@ -253,11 +261,11 @@ checksum = "sha256:5b2d91569f3fe87cd5f516623a01b5c7ae855ad587870ef9a2ae6d5619dd9 [[file]] path = "justfiles/anvil/tools.just" -checksum = "sha256:a1e44ca16f172b487afa3997f102512733d3b65a4418cf894cbd749a3abc17dc" +checksum = "sha256:a7ce0dfe39a65b44aa3a7d605f8819673012600b16822ae29c90e95a3b7e708a" [[file]] path = "justfiles/anvil/versions.just" -checksum = "sha256:acbea93d5117db747537f4f7b9a5eb90b7d3e0dd3e8684cc0e4dc1dcb15ac93e" +checksum = "sha256:2e6f224497a5b4ea68ab096bafc65622faf9c846c5c8caf7ea68c76e72a791c1" [[region]] host = ".delta.toml" diff --git a/.anvil/container/Containerfile b/.anvil/container/Containerfile index fa68b18f..5709437b 100644 --- a/.anvil/container/Containerfile +++ b/.anvil/container/Containerfile @@ -49,10 +49,6 @@ 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 \ diff --git a/.anvil/container/Containerfile.dockerignore b/.anvil/container/Containerfile.dockerignore index 6566e657..c242cb21 100644 --- a/.anvil/container/Containerfile.dockerignore +++ b/.anvil/container/Containerfile.dockerignore @@ -16,6 +16,8 @@ # 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 diff --git a/.anvil/container/image-id.ps1 b/.anvil/container/image-id.ps1 index dfc6a53f..3ca2e859 100644 --- a/.anvil/container/image-id.ps1 +++ b/.anvil/container/image-id.ps1 @@ -13,12 +13,11 @@ if ($LASTEXITCODE -ne 0 -or -not $repoRoot) { throw 'anvil-container must run from a Git repository.' } -$inputs = @( - 'rust-toolchain.toml' -) -$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.' +$inputs = @('Cargo.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..2dabc348 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) +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/.github/actions/anvil-setup/action.yml b/.github/actions/anvil-setup/action.yml index f1557210..2b4c47a5 100644 --- a/.github/actions/anvil-setup/action.yml +++ b/.github/actions/anvil-setup/action.yml @@ -77,9 +77,29 @@ runs: } "Free after cleanup (GiB): $(Get-FreeDiskGiB)" - - id: rustc-version + # Stable provisioning is part of the generated Just setup graph, so + # bootstrap Just before asking it for the selected compiler version. + # cargo-binstall keeps this cold path fast. + - name: Install cargo-binstall + uses: cargo-bins/cargo-binstall@v1.21.0 # immutable release, the tag cannot be moved + + - name: Install just shell: bash - run: echo "version=$(rustc --version | awk '{print $2}')" >> "$GITHUB_OUTPUT" + run: | + if ! command -v just >/dev/null 2>&1 ; then + cargo binstall --no-confirm --locked just || cargo install --locked just + fi + + - name: Provision stable toolchain and capture version + id: rustc-version + shell: pwsh -NoProfile -Command ". '{0}'" + run: | + $version = (& just _anvil-stable-rustc-version | Select-Object -Last 1) + if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($version)) { + throw 'anvil: failed to provision the selected stable toolchain' + } + $version = $version.Trim() + "version=$version" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append - name: Restore cargo cache id: cargo-cache uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 @@ -100,9 +120,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 @@ -126,41 +146,11 @@ runs: ~/.cargo/.crates.toml ~/.cargo/.crates2.json - # 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 - # 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/.github/workflows/anvil-pr-impl.yml b/.github/workflows/anvil-pr-impl.yml index 116a15ce..911c0f29 100644 --- a/.github/workflows/anvil-pr-impl.yml +++ b/.github/workflows/anvil-pr-impl.yml @@ -222,6 +222,33 @@ jobs: token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: false + pr-msrv: + name: "Check Group: MSRV Tests (${{ matrix.os }})" + needs: [impact-linux, impact-windows] + 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: + lfs: true + - name: Download impact artifact + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + name: anvil-impact-${{ startsWith(matrix.os, 'linux') && 'Linux' || 'Windows' }} + path: target/anvil/impact + - uses: ./.github/actions/anvil-run-group + with: + group: pr-msrv + publish_commit_statuses: ${{ inputs.publish_commit_statuses }} + free-disk-space: true + impact_mode: consume + 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 e8843c60..05a93117 100644 --- a/crates/cargo-anvil/README.md +++ b/crates/cargo-anvil/README.md @@ -118,7 +118,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 @@ -229,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: @@ -250,9 +251,10 @@ sub-groups run sequentially within the one job per OS leg): - + + @@ -445,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]: ggGmYW0CYXZlMC43LjJhdIQbFhzZ8rzWNNYbuRaDSGWynFgbH4PMdoT7GNcbVwNPtPjAhvFhYvRhcoQblcBzF-_WZVYbCN9Rt1pYQLsblkUTM0oENsMbNe4wSAldeq9hZIGDa2NhcmdvLWFudmlsZTAuNS4wa2NhcmdvX2Fudmls + [__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 a5840b54..7c56dbf5 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. @@ -237,14 +237,14 @@ 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/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} ├── 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,14 @@ 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 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 @@ -400,7 +403,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 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-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 8f4a293c..54442b64 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 @@ -353,6 +367,18 @@ which tiers a group's checks consume from that cache is the catalog's concern, n wiring layer's. This means 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 `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 stage consumes the per-OS impact artifact like the other PR +groups; when the root manifest declares no MSRV, the recipe exits successfully +without running tests. +Internal pipelines set `RUSTUP_TOOLCHAIN` to their provisioned stable compiler +and `ANVIL_MSRV_TOOLCHAIN` to the provisioned `ms-prod-*` equivalent of the +declared MSRV. Supplying only the MSRV mapping is rejected because stable checks +would otherwise fall through to a public channel. Public pipelines leave both +unset and let Anvil install the declared MSRV through rustup. + ### 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 @@ -677,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 @@ -717,28 +746,44 @@ mechanics are in [local.md §4](./local.md#4-impact-scoping-via-the-anvil-impact ## 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. - -Why anvil doesn't ship a Rust install step: - -- **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. - -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. +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 +set `RUSTUP_TOOLCHAIN=ms-prod-*` so it overrides a public toolchain file or MSRV, +while their existing installer-specific setting continues to tell msrustup which +toolchain to provision. Current msrustup honors the standard override precedence. +Setup reapplies explicit components and targets from a suppressed root toolchain +file to the provisioned compiler. Unavailable options warn and are skipped, matching +rustup's native toolchain-file behavior. + +In 1ESPT compliance pipelines, msrustup still provisions the selected +`RUSTUP_TOOLCHAIN`; the Anvil setup primitive treats that caller override as +external and only reapplies applicable repository file options. Mapping a +public MSRV to an internal `ms-prod-*` channel remains a root-pipeline policy. +In OSS/non-1ESPT pipelines, the user provides rustup (for example with +`RustInstaller@1`) and Anvil installs a missing public MSRV selection through +the same setup primitive. + +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 0c60bfa6..61bed090 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` | Same default as `pr-test` | Affected-package unit and integration tests under the declared MSRV, with all features and default features. The recipe is a no-op when no root MSRV is declared. | | `pr-runtime-analysis` | Same default as `pr-fast` | Stricter-runtime correctness: `miri`, `careful`, `loom` (concurrency model checking), `bolero` (short-duration fuzzing smoke). Impact-scoped to the affected set so wall-clock is proportional to the PR's blast radius; the cheap checks (loom/bolero) self-skip when no affected crate ships their harness. | | `pr-mutants` | Linux x86_64 + Windows x86_64 + Linux aarch64 (GH) / Linux x86_64 + Windows x86_64 (ADO) | Diff-scoped mutation testing (`mutants --in-diff`). The recipe self-skips on `aarch64-pc-windows-msvc` (cargo-mutants doesn't build there), so the GH windows-arm leg is a no-op rather than a job failure. | -The 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. @@ -153,6 +163,20 @@ 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, 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 +depend on installation. + ### `pr-fast` | Check | Invocation | Source | @@ -175,10 +199,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 +216,29 @@ 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 tests) + +When the root manifest declares an MSRV, Anvil runs affected-package unit and +integration tests under that compiler with both all features and default features. +This is the ordinary test-suite execution at the minimum supported compiler; +`pr-test` runs the same affected suite through coverage instrumentation on the +catalog nightly. Other checks that use the selected stable compiler do not execute +this unit/integration suite, so an MSRV fallback does not make `pr-msrv` a duplicate. +A selecting toolchain file does not suppress the MSRV run, even when it selects the +same compiler, because the MSRV group is the authoritative minimum-version test +result. + +The group uses the same OS/architecture matrix and per-OS impact sets as `pr-test` +so cfg-gated targets and dependencies are exercised under the MSRV. It runs in +parallel with the other PR groups. When no root MSRV exists, the recipe exits +successfully without running tests. +`ANVIL_MSRV_TOOLCHAIN` may map the 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) | Check | Invocation | Source | @@ -355,7 +402,7 @@ Bucket assignments per check: | Bucket | Checks | |-----------|-----------------------------------------------------------------------------------------------------------------------| | modified | `fmt`, `cargo-sort`, `license-headers`, `ensure-no-cyclic-deps`, `ensure-no-default-features` | -| affected | `clippy`*, `llvm-cov`, `doc-test`, `examples`, `mutants-diff`, `miri`, `miri-tree-borrows`, `miri-strict-provenance`, `miri-race-coverage`, `careful`, `loom`, `bolero`, `semver-check`, `external-types`, `bench` | +| affected | `clippy`*, `llvm-cov`, `doc-test`, `examples`, `msrv-test`, `mutants-diff`, `miri`, `miri-tree-borrows`, `miri-strict-provenance`, `miri-race-coverage`, `careful`, `loom`, `bolero`, `semver-check`, `external-types`, `bench` | | required | `doc-build`, `udeps`, `cargo-hack` (feature powerset) | | unscoped | `pr-title`, `deny`, `audit`, `aprz`, `mutants-full`, `readme-check`, `spellcheck` | diff --git a/crates/cargo-anvil/docs/design/containers.md b/crates/cargo-anvil/docs/design/containers.md index 7ff5a512..8ddeb62d 100644 --- a/crates/cargo-anvil/docs/design/containers.md +++ b/crates/cargo-anvil/docs/design/containers.md @@ -136,8 +136,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 @@ -148,7 +148,8 @@ 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`; - generated `justfiles/anvil/**/*.just` recipes; - the `Containerfile`, `Containerfile.dockerignore`, entrypoint, and other static image inputs. @@ -169,8 +170,21 @@ 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 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 +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 @@ -362,7 +376,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 ec9fe696..94535d65 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 @@ -380,10 +385,28 @@ jobs: impact_mode: consume free-disk-space: true - # 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 Tests (${{ matrix.os }})" + needs: [impact-linux, impact-windows] + 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: actions/download-artifact@v4 + with: + name: anvil-impact-${{ startsWith(matrix.os, 'linux') && 'Linux' || 'Windows' }} + path: target/anvil/impact + - uses: ./.github/actions/anvil-run-group + with: + group: pr-msrv + impact_mode: consume + + # 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 @@ -396,6 +419,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 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-* jobs are skipped and the run fails at impact (we add no `if: always()` / `if: @@ -705,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 @@ -726,7 +759,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 @@ -804,25 +838,38 @@ need a per-tier side decision read the downloaded cache file directly (e.g. the upload is gated on the coverage files existing via `hashFiles(...)`), never on a job output. - The check → bucket mapping is in [checks.md §5](./checks.md#5-impact-scoping-check--include-mapping). ## 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 / tool 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 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 PowerShell process. The selector is an inline +PowerShell array expression rather than an expression-time shell subprocess, so +it has no host-OS branch and does not alter the adopter's configured shell. +Selecting toolchain files remain unoverridden so rustup processes them natively. +All matrix legs therefore use the same repository selection instead of the +different stable versions that runner images may carry, without changing +unrelated recipe environments. + +Selection follows the shared local contract: an existing `RUSTUP_TOOLCHAIN`, then a +root toolchain file with `channel` or `path`, then the root MSRV. There is no +runner-default fallback. GH-hosted runners provide the rustup proxy used to install a +selected public channel. A repository that supplies a path toolchain must provision +that path before the Anvil action runs. When an environment override or MSRV fallback +suppresses a root toolchain file, setup reapplies its explicit components and targets +to the selected compiler; unavailable options produce warnings as they do when rustup +processes the file natively. + +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: @@ -837,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 aa04d90e..638a5437 100644 --- a/crates/cargo-anvil/docs/design/local.md +++ b/crates/cargo-anvil/docs/design/local.md @@ -43,17 +43,17 @@ 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, │ │ 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/container/ optional non-recipe container assets ├── Containerfile @@ -67,12 +67,13 @@ repo/ ``` 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/`; +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. @@ -115,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 @@ -123,10 +134,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: @@ -137,8 +148,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 @@ -249,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. @@ -370,21 +384,100 @@ 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. 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. + +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. + +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 it or declare an equal +or lower minimum. A repository with missing package MSRVs, or a member minimum +newer than the root, must instead correct the root floor or add a root toolchain +file to select the single compiler used by catalog checks. Internal builds may +set `RUSTUP_TOOLCHAIN` to a provisioned toolchain +rather than adding a public compiler pin. Setting `ANVIL_MSRV_TOOLCHAIN` +without that stable override (and without a selecting repository toolchain +file) is rejected as incomplete internal configuration. An empty root +toolchain file is invalid and produces an actionable error. + +`versions.just` holds the selector as the lazy, non-exported +`_anvil_stable_toolchain_args` value. Its value is a multiline PowerShell array +expression, not a command for Just's expression-time `shell()` function. Just +interpolates the expression into an existing PowerShell recipe, where it +evaluates to one `+toolchain` string. For a repository toolchain file, it asks +`rustup show active-toolchain` from the repository root and uses rustup's +resolved concrete toolchain. For an override or MSRV fallback, it uses that +value directly. Toolchain names, including apostrophes, therefore remain +runtime string data rather than source text that needs escaping or reparsing. +`RUSTUP_TOOLCHAIN` is an input to selection, never a globally exported output. +The expression embeds an escaped PowerShell literal produced from +`justfile_directory()`, matching the setup resolver's repository root. +Recipe-local `Set-Location` calls and Just's `--working-directory` option +therefore cannot redirect selection to a nested manifest. + +Each stable recipe executes Cargo or Rust directly in its current PowerShell +recipe process, for example +`& cargo {{_anvil_stable_toolchain_args}} clippy ...`. Repository toolchain-file +provisioning runs `rustc` from the repository root, so rustup natively owns the +file's channel, path, profile, components, and targets. Default Clippy and +rustfmt setup 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. + +The public setup primitive `anvil-toolchain-stable-install` owns stable +provisioning. Rustup processes repository toolchain files directly; the private +`_anvil-resolve-stable` implementation handles public MSRV installation and +internal mapping. Leaf setup recipes reach the primitive directly or through +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, 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 +features. A selecting toolchain file does not suppress this minimum-version run. +`ANVIL_MSRV_TOOLCHAIN` optionally maps the declared MSRV to a provisioned internal +toolchain. Without a root MSRV the recipe is a no-op. + +`anvil-tool-rustc-validate-prereqs` verifies the selected compiler and the +workspace MSRV compatibility rule. Per-check toolchain requirements (for +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/ado.rs b/crates/cargo-anvil/src/anvil/artifacts/ado.rs index 596888f6..99958e1d 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/ado.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/ado.rs @@ -49,6 +49,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", @@ -166,6 +167,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"), @@ -212,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] @@ -381,8 +394,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/container.rs b/crates/cargo-anvil/src/anvil/artifacts/container.rs index e68fcdb9..3ed8e30e 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/container.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/container.rs @@ -188,7 +188,7 @@ 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("justfiles/anvil/versions.just"), "tool_version := \"1\"\n"); write( &root.join(CONTAINERFILE_PATH), @@ -226,11 +226,14 @@ 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")); 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")); } @@ -359,6 +362,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 +400,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/github.rs b/crates/cargo-anvil/src/anvil/artifacts/github.rs index e4977457..0975a0b7 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", @@ -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-]+$")); @@ -374,6 +377,7 @@ export -f just "impact-windows:", "pr-fast:", "pr-test:", + "pr-msrv:", "pr-runtime-analysis:", "pr-mutants:", ] { @@ -386,6 +390,8 @@ export -f just ); } assert!(PR_IMPL_WORKFLOW.contains("needs: [impact-linux, impact-windows]")); + assert!(!PR_IMPL_WORKFLOW.contains("msrv_test_required")); + assert!(PR_IMPL_WORKFLOW.contains("Check Group: MSRV Tests (${{ matrix.os }})")); assert!(PR_IMPL_WORKFLOW.contains("os: [linux, windows, linux-arm, windows-arm]")); assert!(!PR_IMPL_WORKFLOW.contains("fromJSON")); assert!(PR_IMPL_WORKFLOW.contains("PR_TITLE")); @@ -419,7 +425,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!( @@ -436,8 +442,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 b870538a..e1576954 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/justfile.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/justfile.rs @@ -138,6 +138,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", @@ -154,6 +155,7 @@ const GROUP_FILES: &[(&str, &str)] = split_recipe_files!( "groups", [ "pr-fast", + "pr-msrv", "pr-slow", "pr-test", "pr-runtime-analysis", @@ -307,6 +309,7 @@ mod tests { ("miri-race-coverage", Affected), ("miri-strict-provenance", Affected), ("miri-tree-borrows", Affected), + ("msrv-test", Affected), ("mutants-diff", Affected), ("mutants-full", Unscoped), ("pr-title", Unscoped), @@ -327,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")); } @@ -532,7 +536,7 @@ mod tests { let unscoped = EXPECTED_CHECK_POLICY.len() - scoped; assert_eq!( (scoped, unscoped), - (23, 7), + (24, 7), "impact scoped/unscoped split changed; update EXPECTED_CHECK_POLICY deliberately" ); } @@ -551,7 +555,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}'"); } @@ -564,6 +568,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:", @@ -575,12 +580,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" + )); // PR group recipes list their 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", ] { @@ -617,12 +625,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)" @@ -729,6 +738,724 @@ 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$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")); + } + + #[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 { + 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("\"+") + || toolchain.starts_with("{{_anvil_stable_toolchain_args}}"), + "{path} invokes Cargo without an explicit toolchain selection: {line}" + ); + } + } + } + } + + #[cfg(not(miri))] + mod stable_toolchain_resolver_tests { + use std::path::Path; + use std::process::{Command, Output}; + use std::{env, fs}; + + use tempfile::TempDir; + + use super::{TOOLS_JUST, VERSIONS_JUST}; + + fn fixture(cargo_toml: &str) -> TempDir { + let temp = TempDir::new().expect("temporary repository must be creatable"); + fs::write(temp.path().join("Cargo.toml"), cargo_toml).expect("manifest fixture must be writable"); + fs::write(temp.path().join("versions.just"), VERSIONS_JUST).expect("versions fixture must be writable"); + fs::write(temp.path().join("tools.just"), TOOLS_JUST).expect("tools fixture must be writable"); + 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", + " $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", + "\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 + .args(["--justfile"]) + .arg(root.join("Justfile")) + .current_dir(root) + .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.arg("anvil-toolchain-stable-install"); + } + ["-ValidateWorkspaceMsrv"] => { + command.args(["_anvil-resolve-stable", "validate-workspace-msrv"]); + } + ["-MsrvToolchain"] => { + command.args(["_anvil-resolve-stable", "msrv"]); + } + ["-InstallMsrvIfNeeded"] => { + command.args(["_anvil-resolve-stable", "install-msrv"]); + } + other => panic!("unsupported resolver arguments in test: {other:?}"), + } + match rustup_toolchain { + Some(value) => { + command.env("RUSTUP_TOOLCHAIN", value); + } + None => { + command.env_remove("RUSTUP_TOOLCHAIN"); + } + } + command + .output() + .expect("just must be available to test generated toolchain selection") + } + + 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() + } + + fn normalized_diagnostic(output: &Output) -> String { + format!( + "{}{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ) + .split_whitespace() + .collect::>() + .join(" ") + } + + #[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')"); + + 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("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; + + 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())); + 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]\ncomponents = [\"clippy\"]\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) + ); + 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(), + 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"); + + let output = run(temp.path(), &[], None); + assert!(!output.status.success(), "empty toolchain file must fail resolution"); + let diagnostic = normalized_diagnostic(&output); + assert!( + diagnostic.contains("rustup could not resolve") + && (diagnostic.contains("empty toolchain override") || diagnostic.contains("error parsing override file")), + "unexpected resolver diagnostic: {diagnostic}" + ); + } + } + + #[test] + fn installs_default_components_on_the_selected_stable_toolchain() { + if !tools_available() { + return; + } + let temp = fixture("[workspace.package]\nrust-version = \"1.93\"\n"); + 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; + + 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 = run_install(Some("custom-toolchain")); + assert!( + output.status.success(), + "override-selected component installation failed: {}", + normalized_diagnostic(&output) + ); + 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()); + let diagnostic = normalized_diagnostic(&output); + assert!( + diagnostic.contains("no root") && diagnostic.contains("[workspace.package]"), + "unexpected resolver diagnostic: {diagnostic}" + ); + + let workspace = + fixture("[workspace]\nresolver = \"2\"\nmembers = [\"a\", \"b\"]\n[workspace.package]\nrust-version = \"1.92\"\n"); + 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"), + 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 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!( + diagnostic.contains("newer than the root MSRV") && diagnostic.contains("b (1.93)") && !diagnostic.contains("a (1.91)"), + "unexpected resolver diagnostic: {diagnostic}" + ); + + 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_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) + .arg("_anvil-test-stable-args") + .env("ANVIL_MSRV_TOOLCHAIN", "ms-prod-1.93") + .output() + .expect("just must be available to test the generated toolchain selection") + }; + + 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}" + ); + } + + #[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"); + 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(root); + command + .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")); + 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"); + + 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!(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")); + + 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") + && diagnostic.contains("ANVIL_MSRV_TOOLCHAIN"), + "unexpected mapped MSRV diagnostic: {diagnostic}" + ); + } + + #[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"); + let output = run(no_msrv.path(), &["-MsrvToolchain"], None); + assert!( + output.status.success(), + "a repository without a declared MSRV must skip the MSRV test" + ); + 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 resolve_msrv = |root: &Path| { + let output = run(root, &["-MsrvToolchain"], None); + assert!( + output.status.success(), + "MSRV resolution failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + String::from_utf8(output.stdout) + .expect("resolver output must be UTF-8") + .trim() + .to_owned() + }; + + 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_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_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!(resolve_msrv(root), "1.93"); + let output = command(root) + .args(["_anvil-resolve-stable", "msrv"]) + .env("ANVIL_MSRV_TOOLCHAIN", "ms-prod-1.93") + .output() + .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(), + "ms-prod-1.93" + ); + + fs::write(root.join("rust-toolchain.toml"), "[toolchain]\npath = \"toolchains/custom\"\n") + .expect("toolchain fixture must be writable"); + assert_eq!(resolve_msrv(root), "1.93"); + } + } + #[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 73a800e1..34f6bff1 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/mod.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/mod.rs @@ -42,7 +42,7 @@ use crate::catalog::Artifact; #[must_use] pub(crate) fn impact_mode(group: &str) -> &'static str { match group { - "pr-fast" | "pr-test" | "pr-runtime-analysis" | "pr-mutants" => "consume", + "pr-fast" | "pr-test" | "pr-msrv" | "pr-runtime-analysis" | "pr-mutants" => "consume", "scheduled-test" | "scheduled-advisories" | "scheduled-runtime-analysis" | "scheduled-exhaustive" => "off", other => { panic!("impact_mode: unclassified group '{other}'; add it to the pr/scheduled arms in artifacts::impact_mode") @@ -101,6 +101,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 28e84518..a8853f07 100644 --- a/crates/cargo-anvil/src/lib.rs +++ b/crates/cargo-anvil/src/lib.rs @@ -119,7 +119,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 @@ -230,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
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-testdual feature-config tests under the declared MSRV
pr-runtime-analysismirilibtest, not nextest
carefulself-cleans on a toolchain bump
loomopt-in targets only
//! @@ -251,9 +252,10 @@ //! //! //! -//! +//! //! //! +//! //! //! //! 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 c1f7d613..37600d14 100644 --- a/crates/cargo-anvil/templates/ado/pr-stages.yml +++ b/crates/cargo-anvil/templates/ado/pr-stages.yml @@ -109,9 +109,10 @@ stages: steps: - template: steps/pr-fast.yml - # The PR-tier slow checks are split into three independent stages - # (pr_test, 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 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 stage and downloads the per-OS impact artifact. ADO has no # hosted ARM agents, so the matrix stays Linux + Windows x86_64. @@ -163,6 +164,29 @@ stages: summaryFileLocation: target/coverage/cobertura-*.xml failIfCoverageEmpty: false + - stage: pr_msrv + displayName: anvil pr-msrv (MSRV tests) + dependsOn: [impact] + jobs: + - template: steps/job.yml + parameters: + name: linux + pool: ${{ parameters.linuxPool }} + inputArtifacts: + - name: anvil-impact-linux + path: target/anvil/impact + steps: + - template: steps/pr-msrv.yml + - template: steps/job.yml + parameters: + name: windows + pool: ${{ parameters.windowsPool }} + inputArtifacts: + - name: anvil-impact-windows + path: target/anvil/impact + steps: + - template: steps/pr-msrv.yml + - stage: pr_runtime_analysis displayName: anvil pr-runtime-analysis (miri + careful) dependsOn: [impact] diff --git a/crates/cargo-anvil/templates/ado/steps/setup.yml b/crates/cargo-anvil/templates/ado/steps/setup.yml index 3e21c60d..c7352dd5 100644 --- a/crates/cargo-anvil/templates/ado/steps/setup.yml +++ b/crates/cargo-anvil/templates/ado/steps/setup.yml @@ -18,15 +18,27 @@ parameters: type: string default: '' steps: - - bash: echo "##vso[task.setvariable variable=anvil_rustc_version]$(rustc --version | awk '{print $2}')" - displayName: anvil setup (capture rustc version) + - bash: | + if ! command -v just >/dev/null 2>&1 ; then + cargo install --locked just + fi + displayName: anvil setup (install just) + + - pwsh: | + $version = (& just _anvil-stable-rustc-version | Select-Object -Last 1) + if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($version)) { + throw 'anvil: failed to provision the selected stable toolchain' + } + $version = $version.Trim() + Write-Host "##vso[task.setvariable variable=anvil_rustc_version]$version" + displayName: anvil setup (provision stable toolchain and capture version) - task: Cache@2 inputs: # Same key shape as the GitHub side: OS + arch + rustc version + # lockfile hashes + catalog hash. Including arch keeps any # future ARM pool an adopter adds from colliding with x86_64 on # the same OS namespace. - key: 'anvil-v1 | "$(Agent.OS)" | "$(Agent.OSArchitecture)" | rust$(anvil_rustc_version) | Cargo.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)" @@ -38,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 fa68b18f..5709437b 100644 --- a/crates/cargo-anvil/templates/anvil/container/Containerfile +++ b/crates/cargo-anvil/templates/anvil/container/Containerfile @@ -49,10 +49,6 @@ 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 \ diff --git a/crates/cargo-anvil/templates/anvil/container/Containerfile.dockerignore b/crates/cargo-anvil/templates/anvil/container/Containerfile.dockerignore index 6566e657..c242cb21 100644 --- a/crates/cargo-anvil/templates/anvil/container/Containerfile.dockerignore +++ b/crates/cargo-anvil/templates/anvil/container/Containerfile.dockerignore @@ -16,6 +16,8 @@ # 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 diff --git a/crates/cargo-anvil/templates/anvil/container/image-id.ps1 b/crates/cargo-anvil/templates/anvil/container/image-id.ps1 index dfc6a53f..3ca2e859 100644 --- a/crates/cargo-anvil/templates/anvil/container/image-id.ps1 +++ b/crates/cargo-anvil/templates/anvil/container/image-id.ps1 @@ -13,12 +13,11 @@ if ($LASTEXITCODE -ne 0 -or -not $repoRoot) { throw 'anvil-container must run from a Git repository.' } -$inputs = @( - 'rust-toolchain.toml' -) -$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.' +$inputs = @('Cargo.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..2dabc348 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) +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/github/pr-impl-workflow.yml b/crates/cargo-anvil/templates/github/pr-impl-workflow.yml index 116a15ce..911c0f29 100644 --- a/crates/cargo-anvil/templates/github/pr-impl-workflow.yml +++ b/crates/cargo-anvil/templates/github/pr-impl-workflow.yml @@ -222,6 +222,33 @@ jobs: token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: false + pr-msrv: + name: "Check Group: MSRV Tests (${{ matrix.os }})" + needs: [impact-linux, impact-windows] + 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: + lfs: true + - name: Download impact artifact + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + name: anvil-impact-${{ startsWith(matrix.os, 'linux') && 'Linux' || 'Windows' }} + path: target/anvil/impact + - uses: ./.github/actions/anvil-run-group + with: + group: pr-msrv + publish_commit_statuses: ${{ inputs.publish_commit_statuses }} + free-disk-space: true + impact_mode: consume + 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 f1557210..2b4c47a5 100644 --- a/crates/cargo-anvil/templates/github/setup-action.yml +++ b/crates/cargo-anvil/templates/github/setup-action.yml @@ -77,9 +77,29 @@ runs: } "Free after cleanup (GiB): $(Get-FreeDiskGiB)" - - id: rustc-version + # Stable provisioning is part of the generated Just setup graph, so + # bootstrap Just before asking it for the selected compiler version. + # cargo-binstall keeps this cold path fast. + - name: Install cargo-binstall + uses: cargo-bins/cargo-binstall@v1.21.0 # immutable release, the tag cannot be moved + + - name: Install just shell: bash - run: echo "version=$(rustc --version | awk '{print $2}')" >> "$GITHUB_OUTPUT" + run: | + if ! command -v just >/dev/null 2>&1 ; then + cargo binstall --no-confirm --locked just || cargo install --locked just + fi + + - name: Provision stable toolchain and capture version + id: rustc-version + shell: pwsh -NoProfile -Command ". '{0}'" + run: | + $version = (& just _anvil-stable-rustc-version | Select-Object -Last 1) + if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($version)) { + throw 'anvil: failed to provision the selected stable toolchain' + } + $version = $version.Trim() + "version=$version" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append - name: Restore cargo cache id: cargo-cache uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 @@ -100,9 +120,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 @@ -126,41 +146,11 @@ runs: ~/.cargo/.crates.toml ~/.cargo/.crates2.json - # 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 - # 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/templates/justfiles/anvil/checks/aprz.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/aprz.just index 1627c3a5..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.' } } - 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 20de2046..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' - & 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 2f4d42f7..b26880a8 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 + & 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 @@ -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/bolero.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/bolero.just index 37f7d21b..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 = & 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/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..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 } - & 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 a0af1ccb..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 } - 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 c9167c91..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 } - & 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 c52b613a..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' - & 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 9250d41a..a8edeef7 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 + & 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 @@ -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 1954ae4a..ca6ade8c 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/doc-test.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/doc-test.just @@ -20,14 +20,14 @@ 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 + & cargo {{_anvil_stable_toolchain_args}} test --doc @pkg --all-features --locked if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - & 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. [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/ensure-no-cyclic-deps.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/ensure-no-cyclic-deps.just index 48ef3e7c..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 } - 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 c5668204..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 } - 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 698b1115..fe447b7f 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/examples.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/examples.just @@ -14,12 +14,12 @@ 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 + & 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. [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/external-types.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/external-types.just index 33c0593d..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 = & 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 989f57b4..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 } - 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 ab39d671..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 = & 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]) } - 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 0f88aba6..845381bc 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 = & 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)" - & 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 } } @@ -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/checks/msrv-test.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/msrv-test.just new file mode 100644 index 00000000..362e470c --- /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. +[script("pwsh", "-NoProfile")] +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 = (& "{{ 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 + } + $pkg = @(if ($include) { -split $include } 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 declared MSRV toolchain only when this check is required. +[group("anvil-setup")] +[script("pwsh", "-NoProfile")] +anvil-msrv-test-setup installer="install": + & "{{ 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 = (& "{{ just_executable() }}" _anvil-resolve-stable msrv | 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 toolchain '$toolchain' is unavailable; $hint" + 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 25c98ba9..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 } - 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 a914f1eb..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 } - & 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 1d258e25..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 = & 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) } - & 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 4f7112b6..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 = & 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 = @(& 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 15e734b4..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') { - cargo spellcheck --cfg spellcheck.toml check --code 1 + & cargo {{_anvil_stable_toolchain_args}} spellcheck --cfg spellcheck.toml check --code 1 } else { - 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/groups/pr-msrv.just b/crates/cargo-anvil/templates/justfiles/anvil/groups/pr-msrv.just new file mode 100644 index 00000000..d35d01ac --- /dev/null +++ b/crates/cargo-anvil/templates/justfiles/anvil/groups/pr-msrv.just @@ -0,0 +1,23 @@ +# 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-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-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 75ad63c1..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,32 +6,33 @@ # 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 three 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 # 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/impact.just b/crates/cargo-anvil/templates/justfiles/anvil/impact.just index 42d74502..4f05394c 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/impact.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/impact.just @@ -235,19 +235,29 @@ _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. + $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 + } + $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 +290,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 +322,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 = & 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 @@ -432,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 = (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 @@ -509,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 = & 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/mod.just b/crates/cargo-anvil/templates/justfiles/anvil/mod.just index ba229814..8a4c637e 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/mod.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/mod.just @@ -60,6 +60,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' @@ -69,6 +70,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/templates/justfiles/anvil/tools.just b/crates/cargo-anvil/templates/justfiles/anvil/tools.just index cb6aab14..0d6e45f9 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")] @@ -144,6 +143,10 @@ anvil-tool-rustc-validate-prereqs: Write-Error 'anvil: rustc not found. Install via rustup: https://rustup.rs' exit 1 } + & "{{ just_executable() }}" _anvil-resolve-stable validate-workspace-msrv + if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE + } # Validate that PowerShell Core is available. [group("anvil-setup")] @@ -169,7 +172,227 @@ anvil-tool-pwsh-validate-prereqs: # Private helpers (install/check primitives) # ============================================================================ -# _install-tool: install a cargo subcommand at the catalog version, +# 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' + Set-StrictMode -Version 3 + + $action = '{{action}}' + $validActions = @( + 'resolve', + 'for-environment', + 'install', + 'validate-workspace-msrv', + 'msrv', + 'install-msrv' + ) + if ($action -notin $validActions) { + Write-Error "_anvil-resolve-stable: unknown action '$action'" + exit 2 + } + + $repoRoot = '{{justfile_directory()}}' + + 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) + ) + } + + 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' + $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" + } + $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" + } + + $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 + -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' + } + } + + 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' + Mapped = $true + } + } + return [pscustomobject]@{ Value = $msrv; Source = 'msrv'; Mapped = $false } + } + + function Install-Toolchain([object] $toolchain, [string] $label) { + $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: failed to install $label toolchain '$($toolchain.Value)'" + } + } + } + + if ($action -eq 'validate-workspace-msrv') { + Assert-CompleteInternalToolchainConfiguration + $skipWorkspaceMsrvValidation = -not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN) + if (-not $skipWorkspaceMsrvValidation -and -not (Test-RootToolchainFile)) { + 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" + } + } else { + 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' } + } elseif (Test-RootToolchainFile) { + [pscustomobject]@{ Value = $null; Source = 'file' } + } else { + [pscustomobject]@{ Value = Get-RootMsrv; Source = 'msrv' } + } + + if ($action -eq 'install') { + 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 + } + + if ($action -eq 'for-environment' -and $selection.Source -eq 'file') { + '' + } else { + $selection.Value + } + +# Provision the selected stable toolchain. Repository toolchain files are +# processed natively by rustup. This is the shared prerequisite for setup paths +# that execute stable Cargo/Rust or install Cargo tools; Just deduplicates it +# 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. @@ -179,7 +402,7 @@ anvil-tool-pwsh-validate-prereqs: # `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}}' @@ -199,7 +422,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 (& cargo {{_anvil_stable_toolchain_args}} install --list 2>$null)) { if ($line -match $pattern) { $installed = $Matches[1]; break } } if ($installed) { @@ -217,7 +440,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 + & cargo {{_anvil_stable_toolchain_args}} install --locked cargo-binstall if ($LASTEXITCODE -ne 0) { Write-Error 'cargo-binstall bootstrap failed' exit $LASTEXITCODE @@ -231,7 +454,7 @@ _install-tool name version installer source_prereq="": $binstallArgs += @('--disable-strategies', 'compile') } $binstallArgs += @($name, '--version', "=$version") - 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 } @@ -239,12 +462,15 @@ _install-tool name version installer source_prereq="": & "{{just_executable()}}" $sourcePrereq if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } } - 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 } +# 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")] @@ -255,7 +481,7 @@ _check-tool name version: $installed = $null $pattern = '^' + [regex]::Escape($name) + ' v(\S+):' - foreach ($line in (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) { @@ -306,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 @@ -324,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` @@ -336,14 +570,22 @@ _install-component toolchain component: } if ($null -ne $subcommand) { if ($toolchain -eq 'default') { - & cargo $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 --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 } @@ -367,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 @@ -396,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' } @@ -404,14 +654,22 @@ _check-component toolchain component: } if ($null -ne $subcommand) { if ($toolchain -eq 'default') { - & cargo $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 --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 } @@ -433,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 } @@ -462,14 +726,13 @@ 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")] -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")] @@ -477,7 +740,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")] @@ -564,12 +827,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. @@ -681,12 +944,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 564a3b90..1641ee8a 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/versions.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/versions.just @@ -5,6 +5,7 @@ # 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. + # # Everything anvil installs (cargo subcommands + rustup toolchains) # is pinned here. The pinning policy: @@ -30,6 +31,78 @@ # Rustup toolchains # ============================================================================ +# 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("@(& {\n$RepoRoot = '" + replace(justfile_directory(), "'", "''") + "'\n\n" + ''' +$ErrorActionPreference = 'Stop' +Set-StrictMode -Version 3 + +if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { + Write-Output ('+' + $env:RUSTUP_TOOLCHAIN) + return +} + +if ((Test-Path -LiteralPath (Join-Path $RepoRoot 'rust-toolchain') -PathType Leaf) -or + (Test-Path -LiteralPath (Join-Path $RepoRoot 'rust-toolchain.toml') -PathType Leaf)) { + Push-Location $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 + } + $resolved = [string] $active + $sourceMarker = $resolved.LastIndexOf(' (') + if ($sourceMarker -ge 0) { + $resolved = $resolved.Substring(0, $sourceMarker) + } + Write-Output ('+' + $resolved) + return +} + +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-Output ('+' + $msrv) +}) +''') + # 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 2aef9804..9260fa06 100644 --- a/crates/cargo-anvil/tests/container_customization.rs +++ b/crates/cargo-anvil/tests/container_customization.rs @@ -532,6 +532,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"); @@ -547,6 +548,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..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,12 +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); } + 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 { @@ -136,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 42363af8..aaf8565f 100644 --- a/crates/cargo-anvil/tests/impact.rs +++ b/crates/cargo-anvil/tests/impact.rs @@ -169,7 +169,11 @@ fn workspace_at_base() -> TempDir { // A minimal two-crate workspace cargo-delta can snapshot. 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 windows-shell := [\"pwsh\", \"-NoProfile\", \"-Command\"]\n", ); write( &root.join("crates/alpha/Cargo.toml"), @@ -836,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"), @@ -879,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)] { @@ -971,6 +988,68 @@ 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("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 @@ -980,16 +1059,22 @@ 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)] { 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() ); @@ -1287,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"), @@ -1314,23 +1403,27 @@ 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(); - // 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. + 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 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\ @@ -1344,7 +1437,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", @@ -1368,35 +1461,38 @@ 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 the caller chose -- restoring a prior value, or removing the - // override entirely when the caller set none. 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. The recipe introduces RUSTUP_TOOLCHAIN - // only for the baseline, then must REMOVE it so the current snapshot runs - // under the caller's (unset) toolchain rather than a leaked 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(), "".to_owned()], - "with no caller RUSTUP_TOOLCHAIN, the recipe must remove the baseline override so the current snapshot sees it unset" + vec!["1.97".to_owned(), "1.97".to_owned()], + "baseline and current snapshots must both use Anvil's selected MSRV" + ); + + // Case C: rustup resolves the current checkout's toolchain file once and + // the concrete identifier selects both snapshots. + let with_file = snapshot_toolchain_probe(None, true); + assert_eq!( + with_file, + vec!["file-active-toolchain".to_owned(), "file-active-toolchain".to_owned()], + "baseline and current snapshots must both use the current file's concrete active toolchain" ); } diff --git a/crates/cargo-anvil/tests/recipe_contracts.rs b/crates/cargo-anvil/tests/recipe_contracts.rs index 31190358..4dbe8e7d 100644 --- a/crates/cargo-anvil/tests/recipe_contracts.rs +++ b/crates/cargo-anvil/tests/recipe_contracts.rs @@ -26,10 +26,22 @@ const EXTERNAL_TYPES: &str = include_str!("../templates/justfiles/anvil/checks/e const TOOLS: &str = include_str!("../templates/justfiles/anvil/tools.just"); const VERSIONS: &str = include_str!("../templates/justfiles/anvil/versions.just"); 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 } +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) { @@ -137,11 +149,12 @@ 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); @@ -155,7 +168,7 @@ 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", ); let bin = tmp.path().join("fake-bin"); @@ -172,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 { @@ -191,6 +212,48 @@ fn assert_failed(output: &Output, context: &str) { ); } +#[test] +fn stable_command_runs_directly_with_explicit_toolchain_arguments() { + if !tools_available() { + return; + } + let tmp = fixture(&[("versions.just", VERSIONS), ("tools.just", TOOLS)], &[]); + 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( + tmp.path(), + &["_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(), + "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(), "+test-stable doc2readme --check"); + assert_eq!( + fs::canonicalize(fs::read_to_string(cwd_log).unwrap().trim()).unwrap(), + fs::canonicalize(tmp.path()).unwrap(), + "the command must run directly in its recipe process" + ); +} + #[test] fn impact_format_resolves_directory_aliases_and_fails_hard() { if !tools_available() { 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 c00d04a3..ed5ab45a 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap @@ -54,10 +54,6 @@ 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 \ @@ -92,6 +88,8 @@ 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 @@ -372,12 +370,11 @@ if ($LASTEXITCODE -ne 0 -or -not $repoRoot) { throw 'anvil-container must run from a Git repository.' } -$inputs = @( - 'rust-toolchain.toml' -) -$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.' +$inputs = @('Cargo.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 +441,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 +453,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) +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 @@ -1421,9 +1417,10 @@ stages: steps: - template: steps/pr-fast.yml - # The PR-tier slow checks are split into three independent stages - # (pr_test, 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 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 stage and downloads the per-OS impact artifact. ADO has no # hosted ARM agents, so the matrix stays Linux + Windows x86_64. @@ -1475,6 +1472,29 @@ stages: summaryFileLocation: target/coverage/cobertura-*.xml failIfCoverageEmpty: false + - stage: pr_msrv + displayName: anvil pr-msrv (MSRV tests) + dependsOn: [impact] + jobs: + - template: steps/job.yml + parameters: + name: linux + pool: ${{ parameters.linuxPool }} + inputArtifacts: + - name: anvil-impact-linux + path: target/anvil/impact + steps: + - template: steps/pr-msrv.yml + - template: steps/job.yml + parameters: + name: windows + pool: ${{ parameters.windowsPool }} + inputArtifacts: + - name: anvil-impact-windows + path: target/anvil/impact + steps: + - template: steps/pr-msrv.yml + - stage: pr_runtime_analysis displayName: anvil pr-runtime-analysis (miri + careful) dependsOn: [impact] @@ -1905,6 +1925,71 @@ steps: # workflow-level CARGO_INCREMENTAL=0. CARGO_INCREMENTAL: "0" +=== .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, ...). +# +# The impact set reaches this group as the downloaded target/anvil/impact/ +# cache. A PR group job downloads the `anvil-impact-` artifact into +# target/anvil/impact/ (via job.yml's inputArtifacts) and the scoped checks +# read that cache via their `anvil-impact` dependency -- the same code path as +# a local run. Scheduled group jobs download nothing and run full-workspace +# (see the bash step). +steps: + - template: setup.yml + parameters: + group: pr-msrv + # ADO has no PR-title predefined variable: `System.PullRequest.Title` + # does NOT exist, so `$(System.PullRequest.Title)` would expand to the + # literal unexpanded macro text (non-empty) and make anvil-pr-title + # validate that string. Resolve the real title from the REST API on PR + # builds and publish it as the PR_TITLE pipeline variable. Only + # anvil-pr-title (in pr-fast) consults it; other groups ignore it, but we + # resolve uniformly to keep group.yml the same across groups. Non-PR builds + # skip; a known PR whose metadata cannot be retrieved fails closed. + - pwsh: | + $ErrorActionPreference = 'Stop' + $prId = $env:SYSTEM_PULLREQUEST_PULLREQUESTID + if (-not $prId) { + Write-Host 'anvil: not a PR build; leaving PR_TITLE empty' + Write-Host '##vso[task.setvariable variable=PR_TITLE]' + exit 0 + } + $uri = "$($env:SYSTEM_COLLECTIONURI)$($env:SYSTEM_TEAMPROJECTID)/_apis/git/repositories/$($env:BUILD_REPOSITORY_ID)/pullRequests/${prId}?api-version=7.0" + try { + $resp = Invoke-RestMethod -Uri $uri -Headers @{ Authorization = "Bearer $($env:SYSTEM_ACCESSTOKEN)" } + Write-Host "##vso[task.setvariable variable=PR_TITLE]$($resp.title)" + } catch { + Write-Error "anvil: could not resolve PR title for PR $prId ($_). Ensure the job can access the repository OAuth token." + exit 1 + } + displayName: anvil-pr-msrv (resolve PR title) + env: + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + - bash: | + set -euo pipefail + # The impact mode is fixed by group class at emit time -- never probed from a + # marker file. PR group jobs downloaded the target/anvil/impact artifact + # and trust it verbatim (consume: anvil-impact no-ops -- no snapshot, + # cargo-delta, or base ref); scheduled group jobs force off so every + # tier runs full-workspace. + export ANVIL_IMPACT=consume + just anvil-pr-msrv + displayName: anvil-pr-msrv + env: + # Resolved by the preceding step; empty only outside PR builds. + PR_TITLE: $(PR_TITLE) + # Disable cargo incremental compilation in CI: the incremental dir is + # not part of the anvil-setup cache (see setup.yml), so generating it is + # pure cost with no cross-run benefit. Mirrors the GitHub impl workflows' + # workflow-level CARGO_INCREMENTAL=0. + CARGO_INCREMENTAL: "0" + === .pipelines/anvil/steps/pr-mutants.yml === # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. @@ -2381,15 +2466,27 @@ parameters: type: string default: '' steps: - - bash: echo "##vso[task.setvariable variable=anvil_rustc_version]$(rustc --version | awk '{print $2}')" - displayName: anvil setup (capture rustc version) + - bash: | + if ! command -v just >/dev/null 2>&1 ; then + cargo install --locked just + fi + displayName: anvil setup (install just) + + - pwsh: | + $version = (& just _anvil-stable-rustc-version | Select-Object -Last 1) + if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($version)) { + throw 'anvil: failed to provision the selected stable toolchain' + } + $version = $version.Trim() + Write-Host "##vso[task.setvariable variable=anvil_rustc_version]$version" + displayName: anvil setup (provision stable toolchain and capture version) - task: Cache@2 inputs: # Same key shape as the GitHub side: OS + arch + rustc version + # lockfile hashes + catalog hash. Including arch keeps any # future ARM pool an adopter adds from colliding with x86_64 on # the same OS namespace. - key: 'anvil-v1 | "$(Agent.OS)" | "$(Agent.OSArchitecture)" | rust$(anvil_rustc_version) | Cargo.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)" @@ -2401,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 @@ -2713,7 +2804,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 + & 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. @@ -2739,7 +2830,7 @@ anvil-aprz-validate-prereqs: anvil-tool-cargo-aprz-validate-prereqs [script("pwsh", "-NoProfile")] anvil-audit: anvil-audit-validate-prereqs $ErrorActionPreference = 'Stop' - & cargo audit + & cargo {{_anvil_stable_toolchain_args}} audit if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-audit` recipe. @@ -2767,7 +2858,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 + & 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 @@ -2775,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")] @@ -2838,7 +2929,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 = & cargo {{_anvil_stable_toolchain_args}} metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-bolero: cargo metadata failed' exit $LASTEXITCODE @@ -2965,7 +3056,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 @@ -3002,7 +3093,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 + & 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. @@ -3041,7 +3132,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 + & 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. @@ -3074,7 +3165,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' + & 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. @@ -3102,7 +3193,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 + & cargo {{_anvil_stable_toolchain_args}} deny check if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-deny` recipe. @@ -3133,7 +3224,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 + & 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 @@ -3142,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")] @@ -3171,14 +3262,14 @@ 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 + & cargo {{_anvil_stable_toolchain_args}} test --doc @pkg --all-features --locked if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - & 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. [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")] @@ -3201,7 +3292,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 + & 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. @@ -3229,7 +3320,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 + & 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. @@ -3257,12 +3348,12 @@ 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 + & 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. [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")] @@ -3302,7 +3393,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 = & 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 @@ -3430,7 +3521,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 + & cargo {{_anvil_stable_toolchain_args}} heather if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-license-headers` recipe. @@ -3481,7 +3572,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 = & 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 @@ -3612,7 +3703,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 + & 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) --- @@ -3683,7 +3774,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 = & 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 @@ -3736,7 +3827,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 + & 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 } } @@ -3746,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")] @@ -3928,6 +4019,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. +[script("pwsh", "-NoProfile")] +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 = (& "{{ 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 + } + $pkg = @(if ($include) { -split $include } 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 declared MSRV toolchain only when this check is required. +[group("anvil-setup")] +[script("pwsh", "-NoProfile")] +anvil-msrv-test-setup installer="install": + & "{{ 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 = (& "{{ just_executable() }}" _anvil-resolve-stable msrv | 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 toolchain '$toolchain' is unavailable; $hint" + exit $LASTEXITCODE + } + === justfiles/anvil/checks/mutants-diff.just === # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. @@ -3974,7 +4115,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 + & cargo {{_anvil_stable_toolchain_args}} mutants --in-diff $diff_path --no-shuffle --jobs 0 if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # --- pr-mutants members --- @@ -4007,7 +4148,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 + & 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; @@ -4155,7 +4296,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 = & 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 @@ -4197,7 +4338,7 @@ anvil-readme-check: anvil-readme-check-validate-prereqs } $args = @('doc2readme', '--check') if ($relTemplate) { $args += @('--template', $relTemplate) } - & cargo @args + & cargo {{_anvil_stable_toolchain_args}} @args if ($LASTEXITCODE -ne 0) { $hadFailure = $true } } finally { Pop-Location @@ -4281,7 +4422,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 = & 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 @@ -4358,7 +4499,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 = @(& cargo {{_anvil_stable_toolchain_args}} semver-checks --package $p --baseline-rev $base 2>&1) $semverExit = $LASTEXITCODE $output = ($outputLines | Out-String) if ($semverExit -eq 100) { @@ -4497,9 +4638,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 + & cargo {{_anvil_stable_toolchain_args}} spellcheck --cfg spellcheck.toml check --code 1 } else { - cargo spellcheck check --code 1 + & cargo {{_anvil_stable_toolchain_args}} spellcheck check --code 1 } if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } @@ -4666,6 +4807,31 @@ 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-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-tool-cargo-delta-validate-prereqs \ + anvil-msrv-test-validate-prereqs + === justfiles/anvil/groups/pr-mutants.just === # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. @@ -4735,32 +4901,33 @@ 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 three 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 # 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) @@ -4768,6 +4935,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 @@ -5333,19 +5501,29 @@ _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. + $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 + } + $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 @@ -5378,14 +5556,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 { @@ -5412,7 +5588,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 = & 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 @@ -5530,7 +5706,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 = (& 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 @@ -5607,7 +5783,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 = & cargo {{_anvil_stable_toolchain_args}} metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-impact: cargo metadata failed' exit $LASTEXITCODE @@ -5786,6 +5962,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' @@ -5795,6 +5972,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' @@ -6138,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")] @@ -6154,6 +6331,10 @@ anvil-tool-rustc-validate-prereqs: Write-Error 'anvil: rustc not found. Install via rustup: https://rustup.rs' exit 1 } + & "{{ just_executable() }}" _anvil-resolve-stable validate-workspace-msrv + if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE + } # Validate that PowerShell Core is available. [group("anvil-setup")] @@ -6179,7 +6360,227 @@ anvil-tool-pwsh-validate-prereqs: # Private helpers (install/check primitives) # ============================================================================ -# _install-tool: install a cargo subcommand at the catalog version, +# 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' + Set-StrictMode -Version 3 + + $action = '{{action}}' + $validActions = @( + 'resolve', + 'for-environment', + 'install', + 'validate-workspace-msrv', + 'msrv', + 'install-msrv' + ) + if ($action -notin $validActions) { + Write-Error "_anvil-resolve-stable: unknown action '$action'" + exit 2 + } + + $repoRoot = '{{justfile_directory()}}' + + 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) + ) + } + + 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' + $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" + } + $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" + } + + $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 + -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' + } + } + + 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' + Mapped = $true + } + } + return [pscustomobject]@{ Value = $msrv; Source = 'msrv'; Mapped = $false } + } + + function Install-Toolchain([object] $toolchain, [string] $label) { + $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: failed to install $label toolchain '$($toolchain.Value)'" + } + } + } + + if ($action -eq 'validate-workspace-msrv') { + Assert-CompleteInternalToolchainConfiguration + $skipWorkspaceMsrvValidation = -not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN) + if (-not $skipWorkspaceMsrvValidation -and -not (Test-RootToolchainFile)) { + 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" + } + } else { + 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' } + } elseif (Test-RootToolchainFile) { + [pscustomobject]@{ Value = $null; Source = 'file' } + } else { + [pscustomobject]@{ Value = Get-RootMsrv; Source = 'msrv' } + } + + if ($action -eq 'install') { + 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 + } + + if ($action -eq 'for-environment' -and $selection.Source -eq 'file') { + '' + } else { + $selection.Value + } + +# Provision the selected stable toolchain. Repository toolchain files are +# processed natively by rustup. This is the shared prerequisite for setup paths +# that execute stable Cargo/Rust or install Cargo tools; Just deduplicates it +# 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. @@ -6189,7 +6590,7 @@ anvil-tool-pwsh-validate-prereqs: # `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}}' @@ -6209,7 +6610,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 (& cargo {{_anvil_stable_toolchain_args}} install --list 2>$null)) { if ($line -match $pattern) { $installed = $Matches[1]; break } } if ($installed) { @@ -6227,7 +6628,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 + & cargo {{_anvil_stable_toolchain_args}} install --locked cargo-binstall if ($LASTEXITCODE -ne 0) { Write-Error 'cargo-binstall bootstrap failed' exit $LASTEXITCODE @@ -6241,7 +6642,7 @@ _install-tool name version installer source_prereq="": $binstallArgs += @('--disable-strategies', 'compile') } $binstallArgs += @($name, '--version', "=$version") - 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 } @@ -6249,12 +6650,15 @@ _install-tool name version installer source_prereq="": & "{{just_executable()}}" $sourcePrereq if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } } - 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 } +# 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")] @@ -6265,7 +6669,7 @@ _check-tool name version: $installed = $null $pattern = '^' + [regex]::Escape($name) + ' v(\S+):' - foreach ($line in (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) { @@ -6316,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 @@ -6334,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` @@ -6346,14 +6758,22 @@ _install-component toolchain component: } if ($null -ne $subcommand) { if ($toolchain -eq 'default') { - & cargo $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 --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 } @@ -6377,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 @@ -6406,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' } @@ -6414,14 +6842,22 @@ _check-component toolchain component: } if ($null -ne $subcommand) { if ($toolchain -eq 'default') { - & cargo $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 --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 } @@ -6443,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 } @@ -6472,14 +6914,13 @@ 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")] -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")] @@ -6487,7 +6928,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")] @@ -6574,12 +7015,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. @@ -6691,12 +7132,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. @@ -6758,6 +7199,7 @@ 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. + # # Everything anvil installs (cargo subcommands + rustup toolchains) # is pinned here. The pinning policy: @@ -6783,6 +7225,78 @@ 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. 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("@(& {\n$RepoRoot = '" + replace(justfile_directory(), "'", "''") + "'\n\n" + ''' +$ErrorActionPreference = 'Stop' +Set-StrictMode -Version 3 + +if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { + Write-Output ('+' + $env:RUSTUP_TOOLCHAIN) + return +} + +if ((Test-Path -LiteralPath (Join-Path $RepoRoot 'rust-toolchain') -PathType Leaf) -or + (Test-Path -LiteralPath (Join-Path $RepoRoot 'rust-toolchain.toml') -PathType Leaf)) { + Push-Location $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 + } + $resolved = [string] $active + $sourceMarker = $resolved.LastIndexOf(' (') + if ($sourceMarker -ge 0) { + $resolved = $resolved.Substring(0, $sourceMarker) + } + Write-Output ('+' + $resolved) + return +} + +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-Output ('+' + $msrv) +}) +''') + # 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 f20c14d0..714f7b6d 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap @@ -54,10 +54,6 @@ 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 \ @@ -92,6 +88,8 @@ 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 @@ -372,12 +370,11 @@ if ($LASTEXITCODE -ne 0 -or -not $repoRoot) { throw 'anvil-container must run from a Git repository.' } -$inputs = @( - 'rust-toolchain.toml' -) -$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.' +$inputs = @('Cargo.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 +441,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 +453,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) +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 @@ -1606,9 +1602,29 @@ runs: } "Free after cleanup (GiB): $(Get-FreeDiskGiB)" - - id: rustc-version + # Stable provisioning is part of the generated Just setup graph, so + # bootstrap Just before asking it for the selected compiler version. + # cargo-binstall keeps this cold path fast. + - name: Install cargo-binstall + uses: cargo-bins/cargo-binstall@v1.21.0 # immutable release, the tag cannot be moved + + - name: Install just shell: bash - run: echo "version=$(rustc --version | awk '{print $2}')" >> "$GITHUB_OUTPUT" + run: | + if ! command -v just >/dev/null 2>&1 ; then + cargo binstall --no-confirm --locked just || cargo install --locked just + fi + + - name: Provision stable toolchain and capture version + id: rustc-version + shell: pwsh -NoProfile -Command ". '{0}'" + run: | + $version = (& just _anvil-stable-rustc-version | Select-Object -Last 1) + if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($version)) { + throw 'anvil: failed to provision the selected stable toolchain' + } + $version = $version.Trim() + "version=$version" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append - name: Restore cargo cache id: cargo-cache uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 @@ -1629,9 +1645,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 @@ -1655,41 +1671,11 @@ runs: ~/.cargo/.crates.toml ~/.cargo/.crates2.json - # 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 - # 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 @@ -1993,6 +1979,33 @@ jobs: token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: false + pr-msrv: + name: "Check Group: MSRV Tests (${{ matrix.os }})" + needs: [impact-linux, impact-windows] + 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: + lfs: true + - name: Download impact artifact + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + name: anvil-impact-${{ startsWith(matrix.os, 'linux') && 'Linux' || 'Windows' }} + path: target/anvil/impact + - uses: ./.github/actions/anvil-run-group + with: + group: pr-msrv + publish_commit_statuses: ${{ inputs.publish_commit_statuses }} + free-disk-space: true + impact_mode: consume + pr-runtime-analysis: name: "Check Group: Runtime Analysis (${{ matrix.os }})" # Stricter-runtime correctness: miri + careful. @@ -2592,7 +2605,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 + & 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. @@ -2618,7 +2631,7 @@ anvil-aprz-validate-prereqs: anvil-tool-cargo-aprz-validate-prereqs [script("pwsh", "-NoProfile")] anvil-audit: anvil-audit-validate-prereqs $ErrorActionPreference = 'Stop' - & cargo audit + & cargo {{_anvil_stable_toolchain_args}} audit if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-audit` recipe. @@ -2646,7 +2659,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 + & 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 @@ -2654,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")] @@ -2717,7 +2730,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 = & cargo {{_anvil_stable_toolchain_args}} metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-bolero: cargo metadata failed' exit $LASTEXITCODE @@ -2844,7 +2857,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 @@ -2881,7 +2894,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 + & 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. @@ -2920,7 +2933,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 + & 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. @@ -2953,7 +2966,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' + & 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. @@ -2981,7 +2994,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 + & cargo {{_anvil_stable_toolchain_args}} deny check if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-deny` recipe. @@ -3012,7 +3025,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 + & 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 @@ -3021,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")] @@ -3050,14 +3063,14 @@ 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 + & cargo {{_anvil_stable_toolchain_args}} test --doc @pkg --all-features --locked if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - & 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. [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")] @@ -3080,7 +3093,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 + & 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. @@ -3108,7 +3121,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 + & 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. @@ -3136,12 +3149,12 @@ 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 + & 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. [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")] @@ -3181,7 +3194,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 = & 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 @@ -3309,7 +3322,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 + & cargo {{_anvil_stable_toolchain_args}} heather if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-license-headers` recipe. @@ -3360,7 +3373,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 = & 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 @@ -3491,7 +3504,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 + & 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) --- @@ -3562,7 +3575,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 = & 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 @@ -3615,7 +3628,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 + & 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 } } @@ -3625,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")] @@ -3807,6 +3820,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. +[script("pwsh", "-NoProfile")] +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 = (& "{{ 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 + } + $pkg = @(if ($include) { -split $include } 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 declared MSRV toolchain only when this check is required. +[group("anvil-setup")] +[script("pwsh", "-NoProfile")] +anvil-msrv-test-setup installer="install": + & "{{ 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 = (& "{{ just_executable() }}" _anvil-resolve-stable msrv | 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 toolchain '$toolchain' is unavailable; $hint" + exit $LASTEXITCODE + } + === justfiles/anvil/checks/mutants-diff.just === # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. @@ -3853,7 +3916,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 + & cargo {{_anvil_stable_toolchain_args}} mutants --in-diff $diff_path --no-shuffle --jobs 0 if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # --- pr-mutants members --- @@ -3886,7 +3949,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 + & 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; @@ -4034,7 +4097,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 = & 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 @@ -4076,7 +4139,7 @@ anvil-readme-check: anvil-readme-check-validate-prereqs } $args = @('doc2readme', '--check') if ($relTemplate) { $args += @('--template', $relTemplate) } - & cargo @args + & cargo {{_anvil_stable_toolchain_args}} @args if ($LASTEXITCODE -ne 0) { $hadFailure = $true } } finally { Pop-Location @@ -4160,7 +4223,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 = & 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 @@ -4237,7 +4300,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 = @(& cargo {{_anvil_stable_toolchain_args}} semver-checks --package $p --baseline-rev $base 2>&1) $semverExit = $LASTEXITCODE $output = ($outputLines | Out-String) if ($semverExit -eq 100) { @@ -4376,9 +4439,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 + & cargo {{_anvil_stable_toolchain_args}} spellcheck --cfg spellcheck.toml check --code 1 } else { - cargo spellcheck check --code 1 + & cargo {{_anvil_stable_toolchain_args}} spellcheck check --code 1 } if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } @@ -4545,6 +4608,31 @@ 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-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-tool-cargo-delta-validate-prereqs \ + anvil-msrv-test-validate-prereqs + === justfiles/anvil/groups/pr-mutants.just === # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. @@ -4614,32 +4702,33 @@ 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 three 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 # 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) @@ -4647,6 +4736,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 @@ -5212,19 +5302,29 @@ _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. + $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 + } + $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 @@ -5257,14 +5357,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 { @@ -5291,7 +5389,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 = & 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 @@ -5409,7 +5507,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 = (& 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 @@ -5486,7 +5584,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 = & cargo {{_anvil_stable_toolchain_args}} metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-impact: cargo metadata failed' exit $LASTEXITCODE @@ -5665,6 +5763,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' @@ -5674,6 +5773,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' @@ -6017,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")] @@ -6033,6 +6132,10 @@ anvil-tool-rustc-validate-prereqs: Write-Error 'anvil: rustc not found. Install via rustup: https://rustup.rs' exit 1 } + & "{{ just_executable() }}" _anvil-resolve-stable validate-workspace-msrv + if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE + } # Validate that PowerShell Core is available. [group("anvil-setup")] @@ -6058,7 +6161,227 @@ anvil-tool-pwsh-validate-prereqs: # Private helpers (install/check primitives) # ============================================================================ -# _install-tool: install a cargo subcommand at the catalog version, +# 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' + Set-StrictMode -Version 3 + + $action = '{{action}}' + $validActions = @( + 'resolve', + 'for-environment', + 'install', + 'validate-workspace-msrv', + 'msrv', + 'install-msrv' + ) + if ($action -notin $validActions) { + Write-Error "_anvil-resolve-stable: unknown action '$action'" + exit 2 + } + + $repoRoot = '{{justfile_directory()}}' + + 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) + ) + } + + 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' + $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" + } + $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" + } + + $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 + -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' + } + } + + 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' + Mapped = $true + } + } + return [pscustomobject]@{ Value = $msrv; Source = 'msrv'; Mapped = $false } + } + + function Install-Toolchain([object] $toolchain, [string] $label) { + $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: failed to install $label toolchain '$($toolchain.Value)'" + } + } + } + + if ($action -eq 'validate-workspace-msrv') { + Assert-CompleteInternalToolchainConfiguration + $skipWorkspaceMsrvValidation = -not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN) + if (-not $skipWorkspaceMsrvValidation -and -not (Test-RootToolchainFile)) { + 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" + } + } else { + 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' } + } elseif (Test-RootToolchainFile) { + [pscustomobject]@{ Value = $null; Source = 'file' } + } else { + [pscustomobject]@{ Value = Get-RootMsrv; Source = 'msrv' } + } + + if ($action -eq 'install') { + 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 + } + + if ($action -eq 'for-environment' -and $selection.Source -eq 'file') { + '' + } else { + $selection.Value + } + +# Provision the selected stable toolchain. Repository toolchain files are +# processed natively by rustup. This is the shared prerequisite for setup paths +# that execute stable Cargo/Rust or install Cargo tools; Just deduplicates it +# 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. @@ -6068,7 +6391,7 @@ anvil-tool-pwsh-validate-prereqs: # `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}}' @@ -6088,7 +6411,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 (& cargo {{_anvil_stable_toolchain_args}} install --list 2>$null)) { if ($line -match $pattern) { $installed = $Matches[1]; break } } if ($installed) { @@ -6106,7 +6429,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 + & cargo {{_anvil_stable_toolchain_args}} install --locked cargo-binstall if ($LASTEXITCODE -ne 0) { Write-Error 'cargo-binstall bootstrap failed' exit $LASTEXITCODE @@ -6120,7 +6443,7 @@ _install-tool name version installer source_prereq="": $binstallArgs += @('--disable-strategies', 'compile') } $binstallArgs += @($name, '--version', "=$version") - 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 } @@ -6128,12 +6451,15 @@ _install-tool name version installer source_prereq="": & "{{just_executable()}}" $sourcePrereq if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } } - 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 } +# 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")] @@ -6144,7 +6470,7 @@ _check-tool name version: $installed = $null $pattern = '^' + [regex]::Escape($name) + ' v(\S+):' - foreach ($line in (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) { @@ -6195,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 @@ -6213,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` @@ -6225,14 +6559,22 @@ _install-component toolchain component: } if ($null -ne $subcommand) { if ($toolchain -eq 'default') { - & cargo $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 --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 } @@ -6256,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 @@ -6285,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' } @@ -6293,14 +6643,22 @@ _check-component toolchain component: } if ($null -ne $subcommand) { if ($toolchain -eq 'default') { - & cargo $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 --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 } @@ -6322,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 } @@ -6351,14 +6715,13 @@ 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")] -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")] @@ -6366,7 +6729,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")] @@ -6453,12 +6816,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. @@ -6570,12 +6933,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. @@ -6637,6 +7000,7 @@ 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. + # # Everything anvil installs (cargo subcommands + rustup toolchains) # is pinned here. The pinning policy: @@ -6662,6 +7026,78 @@ 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. 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("@(& {\n$RepoRoot = '" + replace(justfile_directory(), "'", "''") + "'\n\n" + ''' +$ErrorActionPreference = 'Stop' +Set-StrictMode -Version 3 + +if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { + Write-Output ('+' + $env:RUSTUP_TOOLCHAIN) + return +} + +if ((Test-Path -LiteralPath (Join-Path $RepoRoot 'rust-toolchain') -PathType Leaf) -or + (Test-Path -LiteralPath (Join-Path $RepoRoot 'rust-toolchain.toml') -PathType Leaf)) { + Push-Location $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 + } + $resolved = [string] $active + $sourceMarker = $resolved.LastIndexOf(' (') + if ($sourceMarker -ge 0) { + $resolved = $resolved.Substring(0, $sourceMarker) + } + Write-Output ('+' + $resolved) + return +} + +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-Output ('+' + $msrv) +}) +''') + # 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 20556135..2bee1d42 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap @@ -54,10 +54,6 @@ 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 \ @@ -92,6 +88,8 @@ 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 @@ -372,12 +370,11 @@ if ($LASTEXITCODE -ne 0 -or -not $repoRoot) { throw 'anvil-container must run from a Git repository.' } -$inputs = @( - 'rust-toolchain.toml' -) -$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.' +$inputs = @('Cargo.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 +441,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 +453,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) +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 @@ -1465,7 +1461,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 + & 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. @@ -1491,7 +1487,7 @@ anvil-aprz-validate-prereqs: anvil-tool-cargo-aprz-validate-prereqs [script("pwsh", "-NoProfile")] anvil-audit: anvil-audit-validate-prereqs $ErrorActionPreference = 'Stop' - & cargo audit + & cargo {{_anvil_stable_toolchain_args}} audit if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-audit` recipe. @@ -1519,7 +1515,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 + & 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 @@ -1527,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")] @@ -1590,7 +1586,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 = & cargo {{_anvil_stable_toolchain_args}} metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-bolero: cargo metadata failed' exit $LASTEXITCODE @@ -1717,7 +1713,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 @@ -1754,7 +1750,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 + & 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. @@ -1793,7 +1789,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 + & 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. @@ -1826,7 +1822,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' + & 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. @@ -1854,7 +1850,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 + & cargo {{_anvil_stable_toolchain_args}} deny check if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-deny` recipe. @@ -1885,7 +1881,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 + & 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 @@ -1894,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")] @@ -1923,14 +1919,14 @@ 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 + & cargo {{_anvil_stable_toolchain_args}} test --doc @pkg --all-features --locked if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - & 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. [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")] @@ -1953,7 +1949,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 + & 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. @@ -1981,7 +1977,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 + & 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. @@ -2009,12 +2005,12 @@ 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 + & 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. [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")] @@ -2054,7 +2050,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 = & 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 @@ -2182,7 +2178,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 + & cargo {{_anvil_stable_toolchain_args}} heather if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-license-headers` recipe. @@ -2233,7 +2229,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 = & 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 @@ -2364,7 +2360,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 + & 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) --- @@ -2435,7 +2431,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 = & 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 @@ -2488,7 +2484,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 + & 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 } } @@ -2498,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")] @@ -2680,6 +2676,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. +[script("pwsh", "-NoProfile")] +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 = (& "{{ 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 + } + $pkg = @(if ($include) { -split $include } 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 declared MSRV toolchain only when this check is required. +[group("anvil-setup")] +[script("pwsh", "-NoProfile")] +anvil-msrv-test-setup installer="install": + & "{{ 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 = (& "{{ just_executable() }}" _anvil-resolve-stable msrv | 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 toolchain '$toolchain' is unavailable; $hint" + exit $LASTEXITCODE + } + === justfiles/anvil/checks/mutants-diff.just === # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. @@ -2726,7 +2772,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 + & cargo {{_anvil_stable_toolchain_args}} mutants --in-diff $diff_path --no-shuffle --jobs 0 if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # --- pr-mutants members --- @@ -2759,7 +2805,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 + & 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; @@ -2907,7 +2953,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 = & 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 @@ -2949,7 +2995,7 @@ anvil-readme-check: anvil-readme-check-validate-prereqs } $args = @('doc2readme', '--check') if ($relTemplate) { $args += @('--template', $relTemplate) } - & cargo @args + & cargo {{_anvil_stable_toolchain_args}} @args if ($LASTEXITCODE -ne 0) { $hadFailure = $true } } finally { Pop-Location @@ -3033,7 +3079,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 = & 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 @@ -3110,7 +3156,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 = @(& cargo {{_anvil_stable_toolchain_args}} semver-checks --package $p --baseline-rev $base 2>&1) $semverExit = $LASTEXITCODE $output = ($outputLines | Out-String) if ($semverExit -eq 100) { @@ -3249,9 +3295,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 + & cargo {{_anvil_stable_toolchain_args}} spellcheck --cfg spellcheck.toml check --code 1 } else { - cargo spellcheck check --code 1 + & cargo {{_anvil_stable_toolchain_args}} spellcheck check --code 1 } if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } @@ -3418,6 +3464,31 @@ 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-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-tool-cargo-delta-validate-prereqs \ + anvil-msrv-test-validate-prereqs + === justfiles/anvil/groups/pr-mutants.just === # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. @@ -3487,32 +3558,33 @@ 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 three 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 # 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) @@ -3520,6 +3592,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 @@ -4085,19 +4158,29 @@ _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. + $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 + } + $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 @@ -4130,14 +4213,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 { @@ -4164,7 +4245,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 = & 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 @@ -4282,7 +4363,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 = (& 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 @@ -4359,7 +4440,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 = & cargo {{_anvil_stable_toolchain_args}} metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-impact: cargo metadata failed' exit $LASTEXITCODE @@ -4538,6 +4619,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' @@ -4547,6 +4629,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' @@ -4890,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")] @@ -4906,6 +4988,10 @@ anvil-tool-rustc-validate-prereqs: Write-Error 'anvil: rustc not found. Install via rustup: https://rustup.rs' exit 1 } + & "{{ just_executable() }}" _anvil-resolve-stable validate-workspace-msrv + if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE + } # Validate that PowerShell Core is available. [group("anvil-setup")] @@ -4931,7 +5017,227 @@ anvil-tool-pwsh-validate-prereqs: # Private helpers (install/check primitives) # ============================================================================ -# _install-tool: install a cargo subcommand at the catalog version, +# 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' + Set-StrictMode -Version 3 + + $action = '{{action}}' + $validActions = @( + 'resolve', + 'for-environment', + 'install', + 'validate-workspace-msrv', + 'msrv', + 'install-msrv' + ) + if ($action -notin $validActions) { + Write-Error "_anvil-resolve-stable: unknown action '$action'" + exit 2 + } + + $repoRoot = '{{justfile_directory()}}' + + 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) + ) + } + + 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' + $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" + } + $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" + } + + $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 + -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' + } + } + + 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' + Mapped = $true + } + } + return [pscustomobject]@{ Value = $msrv; Source = 'msrv'; Mapped = $false } + } + + function Install-Toolchain([object] $toolchain, [string] $label) { + $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: failed to install $label toolchain '$($toolchain.Value)'" + } + } + } + + if ($action -eq 'validate-workspace-msrv') { + Assert-CompleteInternalToolchainConfiguration + $skipWorkspaceMsrvValidation = -not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN) + if (-not $skipWorkspaceMsrvValidation -and -not (Test-RootToolchainFile)) { + 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" + } + } else { + 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' } + } elseif (Test-RootToolchainFile) { + [pscustomobject]@{ Value = $null; Source = 'file' } + } else { + [pscustomobject]@{ Value = Get-RootMsrv; Source = 'msrv' } + } + + if ($action -eq 'install') { + 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 + } + + if ($action -eq 'for-environment' -and $selection.Source -eq 'file') { + '' + } else { + $selection.Value + } + +# Provision the selected stable toolchain. Repository toolchain files are +# processed natively by rustup. This is the shared prerequisite for setup paths +# that execute stable Cargo/Rust or install Cargo tools; Just deduplicates it +# 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. @@ -4941,7 +5247,7 @@ anvil-tool-pwsh-validate-prereqs: # `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}}' @@ -4961,7 +5267,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 (& cargo {{_anvil_stable_toolchain_args}} install --list 2>$null)) { if ($line -match $pattern) { $installed = $Matches[1]; break } } if ($installed) { @@ -4979,7 +5285,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 + & cargo {{_anvil_stable_toolchain_args}} install --locked cargo-binstall if ($LASTEXITCODE -ne 0) { Write-Error 'cargo-binstall bootstrap failed' exit $LASTEXITCODE @@ -4993,7 +5299,7 @@ _install-tool name version installer source_prereq="": $binstallArgs += @('--disable-strategies', 'compile') } $binstallArgs += @($name, '--version', "=$version") - 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 } @@ -5001,12 +5307,15 @@ _install-tool name version installer source_prereq="": & "{{just_executable()}}" $sourcePrereq if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } } - 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 } +# 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")] @@ -5017,7 +5326,7 @@ _check-tool name version: $installed = $null $pattern = '^' + [regex]::Escape($name) + ' v(\S+):' - foreach ($line in (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) { @@ -5068,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 @@ -5086,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` @@ -5098,14 +5415,22 @@ _install-component toolchain component: } if ($null -ne $subcommand) { if ($toolchain -eq 'default') { - & cargo $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 --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 } @@ -5129,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 @@ -5158,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' } @@ -5166,14 +5499,22 @@ _check-component toolchain component: } if ($null -ne $subcommand) { if ($toolchain -eq 'default') { - & cargo $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 --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 } @@ -5195,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 } @@ -5224,14 +5571,13 @@ 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")] -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")] @@ -5239,7 +5585,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")] @@ -5326,12 +5672,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. @@ -5443,12 +5789,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. @@ -5510,6 +5856,7 @@ 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. + # # Everything anvil installs (cargo subcommands + rustup toolchains) # is pinned here. The pinning policy: @@ -5535,6 +5882,78 @@ 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. 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("@(& {\n$RepoRoot = '" + replace(justfile_directory(), "'", "''") + "'\n\n" + ''' +$ErrorActionPreference = 'Stop' +Set-StrictMode -Version 3 + +if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { + Write-Output ('+' + $env:RUSTUP_TOOLCHAIN) + return +} + +if ((Test-Path -LiteralPath (Join-Path $RepoRoot 'rust-toolchain') -PathType Leaf) -or + (Test-Path -LiteralPath (Join-Path $RepoRoot 'rust-toolchain.toml') -PathType Leaf)) { + Push-Location $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 + } + $resolved = [string] $active + $sourceMarker = $resolved.LastIndexOf(' (') + if ($sourceMarker -ge 0) { + $resolved = $resolved.Substring(0, $sourceMarker) + } + Write-Output ('+' + $resolved) + return +} + +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-Output ('+' + $msrv) +}) +''') + # 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/checks/aprz.just b/justfiles/anvil/checks/aprz.just index 1627c3a5..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.' } } - 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 20de2046..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' - & 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 2f4d42f7..b26880a8 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 + & 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 @@ -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/bolero.just b/justfiles/anvil/checks/bolero.just index 37f7d21b..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 = & 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/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..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 } - & 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 a0af1ccb..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 } - 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 c9167c91..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 } - & 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 c52b613a..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' - & 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 9250d41a..a8edeef7 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 + & 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 @@ -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 1954ae4a..ca6ade8c 100644 --- a/justfiles/anvil/checks/doc-test.just +++ b/justfiles/anvil/checks/doc-test.just @@ -20,14 +20,14 @@ 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 + & cargo {{_anvil_stable_toolchain_args}} test --doc @pkg --all-features --locked if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - & 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. [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/ensure-no-cyclic-deps.just b/justfiles/anvil/checks/ensure-no-cyclic-deps.just index 48ef3e7c..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 } - 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 c5668204..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 } - 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 698b1115..fe447b7f 100644 --- a/justfiles/anvil/checks/examples.just +++ b/justfiles/anvil/checks/examples.just @@ -14,12 +14,12 @@ 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 + & 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. [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/external-types.just b/justfiles/anvil/checks/external-types.just index 33c0593d..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 = & 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 989f57b4..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 } - 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 ab39d671..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 = & 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]) } - 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 0f88aba6..845381bc 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 = & 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)" - & 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 } } @@ -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/checks/msrv-test.just b/justfiles/anvil/checks/msrv-test.just new file mode 100644 index 00000000..362e470c --- /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. +[script("pwsh", "-NoProfile")] +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 = (& "{{ 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 + } + $pkg = @(if ($include) { -split $include } 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 declared MSRV toolchain only when this check is required. +[group("anvil-setup")] +[script("pwsh", "-NoProfile")] +anvil-msrv-test-setup installer="install": + & "{{ 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 = (& "{{ just_executable() }}" _anvil-resolve-stable msrv | 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 toolchain '$toolchain' is unavailable; $hint" + exit $LASTEXITCODE + } diff --git a/justfiles/anvil/checks/mutants-diff.just b/justfiles/anvil/checks/mutants-diff.just index 25c98ba9..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 } - 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 a914f1eb..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 } - & 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 1d258e25..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 = & 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) } - & 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 4f7112b6..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 = & 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 = @(& 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 15e734b4..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') { - cargo spellcheck --cfg spellcheck.toml check --code 1 + & cargo {{_anvil_stable_toolchain_args}} spellcheck --cfg spellcheck.toml check --code 1 } else { - 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/groups/pr-msrv.just b/justfiles/anvil/groups/pr-msrv.just new file mode 100644 index 00000000..d35d01ac --- /dev/null +++ b/justfiles/anvil/groups/pr-msrv.just @@ -0,0 +1,23 @@ +# 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-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-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 75ad63c1..3ac2c01f 100644 --- a/justfiles/anvil/groups/pr-slow.just +++ b/justfiles/anvil/groups/pr-slow.just @@ -6,32 +6,33 @@ # 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 three 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 # 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/impact.just b/justfiles/anvil/impact.just index 42d74502..4f05394c 100644 --- a/justfiles/anvil/impact.just +++ b/justfiles/anvil/impact.just @@ -235,19 +235,29 @@ _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. + $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 + } + $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 +290,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 +322,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 = & 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 @@ -432,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 = (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 @@ -509,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 = & 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/mod.just b/justfiles/anvil/mod.just index ba229814..8a4c637e 100644 --- a/justfiles/anvil/mod.just +++ b/justfiles/anvil/mod.just @@ -60,6 +60,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' @@ -69,6 +70,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/tools.just b/justfiles/anvil/tools.just index cb6aab14..0d6e45f9 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")] @@ -144,6 +143,10 @@ anvil-tool-rustc-validate-prereqs: Write-Error 'anvil: rustc not found. Install via rustup: https://rustup.rs' exit 1 } + & "{{ just_executable() }}" _anvil-resolve-stable validate-workspace-msrv + if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE + } # Validate that PowerShell Core is available. [group("anvil-setup")] @@ -169,7 +172,227 @@ anvil-tool-pwsh-validate-prereqs: # Private helpers (install/check primitives) # ============================================================================ -# _install-tool: install a cargo subcommand at the catalog version, +# 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' + Set-StrictMode -Version 3 + + $action = '{{action}}' + $validActions = @( + 'resolve', + 'for-environment', + 'install', + 'validate-workspace-msrv', + 'msrv', + 'install-msrv' + ) + if ($action -notin $validActions) { + Write-Error "_anvil-resolve-stable: unknown action '$action'" + exit 2 + } + + $repoRoot = '{{justfile_directory()}}' + + 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) + ) + } + + 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' + $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" + } + $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" + } + + $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 + -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' + } + } + + 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' + Mapped = $true + } + } + return [pscustomobject]@{ Value = $msrv; Source = 'msrv'; Mapped = $false } + } + + function Install-Toolchain([object] $toolchain, [string] $label) { + $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: failed to install $label toolchain '$($toolchain.Value)'" + } + } + } + + if ($action -eq 'validate-workspace-msrv') { + Assert-CompleteInternalToolchainConfiguration + $skipWorkspaceMsrvValidation = -not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN) + if (-not $skipWorkspaceMsrvValidation -and -not (Test-RootToolchainFile)) { + 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" + } + } else { + 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' } + } elseif (Test-RootToolchainFile) { + [pscustomobject]@{ Value = $null; Source = 'file' } + } else { + [pscustomobject]@{ Value = Get-RootMsrv; Source = 'msrv' } + } + + if ($action -eq 'install') { + 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 + } + + if ($action -eq 'for-environment' -and $selection.Source -eq 'file') { + '' + } else { + $selection.Value + } + +# Provision the selected stable toolchain. Repository toolchain files are +# processed natively by rustup. This is the shared prerequisite for setup paths +# that execute stable Cargo/Rust or install Cargo tools; Just deduplicates it +# 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. @@ -179,7 +402,7 @@ anvil-tool-pwsh-validate-prereqs: # `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}}' @@ -199,7 +422,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 (& cargo {{_anvil_stable_toolchain_args}} install --list 2>$null)) { if ($line -match $pattern) { $installed = $Matches[1]; break } } if ($installed) { @@ -217,7 +440,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 + & cargo {{_anvil_stable_toolchain_args}} install --locked cargo-binstall if ($LASTEXITCODE -ne 0) { Write-Error 'cargo-binstall bootstrap failed' exit $LASTEXITCODE @@ -231,7 +454,7 @@ _install-tool name version installer source_prereq="": $binstallArgs += @('--disable-strategies', 'compile') } $binstallArgs += @($name, '--version', "=$version") - 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 } @@ -239,12 +462,15 @@ _install-tool name version installer source_prereq="": & "{{just_executable()}}" $sourcePrereq if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } } - 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 } +# 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")] @@ -255,7 +481,7 @@ _check-tool name version: $installed = $null $pattern = '^' + [regex]::Escape($name) + ' v(\S+):' - foreach ($line in (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) { @@ -306,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 @@ -324,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` @@ -336,14 +570,22 @@ _install-component toolchain component: } if ($null -ne $subcommand) { if ($toolchain -eq 'default') { - & cargo $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 --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 } @@ -367,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 @@ -396,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' } @@ -404,14 +654,22 @@ _check-component toolchain component: } if ($null -ne $subcommand) { if ($toolchain -eq 'default') { - & cargo $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 --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 } @@ -433,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 } @@ -462,14 +726,13 @@ 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")] -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")] @@ -477,7 +740,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")] @@ -564,12 +827,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. @@ -681,12 +944,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 564a3b90..1641ee8a 100644 --- a/justfiles/anvil/versions.just +++ b/justfiles/anvil/versions.just @@ -5,6 +5,7 @@ # 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. + # # Everything anvil installs (cargo subcommands + rustup toolchains) # is pinned here. The pinning policy: @@ -30,6 +31,78 @@ # Rustup toolchains # ============================================================================ +# 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("@(& {\n$RepoRoot = '" + replace(justfile_directory(), "'", "''") + "'\n\n" + ''' +$ErrorActionPreference = 'Stop' +Set-StrictMode -Version 3 + +if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { + Write-Output ('+' + $env:RUSTUP_TOOLCHAIN) + return +} + +if ((Test-Path -LiteralPath (Join-Path $RepoRoot 'rust-toolchain') -PathType Leaf) -or + (Test-Path -LiteralPath (Join-Path $RepoRoot 'rust-toolchain.toml') -PathType Leaf)) { + Push-Location $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 + } + $resolved = [string] $active + $sourceMarker = $resolved.LastIndexOf(' (') + if ($sourceMarker -ge 0) { + $resolved = $resolved.Substring(0, $sourceMarker) + } + Write-Output ('+' + $resolved) + return +} + +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-Output ('+' + $msrv) +}) +''') + # 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 /
JobSub-groupCheckNotes
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-testdual feature-config tests under the declared MSRV
pr-runtime-analysismirilibtest, not nextest
carefulself-cleans on a toolchain bump
loomopt-in targets only