diff --git a/.anvil.lock b/.anvil.lock index 8e99d392b..a127283c7 100644 --- a/.anvil.lock +++ b/.anvil.lock @@ -1,7 +1,7 @@ version = 1 tool = "anvil" tool_version = "0.6.0" -catalog_checksum = "sha256:7e4e7f7fa0e7f9491d3f5123f80ef209261e71b9cd9039e2f388e4287dc2f6bc" +catalog_checksum = "sha256:411e8b3d247721c856c6a8a69f7b388711159325472ab687eff9c40bdd9727b0" [[file]] path = ".anvil/container/Dockerfile.dockerignore" @@ -21,7 +21,7 @@ checksum = "sha256:ff8def6c0786b6e146c4b633dfe38cb9b8ede398345516cca32bbcd558608 [[file]] path = ".github/actions/anvil-setup/action.yml" -checksum = "sha256:3df22c126c79170d39c515a06b597a0c20a05a5803eacd38326f0fa63bea846b" +checksum = "sha256:e1029f7ff95a5966184b4c04ccbed04ee8ba93bfc93bc9ac307d3dcef31374fb" [[file]] path = ".github/actions/anvil-setup/just-problem-matcher.json" @@ -29,7 +29,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" @@ -45,79 +45,79 @@ checksum = "sha256:d4d3bd645a5586e9a1cc3a5fc27e38c93e59b1e29f83ede0ccd610273eec0 [[file]] path = "justfiles/anvil/checks/aprz.just" -checksum = "sha256:0f9f3dd3c8a2f034ebf4f2ac0344cba3db79612ff2fc2ea037bfabbaacb1be12" +checksum = "sha256:e10edbbe60358ec930241cd5d4e681a84f30f9d5278e652696e2f149e112d066" [[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:754827bb664723169b8d48a6f1e69f9122b5440ba0b94a91622f4d23313e2503" +checksum = "sha256:5a1767c4ff911cec3b0870d675cbefb563193146cedd3a1cbfe1667215bdb264" [[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:98c9f90e1b6b3beba5d818bf9a4023068eb61e9f071ffb6f9cd009a7374e9a30" +checksum = "sha256:ab342c014199ffb58a9346b7b9fcea88201b5b60a07b06cbc9c4e5ef03bd18f5" [[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" -checksum = "sha256:618907282b9b7d1e5c094d0b29c1c7231465078b617fa233219fe8c4870437cb" +checksum = "sha256:0434e1a8720453a4bc635ea56af1d19875ac79e6fdd76349052cd57e70392eb8" [[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:a919429e9693c230ba8c9a194c41127708acf2cb1bfd196049a1386e7bb7b4e4" +checksum = "sha256:f4822a8f9cb282bc426790355ba83b63e778db9327e986ba3c86ae6205cfdabb" [[file]] path = "justfiles/anvil/checks/miri-race-coverage.just" @@ -135,13 +135,17 @@ checksum = "sha256:c82f586402f2cb05397a40eec6fe95c7590df2cc52f0dd9da6879de085175 path = "justfiles/anvil/checks/miri.just" checksum = "sha256:9e9d0cbfef1e1e1586c2af4e9c387719c2f017ea1203d326baacc3ae25df80e2" +[[file]] +path = "justfiles/anvil/checks/msrv-test.just" +checksum = "sha256:9c25b140dcaa4b38db7701fd627dd9a2d5ad5f9bb83d4490c146058bf4328cc8" + [[file]] path = "justfiles/anvil/checks/mutants-diff.just" -checksum = "sha256:d13b1c467b8899d54e3567cb8d1a2157c4a54ac7ba9dabc00a09f3105fddb9b4" +checksum = "sha256:e8d23688dcb86f82bf537e82c35ab8af9296d5530898645ddfe95132f9192bd0" [[file]] path = "justfiles/anvil/checks/mutants-full.just" -checksum = "sha256:9792344e6696f5c63f30fdccb5ef90a3598c56adb7561cec33f074c84ce6f89b" +checksum = "sha256:604dfb6aa2742695ff638a7d2db6811a1bcbf27da73660027baacc86b2fe1509" [[file]] path = "justfiles/anvil/checks/pr-title.just" @@ -149,15 +153,15 @@ checksum = "sha256:28734aa77526ca5c53d89a32c3e20ae6b42d2032c73ab6a1dbc9831f25cb0 [[file]] path = "justfiles/anvil/checks/readme-check.just" -checksum = "sha256:6d4b4c3e4a59e825a3f613e1011272c711d8d6cbdef315e5f6aadf653c6ae206" +checksum = "sha256:4993e662875561ddf976ec94c69ec0d647a5c957bfb8f5174f79162a640ed892" [[file]] path = "justfiles/anvil/checks/semver-check.just" -checksum = "sha256:b214a8abbf2895df838ac1ff06c11bec4e8b8b3105c2cc4c0982a8a13414abd1" +checksum = "sha256:be0430a53007301db00602d4bc9296f51ad9b2f011583084d069c51b30939b74" [[file]] path = "justfiles/anvil/checks/spellcheck.just" -checksum = "sha256:8f47518b756607402a7b3652762d5d37cff5d91eecd1052053176eedd65a9958" +checksum = "sha256:2b2063f7e494dba51f805534bce5235132be1eeabde3a118d42d87ce49d51fd6" [[file]] path = "justfiles/anvil/checks/udeps.just" @@ -171,6 +175,10 @@ checksum = "sha256:3363992c6c006c649eae3732b2a7b04c2a4925eff369179b128d43380e280 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" @@ -181,7 +189,7 @@ checksum = "sha256:9a94b6ae2d212f91d324e401ebdb2238805f3450d95d4d45df791d7f1679a [[file]] path = "justfiles/anvil/groups/pr-slow.just" -checksum = "sha256:8b7c490cb6eb20c31549ef71eb75eac86ae145c8c6380ee1f590ccd81eec48ea" +checksum = "sha256:c3281f2044494dcfcfa7544b022ce3d751444241d93d3e8f136669d72d102528" [[file]] path = "justfiles/anvil/groups/pr-test.just" @@ -209,11 +217,11 @@ checksum = "sha256:dc42a4b2ed4a25e3a37b322dad209df34d7adca88bf3c4de60b2f8c1b396e [[file]] path = "justfiles/anvil/impact.just" -checksum = "sha256:5714a138135154b91b234f6bad06ec3ade0e5780f761d631c1eca92b6010e5ad" +checksum = "sha256:0b3a5c96dd33384b4c86ac02b17396bc1a9fe9c82edbc516a2ceea7f4fe3833d" [[file]] path = "justfiles/anvil/mod.just" -checksum = "sha256:2f7f0187f8c45716a1bed85f0c45ffbc71cdf592ed6414c9bc4cd72cf716a3a5" +checksum = "sha256:4892e7a17d5f576241834041c89916708b23e88503156721af3048b2a358538f" [[file]] path = "justfiles/anvil/tiers.just" @@ -221,11 +229,11 @@ checksum = "sha256:00453a12cbb34811ee6a2c083dade5f6198575e3b0610f49e4743366326cd [[file]] path = "justfiles/anvil/tools.just" -checksum = "sha256:c1f5181d14a734cde8ccc32bbf5acece485791573b7a95cfff61b63d0f1fcb97" +checksum = "sha256:48b887481a7eb8dc7615367347c93b8a02dd7baafb0ef80ece883f03600285dd" [[file]] path = "justfiles/anvil/versions.just" -checksum = "sha256:b48c7a29f694acdc4ff02a15bfdae5a286c1d0b6272fffad3acefbd8b1f2dcaa" +checksum = "sha256:ae6676fb883aa58adb0995384adef477957ad7df10fc43439517cfb617fd92d1" [[region]] host = ".anvil/container/Dockerfile" @@ -250,7 +258,7 @@ checksum = "sha256:3788845ea3d4c483917954ddf9bccf918bf69ab7c64bbf559baf59d5781c4 [[region]] host = ".anvil/container/Dockerfile" id = "anvil-container-tools" -checksum = "sha256:0b6923e81ec99170ece298fcd9ef875f26a3ff018708ce5ec21b4b1dd7a18900" +checksum = "sha256:ba7456ca88da39f72024dd0616e56d49d1aef7a5d26b81a8b33efccb13321379" [[region]] host = ".delta.toml" diff --git a/.anvil/container/Dockerfile b/.anvil/container/Dockerfile index da41040cc..bcdd96981 100644 --- a/.anvil/container/Dockerfile +++ b/.anvil/container/Dockerfile @@ -75,7 +75,6 @@ RUN curl -fsSLo /tmp/cargo-binstall.tgz \ && tar -xzf /tmp/cargo-binstall.tgz -C "${CARGO_HOME}/bin" cargo-binstall \ && chmod 755 "${CARGO_HOME}/bin/cargo-binstall" \ && rm /tmp/cargo-binstall.tgz - # <<< anvil-managed: anvil-container-tools # >>> anvil-managed: anvil-container-setup diff --git a/.github/actions/anvil-setup/action.yml b/.github/actions/anvil-setup/action.yml index f1557210d..0c5c0077a 100644 --- a/.github/actions/anvil-setup/action.yml +++ b/.github/actions/anvil-setup/action.yml @@ -13,8 +13,8 @@ inputs: - "" (default): install the full catalog via `just anvil-setup` -- use for local "give me everything" flows. - - "none": skip tool installation entirely; just bootstrap the - rust toolchain + just + binstall + cache. Used by + - "none": skip tool installation entirely; just restore the + Cargo cache and bootstrap just + binstall. Used by anvil-impact, which installs only cargo-delta afterwards. - a name containing only lowercase letters, digits, and hyphens: install only that group's prerequisites via @@ -77,48 +77,29 @@ runs: } "Free after cleanup (GiB): $(Get-FreeDiskGiB)" - - id: rustc-version - shell: bash - run: echo "version=$(rustc --version | awk '{print $2}')" >> "$GITHUB_OUTPUT" + # Restore before bootstrapping Just so a warm job can reuse the cached + # executable. Repository toolchain files intentionally define a fresh + # cache generation, bounding registry growth without invalidating on + # routine Cargo.toml or Cargo.lock edits. - name: Restore cargo cache id: cargo-cache uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: - # Key on OS + arch + rustc version + lockfile hashes + catalog - # hash so a Rust toolchain bump, a cross-arch matrix leg, or a - # tool-versions update all invalidate the cache cleanly. - # runner.arch resolves to X64 / ARM64 / X86, which keeps the - # x86_64 and aarch64 legs of the same OS from colliding on - # arch-incompatible cached binaries in ~/.cargo/bin. - # - # ${{ github.job }} discriminates by workflow-job-id (`pr-fast`, - # `pr-test`, `pr-slow`, etc.) so concurrent matrix legs that - # share OS+arch (e.g. pr-fast linux + pr-test linux) don't race - # on the same cache key. Without this, the first-to-finish job - # reserves the key, the others get "Unable to reserve cache: - # another job may be creating this cache" and silently skip - # the save -- leaving the cache empty forever. The restore-keys - # fall back across jobs so the install work is still shared - # across sibling legs on subsequent runs. - key: anvil-v1-${{ runner.os }}-${{ runner.arch }}-rust${{ steps.rustc-version.outputs.version }}-${{ hashFiles('Cargo.lock', '.cargo/config.toml', 'rust-toolchain.toml', 'justfiles/anvil/versions.just') }}-${{ github.job }} + # The job suffix prevents concurrent matrix jobs from racing to save + # one key. The prefix shares a completed cache across sibling jobs. + # There is deliberately no fallback across toolchain/catalog changes: + # those inputs are the cache-pruning boundary. + key: anvil-v2-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('.cargo/config.toml', 'rust-toolchain', 'rust-toolchain.toml', 'justfiles/anvil/versions.just') }}-${{ github.job }} restore-keys: | - anvil-v1-${{ runner.os }}-${{ runner.arch }}-rust${{ steps.rustc-version.outputs.version }}-${{ hashFiles('Cargo.lock', '.cargo/config.toml', 'rust-toolchain.toml', 'justfiles/anvil/versions.just') }}- - anvil-v1-${{ runner.os }}-${{ runner.arch }}-rust${{ steps.rustc-version.outputs.version }}- - anvil-v1-${{ runner.os }}-${{ runner.arch }}- + anvil-v2-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('.cargo/config.toml', 'rust-toolchain', 'rust-toolchain.toml', 'justfiles/anvil/versions.just') }}- # `.crates.toml` and `.crates2.json` track which cargo-installed # tools and versions live in ~/.cargo/bin/. Without them in the # cache, `cargo install --list` and (downstream) the # tool-install recipes' early-skip on "installed >= pin" don't - # see the cached binaries — every run then tries to reinstall - # on top of them and fails with "binary X already exists in - # destination". + # see the cached binaries. # - # target/ (the compilation output dir) is deliberately NOT cached here: - # per the GitHub backend design's caching policy, multi-GB per-job - # target/ trees dwarf the high-value tool cache under the repo cache - # quota, give only modest recompile savings once tools are cached, and - # could overwrite the downloaded target/anvil/impact/ cache. Only the - # ~/.cargo tool + registry state below is cached. + # target/ is deliberately excluded: per-job target trees dwarf the + # tool cache and could overwrite the downloaded impact cache. path: | ~/.cargo/registry/cache/ ~/.cargo/registry/index/ @@ -126,31 +107,7 @@ 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 - # 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. + # cargo-binstall keeps the cold bootstrap path fast. - name: Install cargo-binstall uses: cargo-bins/cargo-binstall@v1.21.0 # immutable release, the tag cannot be moved @@ -161,6 +118,11 @@ runs: cargo binstall --no-confirm --locked just || cargo install --locked just fi + # The catalog recipe `anvil-setup` (or `anvil--setup` + # when a group is specified via the `group` input) uses the Just executable + # bootstrapped above, then handles everything else. The setup recipes are idempotent and + # short-circuit on tools already installed at or above the pinned + # version, so re-running on cache-hit runs is cheap. - 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 116a15ceb..911c0f29d 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 6c565c5ae..d3eb1fb20 100644 --- a/crates/cargo-anvil/README.md +++ b/crates/cargo-anvil/README.md @@ -77,6 +77,14 @@ Flags: `catalog:` checksum — a `sha256` over the whole compiled-in catalog — so two builds at the same version but different catalogs are distinguishable. +Before running ordinary checks, provide a deterministic Rust selection: +a caller-set `RUSTUP_TOOLCHAIN`, either root `rust-toolchain` file spelling, +or a root `[workspace.package].rust-version` / `[package].rust-version`. +Anvil passes no explicit selector for the environment or file cases so +rustup handles them natively; only the root MSRV becomes `+`. +Repositories with none of these sources fail instead of inheriting the +runner’s ambient compiler. + ### Daily driver After the first run, your daily workflow is plain `just`: @@ -266,11 +274,11 @@ the tables below map each check to the group that runs it, link each check to its tool’s documentation, and note anything anvil-specific. **PR tier** (`anvil-pr`) — runs on every pull request, impact-scoped -both locally and in cloud workflows. 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 +`pr-slow` groups run as independent parallel jobs per OS leg: - + @@ -287,9 +295,10 @@ sub-groups run sequentially within the one job per OS leg): - + + @@ -482,7 +491,7 @@ And `docs/verification.md` for the continuous-validation strategy. This crate was developed as part of The Oxidizer Project. Browse this crate's source code. - [__cargo_doc2readme_dependencies_info]: ggGmYW0CYXZlMC43LjNhdIQbFhzZ8rzWNNYbuRaDSGWynFgbH4PMdoT7GNcbVwNPtPjAhvFhYvRhcoQb0-jr0JbRf5oblTuBxsHXMUIbSWUr86twfxQbfGu3X0VHoephZIGDa2NhcmdvLWFudmlsZTAuNi4wa2NhcmdvX2Fudmls + [__cargo_doc2readme_dependencies_info]: ggGmYW0CYXZlMC43LjNhdIQbFhzZ8rzWNNYbuRaDSGWynFgbH4PMdoT7GNcbVwNPtPjAhvFhYvRhcoQbLvVGTNtetQUbnp9vX0Ew7_gbkZEyxfXZXyMbltL72AXa-o1hZIGDa2NhcmdvLWFudmlsZTAuNi4wa2NhcmdvX2Fudmls [__link0]: https://crates.io/crates/cargo-delta [__link1]: https://docs.rs/cargo-anvil/0.6.0/cargo_anvil/?search=artifacts::container [__link10]: https://docs.rs/cargo-anvil/0.6.0/cargo_anvil/?search=artifacts diff --git a/crates/cargo-anvil/docs/design/README.md b/crates/cargo-anvil/docs/design/README.md index 015aa6332..205747376 100644 --- a/crates/cargo-anvil/docs/design/README.md +++ b/crates/cargo-anvil/docs/design/README.md @@ -12,7 +12,7 @@ user-visible shape of the tool. Detail lives in companion documents: - [updates.md](./updates.md) — the drift-detection and update algorithm; opt-out semantics. - [extensibility.md](./extensibility.md) — how downstream tools ship their own brand + catalog. - [github.md](./github.md) — GitHub Actions emission, example workflows, impact wiring. -- [ado.md](./ado.md) — Azure DevOps Pipelines emission, 1ESPT/msrustup composition. +- [ado.md](./ado.md) — Azure DevOps Pipelines emission and compliance-template composition. - [containers.md](./containers.md) — containerized execution: the explicit `anvil-container` recipe, the content-addressed image, and the credential hook. - [../implementation.md](../implementation.md) — internal implementation guidance. @@ -27,12 +27,12 @@ implemented six different ways: | Repo | cloud workflows | Justfile shape | Toolchain | Notable specifics | |------|----|----------------|-----------|-------------------| -| `oxidizer` | ADO 1ESPT (`SubstratePT`) | 500-line monolith + `just_mutants.just` | `ms-prod-1.93` | Stage flags (`enableStages`), `cargo-aprz`, stable-API checks | +| `oxidizer` | ADO 1ESPT (`SubstratePT`) | 500-line monolith + `just_mutants.just` | caller-provisioned | Stage flags (`enableStages`), `cargo-aprz`, stable-API checks | | `oxidizer-github` | GitHub Actions | Modular `justfiles/{basic,coverage,format,setup,spelling}.just` + `constants.env` | `1.93` | `cargo-delta` impact-scoped builds, sticky semver comments, composite `setup` action | -| `ox-tools` | ADO (CloudBuild + classic) | none in worktree | `ms-prod-1.92` | NuGet/MSBuild scaffolding, internal templates | +| `ox-tools` | ADO (CloudBuild + classic) | none in worktree | caller-provisioned | NuGet/MSBuild scaffolding, internal templates | | `ox-tools-gh` | GitHub Actions | Same modular shape as `oxidizer-github` | `1.93` | Mirror surface to OSS oxidizer | -| `assistants-oxide` | ADO 1ESPT (custom `rust/`) | Monolith + `.just/tds.just` | `ms-prod-1.93` | Symcrypt setup steps, NuGet publish stage | -| `ox-docs` | ADO classic | Monolith | `ms-prod-1.88` | Mixed C#/.NET + Rust, mdbook/docfx | +| `assistants-oxide` | ADO 1ESPT (custom `rust/`) | Monolith + `.just/tds.just` | caller-provisioned | Symcrypt setup steps, NuGet publish stage | +| `ox-docs` | ADO classic | Monolith | caller-provisioned | Mixed C#/.NET + Rust, mdbook/docfx | The same logical checks (clippy, fmt, deny, miri, mutants, coverage, hack feature-powerset, udeps, semver, spellcheck, license headers, doc/doctest, careful, audit, ensure-no-cyclic-deps, @@ -58,11 +58,15 @@ missed, and onboarding new Rust repos requires copying-and-praying. full tier are all reproducible locally with a single `just` invocation, using the exact same arguments cloud workflows uses. The three commands `just anvil-pr`, `just anvil-scheduled`, and `just anvil-full` (= pr + scheduled) are first-class local entry points. -6. **Plain-cargo fallback**: a developer with only `cargo` installed (no `just`, no +6. **Deterministic ordinary-check compiler selection**: use the caller's + `RUSTUP_TOOLCHAIN`, otherwise defer to either root toolchain-file spelling, + otherwise use the root manifest MSRV explicitly. Never inherit a + runner-default compiler when none of those sources exists. +7. **Plain-cargo fallback**: a developer with only `cargo` installed (no `just`, no `cargo-anvil`) can still build and run tests. -7. **Friendly updates**: the tool detects, per file and per managed region, whether the user has +8. **Friendly updates**: the tool detects, per file and per managed region, whether the user has modified it, and updates only the unmodified bits. -8. **Open source**: the crate ships from `github.com/microsoft/ox-tools` and publishes to +9. **Open source**: the crate ships from `github.com/microsoft/ox-tools` and publishes to crates.io. The binary contains no Microsoft-internal dependencies; everything it can install on the user's behalf comes from crates.io. @@ -72,12 +76,15 @@ 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; caller-provisioned + and path toolchains remain the execution environment's responsibility. +- Provisioning a separate compiler for Cargo-installed tools or stable analyzers. They use the + same repository-selected compiler as ordinary checks; nightly-only checks retain their + catalog pins. - Managing exact tool versions on the user's behalf — we enforce minimums only. See [local.md §3](./local.md#3-tool-versions-and-installation). - Hosting a service. The tool is a CLI binary; updates ship via crates.io. @@ -244,7 +251,7 @@ repo/ ├── rustfmt.toml managed-region: anvil-rustfmt (opt out with empty stub) ├── .delta.toml managed-region: anvil-delta (points to owned cloud config) ├── .gitattributes managed-region: anvil-gitattributes (pins *.rs to LF) -├── rust-toolchain.toml user-authored (read only) +├── rust-toolchain[.toml] optional, user-authored (read only) ├── .cargo/config.toml user-authored (read only) │ ├── .github/ only if --backend github (or autodetected) — see github.md @@ -305,11 +312,13 @@ 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 root spelling exists, Anvil selects the root manifest's + `[workspace.package].rust-version` or `[package].rust-version`. GitHub setup installs a + missing public selection through rustup. ADO callers provide their Rust bootstrap before + Anvil runs. The caller's `RUSTUP_TOOLCHAIN` is an input override only and is inherited by + child commands rather than rewritten or exported by Anvil. The tool's persistent state lives in `.anvil.lock` at the repo root — the sidecar manifest tracking last-rendered checksums per owned file and per managed region. See @@ -400,7 +409,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 c985a3433..135df73a4 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,16 @@ 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 +after reporting that it skipped the test. Otherwise it runs affected-package +`cargo test --all-targets` in all-features and default-features configurations. +Ordinary stable checks honor a caller-provided `RUSTUP_TOOLCHAIN`. The dedicated +MSRV setup ensures the declared root MSRV is available through rustup. + ### 4.1 Per-job wrapper (`steps/job.yml`) — the 1ESPT extensibility point Every job in `pr.yml` and `scheduled.yml` is rendered through a wrapper template at @@ -648,13 +672,11 @@ steps: # probed from the cacheable impact.state marker. See §4.3. ``` -The only per-group parameters are the PR-context strings a group's checks consume: - -| Template | Parameters | -|---------------------------|-------------------------------------------------------------------------| -| `pr-fast.yml` | `prTitle` (resolved from the REST API; ADO has no PR-title variable) | -| `pr-mutants.yml` | `prBaseRef` (default `$(System.PullRequest.TargetBranch)`) | -| `pr-test.yml`, `pr-runtime-analysis.yml`, `scheduled-*.yml` | — | +The generated per-group step templates take no parameters. Only `pr-fast.yml` +performs the PR-title REST lookup and injects `PR_TITLE`; groups that do not run +`anvil-pr-title`, including `pr-msrv`, have no title API or OAuth-token +dependency. The mutation recipe reads its base ref from ADO's predefined +pull-request environment. `$(System.PullRequest.*)` are auto-populated by ADO on PR build-validation runs. No manual web-UI wiring is needed. @@ -676,13 +698,14 @@ 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 +`setup.yml` is a step template that restores Cargo home, bootstraps Just, and +then invokes the requested catalog setup recipe, whose prerequisites provision +the selected compiler and tools. It takes a single `group` parameter that controls which recipes run: - empty (default): runs `just anvil-setup` -- the full catalog. Use for "give me everything" flows. -- `none`: skips the catalog setup entirely. Used by `impact.yml`, which only +- `none`: skips the group/full tool fan-out. Used by `impact.yml`, which only needs `cargo-delta` and installs it itself afterwards. - any other value (e.g. `pr-fast`, `scheduled-advisories`): runs `just anvil--setup` -- only the tools, components, and toolchains @@ -690,10 +713,9 @@ takes a single `group` parameter that controls which recipes run: (`.pipelines/anvil/steps/.yml`) passes its own group name here, so a `pr-fast` matrix leg never installs cargo-mutants. -The template does not install Rust; it expects `cargo` on PATH -- provided by the -user's msrustup step in 1ESPT pipelines or by a previous step in OSS pipelines -(see §6). ADO uses the default `install` backend (source builds) because -`cargo-binstall` has unresolved compliance issues for internal ADO pipelines. +The template expects the caller to provide the Rust/rustup bootstrap and +`cargo` on PATH (see §6). ADO uses the default `install` backend (source +builds) so adopters do not need to approve a binary-installation service. `impact.yml` invokes `setup.yml` with `group: none`, then installs `cargo-delta` via `anvil-tool-cargo-delta-install` and runs the shared **`just anvil-impact`** @@ -717,34 +739,38 @@ 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. Selection follows the shared local +contract: + +1. inherit an existing caller-provided `RUSTUP_TOOLCHAIN` unchanged; +2. when either root `rust-toolchain` spelling exists, pass no explicit selector + and let rustup process toolchain files natively; +3. otherwise pass the root manifest MSRV explicitly as `+`. + +There is no agent-default fallback. With no environment override, root +toolchain file, or root MSRV, setup and checks fail. Anvil does not parse +repository toolchain files or replay options from a file suppressed by the +environment override. Native file processing follows rustup's lookup from each +Cargo or Rust command's working directory. + +The generated setup step restores Cargo home before bootstrapping Just, then +invokes the selected catalog setup recipe. Setup ensures the selected compiler +is available before stable Cargo or Rust use and does not publish a rewritten +`RUSTUP_TOOLCHAIN` to later steps. Prerequisite validation remains read-only: +for a root-MSRV fallback, it requires rustup and verifies the exact toolchain is +already installed before running Cargo metadata. Nightly-requiring checks still +fail with a provisioning hint when their dated catalog toolchain is absent. ## 7. Caching -`setup.yml` computes a cache key from agent OS and architecture, the actual -`rustc --version`, and hashes of `Cargo.lock`, `.cargo/config.toml`, -`rust-toolchain.toml`, and `versions.just`. It uses the ADO `Cache@2` task. +`setup.yml` restores Cargo home before bootstrapping Just so a warm job does not +compile Just before reaching the cache. The ADO `Cache@2` key uses agent OS and +architecture plus Cargo configuration, the repository toolchain-file +fingerprint, and `versions.just`. Toolchain-file changes deliberately start a +fresh cache generation to bound registry growth. Routine `Cargo.toml`, +`Cargo.lock`, and compiler-version changes do not invalidate standalone cached +tools. Rustup toolchains themselves are outside Cargo home. The cache covers: @@ -802,7 +828,7 @@ For repos with an existing 1ESPT-extending pipeline, adopting anvil is increment anvil's owned templates compose cleanly with the 1ESPT `enableStages` flag system: each group is its own job inside the `ANVIL_pr` stage, so 1ESPT can gate or split them as -needed. The pre-existing repo-specific compliance steps (msrustup, NuGet pushes, signing, +needed. The pre-existing repo-specific compliance steps (provisioning, signing, …) keep running alongside the anvil stage. anvil does not own the pipeline's shape — it just contributes a stage. diff --git a/crates/cargo-anvil/docs/design/checks.md b/crates/cargo-anvil/docs/design/checks.md index 7a103a898..7ccb24994 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,20 +106,28 @@ 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 groups collected by the `pr-slow` umbrella run as parallel cloud-workflow +jobs/stages. Locally, `just anvil-pr-slow` invokes those groups in order, and +`just anvil` is an alias for `just anvil-pr`.) -### PR tier (4 groups) +### PR tier | Group | OS scope | Purpose | |--------------------|---------------------------------------|----------------------------------------------------------------------------------------------------------------------| | `pr-fast` | Linux x86_64 + Windows x86_64 + Linux aarch64 + Windows aarch64 (GH) / Linux x86_64 + Windows x86_64 (ADO) | All static analysis: clippy, `udeps`, `semver-check`, `external-types`, plus the text/metadata checks (fmt, license-headers, ...). Cross-OS because clippy, doc-build, udeps, semver-check, and external-types all compile per host target. Text/metadata checks run on every leg too; the redundancy cost is negligible compared to a separate job's setup overhead. | | `pr-test` | Same default as `pr-fast` | Tests + coverage: `llvm-cov` (instrumented `nextest`), `doc-test`, `examples`. Coverage is uploaded once from the canonical x86_64 Linux leg. | +| `pr-msrv` | Same default as `pr-test` | Affected-package all-target tests under the declared MSRV, in all-features and default-features configurations. The recipe is a no-op when no root MSRV is declared. | | `pr-runtime-analysis` | Same default as `pr-fast` | Stricter-runtime correctness: `miri`, `careful`, `loom` (concurrency model checking), `bolero` (short-duration fuzzing smoke). Impact-scoped to the affected set so wall-clock is proportional to the PR's blast radius; the cheap checks (loom/bolero) self-skip when no affected crate ships their harness. | | `pr-mutants` | Linux x86_64 + Windows x86_64 + Linux aarch64 (GH) / Linux x86_64 + Windows x86_64 (ADO) | Diff-scoped mutation testing (`mutants --in-diff`). The recipe self-skips on `aarch64-pc-windows-msvc` (cargo-mutants doesn't build there), so the GH windows-arm leg is a no-op rather than a job failure. | -The 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 `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 invokes those groups sequentially. -### scheduled tier (4 groups) +### scheduled tier | Group | OS scope | Purpose | |----------------------|---------------------------|----------------------------------------------------------------------------------------------------------------------------------------| @@ -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) invokes its groups in sequence against whatever OS the developer is on. See [README.md §8.3](./README.md#83-cross-os-test-matrices) for the overall rationale. @@ -153,6 +163,15 @@ 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. Caller-provided `RUSTUP_TOOLCHAIN` remains a native rustup input and +is inherited unchanged by child commands. The presence of either root +toolchain-file spelling suppresses an explicit selector so rustup can process +the file natively at each command's working directory. Only the root MSRV +fallback produces an explicit `+toolchain`; with no source, the command fails. +Setup makes the selected compiler available before stable Cargo or Rust runs, +while paired prerequisite validation remains read-only. + ### `pr-fast` | Check | Invocation | Source | @@ -173,12 +192,12 @@ that provided the strongest version of the check. | `semver-check` | `cargo semver-checks --baseline-rev ` per affected publishable library crate. Crates with `publish = false` and bin-only crates are skipped. The PR target is the baseline. Exit 100 is a completed check with deny-level findings; exit 101 or another nonzero status means the comparison was inconclusive. Both outcomes write `target/anvil/comments/semver.md` and remain advisory, matching the repository's native `semver` job (`continue-on-error: true`). Proven rename and bin→lib transitions with no comparable baseline, and dependencies proven to be yanked only in the checked-out baseline tree, are skipped without a comment. Anvil preflight failures such as invalid current-workspace metadata or an unavailable baseline ref still fail because the recipe cannot establish what to compare. | oxidizer-github | | `external-types` | `cargo + check-external-types --manifest-path` per library crate (per-manifest because the tool has no `--workspace`/`--package`; bin-only crates have no public API surface and are skipped). Setup installs the catalog version but validation accepts newer installed tools. The selected nightly is tested with the catalog version; an incompatible newer tool fails closed with a tool/nightly compatibility diagnostic rather than silently selecting a different schema. | oxidizer-github | -### `pr-slow` +### `pr-slow` umbrella -The PR-tier slow checks are split into 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 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 those groups sequentially so adopters can type one command to run "everything slow" without needing the cloud workflow matrix overhead. @@ -190,7 +209,28 @@ matrix overhead. | `doc-test` | Two cargo-test runs over the same affected set: `cargo test --doc --workspace --all-features --locked` and `cargo test --doc --workspace --locked` (default features). Running both catches doctests that only compile under one feature configuration (oxidizer-github runs both). nextest does not run doctests, so this stays a separate cargo-test invocation. | oxidizer, oxidizer-github | | `examples` | `cargo build --workspace --examples --all-features --locked` -- verifies that example targets compile. Running each example is intentionally not part of the check (examples are not test scaffolding; their runtime behavior isn't part of what we gate on). | oxidizer, oxidizer-github | -This is the same set of checks that used to live in the standalone `pr-test` group; merging into `pr-test` removes one cloud-workflow job from the matrix without changing what runs. +#### `pr-msrv` (minimum-version tests) + +When the root manifest declares an MSRV, Anvil runs `cargo test --all-targets` +for affected packages under that compiler. Cargo's all-target set covers +library and binary unit tests, integration tests, examples, and benches as test +targets. Anvil runs exactly two feature configurations: `--all-features` and +the default features. It does not add a `--no-default-features` pass; such a +pass can exercise feature-negative code, but it is outside the current policy. +This is the all-target test execution at the minimum supported compiler; +`pr-test` runs the same affected suite through coverage instrumentation on the +catalog nightly. Other checks that use the selected stable compiler do not execute +this all-target suite, so an MSRV fallback does not make `pr-msrv` a duplicate. +A selecting toolchain file does not suppress the MSRV run, even when it selects the +same compiler, because the MSRV group is the authoritative minimum-version test +result. + +The group uses the same OS/architecture matrix and per-OS impact sets as `pr-test` +so cfg-gated targets and dependencies are exercised under the MSRV. It runs in +parallel with the other PR groups. When no root MSRV exists, +`anvil-msrv-test` reports the skip and exits successfully without running tests. +Setup installs the declared MSRV through rustup when it is not already +available. #### `pr-runtime-analysis` (stricter-runtime correctness) @@ -233,9 +273,9 @@ publish a full-coverage snapshot for the current state of `main`. These checks share a property: their outcome can change without a commit to this repo. `deny`/`audit`/`aprz` consult external databases (RustSec advisory DB, license registries, Azure risk indices). `clippy` reflects whatever lint set ships with the currently-installed -toolchain -- even when `rust-toolchain.toml` is pinned, repos using floating channels -(`stable`, or msrustup channel pointers like `ms-prod-1.93`) can pick up new lints when the -pointer is bumped upstream. Re-running these on the scheduled tier turns "something landed +toolchain -- even when `rust-toolchain.toml` is pinned, repositories using a +floating channel such as `stable` can pick up new lints when the channel moves. +Re-running these on the scheduled tier turns "something landed upstream yesterday" into a tracked failure rather than an invisible regression discovered next time someone opens an unrelated PR. @@ -355,7 +395,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 3aaeb6941..b4753c5be 100644 --- a/crates/cargo-anvil/docs/design/containers.md +++ b/crates/cargo-anvil/docs/design/containers.md @@ -53,6 +53,10 @@ Both are addressed by executing the recipe unchanged inside an image built from bodies are identical in either mode, and cloud workflows are unaffected: they run the same recipes natively on their own agents. The image is pinned to resemble that environment, not to reproduce it. +Image construction has a deliberately stricter compiler contract than native execution: the repository must own +`rust-toolchain.toml`. The build context admits that file but not `rust-toolchain`, and the build does not inherit the +host's `RUSTUP_TOOLCHAIN`; rustup therefore selects the image compiler from the repository-owned TOML file. + ## 2. Command surface `anvil-container` takes the argv to run inside the image; nothing is routed there automatically, so everything @@ -74,7 +78,7 @@ through the environment instead. | `just anvil-container-status` | Print the engine, working directory, image reference, and whether it is present. Never builds or pulls. | | `just anvil-container-down` | Remove this repository's cache volumes. The image is retained. | -All four are annotated `[group("anvil-container")]` and appear as one cluster in `just --groups`. Each repeats a +These recipes are annotated `[group("anvil-container")]` and appear as one cluster in `just --groups`. Each repeats a one-line summary immediately above its attributes, because `just --list` takes the last comment line before them as the description and would otherwise print the tail of a rationale paragraph as a fragment. @@ -604,7 +608,7 @@ belongs everywhere is still better made in a catalog, where every consumer gets Replacing a *region* rather than the whole file is what makes a downstream catalog cheap to keep current: `dockerfile_setup()` and `dockerfile_entry()` are the contract with the driver and are inherited, so an Azure Linux or -msrustup catalog rewrites the base and tool layers and nothing else. Replacing `dockerfile_setup()` reintroduces the +private-environment catalog rewrites the base and tool layers and nothing else. Replacing `dockerfile_setup()` reintroduces the second tool list the design exists to avoid, and is almost never right. **A replacement must keep the ignore file in step.** A region that `COPY`s anything outside `justfiles/anvil/`, diff --git a/crates/cargo-anvil/docs/design/github.md b/crates/cargo-anvil/docs/design/github.md index d216e6c38..2f48f5ec3 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 @@ -296,7 +301,8 @@ action in `consume` mode; which tiers a group's checks actually consume from tha is the catalog's concern, not the wiring layer's. Moving a check between groups never changes the reusable workflow. -Approximate shape (anvil writes this verbatim; users never edit it): +Representative emitted shape (the values shown are executable; repeated jobs +and steps called out in comments are omitted for brevity): ```yaml # .github/workflows/anvil-pr-impl.yml (owned by cargo-anvil) @@ -316,13 +322,13 @@ jobs: impact-linux: runs-on: ${{ inputs.linux_runner }} steps: - - uses: actions/checkout + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: { fetch-depth: 0 } - uses: ./.github/actions/anvil-impact # runs `just anvil-impact` + upload-artifact anvil-impact-Linux impact-windows: runs-on: ${{ inputs.windows_runner }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: { fetch-depth: 0 } - uses: ./.github/actions/anvil-impact # uploads anvil-impact-Windows @@ -338,12 +344,12 @@ jobs: || matrix.os == 'linux-arm' && inputs.linux_arm_runner || inputs.windows_arm_runner }} steps: - - uses: actions/checkout + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: { fetch-depth: 0 } # semver-check needs origin/ resolvable for --baseline-rev # Download the impact cache computed on this leg's OS into # target/anvil/impact/ (arm reuses its OS-family artifact). pr-test / # pr-runtime-analysis / pr-mutants do the identical download. - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: anvil-impact-${{ startsWith(matrix.os, 'linux') && 'Linux' || 'Windows' }} path: target/anvil/impact @@ -368,9 +374,9 @@ jobs: || matrix.os == 'linux-arm' && inputs.linux_arm_runner || inputs.windows_arm_runner }} steps: - - uses: actions/checkout + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 # preceded by the same per-OS download-artifact step as pr-fast - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: anvil-impact-${{ startsWith(matrix.os, 'linux') && 'Linux' || 'Windows' }} path: target/anvil/impact @@ -380,10 +386,32 @@ 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: ${{ 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 + - 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 + impact_mode: consume + free-disk-space: true + + # pr-runtime-analysis (miri + careful) and pr-mutants (mutants) follow the + # multi-OS shape; pr-mutants additionally sets `env: BASE_REF` for + # diff-scoped cargo-mutants. ``` Every multi-OS job hardcodes its OS axis as an inline YAML array. Per-leg runner @@ -396,6 +424,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. GitHub always +creates the matrix; the generated recipe exits successfully at recipe level when no +root MSRV is declared. + The pr-* jobs gate on the impact jobs *succeeding*: their `needs: [impact-linux, impact-windows]` uses GitHub's default behavior, so if an impact job fails the pr-* jobs are skipped and the run fails at impact (we add no `if: always()` / `if: @@ -428,7 +463,7 @@ caller anvil-scheduled.yml ├─ scheduled-runtime-analysis (Linux/Windows × x64/ARM64) ├─ scheduled-exhaustive (Linux/Windows x64) └─ publish-failure - needs: all four scheduled groups + needs: all scheduled groups condition: at least one failure and publication not disabled job override: issues:write only ``` @@ -704,13 +739,14 @@ 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 +`anvil-setup` is a composite action that restores Cargo home, bootstraps +cargo-binstall and Just, then invokes the requested catalog setup recipe, whose +prerequisites provision the selected compiler and tools. Its `group` input controls which recipes run: - empty (default): runs `just anvil-setup binstall` -- the full catalog. Use for local "give me everything" flows. -- `none`: skips the catalog setup entirely. Used by `anvil-impact`, which only +- `none`: skips the group/full tool fan-out. Used by `anvil-impact`, which only needs `cargo-delta` and installs it itself afterwards. - any other value (e.g. `pr-fast`, `scheduled-advisories`): runs `just anvil--setup binstall` -- only the tools, components, and @@ -726,15 +762,16 @@ 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 GitHub-hosted runner, it removes pre-installed toolchains that anvil's Rust checks do not use: Android, Haskell/GHC, Swift and browser drivers on Linux; Android and Haskell/GHC on Windows. This reclaims approximately 18 GB on Linux and 17 GB on -Windows. It is a no-op on macOS and self-hosted runners. The generated reusable -workflows explicitly enable this input only for `pr-test` and `scheduled-test`, +Windows. It is a no-op on macOS and self-hosted runners. The generated reusable +workflows explicitly enable this input only for `pr-test`, `pr-msrv`, and `scheduled-test`, mirroring the testing-job integration in [microsoft/oxidizer#583](https://github.com/microsoft/oxidizer/pull/583). Other groups retain the action's disabled default. @@ -804,25 +841,34 @@ 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. +Selection follows the shared local contract: + +1. inherit an existing caller-provided `RUSTUP_TOOLCHAIN` unchanged; +2. when either root `rust-toolchain` spelling exists, pass no explicit selector + and let rustup process toolchain files natively; +3. otherwise pass the root manifest MSRV explicitly as `+`. + +There is no runner-default fallback. With no environment override, root +toolchain file, or root MSRV, setup and checks fail. Anvil never parses a +toolchain file or replays options from a file suppressed by +`RUSTUP_TOOLCHAIN`. Because file selection remains native, rustup applies its +normal lookup from each Cargo or Rust command's working directory. + +The setup action restores Cargo home, bootstraps cargo-binstall and Just, and +then invokes the selected catalog setup recipe. Setup ensures the selected +compiler is available before stable Cargo or Rust runs. GH-hosted runners +provide the rustup proxy used to install a missing public MSRV or process a +repository toolchain file. A caller-provided or path toolchain remains the +runner owner's provisioning responsibility. Setup does not publish a rewritten +`RUSTUP_TOOLCHAIN` through `GITHUB_ENV`. -On self-hosted runners or pre-baked images without rustup, the user adds a Rust install -step to their root workflow before the `uses:` of the reusable workflow: +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,17 +883,22 @@ 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. +Prerequisite validation remains read-only. For the root-MSRV fallback, it +requires rustup and verifies the exact MSRV toolchain is already installed +before running Cargo metadata, preventing validation from auto-installing a +compiler. ## 8. Caching The `anvil-setup` composite action computes a cache key from runner OS and -architecture, the actual `rustc --version`, hashes of `Cargo.lock`, -`.cargo/config.toml`, `rust-toolchain.toml`, and `versions.just`, plus the workflow -job ID. Job discrimination prevents concurrent jobs from racing to save one key; -prefix restore keys still share prior installs across jobs. +architecture plus hashes of `.cargo/config.toml`, either supported repository +toolchain file, and `versions.just`, followed by the workflow job ID. +Toolchain-file or catalog changes deliberately start a fresh cache generation +to bound registry growth; there is no restore fallback across those boundaries. +Routine `Cargo.toml`, `Cargo.lock`, and compiler-version changes do not +invalidate standalone cached tools. Job discrimination prevents concurrent +jobs from racing to save one key, while the fingerprint prefix shares prior +installs across jobs within the same cache generation. The cache covers: diff --git a/crates/cargo-anvil/docs/design/local.md b/crates/cargo-anvil/docs/design/local.md index 32ef23ef5..f2aecb668 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 pr-slow groups. │ ├── container.just containerized execution (`anvil-container`). See containers.md. │ ├── tiers.just tier aggregators (anvil-pr, anvil-scheduled, anvil-full). │ ├── tools.just tool/component/toolchain install + validate-prereqs recipes, │ │ 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/ the container image definition ├── Dockerfile composed: anvil-managed regions, your content between @@ -114,6 +114,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 @@ -122,10 +132,10 @@ namespaces are kept disjoint by naming choice: no check is named `- any tier × group combination (e.g. the coverage-instrumented test check is named `llvm-cov`, not `test`, so that group names like `anvil-pr-test` unambiguously refer to a group recipe). -The `pr-slow` work is split into 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 independent cloud-workflow-visible groups +(`pr-test`, `pr-msrv`, `pr-runtime-analysis`, `pr-mutants`) so they run as parallel cloud-workflow jobs/stages. A convenience umbrella `anvil-pr-slow` recipe is also provided for local -use; it invokes the three sub-recipes sequentially. `pr-mutants` (mutants) is +use; it invokes those groups sequentially. `pr-mutants` (mutants) is diff-scoped against the PR base; `scheduled-exhaustive` runs the full-workspace mutants recipe: @@ -136,8 +146,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 @@ -248,7 +259,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. @@ -327,8 +340,8 @@ install recipe can run: Trade-off acknowledged: `cargo install --locked` is slow on a cold cache (several minutes for the full catalog). It is also the most reliable mechanism in restricted networks. Caching (via the GH cache action and the ADO pipeline workspace cache) is -configured by the setup action/template to key on `Cargo.lock`, the toolchain -channel, and `versions.just`. See +configured by the setup action/template to key on platform, toolchain +configuration, Cargo configuration, and `versions.just`. See [github.md](./github.md#caching) and [ado.md](./ado.md#caching). #### 3.3.1 System-level prerequisites @@ -369,21 +382,73 @@ 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 rustup's standard + override for selecting an already available toolchain. +2. A root `rust-toolchain` or `rust-toolchain.toml`, delegated to rustup without + Anvil parsing or modifying the file. +3. The root package or `[workspace.package]` `rust-version`, treated as the + repository MSRV. + +There is no runner-default fallback. A repository with neither a root toolchain +file nor a root MSRV fails with an actionable setup error rather than inheriting a +compiler that can change with the machine image. + +Repositories with one compatibility floor should declare root +`[workspace.package].rust-version` (or package +`rust-version`) and have every workspace member inherit it or declare an equal +or lower minimum. Repositories with missing package MSRVs, or a member minimum +newer than the root, correct the root floor or add a root toolchain file to +select one compiler for catalog checks. Callers can instead set +`RUSTUP_TOOLCHAIN` to an already provisioned toolchain. + +A caller-provided `RUSTUP_TOOLCHAIN` and the presence of either root +toolchain-file spelling both produce no explicit `+toolchain` argument. The +environment value is inherited unchanged. In the file case, rustup owns all +parsing and applies its native lookup from each Cargo or Rust command's actual +working directory; Anvil does not parse the root file, replay options from it, +or promise isolation from nested toolchain files. Only the root MSRV fallback +produces an explicit `+` argument. + +Setup ensures that the selected stable compiler is available before stable +Cargo or Rust use. Repository toolchain files are processed by rustup; a +missing root-MSRV toolchain is installed explicitly. Prerequisite validation is +read-only: before running workspace metadata under the root MSRV, it requires +rustup and verifies that exact toolchain is already installed. A missing +toolchain fails with the stable-setup recipe as the corrective action instead +of allowing Cargo to auto-install it. + +When selection falls back to the root MSRV, Anvil reads the root manifest just +far enough to bootstrap that compiler, then prerequisite validation reads +`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. + +Cargo-installed tools and stable analyzers use this same repository-selected +compiler. Anvil does not provision a separate tooling compiler; checks that +require nightly continue to use their catalog-pinned nightly. + +When the root manifest declares an MSRV, `anvil-msrv-test` runs affected-package +`cargo test --all-targets` in all-features and default-features configurations +under that compiler. This includes library and binary unit tests, integration +tests, examples, and benches as test targets. It does not add a +`--no-default-features` pass. A root toolchain file does not suppress this +minimum-version run. Without a root MSRV the recipe is a no-op. + +`anvil-tool-rustc-validate-prereqs` verifies that `rustc` is available and +enforces the workspace MSRV compatibility rule. Per-check toolchain +requirements (for example, miri, careful, and udeps need nightly) remain +enforced by their matching prerequisite validation. ### 3.6 Nightly pinning @@ -549,7 +614,7 @@ dependency. This is exactly how the **scheduled** and **full** tiers stay full-w `ANVIL_IMPACT=off` before invoking the private `_anvil-` recipe, so the whole dependency tree runs unscoped. The export lives in the wrapper because a dependency-only tier recipe cannot set env for its own dependencies — they run before its -body. The four scheduled *groups* (`anvil-scheduled-test`, …) wrap the same way, so a +body. The scheduled *groups* (`anvil-scheduled-test`, …) wrap the same way, so a scheduled group invoked directly is full-workspace too. `ANVIL_IMPACT` is a strict tri-state — `off`, `consume`, or unset. Any other value makes diff --git a/crates/cargo-anvil/docs/implementation.md b/crates/cargo-anvil/docs/implementation.md index 005554372..cc2145400 100644 --- a/crates/cargo-anvil/docs/implementation.md +++ b/crates/cargo-anvil/docs/implementation.md @@ -44,6 +44,59 @@ The canonical recipe owns this mapping, baseline availability checks, and per-package execution. Focused contract tests cover every publication form, while generated copies and snapshots detect drift from the template. +## Stable toolchain selection and setup + +The stable selector has two canonical implementation sites because it must run +before Cargo can parse the root manifest. `versions.just` owns the lazy +per-command argument expression; `tools.just` owns provisioning, workspace-MSRV +validation, and the dedicated MSRV-test selection. Their small line-oriented +root-manifest scanners intentionally duplicate the same accepted syntax and +`workspace.package`-before-`package` precedence. Changes to one scanner must +update the other and their focused resolver tests in the same patch. + +Both implementations enforce the same ordinary-check decision table: + +1. a nonempty caller `RUSTUP_TOOLCHAIN` emits no explicit argument; +2. the presence of either root toolchain-file spelling emits no explicit + argument; +3. otherwise the root MSRV emits `+`; +4. absence of every source fails. + +The empty argument in the first two cases is load-bearing. It preserves +rustup's native environment and working-directory-sensitive toolchain-file +behavior, including profiles, components, targets, and path channels, without +Anvil parsing or replaying suppressed options. + +`_anvil-resolve-stable` owns the setup actions. The optional +`ANVIL_MSRV_TOOLCHAIN` maps only the dedicated MSRV run and is rejected as an +unpaired stable configuration when no ordinary stable selector exists. +Workspace-MSRV validation is read-only: it confirms rustup is present and the +exact root-MSRV toolchain is installed before invoking metadata, so Cargo +cannot auto-install a compiler during validation. Installation uses the same +anchored toolchain-list match and emits a dedicated rustup bootstrap diagnostic +when the executable is absent. + +Setup dependencies, rather than the cloud templates, route provisioning. +Cargo-tool installers, default-component installers, and stable-only setup +leaves depend on `anvil-toolchain-stable-install`; group and tier fan-out lets +Just deduplicate that prerequisite. GitHub and ADO setup restore Cargo home, +bootstrap Just, and invoke the selected catalog setup recipe. Neither backend +adds a duplicate standalone stable-provisioning step. + +Cloud Cargo-home keys include `.cargo/config.toml`, both repository +toolchain-file spellings, and `versions.just`. They deliberately exclude +`Cargo.toml`, `Cargo.lock`, and the resolved compiler version because cached +Cargo-installed tools do not depend on routine workspace dependency changes, +while toolchain files and catalog pins define the cache-pruning boundary. +Rustup toolchains live outside Cargo home. + +Canonical recipes and workflow fragments live under `templates/`. The artifact +registry embeds those files, renderer tests pin conditional substitutions such +as ADO's `pr-fast`-only title lookup, and snapshot tests pin complete emitted +backends. After a canonical change, run cargo-anvil against this repository to +refresh owned mirrors and `.anvil.lock`; regenerate crate README content from +`src/lib.rs`, never by editing `README.md`. + ## Impact scoping and tier routing Impact scoping lets a clean PR run each check against only the cargo packages its committed diff --git a/crates/cargo-anvil/src/anvil/artifacts/ado.rs b/crates/cargo-anvil/src/anvil/artifacts/ado.rs index 596888f65..7d9b7247d 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", @@ -61,22 +62,36 @@ const GROUPS: &[&str] = &[ /// the group name at emit time. const GROUP_STEP_TEMPLATE: &str = include_str!("../../../templates/ado/steps/group.yml"); +/// Optional ADO title lookup inserted only into the `pr-fast` group. +const PR_TITLE_STEP_TEMPLATE: &str = include_str!("../../../templates/ado/steps/pr-title-step.yml"); + +/// Optional `PR_TITLE` environment entry inserted only into `pr-fast`. +const PR_TITLE_ENV_TEMPLATE: &str = include_str!("../../../templates/ado/steps/pr-title-env.yml"); + /// Placeholder token the per-group template uses for the group name. const GROUP_PLACEHOLDER: &str = "__GROUP__"; /// Placeholder token the per-group template uses for the impact-mode selection. const IMPACT_MODE_PLACEHOLDER: &str = "__IMPACT_MODE__"; +/// Placeholder token for the optional PR-title lookup step. +const PR_TITLE_STEP_PLACEHOLDER: &str = "__PR_TITLE_STEP__"; + +/// Placeholder token for the optional `PR_TITLE` environment entry. +const PR_TITLE_ENV_PLACEHOLDER: &str = "__PR_TITLE_ENV__"; + /// Render the step template for one group. #[must_use] fn render_group_step(group: &str) -> String { - // Substitute the group name first, then replace the single impact-mode - // token in place -- reusing the already-allocated buffer instead of - // allocating a second full-template String for the second substitution. let mut rendered = GROUP_STEP_TEMPLATE.replace(GROUP_PLACEHOLDER, group); - if let Some(pos) = rendered.find(IMPACT_MODE_PLACEHOLDER) { - rendered.replace_range(pos..pos + IMPACT_MODE_PLACEHOLDER.len(), impact_mode(group)); - } + rendered = rendered.replace(IMPACT_MODE_PLACEHOLDER, impact_mode(group)); + let (title_step, title_env) = if group == "pr-fast" { + (PR_TITLE_STEP_TEMPLATE.trim_end(), PR_TITLE_ENV_TEMPLATE.trim_end()) + } else { + ("", "") + }; + rendered = rendered.replace(PR_TITLE_STEP_PLACEHOLDER, title_step); + rendered = rendered.replace(PR_TITLE_ENV_PLACEHOLDER, title_env); rendered } @@ -166,6 +181,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 +228,29 @@ mod tests { fn setup_step_takes_group_parameter_and_dispatches() { assert!(SETUP_STEP.contains("name: group")); assert!(SETUP_STEP.contains("just anvil-setup")); + assert!(!SETUP_STEP.contains("just anvil-toolchain-stable-install")); + assert!(!SETUP_STEP.contains("_anvil-resolve-stable")); assert!(SETUP_STEP.contains("just anvil-${{ parameters.group }}-setup")); assert!(SETUP_STEP.contains("eq(parameters.group, 'none')")); + assert!(SETUP_STEP.contains("anvil_toolchain_fingerprint")); + assert!(!SETUP_STEP.contains("Cargo.toml | Cargo.lock")); + assert!(SETUP_STEP.contains("'rust-toolchain.toml'")); + assert!(!SETUP_STEP.contains("restoreKeys:")); + let fingerprint = SETUP_STEP + .find("anvil setup (fingerprint repository toolchain)") + .expect("setup must fingerprint optional repository toolchain files"); + let cache_restore = SETUP_STEP + .find("anvil setup (cache cargo home)") + .expect("setup must restore Cargo home"); + let just_bootstrap = SETUP_STEP.find("anvil setup (install just)").expect("setup must bootstrap Just"); + assert!( + fingerprint < cache_restore, + "optional toolchain files must be fingerprinted before cache restore" + ); + assert!( + cache_restore < just_bootstrap, + "Cargo home must be restored before Just is bootstrapped" + ); } #[test] @@ -281,7 +318,7 @@ mod tests { } #[test] - fn render_group_step_shares_impact_via_cache_not_env() { + fn render_pr_fast_group_shares_impact_and_resolves_title() { let body = render_group_step("pr-fast"); assert!(body.contains("just anvil-pr-fast")); // The impact set is shared as a downloaded artifact, so the group step @@ -302,13 +339,22 @@ mod tests { !body.contains("[ -f target/anvil/impact/impact.state ]"), "PR group must not gate its mode on a runtime marker-file probe" ); - // PR_TITLE is resolved from the REST API (ADO has no PR-title - // predefined variable) and threaded via the PR_TITLE pipeline var. + // Only pr-fast needs the PR title, which ADO does not expose as a + // predefined variable. assert!(body.contains("PR_TITLE: $(PR_TITLE)")); assert!(body.contains("setvariable variable=PR_TITLE")); + assert!(body.contains("Invoke-RestMethod")); assert!(!body.contains("PR_TITLE: $(System.PullRequest.Title)")); } + #[test] + fn render_pr_msrv_group_has_no_title_api_or_token_dependency() { + let body = render_group_step("pr-msrv"); + for needle in ["PR_TITLE", "Invoke-RestMethod", "SYSTEM_ACCESSTOKEN", "System.AccessToken"] { + assert!(!body.contains(needle), "pr-msrv must not contain '{needle}'"); + } + } + #[test] fn scheduled_group_step_forces_impact_off_unconditionally() { // The scheduled tier always validates the full workspace. Its impact @@ -381,8 +427,8 @@ mod tests { // Every pr-* stage depends on the single impact stage. assert_eq!( PR_STAGES.matches("dependsOn: [impact]").count(), - 4, - "each of the four pr-* stages must depend on the single impact stage" + 5, + "each of the five pr-* stages must depend on the single impact stage" ); } diff --git a/crates/cargo-anvil/src/anvil/artifacts/github.rs b/crates/cargo-anvil/src/anvil/artifacts/github.rs index e4977457d..6eaa33e45 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,24 @@ mod tests { fn setup_action_takes_group_input_and_dispatches() { assert!(SETUP_ACTION.contains("group:")); assert!(SETUP_ACTION.contains("just anvil-setup binstall")); + assert!(!SETUP_ACTION.contains("_anvil-resolve-stable")); + assert!(!SETUP_ACTION.contains("just anvil-toolchain-stable-install")); + assert!(!SETUP_ACTION.contains("rustc-version")); + assert!(!SETUP_ACTION.contains("hashFiles('Cargo.toml'")); + assert!(!SETUP_ACTION.contains("'Cargo.lock'")); + assert!(SETUP_ACTION.contains("'rust-toolchain.toml'")); + let cache_restore = SETUP_ACTION + .find("name: Restore cargo cache") + .expect("setup must restore Cargo home"); + let just_bootstrap = SETUP_ACTION.find("name: Install just").expect("setup must bootstrap Just"); + let catalog_setup = SETUP_ACTION + .find("name: Install anvil toolchains + tools") + .expect("setup must dispatch catalog setup"); + assert!( + cache_restore < just_bootstrap, + "Cargo home must be restored before Just is bootstrapped" + ); + assert!(just_bootstrap < catalog_setup, "Just must be bootstrapped before catalog setup"); assert!(SETUP_ACTION.contains("ANVIL_GROUP: ${{ inputs.group }}")); assert!(SETUP_ACTION.contains("just \"anvil-$ANVIL_GROUP-setup\" binstall")); assert!(SETUP_ACTION.contains(r"^[a-z0-9-]+$")); @@ -374,6 +392,7 @@ export -f just "impact-windows:", "pr-fast:", "pr-test:", + "pr-msrv:", "pr-runtime-analysis:", "pr-mutants:", ] { @@ -386,6 +405,10 @@ export -f just ); } assert!(PR_IMPL_WORKFLOW.contains("needs: [impact-linux, impact-windows]")); + // The matrix is intentionally always present; the generated MSRV + // recipe owns the successful no-MSRV no-op. + assert!(!PR_IMPL_WORKFLOW.contains("msrv_test_required")); + assert!(PR_IMPL_WORKFLOW.contains("Check Group: MSRV Tests (${{ matrix.os }})")); assert!(PR_IMPL_WORKFLOW.contains("os: [linux, windows, linux-arm, windows-arm]")); assert!(!PR_IMPL_WORKFLOW.contains("fromJSON")); assert!(PR_IMPL_WORKFLOW.contains("PR_TITLE")); @@ -419,7 +442,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 +459,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 e6a23e068..7af410639 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/justfile.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/justfile.rs @@ -115,6 +115,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", @@ -131,6 +132,7 @@ const GROUP_FILES: &[(&str, &str)] = split_recipe_files!( "groups", [ "pr-fast", + "pr-msrv", "pr-slow", "pr-test", "pr-runtime-analysis", @@ -273,6 +275,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), @@ -293,6 +296,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")); } @@ -502,7 +506,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" ); } @@ -521,19 +525,21 @@ mod tests { "_anvil-base-ref", "git rev-parse --verify \"$base^{commit}\"", "git cat-file -e $baselineManifest", - "cargo semver-checks --package $p --baseline-rev $base", + "$stableArgs = {{_anvil_stable_toolchain_args}}", + "cargo @stableArgs semver-checks --package $p --baseline-rev $base", ] { assert!(body.contains(needle), "semver check template missing '{needle}'"); } } #[test] - fn groups_just_template_includes_all_groups_and_pr_slow_sub_recipes() { + fn groups_just_template_includes_all_groups_and_pr_slow_groups() { let groups = all_group_bodies(); for needle in [ "anvil-pr-fast:", "anvil-pr-slow:", "anvil-pr-test:", + "anvil-pr-msrv:", "anvil-pr-runtime-analysis:", "anvil-pr-mutants:", "anvil-scheduled-test:", @@ -545,12 +551,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", ] { @@ -587,12 +596,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)" @@ -696,6 +706,110 @@ 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=\"install\":")); + assert!(!TOOLS_JUST.contains("_anvil-with-stable")); + assert!(TOOLS_JUST.contains("& cargo {{_anvil_stable_toolchain_args}}")); + assert!(!VERSIONS_JUST.contains("rustup show active-toolchain")); + assert!(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" + ); + } + 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" + ); + assert!( + TOOLS_JUST.contains("$env:RUSTUP_AUTO_INSTALL = '0'"), + "Cargo-tool validation must disable rustup auto-installation" + ); + + 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 { + let caches_stable_args = body.contains("$stableArgs = {{_anvil_stable_toolchain_args}}"); + for line in body.lines().map(str::trim) { + let invokes_cargo = line.starts_with("cargo ") + || line.starts_with("& cargo ") + || 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}}") + || (caches_stable_args && toolchain.starts_with("@stableArgs")), + "{path} invokes Cargo without an explicit toolchain selection: {line}" + ); + } + } + } + } + #[test] fn mod_just_imports_siblings_and_defines_alias() { for needle in [ @@ -755,4 +869,724 @@ mod tests { let body = JUSTFILE_IMPORTS_BODY.trim(); assert_eq!(body, "import 'justfiles/anvil/mod.just'"); } + + #[cfg(not(miri))] + mod stable_toolchain_resolver_tests { + #[cfg(unix)] + use std::os::unix::fs::PermissionsExt; + use std::path::{Path, PathBuf}; + use std::process::{Command, Output}; + use std::{env, fs}; + + use tempfile::{Builder, TempDir}; + + use super::{TOOLS_JUST, VERSIONS_JUST}; + + #[derive(Clone, Copy)] + enum ResolverOperation { + StableArgs, + InstallIfMissing, + ValidateWorkspaceMsrv, + MsrvToolchain, + InstallMsrvIfNeeded, + } + + fn fixture(cargo_toml: &str) -> TempDir { + let temp = Builder::new() + .prefix("anvil repo's [copy] (fork) ") + .tempdir() + .expect("temporary repository must be creatable"); + fs::write(temp.path().join("Cargo.toml"), cargo_toml).expect("manifest fixture must be writable"); + fs::write(temp.path().join("versions.just"), VERSIONS_JUST).expect("versions fixture must be writable"); + fs::write(temp.path().join("tools.just"), TOOLS_JUST).expect("tools fixture must be writable"); + 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 tool_path(name: &str) -> Option { + let executable = format!("{name}{}", env::consts::EXE_SUFFIX); + env::split_paths(&env::var_os("PATH").unwrap_or_default()) + .map(|directory| directory.join(&executable)) + .find(|candidate| candidate.is_file()) + } + + fn command(root: &Path) -> Command { + let just = tool_path("just").expect("resolver tests call command only after tools_available confirms Just is on PATH"); + let mut command = Command::new(just); + command + .args(["--justfile"]) + .arg(root.join("Justfile")) + .current_dir(root) + .env_remove("ANVIL_MSRV_TOOLCHAIN") + .env_remove("RUSTUP_TOOLCHAIN"); + command + } + + fn command_for_operation(root: &Path, operation: ResolverOperation) -> Command { + let mut command = command(root); + match operation { + ResolverOperation::StableArgs => { + command.arg("_anvil-test-stable-args"); + } + ResolverOperation::InstallIfMissing => { + command.arg("anvil-toolchain-stable-install"); + } + ResolverOperation::ValidateWorkspaceMsrv => { + command.args(["_anvil-resolve-stable", "validate-workspace-msrv"]); + } + ResolverOperation::MsrvToolchain => { + command.args(["_anvil-resolve-stable", "msrv"]); + } + ResolverOperation::InstallMsrvIfNeeded => { + command.args(["_anvil-resolve-stable", "install-msrv"]); + } + } + command + } + + fn run(root: &Path, operation: ResolverOperation, rustup_toolchain: Option<&str>) -> Output { + let mut command = command_for_operation(root, operation); + match rustup_toolchain { + Some(value) => { + command.env("RUSTUP_TOOLCHAIN", value); + } + 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, ResolverOperation::StableArgs, 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(" ") + } + + fn path_without_rustup() -> std::ffi::OsString { + let path = env::var_os("PATH").unwrap_or_default(); + let paths = env::split_paths(&path).filter(|directory| { + !["rustup", "rustup.exe", "rustup.cmd", "rustup.ps1"] + .iter() + .any(|name| directory.join(name).is_file()) + }); + env::join_paths(paths).expect("PATH without rustup must be valid") + } + + #[test] + fn honors_environment_files_and_msrv_in_precedence_order() { + if !tools_available() { + 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")), "@()"); + assert_eq!(resolved(root, Some("team's-toolchain")), "@()"); + + fs::write(root.join("rust-toolchain.toml"), "[toolchain]\nchannel = \"1.94\"\n").expect("toolchain fixture must be writable"); + assert_eq!(resolved(root, None), "@()"); + } + + #[test] + 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"); + } + #[cfg(unix)] + { + let cargo = shim.path().join("cargo"); + fs::write(&cargo, "#!/bin/sh\nprintf '%s\\n' \"$*\"\nexit 0\n").expect("Cargo shim must be writable"); + fs::set_permissions(&cargo, fs::Permissions::from_mode(0o755)).expect("Cargo shim must be executable"); + } + let mut paths = vec![shim.path().to_path_buf()]; + paths.extend(env::split_paths(&env::var_os("PATH").unwrap_or_default())); + let paths = env::join_paths(paths).expect("shim PATH must be valid"); + + let output = command(temp.path()) + .arg("_anvil-test-stable-command") + .env("RUSTUP_TOOLCHAIN", "team's-toolchain") + .env("PATH", &paths) + .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(), + "--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", &paths) + .output() + .expect("repository-selected Cargo command must run"); + assert!( + output.status.success(), + "repository-selected Cargo command failed: {}", + normalized_diagnostic(&output) + ); + assert_eq!( + String::from_utf8(output.stdout).expect("Cargo shim output must be UTF-8").trim(), + "--version" + ); + + 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", &paths) + .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", paths) + .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 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\nexit /b 0\r\n", + ) + .expect("rustup shim must be writable"); + } + #[cfg(unix)] + { + for (name, log_variable, status) in [("cargo", "ANVIL_CARGO_LOG", 1), ("rustup", "ANVIL_RUSTUP_LOG", 0)] { + let executable = shim.path().join(name); + fs::write( + &executable, + format!("#!/bin/sh\nprintf '%s\\n' \"$*\" >> \"${log_variable}\"\nexit {status}\n"), + ) + .expect("component shim must be writable"); + fs::set_permissions(&executable, fs::Permissions::from_mode(0o755)).expect("component shim must be executable"); + } + } + let mut paths = vec![shim.path().to_path_buf()]; + paths.extend(env::split_paths(&env::var_os("PATH").unwrap_or_default())); + let paths = env::join_paths(paths).expect("shim PATH must be valid"); + let run_install = |rustup_toolchain: Option<&str>| { + let mut command = command(temp.path()); + command + .args(["_install-component", "default", "clippy"]) + .env("ANVIL_CARGO_LOG", &cargo_log) + .env("ANVIL_RUSTUP_LOG", &rustup_log) + .env("PATH", &paths); + 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(), + "clippy --version" + ); + assert_eq!( + fs::read_to_string(&rustup_log).expect("rustup shim log must be readable").trim(), + "component add 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(), + "clippy --version" + ); + assert_eq!( + fs::read_to_string(&rustup_log).expect("rustup shim log must be readable").trim(), + "component add clippy" + ); + + fs::remove_file(temp.path().join("rust-toolchain.toml")).expect("toolchain fixture must be removable"); + fs::write(&cargo_log, "").expect("Cargo shim log must be resettable"); + fs::write(&rustup_log, "").expect("rustup shim log must be resettable"); + let output = run_install(None); + assert!( + output.status.success(), + "MSRV-selected component installation failed: {}", + normalized_diagnostic(&output) + ); + assert_eq!( + fs::read_to_string(&cargo_log).expect("Cargo shim log must be readable").trim(), + "+1.93 clippy --version" + ); + assert_eq!( + fs::read_to_string(&rustup_log).expect("rustup shim log must be readable").trim(), + "component add --toolchain 1.93 clippy" + ); + } + + #[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(), ResolverOperation::StableArgs, 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"); + fs::write( + shim.path().join("rustup.ps1"), + concat!( + "if (($args -contains 'toolchain') -and ($args -contains 'list')) {\n", + " $env:ANVIL_TEST_INSTALLED_TOOLCHAINS -split ';' | Where-Object { $_ }\n", + "}\n", + "exit 0\n", + ), + ) + .expect("rustup shim must be writable"); + let mut paths = vec![shim.path().to_path_buf()]; + paths.extend(env::split_paths(&env::var_os("PATH").unwrap_or_default())); + let paths = env::join_paths(paths).expect("shim PATH must be valid"); + let run_validation = |b_msrv: &str, rustup_toolchain: Option<&str>, installed: &str| { + let mut command = command(workspace.path()); + command + .args(["_anvil-resolve-stable", "validate-workspace-msrv"]) + .env("ANVIL_TEST_B_MSRV", b_msrv) + .env("ANVIL_TEST_INSTALLED_TOOLCHAINS", installed) + .env("ANVIL_TEST_LOG", &log) + .env("PATH", &paths); + if let Some(value) = rustup_toolchain { + command.env("RUSTUP_TOOLCHAIN", value); + } + command.output().expect("workspace MSRV validation must run") + }; + + let output = run_validation("1.92", None, "stable-x86_64-pc-windows-msvc"); + assert!(!output.status.success(), "validation must not auto-install a missing root MSRV"); + let diagnostic = normalized_diagnostic(&output); + assert!( + diagnostic.contains("root MSRV toolchain '1.92' is not installed") && diagnostic.contains("anvil-toolchain-stable-install"), + "unexpected missing-toolchain diagnostic: {diagnostic}" + ); + assert!( + fs::read_to_string(&log).unwrap_or_default().is_empty(), + "Cargo metadata must not run before the root MSRV is known to be installed" + ); + + let output = run_validation("1.93", None, "1.92-x86_64-pc-windows-msvc"); + assert!(!output.status.success()); + let diagnostic = normalized_diagnostic(&output); + assert!( + 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.0", None, "1.92-x86_64-pc-windows-msvc"); + 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 diagnoses_missing_rustup_before_installing_a_toolchain() { + if !tools_available() { + return; + } + let temp = fixture("[workspace.package]\nrust-version = \"1.93\"\n"); + let output = command_for_operation(temp.path(), ResolverOperation::InstallIfMissing) + .env("PATH", path_without_rustup()) + .output() + .expect("stable setup must run far enough to diagnose missing rustup"); + assert!(!output.status.success(), "stable setup without rustup must fail"); + let diagnostic = normalized_diagnostic(&output); + assert!( + diagnostic.contains("rustup not found") + && diagnostic.contains("https://rustup.rs") + && diagnostic.contains("ensure it is on PATH"), + "unexpected missing-rustup diagnostic: {diagnostic}" + ); + + let output = command_for_operation(temp.path(), ResolverOperation::InstallIfMissing) + .env("RUSTUP_TOOLCHAIN", "selected-stable") + .env("PATH", path_without_rustup()) + .output() + .expect("environment-selected setup must diagnose missing rustup"); + assert!(!output.status.success(), "environment selection without rustup must fail"); + assert!( + normalized_diagnostic(&output).contains("rustup not found"), + "environment selection must require rustup" + ); + + fs::write(temp.path().join("rust-toolchain.toml"), "[toolchain]\nchannel = \"1.93\"\n") + .expect("toolchain fixture must be writable"); + let output = command_for_operation(temp.path(), ResolverOperation::InstallIfMissing) + .env("PATH", path_without_rustup()) + .output() + .expect("file-selected setup must diagnose missing rustup"); + assert!(!output.status.success(), "toolchain-file selection without rustup must fail"); + assert!( + normalized_diagnostic(&output).contains("rustup not found"), + "toolchain-file selection must require rustup" + ); + } + + #[test] + 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"); + let installed_output = installed + .lines() + .map(|line| format!("Write-Output '{line}'")) + .collect::>() + .join("\n"); + fs::write( + shim.path().join("rustup.ps1"), + format!( + "Add-Content -LiteralPath $env:ANVIL_RUSTUP_LOG -Value ($args -join ' ')\n\ + if (($args -contains 'toolchain') -and ($args -contains 'list')) {{ {installed_output} }}\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, + "nightly-2026-01-01-x86_64-pc-windows-msvc\n1.93-x86_64-pc-windows-msvc", + 0, + ); + assert!( + output.status.success(), + "installed public MSRV provisioning failed: {}", + 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(), + None, + "nightly-2026-01-01-x86_64-pc-windows-msvc\n1.93.0-x86_64-pc-windows-msvc", + 0, + ); + assert!( + output.status.success(), + "distinct patch-qualified installation must not prevent provisioning the selected MSRV: {}", + normalized_diagnostic(&output) + ); + assert!( + rustup_calls.contains("toolchain install 1.93 --profile minimal"), + "the exact 1.93 selector must be installed when only 1.93.0 exists" + ); + assert!(cargo_calls.is_empty(), "public MSRV availability is checked through rustup"); + + 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(), ResolverOperation::InstallMsrvIfNeeded, None); + assert!( + output.status.success(), + "a repository without a declared MSRV must not provision an MSRV" + ); + let output = run(no_msrv.path(), ResolverOperation::MsrvToolchain, None); + assert!( + output.status.success(), + "a repository without a declared MSRV must skip the MSRV test" + ); + 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 output = run(root, ResolverOperation::ValidateWorkspaceMsrv, Some("explicit-toolchain")); + assert!( + output.status.success(), + "an explicit stable override must bypass root-MSRV workspace validation" + ); + let resolve_msrv = |root: &Path| { + let output = run(root, ResolverOperation::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"); + } + } } diff --git a/crates/cargo-anvil/src/anvil/artifacts/mod.rs b/crates/cargo-anvil/src/anvil/artifacts/mod.rs index 5d1773bcf..e7f1e74c6 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, ComposedHost}; #[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") @@ -139,6 +139,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 285dfcd66..31fd78811 100644 --- a/crates/cargo-anvil/src/lib.rs +++ b/crates/cargo-anvil/src/lib.rs @@ -78,6 +78,14 @@ //! `catalog:` checksum — a `sha256` over the whole compiled-in catalog — so //! two builds at the same version but different catalogs are distinguishable. //! +//! Before running ordinary checks, provide a deterministic Rust selection: +//! a caller-set `RUSTUP_TOOLCHAIN`, either root `rust-toolchain` file spelling, +//! or a root `[workspace.package].rust-version` / `[package].rust-version`. +//! Anvil passes no explicit selector for the environment or file cases so +//! rustup handles them natively; only the root MSRV becomes `+`. +//! Repositories with none of these sources fail instead of inheriting the +//! runner's ambient compiler. +//! //! ## Daily driver //! //! After the first run, your daily workflow is plain `just`: @@ -267,11 +275,11 @@ //! check to its tool's documentation, and note anything anvil-specific. //! //! **PR tier** (`anvil-pr`) — runs on every pull request, impact-scoped -//! both locally and in cloud workflows. 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 +//! `pr-slow` groups run as independent parallel jobs per OS leg: //! //!
JobSub-groupCheckNotes
UmbrellaGroupCheckNotes
pr-fastfmtpredefined configuration with nightly features
clippypredefined lints
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, all-target tests under the declared MSRV
pr-runtime-analysismirilibtest, not nextest
carefulself-cleans on a toolchain bump
loomopt-in targets only
-//! +//! //! //! //! @@ -288,9 +296,10 @@ //! //! //! -//! +//! //! //! +//! //! //! //! diff --git a/crates/cargo-anvil/src/run.rs b/crates/cargo-anvil/src/run.rs index 0f1dcc6e9..0637b9e9f 100644 --- a/crates/cargo-anvil/src/run.rs +++ b/crates/cargo-anvil/src/run.rs @@ -1622,6 +1622,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 c1f7d613e..37600d142 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/group.yml b/crates/cargo-anvil/templates/ado/steps/group.yml index 173e18946..335afcfc6 100644 --- a/crates/cargo-anvil/templates/ado/steps/group.yml +++ b/crates/cargo-anvil/templates/ado/steps/group.yml @@ -16,33 +16,7 @@ steps: - template: setup.yml parameters: group: __GROUP__ - # ADO has no PR-title predefined variable: `System.PullRequest.Title` - # does NOT exist, so `$(System.PullRequest.Title)` would expand to the - # literal unexpanded macro text (non-empty) and make anvil-pr-title - # validate that string. Resolve the real title from the REST API on PR - # builds and publish it as the PR_TITLE pipeline variable. Only - # anvil-pr-title (in pr-fast) consults it; other groups ignore it, but we - # resolve uniformly to keep group.yml the same across groups. Non-PR builds - # skip; a known PR whose metadata cannot be retrieved fails closed. - - pwsh: | - $ErrorActionPreference = 'Stop' - $prId = $env:SYSTEM_PULLREQUEST_PULLREQUESTID - if (-not $prId) { - Write-Host 'anvil: not a PR build; leaving PR_TITLE empty' - Write-Host '##vso[task.setvariable variable=PR_TITLE]' - exit 0 - } - $uri = "$($env:SYSTEM_COLLECTIONURI)$($env:SYSTEM_TEAMPROJECTID)/_apis/git/repositories/$($env:BUILD_REPOSITORY_ID)/pullRequests/${prId}?api-version=7.0" - try { - $resp = Invoke-RestMethod -Uri $uri -Headers @{ Authorization = "Bearer $($env:SYSTEM_ACCESSTOKEN)" } - Write-Host "##vso[task.setvariable variable=PR_TITLE]$($resp.title)" - } catch { - Write-Error "anvil: could not resolve PR title for PR $prId ($_). Ensure the job can access the repository OAuth token." - exit 1 - } - displayName: anvil-__GROUP__ (resolve PR title) - env: - SYSTEM_ACCESSTOKEN: $(System.AccessToken) +__PR_TITLE_STEP__ - bash: | set -euo pipefail # The impact mode is fixed by group class at emit time -- never probed from a @@ -54,8 +28,7 @@ steps: just anvil-__GROUP__ displayName: anvil-__GROUP__ env: - # Resolved by the preceding step; empty only outside PR builds. - PR_TITLE: $(PR_TITLE) +__PR_TITLE_ENV__ # Disable cargo incremental compilation in CI: the incremental dir is # not part of the anvil-setup cache (see setup.yml), so generating it is # pure cost with no cross-run benefit. Mirrors the GitHub impl workflows' diff --git a/crates/cargo-anvil/templates/ado/steps/pr-title-env.yml b/crates/cargo-anvil/templates/ado/steps/pr-title-env.yml new file mode 100644 index 000000000..007650084 --- /dev/null +++ b/crates/cargo-anvil/templates/ado/steps/pr-title-env.yml @@ -0,0 +1,2 @@ + # Resolved by the preceding pr-fast step; empty only outside PR builds. + PR_TITLE: $(PR_TITLE) diff --git a/crates/cargo-anvil/templates/ado/steps/pr-title-step.yml b/crates/cargo-anvil/templates/ado/steps/pr-title-step.yml new file mode 100644 index 000000000..e66e4c2f1 --- /dev/null +++ b/crates/cargo-anvil/templates/ado/steps/pr-title-step.yml @@ -0,0 +1,24 @@ + # ADO has no PR-title predefined variable: `System.PullRequest.Title` + # does NOT exist, so `$(System.PullRequest.Title)` would expand to literal + # macro text. Resolve the title only for pr-fast, which owns anvil-pr-title. + # Non-PR builds skip; a known PR whose metadata cannot be retrieved fails + # closed. + - pwsh: | + $ErrorActionPreference = 'Stop' + $prId = $env:SYSTEM_PULLREQUEST_PULLREQUESTID + if (-not $prId) { + Write-Host 'anvil: not a PR build; leaving PR_TITLE empty' + Write-Host '##vso[task.setvariable variable=PR_TITLE]' + exit 0 + } + $uri = "$($env:SYSTEM_COLLECTIONURI)$($env:SYSTEM_TEAMPROJECTID)/_apis/git/repositories/$($env:BUILD_REPOSITORY_ID)/pullRequests/${prId}?api-version=7.0" + try { + $resp = Invoke-RestMethod -Uri $uri -Headers @{ Authorization = "Bearer $env:SYSTEM_ACCESSTOKEN" } + Write-Host "##vso[task.setvariable variable=PR_TITLE]$($resp.title)" + } catch { + Write-Error "anvil: could not resolve PR title for PR $prId ($_). Ensure the job can access the repository OAuth token." + exit 1 + } + displayName: anvil-pr-fast (resolve PR title) + env: + SYSTEM_ACCESSTOKEN: $(System.AccessToken) diff --git a/crates/cargo-anvil/templates/ado/steps/setup.yml b/crates/cargo-anvil/templates/ado/steps/setup.yml index 3e21c60d7..89d2aa908 100644 --- a/crates/cargo-anvil/templates/ado/steps/setup.yml +++ b/crates/cargo-anvil/templates/ado/steps/setup.yml @@ -8,9 +8,9 @@ # group: which anvil group to install setup for. Special values: # - "" (default): install the full catalog via # `just anvil-setup` -- use for "give me everything" flows. -# - "none": skip tool installation entirely; just bootstrap the -# rust cache + libclang + just. Used by impact.yml, which installs -# only cargo-delta afterwards. +# - "none": skip tool installation entirely; just restore the +# Cargo cache and bootstrap libclang + just. Used by impact.yml, +# which installs only cargo-delta afterwards. # - anything else (e.g. "pr-fast"): install only that group's # prerequisites via `just anvil--setup`. parameters: @@ -18,18 +18,30 @@ 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) + # Toolchain changes deliberately start a fresh Cargo-home cache. Besides + # bounding registry growth, this gives repositories a predictable cache + # reset point without coupling cache identity to routine manifest edits. + # Compute the digest explicitly because either supported toolchain file is + # optional and Cache@2 rejects missing file-path key segments. + - pwsh: | + $parts = @( + foreach ($name in @('rust-toolchain', 'rust-toolchain.toml')) { + if (Test-Path -LiteralPath $name -PathType Leaf) { + "$name=$((Get-FileHash -LiteralPath $name -Algorithm SHA256).Hash.ToLowerInvariant())" + } + } + ) + $fingerprint = if ($parts.Count -eq 0) { 'none' } else { $parts -join '-' } + Write-Host "##vso[task.setvariable variable=anvil_toolchain_fingerprint]$fingerprint" + displayName: anvil setup (fingerprint repository toolchain) + + # Restore Cargo's registry and installed binaries before bootstrapping Just, + # so warm ADO jobs do not compile Just again before reaching the cache. + # Rustup toolchains are not stored under Cargo home, so the selected compiler + # version does not affect this cache's contents. - task: Cache@2 inputs: - # 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' - restoreKeys: | - anvil-v1 | "$(Agent.OS)" | "$(Agent.OSArchitecture)" | rust$(anvil_rustc_version) - anvil-v1 | "$(Agent.OS)" | "$(Agent.OSArchitecture)" + key: 'anvil-v3 | "$(Agent.OS)" | "$(Agent.OSArchitecture)" | "$(anvil_toolchain_fingerprint)" | .cargo/config.toml | justfiles/anvil/versions.just' path: | $(HOME)/.cargo/registry/cache/ $(HOME)/.cargo/registry/index/ diff --git a/crates/cargo-anvil/templates/anvil/container/Dockerfile.tools.region b/crates/cargo-anvil/templates/anvil/container/Dockerfile.tools.region index dd3e315c6..537c0c510 100644 --- a/crates/cargo-anvil/templates/anvil/container/Dockerfile.tools.region +++ b/crates/cargo-anvil/templates/anvil/container/Dockerfile.tools.region @@ -39,4 +39,3 @@ RUN curl -fsSLo /tmp/cargo-binstall.tgz \ && tar -xzf /tmp/cargo-binstall.tgz -C "${CARGO_HOME}/bin" cargo-binstall \ && chmod 755 "${CARGO_HOME}/bin/cargo-binstall" \ && rm /tmp/cargo-binstall.tgz - diff --git a/crates/cargo-anvil/templates/github/pr-impl-workflow.yml b/crates/cargo-anvil/templates/github/pr-impl-workflow.yml index 116a15ceb..911c0f29d 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 f1557210d..0c5c0077a 100644 --- a/crates/cargo-anvil/templates/github/setup-action.yml +++ b/crates/cargo-anvil/templates/github/setup-action.yml @@ -13,8 +13,8 @@ inputs: - "" (default): install the full catalog via `just anvil-setup` -- use for local "give me everything" flows. - - "none": skip tool installation entirely; just bootstrap the - rust toolchain + just + binstall + cache. Used by + - "none": skip tool installation entirely; just restore the + Cargo cache and bootstrap just + binstall. Used by anvil-impact, which installs only cargo-delta afterwards. - a name containing only lowercase letters, digits, and hyphens: install only that group's prerequisites via @@ -77,48 +77,29 @@ runs: } "Free after cleanup (GiB): $(Get-FreeDiskGiB)" - - id: rustc-version - shell: bash - run: echo "version=$(rustc --version | awk '{print $2}')" >> "$GITHUB_OUTPUT" + # Restore before bootstrapping Just so a warm job can reuse the cached + # executable. Repository toolchain files intentionally define a fresh + # cache generation, bounding registry growth without invalidating on + # routine Cargo.toml or Cargo.lock edits. - name: Restore cargo cache id: cargo-cache uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: - # Key on OS + arch + rustc version + lockfile hashes + catalog - # hash so a Rust toolchain bump, a cross-arch matrix leg, or a - # tool-versions update all invalidate the cache cleanly. - # runner.arch resolves to X64 / ARM64 / X86, which keeps the - # x86_64 and aarch64 legs of the same OS from colliding on - # arch-incompatible cached binaries in ~/.cargo/bin. - # - # ${{ github.job }} discriminates by workflow-job-id (`pr-fast`, - # `pr-test`, `pr-slow`, etc.) so concurrent matrix legs that - # share OS+arch (e.g. pr-fast linux + pr-test linux) don't race - # on the same cache key. Without this, the first-to-finish job - # reserves the key, the others get "Unable to reserve cache: - # another job may be creating this cache" and silently skip - # the save -- leaving the cache empty forever. The restore-keys - # fall back across jobs so the install work is still shared - # across sibling legs on subsequent runs. - key: anvil-v1-${{ runner.os }}-${{ runner.arch }}-rust${{ steps.rustc-version.outputs.version }}-${{ hashFiles('Cargo.lock', '.cargo/config.toml', 'rust-toolchain.toml', 'justfiles/anvil/versions.just') }}-${{ github.job }} + # The job suffix prevents concurrent matrix jobs from racing to save + # one key. The prefix shares a completed cache across sibling jobs. + # There is deliberately no fallback across toolchain/catalog changes: + # those inputs are the cache-pruning boundary. + key: anvil-v2-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('.cargo/config.toml', 'rust-toolchain', 'rust-toolchain.toml', 'justfiles/anvil/versions.just') }}-${{ github.job }} restore-keys: | - anvil-v1-${{ runner.os }}-${{ runner.arch }}-rust${{ steps.rustc-version.outputs.version }}-${{ hashFiles('Cargo.lock', '.cargo/config.toml', 'rust-toolchain.toml', 'justfiles/anvil/versions.just') }}- - anvil-v1-${{ runner.os }}-${{ runner.arch }}-rust${{ steps.rustc-version.outputs.version }}- - anvil-v1-${{ runner.os }}-${{ runner.arch }}- + anvil-v2-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('.cargo/config.toml', 'rust-toolchain', 'rust-toolchain.toml', 'justfiles/anvil/versions.just') }}- # `.crates.toml` and `.crates2.json` track which cargo-installed # tools and versions live in ~/.cargo/bin/. Without them in the # cache, `cargo install --list` and (downstream) the # tool-install recipes' early-skip on "installed >= pin" don't - # see the cached binaries — every run then tries to reinstall - # on top of them and fails with "binary X already exists in - # destination". + # see the cached binaries. # - # target/ (the compilation output dir) is deliberately NOT cached here: - # per the GitHub backend design's caching policy, multi-GB per-job - # target/ trees dwarf the high-value tool cache under the repo cache - # quota, give only modest recompile savings once tools are cached, and - # could overwrite the downloaded target/anvil/impact/ cache. Only the - # ~/.cargo tool + registry state below is cached. + # target/ is deliberately excluded: per-job target trees dwarf the + # tool cache and could overwrite the downloaded impact cache. path: | ~/.cargo/registry/cache/ ~/.cargo/registry/index/ @@ -126,31 +107,7 @@ 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 - # 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. + # cargo-binstall keeps the cold bootstrap path fast. - name: Install cargo-binstall uses: cargo-bins/cargo-binstall@v1.21.0 # immutable release, the tag cannot be moved @@ -161,6 +118,11 @@ runs: cargo binstall --no-confirm --locked just || cargo install --locked just fi + # The catalog recipe `anvil-setup` (or `anvil--setup` + # when a group is specified via the `group` input) uses the Just executable + # bootstrapped above, then handles everything else. The setup recipes are idempotent and + # short-circuit on tools already installed at or above the pinned + # version, so re-running on cache-hit runs is cheap. - 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 9b2454d19..e068388cd 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/aprz.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/aprz.just @@ -36,7 +36,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 20de20468..a43c5de01 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/audit.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/audit.just @@ -12,7 +12,7 @@ [script("pwsh", "-NoProfile")] anvil-audit: anvil-audit-validate-prereqs $ErrorActionPreference = 'Stop' - & 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 2f4d42f74..b26880a81 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 d23fb3a9a..344fabdcc 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 be9871403..c937a5dc2 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/careful.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/careful.just @@ -59,7 +59,7 @@ anvil-careful: anvil-careful-validate-prereqs anvil-impact Write-Host " reason : the nightly toolchain and/or resolved cargo-careful executable changed" Write-Host " now : $idLabel" Write-Host " identity: $($prev.Substring(0, [Math]::Min(12, $prev.Length)))... -> $($idHash.Substring(0, 12))..." - cargo clean + & cargo '+{{ rust_nightly }}' clean if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } } # Persist the current identity (also seeds the marker on first run, when diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/cargo-hack.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/cargo-hack.just index ea604935a..0377fb4da 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/cargo-hack.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/cargo-hack.just @@ -16,7 +16,7 @@ anvil-cargo-hack: anvil-cargo-hack-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include required) if ($include -eq '--skip') { Write-Host 'anvil-cargo-hack: no affected packages; skipping'; exit 0 } - & 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 773d7ed9f..9744892f0 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/cargo-sort.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/cargo-sort.just @@ -21,7 +21,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 c9167c91c..a0354f2c4 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/clippy.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/clippy.just @@ -19,7 +19,7 @@ anvil-clippy: anvil-clippy-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include affected) if ($include -eq '--skip') { Write-Host 'anvil-clippy: no affected packages; skipping'; exit 0 } - & 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 c52b613a4..c798669b5 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/deny.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/deny.just @@ -14,7 +14,7 @@ [script("pwsh", "-NoProfile")] anvil-deny: anvil-deny-validate-prereqs $ErrorActionPreference = 'Stop' - & 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 9250d41af..a8edeef7c 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 1954ae4a7..ca6ade8c9 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 48ef3e7c0..57f360170 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/ensure-no-cyclic-deps.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/ensure-no-cyclic-deps.just @@ -14,7 +14,7 @@ anvil-ensure-no-cyclic-deps: anvil-ensure-no-cyclic-deps-validate-prereqs anvil- $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include modified) if ($include -eq '--skip') { Write-Host 'anvil-ensure-no-cyclic-deps: no modified packages; skipping'; exit 0 } - 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 c56682046..cee4d24d5 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/ensure-no-default-features.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/ensure-no-default-features.just @@ -14,7 +14,7 @@ anvil-ensure-no-default-features: anvil-ensure-no-default-features-validate-prer $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include modified) if ($include -eq '--skip') { Write-Host 'anvil-ensure-no-default-features: no modified packages; skipping'; exit 0 } - 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 698b11157..fe447b7fb 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 33c0593d2..d659a3d4d 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/external-types.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/external-types.just @@ -31,7 +31,7 @@ anvil-external-types: anvil-external-types-validate-prereqs anvil-impact $pkg = @(if ($include) { -split $include } else { '--workspace' }) # Build pkg-name -> manifest-path map, restricted to library crates. $libPkgs = @{} - $metadataJson = & 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/fmt.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/fmt.just index 9d855e7df..48291c5e4 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/fmt.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/fmt.just @@ -23,7 +23,7 @@ anvil-fmt: anvil-fmt-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include modified) if ($include -eq '--skip') { Write-Host 'anvil-fmt: no modified packages; skipping'; exit 0 } - cargo each --workspace --keep-going '--' cargo '+{{ rust_nightly }}' fmt --manifest-path '{manifest}' --check + cargo {{_anvil_stable_toolchain_args}} each --workspace --keep-going '--' cargo '+{{ rust_nightly }}' fmt --manifest-path '{manifest}' --check if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Per-check setup + validate-prereqs diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/license-headers.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/license-headers.just index 989f57b4b..fb794c32e 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/license-headers.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/license-headers.just @@ -14,7 +14,7 @@ anvil-license-headers: anvil-license-headers-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include modified) if ($include -eq '--skip') { Write-Host 'anvil-license-headers: no modified packages; skipping'; exit 0 } - 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 ab39d6717..1096d2743 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/llvm-cov.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/llvm-cov.just @@ -37,7 +37,7 @@ anvil-llvm-cov: anvil-llvm-cov-validate-prereqs anvil-impact # never opts a changed package out of tests. $testOnlyPkg = [System.Collections.Generic.List[string]]::new() if ($pkg -contains '--package') { - $metadataJson = & 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 3db72eacb..4703a71f3 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/loom.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/loom.just @@ -57,7 +57,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 @@ -110,7 +110,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 } } @@ -120,7 +120,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 000000000..f95b38cff --- /dev/null +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/msrv-test.just @@ -0,0 +1,56 @@ +# 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) + $impactExit = $LASTEXITCODE + if ($impactExit -ne 0) { exit $impactExit } + if ($include -eq '--skip') { Write-Host 'anvil-msrv-test: no affected packages; skipping'; exit 0 } + $toolchainOutput = & "{{ just_executable() }}" _anvil-resolve-stable msrv | Out-String + $resolverExit = $LASTEXITCODE + if ($resolverExit -ne 0) { exit $resolverExit } + $toolchain = $toolchainOutput.Trim() + if ([string]::IsNullOrWhiteSpace($toolchain)) { + Write-Host 'anvil-msrv-test: no root MSRV declared; skipping' + exit 0 + } + $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: + $toolchainOutput = & "{{ just_executable() }}" _anvil-resolve-stable msrv | Out-String + $resolverExit = $LASTEXITCODE + if ($resolverExit -ne 0) { exit $resolverExit } + $toolchain = $toolchainOutput.Trim() + if ([string]::IsNullOrWhiteSpace($toolchain)) { exit 0 } + & cargo "+$toolchain" --version + if ($LASTEXITCODE -ne 0) { + $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 079a54cb9..e4efc812f 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/mutants-diff.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/mutants-diff.just @@ -50,7 +50,7 @@ anvil-mutants-diff: anvil-mutants-diff-validate-prereqs anvil-impact # not a behaviour change for it. git diff "$base" --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 a914f1eb6..e3e32a16b 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/mutants-full.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/mutants-full.just @@ -17,7 +17,7 @@ anvil-mutants-full: anvil-mutants-full-validate-prereqs Write-Host 'anvil-mutants-full: aarch64-pc-windows-msvc -- cargo-mutants does not build here (winapi); skipping' exit 0 } - & 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 5ec58e430..7ae42342d 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/readme-check.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/readme-check.just @@ -37,6 +37,7 @@ [script("pwsh", "-NoProfile")] _anvil-readme mode: anvil-readme-check-validate-prereqs $ErrorActionPreference = 'Stop' + $stableArgs = {{_anvil_stable_toolchain_args}} $action = '{{ mode }}' if ($action -notin @('generate', 'check')) { [Console]::Error.WriteLine("anvil-readme: invalid mode '$action'; expected 'generate' or 'check'") @@ -55,7 +56,7 @@ _anvil-readme mode: anvil-readme-check-validate-prereqs # Unscoped: a change to the workspace-level README template is a repo-level # input cargo-delta cannot map to a package, so this check always runs the # full eligible-crate set rather than an impact subset. - $metadataJson = & cargo metadata --no-deps --format-version 1 + $metadataJson = & cargo @stableArgs metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error "${prefix}: cargo metadata failed" exit $LASTEXITCODE @@ -101,7 +102,7 @@ _anvil-readme mode: anvil-readme-check-validate-prereqs $args = @('doc2readme', $target) if ($check) { $args += '--check' } if ($relTemplate) { $args += @('--template', $relTemplate) } - & cargo @args + & cargo @stableArgs @args if ($LASTEXITCODE -ne 0) { $hadFailure = $true } } finally { Pop-Location diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/semver-check.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/semver-check.just index e1d8273e4..5dbd16128 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/semver-check.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/semver-check.just @@ -49,6 +49,7 @@ [script("pwsh", "-NoProfile")] anvil-semver-check: anvil-semver-check-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' + $stableArgs = {{_anvil_stable_toolchain_args}} $include = (& "{{ just_executable() }}" _anvil-impact-include affected) $commentFile = 'target/anvil/comments/semver.md' if ($include -eq '--skip') { @@ -68,7 +69,7 @@ anvil-semver-check: anvil-semver-check-validate-prereqs anvil-impact # null, `publish = false` as an empty array, and named-registry restrictions # as a nonempty array. Only the empty array is non-publishable. $libPkgs = @{} - $metadataJson = & cargo metadata --no-deps --format-version 1 + $metadataJson = & cargo @stableArgs metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-semver-check: cargo metadata failed' exit $LASTEXITCODE @@ -146,7 +147,7 @@ anvil-semver-check: anvil-semver-check-validate-prereqs anvil-impact } Write-Host "anvil-semver-check: $p (baseline $base)" - $outputLines = @(& cargo semver-checks --package $p --baseline-rev $base 2>&1) + $outputLines = @(& cargo @stableArgs semver-checks --package $p --baseline-rev $base 2>&1) $semverExit = $LASTEXITCODE $output = ($outputLines | Out-String) if ($semverExit -eq 100) { diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/spellcheck.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/spellcheck.just index 15e734b47..cdf2aef95 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/spellcheck.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/spellcheck.just @@ -42,9 +42,9 @@ anvil-spellcheck: anvil-spellcheck-validate-prereqs # ignores user-curated dictionaries (`extra_dictionaries`, custom # hunspell langs, etc.). if (Test-Path 'spellcheck.toml') { - 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 000000000..d35d01ac8 --- /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 75ad63c1d..ddce5d420 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,32 @@ # 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). Its +# groups can be invoked individually without invoking the other groups: # -# slow1: tests + coverage (replaces the former pr-test group) -# slow2: stricter-runtime correctness (miri, careful) -# slow3: mutation testing +# pr-test: tests + coverage +# pr-msrv: all-target 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 these groups as independent parallel jobs. +# The umbrella runs them sequentially only when invoked locally: # # $ just anvil-pr-test # tests + coverage only +# $ just anvil-pr-msrv # MSRV all-target 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 +39,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 42d745026..c9e7db74e 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/impact.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/impact.just @@ -235,19 +235,24 @@ _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, use the selected rustc sysroot as an unambiguous path + # toolchain. + $stableArgs = {{_anvil_stable_toolchain_args}} + $baselineToolchain = if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { + $env:RUSTUP_TOOLCHAIN.Trim() + } elseif ($stableArgs.Count -eq 1) { + ([string] $stableArgs[0]).Substring(1) + } else { + $sysroot = @(& rustc --print sysroot) + if ($LASTEXITCODE -ne 0 -or $sysroot.Count -ne 1 -or [string]::IsNullOrWhiteSpace([string] $sysroot[0])) { + Write-Error 'anvil-impact: rustc could not report the current checkout stable toolchain sysroot' + exit 1 + } + ([string] $sysroot[0]).Trim() } # Temp-root precedence follows the runner-managed scratch dir on each @@ -280,14 +285,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 +317,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 +435,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 +512,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 ee29054c9..0e444fef5 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' @@ -73,6 +74,7 @@ import 'checks/udeps.just' # would break the whole Justfile. 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 d7062aa7a..8e942948d 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/tools.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/tools.just @@ -128,15 +128,14 @@ 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. +# Validate that rustc is available and workspace MSRVs are compatible. [group("anvil-setup")] [script("pwsh", "-NoProfile")] anvil-tool-rustc-validate-prereqs: @@ -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,228 @@ 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="install": + $ErrorActionPreference = 'Stop' + Set-StrictMode -Version 3 + + $action = '{{action}}' + $validActions = @( + 'install', + 'validate-workspace-msrv', + 'msrv', + 'install-msrv' + ) + if ($action -notin $validActions) { + Write-Error "_anvil-resolve-stable: unknown action '$action'" + exit 2 + } + + $repoRoot = '{{replace(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'" + } + + # This bootstrap scan must choose Cargo before Cargo is available to + # parse the manifest. Keep its workspace.package-before-package + # precedence and accepted syntax synchronized with the selector in + # versions.just. + $values = @{} + $section = '' + foreach ($line in (Get-Content -LiteralPath $manifestPath)) { + 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 Get-InstalledToolchains { + if (-not (Get-Command rustup -ErrorAction SilentlyContinue)) { + throw 'anvil: rustup not found. Install rustup from https://rustup.rs and ensure it is on PATH' + } + $installed = @(rustup toolchain list) + if ($LASTEXITCODE -ne 0) { + throw 'anvil: rustup toolchain list failed' + } + return $installed + } + + function Test-ToolchainInstalled([string] $toolchain) { + $installed = Get-InstalledToolchains + return (($installed -join "`n") -match "(?m)^$([regex]::Escape($toolchain))(?:-|$)") + } + + function Assert-WorkspaceMsrvCompatibility { + $manifestPath = Join-Path $repoRoot 'Cargo.toml' + $rootMsrv = Get-RootMsrv + if (-not (Test-ToolchainInstalled $rootMsrv)) { + throw "anvil: root MSRV toolchain '$rootMsrv' is not installed; run 'just anvil-toolchain-stable-install' before validating prerequisites" + } + $metadataText = (& cargo "+$rootMsrv" metadata --manifest-path $manifestPath --format-version 1 --no-deps 2>&1 | Out-String) + if ($LASTEXITCODE -ne 0) { + throw "anvil: cargo metadata failed while validating workspace MSRVs:`n$metadataText" + } + $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" + } + + function ConvertTo-ComparableRustVersion([string] $value) { + if ($value -notmatch '^\s*(\d+)\.(\d+)(?:\.(\d+))?\s*$') { + throw "anvil: invalid resolved rust-version '$value'" + } + # Cargo treats 1.x and 1.x.0 as the same floor, while + # System.Version sorts an unspecified build component differently. + $patch = if ([string]::IsNullOrWhiteSpace($Matches[3])) { 0 } else { [int] $Matches[3] } + return [version]::new([int] $Matches[1], [int] $Matches[2], $patch) + } + + $rootVersion = ConvertTo-ComparableRustVersion $rootMsrv + $newer = @( + $members | + Where-Object { (ConvertTo-ComparableRustVersion ([string] $_.rust_version)) -gt $rootVersion } | + 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 + Mapped = $true + } + } + return [pscustomobject]@{ Value = $msrv; Mapped = $false } + } + + function Install-Toolchain([string] $toolchain, [string] $label) { + if (-not (Test-ToolchainInstalled $toolchain)) { + Write-Host "anvil: installing $label toolchain '$toolchain'" + rustup toolchain install $toolchain --profile minimal + if ($LASTEXITCODE -ne 0) { + throw "anvil: failed to install $label toolchain '$toolchain'" + } + } + } + + if ($action -eq 'validate-workspace-msrv') { + Assert-CompleteInternalToolchainConfiguration + $skipWorkspaceMsrvValidation = -not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN) + $hasRootToolchainFile = Test-RootToolchainFile + if ($skipWorkspaceMsrvValidation -or $hasRootToolchainFile) { + Get-InstalledToolchains | Out-Null + } else { + 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.Value 'MSRV' + } + } else { + $msrvSelection.Value + } + exit 0 + } + + Assert-CompleteInternalToolchainConfiguration + if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { + Get-InstalledToolchains | Out-Null + & cargo --version *> $null + if ($LASTEXITCODE -ne 0) { + throw "anvil: selected stable toolchain '$($env:RUSTUP_TOOLCHAIN)' is unavailable; provision RUSTUP_TOOLCHAIN or unset it" + } + exit 0 + } + + if (Test-RootToolchainFile) { + Get-InstalledToolchains | Out-Null + Push-Location -LiteralPath $repoRoot + try { + & rustc --version + if ($LASTEXITCODE -ne 0) { + throw 'anvil: rustup could not provision the repository toolchain file' + } + } finally { + Pop-Location + } + exit 0 + } + + Install-Toolchain (Get-RootMsrv) 'stable' + +# Setup paths that execute stable Cargo/Rust or install Cargo tools share this +# dependency, which Just deduplicates within one setup invocation. + +# Ensure the selected stable toolchain is available while preserving native repository toolchain-file behavior. +[group("anvil-setup")] +anvil-toolchain-stable-install: (_anvil-resolve-stable "install") + +# _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 +403,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}}' @@ -187,7 +411,7 @@ _install-tool name version installer source_prereq="": $sourcePrereq = '{{source_prereq}}' if ($installer -ne 'install' -and $installer -ne 'binstall') { - Write-Error "_install-tool: unknown installer '$installer' (expected 'install' or 'binstall')" + Write-Error "_install-tool-core: unknown installer '$installer' (expected 'install' or 'binstall')" exit 2 } @@ -199,7 +423,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 +441,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 +455,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 +463,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,8 +482,19 @@ _check-tool name version: $installed = $null $pattern = '^' + [regex]::Escape($name) + ' v(\S+):' - foreach ($line in (cargo install --list 2>$null)) { - if ($line -match $pattern) { $installed = $Matches[1]; break } + $previousAutoInstall = $env:RUSTUP_AUTO_INSTALL + try { + # Validation must not let the rustup proxy provision a missing selection. + $env:RUSTUP_AUTO_INSTALL = '0' + foreach ($line in (& cargo {{_anvil_stable_toolchain_args}} install --list 2>$null)) { + if ($line -match $pattern) { $installed = $Matches[1]; break } + } + } finally { + if ($null -eq $previousAutoInstall) { + Remove-Item Env:RUSTUP_AUTO_INSTALL -ErrorAction SilentlyContinue + } else { + $env:RUSTUP_AUTO_INSTALL = $previousAutoInstall + } } if (-not $installed) { Write-Error "anvil: required tool '$name' not found. Install: cargo install --locked --version =$version $name" @@ -306,11 +544,12 @@ _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 +# MSRV fallback selections target `rustup component add --toolchain`; native +# rustup selections use `rustup component add` without restating the selector. # # Probe-first: if the component is already available, skip the # `rustup component add`. This keeps the recipe idempotent and robust @@ -324,6 +563,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 +583,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 } @@ -366,8 +621,11 @@ _install-component toolchain component: exit 0 } - if ($toolchain -eq 'default') { - Write-Host "rustup component add $component (default toolchain)" + if ($toolchain -eq 'default' -and $null -ne $selectedStableToolchain) { + Write-Host "rustup component add --toolchain $selectedStableToolchain $component" + rustup component add --toolchain $selectedStableToolchain $component + } elseif ($toolchain -eq 'default') { + Write-Host "rustup component add $component" rustup component add $component } else { Write-Host "rustup component add --toolchain $toolchain $component" @@ -396,6 +654,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 +670,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 +707,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 +742,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 +756,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 +843,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. @@ -689,12 +968,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 6dbdfa728..f330945cf 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,61 @@ # 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)) { + return +} + +if ((Test-Path -LiteralPath (Join-Path $RepoRoot 'rust-toolchain') -PathType Leaf) -or + (Test-Path -LiteralPath (Join-Path $RepoRoot 'rust-toolchain.toml') -PathType Leaf)) { + 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'" +} + +# This bootstrap scan must choose Cargo before Cargo is available to parse the +# manifest. Keep its workspace.package-before-package precedence and accepted +# syntax synchronized with Get-RootMsrv in tools.just. +$values = @{} +$section = '' +foreach ($line in (Get-Content -LiteralPath $manifestPath)) { + 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/impact.rs b/crates/cargo-anvil/tests/impact.rs index ad0bd838d..bb7649b09 100644 --- a/crates/cargo-anvil/tests/impact.rs +++ b/crates/cargo-anvil/tests/impact.rs @@ -170,7 +170,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"), @@ -848,7 +852,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"), @@ -891,10 +899,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)] { @@ -984,6 +1001,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 @@ -993,16 +1072,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() ); @@ -1307,7 +1392,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"), @@ -1334,23 +1423,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\ @@ -1362,12 +1455,8 @@ fn snapshot_toolchain_probe(caller_toolchain: Option<&str>) -> Vec { exit 0\n", ), ( - "rustup.ps1", - "if (($args -contains 'show') -and ($args -contains 'active-toolchain')) {\n\ - \x20 Write-Output 'anvil-active-toolchain'\n\ - \x20 exit 0\n\ - }\n\ - exit 0\n", + "rustc.ps1", + "if ($args -contains 'sysroot') { Write-Output '/toolchains/test (fork) toolchain' }\nexit 0\n", ), ]); @@ -1389,35 +1478,39 @@ fn snapshot_toolchain_probe(caller_toolchain: Option<&str>) -> Vec { #[test] #[serial] -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: the baseline receives the current checkout's selected rustc + // sysroot as an unambiguous path toolchain. The current snapshot needs no + // override because it runs under the selecting repository file itself. + let with_file = snapshot_toolchain_probe(None, true); + assert_eq!( + with_file, + vec!["/toolchains/test (fork) toolchain".to_owned(), "".to_owned()], + "the baseline must inherit the current file selection while the current snapshot uses it natively" ); } diff --git a/crates/cargo-anvil/tests/recipe_contracts.rs b/crates/cargo-anvil/tests/recipe_contracts.rs index c8b80769e..45f13217a 100644 --- a/crates/cargo-anvil/tests/recipe_contracts.rs +++ b/crates/cargo-anvil/tests/recipe_contracts.rs @@ -23,6 +23,7 @@ const BOLERO: &str = include_str!("../templates/justfiles/anvil/checks/bolero.ju const FMT: &str = include_str!("../templates/justfiles/anvil/checks/fmt.just"); const LLVM_COV: &str = include_str!("../templates/justfiles/anvil/checks/llvm-cov.just"); const LOOM: &str = include_str!("../templates/justfiles/anvil/checks/loom.just"); +const MSRV_TEST: &str = include_str!("../templates/justfiles/anvil/checks/msrv-test.just"); const SEMVER: &str = include_str!("../templates/justfiles/anvil/checks/semver-check.just"); const EXTERNAL_TYPES: &str = include_str!("../templates/justfiles/anvil/checks/external-types.just"); const TOOLS: &str = include_str!("../templates/justfiles/anvil/tools.just"); @@ -64,10 +65,25 @@ fn bolero_uses_its_supported_release_profile_option() { } 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 ($env:FAKE_CARGO_TOOLCHAIN_LOG) { + Add-Content -LiteralPath $env:FAKE_CARGO_TOOLCHAIN_LOG -Value $env:RUSTUP_TOOLCHAIN +} if ($args -contains 'each') { exit [int]$env:FAKE_EACH_EXIT } @@ -201,7 +217,8 @@ 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") { @@ -209,6 +226,7 @@ fn fixture(imports: &[(&str, &str)], dependency_recipes: &[&str]) -> TempDir { justfile.push_str("rust_nightly_external_types := \"nightly-test\"\n\n"); // A visibly synthetic placeholder used only for recipe interpolation. justfile.push_str("cargo_check_external_types_version := \"0.0.0-test\"\n\n"); + justfile.push_str("_anvil_stable_toolchain_args := \"@()\"\n\n"); } for (name, contents) in imports { write(&tmp.path().join(name), contents); @@ -222,7 +240,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"); @@ -240,7 +258,11 @@ fn path_with_fake_bin(root: &Path) -> OsString { fn run_just(root: &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(root); command.env("PATH", path_with_fake_bin(root)); command.env("FAKE_WORKSPACE_ROOT", root); // A fixture is a scratch workspace, so it must not inherit the impact @@ -272,6 +294,117 @@ fn assert_failed(output: &Output, context: &str) { ); } +#[test] +fn stable_command_leaves_environment_toolchain_selection_native() { + 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 toolchain_log = tmp.path().join("cargo-toolchain.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()), + ("FAKE_CARGO_TOOLCHAIN_LOG", toolchain_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(), "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" + ); + assert_eq!(fs::read_to_string(toolchain_log).unwrap().trim(), "test-stable"); +} + +#[test] +fn msrv_test_propagates_nested_just_failures() { + if !tools_available() { + return; + } + let helper_recipes = "\n[script(\"pwsh\", \"-NoProfile\")]\n\ + _anvil-impact-include tier:\n\ + \x20 if ($env:FAKE_IMPACT_HELPER_OUTPUT) { Write-Output $env:FAKE_IMPACT_HELPER_OUTPUT }\n\ + \x20 exit [int]$env:FAKE_IMPACT_HELPER_EXIT\n\ + \n[script(\"pwsh\", \"-NoProfile\")]\n\ + _anvil-resolve-stable action:\n\ + \x20 if ($env:FAKE_RESOLVER_OUTPUT) { Write-Output $env:FAKE_RESOLVER_OUTPUT }\n\ + \x20 exit [int]$env:FAKE_RESOLVER_EXIT\n"; + + let validation_tmp = fixture(&[("msrv-test.just", MSRV_TEST)], &["anvil-impact"]); + let validation_justfile = validation_tmp.path().join("Justfile"); + let mut validation_body = fs::read_to_string(&validation_justfile).unwrap(); + validation_body.push_str(helper_recipes); + write(&validation_justfile, &validation_body); + let validation_failure = run_just( + validation_tmp.path(), + &["anvil-msrv-test-validate-prereqs"], + &[("FAKE_RESOLVER_EXIT", OsStr::new("30"))], + ); + assert_failed(&validation_failure, "failed prerequisite MSRV resolver"); + + let tmp = fixture( + &[("msrv-test.just", MSRV_TEST)], + &["anvil-msrv-test-validate-prereqs", "anvil-impact"], + ); + let justfile_path = tmp.path().join("Justfile"); + let mut justfile = fs::read_to_string(&justfile_path).unwrap(); + justfile.push_str(helper_recipes); + write(&justfile_path, &justfile); + let cargo_log = tmp.path().join("cargo.log"); + + let impact_failure = run_just( + tmp.path(), + &["anvil-msrv-test"], + &[ + ("FAKE_IMPACT_HELPER_EXIT", OsStr::new("31")), + ("FAKE_CARGO_LOG", cargo_log.as_os_str()), + ], + ); + assert_failed(&impact_failure, "failed impact helper"); + assert!( + fs::read_to_string(&cargo_log).unwrap_or_default().is_empty(), + "Cargo must not run after impact helper failure" + ); + + let resolver_failure = run_just( + tmp.path(), + &["anvil-msrv-test"], + &[ + ("FAKE_IMPACT_HELPER_EXIT", OsStr::new("0")), + ("FAKE_RESOLVER_EXIT", OsStr::new("32")), + ("FAKE_CARGO_LOG", cargo_log.as_os_str()), + ], + ); + assert_failed(&resolver_failure, "failed MSRV resolver"); + assert!( + fs::read_to_string(cargo_log).unwrap_or_default().is_empty(), + "Cargo must not run after resolver failure" + ); +} + #[test] 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 83580c5a3..2cb084555 100644 --- a/crates/cargo-anvil/tests/schemas.rs +++ b/crates/cargo-anvil/tests/schemas.rs @@ -134,6 +134,7 @@ fn just_lists_emitted_recipes() { ), ("anvil-pr", "# Run all pull request checks."), ("anvil-pr-fast", "# Run the fast pull request checks."), + ("anvil-pr-msrv", "# Run pull request compatibility tests under the declared MSRV."), ("anvil-pr-mutants", "# Run the pull request mutation tests."), ("anvil-pr-runtime-analysis", "# Run the pull request runtime analysis."), ("anvil-pr-slow", "# Run the slow pull request checks."), diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap index 53a6bd7b2..e8063b707 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap @@ -77,7 +77,6 @@ RUN curl -fsSLo /tmp/cargo-binstall.tgz \ && tar -xzf /tmp/cargo-binstall.tgz -C "${CARGO_HOME}/bin" cargo-binstall \ && chmod 755 "${CARGO_HOME}/bin/cargo-binstall" \ && rm /tmp/cargo-binstall.tgz - # <<< anvil-managed: anvil-container-tools # >>> anvil-managed: anvil-container-setup @@ -385,9 +384,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. @@ -439,6 +439,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] @@ -824,13 +847,10 @@ steps: parameters: group: pr-fast # ADO has no PR-title predefined variable: `System.PullRequest.Title` - # does NOT exist, so `$(System.PullRequest.Title)` would expand to the - # literal unexpanded macro text (non-empty) and make anvil-pr-title - # validate that string. Resolve the real title from the REST API on PR - # builds and publish it as the PR_TITLE pipeline variable. Only - # anvil-pr-title (in pr-fast) consults it; other groups ignore it, but we - # resolve uniformly to keep group.yml the same across groups. Non-PR builds - # skip; a known PR whose metadata cannot be retrieved fails closed. + # does NOT exist, so `$(System.PullRequest.Title)` would expand to literal + # macro text. Resolve the title only for pr-fast, which owns anvil-pr-title. + # Non-PR builds skip; a known PR whose metadata cannot be retrieved fails + # closed. - pwsh: | $ErrorActionPreference = 'Stop' $prId = $env:SYSTEM_PULLREQUEST_PULLREQUESTID @@ -841,7 +861,7 @@ steps: } $uri = "$($env:SYSTEM_COLLECTIONURI)$($env:SYSTEM_TEAMPROJECTID)/_apis/git/repositories/$($env:BUILD_REPOSITORY_ID)/pullRequests/${prId}?api-version=7.0" try { - $resp = Invoke-RestMethod -Uri $uri -Headers @{ Authorization = "Bearer $($env:SYSTEM_ACCESSTOKEN)" } + $resp = Invoke-RestMethod -Uri $uri -Headers @{ Authorization = "Bearer $env:SYSTEM_ACCESSTOKEN" } Write-Host "##vso[task.setvariable variable=PR_TITLE]$($resp.title)" } catch { Write-Error "anvil: could not resolve PR title for PR $prId ($_). Ensure the job can access the repository OAuth token." @@ -861,7 +881,7 @@ steps: just anvil-pr-fast displayName: anvil-pr-fast env: - # Resolved by the preceding step; empty only outside PR builds. + # Resolved by the preceding pr-fast step; empty only outside PR builds. PR_TITLE: $(PR_TITLE) # Disable cargo incremental compilation in CI: the incremental dir is # not part of the anvil-setup cache (see setup.yml), so generating it is @@ -869,6 +889,44 @@ 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 + + - 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: + + # 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. @@ -888,33 +946,7 @@ steps: - template: setup.yml parameters: group: pr-mutants - # ADO has no PR-title predefined variable: `System.PullRequest.Title` - # does NOT exist, so `$(System.PullRequest.Title)` would expand to the - # literal unexpanded macro text (non-empty) and make anvil-pr-title - # validate that string. Resolve the real title from the REST API on PR - # builds and publish it as the PR_TITLE pipeline variable. Only - # anvil-pr-title (in pr-fast) consults it; other groups ignore it, but we - # resolve uniformly to keep group.yml the same across groups. Non-PR builds - # skip; a known PR whose metadata cannot be retrieved fails closed. - - pwsh: | - $ErrorActionPreference = 'Stop' - $prId = $env:SYSTEM_PULLREQUEST_PULLREQUESTID - if (-not $prId) { - Write-Host 'anvil: not a PR build; leaving PR_TITLE empty' - Write-Host '##vso[task.setvariable variable=PR_TITLE]' - exit 0 - } - $uri = "$($env:SYSTEM_COLLECTIONURI)$($env:SYSTEM_TEAMPROJECTID)/_apis/git/repositories/$($env:BUILD_REPOSITORY_ID)/pullRequests/${prId}?api-version=7.0" - try { - $resp = Invoke-RestMethod -Uri $uri -Headers @{ Authorization = "Bearer $($env:SYSTEM_ACCESSTOKEN)" } - Write-Host "##vso[task.setvariable variable=PR_TITLE]$($resp.title)" - } catch { - Write-Error "anvil: could not resolve PR title for PR $prId ($_). Ensure the job can access the repository OAuth token." - exit 1 - } - displayName: anvil-pr-mutants (resolve PR title) - env: - SYSTEM_ACCESSTOKEN: $(System.AccessToken) + - bash: | set -euo pipefail # The impact mode is fixed by group class at emit time -- never probed from a @@ -926,8 +958,7 @@ steps: just anvil-pr-mutants displayName: anvil-pr-mutants env: - # Resolved by the preceding step; empty only outside PR builds. - PR_TITLE: $(PR_TITLE) + # Disable cargo incremental compilation in CI: the incremental dir is # not part of the anvil-setup cache (see setup.yml), so generating it is # pure cost with no cross-run benefit. Mirrors the GitHub impl workflows' @@ -953,33 +984,7 @@ steps: - template: setup.yml parameters: group: pr-runtime-analysis - # ADO has no PR-title predefined variable: `System.PullRequest.Title` - # does NOT exist, so `$(System.PullRequest.Title)` would expand to the - # literal unexpanded macro text (non-empty) and make anvil-pr-title - # validate that string. Resolve the real title from the REST API on PR - # builds and publish it as the PR_TITLE pipeline variable. Only - # anvil-pr-title (in pr-fast) consults it; other groups ignore it, but we - # resolve uniformly to keep group.yml the same across groups. Non-PR builds - # skip; a known PR whose metadata cannot be retrieved fails closed. - - pwsh: | - $ErrorActionPreference = 'Stop' - $prId = $env:SYSTEM_PULLREQUEST_PULLREQUESTID - if (-not $prId) { - Write-Host 'anvil: not a PR build; leaving PR_TITLE empty' - Write-Host '##vso[task.setvariable variable=PR_TITLE]' - exit 0 - } - $uri = "$($env:SYSTEM_COLLECTIONURI)$($env:SYSTEM_TEAMPROJECTID)/_apis/git/repositories/$($env:BUILD_REPOSITORY_ID)/pullRequests/${prId}?api-version=7.0" - try { - $resp = Invoke-RestMethod -Uri $uri -Headers @{ Authorization = "Bearer $($env:SYSTEM_ACCESSTOKEN)" } - Write-Host "##vso[task.setvariable variable=PR_TITLE]$($resp.title)" - } catch { - Write-Error "anvil: could not resolve PR title for PR $prId ($_). Ensure the job can access the repository OAuth token." - exit 1 - } - displayName: anvil-pr-runtime-analysis (resolve PR title) - env: - SYSTEM_ACCESSTOKEN: $(System.AccessToken) + - bash: | set -euo pipefail # The impact mode is fixed by group class at emit time -- never probed from a @@ -991,8 +996,7 @@ steps: just anvil-pr-runtime-analysis displayName: anvil-pr-runtime-analysis env: - # Resolved by the preceding step; empty only outside PR builds. - PR_TITLE: $(PR_TITLE) + # Disable cargo incremental compilation in CI: the incremental dir is # not part of the anvil-setup cache (see setup.yml), so generating it is # pure cost with no cross-run benefit. Mirrors the GitHub impl workflows' @@ -1018,33 +1022,7 @@ steps: - template: setup.yml parameters: group: pr-test - # ADO has no PR-title predefined variable: `System.PullRequest.Title` - # does NOT exist, so `$(System.PullRequest.Title)` would expand to the - # literal unexpanded macro text (non-empty) and make anvil-pr-title - # validate that string. Resolve the real title from the REST API on PR - # builds and publish it as the PR_TITLE pipeline variable. Only - # anvil-pr-title (in pr-fast) consults it; other groups ignore it, but we - # resolve uniformly to keep group.yml the same across groups. Non-PR builds - # skip; a known PR whose metadata cannot be retrieved fails closed. - - pwsh: | - $ErrorActionPreference = 'Stop' - $prId = $env:SYSTEM_PULLREQUEST_PULLREQUESTID - if (-not $prId) { - Write-Host 'anvil: not a PR build; leaving PR_TITLE empty' - Write-Host '##vso[task.setvariable variable=PR_TITLE]' - exit 0 - } - $uri = "$($env:SYSTEM_COLLECTIONURI)$($env:SYSTEM_TEAMPROJECTID)/_apis/git/repositories/$($env:BUILD_REPOSITORY_ID)/pullRequests/${prId}?api-version=7.0" - try { - $resp = Invoke-RestMethod -Uri $uri -Headers @{ Authorization = "Bearer $($env:SYSTEM_ACCESSTOKEN)" } - Write-Host "##vso[task.setvariable variable=PR_TITLE]$($resp.title)" - } catch { - Write-Error "anvil: could not resolve PR title for PR $prId ($_). Ensure the job can access the repository OAuth token." - exit 1 - } - displayName: anvil-pr-test (resolve PR title) - env: - SYSTEM_ACCESSTOKEN: $(System.AccessToken) + - bash: | set -euo pipefail # The impact mode is fixed by group class at emit time -- never probed from a @@ -1056,8 +1034,7 @@ steps: just anvil-pr-test displayName: anvil-pr-test env: - # Resolved by the preceding step; empty only outside PR builds. - PR_TITLE: $(PR_TITLE) + # Disable cargo incremental compilation in CI: the incremental dir is # not part of the anvil-setup cache (see setup.yml), so generating it is # pure cost with no cross-run benefit. Mirrors the GitHub impl workflows' @@ -1083,33 +1060,7 @@ steps: - template: setup.yml parameters: group: scheduled-advisories - # ADO has no PR-title predefined variable: `System.PullRequest.Title` - # does NOT exist, so `$(System.PullRequest.Title)` would expand to the - # literal unexpanded macro text (non-empty) and make anvil-pr-title - # validate that string. Resolve the real title from the REST API on PR - # builds and publish it as the PR_TITLE pipeline variable. Only - # anvil-pr-title (in pr-fast) consults it; other groups ignore it, but we - # resolve uniformly to keep group.yml the same across groups. Non-PR builds - # skip; a known PR whose metadata cannot be retrieved fails closed. - - pwsh: | - $ErrorActionPreference = 'Stop' - $prId = $env:SYSTEM_PULLREQUEST_PULLREQUESTID - if (-not $prId) { - Write-Host 'anvil: not a PR build; leaving PR_TITLE empty' - Write-Host '##vso[task.setvariable variable=PR_TITLE]' - exit 0 - } - $uri = "$($env:SYSTEM_COLLECTIONURI)$($env:SYSTEM_TEAMPROJECTID)/_apis/git/repositories/$($env:BUILD_REPOSITORY_ID)/pullRequests/${prId}?api-version=7.0" - try { - $resp = Invoke-RestMethod -Uri $uri -Headers @{ Authorization = "Bearer $($env:SYSTEM_ACCESSTOKEN)" } - Write-Host "##vso[task.setvariable variable=PR_TITLE]$($resp.title)" - } catch { - Write-Error "anvil: could not resolve PR title for PR $prId ($_). Ensure the job can access the repository OAuth token." - exit 1 - } - displayName: anvil-scheduled-advisories (resolve PR title) - env: - SYSTEM_ACCESSTOKEN: $(System.AccessToken) + - bash: | set -euo pipefail # The impact mode is fixed by group class at emit time -- never probed from a @@ -1121,8 +1072,7 @@ steps: just anvil-scheduled-advisories displayName: anvil-scheduled-advisories env: - # Resolved by the preceding step; empty only outside PR builds. - PR_TITLE: $(PR_TITLE) + # Disable cargo incremental compilation in CI: the incremental dir is # not part of the anvil-setup cache (see setup.yml), so generating it is # pure cost with no cross-run benefit. Mirrors the GitHub impl workflows' @@ -1148,33 +1098,7 @@ steps: - template: setup.yml parameters: group: scheduled-exhaustive - # ADO has no PR-title predefined variable: `System.PullRequest.Title` - # does NOT exist, so `$(System.PullRequest.Title)` would expand to the - # literal unexpanded macro text (non-empty) and make anvil-pr-title - # validate that string. Resolve the real title from the REST API on PR - # builds and publish it as the PR_TITLE pipeline variable. Only - # anvil-pr-title (in pr-fast) consults it; other groups ignore it, but we - # resolve uniformly to keep group.yml the same across groups. Non-PR builds - # skip; a known PR whose metadata cannot be retrieved fails closed. - - pwsh: | - $ErrorActionPreference = 'Stop' - $prId = $env:SYSTEM_PULLREQUEST_PULLREQUESTID - if (-not $prId) { - Write-Host 'anvil: not a PR build; leaving PR_TITLE empty' - Write-Host '##vso[task.setvariable variable=PR_TITLE]' - exit 0 - } - $uri = "$($env:SYSTEM_COLLECTIONURI)$($env:SYSTEM_TEAMPROJECTID)/_apis/git/repositories/$($env:BUILD_REPOSITORY_ID)/pullRequests/${prId}?api-version=7.0" - try { - $resp = Invoke-RestMethod -Uri $uri -Headers @{ Authorization = "Bearer $($env:SYSTEM_ACCESSTOKEN)" } - Write-Host "##vso[task.setvariable variable=PR_TITLE]$($resp.title)" - } catch { - Write-Error "anvil: could not resolve PR title for PR $prId ($_). Ensure the job can access the repository OAuth token." - exit 1 - } - displayName: anvil-scheduled-exhaustive (resolve PR title) - env: - SYSTEM_ACCESSTOKEN: $(System.AccessToken) + - bash: | set -euo pipefail # The impact mode is fixed by group class at emit time -- never probed from a @@ -1186,8 +1110,7 @@ steps: just anvil-scheduled-exhaustive displayName: anvil-scheduled-exhaustive env: - # Resolved by the preceding step; empty only outside PR builds. - PR_TITLE: $(PR_TITLE) + # Disable cargo incremental compilation in CI: the incremental dir is # not part of the anvil-setup cache (see setup.yml), so generating it is # pure cost with no cross-run benefit. Mirrors the GitHub impl workflows' @@ -1213,33 +1136,7 @@ steps: - template: setup.yml parameters: group: scheduled-runtime-analysis - # ADO has no PR-title predefined variable: `System.PullRequest.Title` - # does NOT exist, so `$(System.PullRequest.Title)` would expand to the - # literal unexpanded macro text (non-empty) and make anvil-pr-title - # validate that string. Resolve the real title from the REST API on PR - # builds and publish it as the PR_TITLE pipeline variable. Only - # anvil-pr-title (in pr-fast) consults it; other groups ignore it, but we - # resolve uniformly to keep group.yml the same across groups. Non-PR builds - # skip; a known PR whose metadata cannot be retrieved fails closed. - - pwsh: | - $ErrorActionPreference = 'Stop' - $prId = $env:SYSTEM_PULLREQUEST_PULLREQUESTID - if (-not $prId) { - Write-Host 'anvil: not a PR build; leaving PR_TITLE empty' - Write-Host '##vso[task.setvariable variable=PR_TITLE]' - exit 0 - } - $uri = "$($env:SYSTEM_COLLECTIONURI)$($env:SYSTEM_TEAMPROJECTID)/_apis/git/repositories/$($env:BUILD_REPOSITORY_ID)/pullRequests/${prId}?api-version=7.0" - try { - $resp = Invoke-RestMethod -Uri $uri -Headers @{ Authorization = "Bearer $($env:SYSTEM_ACCESSTOKEN)" } - Write-Host "##vso[task.setvariable variable=PR_TITLE]$($resp.title)" - } catch { - Write-Error "anvil: could not resolve PR title for PR $prId ($_). Ensure the job can access the repository OAuth token." - exit 1 - } - displayName: anvil-scheduled-runtime-analysis (resolve PR title) - env: - SYSTEM_ACCESSTOKEN: $(System.AccessToken) + - bash: | set -euo pipefail # The impact mode is fixed by group class at emit time -- never probed from a @@ -1251,8 +1148,7 @@ steps: just anvil-scheduled-runtime-analysis displayName: anvil-scheduled-runtime-analysis env: - # Resolved by the preceding step; empty only outside PR builds. - PR_TITLE: $(PR_TITLE) + # Disable cargo incremental compilation in CI: the incremental dir is # not part of the anvil-setup cache (see setup.yml), so generating it is # pure cost with no cross-run benefit. Mirrors the GitHub impl workflows' @@ -1278,33 +1174,7 @@ steps: - template: setup.yml parameters: group: scheduled-test - # ADO has no PR-title predefined variable: `System.PullRequest.Title` - # does NOT exist, so `$(System.PullRequest.Title)` would expand to the - # literal unexpanded macro text (non-empty) and make anvil-pr-title - # validate that string. Resolve the real title from the REST API on PR - # builds and publish it as the PR_TITLE pipeline variable. Only - # anvil-pr-title (in pr-fast) consults it; other groups ignore it, but we - # resolve uniformly to keep group.yml the same across groups. Non-PR builds - # skip; a known PR whose metadata cannot be retrieved fails closed. - - pwsh: | - $ErrorActionPreference = 'Stop' - $prId = $env:SYSTEM_PULLREQUEST_PULLREQUESTID - if (-not $prId) { - Write-Host 'anvil: not a PR build; leaving PR_TITLE empty' - Write-Host '##vso[task.setvariable variable=PR_TITLE]' - exit 0 - } - $uri = "$($env:SYSTEM_COLLECTIONURI)$($env:SYSTEM_TEAMPROJECTID)/_apis/git/repositories/$($env:BUILD_REPOSITORY_ID)/pullRequests/${prId}?api-version=7.0" - try { - $resp = Invoke-RestMethod -Uri $uri -Headers @{ Authorization = "Bearer $($env:SYSTEM_ACCESSTOKEN)" } - Write-Host "##vso[task.setvariable variable=PR_TITLE]$($resp.title)" - } catch { - Write-Error "anvil: could not resolve PR title for PR $prId ($_). Ensure the job can access the repository OAuth token." - exit 1 - } - displayName: anvil-scheduled-test (resolve PR title) - env: - SYSTEM_ACCESSTOKEN: $(System.AccessToken) + - bash: | set -euo pipefail # The impact mode is fixed by group class at emit time -- never probed from a @@ -1316,8 +1186,7 @@ steps: just anvil-scheduled-test displayName: anvil-scheduled-test env: - # Resolved by the preceding step; empty only outside PR builds. - PR_TITLE: $(PR_TITLE) + # Disable cargo incremental compilation in CI: the incremental dir is # not part of the anvil-setup cache (see setup.yml), so generating it is # pure cost with no cross-run benefit. Mirrors the GitHub impl workflows' @@ -1335,9 +1204,9 @@ steps: # group: which anvil group to install setup for. Special values: # - "" (default): install the full catalog via # `just anvil-setup` -- use for "give me everything" flows. -# - "none": skip tool installation entirely; just bootstrap the -# rust cache + libclang + just. Used by impact.yml, which installs -# only cargo-delta afterwards. +# - "none": skip tool installation entirely; just restore the +# Cargo cache and bootstrap libclang + just. Used by impact.yml, +# which installs only cargo-delta afterwards. # - anything else (e.g. "pr-fast"): install only that group's # prerequisites via `just anvil--setup`. parameters: @@ -1345,18 +1214,30 @@ 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) + # Toolchain changes deliberately start a fresh Cargo-home cache. Besides + # bounding registry growth, this gives repositories a predictable cache + # reset point without coupling cache identity to routine manifest edits. + # Compute the digest explicitly because either supported toolchain file is + # optional and Cache@2 rejects missing file-path key segments. + - pwsh: | + $parts = @( + foreach ($name in @('rust-toolchain', 'rust-toolchain.toml')) { + if (Test-Path -LiteralPath $name -PathType Leaf) { + "$name=$((Get-FileHash -LiteralPath $name -Algorithm SHA256).Hash.ToLowerInvariant())" + } + } + ) + $fingerprint = if ($parts.Count -eq 0) { 'none' } else { $parts -join '-' } + Write-Host "##vso[task.setvariable variable=anvil_toolchain_fingerprint]$fingerprint" + displayName: anvil setup (fingerprint repository toolchain) + + # Restore Cargo's registry and installed binaries before bootstrapping Just, + # so warm ADO jobs do not compile Just again before reaching the cache. + # Rustup toolchains are not stored under Cargo home, so the selected compiler + # version does not affect this cache's contents. - task: Cache@2 inputs: - # 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' - restoreKeys: | - anvil-v1 | "$(Agent.OS)" | "$(Agent.OSArchitecture)" | rust$(anvil_rustc_version) - anvil-v1 | "$(Agent.OS)" | "$(Agent.OSArchitecture)" + key: 'anvil-v3 | "$(Agent.OS)" | "$(Agent.OSArchitecture)" | "$(anvil_toolchain_fingerprint)" | .cargo/config.toml | justfiles/anvil/versions.just' path: | $(HOME)/.cargo/registry/cache/ $(HOME)/.cargo/registry/index/ @@ -1665,7 +1546,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. @@ -1691,7 +1572,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. @@ -1719,7 +1600,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 @@ -1727,7 +1608,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")] @@ -1790,7 +1671,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 @@ -1917,7 +1798,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 @@ -1954,7 +1835,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. @@ -1989,7 +1870,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. @@ -2022,7 +1903,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. @@ -2050,7 +1931,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. @@ -2081,7 +1962,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 @@ -2090,7 +1971,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")] @@ -2119,14 +2000,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")] @@ -2149,7 +2030,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. @@ -2177,7 +2058,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. @@ -2205,12 +2086,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")] @@ -2250,7 +2131,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 @@ -2339,7 +2220,7 @@ anvil-fmt: anvil-fmt-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include modified) if ($include -eq '--skip') { Write-Host 'anvil-fmt: no modified packages; skipping'; exit 0 } - cargo each --workspace --keep-going '--' cargo '+{{ rust_nightly }}' fmt --manifest-path '{manifest}' --check + cargo {{_anvil_stable_toolchain_args}} each --workspace --keep-going '--' cargo '+{{ rust_nightly }}' fmt --manifest-path '{manifest}' --check if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Per-check setup + validate-prereqs @@ -2382,7 +2263,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. @@ -2433,7 +2314,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 @@ -2564,7 +2445,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) --- @@ -2637,7 +2518,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 @@ -2690,7 +2571,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 } } @@ -2700,7 +2581,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")] @@ -2882,6 +2763,64 @@ 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) + $impactExit = $LASTEXITCODE + if ($impactExit -ne 0) { exit $impactExit } + if ($include -eq '--skip') { Write-Host 'anvil-msrv-test: no affected packages; skipping'; exit 0 } + $toolchainOutput = & "{{ just_executable() }}" _anvil-resolve-stable msrv | Out-String + $resolverExit = $LASTEXITCODE + if ($resolverExit -ne 0) { exit $resolverExit } + $toolchain = $toolchainOutput.Trim() + if ([string]::IsNullOrWhiteSpace($toolchain)) { + Write-Host 'anvil-msrv-test: no root MSRV declared; skipping' + exit 0 + } + $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: + $toolchainOutput = & "{{ just_executable() }}" _anvil-resolve-stable msrv | Out-String + $resolverExit = $LASTEXITCODE + if ($resolverExit -ne 0) { exit $resolverExit } + $toolchain = $toolchainOutput.Trim() + if ([string]::IsNullOrWhiteSpace($toolchain)) { exit 0 } + & cargo "+$toolchain" --version + if ($LASTEXITCODE -ne 0) { + $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. @@ -2935,7 +2874,7 @@ anvil-mutants-diff: anvil-mutants-diff-validate-prereqs anvil-impact # not a behaviour change for it. git diff "$base" --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 --- @@ -2968,7 +2907,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; @@ -3106,6 +3045,7 @@ anvil-pr-title-validate-prereqs: anvil-tool-pwsh-validate-prereqs [script("pwsh", "-NoProfile")] _anvil-readme mode: anvil-readme-check-validate-prereqs $ErrorActionPreference = 'Stop' + $stableArgs = {{_anvil_stable_toolchain_args}} $action = '{{ mode }}' if ($action -notin @('generate', 'check')) { [Console]::Error.WriteLine("anvil-readme: invalid mode '$action'; expected 'generate' or 'check'") @@ -3124,7 +3064,7 @@ _anvil-readme mode: anvil-readme-check-validate-prereqs # Unscoped: a change to the workspace-level README template is a repo-level # input cargo-delta cannot map to a package, so this check always runs the # full eligible-crate set rather than an impact subset. - $metadataJson = & cargo metadata --no-deps --format-version 1 + $metadataJson = & cargo @stableArgs metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error "${prefix}: cargo metadata failed" exit $LASTEXITCODE @@ -3170,7 +3110,7 @@ _anvil-readme mode: anvil-readme-check-validate-prereqs $args = @('doc2readme', $target) if ($check) { $args += '--check' } if ($relTemplate) { $args += @('--template', $relTemplate) } - & cargo @args + & cargo @stableArgs @args if ($LASTEXITCODE -ne 0) { $hadFailure = $true } } finally { Pop-Location @@ -3241,6 +3181,7 @@ anvil-readme-check-validate-prereqs: anvil-tool-cargo-doc2readme-validate-prereq [script("pwsh", "-NoProfile")] anvil-semver-check: anvil-semver-check-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' + $stableArgs = {{_anvil_stable_toolchain_args}} $include = (& "{{ just_executable() }}" _anvil-impact-include affected) $commentFile = 'target/anvil/comments/semver.md' if ($include -eq '--skip') { @@ -3260,7 +3201,7 @@ anvil-semver-check: anvil-semver-check-validate-prereqs anvil-impact # null, `publish = false` as an empty array, and named-registry restrictions # as a nonempty array. Only the empty array is non-publishable. $libPkgs = @{} - $metadataJson = & cargo metadata --no-deps --format-version 1 + $metadataJson = & cargo @stableArgs metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-semver-check: cargo metadata failed' exit $LASTEXITCODE @@ -3338,7 +3279,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 @stableArgs semver-checks --package $p --baseline-rev $base 2>&1) $semverExit = $LASTEXITCODE $output = ($outputLines | Out-String) if ($semverExit -eq 100) { @@ -3480,9 +3421,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 } @@ -4774,6 +4715,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. @@ -4843,32 +4809,32 @@ 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). Its +# groups can be invoked individually without invoking the other groups: # -# slow1: tests + coverage (replaces the former pr-test group) -# slow2: stricter-runtime correctness (miri, careful) -# slow3: mutation testing +# pr-test: tests + coverage +# pr-msrv: all-target 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 these groups as independent parallel jobs. +# The umbrella runs them sequentially only when invoked locally: # # $ just anvil-pr-test # tests + coverage only +# $ just anvil-pr-msrv # MSRV all-target 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) @@ -4876,6 +4842,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 @@ -5462,19 +5429,24 @@ _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, use the selected rustc sysroot as an unambiguous path + # toolchain. + $stableArgs = {{_anvil_stable_toolchain_args}} + $baselineToolchain = if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { + $env:RUSTUP_TOOLCHAIN.Trim() + } elseif ($stableArgs.Count -eq 1) { + ([string] $stableArgs[0]).Substring(1) + } else { + $sysroot = @(& rustc --print sysroot) + if ($LASTEXITCODE -ne 0 -or $sysroot.Count -ne 1 -or [string]::IsNullOrWhiteSpace([string] $sysroot[0])) { + Write-Error 'anvil-impact: rustc could not report the current checkout stable toolchain sysroot' + exit 1 + } + ([string] $sysroot[0]).Trim() } # Temp-root precedence follows the runner-managed scratch dir on each @@ -5507,14 +5479,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 { @@ -5541,7 +5511,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 @@ -5659,7 +5629,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 @@ -5736,7 +5706,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 @@ -5915,6 +5885,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' @@ -5928,6 +5899,7 @@ import 'checks/udeps.just' # would break the whole Justfile. 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' @@ -6203,15 +6175,14 @@ 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. +# Validate that rustc is available and workspace MSRVs are compatible. [group("anvil-setup")] [script("pwsh", "-NoProfile")] anvil-tool-rustc-validate-prereqs: @@ -6219,6 +6190,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")] @@ -6244,7 +6219,228 @@ 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="install": + $ErrorActionPreference = 'Stop' + Set-StrictMode -Version 3 + + $action = '{{action}}' + $validActions = @( + 'install', + 'validate-workspace-msrv', + 'msrv', + 'install-msrv' + ) + if ($action -notin $validActions) { + Write-Error "_anvil-resolve-stable: unknown action '$action'" + exit 2 + } + + $repoRoot = '{{replace(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'" + } + + # This bootstrap scan must choose Cargo before Cargo is available to + # parse the manifest. Keep its workspace.package-before-package + # precedence and accepted syntax synchronized with the selector in + # versions.just. + $values = @{} + $section = '' + foreach ($line in (Get-Content -LiteralPath $manifestPath)) { + 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 Get-InstalledToolchains { + if (-not (Get-Command rustup -ErrorAction SilentlyContinue)) { + throw 'anvil: rustup not found. Install rustup from https://rustup.rs and ensure it is on PATH' + } + $installed = @(rustup toolchain list) + if ($LASTEXITCODE -ne 0) { + throw 'anvil: rustup toolchain list failed' + } + return $installed + } + + function Test-ToolchainInstalled([string] $toolchain) { + $installed = Get-InstalledToolchains + return (($installed -join "`n") -match "(?m)^$([regex]::Escape($toolchain))(?:-|$)") + } + + function Assert-WorkspaceMsrvCompatibility { + $manifestPath = Join-Path $repoRoot 'Cargo.toml' + $rootMsrv = Get-RootMsrv + if (-not (Test-ToolchainInstalled $rootMsrv)) { + throw "anvil: root MSRV toolchain '$rootMsrv' is not installed; run 'just anvil-toolchain-stable-install' before validating prerequisites" + } + $metadataText = (& cargo "+$rootMsrv" metadata --manifest-path $manifestPath --format-version 1 --no-deps 2>&1 | Out-String) + if ($LASTEXITCODE -ne 0) { + throw "anvil: cargo metadata failed while validating workspace MSRVs:`n$metadataText" + } + $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" + } + + function ConvertTo-ComparableRustVersion([string] $value) { + if ($value -notmatch '^\s*(\d+)\.(\d+)(?:\.(\d+))?\s*$') { + throw "anvil: invalid resolved rust-version '$value'" + } + # Cargo treats 1.x and 1.x.0 as the same floor, while + # System.Version sorts an unspecified build component differently. + $patch = if ([string]::IsNullOrWhiteSpace($Matches[3])) { 0 } else { [int] $Matches[3] } + return [version]::new([int] $Matches[1], [int] $Matches[2], $patch) + } + + $rootVersion = ConvertTo-ComparableRustVersion $rootMsrv + $newer = @( + $members | + Where-Object { (ConvertTo-ComparableRustVersion ([string] $_.rust_version)) -gt $rootVersion } | + 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 + Mapped = $true + } + } + return [pscustomobject]@{ Value = $msrv; Mapped = $false } + } + + function Install-Toolchain([string] $toolchain, [string] $label) { + if (-not (Test-ToolchainInstalled $toolchain)) { + Write-Host "anvil: installing $label toolchain '$toolchain'" + rustup toolchain install $toolchain --profile minimal + if ($LASTEXITCODE -ne 0) { + throw "anvil: failed to install $label toolchain '$toolchain'" + } + } + } + + if ($action -eq 'validate-workspace-msrv') { + Assert-CompleteInternalToolchainConfiguration + $skipWorkspaceMsrvValidation = -not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN) + $hasRootToolchainFile = Test-RootToolchainFile + if ($skipWorkspaceMsrvValidation -or $hasRootToolchainFile) { + Get-InstalledToolchains | Out-Null + } else { + 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.Value 'MSRV' + } + } else { + $msrvSelection.Value + } + exit 0 + } + + Assert-CompleteInternalToolchainConfiguration + if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { + Get-InstalledToolchains | Out-Null + & cargo --version *> $null + if ($LASTEXITCODE -ne 0) { + throw "anvil: selected stable toolchain '$($env:RUSTUP_TOOLCHAIN)' is unavailable; provision RUSTUP_TOOLCHAIN or unset it" + } + exit 0 + } + + if (Test-RootToolchainFile) { + Get-InstalledToolchains | Out-Null + Push-Location -LiteralPath $repoRoot + try { + & rustc --version + if ($LASTEXITCODE -ne 0) { + throw 'anvil: rustup could not provision the repository toolchain file' + } + } finally { + Pop-Location + } + exit 0 + } + + Install-Toolchain (Get-RootMsrv) 'stable' + +# Setup paths that execute stable Cargo/Rust or install Cargo tools share this +# dependency, which Just deduplicates within one setup invocation. + +# Ensure the selected stable toolchain is available while preserving native repository toolchain-file behavior. +[group("anvil-setup")] +anvil-toolchain-stable-install: (_anvil-resolve-stable "install") + +# _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. @@ -6254,7 +6450,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}}' @@ -6262,7 +6458,7 @@ _install-tool name version installer source_prereq="": $sourcePrereq = '{{source_prereq}}' if ($installer -ne 'install' -and $installer -ne 'binstall') { - Write-Error "_install-tool: unknown installer '$installer' (expected 'install' or 'binstall')" + Write-Error "_install-tool-core: unknown installer '$installer' (expected 'install' or 'binstall')" exit 2 } @@ -6274,7 +6470,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) { @@ -6292,7 +6488,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 @@ -6306,7 +6502,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 } @@ -6314,12 +6510,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")] @@ -6330,8 +6529,19 @@ _check-tool name version: $installed = $null $pattern = '^' + [regex]::Escape($name) + ' v(\S+):' - foreach ($line in (cargo install --list 2>$null)) { - if ($line -match $pattern) { $installed = $Matches[1]; break } + $previousAutoInstall = $env:RUSTUP_AUTO_INSTALL + try { + # Validation must not let the rustup proxy provision a missing selection. + $env:RUSTUP_AUTO_INSTALL = '0' + foreach ($line in (& cargo {{_anvil_stable_toolchain_args}} install --list 2>$null)) { + if ($line -match $pattern) { $installed = $Matches[1]; break } + } + } finally { + if ($null -eq $previousAutoInstall) { + Remove-Item Env:RUSTUP_AUTO_INSTALL -ErrorAction SilentlyContinue + } else { + $env:RUSTUP_AUTO_INSTALL = $previousAutoInstall + } } if (-not $installed) { Write-Error "anvil: required tool '$name' not found. Install: cargo install --locked --version =$version $name" @@ -6381,11 +6591,12 @@ _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 +# MSRV fallback selections target `rustup component add --toolchain`; native +# rustup selections use `rustup component add` without restating the selector. # # Probe-first: if the component is already available, skip the # `rustup component add`. This keeps the recipe idempotent and robust @@ -6399,6 +6610,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` @@ -6411,14 +6630,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 } @@ -6441,8 +6668,11 @@ _install-component toolchain component: exit 0 } - if ($toolchain -eq 'default') { - Write-Host "rustup component add $component (default toolchain)" + if ($toolchain -eq 'default' -and $null -ne $selectedStableToolchain) { + Write-Host "rustup component add --toolchain $selectedStableToolchain $component" + rustup component add --toolchain $selectedStableToolchain $component + } elseif ($toolchain -eq 'default') { + Write-Host "rustup component add $component" rustup component add $component } else { Write-Host "rustup component add --toolchain $toolchain $component" @@ -6471,6 +6701,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' } @@ -6479,14 +6717,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 } @@ -6508,7 +6754,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 } @@ -6537,14 +6789,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")] @@ -6552,7 +6803,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")] @@ -6639,12 +6890,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. @@ -6764,12 +7015,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. @@ -6831,6 +7082,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: @@ -6856,6 +7108,61 @@ 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)) { + return +} + +if ((Test-Path -LiteralPath (Join-Path $RepoRoot 'rust-toolchain') -PathType Leaf) -or + (Test-Path -LiteralPath (Join-Path $RepoRoot 'rust-toolchain.toml') -PathType Leaf)) { + 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'" +} + +# This bootstrap scan must choose Cargo before Cargo is available to parse the +# manifest. Keep its workspace.package-before-package precedence and accepted +# syntax synchronized with Get-RootMsrv in tools.just. +$values = @{} +$section = '' +foreach ($line in (Get-Content -LiteralPath $manifestPath)) { + 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 eb25c24da..45d27cafe 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap @@ -77,7 +77,6 @@ RUN curl -fsSLo /tmp/cargo-binstall.tgz \ && tar -xzf /tmp/cargo-binstall.tgz -C "${CARGO_HOME}/bin" cargo-binstall \ && chmod 755 "${CARGO_HOME}/bin/cargo-binstall" \ && rm /tmp/cargo-binstall.tgz - # <<< anvil-managed: anvil-container-tools # >>> anvil-managed: anvil-container-setup @@ -506,8 +505,8 @@ inputs: - "" (default): install the full catalog via `just anvil-setup` -- use for local "give me everything" flows. - - "none": skip tool installation entirely; just bootstrap the - rust toolchain + just + binstall + cache. Used by + - "none": skip tool installation entirely; just restore the + Cargo cache and bootstrap just + binstall. Used by anvil-impact, which installs only cargo-delta afterwards. - a name containing only lowercase letters, digits, and hyphens: install only that group's prerequisites via @@ -570,48 +569,29 @@ runs: } "Free after cleanup (GiB): $(Get-FreeDiskGiB)" - - id: rustc-version - shell: bash - run: echo "version=$(rustc --version | awk '{print $2}')" >> "$GITHUB_OUTPUT" + # Restore before bootstrapping Just so a warm job can reuse the cached + # executable. Repository toolchain files intentionally define a fresh + # cache generation, bounding registry growth without invalidating on + # routine Cargo.toml or Cargo.lock edits. - name: Restore cargo cache id: cargo-cache uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: - # Key on OS + arch + rustc version + lockfile hashes + catalog - # hash so a Rust toolchain bump, a cross-arch matrix leg, or a - # tool-versions update all invalidate the cache cleanly. - # runner.arch resolves to X64 / ARM64 / X86, which keeps the - # x86_64 and aarch64 legs of the same OS from colliding on - # arch-incompatible cached binaries in ~/.cargo/bin. - # - # ${{ github.job }} discriminates by workflow-job-id (`pr-fast`, - # `pr-test`, `pr-slow`, etc.) so concurrent matrix legs that - # share OS+arch (e.g. pr-fast linux + pr-test linux) don't race - # on the same cache key. Without this, the first-to-finish job - # reserves the key, the others get "Unable to reserve cache: - # another job may be creating this cache" and silently skip - # the save -- leaving the cache empty forever. The restore-keys - # fall back across jobs so the install work is still shared - # across sibling legs on subsequent runs. - key: anvil-v1-${{ runner.os }}-${{ runner.arch }}-rust${{ steps.rustc-version.outputs.version }}-${{ hashFiles('Cargo.lock', '.cargo/config.toml', 'rust-toolchain.toml', 'justfiles/anvil/versions.just') }}-${{ github.job }} + # The job suffix prevents concurrent matrix jobs from racing to save + # one key. The prefix shares a completed cache across sibling jobs. + # There is deliberately no fallback across toolchain/catalog changes: + # those inputs are the cache-pruning boundary. + key: anvil-v2-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('.cargo/config.toml', 'rust-toolchain', 'rust-toolchain.toml', 'justfiles/anvil/versions.just') }}-${{ github.job }} restore-keys: | - anvil-v1-${{ runner.os }}-${{ runner.arch }}-rust${{ steps.rustc-version.outputs.version }}-${{ hashFiles('Cargo.lock', '.cargo/config.toml', 'rust-toolchain.toml', 'justfiles/anvil/versions.just') }}- - anvil-v1-${{ runner.os }}-${{ runner.arch }}-rust${{ steps.rustc-version.outputs.version }}- - anvil-v1-${{ runner.os }}-${{ runner.arch }}- + anvil-v2-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('.cargo/config.toml', 'rust-toolchain', 'rust-toolchain.toml', 'justfiles/anvil/versions.just') }}- # `.crates.toml` and `.crates2.json` track which cargo-installed # tools and versions live in ~/.cargo/bin/. Without them in the # cache, `cargo install --list` and (downstream) the # tool-install recipes' early-skip on "installed >= pin" don't - # see the cached binaries — every run then tries to reinstall - # on top of them and fails with "binary X already exists in - # destination". + # see the cached binaries. # - # target/ (the compilation output dir) is deliberately NOT cached here: - # per the GitHub backend design's caching policy, multi-GB per-job - # target/ trees dwarf the high-value tool cache under the repo cache - # quota, give only modest recompile savings once tools are cached, and - # could overwrite the downloaded target/anvil/impact/ cache. Only the - # ~/.cargo tool + registry state below is cached. + # target/ is deliberately excluded: per-job target trees dwarf the + # tool cache and could overwrite the downloaded impact cache. path: | ~/.cargo/registry/cache/ ~/.cargo/registry/index/ @@ -619,31 +599,7 @@ 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 - # 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. + # cargo-binstall keeps the cold bootstrap path fast. - name: Install cargo-binstall uses: cargo-bins/cargo-binstall@v1.21.0 # immutable release, the tag cannot be moved @@ -654,6 +610,11 @@ runs: cargo binstall --no-confirm --locked just || cargo install --locked just fi + # The catalog recipe `anvil-setup` (or `anvil--setup` + # when a group is specified via the `group` input) uses the Just executable + # bootstrapped above, then handles everything else. The setup recipes are idempotent and + # short-circuit on tools already installed at or above the pinned + # version, so re-running on cache-hit runs is cheap. - name: Install anvil toolchains + tools shell: bash # Carry action data through the environment instead of interpolating it @@ -957,6 +918,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. @@ -1544,7 +1532,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. @@ -1570,7 +1558,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. @@ -1598,7 +1586,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 @@ -1606,7 +1594,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")] @@ -1669,7 +1657,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 @@ -1796,7 +1784,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 @@ -1833,7 +1821,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. @@ -1868,7 +1856,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. @@ -1901,7 +1889,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. @@ -1929,7 +1917,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. @@ -1960,7 +1948,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 @@ -1969,7 +1957,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")] @@ -1998,14 +1986,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")] @@ -2028,7 +2016,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. @@ -2056,7 +2044,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. @@ -2084,12 +2072,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")] @@ -2129,7 +2117,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 @@ -2218,7 +2206,7 @@ anvil-fmt: anvil-fmt-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include modified) if ($include -eq '--skip') { Write-Host 'anvil-fmt: no modified packages; skipping'; exit 0 } - cargo each --workspace --keep-going '--' cargo '+{{ rust_nightly }}' fmt --manifest-path '{manifest}' --check + cargo {{_anvil_stable_toolchain_args}} each --workspace --keep-going '--' cargo '+{{ rust_nightly }}' fmt --manifest-path '{manifest}' --check if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Per-check setup + validate-prereqs @@ -2261,7 +2249,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. @@ -2312,7 +2300,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 @@ -2443,7 +2431,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) --- @@ -2516,7 +2504,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 @@ -2569,7 +2557,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 } } @@ -2579,7 +2567,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")] @@ -2761,6 +2749,64 @@ 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) + $impactExit = $LASTEXITCODE + if ($impactExit -ne 0) { exit $impactExit } + if ($include -eq '--skip') { Write-Host 'anvil-msrv-test: no affected packages; skipping'; exit 0 } + $toolchainOutput = & "{{ just_executable() }}" _anvil-resolve-stable msrv | Out-String + $resolverExit = $LASTEXITCODE + if ($resolverExit -ne 0) { exit $resolverExit } + $toolchain = $toolchainOutput.Trim() + if ([string]::IsNullOrWhiteSpace($toolchain)) { + Write-Host 'anvil-msrv-test: no root MSRV declared; skipping' + exit 0 + } + $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: + $toolchainOutput = & "{{ just_executable() }}" _anvil-resolve-stable msrv | Out-String + $resolverExit = $LASTEXITCODE + if ($resolverExit -ne 0) { exit $resolverExit } + $toolchain = $toolchainOutput.Trim() + if ([string]::IsNullOrWhiteSpace($toolchain)) { exit 0 } + & cargo "+$toolchain" --version + if ($LASTEXITCODE -ne 0) { + $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. @@ -2814,7 +2860,7 @@ anvil-mutants-diff: anvil-mutants-diff-validate-prereqs anvil-impact # not a behaviour change for it. git diff "$base" --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 --- @@ -2847,7 +2893,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; @@ -2985,6 +3031,7 @@ anvil-pr-title-validate-prereqs: anvil-tool-pwsh-validate-prereqs [script("pwsh", "-NoProfile")] _anvil-readme mode: anvil-readme-check-validate-prereqs $ErrorActionPreference = 'Stop' + $stableArgs = {{_anvil_stable_toolchain_args}} $action = '{{ mode }}' if ($action -notin @('generate', 'check')) { [Console]::Error.WriteLine("anvil-readme: invalid mode '$action'; expected 'generate' or 'check'") @@ -3003,7 +3050,7 @@ _anvil-readme mode: anvil-readme-check-validate-prereqs # Unscoped: a change to the workspace-level README template is a repo-level # input cargo-delta cannot map to a package, so this check always runs the # full eligible-crate set rather than an impact subset. - $metadataJson = & cargo metadata --no-deps --format-version 1 + $metadataJson = & cargo @stableArgs metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error "${prefix}: cargo metadata failed" exit $LASTEXITCODE @@ -3049,7 +3096,7 @@ _anvil-readme mode: anvil-readme-check-validate-prereqs $args = @('doc2readme', $target) if ($check) { $args += '--check' } if ($relTemplate) { $args += @('--template', $relTemplate) } - & cargo @args + & cargo @stableArgs @args if ($LASTEXITCODE -ne 0) { $hadFailure = $true } } finally { Pop-Location @@ -3120,6 +3167,7 @@ anvil-readme-check-validate-prereqs: anvil-tool-cargo-doc2readme-validate-prereq [script("pwsh", "-NoProfile")] anvil-semver-check: anvil-semver-check-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' + $stableArgs = {{_anvil_stable_toolchain_args}} $include = (& "{{ just_executable() }}" _anvil-impact-include affected) $commentFile = 'target/anvil/comments/semver.md' if ($include -eq '--skip') { @@ -3139,7 +3187,7 @@ anvil-semver-check: anvil-semver-check-validate-prereqs anvil-impact # null, `publish = false` as an empty array, and named-registry restrictions # as a nonempty array. Only the empty array is non-publishable. $libPkgs = @{} - $metadataJson = & cargo metadata --no-deps --format-version 1 + $metadataJson = & cargo @stableArgs metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-semver-check: cargo metadata failed' exit $LASTEXITCODE @@ -3217,7 +3265,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 @stableArgs semver-checks --package $p --baseline-rev $base 2>&1) $semverExit = $LASTEXITCODE $output = ($outputLines | Out-String) if ($semverExit -eq 100) { @@ -3359,9 +3407,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 } @@ -4653,6 +4701,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. @@ -4722,32 +4795,32 @@ 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). Its +# groups can be invoked individually without invoking the other groups: # -# slow1: tests + coverage (replaces the former pr-test group) -# slow2: stricter-runtime correctness (miri, careful) -# slow3: mutation testing +# pr-test: tests + coverage +# pr-msrv: all-target 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 these groups as independent parallel jobs. +# The umbrella runs them sequentially only when invoked locally: # # $ just anvil-pr-test # tests + coverage only +# $ just anvil-pr-msrv # MSRV all-target 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) @@ -4755,6 +4828,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 @@ -5341,19 +5415,24 @@ _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, use the selected rustc sysroot as an unambiguous path + # toolchain. + $stableArgs = {{_anvil_stable_toolchain_args}} + $baselineToolchain = if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { + $env:RUSTUP_TOOLCHAIN.Trim() + } elseif ($stableArgs.Count -eq 1) { + ([string] $stableArgs[0]).Substring(1) + } else { + $sysroot = @(& rustc --print sysroot) + if ($LASTEXITCODE -ne 0 -or $sysroot.Count -ne 1 -or [string]::IsNullOrWhiteSpace([string] $sysroot[0])) { + Write-Error 'anvil-impact: rustc could not report the current checkout stable toolchain sysroot' + exit 1 + } + ([string] $sysroot[0]).Trim() } # Temp-root precedence follows the runner-managed scratch dir on each @@ -5386,14 +5465,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 { @@ -5420,7 +5497,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 @@ -5538,7 +5615,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 @@ -5615,7 +5692,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 @@ -5794,6 +5871,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' @@ -5807,6 +5885,7 @@ import 'checks/udeps.just' # would break the whole Justfile. 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' @@ -6082,15 +6161,14 @@ 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. +# Validate that rustc is available and workspace MSRVs are compatible. [group("anvil-setup")] [script("pwsh", "-NoProfile")] anvil-tool-rustc-validate-prereqs: @@ -6098,6 +6176,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")] @@ -6123,7 +6205,228 @@ 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="install": + $ErrorActionPreference = 'Stop' + Set-StrictMode -Version 3 + + $action = '{{action}}' + $validActions = @( + 'install', + 'validate-workspace-msrv', + 'msrv', + 'install-msrv' + ) + if ($action -notin $validActions) { + Write-Error "_anvil-resolve-stable: unknown action '$action'" + exit 2 + } + + $repoRoot = '{{replace(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'" + } + + # This bootstrap scan must choose Cargo before Cargo is available to + # parse the manifest. Keep its workspace.package-before-package + # precedence and accepted syntax synchronized with the selector in + # versions.just. + $values = @{} + $section = '' + foreach ($line in (Get-Content -LiteralPath $manifestPath)) { + 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 Get-InstalledToolchains { + if (-not (Get-Command rustup -ErrorAction SilentlyContinue)) { + throw 'anvil: rustup not found. Install rustup from https://rustup.rs and ensure it is on PATH' + } + $installed = @(rustup toolchain list) + if ($LASTEXITCODE -ne 0) { + throw 'anvil: rustup toolchain list failed' + } + return $installed + } + + function Test-ToolchainInstalled([string] $toolchain) { + $installed = Get-InstalledToolchains + return (($installed -join "`n") -match "(?m)^$([regex]::Escape($toolchain))(?:-|$)") + } + + function Assert-WorkspaceMsrvCompatibility { + $manifestPath = Join-Path $repoRoot 'Cargo.toml' + $rootMsrv = Get-RootMsrv + if (-not (Test-ToolchainInstalled $rootMsrv)) { + throw "anvil: root MSRV toolchain '$rootMsrv' is not installed; run 'just anvil-toolchain-stable-install' before validating prerequisites" + } + $metadataText = (& cargo "+$rootMsrv" metadata --manifest-path $manifestPath --format-version 1 --no-deps 2>&1 | Out-String) + if ($LASTEXITCODE -ne 0) { + throw "anvil: cargo metadata failed while validating workspace MSRVs:`n$metadataText" + } + $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" + } + + function ConvertTo-ComparableRustVersion([string] $value) { + if ($value -notmatch '^\s*(\d+)\.(\d+)(?:\.(\d+))?\s*$') { + throw "anvil: invalid resolved rust-version '$value'" + } + # Cargo treats 1.x and 1.x.0 as the same floor, while + # System.Version sorts an unspecified build component differently. + $patch = if ([string]::IsNullOrWhiteSpace($Matches[3])) { 0 } else { [int] $Matches[3] } + return [version]::new([int] $Matches[1], [int] $Matches[2], $patch) + } + + $rootVersion = ConvertTo-ComparableRustVersion $rootMsrv + $newer = @( + $members | + Where-Object { (ConvertTo-ComparableRustVersion ([string] $_.rust_version)) -gt $rootVersion } | + 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 + Mapped = $true + } + } + return [pscustomobject]@{ Value = $msrv; Mapped = $false } + } + + function Install-Toolchain([string] $toolchain, [string] $label) { + if (-not (Test-ToolchainInstalled $toolchain)) { + Write-Host "anvil: installing $label toolchain '$toolchain'" + rustup toolchain install $toolchain --profile minimal + if ($LASTEXITCODE -ne 0) { + throw "anvil: failed to install $label toolchain '$toolchain'" + } + } + } + + if ($action -eq 'validate-workspace-msrv') { + Assert-CompleteInternalToolchainConfiguration + $skipWorkspaceMsrvValidation = -not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN) + $hasRootToolchainFile = Test-RootToolchainFile + if ($skipWorkspaceMsrvValidation -or $hasRootToolchainFile) { + Get-InstalledToolchains | Out-Null + } else { + 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.Value 'MSRV' + } + } else { + $msrvSelection.Value + } + exit 0 + } + + Assert-CompleteInternalToolchainConfiguration + if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { + Get-InstalledToolchains | Out-Null + & cargo --version *> $null + if ($LASTEXITCODE -ne 0) { + throw "anvil: selected stable toolchain '$($env:RUSTUP_TOOLCHAIN)' is unavailable; provision RUSTUP_TOOLCHAIN or unset it" + } + exit 0 + } + + if (Test-RootToolchainFile) { + Get-InstalledToolchains | Out-Null + Push-Location -LiteralPath $repoRoot + try { + & rustc --version + if ($LASTEXITCODE -ne 0) { + throw 'anvil: rustup could not provision the repository toolchain file' + } + } finally { + Pop-Location + } + exit 0 + } + + Install-Toolchain (Get-RootMsrv) 'stable' + +# Setup paths that execute stable Cargo/Rust or install Cargo tools share this +# dependency, which Just deduplicates within one setup invocation. + +# Ensure the selected stable toolchain is available while preserving native repository toolchain-file behavior. +[group("anvil-setup")] +anvil-toolchain-stable-install: (_anvil-resolve-stable "install") + +# _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. @@ -6133,7 +6436,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}}' @@ -6141,7 +6444,7 @@ _install-tool name version installer source_prereq="": $sourcePrereq = '{{source_prereq}}' if ($installer -ne 'install' -and $installer -ne 'binstall') { - Write-Error "_install-tool: unknown installer '$installer' (expected 'install' or 'binstall')" + Write-Error "_install-tool-core: unknown installer '$installer' (expected 'install' or 'binstall')" exit 2 } @@ -6153,7 +6456,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) { @@ -6171,7 +6474,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 @@ -6185,7 +6488,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 } @@ -6193,12 +6496,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")] @@ -6209,8 +6515,19 @@ _check-tool name version: $installed = $null $pattern = '^' + [regex]::Escape($name) + ' v(\S+):' - foreach ($line in (cargo install --list 2>$null)) { - if ($line -match $pattern) { $installed = $Matches[1]; break } + $previousAutoInstall = $env:RUSTUP_AUTO_INSTALL + try { + # Validation must not let the rustup proxy provision a missing selection. + $env:RUSTUP_AUTO_INSTALL = '0' + foreach ($line in (& cargo {{_anvil_stable_toolchain_args}} install --list 2>$null)) { + if ($line -match $pattern) { $installed = $Matches[1]; break } + } + } finally { + if ($null -eq $previousAutoInstall) { + Remove-Item Env:RUSTUP_AUTO_INSTALL -ErrorAction SilentlyContinue + } else { + $env:RUSTUP_AUTO_INSTALL = $previousAutoInstall + } } if (-not $installed) { Write-Error "anvil: required tool '$name' not found. Install: cargo install --locked --version =$version $name" @@ -6260,11 +6577,12 @@ _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 +# MSRV fallback selections target `rustup component add --toolchain`; native +# rustup selections use `rustup component add` without restating the selector. # # Probe-first: if the component is already available, skip the # `rustup component add`. This keeps the recipe idempotent and robust @@ -6278,6 +6596,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` @@ -6290,14 +6616,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 } @@ -6320,8 +6654,11 @@ _install-component toolchain component: exit 0 } - if ($toolchain -eq 'default') { - Write-Host "rustup component add $component (default toolchain)" + if ($toolchain -eq 'default' -and $null -ne $selectedStableToolchain) { + Write-Host "rustup component add --toolchain $selectedStableToolchain $component" + rustup component add --toolchain $selectedStableToolchain $component + } elseif ($toolchain -eq 'default') { + Write-Host "rustup component add $component" rustup component add $component } else { Write-Host "rustup component add --toolchain $toolchain $component" @@ -6350,6 +6687,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' } @@ -6358,14 +6703,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 } @@ -6387,7 +6740,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 } @@ -6416,14 +6775,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")] @@ -6431,7 +6789,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")] @@ -6518,12 +6876,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. @@ -6643,12 +7001,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. @@ -6710,6 +7068,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: @@ -6735,6 +7094,61 @@ 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)) { + return +} + +if ((Test-Path -LiteralPath (Join-Path $RepoRoot 'rust-toolchain') -PathType Leaf) -or + (Test-Path -LiteralPath (Join-Path $RepoRoot 'rust-toolchain.toml') -PathType Leaf)) { + 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'" +} + +# This bootstrap scan must choose Cargo before Cargo is available to parse the +# manifest. Keep its workspace.package-before-package precedence and accepted +# syntax synchronized with Get-RootMsrv in tools.just. +$values = @{} +$section = '' +foreach ($line in (Get-Content -LiteralPath $manifestPath)) { + 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 75319a8fe..dd2197ec7 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap @@ -77,7 +77,6 @@ RUN curl -fsSLo /tmp/cargo-binstall.tgz \ && tar -xzf /tmp/cargo-binstall.tgz -C "${CARGO_HOME}/bin" cargo-binstall \ && chmod 755 "${CARGO_HOME}/bin/cargo-binstall" \ && rm /tmp/cargo-binstall.tgz - # <<< anvil-managed: anvil-container-tools # >>> anvil-managed: anvil-container-setup @@ -417,7 +416,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. @@ -443,7 +442,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. @@ -471,7 +470,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 @@ -479,7 +478,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")] @@ -542,7 +541,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 @@ -669,7 +668,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 @@ -706,7 +705,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. @@ -741,7 +740,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. @@ -774,7 +773,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. @@ -802,7 +801,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. @@ -833,7 +832,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 @@ -842,7 +841,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")] @@ -871,14 +870,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")] @@ -901,7 +900,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. @@ -929,7 +928,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. @@ -957,12 +956,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")] @@ -1002,7 +1001,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 @@ -1091,7 +1090,7 @@ anvil-fmt: anvil-fmt-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include modified) if ($include -eq '--skip') { Write-Host 'anvil-fmt: no modified packages; skipping'; exit 0 } - cargo each --workspace --keep-going '--' cargo '+{{ rust_nightly }}' fmt --manifest-path '{manifest}' --check + cargo {{_anvil_stable_toolchain_args}} each --workspace --keep-going '--' cargo '+{{ rust_nightly }}' fmt --manifest-path '{manifest}' --check if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Per-check setup + validate-prereqs @@ -1134,7 +1133,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. @@ -1185,7 +1184,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 @@ -1316,7 +1315,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) --- @@ -1389,7 +1388,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 @@ -1442,7 +1441,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 } } @@ -1452,7 +1451,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")] @@ -1634,6 +1633,64 @@ 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) + $impactExit = $LASTEXITCODE + if ($impactExit -ne 0) { exit $impactExit } + if ($include -eq '--skip') { Write-Host 'anvil-msrv-test: no affected packages; skipping'; exit 0 } + $toolchainOutput = & "{{ just_executable() }}" _anvil-resolve-stable msrv | Out-String + $resolverExit = $LASTEXITCODE + if ($resolverExit -ne 0) { exit $resolverExit } + $toolchain = $toolchainOutput.Trim() + if ([string]::IsNullOrWhiteSpace($toolchain)) { + Write-Host 'anvil-msrv-test: no root MSRV declared; skipping' + exit 0 + } + $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: + $toolchainOutput = & "{{ just_executable() }}" _anvil-resolve-stable msrv | Out-String + $resolverExit = $LASTEXITCODE + if ($resolverExit -ne 0) { exit $resolverExit } + $toolchain = $toolchainOutput.Trim() + if ([string]::IsNullOrWhiteSpace($toolchain)) { exit 0 } + & cargo "+$toolchain" --version + if ($LASTEXITCODE -ne 0) { + $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. @@ -1687,7 +1744,7 @@ anvil-mutants-diff: anvil-mutants-diff-validate-prereqs anvil-impact # not a behaviour change for it. git diff "$base" --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 --- @@ -1720,7 +1777,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; @@ -1858,6 +1915,7 @@ anvil-pr-title-validate-prereqs: anvil-tool-pwsh-validate-prereqs [script("pwsh", "-NoProfile")] _anvil-readme mode: anvil-readme-check-validate-prereqs $ErrorActionPreference = 'Stop' + $stableArgs = {{_anvil_stable_toolchain_args}} $action = '{{ mode }}' if ($action -notin @('generate', 'check')) { [Console]::Error.WriteLine("anvil-readme: invalid mode '$action'; expected 'generate' or 'check'") @@ -1876,7 +1934,7 @@ _anvil-readme mode: anvil-readme-check-validate-prereqs # Unscoped: a change to the workspace-level README template is a repo-level # input cargo-delta cannot map to a package, so this check always runs the # full eligible-crate set rather than an impact subset. - $metadataJson = & cargo metadata --no-deps --format-version 1 + $metadataJson = & cargo @stableArgs metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error "${prefix}: cargo metadata failed" exit $LASTEXITCODE @@ -1922,7 +1980,7 @@ _anvil-readme mode: anvil-readme-check-validate-prereqs $args = @('doc2readme', $target) if ($check) { $args += '--check' } if ($relTemplate) { $args += @('--template', $relTemplate) } - & cargo @args + & cargo @stableArgs @args if ($LASTEXITCODE -ne 0) { $hadFailure = $true } } finally { Pop-Location @@ -1993,6 +2051,7 @@ anvil-readme-check-validate-prereqs: anvil-tool-cargo-doc2readme-validate-prereq [script("pwsh", "-NoProfile")] anvil-semver-check: anvil-semver-check-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' + $stableArgs = {{_anvil_stable_toolchain_args}} $include = (& "{{ just_executable() }}" _anvil-impact-include affected) $commentFile = 'target/anvil/comments/semver.md' if ($include -eq '--skip') { @@ -2012,7 +2071,7 @@ anvil-semver-check: anvil-semver-check-validate-prereqs anvil-impact # null, `publish = false` as an empty array, and named-registry restrictions # as a nonempty array. Only the empty array is non-publishable. $libPkgs = @{} - $metadataJson = & cargo metadata --no-deps --format-version 1 + $metadataJson = & cargo @stableArgs metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-semver-check: cargo metadata failed' exit $LASTEXITCODE @@ -2090,7 +2149,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 @stableArgs semver-checks --package $p --baseline-rev $base 2>&1) $semverExit = $LASTEXITCODE $output = ($outputLines | Out-String) if ($semverExit -eq 100) { @@ -2232,9 +2291,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 } @@ -3526,6 +3585,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. @@ -3595,32 +3679,32 @@ 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). Its +# groups can be invoked individually without invoking the other groups: # -# slow1: tests + coverage (replaces the former pr-test group) -# slow2: stricter-runtime correctness (miri, careful) -# slow3: mutation testing +# pr-test: tests + coverage +# pr-msrv: all-target 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 these groups as independent parallel jobs. +# The umbrella runs them sequentially only when invoked locally: # # $ just anvil-pr-test # tests + coverage only +# $ just anvil-pr-msrv # MSRV all-target 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) @@ -3628,6 +3712,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 @@ -4214,19 +4299,24 @@ _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, use the selected rustc sysroot as an unambiguous path + # toolchain. + $stableArgs = {{_anvil_stable_toolchain_args}} + $baselineToolchain = if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { + $env:RUSTUP_TOOLCHAIN.Trim() + } elseif ($stableArgs.Count -eq 1) { + ([string] $stableArgs[0]).Substring(1) + } else { + $sysroot = @(& rustc --print sysroot) + if ($LASTEXITCODE -ne 0 -or $sysroot.Count -ne 1 -or [string]::IsNullOrWhiteSpace([string] $sysroot[0])) { + Write-Error 'anvil-impact: rustc could not report the current checkout stable toolchain sysroot' + exit 1 + } + ([string] $sysroot[0]).Trim() } # Temp-root precedence follows the runner-managed scratch dir on each @@ -4259,14 +4349,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 { @@ -4293,7 +4381,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 @@ -4411,7 +4499,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 @@ -4488,7 +4576,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 @@ -4667,6 +4755,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' @@ -4680,6 +4769,7 @@ import 'checks/udeps.just' # would break the whole Justfile. 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' @@ -4955,15 +5045,14 @@ 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. +# Validate that rustc is available and workspace MSRVs are compatible. [group("anvil-setup")] [script("pwsh", "-NoProfile")] anvil-tool-rustc-validate-prereqs: @@ -4971,6 +5060,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")] @@ -4996,7 +5089,228 @@ 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="install": + $ErrorActionPreference = 'Stop' + Set-StrictMode -Version 3 + + $action = '{{action}}' + $validActions = @( + 'install', + 'validate-workspace-msrv', + 'msrv', + 'install-msrv' + ) + if ($action -notin $validActions) { + Write-Error "_anvil-resolve-stable: unknown action '$action'" + exit 2 + } + + $repoRoot = '{{replace(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'" + } + + # This bootstrap scan must choose Cargo before Cargo is available to + # parse the manifest. Keep its workspace.package-before-package + # precedence and accepted syntax synchronized with the selector in + # versions.just. + $values = @{} + $section = '' + foreach ($line in (Get-Content -LiteralPath $manifestPath)) { + 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 Get-InstalledToolchains { + if (-not (Get-Command rustup -ErrorAction SilentlyContinue)) { + throw 'anvil: rustup not found. Install rustup from https://rustup.rs and ensure it is on PATH' + } + $installed = @(rustup toolchain list) + if ($LASTEXITCODE -ne 0) { + throw 'anvil: rustup toolchain list failed' + } + return $installed + } + + function Test-ToolchainInstalled([string] $toolchain) { + $installed = Get-InstalledToolchains + return (($installed -join "`n") -match "(?m)^$([regex]::Escape($toolchain))(?:-|$)") + } + + function Assert-WorkspaceMsrvCompatibility { + $manifestPath = Join-Path $repoRoot 'Cargo.toml' + $rootMsrv = Get-RootMsrv + if (-not (Test-ToolchainInstalled $rootMsrv)) { + throw "anvil: root MSRV toolchain '$rootMsrv' is not installed; run 'just anvil-toolchain-stable-install' before validating prerequisites" + } + $metadataText = (& cargo "+$rootMsrv" metadata --manifest-path $manifestPath --format-version 1 --no-deps 2>&1 | Out-String) + if ($LASTEXITCODE -ne 0) { + throw "anvil: cargo metadata failed while validating workspace MSRVs:`n$metadataText" + } + $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" + } + + function ConvertTo-ComparableRustVersion([string] $value) { + if ($value -notmatch '^\s*(\d+)\.(\d+)(?:\.(\d+))?\s*$') { + throw "anvil: invalid resolved rust-version '$value'" + } + # Cargo treats 1.x and 1.x.0 as the same floor, while + # System.Version sorts an unspecified build component differently. + $patch = if ([string]::IsNullOrWhiteSpace($Matches[3])) { 0 } else { [int] $Matches[3] } + return [version]::new([int] $Matches[1], [int] $Matches[2], $patch) + } + + $rootVersion = ConvertTo-ComparableRustVersion $rootMsrv + $newer = @( + $members | + Where-Object { (ConvertTo-ComparableRustVersion ([string] $_.rust_version)) -gt $rootVersion } | + 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 + Mapped = $true + } + } + return [pscustomobject]@{ Value = $msrv; Mapped = $false } + } + + function Install-Toolchain([string] $toolchain, [string] $label) { + if (-not (Test-ToolchainInstalled $toolchain)) { + Write-Host "anvil: installing $label toolchain '$toolchain'" + rustup toolchain install $toolchain --profile minimal + if ($LASTEXITCODE -ne 0) { + throw "anvil: failed to install $label toolchain '$toolchain'" + } + } + } + + if ($action -eq 'validate-workspace-msrv') { + Assert-CompleteInternalToolchainConfiguration + $skipWorkspaceMsrvValidation = -not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN) + $hasRootToolchainFile = Test-RootToolchainFile + if ($skipWorkspaceMsrvValidation -or $hasRootToolchainFile) { + Get-InstalledToolchains | Out-Null + } else { + 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.Value 'MSRV' + } + } else { + $msrvSelection.Value + } + exit 0 + } + + Assert-CompleteInternalToolchainConfiguration + if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { + Get-InstalledToolchains | Out-Null + & cargo --version *> $null + if ($LASTEXITCODE -ne 0) { + throw "anvil: selected stable toolchain '$($env:RUSTUP_TOOLCHAIN)' is unavailable; provision RUSTUP_TOOLCHAIN or unset it" + } + exit 0 + } + + if (Test-RootToolchainFile) { + Get-InstalledToolchains | Out-Null + Push-Location -LiteralPath $repoRoot + try { + & rustc --version + if ($LASTEXITCODE -ne 0) { + throw 'anvil: rustup could not provision the repository toolchain file' + } + } finally { + Pop-Location + } + exit 0 + } + + Install-Toolchain (Get-RootMsrv) 'stable' + +# Setup paths that execute stable Cargo/Rust or install Cargo tools share this +# dependency, which Just deduplicates within one setup invocation. + +# Ensure the selected stable toolchain is available while preserving native repository toolchain-file behavior. +[group("anvil-setup")] +anvil-toolchain-stable-install: (_anvil-resolve-stable "install") + +# _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. @@ -5006,7 +5320,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}}' @@ -5014,7 +5328,7 @@ _install-tool name version installer source_prereq="": $sourcePrereq = '{{source_prereq}}' if ($installer -ne 'install' -and $installer -ne 'binstall') { - Write-Error "_install-tool: unknown installer '$installer' (expected 'install' or 'binstall')" + Write-Error "_install-tool-core: unknown installer '$installer' (expected 'install' or 'binstall')" exit 2 } @@ -5026,7 +5340,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) { @@ -5044,7 +5358,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 @@ -5058,7 +5372,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 } @@ -5066,12 +5380,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")] @@ -5082,8 +5399,19 @@ _check-tool name version: $installed = $null $pattern = '^' + [regex]::Escape($name) + ' v(\S+):' - foreach ($line in (cargo install --list 2>$null)) { - if ($line -match $pattern) { $installed = $Matches[1]; break } + $previousAutoInstall = $env:RUSTUP_AUTO_INSTALL + try { + # Validation must not let the rustup proxy provision a missing selection. + $env:RUSTUP_AUTO_INSTALL = '0' + foreach ($line in (& cargo {{_anvil_stable_toolchain_args}} install --list 2>$null)) { + if ($line -match $pattern) { $installed = $Matches[1]; break } + } + } finally { + if ($null -eq $previousAutoInstall) { + Remove-Item Env:RUSTUP_AUTO_INSTALL -ErrorAction SilentlyContinue + } else { + $env:RUSTUP_AUTO_INSTALL = $previousAutoInstall + } } if (-not $installed) { Write-Error "anvil: required tool '$name' not found. Install: cargo install --locked --version =$version $name" @@ -5133,11 +5461,12 @@ _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 +# MSRV fallback selections target `rustup component add --toolchain`; native +# rustup selections use `rustup component add` without restating the selector. # # Probe-first: if the component is already available, skip the # `rustup component add`. This keeps the recipe idempotent and robust @@ -5151,6 +5480,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` @@ -5163,14 +5500,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 } @@ -5193,8 +5538,11 @@ _install-component toolchain component: exit 0 } - if ($toolchain -eq 'default') { - Write-Host "rustup component add $component (default toolchain)" + if ($toolchain -eq 'default' -and $null -ne $selectedStableToolchain) { + Write-Host "rustup component add --toolchain $selectedStableToolchain $component" + rustup component add --toolchain $selectedStableToolchain $component + } elseif ($toolchain -eq 'default') { + Write-Host "rustup component add $component" rustup component add $component } else { Write-Host "rustup component add --toolchain $toolchain $component" @@ -5223,6 +5571,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' } @@ -5231,14 +5587,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 } @@ -5260,7 +5624,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 } @@ -5289,14 +5659,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")] @@ -5304,7 +5673,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")] @@ -5391,12 +5760,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. @@ -5516,12 +5885,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. @@ -5583,6 +5952,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: @@ -5608,6 +5978,61 @@ 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)) { + return +} + +if ((Test-Path -LiteralPath (Join-Path $RepoRoot 'rust-toolchain') -PathType Leaf) -or + (Test-Path -LiteralPath (Join-Path $RepoRoot 'rust-toolchain.toml') -PathType Leaf)) { + 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'" +} + +# This bootstrap scan must choose Cargo before Cargo is available to parse the +# manifest. Keep its workspace.package-before-package precedence and accepted +# syntax synchronized with Get-RootMsrv in tools.just. +$values = @{} +$section = '' +foreach ($line in (Get-Content -LiteralPath $manifestPath)) { + 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 9b2454d19..e068388cd 100644 --- a/justfiles/anvil/checks/aprz.just +++ b/justfiles/anvil/checks/aprz.just @@ -36,7 +36,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 20de20468..a43c5de01 100644 --- a/justfiles/anvil/checks/audit.just +++ b/justfiles/anvil/checks/audit.just @@ -12,7 +12,7 @@ [script("pwsh", "-NoProfile")] anvil-audit: anvil-audit-validate-prereqs $ErrorActionPreference = 'Stop' - & 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 2f4d42f74..b26880a81 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 d23fb3a9a..344fabdcc 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 be9871403..c937a5dc2 100644 --- a/justfiles/anvil/checks/careful.just +++ b/justfiles/anvil/checks/careful.just @@ -59,7 +59,7 @@ anvil-careful: anvil-careful-validate-prereqs anvil-impact Write-Host " reason : the nightly toolchain and/or resolved cargo-careful executable changed" Write-Host " now : $idLabel" Write-Host " identity: $($prev.Substring(0, [Math]::Min(12, $prev.Length)))... -> $($idHash.Substring(0, 12))..." - cargo clean + & cargo '+{{ rust_nightly }}' clean if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } } # Persist the current identity (also seeds the marker on first run, when diff --git a/justfiles/anvil/checks/cargo-hack.just b/justfiles/anvil/checks/cargo-hack.just index ea604935a..0377fb4da 100644 --- a/justfiles/anvil/checks/cargo-hack.just +++ b/justfiles/anvil/checks/cargo-hack.just @@ -16,7 +16,7 @@ anvil-cargo-hack: anvil-cargo-hack-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include required) if ($include -eq '--skip') { Write-Host 'anvil-cargo-hack: no affected packages; skipping'; exit 0 } - & 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 773d7ed9f..9744892f0 100644 --- a/justfiles/anvil/checks/cargo-sort.just +++ b/justfiles/anvil/checks/cargo-sort.just @@ -21,7 +21,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 c9167c91c..a0354f2c4 100644 --- a/justfiles/anvil/checks/clippy.just +++ b/justfiles/anvil/checks/clippy.just @@ -19,7 +19,7 @@ anvil-clippy: anvil-clippy-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include affected) if ($include -eq '--skip') { Write-Host 'anvil-clippy: no affected packages; skipping'; exit 0 } - & 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 c52b613a4..c798669b5 100644 --- a/justfiles/anvil/checks/deny.just +++ b/justfiles/anvil/checks/deny.just @@ -14,7 +14,7 @@ [script("pwsh", "-NoProfile")] anvil-deny: anvil-deny-validate-prereqs $ErrorActionPreference = 'Stop' - & 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 9250d41af..a8edeef7c 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 1954ae4a7..ca6ade8c9 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 48ef3e7c0..57f360170 100644 --- a/justfiles/anvil/checks/ensure-no-cyclic-deps.just +++ b/justfiles/anvil/checks/ensure-no-cyclic-deps.just @@ -14,7 +14,7 @@ anvil-ensure-no-cyclic-deps: anvil-ensure-no-cyclic-deps-validate-prereqs anvil- $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include modified) if ($include -eq '--skip') { Write-Host 'anvil-ensure-no-cyclic-deps: no modified packages; skipping'; exit 0 } - 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 c56682046..cee4d24d5 100644 --- a/justfiles/anvil/checks/ensure-no-default-features.just +++ b/justfiles/anvil/checks/ensure-no-default-features.just @@ -14,7 +14,7 @@ anvil-ensure-no-default-features: anvil-ensure-no-default-features-validate-prer $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include modified) if ($include -eq '--skip') { Write-Host 'anvil-ensure-no-default-features: no modified packages; skipping'; exit 0 } - 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 698b11157..fe447b7fb 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 33c0593d2..d659a3d4d 100644 --- a/justfiles/anvil/checks/external-types.just +++ b/justfiles/anvil/checks/external-types.just @@ -31,7 +31,7 @@ anvil-external-types: anvil-external-types-validate-prereqs anvil-impact $pkg = @(if ($include) { -split $include } else { '--workspace' }) # Build pkg-name -> manifest-path map, restricted to library crates. $libPkgs = @{} - $metadataJson = & 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/fmt.just b/justfiles/anvil/checks/fmt.just index 9d855e7df..48291c5e4 100644 --- a/justfiles/anvil/checks/fmt.just +++ b/justfiles/anvil/checks/fmt.just @@ -23,7 +23,7 @@ anvil-fmt: anvil-fmt-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include modified) if ($include -eq '--skip') { Write-Host 'anvil-fmt: no modified packages; skipping'; exit 0 } - cargo each --workspace --keep-going '--' cargo '+{{ rust_nightly }}' fmt --manifest-path '{manifest}' --check + cargo {{_anvil_stable_toolchain_args}} each --workspace --keep-going '--' cargo '+{{ rust_nightly }}' fmt --manifest-path '{manifest}' --check if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Per-check setup + validate-prereqs diff --git a/justfiles/anvil/checks/license-headers.just b/justfiles/anvil/checks/license-headers.just index 989f57b4b..fb794c32e 100644 --- a/justfiles/anvil/checks/license-headers.just +++ b/justfiles/anvil/checks/license-headers.just @@ -14,7 +14,7 @@ anvil-license-headers: anvil-license-headers-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $include = (& "{{ just_executable() }}" _anvil-impact-include modified) if ($include -eq '--skip') { Write-Host 'anvil-license-headers: no modified packages; skipping'; exit 0 } - 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 ab39d6717..1096d2743 100644 --- a/justfiles/anvil/checks/llvm-cov.just +++ b/justfiles/anvil/checks/llvm-cov.just @@ -37,7 +37,7 @@ anvil-llvm-cov: anvil-llvm-cov-validate-prereqs anvil-impact # never opts a changed package out of tests. $testOnlyPkg = [System.Collections.Generic.List[string]]::new() if ($pkg -contains '--package') { - $metadataJson = & 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 3db72eacb..4703a71f3 100644 --- a/justfiles/anvil/checks/loom.just +++ b/justfiles/anvil/checks/loom.just @@ -57,7 +57,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 @@ -110,7 +110,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 } } @@ -120,7 +120,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 000000000..f95b38cff --- /dev/null +++ b/justfiles/anvil/checks/msrv-test.just @@ -0,0 +1,56 @@ +# 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) + $impactExit = $LASTEXITCODE + if ($impactExit -ne 0) { exit $impactExit } + if ($include -eq '--skip') { Write-Host 'anvil-msrv-test: no affected packages; skipping'; exit 0 } + $toolchainOutput = & "{{ just_executable() }}" _anvil-resolve-stable msrv | Out-String + $resolverExit = $LASTEXITCODE + if ($resolverExit -ne 0) { exit $resolverExit } + $toolchain = $toolchainOutput.Trim() + if ([string]::IsNullOrWhiteSpace($toolchain)) { + Write-Host 'anvil-msrv-test: no root MSRV declared; skipping' + exit 0 + } + $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: + $toolchainOutput = & "{{ just_executable() }}" _anvil-resolve-stable msrv | Out-String + $resolverExit = $LASTEXITCODE + if ($resolverExit -ne 0) { exit $resolverExit } + $toolchain = $toolchainOutput.Trim() + if ([string]::IsNullOrWhiteSpace($toolchain)) { exit 0 } + & cargo "+$toolchain" --version + if ($LASTEXITCODE -ne 0) { + $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 079a54cb9..e4efc812f 100644 --- a/justfiles/anvil/checks/mutants-diff.just +++ b/justfiles/anvil/checks/mutants-diff.just @@ -50,7 +50,7 @@ anvil-mutants-diff: anvil-mutants-diff-validate-prereqs anvil-impact # not a behaviour change for it. git diff "$base" --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 a914f1eb6..e3e32a16b 100644 --- a/justfiles/anvil/checks/mutants-full.just +++ b/justfiles/anvil/checks/mutants-full.just @@ -17,7 +17,7 @@ anvil-mutants-full: anvil-mutants-full-validate-prereqs Write-Host 'anvil-mutants-full: aarch64-pc-windows-msvc -- cargo-mutants does not build here (winapi); skipping' exit 0 } - & 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 5ec58e430..7ae42342d 100644 --- a/justfiles/anvil/checks/readme-check.just +++ b/justfiles/anvil/checks/readme-check.just @@ -37,6 +37,7 @@ [script("pwsh", "-NoProfile")] _anvil-readme mode: anvil-readme-check-validate-prereqs $ErrorActionPreference = 'Stop' + $stableArgs = {{_anvil_stable_toolchain_args}} $action = '{{ mode }}' if ($action -notin @('generate', 'check')) { [Console]::Error.WriteLine("anvil-readme: invalid mode '$action'; expected 'generate' or 'check'") @@ -55,7 +56,7 @@ _anvil-readme mode: anvil-readme-check-validate-prereqs # Unscoped: a change to the workspace-level README template is a repo-level # input cargo-delta cannot map to a package, so this check always runs the # full eligible-crate set rather than an impact subset. - $metadataJson = & cargo metadata --no-deps --format-version 1 + $metadataJson = & cargo @stableArgs metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error "${prefix}: cargo metadata failed" exit $LASTEXITCODE @@ -101,7 +102,7 @@ _anvil-readme mode: anvil-readme-check-validate-prereqs $args = @('doc2readme', $target) if ($check) { $args += '--check' } if ($relTemplate) { $args += @('--template', $relTemplate) } - & cargo @args + & cargo @stableArgs @args if ($LASTEXITCODE -ne 0) { $hadFailure = $true } } finally { Pop-Location diff --git a/justfiles/anvil/checks/semver-check.just b/justfiles/anvil/checks/semver-check.just index e1d8273e4..5dbd16128 100644 --- a/justfiles/anvil/checks/semver-check.just +++ b/justfiles/anvil/checks/semver-check.just @@ -49,6 +49,7 @@ [script("pwsh", "-NoProfile")] anvil-semver-check: anvil-semver-check-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' + $stableArgs = {{_anvil_stable_toolchain_args}} $include = (& "{{ just_executable() }}" _anvil-impact-include affected) $commentFile = 'target/anvil/comments/semver.md' if ($include -eq '--skip') { @@ -68,7 +69,7 @@ anvil-semver-check: anvil-semver-check-validate-prereqs anvil-impact # null, `publish = false` as an empty array, and named-registry restrictions # as a nonempty array. Only the empty array is non-publishable. $libPkgs = @{} - $metadataJson = & cargo metadata --no-deps --format-version 1 + $metadataJson = & cargo @stableArgs metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-semver-check: cargo metadata failed' exit $LASTEXITCODE @@ -146,7 +147,7 @@ anvil-semver-check: anvil-semver-check-validate-prereqs anvil-impact } Write-Host "anvil-semver-check: $p (baseline $base)" - $outputLines = @(& cargo semver-checks --package $p --baseline-rev $base 2>&1) + $outputLines = @(& cargo @stableArgs semver-checks --package $p --baseline-rev $base 2>&1) $semverExit = $LASTEXITCODE $output = ($outputLines | Out-String) if ($semverExit -eq 100) { diff --git a/justfiles/anvil/checks/spellcheck.just b/justfiles/anvil/checks/spellcheck.just index 15e734b47..cdf2aef95 100644 --- a/justfiles/anvil/checks/spellcheck.just +++ b/justfiles/anvil/checks/spellcheck.just @@ -42,9 +42,9 @@ anvil-spellcheck: anvil-spellcheck-validate-prereqs # ignores user-curated dictionaries (`extra_dictionaries`, custom # hunspell langs, etc.). if (Test-Path 'spellcheck.toml') { - 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 000000000..d35d01ac8 --- /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 75ad63c1d..ddce5d420 100644 --- a/justfiles/anvil/groups/pr-slow.just +++ b/justfiles/anvil/groups/pr-slow.just @@ -6,32 +6,32 @@ # 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). Its +# groups can be invoked individually without invoking the other groups: # -# slow1: tests + coverage (replaces the former pr-test group) -# slow2: stricter-runtime correctness (miri, careful) -# slow3: mutation testing +# pr-test: tests + coverage +# pr-msrv: all-target 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 these groups as independent parallel jobs. +# The umbrella runs them sequentially only when invoked locally: # # $ just anvil-pr-test # tests + coverage only +# $ just anvil-pr-msrv # MSRV all-target 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 +39,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 42d745026..c9e7db74e 100644 --- a/justfiles/anvil/impact.just +++ b/justfiles/anvil/impact.just @@ -235,19 +235,24 @@ _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, use the selected rustc sysroot as an unambiguous path + # toolchain. + $stableArgs = {{_anvil_stable_toolchain_args}} + $baselineToolchain = if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { + $env:RUSTUP_TOOLCHAIN.Trim() + } elseif ($stableArgs.Count -eq 1) { + ([string] $stableArgs[0]).Substring(1) + } else { + $sysroot = @(& rustc --print sysroot) + if ($LASTEXITCODE -ne 0 -or $sysroot.Count -ne 1 -or [string]::IsNullOrWhiteSpace([string] $sysroot[0])) { + Write-Error 'anvil-impact: rustc could not report the current checkout stable toolchain sysroot' + exit 1 + } + ([string] $sysroot[0]).Trim() } # Temp-root precedence follows the runner-managed scratch dir on each @@ -280,14 +285,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 +317,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 +435,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 +512,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 ee29054c9..0e444fef5 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' @@ -73,6 +74,7 @@ import 'checks/udeps.just' # would break the whole Justfile. 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 d7062aa7a..8e942948d 100644 --- a/justfiles/anvil/tools.just +++ b/justfiles/anvil/tools.just @@ -128,15 +128,14 @@ 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. +# Validate that rustc is available and workspace MSRVs are compatible. [group("anvil-setup")] [script("pwsh", "-NoProfile")] anvil-tool-rustc-validate-prereqs: @@ -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,228 @@ 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="install": + $ErrorActionPreference = 'Stop' + Set-StrictMode -Version 3 + + $action = '{{action}}' + $validActions = @( + 'install', + 'validate-workspace-msrv', + 'msrv', + 'install-msrv' + ) + if ($action -notin $validActions) { + Write-Error "_anvil-resolve-stable: unknown action '$action'" + exit 2 + } + + $repoRoot = '{{replace(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'" + } + + # This bootstrap scan must choose Cargo before Cargo is available to + # parse the manifest. Keep its workspace.package-before-package + # precedence and accepted syntax synchronized with the selector in + # versions.just. + $values = @{} + $section = '' + foreach ($line in (Get-Content -LiteralPath $manifestPath)) { + 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 Get-InstalledToolchains { + if (-not (Get-Command rustup -ErrorAction SilentlyContinue)) { + throw 'anvil: rustup not found. Install rustup from https://rustup.rs and ensure it is on PATH' + } + $installed = @(rustup toolchain list) + if ($LASTEXITCODE -ne 0) { + throw 'anvil: rustup toolchain list failed' + } + return $installed + } + + function Test-ToolchainInstalled([string] $toolchain) { + $installed = Get-InstalledToolchains + return (($installed -join "`n") -match "(?m)^$([regex]::Escape($toolchain))(?:-|$)") + } + + function Assert-WorkspaceMsrvCompatibility { + $manifestPath = Join-Path $repoRoot 'Cargo.toml' + $rootMsrv = Get-RootMsrv + if (-not (Test-ToolchainInstalled $rootMsrv)) { + throw "anvil: root MSRV toolchain '$rootMsrv' is not installed; run 'just anvil-toolchain-stable-install' before validating prerequisites" + } + $metadataText = (& cargo "+$rootMsrv" metadata --manifest-path $manifestPath --format-version 1 --no-deps 2>&1 | Out-String) + if ($LASTEXITCODE -ne 0) { + throw "anvil: cargo metadata failed while validating workspace MSRVs:`n$metadataText" + } + $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" + } + + function ConvertTo-ComparableRustVersion([string] $value) { + if ($value -notmatch '^\s*(\d+)\.(\d+)(?:\.(\d+))?\s*$') { + throw "anvil: invalid resolved rust-version '$value'" + } + # Cargo treats 1.x and 1.x.0 as the same floor, while + # System.Version sorts an unspecified build component differently. + $patch = if ([string]::IsNullOrWhiteSpace($Matches[3])) { 0 } else { [int] $Matches[3] } + return [version]::new([int] $Matches[1], [int] $Matches[2], $patch) + } + + $rootVersion = ConvertTo-ComparableRustVersion $rootMsrv + $newer = @( + $members | + Where-Object { (ConvertTo-ComparableRustVersion ([string] $_.rust_version)) -gt $rootVersion } | + 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 + Mapped = $true + } + } + return [pscustomobject]@{ Value = $msrv; Mapped = $false } + } + + function Install-Toolchain([string] $toolchain, [string] $label) { + if (-not (Test-ToolchainInstalled $toolchain)) { + Write-Host "anvil: installing $label toolchain '$toolchain'" + rustup toolchain install $toolchain --profile minimal + if ($LASTEXITCODE -ne 0) { + throw "anvil: failed to install $label toolchain '$toolchain'" + } + } + } + + if ($action -eq 'validate-workspace-msrv') { + Assert-CompleteInternalToolchainConfiguration + $skipWorkspaceMsrvValidation = -not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN) + $hasRootToolchainFile = Test-RootToolchainFile + if ($skipWorkspaceMsrvValidation -or $hasRootToolchainFile) { + Get-InstalledToolchains | Out-Null + } else { + 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.Value 'MSRV' + } + } else { + $msrvSelection.Value + } + exit 0 + } + + Assert-CompleteInternalToolchainConfiguration + if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { + Get-InstalledToolchains | Out-Null + & cargo --version *> $null + if ($LASTEXITCODE -ne 0) { + throw "anvil: selected stable toolchain '$($env:RUSTUP_TOOLCHAIN)' is unavailable; provision RUSTUP_TOOLCHAIN or unset it" + } + exit 0 + } + + if (Test-RootToolchainFile) { + Get-InstalledToolchains | Out-Null + Push-Location -LiteralPath $repoRoot + try { + & rustc --version + if ($LASTEXITCODE -ne 0) { + throw 'anvil: rustup could not provision the repository toolchain file' + } + } finally { + Pop-Location + } + exit 0 + } + + Install-Toolchain (Get-RootMsrv) 'stable' + +# Setup paths that execute stable Cargo/Rust or install Cargo tools share this +# dependency, which Just deduplicates within one setup invocation. + +# Ensure the selected stable toolchain is available while preserving native repository toolchain-file behavior. +[group("anvil-setup")] +anvil-toolchain-stable-install: (_anvil-resolve-stable "install") + +# _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 +403,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}}' @@ -187,7 +411,7 @@ _install-tool name version installer source_prereq="": $sourcePrereq = '{{source_prereq}}' if ($installer -ne 'install' -and $installer -ne 'binstall') { - Write-Error "_install-tool: unknown installer '$installer' (expected 'install' or 'binstall')" + Write-Error "_install-tool-core: unknown installer '$installer' (expected 'install' or 'binstall')" exit 2 } @@ -199,7 +423,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 +441,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 +455,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 +463,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,8 +482,19 @@ _check-tool name version: $installed = $null $pattern = '^' + [regex]::Escape($name) + ' v(\S+):' - foreach ($line in (cargo install --list 2>$null)) { - if ($line -match $pattern) { $installed = $Matches[1]; break } + $previousAutoInstall = $env:RUSTUP_AUTO_INSTALL + try { + # Validation must not let the rustup proxy provision a missing selection. + $env:RUSTUP_AUTO_INSTALL = '0' + foreach ($line in (& cargo {{_anvil_stable_toolchain_args}} install --list 2>$null)) { + if ($line -match $pattern) { $installed = $Matches[1]; break } + } + } finally { + if ($null -eq $previousAutoInstall) { + Remove-Item Env:RUSTUP_AUTO_INSTALL -ErrorAction SilentlyContinue + } else { + $env:RUSTUP_AUTO_INSTALL = $previousAutoInstall + } } if (-not $installed) { Write-Error "anvil: required tool '$name' not found. Install: cargo install --locked --version =$version $name" @@ -306,11 +544,12 @@ _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 +# MSRV fallback selections target `rustup component add --toolchain`; native +# rustup selections use `rustup component add` without restating the selector. # # Probe-first: if the component is already available, skip the # `rustup component add`. This keeps the recipe idempotent and robust @@ -324,6 +563,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 +583,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 } @@ -366,8 +621,11 @@ _install-component toolchain component: exit 0 } - if ($toolchain -eq 'default') { - Write-Host "rustup component add $component (default toolchain)" + if ($toolchain -eq 'default' -and $null -ne $selectedStableToolchain) { + Write-Host "rustup component add --toolchain $selectedStableToolchain $component" + rustup component add --toolchain $selectedStableToolchain $component + } elseif ($toolchain -eq 'default') { + Write-Host "rustup component add $component" rustup component add $component } else { Write-Host "rustup component add --toolchain $toolchain $component" @@ -396,6 +654,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 +670,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 +707,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 +742,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 +756,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 +843,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. @@ -689,12 +968,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 6dbdfa728..f330945cf 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,61 @@ # 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)) { + return +} + +if ((Test-Path -LiteralPath (Join-Path $RepoRoot 'rust-toolchain') -PathType Leaf) -or + (Test-Path -LiteralPath (Join-Path $RepoRoot 'rust-toolchain.toml') -PathType Leaf)) { + 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'" +} + +# This bootstrap scan must choose Cargo before Cargo is available to parse the +# manifest. Keep its workspace.package-before-package precedence and accepted +# syntax synchronized with Get-RootMsrv in tools.just. +$values = @{} +$section = '' +foreach ($line in (Get-Content -LiteralPath $manifestPath)) { + 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
UmbrellaGroupCheckNotes
pr-fastfmtpredefined configuration with nightly features
clippypredefined lints
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, all-target tests under the declared MSRV
pr-runtime-analysismirilibtest, not nextest
carefulself-cleans on a toolchain bump
loomopt-in targets only