From 65d70f9d07fe27c4ca95bccd90d276abb543e4f1 Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Thu, 3 Sep 2026 10:20:09 +0200 Subject: [PATCH 01/15] refactor(anvil): adopt cargo-each for scoped checks Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .anvil.lock | 54 +-- crates/cargo-anvil/docs/design/ado.md | 8 +- crates/cargo-anvil/docs/design/checks.md | 52 ++- crates/cargo-anvil/docs/design/github.md | 12 +- crates/cargo-anvil/docs/design/local.md | 51 +- crates/cargo-anvil/docs/implementation.md | 5 +- .../src/anvil/artifacts/justfile.rs | 6 +- .../justfiles/anvil/checks/bench.just | 10 +- .../justfiles/anvil/checks/bolero.just | 15 +- .../justfiles/anvil/checks/careful.just | 12 +- .../justfiles/anvil/checks/cargo-hack.just | 10 +- .../justfiles/anvil/checks/cargo-sort.just | 10 +- .../justfiles/anvil/checks/clippy.just | 10 +- .../justfiles/anvil/checks/doc-build.just | 10 +- .../justfiles/anvil/checks/doc-test.just | 13 +- .../anvil/checks/ensure-no-cyclic-deps.just | 10 +- .../checks/ensure-no-default-features.just | 10 +- .../justfiles/anvil/checks/examples.just | 10 +- .../anvil/checks/external-types.just | 76 +-- .../templates/justfiles/anvil/checks/fmt.just | 6 +- .../anvil/checks/license-headers.just | 10 +- .../justfiles/anvil/checks/llvm-cov.just | 17 +- .../justfiles/anvil/checks/loom.just | 11 +- .../anvil/checks/miri-race-coverage.just | 11 +- .../anvil/checks/miri-strict-provenance.just | 11 +- .../anvil/checks/miri-tree-borrows.just | 13 +- .../justfiles/anvil/checks/miri.just | 10 +- .../justfiles/anvil/checks/msrv-test.just | 13 +- .../justfiles/anvil/checks/mutants-diff.just | 11 +- .../justfiles/anvil/checks/semver-check.just | 13 +- .../justfiles/anvil/checks/udeps.just | 13 +- .../templates/justfiles/anvil/helpers.just | 30 +- .../templates/justfiles/anvil/impact.just | 69 ++- crates/cargo-anvil/tests/impact.rs | 104 ++--- crates/cargo-anvil/tests/recipe_contracts.rs | 136 ++---- .../snapshots/snapshots__ado_backend.snap | 434 ++++++++---------- .../snapshots/snapshots__github_backend.snap | 434 ++++++++---------- .../snapshots/snapshots__local_only.snap | 434 ++++++++---------- justfiles/anvil/checks/bench.just | 10 +- justfiles/anvil/checks/bolero.just | 15 +- justfiles/anvil/checks/careful.just | 12 +- justfiles/anvil/checks/cargo-hack.just | 10 +- justfiles/anvil/checks/cargo-sort.just | 10 +- justfiles/anvil/checks/clippy.just | 10 +- justfiles/anvil/checks/doc-build.just | 10 +- justfiles/anvil/checks/doc-test.just | 13 +- .../anvil/checks/ensure-no-cyclic-deps.just | 10 +- .../checks/ensure-no-default-features.just | 10 +- justfiles/anvil/checks/examples.just | 10 +- justfiles/anvil/checks/external-types.just | 76 +-- justfiles/anvil/checks/fmt.just | 6 +- justfiles/anvil/checks/license-headers.just | 10 +- justfiles/anvil/checks/llvm-cov.just | 17 +- justfiles/anvil/checks/loom.just | 11 +- .../anvil/checks/miri-race-coverage.just | 11 +- .../anvil/checks/miri-strict-provenance.just | 11 +- justfiles/anvil/checks/miri-tree-borrows.just | 13 +- justfiles/anvil/checks/miri.just | 10 +- justfiles/anvil/checks/msrv-test.just | 13 +- justfiles/anvil/checks/mutants-diff.just | 11 +- justfiles/anvil/checks/semver-check.just | 13 +- justfiles/anvil/checks/udeps.just | 13 +- justfiles/anvil/helpers.just | 30 +- justfiles/anvil/impact.just | 69 ++- 64 files changed, 1080 insertions(+), 1518 deletions(-) diff --git a/.anvil.lock b/.anvil.lock index 79fc1184a..5eaec2e2c 100644 --- a/.anvil.lock +++ b/.anvil.lock @@ -1,7 +1,7 @@ version = 1 tool = "anvil" tool_version = "0.6.0" -catalog_checksum = "sha256:b4f90b5873db3987b212783d2e0ed9cc6642e00446875e8b57749d5d67bbb506" +catalog_checksum = "sha256:f3ec246c9989f64ba76cd207360839c87349028dbfe3d38ab7ad3e594f046dcd" [[file]] path = ".anvil/container/Dockerfile.dockerignore" @@ -53,27 +53,27 @@ checksum = "sha256:ef33da620683628208b4c5ad51f96f5f6424fe1d56c2d0475487228b1f1ae [[file]] path = "justfiles/anvil/checks/bench.just" -checksum = "sha256:850793443ada2151a895ee2728e81f03da5430facde16522bcc2e0c314a28577" +checksum = "sha256:7f1a072c55eb97213b947f4f457bb0433881d8531381749982833c54e4cd548c" [[file]] path = "justfiles/anvil/checks/bolero.just" -checksum = "sha256:5a1767c4ff911cec3b0870d675cbefb563193146cedd3a1cbfe1667215bdb264" +checksum = "sha256:2fc7af6d93fb3fdf810f5df00b2e6819f13e2809feb18c76407a996cd1660d18" [[file]] path = "justfiles/anvil/checks/careful.just" -checksum = "sha256:ea06cf7ebd7d5955b409db500a9ab193311af584437412d7bf0429fa70e43fac" +checksum = "sha256:027fb81dfa00bf6462d95425feee418eaeb44f4215f44c1b0798ba4ca7594a4a" [[file]] path = "justfiles/anvil/checks/cargo-hack.just" -checksum = "sha256:4eca78f197c2cf428b693c49cf0af4afb735b9c6caa8c83a71e5f5fd312a9bc1" +checksum = "sha256:c7448ed496b97ba11f046676e08562377242f22412be03b6f51d058461bd71a5" [[file]] path = "justfiles/anvil/checks/cargo-sort.just" -checksum = "sha256:ab342c014199ffb58a9346b7b9fcea88201b5b60a07b06cbc9c4e5ef03bd18f5" +checksum = "sha256:79474817533bea5c9a7539058404d995a6231cf080a4ee1a7b5921130fd51aa8" [[file]] path = "justfiles/anvil/checks/clippy.just" -checksum = "sha256:945a1bf3bb1372b2bfbbfec00d520a97da56ebc5575b1e6292c421b52cc77243" +checksum = "sha256:32579c5f33f60f029513ddc09bf9657588724369ac5b43dcdfb6b86a6a0bbeb1" [[file]] path = "justfiles/anvil/checks/deny.just" @@ -81,67 +81,67 @@ checksum = "sha256:c5604e8cb0eaa2baf4e2e2c70032d037d21873090375ffcf1ee99983965f6 [[file]] path = "justfiles/anvil/checks/doc-build.just" -checksum = "sha256:9e187f61cb1ef9d11fab2e402af8a882b77f6ed17ca332afb9ae0b1d2112e95b" +checksum = "sha256:d2f6a9536d62030db5505c88d510c4114cb0aab81a7af0e995ba8da5ce3a16a6" [[file]] path = "justfiles/anvil/checks/doc-test.just" -checksum = "sha256:0e54c767de8e57a523a8b1b589a88ab6aca3169ac97d157c42d509d429f4b024" +checksum = "sha256:d44faa962fd3422edac2090b982a9839f0ce6b3f14a7aed83d1630dd8bfc9040" [[file]] path = "justfiles/anvil/checks/ensure-no-cyclic-deps.just" -checksum = "sha256:2d19930c33a93b49d0c9e9672a31d85e1ad4fb92faecc0fb599803139b11c18f" +checksum = "sha256:fb70af53b1cb9797dabba172e657816aaedebcb68525793bdf02b52d862b02fe" [[file]] path = "justfiles/anvil/checks/ensure-no-default-features.just" -checksum = "sha256:38e089afe176aea92c93ac6758154b206b6fc8940931035c0d059449fa824d6c" +checksum = "sha256:df001306d8091f13eda00d2f9525c4594774e45f33ff71c39e2e62b15040f166" [[file]] path = "justfiles/anvil/checks/examples.just" -checksum = "sha256:8d01fa3e303a9f2b768213636f12c68cc8b41932b68549239e9f10d3be43507b" +checksum = "sha256:8b2118916ea76448382864177baa6a8e418817c9aba0a9d7fe52218cd89a135e" [[file]] path = "justfiles/anvil/checks/external-types.just" -checksum = "sha256:7e67d90a9d8bd8cd5c8adb68baf41fdd4625bff4d425b5bf18b75baee62892a8" +checksum = "sha256:33f28dd471646d8544be836dd61543c254a71ea2b11deac1a0870a94da1fc0cb" [[file]] path = "justfiles/anvil/checks/fmt.just" -checksum = "sha256:0434e1a8720453a4bc635ea56af1d19875ac79e6fdd76349052cd57e70392eb8" +checksum = "sha256:f0834d42e9df5c41c9b2ac8ad2db7d96ebeb61d1d6e304fcde6d726496f039a5" [[file]] path = "justfiles/anvil/checks/license-headers.just" -checksum = "sha256:7aa0560dc9088b33e80cd55aee0a25090cbd3d3610652afd3734d0ca52ee7b47" +checksum = "sha256:7069b83328f81b280187e45df5f8240152e6bf9245035b7683673e22e2b944f3" [[file]] path = "justfiles/anvil/checks/llvm-cov.just" -checksum = "sha256:cb23283ac3d377b1219c904f4de93b05b437b48c93629fec617307914bba6296" +checksum = "sha256:a0d056607294e44f41d1820d25da77913da99d962a92a6de2404010a81ce0856" [[file]] path = "justfiles/anvil/checks/loom.just" -checksum = "sha256:f4822a8f9cb282bc426790355ba83b63e778db9327e986ba3c86ae6205cfdabb" +checksum = "sha256:1632c1c880efa563aadba6757a2e72656bf950cdcec90a42871d4511ed097939" [[file]] path = "justfiles/anvil/checks/miri-race-coverage.just" -checksum = "sha256:f8435d359f4495a3a1b3693069b0bab371bf94c3698389d9749acf95704a3c45" +checksum = "sha256:f1d9aa72b9f0d102194e10df9532c1578020a5e519d452e23e00389f8b6572b4" [[file]] path = "justfiles/anvil/checks/miri-strict-provenance.just" -checksum = "sha256:2e3fafdb060f10635a0dc34b447db4f06d7dd1549e585440611a5018a1e4ef5c" +checksum = "sha256:26bdef2395b5de38a24d3017369294d0a0ae4b460bc2b7fcd647df5c17ee83a4" [[file]] path = "justfiles/anvil/checks/miri-tree-borrows.just" -checksum = "sha256:c82f586402f2cb05397a40eec6fe95c7590df2cc52f0dd9da6879de08517561c" +checksum = "sha256:8b26ed66559030fe1657bef64dd5b2a53b4fe3c010f2f9d03aa3c1743f1386e1" [[file]] path = "justfiles/anvil/checks/miri.just" -checksum = "sha256:9e9d0cbfef1e1e1586c2af4e9c387719c2f017ea1203d326baacc3ae25df80e2" +checksum = "sha256:8db76d984b84f209e39bad9297eff29bae7fec434eddf12cf5b2074934110eef" [[file]] path = "justfiles/anvil/checks/msrv-test.just" -checksum = "sha256:9c25b140dcaa4b38db7701fd627dd9a2d5ad5f9bb83d4490c146058bf4328cc8" +checksum = "sha256:d4a778eb11002679c0b09d47338194dfdbfbf2f789faa59915314706c475106c" [[file]] path = "justfiles/anvil/checks/mutants-diff.just" -checksum = "sha256:e8d23688dcb86f82bf537e82c35ab8af9296d5530898645ddfe95132f9192bd0" +checksum = "sha256:567c1483d92a7ab5b5b6118f897f5e317157035acf8c0ac6412e95a05dc45bd2" [[file]] path = "justfiles/anvil/checks/mutants-full.just" @@ -157,7 +157,7 @@ checksum = "sha256:4993e662875561ddf976ec94c69ec0d647a5c957bfb8f5174f79162a640ed [[file]] path = "justfiles/anvil/checks/semver-check.just" -checksum = "sha256:be0430a53007301db00602d4bc9296f51ad9b2f011583084d069c51b30939b74" +checksum = "sha256:0de66fdd8d12dcb20fb89f664f14ef72e885f6b92d87b33ca93aaf700684727e" [[file]] path = "justfiles/anvil/checks/spellcheck.just" @@ -165,7 +165,7 @@ checksum = "sha256:2b2063f7e494dba51f805534bce5235132be1eeabde3a118d42d87ce49d51 [[file]] path = "justfiles/anvil/checks/udeps.just" -checksum = "sha256:6efd7378a2cd0f5d86519bd32fd86f2055a60191187dd77a8842b374b8eb7102" +checksum = "sha256:685d109aede25db665f754031f0753455f9eacf21facaaa44acf5901564524a0" [[file]] path = "justfiles/anvil/container.just" @@ -213,11 +213,11 @@ checksum = "sha256:04679222579a090769403f5aae7c9ffabbf5bd559ddb1f17b5a0655152172 [[file]] path = "justfiles/anvil/helpers.just" -checksum = "sha256:dc42a4b2ed4a25e3a37b322dad209df34d7adca88bf3c4de60b2f8c1b396e23b" +checksum = "sha256:c1859557980f733777253e9380d90ef710512fde1748f27795cd4985019986e1" [[file]] path = "justfiles/anvil/impact.just" -checksum = "sha256:0b3a5c96dd33384b4c86ac02b17396bc1a9fe9c82edbc516a2ceea7f4fe3833d" +checksum = "sha256:1a38f46d55f058d6ca823a08d86c928727b230e9141aa15de20d2395aa552803" [[file]] path = "justfiles/anvil/mod.just" diff --git a/crates/cargo-anvil/docs/design/ado.md b/crates/cargo-anvil/docs/design/ado.md index 135df73a4..dd1677434 100644 --- a/crates/cargo-anvil/docs/design/ado.md +++ b/crates/cargo-anvil/docs/design/ado.md @@ -592,8 +592,8 @@ threading pre-formatted strings the local run never produces. The chain: artifact — export `ANVIL_IMPACT=consume`. In consume mode `anvil-impact` is a pure no-op — it trusts the downloaded cache verbatim and **neither snapshots nor recomputes**, so it needs neither cargo-delta nor a fetched base ref. Each scoped - check reads its category's scope from the cache file via `_anvil-impact-include` (into a - local `$include` variable). + check reads its category's selector tokens from the cache file via + `_anvil-impact-include` (into a local `$selection` array). 4. **Scheduled stages download nothing** and always validate the full workspace, so the group step exports `ANVIL_IMPACT=off`. Like the PR `consume`, this is fixed by group class at emit time and is **not** derived from `target/anvil/impact/impact.state`: the @@ -612,7 +612,7 @@ blocking failure. The wiring never branches on impact's *result*, though. When impact succeeds, each group always runs; recipes inside the group decide whether a given check no-ops -by testing for the literal sentinel `--skip` in the relevant include var. This matters +primarily by passing `--none` to cargo-each. This matters because unscoped checks (`deny`, `audit`, `aprz`, `pr-title`, `mutants-full`) must run on every PR. See [local.md §4](./local.md#4-impact-scoping-via-the-anvil-impact-recipe) for the @@ -858,7 +858,7 @@ the coverage of `cfg(target_os = ...)` branches. ADO's `PublishCodeCoverageResul coalesces multiple publishes against the same build into one combined report. `failIfCoverageEmpty: false` keeps the step from failing the build when no cobertura file exists -- which is exactly the "nothing impacted" case (the `anvil-llvm-cov` recipe -no-ops when its tier is `--skip`, producing no file), as well as a tooling issue. No +no-ops when its tier is `--none`, producing no file), as well as a tooling issue. No impact value is threaded into the condition; the presence of the file is the signal. The data appears in the ADO build page's "Code Coverage" tab natively — totals, file diff --git a/crates/cargo-anvil/docs/design/checks.md b/crates/cargo-anvil/docs/design/checks.md index 7ccb24994..b1f091730 100644 --- a/crates/cargo-anvil/docs/design/checks.md +++ b/crates/cargo-anvil/docs/design/checks.md @@ -366,29 +366,32 @@ unaffected workspace members. cargo-delta computes three concentric impact tiers (`required ⊇ affected ⊇ modified`) from the committed diff against the base ref. The shared `anvil-impact` recipe (see [local.md §4](./local.md#4-impact-scoping-via-the-anvil-impact-recipe)) runs cargo-delta once, writes `target/anvil/impact/`, and projects each tier — via the -`_anvil-impact-format` helper — into a pre-built `--package X@ver --package Y@ver` string -(or the literal sentinel `--skip` when the tier is empty). Each package is a -version-qualified cargo spec (`name@version`) so `-p` resolves uniquely to the workspace -member even when a like-named crate is also pulled in as a different-versioned transitive -dependency. - -Every **impact-scoped** per-crate check depends on `anvil-impact` and resolves its -category's scope by calling `_anvil-impact-include ` into a local `$include` -variable, then consumes it (the unscoped checks below take no such dependency). The -**same** cache is read in cloud workflows — the impact job uploads `target/anvil/impact/` -as an artifact and each group job downloads it — so the identical code path runs locally -and in CI, with no scoping threaded through environment variables. Scoping is on by -default both locally and in CI; it is disabled only by `ANVIL_IMPACT=off` (the -scheduled/full tiers), which makes every tier resolve to its full-workspace default. +`_anvil-impact-format` helper — into one selector token per line: `--package`, a bare +workspace package name, and so on, or `--none` when the tier is empty. Unscoped tiers +resolve to `--workspace`. `cargo-each` resolves those workspace names from live Cargo +metadata and emits version-qualified specs to child Cargo commands, so the impact cache +does not duplicate package versions. + +Every **impact-scoped** check depends on `anvil-impact` and resolves its category by +calling `_anvil-impact-include `. Ordinary checks splat those tokens directly +into `cargo each`; its empty-set success behavior replaces recipe-specific skip guards, +and `{packages}` injects the resolved package set into a single child Cargo invocation. +Orchestration-heavy checks capture the same token array and handle `--none` before their +domain-specific work. The **same** cache is read in cloud workflows — the impact job +uploads `target/anvil/impact/` as an artifact and each group job downloads it — so the +identical code path runs locally and in CI, with no scoping threaded through environment +variables. Scoping is on by default both locally and in CI; it is disabled only by +`ANVIL_IMPACT=off` (the scheduled/full tiers), which makes every tier resolve to +`--workspace`. Each catalog check is tagged with one of four buckets: -| Bucket | `$include` tier | Behavior when a tier value is present | Behavior when unscoped (`ANVIL_IMPACT=off` / no cache) | -|-----------|-------------------------------|-----------------------------------------------------------------------------|--------------------------------------| -| modified | `_anvil-impact-include modified` | If `--skip`: exit 0. Otherwise run against the input domain defined by the recipe's own command; do not forward impact-selected package arguments. | Run against the command's normal input domain. | -| affected | `_anvil-impact-include affected` | If `--skip`: exit 0. Otherwise splice the value into the cargo invocation. | Default to `--workspace`. | -| required | `_anvil-impact-include required` | If `--skip`: exit 0. Otherwise splice the value into the cargo invocation. | Default to `--workspace`. | -| unscoped | *(none)* | Always run. | Always run. | +| Bucket | Selector source | Behavior when scoped | Behavior when unscoped (`ANVIL_IMPACT=off` / no cache) | +|-----------|-------------------------------------|-----------------------------------------------------------------------------|--------------------------------------| +| modified | `_anvil-impact-include modified` | `--none` skips through `cargo-each`; otherwise the admitted command uses its normal full input domain. | `--workspace` admits the command. | +| affected | `_anvil-impact-include affected` | `cargo-each` resolves and forwards the selected packages. | `--workspace`. | +| required | `_anvil-impact-include required` | `cargo-each` resolves and forwards the selected packages. | `--workspace`. | +| unscoped | *(none)* | Always run. | Always run. | Bucket assignments per check: @@ -419,15 +422,14 @@ template (`crates/README.j2` / `README.j2`) and the root `.spelling` dictionary change to one of those would be silently scoped out. These ignore impact scoping and always run. -The sentinel `--skip` is a magic string that cannot be a valid cargo argument, so there -is no collision with real package names. Recipes test for it with -`$include -eq '--skip'` and exit 0 to keep the cloud-workflow job green while signalling that -nothing in that tier needed to run. +An empty tier is represented by cargo-each's native `--none` selector. Ordinary recipes +delegate the successful no-op directly to cargo-each; orchestration-heavy recipes detect +`--none` before doing domain-specific setup. Impact and target discovery use three outcomes: work found, proven no work, and failure. Only the first two may continue successfully. Malformed impact tiers, unknown package names, failed Cargo metadata, unavailable PR metadata in a PR -build, and failed tool discovery are errors; they never collapse to `--skip`. +build, and failed tool discovery are errors; they never collapse to `--none`. When cargo-delta reports a manifest directory leaf instead of a package or library name, Anvil accepts it only if it uniquely identifies one workspace package; missing or ambiguous aliases fail rather than silently dropping affected work. diff --git a/crates/cargo-anvil/docs/design/github.md b/crates/cargo-anvil/docs/design/github.md index eabe382d6..6ad8587e4 100644 --- a/crates/cargo-anvil/docs/design/github.md +++ b/crates/cargo-anvil/docs/design/github.md @@ -278,7 +278,7 @@ a `.anvil-proposed` sibling on the next `update` — see `*_runner` inputs. - **Different schedule** for the scheduled tier. - **Path filters** to skip the workflow on docs-only PRs (though anvil's - `cargo delta impact` step already produces a `--skip` sentinel for the include lists + `cargo delta impact` step already produces `--none` selectors for empty include lists when nothing relevant changed). anvil ships two defaults in the root workflow that adopters typically keep but can @@ -439,9 +439,9 @@ blocking failure rather than leaving the run green with a lone red impact job. The wiring never branches on impact's *output values*, though. When impact succeeds, each group always runs; recipes inside the group decide whether a given check no-ops, -by testing for the literal sentinel `--skip` in the relevant include var. This matters +primarily by passing `--none` to cargo-each. This matters because unscoped checks (`deny`, `audit`, `aprz`, `pr-title`, `mutants-full`) -must run on every PR, including docs-only PRs where every tier comes back `--skip`. See +must run on every PR, including docs-only PRs where every tier comes back `--none`. See [local.md §4](./local.md#4-impact-scoping-via-the-anvil-impact-recipe) for the recipe-side contract. @@ -823,7 +823,7 @@ threading pre-formatted strings that local runs never see. The chain in base ref (a group job installs the former and shallow-checks-out without the latter). Each scoped check then reads its category's scope from `target/anvil/impact/include_.txt` via `_anvil-impact-include` (into a local - `$include` variable). This is why the group jobs stay lean and can't be tripped up by + `$selection` array). This is why the group jobs stay lean and can't be tripped up by an environmental difference from the impact job. 4. **Scheduled group jobs download nothing** and always validate the full workspace, so their group action exports `ANVIL_IMPACT=off`. Like the PR `consume`, this is fixed by @@ -834,9 +834,9 @@ threading pre-formatted strings that local runs never see. The chain in state could ever flip a scheduled job into impact scoping and skip the full-workspace backstop.) -The wiring never gates jobs on the impact result — every job runs regardless of `--skip` +The wiring never gates jobs on the impact result — every job runs regardless of `--none` status. This is intentional: unscoped checks (`deny`, `audit`, `aprz`, `pr-title`, -`mutants-full`) must run on every PR even when every tier reports `--skip`. Steps that +`mutants-full`) must run on every PR even when every tier reports `--none`. Steps that need a per-tier side decision read the downloaded cache file directly (e.g. the Codecov upload is gated on both coverage files existing via `hashFiles(...)`), never on a job output. diff --git a/crates/cargo-anvil/docs/design/local.md b/crates/cargo-anvil/docs/design/local.md index f2aecb668..e5c1f5806 100644 --- a/crates/cargo-anvil/docs/design/local.md +++ b/crates/cargo-anvil/docs/design/local.md @@ -33,8 +33,8 @@ repo/ │ │ runs `cargo delta impact`, and writes the │ │ target/anvil/impact/ artifacts that scoped checks read │ │ via _anvil-impact-include. Also owns _anvil-impact-format -│ │ (project a cargo-delta report into the per-tier -│ │ `--package name@version` include list). Same recipe +│ │ (project a cargo-delta report into per-tier +│ │ cargo-each selector tokens). Same recipe │ │ locally and in CI. │ ├── checks/ one file per check: checks/.just holds the │ │ `anvil-` recipe plus its paired `*-setup` and @@ -517,7 +517,7 @@ a CI shell and threading the result between jobs as environment variables. | `snapshots/baseline.json` | cargo-delta snapshot of the base ref. Cached, keyed on the composite of the base commit sha **and** the effective `.delta.toml` identity (`baseline.key` stores ` `) — the expensive throwaway-worktree snapshot is retaken when the base moves *or* the cargo-delta config changes, so the baseline is never diffed against a current snapshot captured under different rules. | | `snapshots/current.json` | cargo-delta snapshot of the working tree. Cached, keyed on the HEAD sha (`current.key`); the dirty-tree guard widens instead of snapshotting, so a snapshotted tree always corresponds exactly to HEAD. | | `impact.json` | the `cargo delta impact --format json` report (the durable source of truth; `{}` when nothing changed). | -| `include_.txt` | the pre-projected per-tier scope string (see below), one file per tier. | +| `include_.txt` | the pre-projected per-tier selector tokens (see below), one token per line. | Because the modified/current set comes from cargo-delta's **committed** git diff against the base, local scoping reflects the commits your branch adds on top of the base ref, the @@ -525,17 +525,15 @@ same way a PR does — not un-committed working-tree edits. ### 4.1 How checks consume it -Every **impact-scoped** per-crate check depends on `anvil-impact` and, at the top of its -body, resolves its category's scope into a local `$include` variable by calling -`_anvil-impact-include`: +Every **impact-scoped** check depends on `anvil-impact`. Ordinary checks pass their +category's selector tokens directly to `cargo-each`: ```just [script("pwsh", "-NoProfile")] anvil-clippy: anvil-clippy-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - $include = (& "{{ just_executable() }}" _anvil-impact-include affected) - if ($include -eq '--skip') { exit 0 } - & cargo clippy @(if ($include) { -split $include } else { '--workspace' }) --all-targets --all-features --locked "--" '-D' 'warnings' + & cargo each @(& "{{ just_executable() }}" _anvil-impact-include affected) --once '--' cargo clippy '{packages}' --all-targets --all-features --locked '--' '-D' 'warnings' + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } ``` `_anvil-impact-include` reads `target/anvil/impact/` — the cache produced by the @@ -549,20 +547,19 @@ both worlds: is threaded between jobs via environment variables. Each check requests one cargo-delta **category** — the selector it passes to -`_anvil-impact-include` — which determines how it uses the returned `$include`: +`_anvil-impact-include`: | Category | What recipes do with it | |------------|------------------------------------------------------------------------------------------------| -| `modified` | `--skip` → recipe exits 0. Otherwise the recipe runs against the input domain defined by its own command; impact-selected package arguments are not forwarded. | -| `affected` | `--skip` → recipe exits 0. Otherwise: splice the value into the cargo invocation, defaulting to `--workspace` when empty. | -| `required` | Same semantics as affected, but consumed by recipes that need the transitive dep graph in scope (doc-build, cargo-hack, udeps). | +| `modified` | `--none` makes `cargo-each` a successful no-op. Otherwise `--workspace` admits the recipe's command against its normal full input domain. | +| `affected` | `cargo-each` resolves the selected members and `{packages}` forwards them to the child command. | +| `required` | Same semantics as affected, but consumed by recipes that need the transitive dependency graph in scope (doc-build, cargo-hack, udeps). | -`$include` is either the literal sentinel `--skip` (the tier is empty), a pre-built -argument string like `--package alpha@1.0.0 --package beta@0.2.0` (version-qualified cargo -specs, so `-p` resolves uniquely even against a like-named transitive dependency), or a -full-workspace default (`--workspace` for affected/required, empty for modified) when the -tier is unscoped. Every value comes from the shared `_anvil-impact-format` helper via the -cache. +The helper emits one token per line: `--none`, `--workspace`, or repeated +`--package` / bare-name pairs. PowerShell captures that output as an array ready to splat +without string parsing. `cargo-each` resolves the names against live workspace metadata; +when `{packages}` is used, it forwards version-qualified specs so child Cargo commands +remain unambiguous. `_anvil-impact-format` **fails the recipe** (non-zero exit, aborting `anvil-impact`) when at least one name cargo-delta reported cannot be resolved to exactly one workspace @@ -592,21 +589,19 @@ The mapping from check to bucket is fixed in the catalog (see `anvil-impact` dependency and never resolve a scope; they always run. Group recipes do not resolve scope themselves; each underlying check reads what it needs. -### 4.2 The `--skip` sentinel +### 4.2 Empty tiers -`--skip` is a magic string `_anvil-impact-format` emits when a tier is empty (typically a -docs-only PR, or a PR touching only files cargo-delta's `file_exclude_patterns` ignore). -It is not a valid cargo argument, so there is no risk of collision with a real package -name. Recipes test for it and exit 0 cleanly, keeping the job green while signalling that -nothing in that tier needed to run. "Which checks can no-op when their tier is empty" is a -per-check property living in the catalog/recipe, not in the wiring layer. +`_anvil-impact-format` emits cargo-each's native `--none` selector when a tier is empty +(typically a docs-only PR, or a PR touching only files cargo-delta's +`file_exclude_patterns` ignore). Ordinary recipes pass it directly to cargo-each, which +exits successfully without spawning the child command. Orchestration-heavy recipes +detect it before performing setup that would otherwise have side effects. ### 4.3 Disabling scoping and the escape hatch Set `ANVIL_IMPACT=off` in the environment to disable scoping entirely: `anvil-impact` (and its snapshot dependency) no-op without touching git or cargo-delta, and -`_anvil-impact-include` returns each tier's full-workspace default (`--workspace` for -affected/required, empty for modified). Because `just` runs a recipe's dependencies in the +`_anvil-impact-include` returns `--workspace` for every tier. Because `just` runs a recipe's dependencies in the same environment, the guard is honored even when `anvil-impact` fires as a check dependency. This is exactly how the **scheduled** and **full** tiers stay full-workspace: `anvil-scheduled` / `anvil-full` wrap their private recipe in `_anvil-unscoped` diff --git a/crates/cargo-anvil/docs/implementation.md b/crates/cargo-anvil/docs/implementation.md index 1515934f2..cb4b85bd6 100644 --- a/crates/cargo-anvil/docs/implementation.md +++ b/crates/cargo-anvil/docs/implementation.md @@ -156,11 +156,12 @@ runs never collide or remove each other's checkout; it is always removed in a `f ### Fail-closed mapping and the dirty-tree safety net cargo-delta reports library/target identifiers that do not uniquely name a Cargo package, so -`_anvil-impact-format` reverse-maps them to version-qualified package selectors using three +`_anvil-impact-format` reverse-maps them to bare workspace package selectors using three Ordinal (case-sensitive) lookups (package name, lib/proc-macro target, manifest-dir leaf). When a reported identifier resolves to **zero** packages (an unmapped gap) or **more than one** (an ambiguity), the recipe fails hard rather than guessing — under-scoping would silently skip -affected work. Complementarily, a dirty working tree (any uncommitted change outside `target/`, +affected work. cargo-each resolves those names and version-qualifies package arguments +before invoking child Cargo commands. Complementarily, a dirty working tree (any uncommitted change outside `target/`, detected via a git `:(exclude)` pathspec) widens *every* tier to `--workspace` locally, because cargo-delta scopes on the committed diff and cannot see working-tree edits. Cloud checkouts are clean, so this only affects local runs. diff --git a/crates/cargo-anvil/src/anvil/artifacts/justfile.rs b/crates/cargo-anvil/src/anvil/artifacts/justfile.rs index 7af410639..45c6d1f4f 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/justfile.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/justfile.rs @@ -474,15 +474,15 @@ mod tests { "{path}: declared {policy:?} but its _anvil-impact-include category is {calls:?}" ); // A scoped check must depend on anvil-impact so the cache is - // fresh, and capture the scope into a local $include -- no + // fresh, and capture the tokens into a local $selection -- no // ANVIL_INCLUDE_* env-var indirection. assert!( body.contains("-validate-prereqs anvil-impact"), "{path} reads the impact cache but does not depend on anvil-impact" ); assert!( - body.contains("$include = (& \"{{ just_executable() }}\" _anvil-impact-include"), - "{path} must capture _anvil-impact-include into a local $include variable" + body.contains("$selection = @(& \"{{ just_executable() }}\" _anvil-impact-include"), + "{path} must capture _anvil-impact-include into a local $selection array" ); } } diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/bench.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/bench.just index b26880a81..1cec79c16 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/bench.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/bench.just @@ -12,9 +12,9 @@ [script("pwsh", "-NoProfile")] 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 {{_anvil_stable_toolchain_args}} bench @(if ($include) { -split $include } else { '--workspace' }) --all-features --no-run + $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo {{_anvil_stable_toolchain_args}} bench '{packages}' --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,8 +22,8 @@ anvil-bench: anvil-bench-validate-prereqs anvil-impact # Install prerequisites for the `anvil-bench` recipe. [group("anvil-setup")] -anvil-bench-setup installer="install": anvil-toolchain-stable-install +anvil-bench-setup installer="install": anvil-toolchain-stable-install (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-bench` recipe. [group("anvil-setup")] -anvil-bench-validate-prereqs: anvil-tool-rustc-validate-prereqs +anvil-bench-validate-prereqs: anvil-tool-rustc-validate-prereqs anvil-tool-cargo-each-validate-prereqs diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/bolero.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/bolero.just index 344fabdcc..60d0302cb 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/bolero.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/bolero.just @@ -32,26 +32,25 @@ [script("pwsh", "-NoProfile")] anvil-bolero: anvil-bolero-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - $include = (& "{{ just_executable() }}" _anvil-impact-include affected) + $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } if (-not $IsLinux) { Write-Host 'anvil-bolero: non-Linux host -- skipping (cargo-bolero/libfuzzer is Linux-only; harnesses still run under anvil-llvm-cov)' exit 0 } - if ($include -eq '--skip') { + if ($selection -contains '--none') { Write-Host 'anvil-bolero: no affected packages; skipping' exit 0 } $packageNames = [System.Collections.Generic.List[string]]::new() - if ($include -and $include -ne '--workspace') { - $pkgArgs = @(-split $include) + if ($selection -notcontains '--workspace') { + $pkgArgs = $selection for ($i = 0; $i -lt $pkgArgs.Count; $i++) { if ($pkgArgs[$i] -ne '--package' -or ($i + 1) -ge $pkgArgs.Count) { - Write-Error "anvil-bolero: unsupported affected-package arguments: $include" + Write-Error "anvil-bolero: unsupported affected-package arguments: $($selection -join ' ')" exit 1 } - # Impact emits version-qualified specs (name@version); cargo-bolero - # resolves workspace members by bare package name. - $packageNames.Add(($pkgArgs[++$i] -split '@', 2)[0]) + $packageNames.Add($pkgArgs[++$i]) } } else { $metadataJson = & cargo {{_anvil_stable_toolchain_args}} metadata --no-deps --format-version 1 diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/careful.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/careful.just index c937a5dc2..3400d6e9a 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/careful.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/careful.just @@ -10,9 +10,9 @@ [script("pwsh", "-NoProfile")] anvil-careful: anvil-careful-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - $include = (& "{{ just_executable() }}" _anvil-impact-include affected) - if ($include -eq '--skip') { Write-Host 'anvil-careful: no affected packages; skipping'; exit 0 } - $pkg = @(if ($include) { -split $include } else { '--workspace' }) + $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + if ($selection -contains '--none') { Write-Host 'anvil-careful: no affected packages; skipping'; exit 0 } # Guard against cargo-careful's stale-sysroot hazard. cargo-careful # builds a custom std ("careful sysroot": debug-assertions + extra UB # checks) into a STABLE cache path -- ProjectDirs(de.ralfj.cargo-careful), @@ -66,13 +66,13 @@ anvil-careful: anvil-careful-validate-prereqs anvil-impact # there is nothing to clean). Written after the clean so it survives it. New-Item -ItemType Directory -Force -Path (Split-Path $marker) | Out-Null Set-Content -LiteralPath $marker -Value $idHash - & cargo '+{{ rust_nightly }}' careful test @pkg --all-features --locked + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo '+{{ rust_nightly }}' careful test '{packages}' --all-features --locked if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-careful` recipe. [group("anvil-setup")] -anvil-careful-setup installer="install": anvil-component-nightly-rust-src-install (anvil-tool-cargo-careful-install installer) +anvil-careful-setup installer="install": anvil-component-nightly-rust-src-install (anvil-tool-cargo-careful-install installer) (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-careful` recipe. [group("anvil-setup")] -anvil-careful-validate-prereqs: anvil-component-nightly-rust-src-validate-prereqs anvil-tool-cargo-careful-validate-prereqs +anvil-careful-validate-prereqs: anvil-component-nightly-rust-src-validate-prereqs anvil-tool-cargo-careful-validate-prereqs anvil-tool-cargo-each-validate-prereqs 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 0377fb4da..da8b5d623 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/cargo-hack.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/cargo-hack.just @@ -14,15 +14,15 @@ [script("pwsh", "-NoProfile")] 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 {{_anvil_stable_toolchain_args}} hack @(if ($include) { -split $include } else { '--workspace' }) --feature-powerset --depth 2 check + $selection = @(& "{{ just_executable() }}" _anvil-impact-include required) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo {{_anvil_stable_toolchain_args}} hack '{packages}' --feature-powerset --depth 2 check if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-cargo-hack` recipe. [group("anvil-setup")] -anvil-cargo-hack-setup installer="install": (anvil-tool-cargo-hack-install installer) +anvil-cargo-hack-setup installer="install": (anvil-tool-cargo-hack-install installer) (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-cargo-hack` recipe. [group("anvil-setup")] -anvil-cargo-hack-validate-prereqs: anvil-tool-cargo-hack-validate-prereqs +anvil-cargo-hack-validate-prereqs: anvil-tool-cargo-hack-validate-prereqs anvil-tool-cargo-each-validate-prereqs 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 9744892f0..5986678a3 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/cargo-sort.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/cargo-sort.just @@ -19,15 +19,15 @@ [script("pwsh", "-NoProfile")] 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 {{_anvil_stable_toolchain_args}} sort --workspace --grouped --check --check-format + $selection = @(& "{{ just_executable() }}" _anvil-impact-include modified) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' 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. [group("anvil-setup")] -anvil-cargo-sort-setup installer="install": (anvil-tool-cargo-sort-install installer) +anvil-cargo-sort-setup installer="install": (anvil-tool-cargo-sort-install installer) (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-cargo-sort` recipe. [group("anvil-setup")] -anvil-cargo-sort-validate-prereqs: anvil-tool-cargo-sort-validate-prereqs +anvil-cargo-sort-validate-prereqs: anvil-tool-cargo-sort-validate-prereqs anvil-tool-cargo-each-validate-prereqs diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/clippy.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/clippy.just index a0354f2c4..d7b1fa8db 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/clippy.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/clippy.just @@ -17,15 +17,15 @@ [script("pwsh", "-NoProfile")] 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 {{_anvil_stable_toolchain_args}} clippy @(if ($include) { -split $include } else { '--workspace' }) --all-targets --all-features --locked "--" '-D' 'warnings' + $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo {{_anvil_stable_toolchain_args}} clippy '{packages}' --all-targets --all-features --locked '--' '-D' 'warnings' if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-clippy` recipe. [group("anvil-setup")] -anvil-clippy-setup installer="install": anvil-component-default-clippy-install +anvil-clippy-setup installer="install": anvil-component-default-clippy-install (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-clippy` recipe. [group("anvil-setup")] -anvil-clippy-validate-prereqs: anvil-component-default-clippy-validate-prereqs +anvil-clippy-validate-prereqs: anvil-component-default-clippy-validate-prereqs anvil-tool-cargo-each-validate-prereqs 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 a8edeef7c..477fd5c6b 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/doc-build.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/doc-build.just @@ -14,10 +14,10 @@ [script("pwsh", "-NoProfile")] anvil-doc-build: anvil-doc-build-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - $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 {{_anvil_stable_toolchain_args}} doc @(if ($include) { -split $include } else { '--workspace' }) --all-features --no-deps + $selection = @(& "{{ just_executable() }}" _anvil-impact-include required) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo {{_anvil_stable_toolchain_args}} doc '{packages}' --all-features --no-deps if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # doc-build, examples and doc-test are pure cargo built-ins; the rust @@ -26,8 +26,8 @@ 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-toolchain-stable-install +anvil-doc-build-setup installer="install": anvil-toolchain-stable-install (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-doc-build` recipe. [group("anvil-setup")] -anvil-doc-build-validate-prereqs: anvil-tool-rustc-validate-prereqs +anvil-doc-build-validate-prereqs: anvil-tool-rustc-validate-prereqs anvil-tool-cargo-each-validate-prereqs 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 ca6ade8c9..d7e0f9f58 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/doc-test.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/doc-test.just @@ -17,18 +17,17 @@ [script("pwsh", "-NoProfile")] anvil-doc-test: anvil-doc-test-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - $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 {{_anvil_stable_toolchain_args}} test --doc @pkg --all-features --locked + $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - & cargo {{_anvil_stable_toolchain_args}} test --doc @pkg --locked + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo {{_anvil_stable_toolchain_args}} test --doc '{packages}' --all-features --locked + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo {{_anvil_stable_toolchain_args}} test --doc '{packages}' --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-toolchain-stable-install +anvil-doc-test-setup installer="install": anvil-toolchain-stable-install (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-doc-test` recipe. [group("anvil-setup")] -anvil-doc-test-validate-prereqs: anvil-tool-rustc-validate-prereqs +anvil-doc-test-validate-prereqs: anvil-tool-rustc-validate-prereqs anvil-tool-cargo-each-validate-prereqs 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 57f360170..0a0028597 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 @@ -12,15 +12,15 @@ [script("pwsh", "-NoProfile")] anvil-ensure-no-cyclic-deps: anvil-ensure-no-cyclic-deps-validate-prereqs anvil-impact $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 {{_anvil_stable_toolchain_args}} ensure-no-cyclic-deps + $selection = @(& "{{ just_executable() }}" _anvil-impact-include modified) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' 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. [group("anvil-setup")] -anvil-ensure-no-cyclic-deps-setup installer="install": (anvil-tool-cargo-ensure-no-cyclic-deps-install installer) +anvil-ensure-no-cyclic-deps-setup installer="install": (anvil-tool-cargo-ensure-no-cyclic-deps-install installer) (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-ensure-no-cyclic-deps` recipe. [group("anvil-setup")] -anvil-ensure-no-cyclic-deps-validate-prereqs: anvil-tool-cargo-ensure-no-cyclic-deps-validate-prereqs +anvil-ensure-no-cyclic-deps-validate-prereqs: anvil-tool-cargo-ensure-no-cyclic-deps-validate-prereqs anvil-tool-cargo-each-validate-prereqs 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 cee4d24d5..6a3a6b019 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 @@ -12,15 +12,15 @@ [script("pwsh", "-NoProfile")] anvil-ensure-no-default-features: anvil-ensure-no-default-features-validate-prereqs anvil-impact $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 {{_anvil_stable_toolchain_args}} ensure-no-default-features + $selection = @(& "{{ just_executable() }}" _anvil-impact-include modified) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' 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. [group("anvil-setup")] -anvil-ensure-no-default-features-setup installer="install": (anvil-tool-cargo-ensure-no-default-features-install installer) +anvil-ensure-no-default-features-setup installer="install": (anvil-tool-cargo-ensure-no-default-features-install installer) (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-ensure-no-default-features` recipe. [group("anvil-setup")] -anvil-ensure-no-default-features-validate-prereqs: anvil-tool-cargo-ensure-no-default-features-validate-prereqs +anvil-ensure-no-default-features-validate-prereqs: anvil-tool-cargo-ensure-no-default-features-validate-prereqs anvil-tool-cargo-each-validate-prereqs diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/examples.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/examples.just index fe447b7fb..aa6f67d63 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/examples.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/examples.just @@ -12,15 +12,15 @@ [script("pwsh", "-NoProfile")] 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 {{_anvil_stable_toolchain_args}} build @(if ($include) { -split $include } else { '--workspace' }) --examples --all-features --locked + $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo {{_anvil_stable_toolchain_args}} build '{packages}' --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-toolchain-stable-install +anvil-examples-setup installer="install": anvil-toolchain-stable-install (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-examples` recipe. [group("anvil-setup")] -anvil-examples-validate-prereqs: anvil-tool-rustc-validate-prereqs +anvil-examples-validate-prereqs: anvil-tool-rustc-validate-prereqs anvil-tool-cargo-each-validate-prereqs 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 d659a3d4d..57f778fdd 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/external-types.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/external-types.just @@ -12,84 +12,26 @@ # only --manifest-path. Iterate the affected library crates and run # the tool once each, pointing at the crate's Cargo.toml. Bin-only # crates have no public API surface and are skipped. -# TODO(anvil-runner): even after a `cargo ox-run` helper absorbs the -# skip/splat preamble, this recipe stays multi-step: cargo-check- -# external-types is per-manifest (no --package/--workspace), so we -# build a name->manifest map from cargo metadata, filter to lib crates, -# intersect with the affected set ($include), and call the tool once per -# crate. Hard-fails on errors (no tolerance, unlike semver-check). +# cargo-each resolves the affected selection, filters it to library crates, +# substitutes each manifest path, and preserves aggregate failure reporting. # Check public APIs for unapproved external types. [script("pwsh", "-NoProfile")] anvil-external-types: anvil-external-types-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - $include = (& "{{ just_executable() }}" _anvil-impact-include affected) - if ($include -eq '--skip') { - Write-Host 'anvil-external-types: no affected packages; skipping' - exit 0 - } - $pkg = @(if ($include) { -split $include } else { '--workspace' }) - # Build pkg-name -> manifest-path map, restricted to library crates. - $libPkgs = @{} - $metadataJson = & cargo {{_anvil_stable_toolchain_args}} metadata --no-deps --format-version 1 + $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + Write-Host "anvil-external-types: catalog tool minimum cargo-check-external-types {{ cargo_check_external_types_version }}; rustdoc toolchain {{ rust_nightly_external_types }}" + & cargo {{_anvil_stable_toolchain_args}} each @selection --filter lib --keep-going '--' cargo '+{{ rust_nightly_external_types }}' check-external-types --manifest-path '{manifest}' if ($LASTEXITCODE -ne 0) { - Write-Error 'anvil-external-types: cargo metadata failed' + Write-Error "anvil-external-types: one or more crates failed. The installed tool may be newer than the catalog minimum; it must remain compatible with rustdoc from {{ rust_nightly_external_types }}." exit $LASTEXITCODE } - try { - $meta = $metadataJson | ConvertFrom-Json - } catch { - Write-Error "anvil-external-types: could not parse cargo metadata output: $_" - exit 1 - } - foreach ($p in $meta.packages) { - if ($p.targets | Where-Object { $_.kind -contains 'lib' }) { - $libPkgs[$p.name] = $p.manifest_path - } - } - # Decide which packages to check. - $packages = @() - if ($pkg -contains '--workspace') { - $packages = $libPkgs.Keys - } else { - for ($i = 0; $i -lt $pkg.Count; $i++) { - if ($pkg[$i] -eq '--package' -and ($i + 1) -lt $pkg.Count) { - # Impact emits version-qualified specs (name@version); - # strip the @version to recover the bare package name - # this manifest-path map keys on. - $name = ($pkg[$i + 1] -split '@', 2)[0] - if ($libPkgs.ContainsKey($name)) { $packages += $name } - $i++ - } - } - } - if ($packages.Count -eq 0) { - Write-Host 'anvil-external-types: no affected library crates; skipping' - exit 0 - } - Write-Host "anvil-external-types: catalog tool minimum cargo-check-external-types {{ cargo_check_external_types_version }}; rustdoc toolchain {{ rust_nightly_external_types }}" - $failed = $false - foreach ($p in $packages) { - Write-Host "anvil-external-types: $p" - # cargo-check-external-types requires nightly rustdoc (uses - # unstable -Z flags) AND pins a specific rustdoc-types schema - # version. We pin nightly narrowly to the schema this tool - # version expects via `rust_nightly_external_types` in - # versions.just — bump that pin alongside any cargo-check- - # external-types upgrade. No tolerance for schema mismatches: - # if it fails, the pin or the tool needs to move. - & cargo '+{{ rust_nightly_external_types }}' check-external-types --manifest-path $libPkgs[$p] - if ($LASTEXITCODE -ne 0) { - Write-Error "anvil-external-types: '$p' failed. The installed tool may be newer than the catalog minimum; it must remain compatible with rustdoc from {{ rust_nightly_external_types }}." - $failed = $true - } - } - if ($failed) { exit 1 } # Install prerequisites for the `anvil-external-types` recipe. [group("anvil-setup")] -anvil-external-types-setup installer="install": anvil-toolchain-nightly-external-types-install (anvil-tool-cargo-check-external-types-install installer) +anvil-external-types-setup installer="install": anvil-toolchain-nightly-external-types-install (anvil-tool-cargo-check-external-types-install installer) (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-external-types` recipe. [group("anvil-setup")] -anvil-external-types-validate-prereqs: anvil-toolchain-nightly-external-types-validate-prereqs anvil-tool-cargo-check-external-types-validate-prereqs +anvil-external-types-validate-prereqs: anvil-toolchain-nightly-external-types-validate-prereqs anvil-tool-cargo-check-external-types-validate-prereqs anvil-tool-cargo-each-validate-prereqs diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/fmt.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/fmt.just index 48291c5e4..50864e4e6 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/fmt.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/fmt.just @@ -21,9 +21,9 @@ [script("pwsh", "-NoProfile")] anvil-fmt: anvil-fmt-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - $include = (& "{{ just_executable() }}" _anvil-impact-include modified) - if ($include -eq '--skip') { Write-Host 'anvil-fmt: no modified packages; skipping'; exit 0 } - cargo {{_anvil_stable_toolchain_args}} each --workspace --keep-going '--' cargo '+{{ rust_nightly }}' fmt --manifest-path '{manifest}' --check + $selection = @(& "{{ just_executable() }}" _anvil-impact-include modified) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' 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 fb794c32e..10b0a77b7 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/license-headers.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/license-headers.just @@ -12,15 +12,15 @@ [script("pwsh", "-NoProfile")] 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 {{_anvil_stable_toolchain_args}} heather + $selection = @(& "{{ just_executable() }}" _anvil-impact-include modified) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo {{_anvil_stable_toolchain_args}} heather if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-license-headers` recipe. [group("anvil-setup")] -anvil-license-headers-setup installer="install": (anvil-tool-cargo-heather-install installer) +anvil-license-headers-setup installer="install": (anvil-tool-cargo-heather-install installer) (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-license-headers` recipe. [group("anvil-setup")] -anvil-license-headers-validate-prereqs: anvil-tool-cargo-heather-validate-prereqs +anvil-license-headers-validate-prereqs: anvil-tool-cargo-heather-validate-prereqs anvil-tool-cargo-each-validate-prereqs 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 1096d2743..946a86a69 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/llvm-cov.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/llvm-cov.just @@ -18,12 +18,13 @@ [script("pwsh", "-NoProfile")] anvil-llvm-cov: anvil-llvm-cov-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - $include = (& "{{ just_executable() }}" _anvil-impact-include affected) - if ($include -eq '--skip') { + $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + if ($selection -contains '--none') { Write-Host 'anvil-llvm-cov: no affected packages; skipping' exit 0 } - $pkg = @(if ($include) { -split $include } else { '--workspace' }) + $pkg = $selection $allTestPkg = $pkg # Separate coverage-opted-out packages from the measured set. A crate that # is entirely `#![cfg_attr(coverage_nightly, coverage(off))]` (e.g. a @@ -50,11 +51,7 @@ anvil-llvm-cov: anvil-llvm-cov-validate-prereqs anvil-impact if ($optedOut.Count -gt 0) { $filtered = [System.Collections.Generic.List[string]]::new() for ($i = 0; $i -lt $pkg.Count; $i++) { - # Impact emits version-qualified specs (name@version); - # compare the bare name against the opt-out set, but keep - # the full name@version token in $pkg so the llvm-cov - # measurement below resolves unambiguously. - if ($pkg[$i] -eq '--package' -and ($i + 1) -lt $pkg.Count -and $optedOut -contains (($pkg[$i + 1] -split '@', 2)[0])) { + if ($pkg[$i] -eq '--package' -and ($i + 1) -lt $pkg.Count -and $optedOut -contains $pkg[$i + 1]) { $testOnlyPkg.Add('--package') $testOnlyPkg.Add($pkg[$i + 1]) $i++ @@ -155,14 +152,12 @@ anvil-llvm-cov: anvil-llvm-cov-validate-prereqs anvil-impact # report NO DATA and fail the gate. When unscoped, $pkg is `--workspace` # (no gate flag); drop it so the gate defaults to every member. # - # cargo-coverage-gate keys on bare package names from cargo metadata, so - # strip the @version qualifier the measurement used for disambiguation. $gateArgs = [System.Collections.Generic.List[string]]::new() for ($i = 0; $i -lt $pkg.Count; $i++) { if ($pkg[$i] -eq '--workspace') { continue } if ($pkg[$i] -eq '--package' -and ($i + 1) -lt $pkg.Count) { $gateArgs.Add('--package') - $gateArgs.Add(($pkg[$i + 1] -split '@', 2)[0]) + $gateArgs.Add($pkg[$i + 1]) $i++ continue } diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/loom.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/loom.just index 4703a71f3..c94791e1b 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/loom.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/loom.just @@ -52,8 +52,9 @@ [script("pwsh", "-NoProfile")] anvil-loom: anvil-loom-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - $include = (& "{{ just_executable() }}" _anvil-impact-include affected) - if ($include -eq '--skip') { + $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + if ($selection -contains '--none') { Write-Host 'anvil-loom: no affected packages; skipping' exit 0 } @@ -61,17 +62,17 @@ anvil-loom: anvil-loom-validate-prereqs anvil-impact if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Optional impact-scoping: recover bare package names from the - # impact step's version-qualified `--package name@version` pairs. + # impact step's `--package name` pairs. # Full-workspace modes (`--workspace` -- e.g. the scheduled tier, # ANVIL_IMPACT=off, or no impact cache) carry no `--package` tokens, # so $affected stays $null and the package set is left unfiltered # (filtering on an empty set would wrongly skip every loom target). $affected = $null - $tokens = @(-split $include) + $tokens = $selection for ($i = 0; $i -lt $tokens.Count; $i++) { if ($tokens[$i] -eq '--package' -and ($i + 1) -lt $tokens.Count) { if ($null -eq $affected) { $affected = @() } - $affected += ($tokens[$i + 1] -split '@', 2)[0] + $affected += $tokens[$i + 1] $i++ } } diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/miri-race-coverage.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/miri-race-coverage.just index 4851799b1..29f89f71a 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/miri-race-coverage.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/miri-race-coverage.just @@ -17,22 +17,21 @@ [script("pwsh", "-NoProfile")] anvil-miri-race-coverage: anvil-miri-race-coverage-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - $include = (& "{{ just_executable() }}" _anvil-impact-include affected) - if ($include -eq '--skip') { Write-Host 'anvil-miri-race-coverage: no affected packages; skipping'; exit 0 } - $pkg = @(if ($include) { -split $include } else { '--workspace' }) + $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } $day = (Get-Date).Day $low = (2 * $day) - 1 $high = (2 * $day) + 1 $env:MIRIFLAGS = "-Zmiri-many-seeds=$low..$high $($env:MIRIFLAGS)".Trim() $env:RUSTFLAGS = "--cfg miri_race_coverage $($env:RUSTFLAGS)".Trim() Write-Host "anvil-miri-race-coverage: seed window $low..$high (day $day)" - & cargo '+{{ rust_nightly }}' miri test --all-features --tests @pkg + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo '+{{ rust_nightly }}' miri test --all-features --tests '{packages}' if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-miri-race-coverage` recipe. [group("anvil-setup")] -anvil-miri-race-coverage-setup installer="install": anvil-component-nightly-miri-install anvil-component-nightly-rust-src-install +anvil-miri-race-coverage-setup installer="install": anvil-component-nightly-miri-install anvil-component-nightly-rust-src-install (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-miri-race-coverage` recipe. [group("anvil-setup")] -anvil-miri-race-coverage-validate-prereqs: anvil-component-nightly-miri-validate-prereqs anvil-component-nightly-rust-src-validate-prereqs +anvil-miri-race-coverage-validate-prereqs: anvil-component-nightly-miri-validate-prereqs anvil-component-nightly-rust-src-validate-prereqs anvil-tool-cargo-each-validate-prereqs diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/miri-strict-provenance.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/miri-strict-provenance.just index 60f36c278..26a492a4a 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/miri-strict-provenance.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/miri-strict-provenance.just @@ -10,18 +10,17 @@ [script("pwsh", "-NoProfile")] anvil-miri-strict-provenance: anvil-miri-strict-provenance-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - $include = (& "{{ just_executable() }}" _anvil-impact-include affected) - if ($include -eq '--skip') { Write-Host 'anvil-miri-strict-provenance: no affected packages; skipping'; exit 0 } - $pkg = @(if ($include) { -split $include } else { '--workspace' }) + $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } $env:MIRIFLAGS = "-Zmiri-strict-provenance $($env:MIRIFLAGS)".Trim() $env:RUSTFLAGS = "--cfg miri_strict_provenance $($env:RUSTFLAGS)".Trim() - & cargo '+{{ rust_nightly }}' miri test --all-features --tests @pkg + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo '+{{ rust_nightly }}' miri test --all-features --tests '{packages}' if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-miri-strict-provenance` recipe. [group("anvil-setup")] -anvil-miri-strict-provenance-setup installer="install": anvil-component-nightly-miri-install anvil-component-nightly-rust-src-install +anvil-miri-strict-provenance-setup installer="install": anvil-component-nightly-miri-install anvil-component-nightly-rust-src-install (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-miri-strict-provenance` recipe. [group("anvil-setup")] -anvil-miri-strict-provenance-validate-prereqs: anvil-component-nightly-miri-validate-prereqs anvil-component-nightly-rust-src-validate-prereqs +anvil-miri-strict-provenance-validate-prereqs: anvil-component-nightly-miri-validate-prereqs anvil-component-nightly-rust-src-validate-prereqs anvil-tool-cargo-each-validate-prereqs diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/miri-tree-borrows.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/miri-tree-borrows.just index c1786354b..ef8487b36 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/miri-tree-borrows.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/miri-tree-borrows.just @@ -18,18 +18,17 @@ # that tier sets ANVIL_IMPACT=off, so _anvil-impact-include resolves # to --workspace. A direct local invocation is impact-scoped like any # other affected-tier check -- it reads the anvil-impact cache and -# honors --skip. +# honors an empty selection. # Run Miri tests with the Tree Borrows model. [script("pwsh", "-NoProfile")] anvil-miri-tree-borrows: anvil-miri-tree-borrows-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - $include = (& "{{ just_executable() }}" _anvil-impact-include affected) - if ($include -eq '--skip') { Write-Host 'anvil-miri-tree-borrows: no affected packages; skipping'; exit 0 } - $pkg = @(if ($include) { -split $include } else { '--workspace' }) + $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } $env:MIRIFLAGS = "-Zmiri-tree-borrows $($env:MIRIFLAGS)".Trim() $env:RUSTFLAGS = "--cfg miri_tree_borrows $($env:RUSTFLAGS)".Trim() - & cargo '+{{ rust_nightly }}' miri test --all-features --tests @pkg + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo '+{{ rust_nightly }}' miri test --all-features --tests '{packages}' if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # The three nightly-miri profiles share the same toolchain prereqs @@ -39,8 +38,8 @@ anvil-miri-tree-borrows: anvil-miri-tree-borrows-validate-prereqs anvil-impact # Install prerequisites for the `anvil-miri-tree-borrows` recipe. [group("anvil-setup")] -anvil-miri-tree-borrows-setup installer="install": anvil-component-nightly-miri-install anvil-component-nightly-rust-src-install +anvil-miri-tree-borrows-setup installer="install": anvil-component-nightly-miri-install anvil-component-nightly-rust-src-install (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-miri-tree-borrows` recipe. [group("anvil-setup")] -anvil-miri-tree-borrows-validate-prereqs: anvil-component-nightly-miri-validate-prereqs anvil-component-nightly-rust-src-validate-prereqs +anvil-miri-tree-borrows-validate-prereqs: anvil-component-nightly-miri-validate-prereqs anvil-component-nightly-rust-src-validate-prereqs anvil-tool-cargo-each-validate-prereqs diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/miri.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/miri.just index 630e2c792..7bc5d258f 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/miri.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/miri.just @@ -16,8 +16,8 @@ [script("pwsh", "-NoProfile")] anvil-miri: anvil-miri-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - $include = (& "{{ just_executable() }}" _anvil-impact-include affected) - if ($include -eq '--skip') { Write-Host 'anvil-miri: no affected packages; skipping'; exit 0 } + $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Run miri via libtest (`cargo miri test`), NOT `cargo miri nextest # run`. nextest uses a process-per-test model; under miri every # process pays the full cost of re-interpreting std initialization, @@ -43,15 +43,15 @@ anvil-miri: anvil-miri-validate-prereqs anvil-impact # whose tests are all `#[cfg_attr(miri, ignore)]` -- the canonical # opt-out for build-tooling / CLI crates), so the old nextest # `--no-tests=pass` workaround is no longer needed. - & cargo '+{{ rust_nightly }}' miri test --all-features --tests @(if ($include) { -split $include } else { '--workspace' }) + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo '+{{ rust_nightly }}' miri test --all-features --tests '{packages}' if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # --- pr-runtime-analysis members --- # Install prerequisites for the `anvil-miri` recipe. [group("anvil-setup")] -anvil-miri-setup installer="install": anvil-component-nightly-miri-install anvil-component-nightly-rust-src-install +anvil-miri-setup installer="install": anvil-component-nightly-miri-install anvil-component-nightly-rust-src-install (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-miri` recipe. [group("anvil-setup")] -anvil-miri-validate-prereqs: anvil-component-nightly-miri-validate-prereqs anvil-component-nightly-rust-src-validate-prereqs +anvil-miri-validate-prereqs: anvil-component-nightly-miri-validate-prereqs anvil-component-nightly-rust-src-validate-prereqs anvil-tool-cargo-each-validate-prereqs diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/msrv-test.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/msrv-test.just index f95b38cff..a3eb020fa 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/msrv-test.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/msrv-test.just @@ -10,10 +10,10 @@ [script("pwsh", "-NoProfile")] anvil-msrv-test: anvil-msrv-test-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - $include = (& "{{ just_executable() }}" _anvil-impact-include affected) + $selection = @(& "{{ 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 } + if ($selection -contains '--none') { 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 } @@ -22,23 +22,22 @@ anvil-msrv-test: anvil-msrv-test-validate-prereqs anvil-impact 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 + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo "+$toolchain" test '{packages}' --all-targets --all-features --locked if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - & cargo "+$toolchain" test @pkg --all-targets --locked + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo "+$toolchain" test '{packages}' --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": +anvil-msrv-test-setup installer="install": (anvil-tool-cargo-each-install installer) & "{{ 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: +anvil-msrv-test-validate-prereqs: anvil-tool-cargo-each-validate-prereqs $toolchainOutput = & "{{ just_executable() }}" _anvil-resolve-stable msrv | Out-String $resolverExit = $LASTEXITCODE if ($resolverExit -ne 0) { exit $resolverExit } 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 e4efc812f..3fdbe5c00 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/mutants-diff.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/mutants-diff.just @@ -20,12 +20,13 @@ [script("pwsh", "-NoProfile")] anvil-mutants-diff: anvil-mutants-diff-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - $include = (& "{{ just_executable() }}" _anvil-impact-include affected) + $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } if ($IsWindows -and ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64' -or $env:PROCESSOR_ARCHITEW6432 -eq 'ARM64')) { Write-Host 'anvil-mutants-diff: anvil-mutants-diff-validate-prereqs aarch64-pc-windows-msvc -- cargo-mutants does not build here (winapi); skipping' exit 0 } - if ($include -eq '--skip') { + if ($selection -contains '--none') { Write-Host 'anvil-mutants-diff: anvil-mutants-diff-validate-prereqs no affected packages; skipping' exit 0 } @@ -50,15 +51,15 @@ 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 {{_anvil_stable_toolchain_args}} mutants --in-diff $diff_path --no-shuffle --jobs 0 + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo {{_anvil_stable_toolchain_args}} mutants --in-diff $diff_path --no-shuffle --jobs 0 if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # --- pr-mutants members --- # Install prerequisites for the `anvil-mutants-diff` recipe. [group("anvil-setup")] -anvil-mutants-diff-setup installer="install": (anvil-tool-cargo-mutants-install installer) +anvil-mutants-diff-setup installer="install": (anvil-tool-cargo-mutants-install installer) (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-mutants-diff` recipe. [group("anvil-setup")] -anvil-mutants-diff-validate-prereqs: anvil-tool-cargo-mutants-validate-prereqs +anvil-mutants-diff-validate-prereqs: anvil-tool-cargo-mutants-validate-prereqs anvil-tool-cargo-each-validate-prereqs 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 5dbd16128..53ffb81a6 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/semver-check.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/semver-check.just @@ -39,7 +39,7 @@ # TODO(anvil-runner): even after a `cargo ox-run` helper absorbs the # skip/splat preamble, this recipe stays multi-step: cargo-metadata # filter to publishable library crates (cargo-semver-checks --workspace -# fails on bin-only workspaces), intersect the affected set ($include) with that +# fails on bin-only workspaces), intersect the affected selection with that # set, verify the PR branch baseline is present, skip newly-added # manifest paths, then invoke per-crate against that baseline with # special handling for renamed crates ("package `` not found in @@ -50,14 +50,15 @@ anvil-semver-check: anvil-semver-check-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $stableArgs = {{_anvil_stable_toolchain_args}} - $include = (& "{{ just_executable() }}" _anvil-impact-include affected) + $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } $commentFile = 'target/anvil/comments/semver.md' - if ($include -eq '--skip') { + if ($selection -contains '--none') { Write-Host 'anvil-semver-check: no affected packages; skipping' Remove-Item -LiteralPath $commentFile -ErrorAction SilentlyContinue exit 0 } - $pkg = @(if ($include) { -split $include } else { '--workspace' }) + $pkg = $selection # Build the publishable candidate package list. Keep each package's metadata so # its manifest path can also be checked in the baseline. Always # iterate per-package over library crates only -- cargo-semver-checks @@ -92,9 +93,7 @@ anvil-semver-check: anvil-semver-check-validate-prereqs anvil-impact $packages = @() for ($i = 0; $i -lt $pkg.Count; $i++) { if ($pkg[$i] -eq '--package' -and ($i + 1) -lt $pkg.Count) { - # Impact emits version-qualified specs (name@version); - # strip the @version to recover the bare package name. - $name = ($pkg[$i + 1] -split '@', 2)[0] + $name = $pkg[$i + 1] if ($libPkgs[$name]) { $packages += $name } $i++ } diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/udeps.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/udeps.just index 83770a4c9..27caed7dc 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/udeps.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/udeps.just @@ -28,21 +28,20 @@ [script("pwsh", "-NoProfile")] anvil-udeps: anvil-udeps-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - $include = (& "{{ just_executable() }}" _anvil-impact-include required) - if ($include -eq '--skip') { Write-Host 'anvil-udeps: no affected packages; skipping'; exit 0 } - $pkg = @(if ($include) { -split $include } else { '--workspace' }) + $selection = @(& "{{ just_executable() }}" _anvil-impact-include required) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Pass 1: default targets (lib + bins) -- surfaces [dependencies] that # are only used by tests/benches/examples (should be dev-deps). - & cargo '+{{ rust_nightly }}' udeps @pkg --all-features + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo '+{{ rust_nightly }}' udeps '{packages}' --all-features if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Pass 2: --all-targets -- surfaces unused [dev-dependencies]. - & cargo '+{{ rust_nightly }}' udeps @pkg --all-features --all-targets + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo '+{{ rust_nightly }}' udeps '{packages}' --all-features --all-targets if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-udeps` recipe. [group("anvil-setup")] -anvil-udeps-setup installer="install": anvil-toolchain-nightly-install (anvil-tool-cargo-udeps-install installer) +anvil-udeps-setup installer="install": anvil-toolchain-nightly-install (anvil-tool-cargo-udeps-install installer) (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-udeps` recipe. [group("anvil-setup")] -anvil-udeps-validate-prereqs: anvil-toolchain-nightly-validate-prereqs anvil-tool-cargo-udeps-validate-prereqs +anvil-udeps-validate-prereqs: anvil-toolchain-nightly-validate-prereqs anvil-tool-cargo-udeps-validate-prereqs anvil-tool-cargo-each-validate-prereqs diff --git a/crates/cargo-anvil/templates/justfiles/anvil/helpers.just b/crates/cargo-anvil/templates/justfiles/anvil/helpers.just index c18dd37ac..ffac2ed96 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/helpers.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/helpers.just @@ -35,30 +35,26 @@ # unscoped by design. # # Each scoped check depends on `anvil-impact` (which produces the -# target/anvil/impact cache) and captures its category's scope into a local -# variable by calling the shared resolver: +# target/anvil/impact cache) and captures its category's selector tokens by +# calling the shared resolver: # -# $include = (& "{{ just_executable() }}" _anvil-impact-include ) +# $selection = @(& "{{ just_executable() }}" _anvil-impact-include ) # # _anvil-impact-include reads that cache both locally and in cloud workflows # (where the group job downloaded the target/anvil/impact artifact), so the # same code path runs everywhere -- no scoping is threaded between jobs via -# environment variables. `$include` is one of: +# environment variables. `$selection` is one of: # -# * "--workspace" (affected/required) or "" (modified) - the tier is -# unscoped: ANVIL_IMPACT=off, or no impact cache is present. Recipes splat -# "--workspace" (an empty string is NOT $null, so recipes test truthiness -# rather than `?? "--workspace"`, which would splat an empty arg cargo -# rejects). -# * "--package A@v --package B@v" - the tier has members. Packages are -# version-qualified cargo specs so they resolve uniquely even when a -# like-named crate is also a transitive dependency. -# * "--skip" - the tier is empty for this change (recipe exits 0). +# * "--workspace" - the tier is unscoped: ANVIL_IMPACT=off, or no impact +# cache is present. +# * "--package", "A", "--package", "B" - the tier has members. One token is +# emitted per line so PowerShell captures a ready-to-splat array. +# * "--none" - the tier is empty; cargo-each exits successfully without +# invoking the child command. # -# This keeps the per-recipe boilerplate to a single capture, a one-line skip -# guard, and the cargo invocation. Modified-tier recipes never forward -# `$include`; they only check the skip sentinel and then let their command -# determine its normal input domain. +# Ordinary recipes pass the array to cargo-each. Modified-tier recipes use +# --once without {packages}, so the selector admits or skips the command while +# the command retains its normal input domain. # # Every recipe whose body uses multi-line conditionals or array-splat # splicing is annotated with [script("pwsh", "-NoProfile")]. pwsh is preinstalled on diff --git a/crates/cargo-anvil/templates/justfiles/anvil/impact.just b/crates/cargo-anvil/templates/justfiles/anvil/impact.just index c9e7db74e..f97a1a4d5 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/impact.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/impact.just @@ -14,7 +14,7 @@ # it, so the checks read the cache instead of recomputing. # # The base ref is resolved by the shared _anvil-base-ref helper (helpers.just, -# also reused by anvil-mutants-diff). The impact -> `--package name@version` +# also reused by anvil-mutants-diff). The impact -> cargo selector # tier projection is _anvil-impact-format, defined in THIS file next to its # sole caller; this recipe orchestrates the snapshots + caching around both so # the exact same projection is used locally and in CI. @@ -324,11 +324,11 @@ _anvil-impact-snapshot: } # Public. Computes the impact set from the two snapshots and projects each -# tier into a pre-built `--package name@version ...` string (or the sentinel -# `--skip` when the tier is empty) via the shared _anvil-impact-format -# helper, written to include_.txt. Per-crate checks depend on this -# recipe and read those files via _anvil-impact-include. Cache-aware: no-ops -# when the snapshots haven't changed. +# tier into tokenized `--package name` selectors (or `--none` when the tier is +# empty) via the shared _anvil-impact-format helper, written to +# include_.txt. Per-crate checks depend on this recipe and read those +# files via _anvil-impact-include. Cache-aware: no-ops when the snapshots +# haven't changed. # Compute the impact set and write the target/anvil/impact cache. [group("anvil")] @@ -387,7 +387,7 @@ anvil-impact: _anvil-impact-snapshot if ($dirty.Count -gt 0) { Write-Host "anvil-impact: warning: $($dirty.Count) uncommitted change(s) in the working tree are invisible to cargo-delta (it scopes on the committed diff vs the base ref); widening all tiers to --workspace so nothing you are editing is skipped -- commit to scope by impact, or set ANVIL_IMPACT=off to disable scoping." -ForegroundColor Yellow [System.IO.Directory]::CreateDirectory($impactDir) | Out-Null - Set-Content -LiteralPath (Join-Path $impactDir 'include_modified.txt') -Value '' -Encoding UTF8 -NoNewline + Set-Content -LiteralPath (Join-Path $impactDir 'include_modified.txt') -Value '--workspace' -Encoding UTF8 -NoNewline Set-Content -LiteralPath (Join-Path $impactDir 'include_affected.txt') -Value '--workspace' -Encoding UTF8 -NoNewline Set-Content -LiteralPath (Join-Path $impactDir 'include_required.txt') -Value '--workspace' -Encoding UTF8 -NoNewline Set-Content -LiteralPath $impactJson -Value '{}' -Encoding UTF8 @@ -411,11 +411,11 @@ anvil-impact: _anvil-impact-snapshot # First-time anvil adoption: the base predates the cargo workspace, so # there is no baseline to diff against. Fall back to full-workspace - # validation -- modified runs unconditionally (empty), affected/required - # default to --workspace -- rather than failing the impact computation. + # validation -- every tier defaults to --workspace rather than failing the + # impact computation. if (Test-Path (Join-Path $snapDir 'baseline-no-workspace')) { Write-Host 'anvil-impact: baseline has no workspace; defaulting all tiers to full-workspace' - Set-Content -LiteralPath (Join-Path $impactDir 'include_modified.txt') -Value '' -Encoding UTF8 -NoNewline + Set-Content -LiteralPath (Join-Path $impactDir 'include_modified.txt') -Value '--workspace' -Encoding UTF8 -NoNewline Set-Content -LiteralPath (Join-Path $impactDir 'include_affected.txt') -Value '--workspace' -Encoding UTF8 -NoNewline Set-Content -LiteralPath (Join-Path $impactDir 'include_required.txt') -Value '--workspace' -Encoding UTF8 -NoNewline Set-Content -LiteralPath $impactJson -Value '{}' -Encoding UTF8 @@ -440,20 +440,19 @@ anvil-impact: _anvil-impact-snapshot if ([string]::IsNullOrWhiteSpace($impactOut)) { $impactOut = '{}' } Set-Content -LiteralPath $impactJson -Value $impactOut.Trim() -Encoding UTF8 - # Project each tier into its `--package name@version ...` / `--skip` - # string via the shared formatter (version-qualified specs + lib-name -> - # package-name translation live there, used identically by the cloud - # impact steps). + # Project each tier into one selector token per line. The formatter owns + # lib-name -> package-name translation; cargo-each resolves package + # versions from live metadata before invoking child commands. foreach ($tier in @('modified', 'affected', 'required')) { - $value = (& "{{ just_executable() }}" _anvil-impact-format $tier $impactJson) + $value = @(& "{{ just_executable() }}" _anvil-impact-format $tier $impactJson) if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - Set-Content -LiteralPath (Join-Path $impactDir "include_$tier.txt") -Value $value.Trim() -Encoding UTF8 -NoNewline - Write-Host "anvil-impact: $tier -> $($value.Trim())" + Set-Content -LiteralPath (Join-Path $impactDir "include_$tier.txt") -Value $value -Encoding UTF8 + Write-Host "anvil-impact: $tier -> $($value -join ' ')" } Set-Content -LiteralPath $impactStateFile -Value $wantState -Encoding UTF8 -NoNewline # _anvil-impact-format: turns a cargo-delta impact report into the -# `--package ...` string the scoped checks consume, projected per +# selector tokens the scoped checks consume, projected per # tier into include_.txt by `anvil-impact` above -- its only caller, # next to which it now lives (impact analysis stays in one place). # @@ -463,14 +462,11 @@ anvil-impact: _anvil-impact-snapshot # impactJson - path to the file the caller produced via # `cargo delta impact --format json`. # -# Output (stdout, exactly one line so the caller can capture it): -# * "--package name@version --package name2@version2 ..." when the -# tier has members. Each package is a version-qualified cargo spec: -# the version (read from `cargo metadata --no-deps`, i.e. workspace -# members only) disambiguates a workspace member from a like-named -# transitive dependency pulled in at a different version, which a -# bare `-p name` cannot do ("multiple packages named name"). -# * "--skip" when the tier is empty (recipes treat this as a no-op). +# Output (stdout, one token per line so PowerShell captures a ready-to-splat array): +# * repeated "--package" / bare workspace package-name pairs when the +# tier has members. cargo-each resolves those names from live metadata +# and emits version-qualified specs to child Cargo commands. +# * "--none" when the tier is empty (cargo-each treats this as a no-op). # Metadata failures and unknown/ambiguous package names are written to # STDERR and fail the recipe, so callers never continue with an incomplete # package set (which would silently under-scope and skip a check). @@ -555,7 +551,7 @@ _anvil-impact-format tier impactJson: } $impact = Get-Content -LiteralPath $impactJson -Raw | ConvertFrom-Json $names = @($impact.$tierKey) - $specs = [System.Collections.Generic.List[string]]::new() + $selectors = [System.Collections.Generic.List[string]]::new() foreach ($name in $names) { if ([string]::IsNullOrWhiteSpace($name)) { continue } # Union candidates from every namespace, de-duplicated by package id @@ -591,20 +587,21 @@ _anvil-impact-format tier impactJson: exit 1 } $p = $candidates[0] - $specs.Add("--package $($p.name)@$($p.version)") + $selectors.Add('--package') + $selectors.Add($p.name) } - if ($specs.Count -eq 0) { - Write-Output '--skip' + if ($selectors.Count -eq 0) { + Write-Output '--none' } else { - Write-Output ($specs -join ' ') + Write-Output $selectors } # Private. Resolves an impact category's scope for a check recipe and echoes it on stdout. # 1. ANVIL_IMPACT=off -> tier default (full-workspace / run). -# 2. include_.txt present -> its contents (a --package list or --skip). +# 2. include_.txt present -> its selector tokens. # 3. otherwise -> tier default. -# Tier default: --workspace for affected/required; empty (run) for modified. -# Each scoped check calls this directly (into a local $include variable) to +# Tier default: --workspace for every tier. +# Each scoped check calls this directly (into a local $selection array) to # read its category's scope from the target/anvil/impact cache -- the same cache # both locally and in cloud workflows (where the group job downloaded it). @@ -613,7 +610,7 @@ _anvil-impact-format tier impactJson: _anvil-impact-include tier: $ErrorActionPreference = 'Stop' $tier = '{{ tier }}' - $default = if ($tier -eq 'modified') { '' } else { '--workspace' } + $default = '--workspace' # Exit 2 = invalid caller configuration (bad ANVIL_IMPACT), distinct from # the generic 1 used for operational failures -- see _anvil-impact-snapshot. if ($env:ANVIL_IMPACT -and $env:ANVIL_IMPACT -notin @('off', 'consume')) { @@ -623,7 +620,7 @@ _anvil-impact-include tier: if ($env:ANVIL_IMPACT -eq 'off') { Write-Output $default; exit 0 } $file = "target/anvil/impact/include_$tier.txt" if (Test-Path $file) { - Write-Output ("$(Get-Content -Raw $file)".Trim()) + Get-Content -LiteralPath $file | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } } else { Write-Output $default } \ No newline at end of file diff --git a/crates/cargo-anvil/tests/impact.rs b/crates/cargo-anvil/tests/impact.rs index bb7649b09..0b3d531f9 100644 --- a/crates/cargo-anvil/tests/impact.rs +++ b/crates/cargo-anvil/tests/impact.rs @@ -488,8 +488,8 @@ fn impact_empty_output_when_head_equals_base() { } // A clean checkout whose HEAD is exactly the base ref: cargo-delta sees no // committed diff and emits no impact JSON. The recipe must still write a - // durable, EMPTY impact set -- `{}` to impact.json and the `--skip` - // sentinel for every tier -- and treat an unchanged repeat run as a cache + // durable, EMPTY impact set -- `{}` to impact.json and the `--none` + // selector for every tier -- and treat an unchanged repeat run as a cache // hit. (The shared `workspace()` fixture always advances HEAD past the // base, so this empty-output path is otherwise never exercised.) let tmp = workspace_at_base(); @@ -507,8 +507,8 @@ fn impact_empty_output_when_head_equals_base() { for tier in ["modified", "affected", "required"] { assert_eq!( fs::read_to_string(impact_dir.join(format!("include_{tier}.txt"))).unwrap().trim(), - "--skip", - "an empty impact set must project tier '{tier}' to the --skip sentinel" + "--none", + "an empty impact set must project tier '{tier}' to the --none selector" ); } @@ -596,8 +596,10 @@ fn impact_widens_to_full_workspace_when_working_tree_is_dirty() { fs::read_to_string(impact_dir.join("include_required.txt")).unwrap().trim(), "--workspace" ); - // modified is empty (not --skip), so its workspace-wide tools still run. - assert_eq!(fs::read_to_string(impact_dir.join("include_modified.txt")).unwrap().trim(), ""); + assert_eq!( + fs::read_to_string(impact_dir.join("include_modified.txt")).unwrap().trim(), + "--workspace" + ); // The warning must fire on EVERY dirty invocation, not just the first -- // running again with the same dirty tree still warns (the dirty check runs @@ -620,45 +622,40 @@ fn impact_widens_to_full_workspace_when_working_tree_is_dirty() { // land in the affected tier (guards against a regression that drops it or // widens without printing the warning). let affected_after = fs::read_to_string(impact_dir.join("include_affected.txt")).unwrap(); + let affected_tokens = affected_after.lines().collect::>(); assert!( - affected_after.contains("--package beta@"), + affected_tokens.windows(2).any(|pair| pair == ["--package", "beta"]), "committing the uncommitted change must scope beta into the affected tier, got: {affected_after}" ); } #[test] #[serial] -fn impact_include_reads_zero_byte_modified_file_without_throwing() { +fn impact_include_reads_workspace_selector_for_modified_widen() { if !tools_available() { return; } - // The dirty-tree widen writes include_modified.txt as a 0-byte file - // (`-Value '' -NoNewline`). `Get-Content -Raw` returns $null for a 0-byte - // file, so `_anvil-impact-include` must read it null-safely rather than - // throwing on `$null.Trim()` -- which every modified-tier check hits on any - // dirty local run. + // A dirty-tree widen admits modified-tier workspace-wide commands through + // cargo-each with the same concrete --workspace selector as every other + // tier. let tmp = workspace(); let root = tmp.path(); - // Uncommitted edit -> anvil-impact widens and writes the 0-byte include. + // Uncommitted edit -> anvil-impact widens every tier. write(&root.join("crates/beta/src/lib.rs"), "pub fn b() {}\npub fn wip() {}\n"); run_impact(root); let modified_file = root.join("target/anvil/impact/include_modified.txt"); - assert_eq!( - fs::metadata(&modified_file).unwrap().len(), - 0, - "the dirty widen must write a 0-byte include_modified.txt for this test to be meaningful" - ); + assert_eq!(fs::read_to_string(&modified_file).unwrap().trim(), "--workspace"); let out = just_cmd(root, &["_anvil-impact-include", "modified"]).output().unwrap(); let combined = format!("{}{}", String::from_utf8_lossy(&out.stdout), String::from_utf8_lossy(&out.stderr)); assert!( out.status.success(), - "_anvil-impact-include must not throw on a 0-byte include file:\n{combined}" + "_anvil-impact-include must resolve the modified selector:\n{combined}" ); assert_eq!( String::from_utf8_lossy(&out.stdout).trim(), - "", - "an empty modified tier must resolve to '' (run), not crash" + "--workspace", + "a widened modified tier must admit its command through cargo-each" ); } @@ -907,6 +904,7 @@ fn fake_cargo(dir: &Path, log: &Path) { foreach ($argument in $args) {{\n\ \x20 if ($argument -is [Array]) {{ $effectiveArgs += @($argument) }} else {{ $effectiveArgs += $argument }}\n\ }}\n\ + if (($effectiveArgs -contains 'install') -and ($effectiveArgs -contains '--list')) {{ Write-Output 'cargo-each v0.1.0:'; exit 0 }}\n\ Add-Content -LiteralPath '{}' -Value ($effectiveArgs -join ' ')\n\ exit 0\n", log.display() @@ -915,7 +913,10 @@ fn fake_cargo(dir: &Path, log: &Path) { } #[cfg(unix)] { - let script = format!("#!/bin/sh\nprintf '%s\\n' \"$*\" >> '{}'\nexit 0\n", log.display()); + let script = format!( + "#!/bin/sh\nif [ \"$1 $2\" = \"install --list\" ]; then printf '%s\\n' 'cargo-each v0.1.0:'; exit 0; fi\nprintf '%s\\n' \"$*\" >> '{}'\nexit 0\n", + log.display() + ); let path = dir.join("cargo"); fs::write(&path, script).unwrap(); fs::set_permissions(&path, fs::Permissions::from_mode(0o755)).unwrap(); @@ -934,18 +935,14 @@ fn path_with_prefix(dir: &Path) -> OsString { #[test] #[serial] -fn scoped_check_consumes_cached_package_list_and_skips_on_sentinel() { +fn scoped_check_delegates_cached_selection_and_empty_set_to_cargo_each() { if !tools_available() { return; } - // End-to-end proof of the shared check contract that the 25 rewritten - // checks all use: a scoped check resolves its tier from the downloaded - // target/anvil/impact cache, splits the `--package name@version` list, and - // passes it to its cargo tool -- and short-circuits (never invoking the - // tool) on the `--skip` sentinel. `anvil-examples` stands in for the - // family; a fake `cargo` on PATH captures the argv the recipe builds. This - // guards the PowerShell capture/splitting/short-circuit path that static - // text-presence assertions on the emitted recipe cannot. + // End-to-end proof of the shared check contract: a scoped check resolves + // one selector token per line from the downloaded cache and splats those + // tokens into cargo-each. cargo-each owns package resolution, child + // expansion, and empty-set success. let tmp = workspace(); let root = tmp.path(); let impact_dir = root.join("target/anvil/impact"); @@ -955,7 +952,7 @@ fn scoped_check_consumes_cached_package_list_and_skips_on_sentinel() { let affected = fs::read_to_string(impact_dir.join("include_affected.txt")).unwrap(); let affected = affected.trim().to_owned(); assert!( - affected.contains("--package alpha@"), + affected.lines().eq(["--package", "alpha"]), "precondition: the affected tier should be a scoped --package list, got: {affected}" ); @@ -964,9 +961,8 @@ fn scoped_check_consumes_cached_package_list_and_skips_on_sentinel() { fake_cargo(&bin, &log); let path = path_with_prefix(&bin); - // consume mode: anvil-impact no-ops (no snapshot / cargo-delta), so the - // ONLY cargo invocation is the recipe's own `cargo build` -- captured by - // the shim. + // consume mode: anvil-impact no-ops, so the check's cargo-each invocation + // is captured by the shim. let out = just_cmd(root, &["anvil-examples"]) .env("ANVIL_IMPACT", "consume") .env("PATH", &path) @@ -976,12 +972,12 @@ fn scoped_check_consumes_cached_package_list_and_skips_on_sentinel() { assert!(out.status.success(), "scoped anvil-examples run failed:\n{combined}"); let argv = fs::read_to_string(&log).unwrap_or_default(); assert!( - argv.contains("build") && argv.contains(&affected) && argv.contains("--examples"), - "the cached --package list must reach the tool; captured argv:\n{argv}\nexpected to contain: build ... {affected} ... --examples" + argv.contains("each --package alpha --once -- cargo") && argv.contains("build {packages} --examples"), + "the cached selector tokens must reach cargo-each; captured argv:\n{argv}" ); - // The `--skip` sentinel must short-circuit: the tool is never invoked. - fs::write(impact_dir.join("include_affected.txt"), "--skip").unwrap(); + // The empty tier is represented in cargo-each's native selector language. + fs::write(impact_dir.join("include_affected.txt"), "--none").unwrap(); fs::write(&log, "").unwrap(); let skipped = just_cmd(root, &["anvil-examples"]) .env("ANVIL_IMPACT", "consume") @@ -996,8 +992,8 @@ fn scoped_check_consumes_cached_package_list_and_skips_on_sentinel() { assert!(skipped.status.success(), "skipped anvil-examples run failed:\n{skip_combined}"); let argv_skip = fs::read_to_string(&log).unwrap_or_default(); assert!( - !argv_skip.contains("build"), - "the --skip sentinel must short-circuit the tool (no cargo build); captured argv:\n{argv_skip}" + argv_skip.contains("each --none --once"), + "the --none selector must be delegated to cargo-each; captured argv:\n{argv_skip}" ); } @@ -1009,10 +1005,8 @@ fn msrv_test_uses_affected_packages_for_both_feature_modes_and_skips_without_msr 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 affected = fs::read_to_string(root.join("target/anvil/impact/include_affected.txt")).unwrap(); + let affected = affected.lines().collect::>().join(" "); let bin = root.join(".fakebin"); let log = root.join("cargo-argv.log"); @@ -1036,8 +1030,8 @@ fn msrv_test_uses_affected_packages_for_both_feature_modes_and_skips_without_msr 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"), + format!("each {affected} --once -- cargo +1.97 test {{packages}} --all-targets --all-features --locked"), + format!("each {affected} --once -- cargo +1.97 test {{packages}} --all-targets --locked"), ] { assert!(argv.contains(&expected), "missing MSRV invocation '{expected}' in:\n{argv}"); } @@ -1058,8 +1052,8 @@ fn msrv_test_uses_affected_packages_for_both_feature_modes_and_skips_without_msr ); 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" + !fs::read_to_string(&log).unwrap().contains(" each "), + "no-MSRV invocation must skip before cargo-each" ); } @@ -1106,7 +1100,7 @@ fn loom_runs_declared_targets_in_full_workspace_mode() { // Regression guard for the loom `--workspace` path: `anvil-loom` parses its // own package set from `cargo metadata`, and a prior bug turned the // unscoped `--workspace` value into an empty affected set that silently - // skipped every loom target. The representative anvil-examples/`--skip` + // skipped every loom target. The representative anvil-examples/`--none` // test can't catch this bespoke parsing. Here a fake `cargo metadata` // reports a crate with a `required-features = ["loom"]` test target; run // under ANVIL_IMPACT=off (so the tier resolves to --workspace), loom must @@ -1292,8 +1286,8 @@ fn consume_without_downloaded_cache_fails_loudly() { // checks their presence, never recomputes. let cache = root.join("target/anvil/impact"); for (file, spec) in [ - ("include_modified.txt", ""), - ("include_affected.txt", "--package alpha@0.1.0"), + ("include_modified.txt", "--workspace"), + ("include_affected.txt", "--package\nalpha"), ("include_required.txt", "--workspace"), ] { write(&cache.join(file), spec); @@ -1416,8 +1410,8 @@ fn impact_format_maps_proc_macro_target_name_to_its_package() { let (stdout, stderr, ok) = run_format(root, "affected", fixture); assert!(ok, "the formatter must exit 0:\nstderr: {stderr}"); - assert_eq!( - stdout, "--package my-macro@0.3.0", + assert!( + stdout.lines().eq(["--package", "my-macro"]), "the proc-macro target `my_macro` must map back to its package `my-macro`:\nstdout: {stdout}\nstderr: {stderr}" ); } diff --git a/crates/cargo-anvil/tests/recipe_contracts.rs b/crates/cargo-anvil/tests/recipe_contracts.rs index 7b1f82209..60a8fb5eb 100644 --- a/crates/cargo-anvil/tests/recipe_contracts.rs +++ b/crates/cargo-anvil/tests/recipe_contracts.rs @@ -8,7 +8,6 @@ reason = "panic-on-failure idioms are appropriate in tests" )] -use std::collections::HashSet; use std::ffi::{OsStr, OsString}; use std::fmt::Write as _; use std::fs; @@ -228,6 +227,8 @@ fn fixture(imports: &[(&str, &str)], dependency_recipes: &[&str]) -> TempDir { justfile.push_str("cargo_check_external_types_version := \"0.0.0-test\"\n\n"); justfile.push_str("_anvil_stable_toolchain_args := \"@()\"\n\n"); } + justfile.push_str("anvil-tool-cargo-each-validate-prereqs:\n\n"); + justfile.push_str("anvil-tool-cargo-each-install installer=\"install\":\n\n"); for (name, contents) in imports { write(&tmp.path().join(name), contents); writeln!(justfile, "import '{name}'").unwrap(); @@ -270,7 +271,7 @@ fn run_just(root: &Path, arguments: &[&str], environment: &[(&str, &OsStr)]) -> // `ANVIL_IMPACT=consume` and downloads a cache into the real repository; // inherited into a temp directory that has no cache, `anvil-impact` fails // hard and takes the recipe under test with it. `ANVIL_INCLUDE_*` is the - // same hazard one level down: a leg whose scope resolved to `--skip` would + // same hazard one level down: a leg whose scope resolved to `--none` would // silently short-circuit the recipe before it did anything. A test that // cares about either value passes it explicitly below. command.env_remove("ANVIL_IMPACT"); @@ -447,9 +448,10 @@ fn impact_format_resolves_directory_aliases_and_fails_hard() { "unique manifest directory alias should resolve:\n{}", String::from_utf8_lossy(&directory_alias.stderr) ); - assert_eq!( - String::from_utf8_lossy(&directory_alias.stdout).trim(), - "--package fixture-package@0.1.0" + assert!( + String::from_utf8_lossy(&directory_alias.stdout) + .lines() + .eq(["--package", "fixture-package"]) ); let ambiguous_alias = run_just( @@ -529,7 +531,7 @@ fn bolero_discovery_failure_propagates() { ], ); let log = tmp.path().join("cargo.log"); - seed_include(tmp.path(), "affected", "--package fixture@0.1.0"); + seed_include(tmp.path(), "affected", "--package\nfixture"); let output = run_just( tmp.path(), &["anvil-bolero"], @@ -620,7 +622,7 @@ fn semver_exit_code_contract_is_executed() { ], ); let log = tmp.path().join("cargo.log"); - seed_include(tmp.path(), "affected", "--package fixture@0.1.0"); + seed_include(tmp.path(), "affected", "--package\nfixture"); let common = [("BASE_REF", OsStr::new("base")), ("FAKE_CARGO_LOG", log.as_os_str())]; let findings = run_just( @@ -870,42 +872,28 @@ fn repository_constants_match_shared_anvil_versions() { } #[test] -fn public_api_checks_fail_when_metadata_discovery_fails() { +fn semver_check_fails_when_metadata_discovery_fails() { if !tools_available() { return; } - for (recipe_file, contents, recipe, dependencies) in [ - ( - "semver.just", - SEMVER, - "anvil-semver-check", - &[ - "anvil-tool-cargo-semver-checks-validate-prereqs", - "anvil-tool-cargo-semver-checks-install installer", - "anvil-impact", - ][..], - ), - ( - "external-types.just", - EXTERNAL_TYPES, - "anvil-external-types", - &[ - "anvil-tool-cargo-check-external-types-validate-prereqs", - "anvil-toolchain-nightly-external-types-validate-prereqs", - "anvil-tool-cargo-check-external-types-install installer", - "anvil-toolchain-nightly-external-types-install", - "anvil-impact", - ][..], - ), - ] { - let tmp = fixture(&[(recipe_file, contents), ("impact.just", IMPACT)], dependencies); - seed_include(tmp.path(), "affected", "--package fixture@0.1.0"); - let output = run_just(tmp.path(), &[recipe], &[("FAKE_METADATA_EXIT", OsStr::new(ARBITRARY_FAILURE_EXIT))]); - assert_failed(&output, &format!("{recipe} cargo metadata failure")); + let tmp = fixture( + &[("semver.just", SEMVER), ("impact.just", IMPACT)], + &[ + "anvil-tool-cargo-semver-checks-validate-prereqs", + "anvil-tool-cargo-semver-checks-install installer", + "anvil-impact", + ], + ); + seed_include(tmp.path(), "affected", "--package\nfixture"); + let output = run_just( + tmp.path(), + &["anvil-semver-check"], + &[("FAKE_METADATA_EXIT", OsStr::new(ARBITRARY_FAILURE_EXIT))], + ); + assert_failed(&output, "anvil-semver-check cargo metadata failure"); - let malformed = run_just(tmp.path(), &[recipe], &[("FAKE_METADATA_INVALID", OsStr::new("1"))]); - assert_failed(&malformed, &format!("{recipe} malformed cargo metadata")); - } + let malformed = run_just(tmp.path(), &["anvil-semver-check"], &[("FAKE_METADATA_INVALID", OsStr::new("1"))]); + assert_failed(&malformed, "anvil-semver-check malformed cargo metadata"); } #[test] @@ -933,7 +921,9 @@ fn fmt_delegates_workspace_iteration_to_cargo_each() { ); let commands = fs::read_to_string(&log).unwrap(); assert!( - commands.contains("each --workspace --keep-going -- cargo +nightly-test fmt --manifest-path {manifest} --check"), + commands.contains( + "each --workspace --once -- cargo each --workspace --keep-going -- cargo +nightly-test fmt --manifest-path {manifest} --check" + ), "unexpected cargo invocation: {commands}" ); assert!(!commands.contains("fmt --all")); @@ -963,7 +953,7 @@ fn fmt_propagates_cargo_each_failure() { } #[test] -fn external_types_checks_every_library_including_non_publishable_ones() { +fn external_types_delegates_library_selection_and_iteration_to_cargo_each() { if !tools_available() { return; } @@ -979,17 +969,7 @@ fn external_types_checks_every_library_including_non_publishable_ones() { ); seed_include(tmp.path(), "affected", "--workspace"); let log = tmp.path().join("cargo.log"); - let output = run_just( - tmp.path(), - &["anvil-external-types"], - &[ - ("FAKE_CARGO_LOG", log.as_os_str()), - ("FAKE_PUBLISH_FALSE", OsStr::new("1")), - ("FAKE_SECOND_PACKAGE_NAME", OsStr::new("public-default")), - ("FAKE_SECOND_PACKAGE_DIR_LEAF", OsStr::new("public-default")), - ("FAKE_THIRD_PACKAGE_NAME", OsStr::new("named-registry")), - ], - ); + let output = run_just(tmp.path(), &["anvil-external-types"], &[("FAKE_CARGO_LOG", log.as_os_str())]); assert!( output.status.success(), "library selection failed\nstdout:\n{}\nstderr:\n{}", @@ -997,29 +977,11 @@ fn external_types_checks_every_library_including_non_publishable_ones() { String::from_utf8_lossy(&output.stderr) ); let commands = fs::read_to_string(log).unwrap(); - assert!(commands.contains("metadata --no-deps --format-version 1")); - let expected_manifests = [ - tmp.path().join("Cargo.toml").to_string_lossy().into_owned(), - tmp.path() - .join("nested") - .join("public-default") - .join("Cargo.toml") - .to_string_lossy() - .into_owned(), - tmp.path() - .join("nested") - .join("named-registry") - .join("Cargo.toml") - .to_string_lossy() - .into_owned(), - ]; - assert_eq!( - commands - .lines() - .filter_map(|command| { command.strip_prefix("+nightly-test check-external-types --manifest-path ") }) - .map(str::to_owned) - .collect::>(), - expected_manifests.into_iter().collect() + assert!(commands.contains("each --workspace --filter lib --keep-going")); + assert!(commands.contains("check-external-types --manifest-path {manifest}")); + assert!( + !commands.contains("metadata --no-deps"), + "the recipe must not duplicate cargo-each metadata filtering" ); } @@ -1036,7 +998,7 @@ fn semver_skips_non_publishable_libraries() { "anvil-impact", ], ); - seed_include(tmp.path(), "affected", "--package fixture@0.1.0"); + seed_include(tmp.path(), "affected", "--package\nfixture"); let log = tmp.path().join("cargo.log"); let output = run_just( tmp.path(), @@ -1066,7 +1028,7 @@ fn semver_includes_libraries_restricted_to_named_registries() { "anvil-impact", ], ); - seed_include(tmp.path(), "affected", "--package named-registry@0.1.0"); + seed_include(tmp.path(), "affected", "--package\nnamed-registry"); let log = tmp.path().join("cargo.log"); let output = run_just( tmp.path(), @@ -1111,7 +1073,7 @@ fn all_coverage_opted_out_packages_run_both_test_configurations() { ], ); let log = tmp.path().join("cargo.log"); - seed_include(tmp.path(), "affected", "--package fixture@0.1.0"); + seed_include(tmp.path(), "affected", "--package\nfixture"); let output = run_just( tmp.path(), &["anvil-llvm-cov"], @@ -1166,7 +1128,7 @@ fn windows_arm64_fallback_accepts_empty_nextest_sets_in_both_configurations() { ], ); let log = tmp.path().join("cargo.log"); - seed_include(tmp.path(), "affected", "--package fixture@0.1.0"); + seed_include(tmp.path(), "affected", "--package\nfixture"); let output = run_just( tmp.path(), &["anvil-llvm-cov"], @@ -1351,22 +1313,14 @@ fn mutants_diff_covers_uncommitted_work() { ("FAKE_CARGO_LOG", log.as_os_str()), ("BASE_REF", OsStr::new(&base)), ("RUNNER_TEMP", root.as_os_str()), - // The other early exit. Impact scoping sets this to `--skip` when a - // job has no affected packages, and the value is inherited from - // whatever environment the test runs in -- so on a CI leg that - // skipped, this test would assert against a recipe that returned - // before doing anything. Pin it to a scope that runs. - // // The architecture guard is deliberately *not* pinned: Windows // re-derives PROCESSOR_ARCHITECTURE for each new process from the // process's real architecture, so it cannot be overridden across a // spawn. That is why this test returns early on ARM64 above rather // than faking its way past the branch. - ("ANVIL_INCLUDE_AFFECTED", OsStr::new("--package fixture@0.1.0")), // The recipe depends on `anvil-impact`, which would otherwise - // invoke cargo-delta against this fixture. The scope this test - // asserts on is pinned above, so computing an impact set would only - // add a tool dependency to a contract that does not exercise it. + // invoke cargo-delta against this fixture. This contract exercises + // diff construction, not impact computation. ("ANVIL_IMPACT", OsStr::new("off")), ], ); @@ -1433,10 +1387,6 @@ fn mutants_diff_skips_on_arm64_windows() { &[ ("FAKE_CARGO_LOG", log.as_os_str()), ("RUNNER_TEMP", root.as_os_str()), - // Not the architecture -- that is the host's, and real here. This - // is the *other* early exit, pinned so a skipped impact scope - // cannot be mistaken for the architecture bail-out. - ("ANVIL_INCLUDE_AFFECTED", OsStr::new("--package fixture@0.1.0")), // Same reason as the sibling contract: `anvil-mutants-diff` depends // on `anvil-impact`, and this test is about the architecture // bail-out, not about computing an impact set. diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap index bbcffad60..919bb2074 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap @@ -1599,9 +1599,9 @@ anvil-audit-validate-prereqs: anvil-tool-cargo-audit-validate-prereqs [script("pwsh", "-NoProfile")] 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 {{_anvil_stable_toolchain_args}} bench @(if ($include) { -split $include } else { '--workspace' }) --all-features --no-run + $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo {{_anvil_stable_toolchain_args}} bench '{packages}' --all-features --no-run if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # bench uses the cargo built-in in compile-only mode; no extra tool install @@ -1609,11 +1609,11 @@ anvil-bench: anvil-bench-validate-prereqs anvil-impact # Install prerequisites for the `anvil-bench` recipe. [group("anvil-setup")] -anvil-bench-setup installer="install": anvil-toolchain-stable-install +anvil-bench-setup installer="install": anvil-toolchain-stable-install (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-bench` recipe. [group("anvil-setup")] -anvil-bench-validate-prereqs: anvil-tool-rustc-validate-prereqs +anvil-bench-validate-prereqs: anvil-tool-rustc-validate-prereqs anvil-tool-cargo-each-validate-prereqs === justfiles/anvil/checks/bolero.just === # Copyright (c) Microsoft Corporation. @@ -1650,26 +1650,25 @@ anvil-bench-validate-prereqs: anvil-tool-rustc-validate-prereqs [script("pwsh", "-NoProfile")] anvil-bolero: anvil-bolero-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - $include = (& "{{ just_executable() }}" _anvil-impact-include affected) + $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } if (-not $IsLinux) { Write-Host 'anvil-bolero: non-Linux host -- skipping (cargo-bolero/libfuzzer is Linux-only; harnesses still run under anvil-llvm-cov)' exit 0 } - if ($include -eq '--skip') { + if ($selection -contains '--none') { Write-Host 'anvil-bolero: no affected packages; skipping' exit 0 } $packageNames = [System.Collections.Generic.List[string]]::new() - if ($include -and $include -ne '--workspace') { - $pkgArgs = @(-split $include) + if ($selection -notcontains '--workspace') { + $pkgArgs = $selection for ($i = 0; $i -lt $pkgArgs.Count; $i++) { if ($pkgArgs[$i] -ne '--package' -or ($i + 1) -ge $pkgArgs.Count) { - Write-Error "anvil-bolero: unsupported affected-package arguments: $include" + Write-Error "anvil-bolero: unsupported affected-package arguments: $($selection -join ' ')" exit 1 } - # Impact emits version-qualified specs (name@version); cargo-bolero - # resolves workspace members by bare package name. - $packageNames.Add(($pkgArgs[++$i] -split '@', 2)[0]) + $packageNames.Add($pkgArgs[++$i]) } } else { $metadataJson = & cargo {{_anvil_stable_toolchain_args}} metadata --no-deps --format-version 1 @@ -1750,9 +1749,9 @@ anvil-bolero-validate-prereqs: anvil-toolchain-nightly-validate-prereqs anvil-to [script("pwsh", "-NoProfile")] anvil-careful: anvil-careful-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - $include = (& "{{ just_executable() }}" _anvil-impact-include affected) - if ($include -eq '--skip') { Write-Host 'anvil-careful: no affected packages; skipping'; exit 0 } - $pkg = @(if ($include) { -split $include } else { '--workspace' }) + $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + if ($selection -contains '--none') { Write-Host 'anvil-careful: no affected packages; skipping'; exit 0 } # Guard against cargo-careful's stale-sysroot hazard. cargo-careful # builds a custom std ("careful sysroot": debug-assertions + extra UB # checks) into a STABLE cache path -- ProjectDirs(de.ralfj.cargo-careful), @@ -1806,16 +1805,16 @@ anvil-careful: anvil-careful-validate-prereqs anvil-impact # there is nothing to clean). Written after the clean so it survives it. New-Item -ItemType Directory -Force -Path (Split-Path $marker) | Out-Null Set-Content -LiteralPath $marker -Value $idHash - & cargo '+{{ rust_nightly }}' careful test @pkg --all-features --locked + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo '+{{ rust_nightly }}' careful test '{packages}' --all-features --locked if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-careful` recipe. [group("anvil-setup")] -anvil-careful-setup installer="install": anvil-component-nightly-rust-src-install (anvil-tool-cargo-careful-install installer) +anvil-careful-setup installer="install": anvil-component-nightly-rust-src-install (anvil-tool-cargo-careful-install installer) (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-careful` recipe. [group("anvil-setup")] -anvil-careful-validate-prereqs: anvil-component-nightly-rust-src-validate-prereqs anvil-tool-cargo-careful-validate-prereqs +anvil-careful-validate-prereqs: anvil-component-nightly-rust-src-validate-prereqs anvil-tool-cargo-careful-validate-prereqs anvil-tool-cargo-each-validate-prereqs === justfiles/anvil/checks/cargo-hack.just === # Copyright (c) Microsoft Corporation. @@ -1834,18 +1833,18 @@ anvil-careful-validate-prereqs: anvil-component-nightly-rust-src-validate-prereq [script("pwsh", "-NoProfile")] 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 {{_anvil_stable_toolchain_args}} hack @(if ($include) { -split $include } else { '--workspace' }) --feature-powerset --depth 2 check + $selection = @(& "{{ just_executable() }}" _anvil-impact-include required) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo {{_anvil_stable_toolchain_args}} hack '{packages}' --feature-powerset --depth 2 check if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-cargo-hack` recipe. [group("anvil-setup")] -anvil-cargo-hack-setup installer="install": (anvil-tool-cargo-hack-install installer) +anvil-cargo-hack-setup installer="install": (anvil-tool-cargo-hack-install installer) (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-cargo-hack` recipe. [group("anvil-setup")] -anvil-cargo-hack-validate-prereqs: anvil-tool-cargo-hack-validate-prereqs +anvil-cargo-hack-validate-prereqs: anvil-tool-cargo-hack-validate-prereqs anvil-tool-cargo-each-validate-prereqs === justfiles/anvil/checks/cargo-sort.just === # Copyright (c) Microsoft Corporation. @@ -1869,18 +1868,18 @@ anvil-cargo-hack-validate-prereqs: anvil-tool-cargo-hack-validate-prereqs [script("pwsh", "-NoProfile")] 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 {{_anvil_stable_toolchain_args}} sort --workspace --grouped --check --check-format + $selection = @(& "{{ just_executable() }}" _anvil-impact-include modified) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' 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. [group("anvil-setup")] -anvil-cargo-sort-setup installer="install": (anvil-tool-cargo-sort-install installer) +anvil-cargo-sort-setup installer="install": (anvil-tool-cargo-sort-install installer) (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-cargo-sort` recipe. [group("anvil-setup")] -anvil-cargo-sort-validate-prereqs: anvil-tool-cargo-sort-validate-prereqs +anvil-cargo-sort-validate-prereqs: anvil-tool-cargo-sort-validate-prereqs anvil-tool-cargo-each-validate-prereqs === justfiles/anvil/checks/clippy.just === # Copyright (c) Microsoft Corporation. @@ -1902,18 +1901,18 @@ anvil-cargo-sort-validate-prereqs: anvil-tool-cargo-sort-validate-prereqs [script("pwsh", "-NoProfile")] 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 {{_anvil_stable_toolchain_args}} clippy @(if ($include) { -split $include } else { '--workspace' }) --all-targets --all-features --locked "--" '-D' 'warnings' + $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo {{_anvil_stable_toolchain_args}} clippy '{packages}' --all-targets --all-features --locked '--' '-D' 'warnings' if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-clippy` recipe. [group("anvil-setup")] -anvil-clippy-setup installer="install": anvil-component-default-clippy-install +anvil-clippy-setup installer="install": anvil-component-default-clippy-install (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-clippy` recipe. [group("anvil-setup")] -anvil-clippy-validate-prereqs: anvil-component-default-clippy-validate-prereqs +anvil-clippy-validate-prereqs: anvil-component-default-clippy-validate-prereqs anvil-tool-cargo-each-validate-prereqs === justfiles/anvil/checks/deny.just === # Copyright (c) Microsoft Corporation. @@ -1960,10 +1959,10 @@ anvil-deny-validate-prereqs: anvil-tool-cargo-deny-validate-prereqs [script("pwsh", "-NoProfile")] anvil-doc-build: anvil-doc-build-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - $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 {{_anvil_stable_toolchain_args}} doc @(if ($include) { -split $include } else { '--workspace' }) --all-features --no-deps + $selection = @(& "{{ just_executable() }}" _anvil-impact-include required) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo {{_anvil_stable_toolchain_args}} doc '{packages}' --all-features --no-deps if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # doc-build, examples and doc-test are pure cargo built-ins; the rust @@ -1972,11 +1971,11 @@ 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-toolchain-stable-install +anvil-doc-build-setup installer="install": anvil-toolchain-stable-install (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-doc-build` recipe. [group("anvil-setup")] -anvil-doc-build-validate-prereqs: anvil-tool-rustc-validate-prereqs +anvil-doc-build-validate-prereqs: anvil-tool-rustc-validate-prereqs anvil-tool-cargo-each-validate-prereqs === justfiles/anvil/checks/doc-test.just === # Copyright (c) Microsoft Corporation. @@ -1998,21 +1997,20 @@ anvil-doc-build-validate-prereqs: anvil-tool-rustc-validate-prereqs [script("pwsh", "-NoProfile")] anvil-doc-test: anvil-doc-test-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - $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 {{_anvil_stable_toolchain_args}} test --doc @pkg --all-features --locked + $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - & cargo {{_anvil_stable_toolchain_args}} test --doc @pkg --locked + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo {{_anvil_stable_toolchain_args}} test --doc '{packages}' --all-features --locked + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo {{_anvil_stable_toolchain_args}} test --doc '{packages}' --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-toolchain-stable-install +anvil-doc-test-setup installer="install": anvil-toolchain-stable-install (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-doc-test` recipe. [group("anvil-setup")] -anvil-doc-test-validate-prereqs: anvil-tool-rustc-validate-prereqs +anvil-doc-test-validate-prereqs: anvil-tool-rustc-validate-prereqs anvil-tool-cargo-each-validate-prereqs === justfiles/anvil/checks/ensure-no-cyclic-deps.just === # Copyright (c) Microsoft Corporation. @@ -2029,18 +2027,18 @@ anvil-doc-test-validate-prereqs: anvil-tool-rustc-validate-prereqs [script("pwsh", "-NoProfile")] anvil-ensure-no-cyclic-deps: anvil-ensure-no-cyclic-deps-validate-prereqs anvil-impact $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 {{_anvil_stable_toolchain_args}} ensure-no-cyclic-deps + $selection = @(& "{{ just_executable() }}" _anvil-impact-include modified) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' 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. [group("anvil-setup")] -anvil-ensure-no-cyclic-deps-setup installer="install": (anvil-tool-cargo-ensure-no-cyclic-deps-install installer) +anvil-ensure-no-cyclic-deps-setup installer="install": (anvil-tool-cargo-ensure-no-cyclic-deps-install installer) (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-ensure-no-cyclic-deps` recipe. [group("anvil-setup")] -anvil-ensure-no-cyclic-deps-validate-prereqs: anvil-tool-cargo-ensure-no-cyclic-deps-validate-prereqs +anvil-ensure-no-cyclic-deps-validate-prereqs: anvil-tool-cargo-ensure-no-cyclic-deps-validate-prereqs anvil-tool-cargo-each-validate-prereqs === justfiles/anvil/checks/ensure-no-default-features.just === # Copyright (c) Microsoft Corporation. @@ -2057,18 +2055,18 @@ anvil-ensure-no-cyclic-deps-validate-prereqs: anvil-tool-cargo-ensure-no-cyclic- [script("pwsh", "-NoProfile")] anvil-ensure-no-default-features: anvil-ensure-no-default-features-validate-prereqs anvil-impact $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 {{_anvil_stable_toolchain_args}} ensure-no-default-features + $selection = @(& "{{ just_executable() }}" _anvil-impact-include modified) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' 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. [group("anvil-setup")] -anvil-ensure-no-default-features-setup installer="install": (anvil-tool-cargo-ensure-no-default-features-install installer) +anvil-ensure-no-default-features-setup installer="install": (anvil-tool-cargo-ensure-no-default-features-install installer) (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-ensure-no-default-features` recipe. [group("anvil-setup")] -anvil-ensure-no-default-features-validate-prereqs: anvil-tool-cargo-ensure-no-default-features-validate-prereqs +anvil-ensure-no-default-features-validate-prereqs: anvil-tool-cargo-ensure-no-default-features-validate-prereqs anvil-tool-cargo-each-validate-prereqs === justfiles/anvil/checks/examples.just === # Copyright (c) Microsoft Corporation. @@ -2085,18 +2083,18 @@ anvil-ensure-no-default-features-validate-prereqs: anvil-tool-cargo-ensure-no-de [script("pwsh", "-NoProfile")] 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 {{_anvil_stable_toolchain_args}} build @(if ($include) { -split $include } else { '--workspace' }) --examples --all-features --locked + $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo {{_anvil_stable_toolchain_args}} build '{packages}' --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-toolchain-stable-install +anvil-examples-setup installer="install": anvil-toolchain-stable-install (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-examples` recipe. [group("anvil-setup")] -anvil-examples-validate-prereqs: anvil-tool-rustc-validate-prereqs +anvil-examples-validate-prereqs: anvil-tool-rustc-validate-prereqs anvil-tool-cargo-each-validate-prereqs === justfiles/anvil/checks/external-types.just === # Copyright (c) Microsoft Corporation. @@ -2113,87 +2111,29 @@ anvil-examples-validate-prereqs: anvil-tool-rustc-validate-prereqs # only --manifest-path. Iterate the affected library crates and run # the tool once each, pointing at the crate's Cargo.toml. Bin-only # crates have no public API surface and are skipped. -# TODO(anvil-runner): even after a `cargo ox-run` helper absorbs the -# skip/splat preamble, this recipe stays multi-step: cargo-check- -# external-types is per-manifest (no --package/--workspace), so we -# build a name->manifest map from cargo metadata, filter to lib crates, -# intersect with the affected set ($include), and call the tool once per -# crate. Hard-fails on errors (no tolerance, unlike semver-check). +# cargo-each resolves the affected selection, filters it to library crates, +# substitutes each manifest path, and preserves aggregate failure reporting. # Check public APIs for unapproved external types. [script("pwsh", "-NoProfile")] anvil-external-types: anvil-external-types-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - $include = (& "{{ just_executable() }}" _anvil-impact-include affected) - if ($include -eq '--skip') { - Write-Host 'anvil-external-types: no affected packages; skipping' - exit 0 - } - $pkg = @(if ($include) { -split $include } else { '--workspace' }) - # Build pkg-name -> manifest-path map, restricted to library crates. - $libPkgs = @{} - $metadataJson = & cargo {{_anvil_stable_toolchain_args}} metadata --no-deps --format-version 1 + $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + Write-Host "anvil-external-types: catalog tool minimum cargo-check-external-types {{ cargo_check_external_types_version }}; rustdoc toolchain {{ rust_nightly_external_types }}" + & cargo {{_anvil_stable_toolchain_args}} each @selection --filter lib --keep-going '--' cargo '+{{ rust_nightly_external_types }}' check-external-types --manifest-path '{manifest}' if ($LASTEXITCODE -ne 0) { - Write-Error 'anvil-external-types: cargo metadata failed' + Write-Error "anvil-external-types: one or more crates failed. The installed tool may be newer than the catalog minimum; it must remain compatible with rustdoc from {{ rust_nightly_external_types }}." exit $LASTEXITCODE } - try { - $meta = $metadataJson | ConvertFrom-Json - } catch { - Write-Error "anvil-external-types: could not parse cargo metadata output: $_" - exit 1 - } - foreach ($p in $meta.packages) { - if ($p.targets | Where-Object { $_.kind -contains 'lib' }) { - $libPkgs[$p.name] = $p.manifest_path - } - } - # Decide which packages to check. - $packages = @() - if ($pkg -contains '--workspace') { - $packages = $libPkgs.Keys - } else { - for ($i = 0; $i -lt $pkg.Count; $i++) { - if ($pkg[$i] -eq '--package' -and ($i + 1) -lt $pkg.Count) { - # Impact emits version-qualified specs (name@version); - # strip the @version to recover the bare package name - # this manifest-path map keys on. - $name = ($pkg[$i + 1] -split '@', 2)[0] - if ($libPkgs.ContainsKey($name)) { $packages += $name } - $i++ - } - } - } - if ($packages.Count -eq 0) { - Write-Host 'anvil-external-types: no affected library crates; skipping' - exit 0 - } - Write-Host "anvil-external-types: catalog tool minimum cargo-check-external-types {{ cargo_check_external_types_version }}; rustdoc toolchain {{ rust_nightly_external_types }}" - $failed = $false - foreach ($p in $packages) { - Write-Host "anvil-external-types: $p" - # cargo-check-external-types requires nightly rustdoc (uses - # unstable -Z flags) AND pins a specific rustdoc-types schema - # version. We pin nightly narrowly to the schema this tool - # version expects via `rust_nightly_external_types` in - # versions.just — bump that pin alongside any cargo-check- - # external-types upgrade. No tolerance for schema mismatches: - # if it fails, the pin or the tool needs to move. - & cargo '+{{ rust_nightly_external_types }}' check-external-types --manifest-path $libPkgs[$p] - if ($LASTEXITCODE -ne 0) { - Write-Error "anvil-external-types: '$p' failed. The installed tool may be newer than the catalog minimum; it must remain compatible with rustdoc from {{ rust_nightly_external_types }}." - $failed = $true - } - } - if ($failed) { exit 1 } # Install prerequisites for the `anvil-external-types` recipe. [group("anvil-setup")] -anvil-external-types-setup installer="install": anvil-toolchain-nightly-external-types-install (anvil-tool-cargo-check-external-types-install installer) +anvil-external-types-setup installer="install": anvil-toolchain-nightly-external-types-install (anvil-tool-cargo-check-external-types-install installer) (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-external-types` recipe. [group("anvil-setup")] -anvil-external-types-validate-prereqs: anvil-toolchain-nightly-external-types-validate-prereqs anvil-tool-cargo-check-external-types-validate-prereqs +anvil-external-types-validate-prereqs: anvil-toolchain-nightly-external-types-validate-prereqs anvil-tool-cargo-check-external-types-validate-prereqs anvil-tool-cargo-each-validate-prereqs === justfiles/anvil/checks/fmt.just === # Copyright (c) Microsoft Corporation. @@ -2219,9 +2159,9 @@ anvil-external-types-validate-prereqs: anvil-toolchain-nightly-external-types-va [script("pwsh", "-NoProfile")] anvil-fmt: anvil-fmt-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - $include = (& "{{ just_executable() }}" _anvil-impact-include modified) - if ($include -eq '--skip') { Write-Host 'anvil-fmt: no modified packages; skipping'; exit 0 } - cargo {{_anvil_stable_toolchain_args}} each --workspace --keep-going '--' cargo '+{{ rust_nightly }}' fmt --manifest-path '{manifest}' --check + $selection = @(& "{{ just_executable() }}" _anvil-impact-include modified) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' 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 @@ -2262,18 +2202,18 @@ anvil-fmt-validate-prereqs: anvil-component-nightly-rustfmt-validate-prereqs anv [script("pwsh", "-NoProfile")] 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 {{_anvil_stable_toolchain_args}} heather + $selection = @(& "{{ just_executable() }}" _anvil-impact-include modified) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo {{_anvil_stable_toolchain_args}} heather if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-license-headers` recipe. [group("anvil-setup")] -anvil-license-headers-setup installer="install": (anvil-tool-cargo-heather-install installer) +anvil-license-headers-setup installer="install": (anvil-tool-cargo-heather-install installer) (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-license-headers` recipe. [group("anvil-setup")] -anvil-license-headers-validate-prereqs: anvil-tool-cargo-heather-validate-prereqs +anvil-license-headers-validate-prereqs: anvil-tool-cargo-heather-validate-prereqs anvil-tool-cargo-each-validate-prereqs === justfiles/anvil/checks/llvm-cov.just === # Copyright (c) Microsoft Corporation. @@ -2296,12 +2236,13 @@ anvil-license-headers-validate-prereqs: anvil-tool-cargo-heather-validate-prereq [script("pwsh", "-NoProfile")] anvil-llvm-cov: anvil-llvm-cov-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - $include = (& "{{ just_executable() }}" _anvil-impact-include affected) - if ($include -eq '--skip') { + $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + if ($selection -contains '--none') { Write-Host 'anvil-llvm-cov: no affected packages; skipping' exit 0 } - $pkg = @(if ($include) { -split $include } else { '--workspace' }) + $pkg = $selection $allTestPkg = $pkg # Separate coverage-opted-out packages from the measured set. A crate that # is entirely `#![cfg_attr(coverage_nightly, coverage(off))]` (e.g. a @@ -2328,11 +2269,7 @@ anvil-llvm-cov: anvil-llvm-cov-validate-prereqs anvil-impact if ($optedOut.Count -gt 0) { $filtered = [System.Collections.Generic.List[string]]::new() for ($i = 0; $i -lt $pkg.Count; $i++) { - # Impact emits version-qualified specs (name@version); - # compare the bare name against the opt-out set, but keep - # the full name@version token in $pkg so the llvm-cov - # measurement below resolves unambiguously. - if ($pkg[$i] -eq '--package' -and ($i + 1) -lt $pkg.Count -and $optedOut -contains (($pkg[$i + 1] -split '@', 2)[0])) { + if ($pkg[$i] -eq '--package' -and ($i + 1) -lt $pkg.Count -and $optedOut -contains $pkg[$i + 1]) { $testOnlyPkg.Add('--package') $testOnlyPkg.Add($pkg[$i + 1]) $i++ @@ -2433,14 +2370,12 @@ anvil-llvm-cov: anvil-llvm-cov-validate-prereqs anvil-impact # report NO DATA and fail the gate. When unscoped, $pkg is `--workspace` # (no gate flag); drop it so the gate defaults to every member. # - # cargo-coverage-gate keys on bare package names from cargo metadata, so - # strip the @version qualifier the measurement used for disambiguation. $gateArgs = [System.Collections.Generic.List[string]]::new() for ($i = 0; $i -lt $pkg.Count; $i++) { if ($pkg[$i] -eq '--workspace') { continue } if ($pkg[$i] -eq '--package' -and ($i + 1) -lt $pkg.Count) { $gateArgs.Add('--package') - $gateArgs.Add(($pkg[$i + 1] -split '@', 2)[0]) + $gateArgs.Add($pkg[$i + 1]) $i++ continue } @@ -2514,8 +2449,9 @@ anvil-llvm-cov-validate-prereqs: anvil-component-nightly-llvm-tools-validate-pre [script("pwsh", "-NoProfile")] anvil-loom: anvil-loom-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - $include = (& "{{ just_executable() }}" _anvil-impact-include affected) - if ($include -eq '--skip') { + $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + if ($selection -contains '--none') { Write-Host 'anvil-loom: no affected packages; skipping' exit 0 } @@ -2523,17 +2459,17 @@ anvil-loom: anvil-loom-validate-prereqs anvil-impact if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Optional impact-scoping: recover bare package names from the - # impact step's version-qualified `--package name@version` pairs. + # impact step's `--package name` pairs. # Full-workspace modes (`--workspace` -- e.g. the scheduled tier, # ANVIL_IMPACT=off, or no impact cache) carry no `--package` tokens, # so $affected stays $null and the package set is left unfiltered # (filtering on an empty set would wrongly skip every loom target). $affected = $null - $tokens = @(-split $include) + $tokens = $selection for ($i = 0; $i -lt $tokens.Count; $i++) { if ($tokens[$i] -eq '--package' -and ($i + 1) -lt $tokens.Count) { if ($null -eq $affected) { $affected = @() } - $affected += ($tokens[$i + 1] -split '@', 2)[0] + $affected += $tokens[$i + 1] $i++ } } @@ -2608,25 +2544,24 @@ anvil-loom-validate-prereqs: anvil-tool-rustc-validate-prereqs [script("pwsh", "-NoProfile")] anvil-miri-race-coverage: anvil-miri-race-coverage-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - $include = (& "{{ just_executable() }}" _anvil-impact-include affected) - if ($include -eq '--skip') { Write-Host 'anvil-miri-race-coverage: no affected packages; skipping'; exit 0 } - $pkg = @(if ($include) { -split $include } else { '--workspace' }) + $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } $day = (Get-Date).Day $low = (2 * $day) - 1 $high = (2 * $day) + 1 $env:MIRIFLAGS = "-Zmiri-many-seeds=$low..$high $($env:MIRIFLAGS)".Trim() $env:RUSTFLAGS = "--cfg miri_race_coverage $($env:RUSTFLAGS)".Trim() Write-Host "anvil-miri-race-coverage: seed window $low..$high (day $day)" - & cargo '+{{ rust_nightly }}' miri test --all-features --tests @pkg + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo '+{{ rust_nightly }}' miri test --all-features --tests '{packages}' if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-miri-race-coverage` recipe. [group("anvil-setup")] -anvil-miri-race-coverage-setup installer="install": anvil-component-nightly-miri-install anvil-component-nightly-rust-src-install +anvil-miri-race-coverage-setup installer="install": anvil-component-nightly-miri-install anvil-component-nightly-rust-src-install (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-miri-race-coverage` recipe. [group("anvil-setup")] -anvil-miri-race-coverage-validate-prereqs: anvil-component-nightly-miri-validate-prereqs anvil-component-nightly-rust-src-validate-prereqs +anvil-miri-race-coverage-validate-prereqs: anvil-component-nightly-miri-validate-prereqs anvil-component-nightly-rust-src-validate-prereqs anvil-tool-cargo-each-validate-prereqs === justfiles/anvil/checks/miri-strict-provenance.just === # Copyright (c) Microsoft Corporation. @@ -2641,21 +2576,20 @@ anvil-miri-race-coverage-validate-prereqs: anvil-component-nightly-miri-validate [script("pwsh", "-NoProfile")] anvil-miri-strict-provenance: anvil-miri-strict-provenance-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - $include = (& "{{ just_executable() }}" _anvil-impact-include affected) - if ($include -eq '--skip') { Write-Host 'anvil-miri-strict-provenance: no affected packages; skipping'; exit 0 } - $pkg = @(if ($include) { -split $include } else { '--workspace' }) + $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } $env:MIRIFLAGS = "-Zmiri-strict-provenance $($env:MIRIFLAGS)".Trim() $env:RUSTFLAGS = "--cfg miri_strict_provenance $($env:RUSTFLAGS)".Trim() - & cargo '+{{ rust_nightly }}' miri test --all-features --tests @pkg + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo '+{{ rust_nightly }}' miri test --all-features --tests '{packages}' if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-miri-strict-provenance` recipe. [group("anvil-setup")] -anvil-miri-strict-provenance-setup installer="install": anvil-component-nightly-miri-install anvil-component-nightly-rust-src-install +anvil-miri-strict-provenance-setup installer="install": anvil-component-nightly-miri-install anvil-component-nightly-rust-src-install (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-miri-strict-provenance` recipe. [group("anvil-setup")] -anvil-miri-strict-provenance-validate-prereqs: anvil-component-nightly-miri-validate-prereqs anvil-component-nightly-rust-src-validate-prereqs +anvil-miri-strict-provenance-validate-prereqs: anvil-component-nightly-miri-validate-prereqs anvil-component-nightly-rust-src-validate-prereqs anvil-tool-cargo-each-validate-prereqs === justfiles/anvil/checks/miri-tree-borrows.just === # Copyright (c) Microsoft Corporation. @@ -2678,18 +2612,17 @@ anvil-miri-strict-provenance-validate-prereqs: anvil-component-nightly-miri-vali # that tier sets ANVIL_IMPACT=off, so _anvil-impact-include resolves # to --workspace. A direct local invocation is impact-scoped like any # other affected-tier check -- it reads the anvil-impact cache and -# honors --skip. +# honors an empty selection. # Run Miri tests with the Tree Borrows model. [script("pwsh", "-NoProfile")] anvil-miri-tree-borrows: anvil-miri-tree-borrows-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - $include = (& "{{ just_executable() }}" _anvil-impact-include affected) - if ($include -eq '--skip') { Write-Host 'anvil-miri-tree-borrows: no affected packages; skipping'; exit 0 } - $pkg = @(if ($include) { -split $include } else { '--workspace' }) + $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } $env:MIRIFLAGS = "-Zmiri-tree-borrows $($env:MIRIFLAGS)".Trim() $env:RUSTFLAGS = "--cfg miri_tree_borrows $($env:RUSTFLAGS)".Trim() - & cargo '+{{ rust_nightly }}' miri test --all-features --tests @pkg + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo '+{{ rust_nightly }}' miri test --all-features --tests '{packages}' if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # The three nightly-miri profiles share the same toolchain prereqs @@ -2699,11 +2632,11 @@ anvil-miri-tree-borrows: anvil-miri-tree-borrows-validate-prereqs anvil-impact # Install prerequisites for the `anvil-miri-tree-borrows` recipe. [group("anvil-setup")] -anvil-miri-tree-borrows-setup installer="install": anvil-component-nightly-miri-install anvil-component-nightly-rust-src-install +anvil-miri-tree-borrows-setup installer="install": anvil-component-nightly-miri-install anvil-component-nightly-rust-src-install (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-miri-tree-borrows` recipe. [group("anvil-setup")] -anvil-miri-tree-borrows-validate-prereqs: anvil-component-nightly-miri-validate-prereqs anvil-component-nightly-rust-src-validate-prereqs +anvil-miri-tree-borrows-validate-prereqs: anvil-component-nightly-miri-validate-prereqs anvil-component-nightly-rust-src-validate-prereqs anvil-tool-cargo-each-validate-prereqs === justfiles/anvil/checks/miri.just === # Copyright (c) Microsoft Corporation. @@ -2724,8 +2657,8 @@ anvil-miri-tree-borrows-validate-prereqs: anvil-component-nightly-miri-validate- [script("pwsh", "-NoProfile")] anvil-miri: anvil-miri-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - $include = (& "{{ just_executable() }}" _anvil-impact-include affected) - if ($include -eq '--skip') { Write-Host 'anvil-miri: no affected packages; skipping'; exit 0 } + $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Run miri via libtest (`cargo miri test`), NOT `cargo miri nextest # run`. nextest uses a process-per-test model; under miri every # process pays the full cost of re-interpreting std initialization, @@ -2751,18 +2684,18 @@ anvil-miri: anvil-miri-validate-prereqs anvil-impact # whose tests are all `#[cfg_attr(miri, ignore)]` -- the canonical # opt-out for build-tooling / CLI crates), so the old nextest # `--no-tests=pass` workaround is no longer needed. - & cargo '+{{ rust_nightly }}' miri test --all-features --tests @(if ($include) { -split $include } else { '--workspace' }) + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo '+{{ rust_nightly }}' miri test --all-features --tests '{packages}' if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # --- pr-runtime-analysis members --- # Install prerequisites for the `anvil-miri` recipe. [group("anvil-setup")] -anvil-miri-setup installer="install": anvil-component-nightly-miri-install anvil-component-nightly-rust-src-install +anvil-miri-setup installer="install": anvil-component-nightly-miri-install anvil-component-nightly-rust-src-install (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-miri` recipe. [group("anvil-setup")] -anvil-miri-validate-prereqs: anvil-component-nightly-miri-validate-prereqs anvil-component-nightly-rust-src-validate-prereqs +anvil-miri-validate-prereqs: anvil-component-nightly-miri-validate-prereqs anvil-component-nightly-rust-src-validate-prereqs anvil-tool-cargo-each-validate-prereqs === justfiles/anvil/checks/msrv-test.just === # Copyright (c) Microsoft Corporation. @@ -2777,10 +2710,10 @@ anvil-miri-validate-prereqs: anvil-component-nightly-miri-validate-prereqs anvil [script("pwsh", "-NoProfile")] anvil-msrv-test: anvil-msrv-test-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - $include = (& "{{ just_executable() }}" _anvil-impact-include affected) + $selection = @(& "{{ 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 } + if ($selection -contains '--none') { 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 } @@ -2789,23 +2722,22 @@ anvil-msrv-test: anvil-msrv-test-validate-prereqs anvil-impact 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 + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo "+$toolchain" test '{packages}' --all-targets --all-features --locked if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - & cargo "+$toolchain" test @pkg --all-targets --locked + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo "+$toolchain" test '{packages}' --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": +anvil-msrv-test-setup installer="install": (anvil-tool-cargo-each-install installer) & "{{ 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: +anvil-msrv-test-validate-prereqs: anvil-tool-cargo-each-validate-prereqs $toolchainOutput = & "{{ just_executable() }}" _anvil-resolve-stable msrv | Out-String $resolverExit = $LASTEXITCODE if ($resolverExit -ne 0) { exit $resolverExit } @@ -2845,12 +2777,13 @@ anvil-msrv-test-validate-prereqs: [script("pwsh", "-NoProfile")] anvil-mutants-diff: anvil-mutants-diff-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - $include = (& "{{ just_executable() }}" _anvil-impact-include affected) + $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } if ($IsWindows -and ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64' -or $env:PROCESSOR_ARCHITEW6432 -eq 'ARM64')) { Write-Host 'anvil-mutants-diff: anvil-mutants-diff-validate-prereqs aarch64-pc-windows-msvc -- cargo-mutants does not build here (winapi); skipping' exit 0 } - if ($include -eq '--skip') { + if ($selection -contains '--none') { Write-Host 'anvil-mutants-diff: anvil-mutants-diff-validate-prereqs no affected packages; skipping' exit 0 } @@ -2875,18 +2808,18 @@ 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 {{_anvil_stable_toolchain_args}} mutants --in-diff $diff_path --no-shuffle --jobs 0 + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo {{_anvil_stable_toolchain_args}} mutants --in-diff $diff_path --no-shuffle --jobs 0 if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # --- pr-mutants members --- # Install prerequisites for the `anvil-mutants-diff` recipe. [group("anvil-setup")] -anvil-mutants-diff-setup installer="install": (anvil-tool-cargo-mutants-install installer) +anvil-mutants-diff-setup installer="install": (anvil-tool-cargo-mutants-install installer) (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-mutants-diff` recipe. [group("anvil-setup")] -anvil-mutants-diff-validate-prereqs: anvil-tool-cargo-mutants-validate-prereqs +anvil-mutants-diff-validate-prereqs: anvil-tool-cargo-mutants-validate-prereqs anvil-tool-cargo-each-validate-prereqs === justfiles/anvil/checks/mutants-full.just === # Copyright (c) Microsoft Corporation. @@ -3172,7 +3105,7 @@ anvil-readme-check-validate-prereqs: anvil-tool-cargo-doc2readme-validate-prereq # TODO(anvil-runner): even after a `cargo ox-run` helper absorbs the # skip/splat preamble, this recipe stays multi-step: cargo-metadata # filter to publishable library crates (cargo-semver-checks --workspace -# fails on bin-only workspaces), intersect the affected set ($include) with that +# fails on bin-only workspaces), intersect the affected selection with that # set, verify the PR branch baseline is present, skip newly-added # manifest paths, then invoke per-crate against that baseline with # special handling for renamed crates ("package `` not found in @@ -3183,14 +3116,15 @@ anvil-readme-check-validate-prereqs: anvil-tool-cargo-doc2readme-validate-prereq anvil-semver-check: anvil-semver-check-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $stableArgs = {{_anvil_stable_toolchain_args}} - $include = (& "{{ just_executable() }}" _anvil-impact-include affected) + $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } $commentFile = 'target/anvil/comments/semver.md' - if ($include -eq '--skip') { + if ($selection -contains '--none') { Write-Host 'anvil-semver-check: no affected packages; skipping' Remove-Item -LiteralPath $commentFile -ErrorAction SilentlyContinue exit 0 } - $pkg = @(if ($include) { -split $include } else { '--workspace' }) + $pkg = $selection # Build the publishable candidate package list. Keep each package's metadata so # its manifest path can also be checked in the baseline. Always # iterate per-package over library crates only -- cargo-semver-checks @@ -3225,9 +3159,7 @@ anvil-semver-check: anvil-semver-check-validate-prereqs anvil-impact $packages = @() for ($i = 0; $i -lt $pkg.Count; $i++) { if ($pkg[$i] -eq '--package' -and ($i + 1) -lt $pkg.Count) { - # Impact emits version-qualified specs (name@version); - # strip the @version to recover the bare package name. - $name = ($pkg[$i + 1] -split '@', 2)[0] + $name = $pkg[$i + 1] if ($libPkgs[$name]) { $packages += $name } $i++ } @@ -3471,24 +3403,23 @@ anvil-spellcheck-validate-prereqs: anvil-tool-cargo-spellcheck-validate-prereqs [script("pwsh", "-NoProfile")] anvil-udeps: anvil-udeps-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - $include = (& "{{ just_executable() }}" _anvil-impact-include required) - if ($include -eq '--skip') { Write-Host 'anvil-udeps: no affected packages; skipping'; exit 0 } - $pkg = @(if ($include) { -split $include } else { '--workspace' }) + $selection = @(& "{{ just_executable() }}" _anvil-impact-include required) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Pass 1: default targets (lib + bins) -- surfaces [dependencies] that # are only used by tests/benches/examples (should be dev-deps). - & cargo '+{{ rust_nightly }}' udeps @pkg --all-features + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo '+{{ rust_nightly }}' udeps '{packages}' --all-features if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Pass 2: --all-targets -- surfaces unused [dev-dependencies]. - & cargo '+{{ rust_nightly }}' udeps @pkg --all-features --all-targets + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo '+{{ rust_nightly }}' udeps '{packages}' --all-features --all-targets if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-udeps` recipe. [group("anvil-setup")] -anvil-udeps-setup installer="install": anvil-toolchain-nightly-install (anvil-tool-cargo-udeps-install installer) +anvil-udeps-setup installer="install": anvil-toolchain-nightly-install (anvil-tool-cargo-udeps-install installer) (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-udeps` recipe. [group("anvil-setup")] -anvil-udeps-validate-prereqs: anvil-toolchain-nightly-validate-prereqs anvil-tool-cargo-udeps-validate-prereqs +anvil-udeps-validate-prereqs: anvil-toolchain-nightly-validate-prereqs anvil-tool-cargo-udeps-validate-prereqs anvil-tool-cargo-each-validate-prereqs === justfiles/anvil/container.just === # Copyright (c) Microsoft Corporation. @@ -5085,30 +5016,26 @@ anvil-scheduled-test-validate-prereqs: \ # unscoped by design. # # Each scoped check depends on `anvil-impact` (which produces the -# target/anvil/impact cache) and captures its category's scope into a local -# variable by calling the shared resolver: +# target/anvil/impact cache) and captures its category's selector tokens by +# calling the shared resolver: # -# $include = (& "{{ just_executable() }}" _anvil-impact-include ) +# $selection = @(& "{{ just_executable() }}" _anvil-impact-include ) # # _anvil-impact-include reads that cache both locally and in cloud workflows # (where the group job downloaded the target/anvil/impact artifact), so the # same code path runs everywhere -- no scoping is threaded between jobs via -# environment variables. `$include` is one of: +# environment variables. `$selection` is one of: # -# * "--workspace" (affected/required) or "" (modified) - the tier is -# unscoped: ANVIL_IMPACT=off, or no impact cache is present. Recipes splat -# "--workspace" (an empty string is NOT $null, so recipes test truthiness -# rather than `?? "--workspace"`, which would splat an empty arg cargo -# rejects). -# * "--package A@v --package B@v" - the tier has members. Packages are -# version-qualified cargo specs so they resolve uniquely even when a -# like-named crate is also a transitive dependency. -# * "--skip" - the tier is empty for this change (recipe exits 0). +# * "--workspace" - the tier is unscoped: ANVIL_IMPACT=off, or no impact +# cache is present. +# * "--package", "A", "--package", "B" - the tier has members. One token is +# emitted per line so PowerShell captures a ready-to-splat array. +# * "--none" - the tier is empty; cargo-each exits successfully without +# invoking the child command. # -# This keeps the per-recipe boilerplate to a single capture, a one-line skip -# guard, and the cargo invocation. Modified-tier recipes never forward -# `$include`; they only check the skip sentinel and then let their command -# determine its normal input domain. +# Ordinary recipes pass the array to cargo-each. Modified-tier recipes use +# --once without {packages}, so the selector admits or skips the command while +# the command retains its normal input domain. # # Every recipe whose body uses multi-line conditionals or array-splat # splicing is annotated with [script("pwsh", "-NoProfile")]. pwsh is preinstalled on @@ -5209,7 +5136,7 @@ _anvil-unscoped name: # it, so the checks read the cache instead of recomputing. # # The base ref is resolved by the shared _anvil-base-ref helper (helpers.just, -# also reused by anvil-mutants-diff). The impact -> `--package name@version` +# also reused by anvil-mutants-diff). The impact -> cargo selector # tier projection is _anvil-impact-format, defined in THIS file next to its # sole caller; this recipe orchestrates the snapshots + caching around both so # the exact same projection is used locally and in CI. @@ -5519,11 +5446,11 @@ _anvil-impact-snapshot: } # Public. Computes the impact set from the two snapshots and projects each -# tier into a pre-built `--package name@version ...` string (or the sentinel -# `--skip` when the tier is empty) via the shared _anvil-impact-format -# helper, written to include_.txt. Per-crate checks depend on this -# recipe and read those files via _anvil-impact-include. Cache-aware: no-ops -# when the snapshots haven't changed. +# tier into tokenized `--package name` selectors (or `--none` when the tier is +# empty) via the shared _anvil-impact-format helper, written to +# include_.txt. Per-crate checks depend on this recipe and read those +# files via _anvil-impact-include. Cache-aware: no-ops when the snapshots +# haven't changed. # Compute the impact set and write the target/anvil/impact cache. [group("anvil")] @@ -5582,7 +5509,7 @@ anvil-impact: _anvil-impact-snapshot if ($dirty.Count -gt 0) { Write-Host "anvil-impact: warning: $($dirty.Count) uncommitted change(s) in the working tree are invisible to cargo-delta (it scopes on the committed diff vs the base ref); widening all tiers to --workspace so nothing you are editing is skipped -- commit to scope by impact, or set ANVIL_IMPACT=off to disable scoping." -ForegroundColor Yellow [System.IO.Directory]::CreateDirectory($impactDir) | Out-Null - Set-Content -LiteralPath (Join-Path $impactDir 'include_modified.txt') -Value '' -Encoding UTF8 -NoNewline + Set-Content -LiteralPath (Join-Path $impactDir 'include_modified.txt') -Value '--workspace' -Encoding UTF8 -NoNewline Set-Content -LiteralPath (Join-Path $impactDir 'include_affected.txt') -Value '--workspace' -Encoding UTF8 -NoNewline Set-Content -LiteralPath (Join-Path $impactDir 'include_required.txt') -Value '--workspace' -Encoding UTF8 -NoNewline Set-Content -LiteralPath $impactJson -Value '{}' -Encoding UTF8 @@ -5606,11 +5533,11 @@ anvil-impact: _anvil-impact-snapshot # First-time anvil adoption: the base predates the cargo workspace, so # there is no baseline to diff against. Fall back to full-workspace - # validation -- modified runs unconditionally (empty), affected/required - # default to --workspace -- rather than failing the impact computation. + # validation -- every tier defaults to --workspace rather than failing the + # impact computation. if (Test-Path (Join-Path $snapDir 'baseline-no-workspace')) { Write-Host 'anvil-impact: baseline has no workspace; defaulting all tiers to full-workspace' - Set-Content -LiteralPath (Join-Path $impactDir 'include_modified.txt') -Value '' -Encoding UTF8 -NoNewline + Set-Content -LiteralPath (Join-Path $impactDir 'include_modified.txt') -Value '--workspace' -Encoding UTF8 -NoNewline Set-Content -LiteralPath (Join-Path $impactDir 'include_affected.txt') -Value '--workspace' -Encoding UTF8 -NoNewline Set-Content -LiteralPath (Join-Path $impactDir 'include_required.txt') -Value '--workspace' -Encoding UTF8 -NoNewline Set-Content -LiteralPath $impactJson -Value '{}' -Encoding UTF8 @@ -5635,20 +5562,19 @@ anvil-impact: _anvil-impact-snapshot if ([string]::IsNullOrWhiteSpace($impactOut)) { $impactOut = '{}' } Set-Content -LiteralPath $impactJson -Value $impactOut.Trim() -Encoding UTF8 - # Project each tier into its `--package name@version ...` / `--skip` - # string via the shared formatter (version-qualified specs + lib-name -> - # package-name translation live there, used identically by the cloud - # impact steps). + # Project each tier into one selector token per line. The formatter owns + # lib-name -> package-name translation; cargo-each resolves package + # versions from live metadata before invoking child commands. foreach ($tier in @('modified', 'affected', 'required')) { - $value = (& "{{ just_executable() }}" _anvil-impact-format $tier $impactJson) + $value = @(& "{{ just_executable() }}" _anvil-impact-format $tier $impactJson) if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - Set-Content -LiteralPath (Join-Path $impactDir "include_$tier.txt") -Value $value.Trim() -Encoding UTF8 -NoNewline - Write-Host "anvil-impact: $tier -> $($value.Trim())" + Set-Content -LiteralPath (Join-Path $impactDir "include_$tier.txt") -Value $value -Encoding UTF8 + Write-Host "anvil-impact: $tier -> $($value -join ' ')" } Set-Content -LiteralPath $impactStateFile -Value $wantState -Encoding UTF8 -NoNewline # _anvil-impact-format: turns a cargo-delta impact report into the -# `--package ...` string the scoped checks consume, projected per +# selector tokens the scoped checks consume, projected per # tier into include_.txt by `anvil-impact` above -- its only caller, # next to which it now lives (impact analysis stays in one place). # @@ -5658,14 +5584,11 @@ anvil-impact: _anvil-impact-snapshot # impactJson - path to the file the caller produced via # `cargo delta impact --format json`. # -# Output (stdout, exactly one line so the caller can capture it): -# * "--package name@version --package name2@version2 ..." when the -# tier has members. Each package is a version-qualified cargo spec: -# the version (read from `cargo metadata --no-deps`, i.e. workspace -# members only) disambiguates a workspace member from a like-named -# transitive dependency pulled in at a different version, which a -# bare `-p name` cannot do ("multiple packages named name"). -# * "--skip" when the tier is empty (recipes treat this as a no-op). +# Output (stdout, one token per line so PowerShell captures a ready-to-splat array): +# * repeated "--package" / bare workspace package-name pairs when the +# tier has members. cargo-each resolves those names from live metadata +# and emits version-qualified specs to child Cargo commands. +# * "--none" when the tier is empty (cargo-each treats this as a no-op). # Metadata failures and unknown/ambiguous package names are written to # STDERR and fail the recipe, so callers never continue with an incomplete # package set (which would silently under-scope and skip a check). @@ -5750,7 +5673,7 @@ _anvil-impact-format tier impactJson: } $impact = Get-Content -LiteralPath $impactJson -Raw | ConvertFrom-Json $names = @($impact.$tierKey) - $specs = [System.Collections.Generic.List[string]]::new() + $selectors = [System.Collections.Generic.List[string]]::new() foreach ($name in $names) { if ([string]::IsNullOrWhiteSpace($name)) { continue } # Union candidates from every namespace, de-duplicated by package id @@ -5786,20 +5709,21 @@ _anvil-impact-format tier impactJson: exit 1 } $p = $candidates[0] - $specs.Add("--package $($p.name)@$($p.version)") + $selectors.Add('--package') + $selectors.Add($p.name) } - if ($specs.Count -eq 0) { - Write-Output '--skip' + if ($selectors.Count -eq 0) { + Write-Output '--none' } else { - Write-Output ($specs -join ' ') + Write-Output $selectors } # Private. Resolves an impact category's scope for a check recipe and echoes it on stdout. # 1. ANVIL_IMPACT=off -> tier default (full-workspace / run). -# 2. include_.txt present -> its contents (a --package list or --skip). +# 2. include_.txt present -> its selector tokens. # 3. otherwise -> tier default. -# Tier default: --workspace for affected/required; empty (run) for modified. -# Each scoped check calls this directly (into a local $include variable) to +# Tier default: --workspace for every tier. +# Each scoped check calls this directly (into a local $selection array) to # read its category's scope from the target/anvil/impact cache -- the same cache # both locally and in cloud workflows (where the group job downloaded it). @@ -5808,7 +5732,7 @@ _anvil-impact-format tier impactJson: _anvil-impact-include tier: $ErrorActionPreference = 'Stop' $tier = '{{ tier }}' - $default = if ($tier -eq 'modified') { '' } else { '--workspace' } + $default = '--workspace' # Exit 2 = invalid caller configuration (bad ANVIL_IMPACT), distinct from # the generic 1 used for operational failures -- see _anvil-impact-snapshot. if ($env:ANVIL_IMPACT -and $env:ANVIL_IMPACT -notin @('off', 'consume')) { @@ -5818,7 +5742,7 @@ _anvil-impact-include tier: if ($env:ANVIL_IMPACT -eq 'off') { Write-Output $default; exit 0 } $file = "target/anvil/impact/include_$tier.txt" if (Test-Path $file) { - Write-Output ("$(Get-Content -Raw $file)".Trim()) + Get-Content -LiteralPath $file | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } } else { Write-Output $default } diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap index b9a3237a9..63059724f 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap @@ -1585,9 +1585,9 @@ anvil-audit-validate-prereqs: anvil-tool-cargo-audit-validate-prereqs [script("pwsh", "-NoProfile")] 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 {{_anvil_stable_toolchain_args}} bench @(if ($include) { -split $include } else { '--workspace' }) --all-features --no-run + $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo {{_anvil_stable_toolchain_args}} bench '{packages}' --all-features --no-run if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # bench uses the cargo built-in in compile-only mode; no extra tool install @@ -1595,11 +1595,11 @@ anvil-bench: anvil-bench-validate-prereqs anvil-impact # Install prerequisites for the `anvil-bench` recipe. [group("anvil-setup")] -anvil-bench-setup installer="install": anvil-toolchain-stable-install +anvil-bench-setup installer="install": anvil-toolchain-stable-install (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-bench` recipe. [group("anvil-setup")] -anvil-bench-validate-prereqs: anvil-tool-rustc-validate-prereqs +anvil-bench-validate-prereqs: anvil-tool-rustc-validate-prereqs anvil-tool-cargo-each-validate-prereqs === justfiles/anvil/checks/bolero.just === # Copyright (c) Microsoft Corporation. @@ -1636,26 +1636,25 @@ anvil-bench-validate-prereqs: anvil-tool-rustc-validate-prereqs [script("pwsh", "-NoProfile")] anvil-bolero: anvil-bolero-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - $include = (& "{{ just_executable() }}" _anvil-impact-include affected) + $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } if (-not $IsLinux) { Write-Host 'anvil-bolero: non-Linux host -- skipping (cargo-bolero/libfuzzer is Linux-only; harnesses still run under anvil-llvm-cov)' exit 0 } - if ($include -eq '--skip') { + if ($selection -contains '--none') { Write-Host 'anvil-bolero: no affected packages; skipping' exit 0 } $packageNames = [System.Collections.Generic.List[string]]::new() - if ($include -and $include -ne '--workspace') { - $pkgArgs = @(-split $include) + if ($selection -notcontains '--workspace') { + $pkgArgs = $selection for ($i = 0; $i -lt $pkgArgs.Count; $i++) { if ($pkgArgs[$i] -ne '--package' -or ($i + 1) -ge $pkgArgs.Count) { - Write-Error "anvil-bolero: unsupported affected-package arguments: $include" + Write-Error "anvil-bolero: unsupported affected-package arguments: $($selection -join ' ')" exit 1 } - # Impact emits version-qualified specs (name@version); cargo-bolero - # resolves workspace members by bare package name. - $packageNames.Add(($pkgArgs[++$i] -split '@', 2)[0]) + $packageNames.Add($pkgArgs[++$i]) } } else { $metadataJson = & cargo {{_anvil_stable_toolchain_args}} metadata --no-deps --format-version 1 @@ -1736,9 +1735,9 @@ anvil-bolero-validate-prereqs: anvil-toolchain-nightly-validate-prereqs anvil-to [script("pwsh", "-NoProfile")] anvil-careful: anvil-careful-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - $include = (& "{{ just_executable() }}" _anvil-impact-include affected) - if ($include -eq '--skip') { Write-Host 'anvil-careful: no affected packages; skipping'; exit 0 } - $pkg = @(if ($include) { -split $include } else { '--workspace' }) + $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + if ($selection -contains '--none') { Write-Host 'anvil-careful: no affected packages; skipping'; exit 0 } # Guard against cargo-careful's stale-sysroot hazard. cargo-careful # builds a custom std ("careful sysroot": debug-assertions + extra UB # checks) into a STABLE cache path -- ProjectDirs(de.ralfj.cargo-careful), @@ -1792,16 +1791,16 @@ anvil-careful: anvil-careful-validate-prereqs anvil-impact # there is nothing to clean). Written after the clean so it survives it. New-Item -ItemType Directory -Force -Path (Split-Path $marker) | Out-Null Set-Content -LiteralPath $marker -Value $idHash - & cargo '+{{ rust_nightly }}' careful test @pkg --all-features --locked + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo '+{{ rust_nightly }}' careful test '{packages}' --all-features --locked if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-careful` recipe. [group("anvil-setup")] -anvil-careful-setup installer="install": anvil-component-nightly-rust-src-install (anvil-tool-cargo-careful-install installer) +anvil-careful-setup installer="install": anvil-component-nightly-rust-src-install (anvil-tool-cargo-careful-install installer) (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-careful` recipe. [group("anvil-setup")] -anvil-careful-validate-prereqs: anvil-component-nightly-rust-src-validate-prereqs anvil-tool-cargo-careful-validate-prereqs +anvil-careful-validate-prereqs: anvil-component-nightly-rust-src-validate-prereqs anvil-tool-cargo-careful-validate-prereqs anvil-tool-cargo-each-validate-prereqs === justfiles/anvil/checks/cargo-hack.just === # Copyright (c) Microsoft Corporation. @@ -1820,18 +1819,18 @@ anvil-careful-validate-prereqs: anvil-component-nightly-rust-src-validate-prereq [script("pwsh", "-NoProfile")] 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 {{_anvil_stable_toolchain_args}} hack @(if ($include) { -split $include } else { '--workspace' }) --feature-powerset --depth 2 check + $selection = @(& "{{ just_executable() }}" _anvil-impact-include required) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo {{_anvil_stable_toolchain_args}} hack '{packages}' --feature-powerset --depth 2 check if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-cargo-hack` recipe. [group("anvil-setup")] -anvil-cargo-hack-setup installer="install": (anvil-tool-cargo-hack-install installer) +anvil-cargo-hack-setup installer="install": (anvil-tool-cargo-hack-install installer) (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-cargo-hack` recipe. [group("anvil-setup")] -anvil-cargo-hack-validate-prereqs: anvil-tool-cargo-hack-validate-prereqs +anvil-cargo-hack-validate-prereqs: anvil-tool-cargo-hack-validate-prereqs anvil-tool-cargo-each-validate-prereqs === justfiles/anvil/checks/cargo-sort.just === # Copyright (c) Microsoft Corporation. @@ -1855,18 +1854,18 @@ anvil-cargo-hack-validate-prereqs: anvil-tool-cargo-hack-validate-prereqs [script("pwsh", "-NoProfile")] 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 {{_anvil_stable_toolchain_args}} sort --workspace --grouped --check --check-format + $selection = @(& "{{ just_executable() }}" _anvil-impact-include modified) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' 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. [group("anvil-setup")] -anvil-cargo-sort-setup installer="install": (anvil-tool-cargo-sort-install installer) +anvil-cargo-sort-setup installer="install": (anvil-tool-cargo-sort-install installer) (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-cargo-sort` recipe. [group("anvil-setup")] -anvil-cargo-sort-validate-prereqs: anvil-tool-cargo-sort-validate-prereqs +anvil-cargo-sort-validate-prereqs: anvil-tool-cargo-sort-validate-prereqs anvil-tool-cargo-each-validate-prereqs === justfiles/anvil/checks/clippy.just === # Copyright (c) Microsoft Corporation. @@ -1888,18 +1887,18 @@ anvil-cargo-sort-validate-prereqs: anvil-tool-cargo-sort-validate-prereqs [script("pwsh", "-NoProfile")] 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 {{_anvil_stable_toolchain_args}} clippy @(if ($include) { -split $include } else { '--workspace' }) --all-targets --all-features --locked "--" '-D' 'warnings' + $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo {{_anvil_stable_toolchain_args}} clippy '{packages}' --all-targets --all-features --locked '--' '-D' 'warnings' if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-clippy` recipe. [group("anvil-setup")] -anvil-clippy-setup installer="install": anvil-component-default-clippy-install +anvil-clippy-setup installer="install": anvil-component-default-clippy-install (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-clippy` recipe. [group("anvil-setup")] -anvil-clippy-validate-prereqs: anvil-component-default-clippy-validate-prereqs +anvil-clippy-validate-prereqs: anvil-component-default-clippy-validate-prereqs anvil-tool-cargo-each-validate-prereqs === justfiles/anvil/checks/deny.just === # Copyright (c) Microsoft Corporation. @@ -1946,10 +1945,10 @@ anvil-deny-validate-prereqs: anvil-tool-cargo-deny-validate-prereqs [script("pwsh", "-NoProfile")] anvil-doc-build: anvil-doc-build-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - $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 {{_anvil_stable_toolchain_args}} doc @(if ($include) { -split $include } else { '--workspace' }) --all-features --no-deps + $selection = @(& "{{ just_executable() }}" _anvil-impact-include required) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo {{_anvil_stable_toolchain_args}} doc '{packages}' --all-features --no-deps if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # doc-build, examples and doc-test are pure cargo built-ins; the rust @@ -1958,11 +1957,11 @@ 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-toolchain-stable-install +anvil-doc-build-setup installer="install": anvil-toolchain-stable-install (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-doc-build` recipe. [group("anvil-setup")] -anvil-doc-build-validate-prereqs: anvil-tool-rustc-validate-prereqs +anvil-doc-build-validate-prereqs: anvil-tool-rustc-validate-prereqs anvil-tool-cargo-each-validate-prereqs === justfiles/anvil/checks/doc-test.just === # Copyright (c) Microsoft Corporation. @@ -1984,21 +1983,20 @@ anvil-doc-build-validate-prereqs: anvil-tool-rustc-validate-prereqs [script("pwsh", "-NoProfile")] anvil-doc-test: anvil-doc-test-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - $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 {{_anvil_stable_toolchain_args}} test --doc @pkg --all-features --locked + $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - & cargo {{_anvil_stable_toolchain_args}} test --doc @pkg --locked + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo {{_anvil_stable_toolchain_args}} test --doc '{packages}' --all-features --locked + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo {{_anvil_stable_toolchain_args}} test --doc '{packages}' --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-toolchain-stable-install +anvil-doc-test-setup installer="install": anvil-toolchain-stable-install (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-doc-test` recipe. [group("anvil-setup")] -anvil-doc-test-validate-prereqs: anvil-tool-rustc-validate-prereqs +anvil-doc-test-validate-prereqs: anvil-tool-rustc-validate-prereqs anvil-tool-cargo-each-validate-prereqs === justfiles/anvil/checks/ensure-no-cyclic-deps.just === # Copyright (c) Microsoft Corporation. @@ -2015,18 +2013,18 @@ anvil-doc-test-validate-prereqs: anvil-tool-rustc-validate-prereqs [script("pwsh", "-NoProfile")] anvil-ensure-no-cyclic-deps: anvil-ensure-no-cyclic-deps-validate-prereqs anvil-impact $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 {{_anvil_stable_toolchain_args}} ensure-no-cyclic-deps + $selection = @(& "{{ just_executable() }}" _anvil-impact-include modified) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' 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. [group("anvil-setup")] -anvil-ensure-no-cyclic-deps-setup installer="install": (anvil-tool-cargo-ensure-no-cyclic-deps-install installer) +anvil-ensure-no-cyclic-deps-setup installer="install": (anvil-tool-cargo-ensure-no-cyclic-deps-install installer) (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-ensure-no-cyclic-deps` recipe. [group("anvil-setup")] -anvil-ensure-no-cyclic-deps-validate-prereqs: anvil-tool-cargo-ensure-no-cyclic-deps-validate-prereqs +anvil-ensure-no-cyclic-deps-validate-prereqs: anvil-tool-cargo-ensure-no-cyclic-deps-validate-prereqs anvil-tool-cargo-each-validate-prereqs === justfiles/anvil/checks/ensure-no-default-features.just === # Copyright (c) Microsoft Corporation. @@ -2043,18 +2041,18 @@ anvil-ensure-no-cyclic-deps-validate-prereqs: anvil-tool-cargo-ensure-no-cyclic- [script("pwsh", "-NoProfile")] anvil-ensure-no-default-features: anvil-ensure-no-default-features-validate-prereqs anvil-impact $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 {{_anvil_stable_toolchain_args}} ensure-no-default-features + $selection = @(& "{{ just_executable() }}" _anvil-impact-include modified) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' 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. [group("anvil-setup")] -anvil-ensure-no-default-features-setup installer="install": (anvil-tool-cargo-ensure-no-default-features-install installer) +anvil-ensure-no-default-features-setup installer="install": (anvil-tool-cargo-ensure-no-default-features-install installer) (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-ensure-no-default-features` recipe. [group("anvil-setup")] -anvil-ensure-no-default-features-validate-prereqs: anvil-tool-cargo-ensure-no-default-features-validate-prereqs +anvil-ensure-no-default-features-validate-prereqs: anvil-tool-cargo-ensure-no-default-features-validate-prereqs anvil-tool-cargo-each-validate-prereqs === justfiles/anvil/checks/examples.just === # Copyright (c) Microsoft Corporation. @@ -2071,18 +2069,18 @@ anvil-ensure-no-default-features-validate-prereqs: anvil-tool-cargo-ensure-no-de [script("pwsh", "-NoProfile")] 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 {{_anvil_stable_toolchain_args}} build @(if ($include) { -split $include } else { '--workspace' }) --examples --all-features --locked + $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo {{_anvil_stable_toolchain_args}} build '{packages}' --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-toolchain-stable-install +anvil-examples-setup installer="install": anvil-toolchain-stable-install (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-examples` recipe. [group("anvil-setup")] -anvil-examples-validate-prereqs: anvil-tool-rustc-validate-prereqs +anvil-examples-validate-prereqs: anvil-tool-rustc-validate-prereqs anvil-tool-cargo-each-validate-prereqs === justfiles/anvil/checks/external-types.just === # Copyright (c) Microsoft Corporation. @@ -2099,87 +2097,29 @@ anvil-examples-validate-prereqs: anvil-tool-rustc-validate-prereqs # only --manifest-path. Iterate the affected library crates and run # the tool once each, pointing at the crate's Cargo.toml. Bin-only # crates have no public API surface and are skipped. -# TODO(anvil-runner): even after a `cargo ox-run` helper absorbs the -# skip/splat preamble, this recipe stays multi-step: cargo-check- -# external-types is per-manifest (no --package/--workspace), so we -# build a name->manifest map from cargo metadata, filter to lib crates, -# intersect with the affected set ($include), and call the tool once per -# crate. Hard-fails on errors (no tolerance, unlike semver-check). +# cargo-each resolves the affected selection, filters it to library crates, +# substitutes each manifest path, and preserves aggregate failure reporting. # Check public APIs for unapproved external types. [script("pwsh", "-NoProfile")] anvil-external-types: anvil-external-types-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - $include = (& "{{ just_executable() }}" _anvil-impact-include affected) - if ($include -eq '--skip') { - Write-Host 'anvil-external-types: no affected packages; skipping' - exit 0 - } - $pkg = @(if ($include) { -split $include } else { '--workspace' }) - # Build pkg-name -> manifest-path map, restricted to library crates. - $libPkgs = @{} - $metadataJson = & cargo {{_anvil_stable_toolchain_args}} metadata --no-deps --format-version 1 + $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + Write-Host "anvil-external-types: catalog tool minimum cargo-check-external-types {{ cargo_check_external_types_version }}; rustdoc toolchain {{ rust_nightly_external_types }}" + & cargo {{_anvil_stable_toolchain_args}} each @selection --filter lib --keep-going '--' cargo '+{{ rust_nightly_external_types }}' check-external-types --manifest-path '{manifest}' if ($LASTEXITCODE -ne 0) { - Write-Error 'anvil-external-types: cargo metadata failed' + Write-Error "anvil-external-types: one or more crates failed. The installed tool may be newer than the catalog minimum; it must remain compatible with rustdoc from {{ rust_nightly_external_types }}." exit $LASTEXITCODE } - try { - $meta = $metadataJson | ConvertFrom-Json - } catch { - Write-Error "anvil-external-types: could not parse cargo metadata output: $_" - exit 1 - } - foreach ($p in $meta.packages) { - if ($p.targets | Where-Object { $_.kind -contains 'lib' }) { - $libPkgs[$p.name] = $p.manifest_path - } - } - # Decide which packages to check. - $packages = @() - if ($pkg -contains '--workspace') { - $packages = $libPkgs.Keys - } else { - for ($i = 0; $i -lt $pkg.Count; $i++) { - if ($pkg[$i] -eq '--package' -and ($i + 1) -lt $pkg.Count) { - # Impact emits version-qualified specs (name@version); - # strip the @version to recover the bare package name - # this manifest-path map keys on. - $name = ($pkg[$i + 1] -split '@', 2)[0] - if ($libPkgs.ContainsKey($name)) { $packages += $name } - $i++ - } - } - } - if ($packages.Count -eq 0) { - Write-Host 'anvil-external-types: no affected library crates; skipping' - exit 0 - } - Write-Host "anvil-external-types: catalog tool minimum cargo-check-external-types {{ cargo_check_external_types_version }}; rustdoc toolchain {{ rust_nightly_external_types }}" - $failed = $false - foreach ($p in $packages) { - Write-Host "anvil-external-types: $p" - # cargo-check-external-types requires nightly rustdoc (uses - # unstable -Z flags) AND pins a specific rustdoc-types schema - # version. We pin nightly narrowly to the schema this tool - # version expects via `rust_nightly_external_types` in - # versions.just — bump that pin alongside any cargo-check- - # external-types upgrade. No tolerance for schema mismatches: - # if it fails, the pin or the tool needs to move. - & cargo '+{{ rust_nightly_external_types }}' check-external-types --manifest-path $libPkgs[$p] - if ($LASTEXITCODE -ne 0) { - Write-Error "anvil-external-types: '$p' failed. The installed tool may be newer than the catalog minimum; it must remain compatible with rustdoc from {{ rust_nightly_external_types }}." - $failed = $true - } - } - if ($failed) { exit 1 } # Install prerequisites for the `anvil-external-types` recipe. [group("anvil-setup")] -anvil-external-types-setup installer="install": anvil-toolchain-nightly-external-types-install (anvil-tool-cargo-check-external-types-install installer) +anvil-external-types-setup installer="install": anvil-toolchain-nightly-external-types-install (anvil-tool-cargo-check-external-types-install installer) (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-external-types` recipe. [group("anvil-setup")] -anvil-external-types-validate-prereqs: anvil-toolchain-nightly-external-types-validate-prereqs anvil-tool-cargo-check-external-types-validate-prereqs +anvil-external-types-validate-prereqs: anvil-toolchain-nightly-external-types-validate-prereqs anvil-tool-cargo-check-external-types-validate-prereqs anvil-tool-cargo-each-validate-prereqs === justfiles/anvil/checks/fmt.just === # Copyright (c) Microsoft Corporation. @@ -2205,9 +2145,9 @@ anvil-external-types-validate-prereqs: anvil-toolchain-nightly-external-types-va [script("pwsh", "-NoProfile")] anvil-fmt: anvil-fmt-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - $include = (& "{{ just_executable() }}" _anvil-impact-include modified) - if ($include -eq '--skip') { Write-Host 'anvil-fmt: no modified packages; skipping'; exit 0 } - cargo {{_anvil_stable_toolchain_args}} each --workspace --keep-going '--' cargo '+{{ rust_nightly }}' fmt --manifest-path '{manifest}' --check + $selection = @(& "{{ just_executable() }}" _anvil-impact-include modified) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' 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 @@ -2248,18 +2188,18 @@ anvil-fmt-validate-prereqs: anvil-component-nightly-rustfmt-validate-prereqs anv [script("pwsh", "-NoProfile")] 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 {{_anvil_stable_toolchain_args}} heather + $selection = @(& "{{ just_executable() }}" _anvil-impact-include modified) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo {{_anvil_stable_toolchain_args}} heather if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-license-headers` recipe. [group("anvil-setup")] -anvil-license-headers-setup installer="install": (anvil-tool-cargo-heather-install installer) +anvil-license-headers-setup installer="install": (anvil-tool-cargo-heather-install installer) (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-license-headers` recipe. [group("anvil-setup")] -anvil-license-headers-validate-prereqs: anvil-tool-cargo-heather-validate-prereqs +anvil-license-headers-validate-prereqs: anvil-tool-cargo-heather-validate-prereqs anvil-tool-cargo-each-validate-prereqs === justfiles/anvil/checks/llvm-cov.just === # Copyright (c) Microsoft Corporation. @@ -2282,12 +2222,13 @@ anvil-license-headers-validate-prereqs: anvil-tool-cargo-heather-validate-prereq [script("pwsh", "-NoProfile")] anvil-llvm-cov: anvil-llvm-cov-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - $include = (& "{{ just_executable() }}" _anvil-impact-include affected) - if ($include -eq '--skip') { + $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + if ($selection -contains '--none') { Write-Host 'anvil-llvm-cov: no affected packages; skipping' exit 0 } - $pkg = @(if ($include) { -split $include } else { '--workspace' }) + $pkg = $selection $allTestPkg = $pkg # Separate coverage-opted-out packages from the measured set. A crate that # is entirely `#![cfg_attr(coverage_nightly, coverage(off))]` (e.g. a @@ -2314,11 +2255,7 @@ anvil-llvm-cov: anvil-llvm-cov-validate-prereqs anvil-impact if ($optedOut.Count -gt 0) { $filtered = [System.Collections.Generic.List[string]]::new() for ($i = 0; $i -lt $pkg.Count; $i++) { - # Impact emits version-qualified specs (name@version); - # compare the bare name against the opt-out set, but keep - # the full name@version token in $pkg so the llvm-cov - # measurement below resolves unambiguously. - if ($pkg[$i] -eq '--package' -and ($i + 1) -lt $pkg.Count -and $optedOut -contains (($pkg[$i + 1] -split '@', 2)[0])) { + if ($pkg[$i] -eq '--package' -and ($i + 1) -lt $pkg.Count -and $optedOut -contains $pkg[$i + 1]) { $testOnlyPkg.Add('--package') $testOnlyPkg.Add($pkg[$i + 1]) $i++ @@ -2419,14 +2356,12 @@ anvil-llvm-cov: anvil-llvm-cov-validate-prereqs anvil-impact # report NO DATA and fail the gate. When unscoped, $pkg is `--workspace` # (no gate flag); drop it so the gate defaults to every member. # - # cargo-coverage-gate keys on bare package names from cargo metadata, so - # strip the @version qualifier the measurement used for disambiguation. $gateArgs = [System.Collections.Generic.List[string]]::new() for ($i = 0; $i -lt $pkg.Count; $i++) { if ($pkg[$i] -eq '--workspace') { continue } if ($pkg[$i] -eq '--package' -and ($i + 1) -lt $pkg.Count) { $gateArgs.Add('--package') - $gateArgs.Add(($pkg[$i + 1] -split '@', 2)[0]) + $gateArgs.Add($pkg[$i + 1]) $i++ continue } @@ -2500,8 +2435,9 @@ anvil-llvm-cov-validate-prereqs: anvil-component-nightly-llvm-tools-validate-pre [script("pwsh", "-NoProfile")] anvil-loom: anvil-loom-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - $include = (& "{{ just_executable() }}" _anvil-impact-include affected) - if ($include -eq '--skip') { + $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + if ($selection -contains '--none') { Write-Host 'anvil-loom: no affected packages; skipping' exit 0 } @@ -2509,17 +2445,17 @@ anvil-loom: anvil-loom-validate-prereqs anvil-impact if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Optional impact-scoping: recover bare package names from the - # impact step's version-qualified `--package name@version` pairs. + # impact step's `--package name` pairs. # Full-workspace modes (`--workspace` -- e.g. the scheduled tier, # ANVIL_IMPACT=off, or no impact cache) carry no `--package` tokens, # so $affected stays $null and the package set is left unfiltered # (filtering on an empty set would wrongly skip every loom target). $affected = $null - $tokens = @(-split $include) + $tokens = $selection for ($i = 0; $i -lt $tokens.Count; $i++) { if ($tokens[$i] -eq '--package' -and ($i + 1) -lt $tokens.Count) { if ($null -eq $affected) { $affected = @() } - $affected += ($tokens[$i + 1] -split '@', 2)[0] + $affected += $tokens[$i + 1] $i++ } } @@ -2594,25 +2530,24 @@ anvil-loom-validate-prereqs: anvil-tool-rustc-validate-prereqs [script("pwsh", "-NoProfile")] anvil-miri-race-coverage: anvil-miri-race-coverage-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - $include = (& "{{ just_executable() }}" _anvil-impact-include affected) - if ($include -eq '--skip') { Write-Host 'anvil-miri-race-coverage: no affected packages; skipping'; exit 0 } - $pkg = @(if ($include) { -split $include } else { '--workspace' }) + $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } $day = (Get-Date).Day $low = (2 * $day) - 1 $high = (2 * $day) + 1 $env:MIRIFLAGS = "-Zmiri-many-seeds=$low..$high $($env:MIRIFLAGS)".Trim() $env:RUSTFLAGS = "--cfg miri_race_coverage $($env:RUSTFLAGS)".Trim() Write-Host "anvil-miri-race-coverage: seed window $low..$high (day $day)" - & cargo '+{{ rust_nightly }}' miri test --all-features --tests @pkg + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo '+{{ rust_nightly }}' miri test --all-features --tests '{packages}' if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-miri-race-coverage` recipe. [group("anvil-setup")] -anvil-miri-race-coverage-setup installer="install": anvil-component-nightly-miri-install anvil-component-nightly-rust-src-install +anvil-miri-race-coverage-setup installer="install": anvil-component-nightly-miri-install anvil-component-nightly-rust-src-install (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-miri-race-coverage` recipe. [group("anvil-setup")] -anvil-miri-race-coverage-validate-prereqs: anvil-component-nightly-miri-validate-prereqs anvil-component-nightly-rust-src-validate-prereqs +anvil-miri-race-coverage-validate-prereqs: anvil-component-nightly-miri-validate-prereqs anvil-component-nightly-rust-src-validate-prereqs anvil-tool-cargo-each-validate-prereqs === justfiles/anvil/checks/miri-strict-provenance.just === # Copyright (c) Microsoft Corporation. @@ -2627,21 +2562,20 @@ anvil-miri-race-coverage-validate-prereqs: anvil-component-nightly-miri-validate [script("pwsh", "-NoProfile")] anvil-miri-strict-provenance: anvil-miri-strict-provenance-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - $include = (& "{{ just_executable() }}" _anvil-impact-include affected) - if ($include -eq '--skip') { Write-Host 'anvil-miri-strict-provenance: no affected packages; skipping'; exit 0 } - $pkg = @(if ($include) { -split $include } else { '--workspace' }) + $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } $env:MIRIFLAGS = "-Zmiri-strict-provenance $($env:MIRIFLAGS)".Trim() $env:RUSTFLAGS = "--cfg miri_strict_provenance $($env:RUSTFLAGS)".Trim() - & cargo '+{{ rust_nightly }}' miri test --all-features --tests @pkg + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo '+{{ rust_nightly }}' miri test --all-features --tests '{packages}' if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-miri-strict-provenance` recipe. [group("anvil-setup")] -anvil-miri-strict-provenance-setup installer="install": anvil-component-nightly-miri-install anvil-component-nightly-rust-src-install +anvil-miri-strict-provenance-setup installer="install": anvil-component-nightly-miri-install anvil-component-nightly-rust-src-install (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-miri-strict-provenance` recipe. [group("anvil-setup")] -anvil-miri-strict-provenance-validate-prereqs: anvil-component-nightly-miri-validate-prereqs anvil-component-nightly-rust-src-validate-prereqs +anvil-miri-strict-provenance-validate-prereqs: anvil-component-nightly-miri-validate-prereqs anvil-component-nightly-rust-src-validate-prereqs anvil-tool-cargo-each-validate-prereqs === justfiles/anvil/checks/miri-tree-borrows.just === # Copyright (c) Microsoft Corporation. @@ -2664,18 +2598,17 @@ anvil-miri-strict-provenance-validate-prereqs: anvil-component-nightly-miri-vali # that tier sets ANVIL_IMPACT=off, so _anvil-impact-include resolves # to --workspace. A direct local invocation is impact-scoped like any # other affected-tier check -- it reads the anvil-impact cache and -# honors --skip. +# honors an empty selection. # Run Miri tests with the Tree Borrows model. [script("pwsh", "-NoProfile")] anvil-miri-tree-borrows: anvil-miri-tree-borrows-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - $include = (& "{{ just_executable() }}" _anvil-impact-include affected) - if ($include -eq '--skip') { Write-Host 'anvil-miri-tree-borrows: no affected packages; skipping'; exit 0 } - $pkg = @(if ($include) { -split $include } else { '--workspace' }) + $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } $env:MIRIFLAGS = "-Zmiri-tree-borrows $($env:MIRIFLAGS)".Trim() $env:RUSTFLAGS = "--cfg miri_tree_borrows $($env:RUSTFLAGS)".Trim() - & cargo '+{{ rust_nightly }}' miri test --all-features --tests @pkg + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo '+{{ rust_nightly }}' miri test --all-features --tests '{packages}' if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # The three nightly-miri profiles share the same toolchain prereqs @@ -2685,11 +2618,11 @@ anvil-miri-tree-borrows: anvil-miri-tree-borrows-validate-prereqs anvil-impact # Install prerequisites for the `anvil-miri-tree-borrows` recipe. [group("anvil-setup")] -anvil-miri-tree-borrows-setup installer="install": anvil-component-nightly-miri-install anvil-component-nightly-rust-src-install +anvil-miri-tree-borrows-setup installer="install": anvil-component-nightly-miri-install anvil-component-nightly-rust-src-install (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-miri-tree-borrows` recipe. [group("anvil-setup")] -anvil-miri-tree-borrows-validate-prereqs: anvil-component-nightly-miri-validate-prereqs anvil-component-nightly-rust-src-validate-prereqs +anvil-miri-tree-borrows-validate-prereqs: anvil-component-nightly-miri-validate-prereqs anvil-component-nightly-rust-src-validate-prereqs anvil-tool-cargo-each-validate-prereqs === justfiles/anvil/checks/miri.just === # Copyright (c) Microsoft Corporation. @@ -2710,8 +2643,8 @@ anvil-miri-tree-borrows-validate-prereqs: anvil-component-nightly-miri-validate- [script("pwsh", "-NoProfile")] anvil-miri: anvil-miri-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - $include = (& "{{ just_executable() }}" _anvil-impact-include affected) - if ($include -eq '--skip') { Write-Host 'anvil-miri: no affected packages; skipping'; exit 0 } + $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Run miri via libtest (`cargo miri test`), NOT `cargo miri nextest # run`. nextest uses a process-per-test model; under miri every # process pays the full cost of re-interpreting std initialization, @@ -2737,18 +2670,18 @@ anvil-miri: anvil-miri-validate-prereqs anvil-impact # whose tests are all `#[cfg_attr(miri, ignore)]` -- the canonical # opt-out for build-tooling / CLI crates), so the old nextest # `--no-tests=pass` workaround is no longer needed. - & cargo '+{{ rust_nightly }}' miri test --all-features --tests @(if ($include) { -split $include } else { '--workspace' }) + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo '+{{ rust_nightly }}' miri test --all-features --tests '{packages}' if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # --- pr-runtime-analysis members --- # Install prerequisites for the `anvil-miri` recipe. [group("anvil-setup")] -anvil-miri-setup installer="install": anvil-component-nightly-miri-install anvil-component-nightly-rust-src-install +anvil-miri-setup installer="install": anvil-component-nightly-miri-install anvil-component-nightly-rust-src-install (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-miri` recipe. [group("anvil-setup")] -anvil-miri-validate-prereqs: anvil-component-nightly-miri-validate-prereqs anvil-component-nightly-rust-src-validate-prereqs +anvil-miri-validate-prereqs: anvil-component-nightly-miri-validate-prereqs anvil-component-nightly-rust-src-validate-prereqs anvil-tool-cargo-each-validate-prereqs === justfiles/anvil/checks/msrv-test.just === # Copyright (c) Microsoft Corporation. @@ -2763,10 +2696,10 @@ anvil-miri-validate-prereqs: anvil-component-nightly-miri-validate-prereqs anvil [script("pwsh", "-NoProfile")] anvil-msrv-test: anvil-msrv-test-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - $include = (& "{{ just_executable() }}" _anvil-impact-include affected) + $selection = @(& "{{ 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 } + if ($selection -contains '--none') { 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 } @@ -2775,23 +2708,22 @@ anvil-msrv-test: anvil-msrv-test-validate-prereqs anvil-impact 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 + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo "+$toolchain" test '{packages}' --all-targets --all-features --locked if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - & cargo "+$toolchain" test @pkg --all-targets --locked + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo "+$toolchain" test '{packages}' --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": +anvil-msrv-test-setup installer="install": (anvil-tool-cargo-each-install installer) & "{{ 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: +anvil-msrv-test-validate-prereqs: anvil-tool-cargo-each-validate-prereqs $toolchainOutput = & "{{ just_executable() }}" _anvil-resolve-stable msrv | Out-String $resolverExit = $LASTEXITCODE if ($resolverExit -ne 0) { exit $resolverExit } @@ -2831,12 +2763,13 @@ anvil-msrv-test-validate-prereqs: [script("pwsh", "-NoProfile")] anvil-mutants-diff: anvil-mutants-diff-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - $include = (& "{{ just_executable() }}" _anvil-impact-include affected) + $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } if ($IsWindows -and ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64' -or $env:PROCESSOR_ARCHITEW6432 -eq 'ARM64')) { Write-Host 'anvil-mutants-diff: anvil-mutants-diff-validate-prereqs aarch64-pc-windows-msvc -- cargo-mutants does not build here (winapi); skipping' exit 0 } - if ($include -eq '--skip') { + if ($selection -contains '--none') { Write-Host 'anvil-mutants-diff: anvil-mutants-diff-validate-prereqs no affected packages; skipping' exit 0 } @@ -2861,18 +2794,18 @@ 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 {{_anvil_stable_toolchain_args}} mutants --in-diff $diff_path --no-shuffle --jobs 0 + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo {{_anvil_stable_toolchain_args}} mutants --in-diff $diff_path --no-shuffle --jobs 0 if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # --- pr-mutants members --- # Install prerequisites for the `anvil-mutants-diff` recipe. [group("anvil-setup")] -anvil-mutants-diff-setup installer="install": (anvil-tool-cargo-mutants-install installer) +anvil-mutants-diff-setup installer="install": (anvil-tool-cargo-mutants-install installer) (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-mutants-diff` recipe. [group("anvil-setup")] -anvil-mutants-diff-validate-prereqs: anvil-tool-cargo-mutants-validate-prereqs +anvil-mutants-diff-validate-prereqs: anvil-tool-cargo-mutants-validate-prereqs anvil-tool-cargo-each-validate-prereqs === justfiles/anvil/checks/mutants-full.just === # Copyright (c) Microsoft Corporation. @@ -3158,7 +3091,7 @@ anvil-readme-check-validate-prereqs: anvil-tool-cargo-doc2readme-validate-prereq # TODO(anvil-runner): even after a `cargo ox-run` helper absorbs the # skip/splat preamble, this recipe stays multi-step: cargo-metadata # filter to publishable library crates (cargo-semver-checks --workspace -# fails on bin-only workspaces), intersect the affected set ($include) with that +# fails on bin-only workspaces), intersect the affected selection with that # set, verify the PR branch baseline is present, skip newly-added # manifest paths, then invoke per-crate against that baseline with # special handling for renamed crates ("package `` not found in @@ -3169,14 +3102,15 @@ anvil-readme-check-validate-prereqs: anvil-tool-cargo-doc2readme-validate-prereq anvil-semver-check: anvil-semver-check-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $stableArgs = {{_anvil_stable_toolchain_args}} - $include = (& "{{ just_executable() }}" _anvil-impact-include affected) + $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } $commentFile = 'target/anvil/comments/semver.md' - if ($include -eq '--skip') { + if ($selection -contains '--none') { Write-Host 'anvil-semver-check: no affected packages; skipping' Remove-Item -LiteralPath $commentFile -ErrorAction SilentlyContinue exit 0 } - $pkg = @(if ($include) { -split $include } else { '--workspace' }) + $pkg = $selection # Build the publishable candidate package list. Keep each package's metadata so # its manifest path can also be checked in the baseline. Always # iterate per-package over library crates only -- cargo-semver-checks @@ -3211,9 +3145,7 @@ anvil-semver-check: anvil-semver-check-validate-prereqs anvil-impact $packages = @() for ($i = 0; $i -lt $pkg.Count; $i++) { if ($pkg[$i] -eq '--package' -and ($i + 1) -lt $pkg.Count) { - # Impact emits version-qualified specs (name@version); - # strip the @version to recover the bare package name. - $name = ($pkg[$i + 1] -split '@', 2)[0] + $name = $pkg[$i + 1] if ($libPkgs[$name]) { $packages += $name } $i++ } @@ -3457,24 +3389,23 @@ anvil-spellcheck-validate-prereqs: anvil-tool-cargo-spellcheck-validate-prereqs [script("pwsh", "-NoProfile")] anvil-udeps: anvil-udeps-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - $include = (& "{{ just_executable() }}" _anvil-impact-include required) - if ($include -eq '--skip') { Write-Host 'anvil-udeps: no affected packages; skipping'; exit 0 } - $pkg = @(if ($include) { -split $include } else { '--workspace' }) + $selection = @(& "{{ just_executable() }}" _anvil-impact-include required) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Pass 1: default targets (lib + bins) -- surfaces [dependencies] that # are only used by tests/benches/examples (should be dev-deps). - & cargo '+{{ rust_nightly }}' udeps @pkg --all-features + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo '+{{ rust_nightly }}' udeps '{packages}' --all-features if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Pass 2: --all-targets -- surfaces unused [dev-dependencies]. - & cargo '+{{ rust_nightly }}' udeps @pkg --all-features --all-targets + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo '+{{ rust_nightly }}' udeps '{packages}' --all-features --all-targets if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-udeps` recipe. [group("anvil-setup")] -anvil-udeps-setup installer="install": anvil-toolchain-nightly-install (anvil-tool-cargo-udeps-install installer) +anvil-udeps-setup installer="install": anvil-toolchain-nightly-install (anvil-tool-cargo-udeps-install installer) (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-udeps` recipe. [group("anvil-setup")] -anvil-udeps-validate-prereqs: anvil-toolchain-nightly-validate-prereqs anvil-tool-cargo-udeps-validate-prereqs +anvil-udeps-validate-prereqs: anvil-toolchain-nightly-validate-prereqs anvil-tool-cargo-udeps-validate-prereqs anvil-tool-cargo-each-validate-prereqs === justfiles/anvil/container.just === # Copyright (c) Microsoft Corporation. @@ -5071,30 +5002,26 @@ anvil-scheduled-test-validate-prereqs: \ # unscoped by design. # # Each scoped check depends on `anvil-impact` (which produces the -# target/anvil/impact cache) and captures its category's scope into a local -# variable by calling the shared resolver: +# target/anvil/impact cache) and captures its category's selector tokens by +# calling the shared resolver: # -# $include = (& "{{ just_executable() }}" _anvil-impact-include ) +# $selection = @(& "{{ just_executable() }}" _anvil-impact-include ) # # _anvil-impact-include reads that cache both locally and in cloud workflows # (where the group job downloaded the target/anvil/impact artifact), so the # same code path runs everywhere -- no scoping is threaded between jobs via -# environment variables. `$include` is one of: +# environment variables. `$selection` is one of: # -# * "--workspace" (affected/required) or "" (modified) - the tier is -# unscoped: ANVIL_IMPACT=off, or no impact cache is present. Recipes splat -# "--workspace" (an empty string is NOT $null, so recipes test truthiness -# rather than `?? "--workspace"`, which would splat an empty arg cargo -# rejects). -# * "--package A@v --package B@v" - the tier has members. Packages are -# version-qualified cargo specs so they resolve uniquely even when a -# like-named crate is also a transitive dependency. -# * "--skip" - the tier is empty for this change (recipe exits 0). +# * "--workspace" - the tier is unscoped: ANVIL_IMPACT=off, or no impact +# cache is present. +# * "--package", "A", "--package", "B" - the tier has members. One token is +# emitted per line so PowerShell captures a ready-to-splat array. +# * "--none" - the tier is empty; cargo-each exits successfully without +# invoking the child command. # -# This keeps the per-recipe boilerplate to a single capture, a one-line skip -# guard, and the cargo invocation. Modified-tier recipes never forward -# `$include`; they only check the skip sentinel and then let their command -# determine its normal input domain. +# Ordinary recipes pass the array to cargo-each. Modified-tier recipes use +# --once without {packages}, so the selector admits or skips the command while +# the command retains its normal input domain. # # Every recipe whose body uses multi-line conditionals or array-splat # splicing is annotated with [script("pwsh", "-NoProfile")]. pwsh is preinstalled on @@ -5195,7 +5122,7 @@ _anvil-unscoped name: # it, so the checks read the cache instead of recomputing. # # The base ref is resolved by the shared _anvil-base-ref helper (helpers.just, -# also reused by anvil-mutants-diff). The impact -> `--package name@version` +# also reused by anvil-mutants-diff). The impact -> cargo selector # tier projection is _anvil-impact-format, defined in THIS file next to its # sole caller; this recipe orchestrates the snapshots + caching around both so # the exact same projection is used locally and in CI. @@ -5505,11 +5432,11 @@ _anvil-impact-snapshot: } # Public. Computes the impact set from the two snapshots and projects each -# tier into a pre-built `--package name@version ...` string (or the sentinel -# `--skip` when the tier is empty) via the shared _anvil-impact-format -# helper, written to include_.txt. Per-crate checks depend on this -# recipe and read those files via _anvil-impact-include. Cache-aware: no-ops -# when the snapshots haven't changed. +# tier into tokenized `--package name` selectors (or `--none` when the tier is +# empty) via the shared _anvil-impact-format helper, written to +# include_.txt. Per-crate checks depend on this recipe and read those +# files via _anvil-impact-include. Cache-aware: no-ops when the snapshots +# haven't changed. # Compute the impact set and write the target/anvil/impact cache. [group("anvil")] @@ -5568,7 +5495,7 @@ anvil-impact: _anvil-impact-snapshot if ($dirty.Count -gt 0) { Write-Host "anvil-impact: warning: $($dirty.Count) uncommitted change(s) in the working tree are invisible to cargo-delta (it scopes on the committed diff vs the base ref); widening all tiers to --workspace so nothing you are editing is skipped -- commit to scope by impact, or set ANVIL_IMPACT=off to disable scoping." -ForegroundColor Yellow [System.IO.Directory]::CreateDirectory($impactDir) | Out-Null - Set-Content -LiteralPath (Join-Path $impactDir 'include_modified.txt') -Value '' -Encoding UTF8 -NoNewline + Set-Content -LiteralPath (Join-Path $impactDir 'include_modified.txt') -Value '--workspace' -Encoding UTF8 -NoNewline Set-Content -LiteralPath (Join-Path $impactDir 'include_affected.txt') -Value '--workspace' -Encoding UTF8 -NoNewline Set-Content -LiteralPath (Join-Path $impactDir 'include_required.txt') -Value '--workspace' -Encoding UTF8 -NoNewline Set-Content -LiteralPath $impactJson -Value '{}' -Encoding UTF8 @@ -5592,11 +5519,11 @@ anvil-impact: _anvil-impact-snapshot # First-time anvil adoption: the base predates the cargo workspace, so # there is no baseline to diff against. Fall back to full-workspace - # validation -- modified runs unconditionally (empty), affected/required - # default to --workspace -- rather than failing the impact computation. + # validation -- every tier defaults to --workspace rather than failing the + # impact computation. if (Test-Path (Join-Path $snapDir 'baseline-no-workspace')) { Write-Host 'anvil-impact: baseline has no workspace; defaulting all tiers to full-workspace' - Set-Content -LiteralPath (Join-Path $impactDir 'include_modified.txt') -Value '' -Encoding UTF8 -NoNewline + Set-Content -LiteralPath (Join-Path $impactDir 'include_modified.txt') -Value '--workspace' -Encoding UTF8 -NoNewline Set-Content -LiteralPath (Join-Path $impactDir 'include_affected.txt') -Value '--workspace' -Encoding UTF8 -NoNewline Set-Content -LiteralPath (Join-Path $impactDir 'include_required.txt') -Value '--workspace' -Encoding UTF8 -NoNewline Set-Content -LiteralPath $impactJson -Value '{}' -Encoding UTF8 @@ -5621,20 +5548,19 @@ anvil-impact: _anvil-impact-snapshot if ([string]::IsNullOrWhiteSpace($impactOut)) { $impactOut = '{}' } Set-Content -LiteralPath $impactJson -Value $impactOut.Trim() -Encoding UTF8 - # Project each tier into its `--package name@version ...` / `--skip` - # string via the shared formatter (version-qualified specs + lib-name -> - # package-name translation live there, used identically by the cloud - # impact steps). + # Project each tier into one selector token per line. The formatter owns + # lib-name -> package-name translation; cargo-each resolves package + # versions from live metadata before invoking child commands. foreach ($tier in @('modified', 'affected', 'required')) { - $value = (& "{{ just_executable() }}" _anvil-impact-format $tier $impactJson) + $value = @(& "{{ just_executable() }}" _anvil-impact-format $tier $impactJson) if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - Set-Content -LiteralPath (Join-Path $impactDir "include_$tier.txt") -Value $value.Trim() -Encoding UTF8 -NoNewline - Write-Host "anvil-impact: $tier -> $($value.Trim())" + Set-Content -LiteralPath (Join-Path $impactDir "include_$tier.txt") -Value $value -Encoding UTF8 + Write-Host "anvil-impact: $tier -> $($value -join ' ')" } Set-Content -LiteralPath $impactStateFile -Value $wantState -Encoding UTF8 -NoNewline # _anvil-impact-format: turns a cargo-delta impact report into the -# `--package ...` string the scoped checks consume, projected per +# selector tokens the scoped checks consume, projected per # tier into include_.txt by `anvil-impact` above -- its only caller, # next to which it now lives (impact analysis stays in one place). # @@ -5644,14 +5570,11 @@ anvil-impact: _anvil-impact-snapshot # impactJson - path to the file the caller produced via # `cargo delta impact --format json`. # -# Output (stdout, exactly one line so the caller can capture it): -# * "--package name@version --package name2@version2 ..." when the -# tier has members. Each package is a version-qualified cargo spec: -# the version (read from `cargo metadata --no-deps`, i.e. workspace -# members only) disambiguates a workspace member from a like-named -# transitive dependency pulled in at a different version, which a -# bare `-p name` cannot do ("multiple packages named name"). -# * "--skip" when the tier is empty (recipes treat this as a no-op). +# Output (stdout, one token per line so PowerShell captures a ready-to-splat array): +# * repeated "--package" / bare workspace package-name pairs when the +# tier has members. cargo-each resolves those names from live metadata +# and emits version-qualified specs to child Cargo commands. +# * "--none" when the tier is empty (cargo-each treats this as a no-op). # Metadata failures and unknown/ambiguous package names are written to # STDERR and fail the recipe, so callers never continue with an incomplete # package set (which would silently under-scope and skip a check). @@ -5736,7 +5659,7 @@ _anvil-impact-format tier impactJson: } $impact = Get-Content -LiteralPath $impactJson -Raw | ConvertFrom-Json $names = @($impact.$tierKey) - $specs = [System.Collections.Generic.List[string]]::new() + $selectors = [System.Collections.Generic.List[string]]::new() foreach ($name in $names) { if ([string]::IsNullOrWhiteSpace($name)) { continue } # Union candidates from every namespace, de-duplicated by package id @@ -5772,20 +5695,21 @@ _anvil-impact-format tier impactJson: exit 1 } $p = $candidates[0] - $specs.Add("--package $($p.name)@$($p.version)") + $selectors.Add('--package') + $selectors.Add($p.name) } - if ($specs.Count -eq 0) { - Write-Output '--skip' + if ($selectors.Count -eq 0) { + Write-Output '--none' } else { - Write-Output ($specs -join ' ') + Write-Output $selectors } # Private. Resolves an impact category's scope for a check recipe and echoes it on stdout. # 1. ANVIL_IMPACT=off -> tier default (full-workspace / run). -# 2. include_.txt present -> its contents (a --package list or --skip). +# 2. include_.txt present -> its selector tokens. # 3. otherwise -> tier default. -# Tier default: --workspace for affected/required; empty (run) for modified. -# Each scoped check calls this directly (into a local $include variable) to +# Tier default: --workspace for every tier. +# Each scoped check calls this directly (into a local $selection array) to # read its category's scope from the target/anvil/impact cache -- the same cache # both locally and in cloud workflows (where the group job downloaded it). @@ -5794,7 +5718,7 @@ _anvil-impact-format tier impactJson: _anvil-impact-include tier: $ErrorActionPreference = 'Stop' $tier = '{{ tier }}' - $default = if ($tier -eq 'modified') { '' } else { '--workspace' } + $default = '--workspace' # Exit 2 = invalid caller configuration (bad ANVIL_IMPACT), distinct from # the generic 1 used for operational failures -- see _anvil-impact-snapshot. if ($env:ANVIL_IMPACT -and $env:ANVIL_IMPACT -notin @('off', 'consume')) { @@ -5804,7 +5728,7 @@ _anvil-impact-include tier: if ($env:ANVIL_IMPACT -eq 'off') { Write-Output $default; exit 0 } $file = "target/anvil/impact/include_$tier.txt" if (Test-Path $file) { - Write-Output ("$(Get-Content -Raw $file)".Trim()) + Get-Content -LiteralPath $file | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } } else { Write-Output $default } diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap index 9855809a6..ac448efad 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap @@ -469,9 +469,9 @@ anvil-audit-validate-prereqs: anvil-tool-cargo-audit-validate-prereqs [script("pwsh", "-NoProfile")] 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 {{_anvil_stable_toolchain_args}} bench @(if ($include) { -split $include } else { '--workspace' }) --all-features --no-run + $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo {{_anvil_stable_toolchain_args}} bench '{packages}' --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,11 +479,11 @@ anvil-bench: anvil-bench-validate-prereqs anvil-impact # Install prerequisites for the `anvil-bench` recipe. [group("anvil-setup")] -anvil-bench-setup installer="install": anvil-toolchain-stable-install +anvil-bench-setup installer="install": anvil-toolchain-stable-install (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-bench` recipe. [group("anvil-setup")] -anvil-bench-validate-prereqs: anvil-tool-rustc-validate-prereqs +anvil-bench-validate-prereqs: anvil-tool-rustc-validate-prereqs anvil-tool-cargo-each-validate-prereqs === justfiles/anvil/checks/bolero.just === # Copyright (c) Microsoft Corporation. @@ -520,26 +520,25 @@ anvil-bench-validate-prereqs: anvil-tool-rustc-validate-prereqs [script("pwsh", "-NoProfile")] anvil-bolero: anvil-bolero-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - $include = (& "{{ just_executable() }}" _anvil-impact-include affected) + $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } if (-not $IsLinux) { Write-Host 'anvil-bolero: non-Linux host -- skipping (cargo-bolero/libfuzzer is Linux-only; harnesses still run under anvil-llvm-cov)' exit 0 } - if ($include -eq '--skip') { + if ($selection -contains '--none') { Write-Host 'anvil-bolero: no affected packages; skipping' exit 0 } $packageNames = [System.Collections.Generic.List[string]]::new() - if ($include -and $include -ne '--workspace') { - $pkgArgs = @(-split $include) + if ($selection -notcontains '--workspace') { + $pkgArgs = $selection for ($i = 0; $i -lt $pkgArgs.Count; $i++) { if ($pkgArgs[$i] -ne '--package' -or ($i + 1) -ge $pkgArgs.Count) { - Write-Error "anvil-bolero: unsupported affected-package arguments: $include" + Write-Error "anvil-bolero: unsupported affected-package arguments: $($selection -join ' ')" exit 1 } - # Impact emits version-qualified specs (name@version); cargo-bolero - # resolves workspace members by bare package name. - $packageNames.Add(($pkgArgs[++$i] -split '@', 2)[0]) + $packageNames.Add($pkgArgs[++$i]) } } else { $metadataJson = & cargo {{_anvil_stable_toolchain_args}} metadata --no-deps --format-version 1 @@ -620,9 +619,9 @@ anvil-bolero-validate-prereqs: anvil-toolchain-nightly-validate-prereqs anvil-to [script("pwsh", "-NoProfile")] anvil-careful: anvil-careful-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - $include = (& "{{ just_executable() }}" _anvil-impact-include affected) - if ($include -eq '--skip') { Write-Host 'anvil-careful: no affected packages; skipping'; exit 0 } - $pkg = @(if ($include) { -split $include } else { '--workspace' }) + $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + if ($selection -contains '--none') { Write-Host 'anvil-careful: no affected packages; skipping'; exit 0 } # Guard against cargo-careful's stale-sysroot hazard. cargo-careful # builds a custom std ("careful sysroot": debug-assertions + extra UB # checks) into a STABLE cache path -- ProjectDirs(de.ralfj.cargo-careful), @@ -676,16 +675,16 @@ anvil-careful: anvil-careful-validate-prereqs anvil-impact # there is nothing to clean). Written after the clean so it survives it. New-Item -ItemType Directory -Force -Path (Split-Path $marker) | Out-Null Set-Content -LiteralPath $marker -Value $idHash - & cargo '+{{ rust_nightly }}' careful test @pkg --all-features --locked + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo '+{{ rust_nightly }}' careful test '{packages}' --all-features --locked if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-careful` recipe. [group("anvil-setup")] -anvil-careful-setup installer="install": anvil-component-nightly-rust-src-install (anvil-tool-cargo-careful-install installer) +anvil-careful-setup installer="install": anvil-component-nightly-rust-src-install (anvil-tool-cargo-careful-install installer) (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-careful` recipe. [group("anvil-setup")] -anvil-careful-validate-prereqs: anvil-component-nightly-rust-src-validate-prereqs anvil-tool-cargo-careful-validate-prereqs +anvil-careful-validate-prereqs: anvil-component-nightly-rust-src-validate-prereqs anvil-tool-cargo-careful-validate-prereqs anvil-tool-cargo-each-validate-prereqs === justfiles/anvil/checks/cargo-hack.just === # Copyright (c) Microsoft Corporation. @@ -704,18 +703,18 @@ anvil-careful-validate-prereqs: anvil-component-nightly-rust-src-validate-prereq [script("pwsh", "-NoProfile")] 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 {{_anvil_stable_toolchain_args}} hack @(if ($include) { -split $include } else { '--workspace' }) --feature-powerset --depth 2 check + $selection = @(& "{{ just_executable() }}" _anvil-impact-include required) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo {{_anvil_stable_toolchain_args}} hack '{packages}' --feature-powerset --depth 2 check if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-cargo-hack` recipe. [group("anvil-setup")] -anvil-cargo-hack-setup installer="install": (anvil-tool-cargo-hack-install installer) +anvil-cargo-hack-setup installer="install": (anvil-tool-cargo-hack-install installer) (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-cargo-hack` recipe. [group("anvil-setup")] -anvil-cargo-hack-validate-prereqs: anvil-tool-cargo-hack-validate-prereqs +anvil-cargo-hack-validate-prereqs: anvil-tool-cargo-hack-validate-prereqs anvil-tool-cargo-each-validate-prereqs === justfiles/anvil/checks/cargo-sort.just === # Copyright (c) Microsoft Corporation. @@ -739,18 +738,18 @@ anvil-cargo-hack-validate-prereqs: anvil-tool-cargo-hack-validate-prereqs [script("pwsh", "-NoProfile")] 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 {{_anvil_stable_toolchain_args}} sort --workspace --grouped --check --check-format + $selection = @(& "{{ just_executable() }}" _anvil-impact-include modified) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' 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. [group("anvil-setup")] -anvil-cargo-sort-setup installer="install": (anvil-tool-cargo-sort-install installer) +anvil-cargo-sort-setup installer="install": (anvil-tool-cargo-sort-install installer) (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-cargo-sort` recipe. [group("anvil-setup")] -anvil-cargo-sort-validate-prereqs: anvil-tool-cargo-sort-validate-prereqs +anvil-cargo-sort-validate-prereqs: anvil-tool-cargo-sort-validate-prereqs anvil-tool-cargo-each-validate-prereqs === justfiles/anvil/checks/clippy.just === # Copyright (c) Microsoft Corporation. @@ -772,18 +771,18 @@ anvil-cargo-sort-validate-prereqs: anvil-tool-cargo-sort-validate-prereqs [script("pwsh", "-NoProfile")] 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 {{_anvil_stable_toolchain_args}} clippy @(if ($include) { -split $include } else { '--workspace' }) --all-targets --all-features --locked "--" '-D' 'warnings' + $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo {{_anvil_stable_toolchain_args}} clippy '{packages}' --all-targets --all-features --locked '--' '-D' 'warnings' if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-clippy` recipe. [group("anvil-setup")] -anvil-clippy-setup installer="install": anvil-component-default-clippy-install +anvil-clippy-setup installer="install": anvil-component-default-clippy-install (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-clippy` recipe. [group("anvil-setup")] -anvil-clippy-validate-prereqs: anvil-component-default-clippy-validate-prereqs +anvil-clippy-validate-prereqs: anvil-component-default-clippy-validate-prereqs anvil-tool-cargo-each-validate-prereqs === justfiles/anvil/checks/deny.just === # Copyright (c) Microsoft Corporation. @@ -830,10 +829,10 @@ anvil-deny-validate-prereqs: anvil-tool-cargo-deny-validate-prereqs [script("pwsh", "-NoProfile")] anvil-doc-build: anvil-doc-build-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - $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 {{_anvil_stable_toolchain_args}} doc @(if ($include) { -split $include } else { '--workspace' }) --all-features --no-deps + $selection = @(& "{{ just_executable() }}" _anvil-impact-include required) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo {{_anvil_stable_toolchain_args}} doc '{packages}' --all-features --no-deps if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # doc-build, examples and doc-test are pure cargo built-ins; the rust @@ -842,11 +841,11 @@ 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-toolchain-stable-install +anvil-doc-build-setup installer="install": anvil-toolchain-stable-install (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-doc-build` recipe. [group("anvil-setup")] -anvil-doc-build-validate-prereqs: anvil-tool-rustc-validate-prereqs +anvil-doc-build-validate-prereqs: anvil-tool-rustc-validate-prereqs anvil-tool-cargo-each-validate-prereqs === justfiles/anvil/checks/doc-test.just === # Copyright (c) Microsoft Corporation. @@ -868,21 +867,20 @@ anvil-doc-build-validate-prereqs: anvil-tool-rustc-validate-prereqs [script("pwsh", "-NoProfile")] anvil-doc-test: anvil-doc-test-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - $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 {{_anvil_stable_toolchain_args}} test --doc @pkg --all-features --locked + $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - & cargo {{_anvil_stable_toolchain_args}} test --doc @pkg --locked + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo {{_anvil_stable_toolchain_args}} test --doc '{packages}' --all-features --locked + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo {{_anvil_stable_toolchain_args}} test --doc '{packages}' --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-toolchain-stable-install +anvil-doc-test-setup installer="install": anvil-toolchain-stable-install (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-doc-test` recipe. [group("anvil-setup")] -anvil-doc-test-validate-prereqs: anvil-tool-rustc-validate-prereqs +anvil-doc-test-validate-prereqs: anvil-tool-rustc-validate-prereqs anvil-tool-cargo-each-validate-prereqs === justfiles/anvil/checks/ensure-no-cyclic-deps.just === # Copyright (c) Microsoft Corporation. @@ -899,18 +897,18 @@ anvil-doc-test-validate-prereqs: anvil-tool-rustc-validate-prereqs [script("pwsh", "-NoProfile")] anvil-ensure-no-cyclic-deps: anvil-ensure-no-cyclic-deps-validate-prereqs anvil-impact $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 {{_anvil_stable_toolchain_args}} ensure-no-cyclic-deps + $selection = @(& "{{ just_executable() }}" _anvil-impact-include modified) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' 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. [group("anvil-setup")] -anvil-ensure-no-cyclic-deps-setup installer="install": (anvil-tool-cargo-ensure-no-cyclic-deps-install installer) +anvil-ensure-no-cyclic-deps-setup installer="install": (anvil-tool-cargo-ensure-no-cyclic-deps-install installer) (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-ensure-no-cyclic-deps` recipe. [group("anvil-setup")] -anvil-ensure-no-cyclic-deps-validate-prereqs: anvil-tool-cargo-ensure-no-cyclic-deps-validate-prereqs +anvil-ensure-no-cyclic-deps-validate-prereqs: anvil-tool-cargo-ensure-no-cyclic-deps-validate-prereqs anvil-tool-cargo-each-validate-prereqs === justfiles/anvil/checks/ensure-no-default-features.just === # Copyright (c) Microsoft Corporation. @@ -927,18 +925,18 @@ anvil-ensure-no-cyclic-deps-validate-prereqs: anvil-tool-cargo-ensure-no-cyclic- [script("pwsh", "-NoProfile")] anvil-ensure-no-default-features: anvil-ensure-no-default-features-validate-prereqs anvil-impact $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 {{_anvil_stable_toolchain_args}} ensure-no-default-features + $selection = @(& "{{ just_executable() }}" _anvil-impact-include modified) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' 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. [group("anvil-setup")] -anvil-ensure-no-default-features-setup installer="install": (anvil-tool-cargo-ensure-no-default-features-install installer) +anvil-ensure-no-default-features-setup installer="install": (anvil-tool-cargo-ensure-no-default-features-install installer) (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-ensure-no-default-features` recipe. [group("anvil-setup")] -anvil-ensure-no-default-features-validate-prereqs: anvil-tool-cargo-ensure-no-default-features-validate-prereqs +anvil-ensure-no-default-features-validate-prereqs: anvil-tool-cargo-ensure-no-default-features-validate-prereqs anvil-tool-cargo-each-validate-prereqs === justfiles/anvil/checks/examples.just === # Copyright (c) Microsoft Corporation. @@ -955,18 +953,18 @@ anvil-ensure-no-default-features-validate-prereqs: anvil-tool-cargo-ensure-no-de [script("pwsh", "-NoProfile")] 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 {{_anvil_stable_toolchain_args}} build @(if ($include) { -split $include } else { '--workspace' }) --examples --all-features --locked + $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo {{_anvil_stable_toolchain_args}} build '{packages}' --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-toolchain-stable-install +anvil-examples-setup installer="install": anvil-toolchain-stable-install (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-examples` recipe. [group("anvil-setup")] -anvil-examples-validate-prereqs: anvil-tool-rustc-validate-prereqs +anvil-examples-validate-prereqs: anvil-tool-rustc-validate-prereqs anvil-tool-cargo-each-validate-prereqs === justfiles/anvil/checks/external-types.just === # Copyright (c) Microsoft Corporation. @@ -983,87 +981,29 @@ anvil-examples-validate-prereqs: anvil-tool-rustc-validate-prereqs # only --manifest-path. Iterate the affected library crates and run # the tool once each, pointing at the crate's Cargo.toml. Bin-only # crates have no public API surface and are skipped. -# TODO(anvil-runner): even after a `cargo ox-run` helper absorbs the -# skip/splat preamble, this recipe stays multi-step: cargo-check- -# external-types is per-manifest (no --package/--workspace), so we -# build a name->manifest map from cargo metadata, filter to lib crates, -# intersect with the affected set ($include), and call the tool once per -# crate. Hard-fails on errors (no tolerance, unlike semver-check). +# cargo-each resolves the affected selection, filters it to library crates, +# substitutes each manifest path, and preserves aggregate failure reporting. # Check public APIs for unapproved external types. [script("pwsh", "-NoProfile")] anvil-external-types: anvil-external-types-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - $include = (& "{{ just_executable() }}" _anvil-impact-include affected) - if ($include -eq '--skip') { - Write-Host 'anvil-external-types: no affected packages; skipping' - exit 0 - } - $pkg = @(if ($include) { -split $include } else { '--workspace' }) - # Build pkg-name -> manifest-path map, restricted to library crates. - $libPkgs = @{} - $metadataJson = & cargo {{_anvil_stable_toolchain_args}} metadata --no-deps --format-version 1 + $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + Write-Host "anvil-external-types: catalog tool minimum cargo-check-external-types {{ cargo_check_external_types_version }}; rustdoc toolchain {{ rust_nightly_external_types }}" + & cargo {{_anvil_stable_toolchain_args}} each @selection --filter lib --keep-going '--' cargo '+{{ rust_nightly_external_types }}' check-external-types --manifest-path '{manifest}' if ($LASTEXITCODE -ne 0) { - Write-Error 'anvil-external-types: cargo metadata failed' + Write-Error "anvil-external-types: one or more crates failed. The installed tool may be newer than the catalog minimum; it must remain compatible with rustdoc from {{ rust_nightly_external_types }}." exit $LASTEXITCODE } - try { - $meta = $metadataJson | ConvertFrom-Json - } catch { - Write-Error "anvil-external-types: could not parse cargo metadata output: $_" - exit 1 - } - foreach ($p in $meta.packages) { - if ($p.targets | Where-Object { $_.kind -contains 'lib' }) { - $libPkgs[$p.name] = $p.manifest_path - } - } - # Decide which packages to check. - $packages = @() - if ($pkg -contains '--workspace') { - $packages = $libPkgs.Keys - } else { - for ($i = 0; $i -lt $pkg.Count; $i++) { - if ($pkg[$i] -eq '--package' -and ($i + 1) -lt $pkg.Count) { - # Impact emits version-qualified specs (name@version); - # strip the @version to recover the bare package name - # this manifest-path map keys on. - $name = ($pkg[$i + 1] -split '@', 2)[0] - if ($libPkgs.ContainsKey($name)) { $packages += $name } - $i++ - } - } - } - if ($packages.Count -eq 0) { - Write-Host 'anvil-external-types: no affected library crates; skipping' - exit 0 - } - Write-Host "anvil-external-types: catalog tool minimum cargo-check-external-types {{ cargo_check_external_types_version }}; rustdoc toolchain {{ rust_nightly_external_types }}" - $failed = $false - foreach ($p in $packages) { - Write-Host "anvil-external-types: $p" - # cargo-check-external-types requires nightly rustdoc (uses - # unstable -Z flags) AND pins a specific rustdoc-types schema - # version. We pin nightly narrowly to the schema this tool - # version expects via `rust_nightly_external_types` in - # versions.just — bump that pin alongside any cargo-check- - # external-types upgrade. No tolerance for schema mismatches: - # if it fails, the pin or the tool needs to move. - & cargo '+{{ rust_nightly_external_types }}' check-external-types --manifest-path $libPkgs[$p] - if ($LASTEXITCODE -ne 0) { - Write-Error "anvil-external-types: '$p' failed. The installed tool may be newer than the catalog minimum; it must remain compatible with rustdoc from {{ rust_nightly_external_types }}." - $failed = $true - } - } - if ($failed) { exit 1 } # Install prerequisites for the `anvil-external-types` recipe. [group("anvil-setup")] -anvil-external-types-setup installer="install": anvil-toolchain-nightly-external-types-install (anvil-tool-cargo-check-external-types-install installer) +anvil-external-types-setup installer="install": anvil-toolchain-nightly-external-types-install (anvil-tool-cargo-check-external-types-install installer) (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-external-types` recipe. [group("anvil-setup")] -anvil-external-types-validate-prereqs: anvil-toolchain-nightly-external-types-validate-prereqs anvil-tool-cargo-check-external-types-validate-prereqs +anvil-external-types-validate-prereqs: anvil-toolchain-nightly-external-types-validate-prereqs anvil-tool-cargo-check-external-types-validate-prereqs anvil-tool-cargo-each-validate-prereqs === justfiles/anvil/checks/fmt.just === # Copyright (c) Microsoft Corporation. @@ -1089,9 +1029,9 @@ anvil-external-types-validate-prereqs: anvil-toolchain-nightly-external-types-va [script("pwsh", "-NoProfile")] anvil-fmt: anvil-fmt-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - $include = (& "{{ just_executable() }}" _anvil-impact-include modified) - if ($include -eq '--skip') { Write-Host 'anvil-fmt: no modified packages; skipping'; exit 0 } - cargo {{_anvil_stable_toolchain_args}} each --workspace --keep-going '--' cargo '+{{ rust_nightly }}' fmt --manifest-path '{manifest}' --check + $selection = @(& "{{ just_executable() }}" _anvil-impact-include modified) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' 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 @@ -1132,18 +1072,18 @@ anvil-fmt-validate-prereqs: anvil-component-nightly-rustfmt-validate-prereqs anv [script("pwsh", "-NoProfile")] 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 {{_anvil_stable_toolchain_args}} heather + $selection = @(& "{{ just_executable() }}" _anvil-impact-include modified) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo {{_anvil_stable_toolchain_args}} heather if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-license-headers` recipe. [group("anvil-setup")] -anvil-license-headers-setup installer="install": (anvil-tool-cargo-heather-install installer) +anvil-license-headers-setup installer="install": (anvil-tool-cargo-heather-install installer) (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-license-headers` recipe. [group("anvil-setup")] -anvil-license-headers-validate-prereqs: anvil-tool-cargo-heather-validate-prereqs +anvil-license-headers-validate-prereqs: anvil-tool-cargo-heather-validate-prereqs anvil-tool-cargo-each-validate-prereqs === justfiles/anvil/checks/llvm-cov.just === # Copyright (c) Microsoft Corporation. @@ -1166,12 +1106,13 @@ anvil-license-headers-validate-prereqs: anvil-tool-cargo-heather-validate-prereq [script("pwsh", "-NoProfile")] anvil-llvm-cov: anvil-llvm-cov-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - $include = (& "{{ just_executable() }}" _anvil-impact-include affected) - if ($include -eq '--skip') { + $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + if ($selection -contains '--none') { Write-Host 'anvil-llvm-cov: no affected packages; skipping' exit 0 } - $pkg = @(if ($include) { -split $include } else { '--workspace' }) + $pkg = $selection $allTestPkg = $pkg # Separate coverage-opted-out packages from the measured set. A crate that # is entirely `#![cfg_attr(coverage_nightly, coverage(off))]` (e.g. a @@ -1198,11 +1139,7 @@ anvil-llvm-cov: anvil-llvm-cov-validate-prereqs anvil-impact if ($optedOut.Count -gt 0) { $filtered = [System.Collections.Generic.List[string]]::new() for ($i = 0; $i -lt $pkg.Count; $i++) { - # Impact emits version-qualified specs (name@version); - # compare the bare name against the opt-out set, but keep - # the full name@version token in $pkg so the llvm-cov - # measurement below resolves unambiguously. - if ($pkg[$i] -eq '--package' -and ($i + 1) -lt $pkg.Count -and $optedOut -contains (($pkg[$i + 1] -split '@', 2)[0])) { + if ($pkg[$i] -eq '--package' -and ($i + 1) -lt $pkg.Count -and $optedOut -contains $pkg[$i + 1]) { $testOnlyPkg.Add('--package') $testOnlyPkg.Add($pkg[$i + 1]) $i++ @@ -1303,14 +1240,12 @@ anvil-llvm-cov: anvil-llvm-cov-validate-prereqs anvil-impact # report NO DATA and fail the gate. When unscoped, $pkg is `--workspace` # (no gate flag); drop it so the gate defaults to every member. # - # cargo-coverage-gate keys on bare package names from cargo metadata, so - # strip the @version qualifier the measurement used for disambiguation. $gateArgs = [System.Collections.Generic.List[string]]::new() for ($i = 0; $i -lt $pkg.Count; $i++) { if ($pkg[$i] -eq '--workspace') { continue } if ($pkg[$i] -eq '--package' -and ($i + 1) -lt $pkg.Count) { $gateArgs.Add('--package') - $gateArgs.Add(($pkg[$i + 1] -split '@', 2)[0]) + $gateArgs.Add($pkg[$i + 1]) $i++ continue } @@ -1384,8 +1319,9 @@ anvil-llvm-cov-validate-prereqs: anvil-component-nightly-llvm-tools-validate-pre [script("pwsh", "-NoProfile")] anvil-loom: anvil-loom-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - $include = (& "{{ just_executable() }}" _anvil-impact-include affected) - if ($include -eq '--skip') { + $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + if ($selection -contains '--none') { Write-Host 'anvil-loom: no affected packages; skipping' exit 0 } @@ -1393,17 +1329,17 @@ anvil-loom: anvil-loom-validate-prereqs anvil-impact if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Optional impact-scoping: recover bare package names from the - # impact step's version-qualified `--package name@version` pairs. + # impact step's `--package name` pairs. # Full-workspace modes (`--workspace` -- e.g. the scheduled tier, # ANVIL_IMPACT=off, or no impact cache) carry no `--package` tokens, # so $affected stays $null and the package set is left unfiltered # (filtering on an empty set would wrongly skip every loom target). $affected = $null - $tokens = @(-split $include) + $tokens = $selection for ($i = 0; $i -lt $tokens.Count; $i++) { if ($tokens[$i] -eq '--package' -and ($i + 1) -lt $tokens.Count) { if ($null -eq $affected) { $affected = @() } - $affected += ($tokens[$i + 1] -split '@', 2)[0] + $affected += $tokens[$i + 1] $i++ } } @@ -1478,25 +1414,24 @@ anvil-loom-validate-prereqs: anvil-tool-rustc-validate-prereqs [script("pwsh", "-NoProfile")] anvil-miri-race-coverage: anvil-miri-race-coverage-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - $include = (& "{{ just_executable() }}" _anvil-impact-include affected) - if ($include -eq '--skip') { Write-Host 'anvil-miri-race-coverage: no affected packages; skipping'; exit 0 } - $pkg = @(if ($include) { -split $include } else { '--workspace' }) + $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } $day = (Get-Date).Day $low = (2 * $day) - 1 $high = (2 * $day) + 1 $env:MIRIFLAGS = "-Zmiri-many-seeds=$low..$high $($env:MIRIFLAGS)".Trim() $env:RUSTFLAGS = "--cfg miri_race_coverage $($env:RUSTFLAGS)".Trim() Write-Host "anvil-miri-race-coverage: seed window $low..$high (day $day)" - & cargo '+{{ rust_nightly }}' miri test --all-features --tests @pkg + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo '+{{ rust_nightly }}' miri test --all-features --tests '{packages}' if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-miri-race-coverage` recipe. [group("anvil-setup")] -anvil-miri-race-coverage-setup installer="install": anvil-component-nightly-miri-install anvil-component-nightly-rust-src-install +anvil-miri-race-coverage-setup installer="install": anvil-component-nightly-miri-install anvil-component-nightly-rust-src-install (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-miri-race-coverage` recipe. [group("anvil-setup")] -anvil-miri-race-coverage-validate-prereqs: anvil-component-nightly-miri-validate-prereqs anvil-component-nightly-rust-src-validate-prereqs +anvil-miri-race-coverage-validate-prereqs: anvil-component-nightly-miri-validate-prereqs anvil-component-nightly-rust-src-validate-prereqs anvil-tool-cargo-each-validate-prereqs === justfiles/anvil/checks/miri-strict-provenance.just === # Copyright (c) Microsoft Corporation. @@ -1511,21 +1446,20 @@ anvil-miri-race-coverage-validate-prereqs: anvil-component-nightly-miri-validate [script("pwsh", "-NoProfile")] anvil-miri-strict-provenance: anvil-miri-strict-provenance-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - $include = (& "{{ just_executable() }}" _anvil-impact-include affected) - if ($include -eq '--skip') { Write-Host 'anvil-miri-strict-provenance: no affected packages; skipping'; exit 0 } - $pkg = @(if ($include) { -split $include } else { '--workspace' }) + $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } $env:MIRIFLAGS = "-Zmiri-strict-provenance $($env:MIRIFLAGS)".Trim() $env:RUSTFLAGS = "--cfg miri_strict_provenance $($env:RUSTFLAGS)".Trim() - & cargo '+{{ rust_nightly }}' miri test --all-features --tests @pkg + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo '+{{ rust_nightly }}' miri test --all-features --tests '{packages}' if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-miri-strict-provenance` recipe. [group("anvil-setup")] -anvil-miri-strict-provenance-setup installer="install": anvil-component-nightly-miri-install anvil-component-nightly-rust-src-install +anvil-miri-strict-provenance-setup installer="install": anvil-component-nightly-miri-install anvil-component-nightly-rust-src-install (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-miri-strict-provenance` recipe. [group("anvil-setup")] -anvil-miri-strict-provenance-validate-prereqs: anvil-component-nightly-miri-validate-prereqs anvil-component-nightly-rust-src-validate-prereqs +anvil-miri-strict-provenance-validate-prereqs: anvil-component-nightly-miri-validate-prereqs anvil-component-nightly-rust-src-validate-prereqs anvil-tool-cargo-each-validate-prereqs === justfiles/anvil/checks/miri-tree-borrows.just === # Copyright (c) Microsoft Corporation. @@ -1548,18 +1482,17 @@ anvil-miri-strict-provenance-validate-prereqs: anvil-component-nightly-miri-vali # that tier sets ANVIL_IMPACT=off, so _anvil-impact-include resolves # to --workspace. A direct local invocation is impact-scoped like any # other affected-tier check -- it reads the anvil-impact cache and -# honors --skip. +# honors an empty selection. # Run Miri tests with the Tree Borrows model. [script("pwsh", "-NoProfile")] anvil-miri-tree-borrows: anvil-miri-tree-borrows-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - $include = (& "{{ just_executable() }}" _anvil-impact-include affected) - if ($include -eq '--skip') { Write-Host 'anvil-miri-tree-borrows: no affected packages; skipping'; exit 0 } - $pkg = @(if ($include) { -split $include } else { '--workspace' }) + $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } $env:MIRIFLAGS = "-Zmiri-tree-borrows $($env:MIRIFLAGS)".Trim() $env:RUSTFLAGS = "--cfg miri_tree_borrows $($env:RUSTFLAGS)".Trim() - & cargo '+{{ rust_nightly }}' miri test --all-features --tests @pkg + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo '+{{ rust_nightly }}' miri test --all-features --tests '{packages}' if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # The three nightly-miri profiles share the same toolchain prereqs @@ -1569,11 +1502,11 @@ anvil-miri-tree-borrows: anvil-miri-tree-borrows-validate-prereqs anvil-impact # Install prerequisites for the `anvil-miri-tree-borrows` recipe. [group("anvil-setup")] -anvil-miri-tree-borrows-setup installer="install": anvil-component-nightly-miri-install anvil-component-nightly-rust-src-install +anvil-miri-tree-borrows-setup installer="install": anvil-component-nightly-miri-install anvil-component-nightly-rust-src-install (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-miri-tree-borrows` recipe. [group("anvil-setup")] -anvil-miri-tree-borrows-validate-prereqs: anvil-component-nightly-miri-validate-prereqs anvil-component-nightly-rust-src-validate-prereqs +anvil-miri-tree-borrows-validate-prereqs: anvil-component-nightly-miri-validate-prereqs anvil-component-nightly-rust-src-validate-prereqs anvil-tool-cargo-each-validate-prereqs === justfiles/anvil/checks/miri.just === # Copyright (c) Microsoft Corporation. @@ -1594,8 +1527,8 @@ anvil-miri-tree-borrows-validate-prereqs: anvil-component-nightly-miri-validate- [script("pwsh", "-NoProfile")] anvil-miri: anvil-miri-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - $include = (& "{{ just_executable() }}" _anvil-impact-include affected) - if ($include -eq '--skip') { Write-Host 'anvil-miri: no affected packages; skipping'; exit 0 } + $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Run miri via libtest (`cargo miri test`), NOT `cargo miri nextest # run`. nextest uses a process-per-test model; under miri every # process pays the full cost of re-interpreting std initialization, @@ -1621,18 +1554,18 @@ anvil-miri: anvil-miri-validate-prereqs anvil-impact # whose tests are all `#[cfg_attr(miri, ignore)]` -- the canonical # opt-out for build-tooling / CLI crates), so the old nextest # `--no-tests=pass` workaround is no longer needed. - & cargo '+{{ rust_nightly }}' miri test --all-features --tests @(if ($include) { -split $include } else { '--workspace' }) + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo '+{{ rust_nightly }}' miri test --all-features --tests '{packages}' if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # --- pr-runtime-analysis members --- # Install prerequisites for the `anvil-miri` recipe. [group("anvil-setup")] -anvil-miri-setup installer="install": anvil-component-nightly-miri-install anvil-component-nightly-rust-src-install +anvil-miri-setup installer="install": anvil-component-nightly-miri-install anvil-component-nightly-rust-src-install (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-miri` recipe. [group("anvil-setup")] -anvil-miri-validate-prereqs: anvil-component-nightly-miri-validate-prereqs anvil-component-nightly-rust-src-validate-prereqs +anvil-miri-validate-prereqs: anvil-component-nightly-miri-validate-prereqs anvil-component-nightly-rust-src-validate-prereqs anvil-tool-cargo-each-validate-prereqs === justfiles/anvil/checks/msrv-test.just === # Copyright (c) Microsoft Corporation. @@ -1647,10 +1580,10 @@ anvil-miri-validate-prereqs: anvil-component-nightly-miri-validate-prereqs anvil [script("pwsh", "-NoProfile")] anvil-msrv-test: anvil-msrv-test-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - $include = (& "{{ just_executable() }}" _anvil-impact-include affected) + $selection = @(& "{{ 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 } + if ($selection -contains '--none') { 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 } @@ -1659,23 +1592,22 @@ anvil-msrv-test: anvil-msrv-test-validate-prereqs anvil-impact 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 + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo "+$toolchain" test '{packages}' --all-targets --all-features --locked if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - & cargo "+$toolchain" test @pkg --all-targets --locked + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo "+$toolchain" test '{packages}' --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": +anvil-msrv-test-setup installer="install": (anvil-tool-cargo-each-install installer) & "{{ 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: +anvil-msrv-test-validate-prereqs: anvil-tool-cargo-each-validate-prereqs $toolchainOutput = & "{{ just_executable() }}" _anvil-resolve-stable msrv | Out-String $resolverExit = $LASTEXITCODE if ($resolverExit -ne 0) { exit $resolverExit } @@ -1715,12 +1647,13 @@ anvil-msrv-test-validate-prereqs: [script("pwsh", "-NoProfile")] anvil-mutants-diff: anvil-mutants-diff-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - $include = (& "{{ just_executable() }}" _anvil-impact-include affected) + $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } if ($IsWindows -and ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64' -or $env:PROCESSOR_ARCHITEW6432 -eq 'ARM64')) { Write-Host 'anvil-mutants-diff: anvil-mutants-diff-validate-prereqs aarch64-pc-windows-msvc -- cargo-mutants does not build here (winapi); skipping' exit 0 } - if ($include -eq '--skip') { + if ($selection -contains '--none') { Write-Host 'anvil-mutants-diff: anvil-mutants-diff-validate-prereqs no affected packages; skipping' exit 0 } @@ -1745,18 +1678,18 @@ 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 {{_anvil_stable_toolchain_args}} mutants --in-diff $diff_path --no-shuffle --jobs 0 + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo {{_anvil_stable_toolchain_args}} mutants --in-diff $diff_path --no-shuffle --jobs 0 if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # --- pr-mutants members --- # Install prerequisites for the `anvil-mutants-diff` recipe. [group("anvil-setup")] -anvil-mutants-diff-setup installer="install": (anvil-tool-cargo-mutants-install installer) +anvil-mutants-diff-setup installer="install": (anvil-tool-cargo-mutants-install installer) (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-mutants-diff` recipe. [group("anvil-setup")] -anvil-mutants-diff-validate-prereqs: anvil-tool-cargo-mutants-validate-prereqs +anvil-mutants-diff-validate-prereqs: anvil-tool-cargo-mutants-validate-prereqs anvil-tool-cargo-each-validate-prereqs === justfiles/anvil/checks/mutants-full.just === # Copyright (c) Microsoft Corporation. @@ -2042,7 +1975,7 @@ anvil-readme-check-validate-prereqs: anvil-tool-cargo-doc2readme-validate-prereq # TODO(anvil-runner): even after a `cargo ox-run` helper absorbs the # skip/splat preamble, this recipe stays multi-step: cargo-metadata # filter to publishable library crates (cargo-semver-checks --workspace -# fails on bin-only workspaces), intersect the affected set ($include) with that +# fails on bin-only workspaces), intersect the affected selection with that # set, verify the PR branch baseline is present, skip newly-added # manifest paths, then invoke per-crate against that baseline with # special handling for renamed crates ("package `` not found in @@ -2053,14 +1986,15 @@ anvil-readme-check-validate-prereqs: anvil-tool-cargo-doc2readme-validate-prereq anvil-semver-check: anvil-semver-check-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $stableArgs = {{_anvil_stable_toolchain_args}} - $include = (& "{{ just_executable() }}" _anvil-impact-include affected) + $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } $commentFile = 'target/anvil/comments/semver.md' - if ($include -eq '--skip') { + if ($selection -contains '--none') { Write-Host 'anvil-semver-check: no affected packages; skipping' Remove-Item -LiteralPath $commentFile -ErrorAction SilentlyContinue exit 0 } - $pkg = @(if ($include) { -split $include } else { '--workspace' }) + $pkg = $selection # Build the publishable candidate package list. Keep each package's metadata so # its manifest path can also be checked in the baseline. Always # iterate per-package over library crates only -- cargo-semver-checks @@ -2095,9 +2029,7 @@ anvil-semver-check: anvil-semver-check-validate-prereqs anvil-impact $packages = @() for ($i = 0; $i -lt $pkg.Count; $i++) { if ($pkg[$i] -eq '--package' -and ($i + 1) -lt $pkg.Count) { - # Impact emits version-qualified specs (name@version); - # strip the @version to recover the bare package name. - $name = ($pkg[$i + 1] -split '@', 2)[0] + $name = $pkg[$i + 1] if ($libPkgs[$name]) { $packages += $name } $i++ } @@ -2341,24 +2273,23 @@ anvil-spellcheck-validate-prereqs: anvil-tool-cargo-spellcheck-validate-prereqs [script("pwsh", "-NoProfile")] anvil-udeps: anvil-udeps-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - $include = (& "{{ just_executable() }}" _anvil-impact-include required) - if ($include -eq '--skip') { Write-Host 'anvil-udeps: no affected packages; skipping'; exit 0 } - $pkg = @(if ($include) { -split $include } else { '--workspace' }) + $selection = @(& "{{ just_executable() }}" _anvil-impact-include required) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Pass 1: default targets (lib + bins) -- surfaces [dependencies] that # are only used by tests/benches/examples (should be dev-deps). - & cargo '+{{ rust_nightly }}' udeps @pkg --all-features + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo '+{{ rust_nightly }}' udeps '{packages}' --all-features if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Pass 2: --all-targets -- surfaces unused [dev-dependencies]. - & cargo '+{{ rust_nightly }}' udeps @pkg --all-features --all-targets + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo '+{{ rust_nightly }}' udeps '{packages}' --all-features --all-targets if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-udeps` recipe. [group("anvil-setup")] -anvil-udeps-setup installer="install": anvil-toolchain-nightly-install (anvil-tool-cargo-udeps-install installer) +anvil-udeps-setup installer="install": anvil-toolchain-nightly-install (anvil-tool-cargo-udeps-install installer) (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-udeps` recipe. [group("anvil-setup")] -anvil-udeps-validate-prereqs: anvil-toolchain-nightly-validate-prereqs anvil-tool-cargo-udeps-validate-prereqs +anvil-udeps-validate-prereqs: anvil-toolchain-nightly-validate-prereqs anvil-tool-cargo-udeps-validate-prereqs anvil-tool-cargo-each-validate-prereqs === justfiles/anvil/container.just === # Copyright (c) Microsoft Corporation. @@ -3955,30 +3886,26 @@ anvil-scheduled-test-validate-prereqs: \ # unscoped by design. # # Each scoped check depends on `anvil-impact` (which produces the -# target/anvil/impact cache) and captures its category's scope into a local -# variable by calling the shared resolver: +# target/anvil/impact cache) and captures its category's selector tokens by +# calling the shared resolver: # -# $include = (& "{{ just_executable() }}" _anvil-impact-include ) +# $selection = @(& "{{ just_executable() }}" _anvil-impact-include ) # # _anvil-impact-include reads that cache both locally and in cloud workflows # (where the group job downloaded the target/anvil/impact artifact), so the # same code path runs everywhere -- no scoping is threaded between jobs via -# environment variables. `$include` is one of: +# environment variables. `$selection` is one of: # -# * "--workspace" (affected/required) or "" (modified) - the tier is -# unscoped: ANVIL_IMPACT=off, or no impact cache is present. Recipes splat -# "--workspace" (an empty string is NOT $null, so recipes test truthiness -# rather than `?? "--workspace"`, which would splat an empty arg cargo -# rejects). -# * "--package A@v --package B@v" - the tier has members. Packages are -# version-qualified cargo specs so they resolve uniquely even when a -# like-named crate is also a transitive dependency. -# * "--skip" - the tier is empty for this change (recipe exits 0). +# * "--workspace" - the tier is unscoped: ANVIL_IMPACT=off, or no impact +# cache is present. +# * "--package", "A", "--package", "B" - the tier has members. One token is +# emitted per line so PowerShell captures a ready-to-splat array. +# * "--none" - the tier is empty; cargo-each exits successfully without +# invoking the child command. # -# This keeps the per-recipe boilerplate to a single capture, a one-line skip -# guard, and the cargo invocation. Modified-tier recipes never forward -# `$include`; they only check the skip sentinel and then let their command -# determine its normal input domain. +# Ordinary recipes pass the array to cargo-each. Modified-tier recipes use +# --once without {packages}, so the selector admits or skips the command while +# the command retains its normal input domain. # # Every recipe whose body uses multi-line conditionals or array-splat # splicing is annotated with [script("pwsh", "-NoProfile")]. pwsh is preinstalled on @@ -4079,7 +4006,7 @@ _anvil-unscoped name: # it, so the checks read the cache instead of recomputing. # # The base ref is resolved by the shared _anvil-base-ref helper (helpers.just, -# also reused by anvil-mutants-diff). The impact -> `--package name@version` +# also reused by anvil-mutants-diff). The impact -> cargo selector # tier projection is _anvil-impact-format, defined in THIS file next to its # sole caller; this recipe orchestrates the snapshots + caching around both so # the exact same projection is used locally and in CI. @@ -4389,11 +4316,11 @@ _anvil-impact-snapshot: } # Public. Computes the impact set from the two snapshots and projects each -# tier into a pre-built `--package name@version ...` string (or the sentinel -# `--skip` when the tier is empty) via the shared _anvil-impact-format -# helper, written to include_.txt. Per-crate checks depend on this -# recipe and read those files via _anvil-impact-include. Cache-aware: no-ops -# when the snapshots haven't changed. +# tier into tokenized `--package name` selectors (or `--none` when the tier is +# empty) via the shared _anvil-impact-format helper, written to +# include_.txt. Per-crate checks depend on this recipe and read those +# files via _anvil-impact-include. Cache-aware: no-ops when the snapshots +# haven't changed. # Compute the impact set and write the target/anvil/impact cache. [group("anvil")] @@ -4452,7 +4379,7 @@ anvil-impact: _anvil-impact-snapshot if ($dirty.Count -gt 0) { Write-Host "anvil-impact: warning: $($dirty.Count) uncommitted change(s) in the working tree are invisible to cargo-delta (it scopes on the committed diff vs the base ref); widening all tiers to --workspace so nothing you are editing is skipped -- commit to scope by impact, or set ANVIL_IMPACT=off to disable scoping." -ForegroundColor Yellow [System.IO.Directory]::CreateDirectory($impactDir) | Out-Null - Set-Content -LiteralPath (Join-Path $impactDir 'include_modified.txt') -Value '' -Encoding UTF8 -NoNewline + Set-Content -LiteralPath (Join-Path $impactDir 'include_modified.txt') -Value '--workspace' -Encoding UTF8 -NoNewline Set-Content -LiteralPath (Join-Path $impactDir 'include_affected.txt') -Value '--workspace' -Encoding UTF8 -NoNewline Set-Content -LiteralPath (Join-Path $impactDir 'include_required.txt') -Value '--workspace' -Encoding UTF8 -NoNewline Set-Content -LiteralPath $impactJson -Value '{}' -Encoding UTF8 @@ -4476,11 +4403,11 @@ anvil-impact: _anvil-impact-snapshot # First-time anvil adoption: the base predates the cargo workspace, so # there is no baseline to diff against. Fall back to full-workspace - # validation -- modified runs unconditionally (empty), affected/required - # default to --workspace -- rather than failing the impact computation. + # validation -- every tier defaults to --workspace rather than failing the + # impact computation. if (Test-Path (Join-Path $snapDir 'baseline-no-workspace')) { Write-Host 'anvil-impact: baseline has no workspace; defaulting all tiers to full-workspace' - Set-Content -LiteralPath (Join-Path $impactDir 'include_modified.txt') -Value '' -Encoding UTF8 -NoNewline + Set-Content -LiteralPath (Join-Path $impactDir 'include_modified.txt') -Value '--workspace' -Encoding UTF8 -NoNewline Set-Content -LiteralPath (Join-Path $impactDir 'include_affected.txt') -Value '--workspace' -Encoding UTF8 -NoNewline Set-Content -LiteralPath (Join-Path $impactDir 'include_required.txt') -Value '--workspace' -Encoding UTF8 -NoNewline Set-Content -LiteralPath $impactJson -Value '{}' -Encoding UTF8 @@ -4505,20 +4432,19 @@ anvil-impact: _anvil-impact-snapshot if ([string]::IsNullOrWhiteSpace($impactOut)) { $impactOut = '{}' } Set-Content -LiteralPath $impactJson -Value $impactOut.Trim() -Encoding UTF8 - # Project each tier into its `--package name@version ...` / `--skip` - # string via the shared formatter (version-qualified specs + lib-name -> - # package-name translation live there, used identically by the cloud - # impact steps). + # Project each tier into one selector token per line. The formatter owns + # lib-name -> package-name translation; cargo-each resolves package + # versions from live metadata before invoking child commands. foreach ($tier in @('modified', 'affected', 'required')) { - $value = (& "{{ just_executable() }}" _anvil-impact-format $tier $impactJson) + $value = @(& "{{ just_executable() }}" _anvil-impact-format $tier $impactJson) if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - Set-Content -LiteralPath (Join-Path $impactDir "include_$tier.txt") -Value $value.Trim() -Encoding UTF8 -NoNewline - Write-Host "anvil-impact: $tier -> $($value.Trim())" + Set-Content -LiteralPath (Join-Path $impactDir "include_$tier.txt") -Value $value -Encoding UTF8 + Write-Host "anvil-impact: $tier -> $($value -join ' ')" } Set-Content -LiteralPath $impactStateFile -Value $wantState -Encoding UTF8 -NoNewline # _anvil-impact-format: turns a cargo-delta impact report into the -# `--package ...` string the scoped checks consume, projected per +# selector tokens the scoped checks consume, projected per # tier into include_.txt by `anvil-impact` above -- its only caller, # next to which it now lives (impact analysis stays in one place). # @@ -4528,14 +4454,11 @@ anvil-impact: _anvil-impact-snapshot # impactJson - path to the file the caller produced via # `cargo delta impact --format json`. # -# Output (stdout, exactly one line so the caller can capture it): -# * "--package name@version --package name2@version2 ..." when the -# tier has members. Each package is a version-qualified cargo spec: -# the version (read from `cargo metadata --no-deps`, i.e. workspace -# members only) disambiguates a workspace member from a like-named -# transitive dependency pulled in at a different version, which a -# bare `-p name` cannot do ("multiple packages named name"). -# * "--skip" when the tier is empty (recipes treat this as a no-op). +# Output (stdout, one token per line so PowerShell captures a ready-to-splat array): +# * repeated "--package" / bare workspace package-name pairs when the +# tier has members. cargo-each resolves those names from live metadata +# and emits version-qualified specs to child Cargo commands. +# * "--none" when the tier is empty (cargo-each treats this as a no-op). # Metadata failures and unknown/ambiguous package names are written to # STDERR and fail the recipe, so callers never continue with an incomplete # package set (which would silently under-scope and skip a check). @@ -4620,7 +4543,7 @@ _anvil-impact-format tier impactJson: } $impact = Get-Content -LiteralPath $impactJson -Raw | ConvertFrom-Json $names = @($impact.$tierKey) - $specs = [System.Collections.Generic.List[string]]::new() + $selectors = [System.Collections.Generic.List[string]]::new() foreach ($name in $names) { if ([string]::IsNullOrWhiteSpace($name)) { continue } # Union candidates from every namespace, de-duplicated by package id @@ -4656,20 +4579,21 @@ _anvil-impact-format tier impactJson: exit 1 } $p = $candidates[0] - $specs.Add("--package $($p.name)@$($p.version)") + $selectors.Add('--package') + $selectors.Add($p.name) } - if ($specs.Count -eq 0) { - Write-Output '--skip' + if ($selectors.Count -eq 0) { + Write-Output '--none' } else { - Write-Output ($specs -join ' ') + Write-Output $selectors } # Private. Resolves an impact category's scope for a check recipe and echoes it on stdout. # 1. ANVIL_IMPACT=off -> tier default (full-workspace / run). -# 2. include_.txt present -> its contents (a --package list or --skip). +# 2. include_.txt present -> its selector tokens. # 3. otherwise -> tier default. -# Tier default: --workspace for affected/required; empty (run) for modified. -# Each scoped check calls this directly (into a local $include variable) to +# Tier default: --workspace for every tier. +# Each scoped check calls this directly (into a local $selection array) to # read its category's scope from the target/anvil/impact cache -- the same cache # both locally and in cloud workflows (where the group job downloaded it). @@ -4678,7 +4602,7 @@ _anvil-impact-format tier impactJson: _anvil-impact-include tier: $ErrorActionPreference = 'Stop' $tier = '{{ tier }}' - $default = if ($tier -eq 'modified') { '' } else { '--workspace' } + $default = '--workspace' # Exit 2 = invalid caller configuration (bad ANVIL_IMPACT), distinct from # the generic 1 used for operational failures -- see _anvil-impact-snapshot. if ($env:ANVIL_IMPACT -and $env:ANVIL_IMPACT -notin @('off', 'consume')) { @@ -4688,7 +4612,7 @@ _anvil-impact-include tier: if ($env:ANVIL_IMPACT -eq 'off') { Write-Output $default; exit 0 } $file = "target/anvil/impact/include_$tier.txt" if (Test-Path $file) { - Write-Output ("$(Get-Content -Raw $file)".Trim()) + Get-Content -LiteralPath $file | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } } else { Write-Output $default } diff --git a/justfiles/anvil/checks/bench.just b/justfiles/anvil/checks/bench.just index b26880a81..1cec79c16 100644 --- a/justfiles/anvil/checks/bench.just +++ b/justfiles/anvil/checks/bench.just @@ -12,9 +12,9 @@ [script("pwsh", "-NoProfile")] 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 {{_anvil_stable_toolchain_args}} bench @(if ($include) { -split $include } else { '--workspace' }) --all-features --no-run + $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo {{_anvil_stable_toolchain_args}} bench '{packages}' --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,8 +22,8 @@ anvil-bench: anvil-bench-validate-prereqs anvil-impact # Install prerequisites for the `anvil-bench` recipe. [group("anvil-setup")] -anvil-bench-setup installer="install": anvil-toolchain-stable-install +anvil-bench-setup installer="install": anvil-toolchain-stable-install (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-bench` recipe. [group("anvil-setup")] -anvil-bench-validate-prereqs: anvil-tool-rustc-validate-prereqs +anvil-bench-validate-prereqs: anvil-tool-rustc-validate-prereqs anvil-tool-cargo-each-validate-prereqs diff --git a/justfiles/anvil/checks/bolero.just b/justfiles/anvil/checks/bolero.just index 344fabdcc..60d0302cb 100644 --- a/justfiles/anvil/checks/bolero.just +++ b/justfiles/anvil/checks/bolero.just @@ -32,26 +32,25 @@ [script("pwsh", "-NoProfile")] anvil-bolero: anvil-bolero-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - $include = (& "{{ just_executable() }}" _anvil-impact-include affected) + $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } if (-not $IsLinux) { Write-Host 'anvil-bolero: non-Linux host -- skipping (cargo-bolero/libfuzzer is Linux-only; harnesses still run under anvil-llvm-cov)' exit 0 } - if ($include -eq '--skip') { + if ($selection -contains '--none') { Write-Host 'anvil-bolero: no affected packages; skipping' exit 0 } $packageNames = [System.Collections.Generic.List[string]]::new() - if ($include -and $include -ne '--workspace') { - $pkgArgs = @(-split $include) + if ($selection -notcontains '--workspace') { + $pkgArgs = $selection for ($i = 0; $i -lt $pkgArgs.Count; $i++) { if ($pkgArgs[$i] -ne '--package' -or ($i + 1) -ge $pkgArgs.Count) { - Write-Error "anvil-bolero: unsupported affected-package arguments: $include" + Write-Error "anvil-bolero: unsupported affected-package arguments: $($selection -join ' ')" exit 1 } - # Impact emits version-qualified specs (name@version); cargo-bolero - # resolves workspace members by bare package name. - $packageNames.Add(($pkgArgs[++$i] -split '@', 2)[0]) + $packageNames.Add($pkgArgs[++$i]) } } else { $metadataJson = & cargo {{_anvil_stable_toolchain_args}} metadata --no-deps --format-version 1 diff --git a/justfiles/anvil/checks/careful.just b/justfiles/anvil/checks/careful.just index c937a5dc2..3400d6e9a 100644 --- a/justfiles/anvil/checks/careful.just +++ b/justfiles/anvil/checks/careful.just @@ -10,9 +10,9 @@ [script("pwsh", "-NoProfile")] anvil-careful: anvil-careful-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - $include = (& "{{ just_executable() }}" _anvil-impact-include affected) - if ($include -eq '--skip') { Write-Host 'anvil-careful: no affected packages; skipping'; exit 0 } - $pkg = @(if ($include) { -split $include } else { '--workspace' }) + $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + if ($selection -contains '--none') { Write-Host 'anvil-careful: no affected packages; skipping'; exit 0 } # Guard against cargo-careful's stale-sysroot hazard. cargo-careful # builds a custom std ("careful sysroot": debug-assertions + extra UB # checks) into a STABLE cache path -- ProjectDirs(de.ralfj.cargo-careful), @@ -66,13 +66,13 @@ anvil-careful: anvil-careful-validate-prereqs anvil-impact # there is nothing to clean). Written after the clean so it survives it. New-Item -ItemType Directory -Force -Path (Split-Path $marker) | Out-Null Set-Content -LiteralPath $marker -Value $idHash - & cargo '+{{ rust_nightly }}' careful test @pkg --all-features --locked + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo '+{{ rust_nightly }}' careful test '{packages}' --all-features --locked if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-careful` recipe. [group("anvil-setup")] -anvil-careful-setup installer="install": anvil-component-nightly-rust-src-install (anvil-tool-cargo-careful-install installer) +anvil-careful-setup installer="install": anvil-component-nightly-rust-src-install (anvil-tool-cargo-careful-install installer) (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-careful` recipe. [group("anvil-setup")] -anvil-careful-validate-prereqs: anvil-component-nightly-rust-src-validate-prereqs anvil-tool-cargo-careful-validate-prereqs +anvil-careful-validate-prereqs: anvil-component-nightly-rust-src-validate-prereqs anvil-tool-cargo-careful-validate-prereqs anvil-tool-cargo-each-validate-prereqs diff --git a/justfiles/anvil/checks/cargo-hack.just b/justfiles/anvil/checks/cargo-hack.just index 0377fb4da..da8b5d623 100644 --- a/justfiles/anvil/checks/cargo-hack.just +++ b/justfiles/anvil/checks/cargo-hack.just @@ -14,15 +14,15 @@ [script("pwsh", "-NoProfile")] 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 {{_anvil_stable_toolchain_args}} hack @(if ($include) { -split $include } else { '--workspace' }) --feature-powerset --depth 2 check + $selection = @(& "{{ just_executable() }}" _anvil-impact-include required) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo {{_anvil_stable_toolchain_args}} hack '{packages}' --feature-powerset --depth 2 check if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-cargo-hack` recipe. [group("anvil-setup")] -anvil-cargo-hack-setup installer="install": (anvil-tool-cargo-hack-install installer) +anvil-cargo-hack-setup installer="install": (anvil-tool-cargo-hack-install installer) (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-cargo-hack` recipe. [group("anvil-setup")] -anvil-cargo-hack-validate-prereqs: anvil-tool-cargo-hack-validate-prereqs +anvil-cargo-hack-validate-prereqs: anvil-tool-cargo-hack-validate-prereqs anvil-tool-cargo-each-validate-prereqs diff --git a/justfiles/anvil/checks/cargo-sort.just b/justfiles/anvil/checks/cargo-sort.just index 9744892f0..5986678a3 100644 --- a/justfiles/anvil/checks/cargo-sort.just +++ b/justfiles/anvil/checks/cargo-sort.just @@ -19,15 +19,15 @@ [script("pwsh", "-NoProfile")] 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 {{_anvil_stable_toolchain_args}} sort --workspace --grouped --check --check-format + $selection = @(& "{{ just_executable() }}" _anvil-impact-include modified) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' 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. [group("anvil-setup")] -anvil-cargo-sort-setup installer="install": (anvil-tool-cargo-sort-install installer) +anvil-cargo-sort-setup installer="install": (anvil-tool-cargo-sort-install installer) (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-cargo-sort` recipe. [group("anvil-setup")] -anvil-cargo-sort-validate-prereqs: anvil-tool-cargo-sort-validate-prereqs +anvil-cargo-sort-validate-prereqs: anvil-tool-cargo-sort-validate-prereqs anvil-tool-cargo-each-validate-prereqs diff --git a/justfiles/anvil/checks/clippy.just b/justfiles/anvil/checks/clippy.just index a0354f2c4..d7b1fa8db 100644 --- a/justfiles/anvil/checks/clippy.just +++ b/justfiles/anvil/checks/clippy.just @@ -17,15 +17,15 @@ [script("pwsh", "-NoProfile")] 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 {{_anvil_stable_toolchain_args}} clippy @(if ($include) { -split $include } else { '--workspace' }) --all-targets --all-features --locked "--" '-D' 'warnings' + $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo {{_anvil_stable_toolchain_args}} clippy '{packages}' --all-targets --all-features --locked '--' '-D' 'warnings' if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-clippy` recipe. [group("anvil-setup")] -anvil-clippy-setup installer="install": anvil-component-default-clippy-install +anvil-clippy-setup installer="install": anvil-component-default-clippy-install (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-clippy` recipe. [group("anvil-setup")] -anvil-clippy-validate-prereqs: anvil-component-default-clippy-validate-prereqs +anvil-clippy-validate-prereqs: anvil-component-default-clippy-validate-prereqs anvil-tool-cargo-each-validate-prereqs diff --git a/justfiles/anvil/checks/doc-build.just b/justfiles/anvil/checks/doc-build.just index a8edeef7c..477fd5c6b 100644 --- a/justfiles/anvil/checks/doc-build.just +++ b/justfiles/anvil/checks/doc-build.just @@ -14,10 +14,10 @@ [script("pwsh", "-NoProfile")] anvil-doc-build: anvil-doc-build-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - $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 {{_anvil_stable_toolchain_args}} doc @(if ($include) { -split $include } else { '--workspace' }) --all-features --no-deps + $selection = @(& "{{ just_executable() }}" _anvil-impact-include required) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo {{_anvil_stable_toolchain_args}} doc '{packages}' --all-features --no-deps if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # doc-build, examples and doc-test are pure cargo built-ins; the rust @@ -26,8 +26,8 @@ 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-toolchain-stable-install +anvil-doc-build-setup installer="install": anvil-toolchain-stable-install (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-doc-build` recipe. [group("anvil-setup")] -anvil-doc-build-validate-prereqs: anvil-tool-rustc-validate-prereqs +anvil-doc-build-validate-prereqs: anvil-tool-rustc-validate-prereqs anvil-tool-cargo-each-validate-prereqs diff --git a/justfiles/anvil/checks/doc-test.just b/justfiles/anvil/checks/doc-test.just index ca6ade8c9..d7e0f9f58 100644 --- a/justfiles/anvil/checks/doc-test.just +++ b/justfiles/anvil/checks/doc-test.just @@ -17,18 +17,17 @@ [script("pwsh", "-NoProfile")] anvil-doc-test: anvil-doc-test-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - $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 {{_anvil_stable_toolchain_args}} test --doc @pkg --all-features --locked + $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - & cargo {{_anvil_stable_toolchain_args}} test --doc @pkg --locked + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo {{_anvil_stable_toolchain_args}} test --doc '{packages}' --all-features --locked + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo {{_anvil_stable_toolchain_args}} test --doc '{packages}' --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-toolchain-stable-install +anvil-doc-test-setup installer="install": anvil-toolchain-stable-install (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-doc-test` recipe. [group("anvil-setup")] -anvil-doc-test-validate-prereqs: anvil-tool-rustc-validate-prereqs +anvil-doc-test-validate-prereqs: anvil-tool-rustc-validate-prereqs anvil-tool-cargo-each-validate-prereqs diff --git a/justfiles/anvil/checks/ensure-no-cyclic-deps.just b/justfiles/anvil/checks/ensure-no-cyclic-deps.just index 57f360170..0a0028597 100644 --- a/justfiles/anvil/checks/ensure-no-cyclic-deps.just +++ b/justfiles/anvil/checks/ensure-no-cyclic-deps.just @@ -12,15 +12,15 @@ [script("pwsh", "-NoProfile")] anvil-ensure-no-cyclic-deps: anvil-ensure-no-cyclic-deps-validate-prereqs anvil-impact $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 {{_anvil_stable_toolchain_args}} ensure-no-cyclic-deps + $selection = @(& "{{ just_executable() }}" _anvil-impact-include modified) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' 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. [group("anvil-setup")] -anvil-ensure-no-cyclic-deps-setup installer="install": (anvil-tool-cargo-ensure-no-cyclic-deps-install installer) +anvil-ensure-no-cyclic-deps-setup installer="install": (anvil-tool-cargo-ensure-no-cyclic-deps-install installer) (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-ensure-no-cyclic-deps` recipe. [group("anvil-setup")] -anvil-ensure-no-cyclic-deps-validate-prereqs: anvil-tool-cargo-ensure-no-cyclic-deps-validate-prereqs +anvil-ensure-no-cyclic-deps-validate-prereqs: anvil-tool-cargo-ensure-no-cyclic-deps-validate-prereqs anvil-tool-cargo-each-validate-prereqs diff --git a/justfiles/anvil/checks/ensure-no-default-features.just b/justfiles/anvil/checks/ensure-no-default-features.just index cee4d24d5..6a3a6b019 100644 --- a/justfiles/anvil/checks/ensure-no-default-features.just +++ b/justfiles/anvil/checks/ensure-no-default-features.just @@ -12,15 +12,15 @@ [script("pwsh", "-NoProfile")] anvil-ensure-no-default-features: anvil-ensure-no-default-features-validate-prereqs anvil-impact $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 {{_anvil_stable_toolchain_args}} ensure-no-default-features + $selection = @(& "{{ just_executable() }}" _anvil-impact-include modified) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' 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. [group("anvil-setup")] -anvil-ensure-no-default-features-setup installer="install": (anvil-tool-cargo-ensure-no-default-features-install installer) +anvil-ensure-no-default-features-setup installer="install": (anvil-tool-cargo-ensure-no-default-features-install installer) (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-ensure-no-default-features` recipe. [group("anvil-setup")] -anvil-ensure-no-default-features-validate-prereqs: anvil-tool-cargo-ensure-no-default-features-validate-prereqs +anvil-ensure-no-default-features-validate-prereqs: anvil-tool-cargo-ensure-no-default-features-validate-prereqs anvil-tool-cargo-each-validate-prereqs diff --git a/justfiles/anvil/checks/examples.just b/justfiles/anvil/checks/examples.just index fe447b7fb..aa6f67d63 100644 --- a/justfiles/anvil/checks/examples.just +++ b/justfiles/anvil/checks/examples.just @@ -12,15 +12,15 @@ [script("pwsh", "-NoProfile")] 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 {{_anvil_stable_toolchain_args}} build @(if ($include) { -split $include } else { '--workspace' }) --examples --all-features --locked + $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo {{_anvil_stable_toolchain_args}} build '{packages}' --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-toolchain-stable-install +anvil-examples-setup installer="install": anvil-toolchain-stable-install (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-examples` recipe. [group("anvil-setup")] -anvil-examples-validate-prereqs: anvil-tool-rustc-validate-prereqs +anvil-examples-validate-prereqs: anvil-tool-rustc-validate-prereqs anvil-tool-cargo-each-validate-prereqs diff --git a/justfiles/anvil/checks/external-types.just b/justfiles/anvil/checks/external-types.just index d659a3d4d..57f778fdd 100644 --- a/justfiles/anvil/checks/external-types.just +++ b/justfiles/anvil/checks/external-types.just @@ -12,84 +12,26 @@ # only --manifest-path. Iterate the affected library crates and run # the tool once each, pointing at the crate's Cargo.toml. Bin-only # crates have no public API surface and are skipped. -# TODO(anvil-runner): even after a `cargo ox-run` helper absorbs the -# skip/splat preamble, this recipe stays multi-step: cargo-check- -# external-types is per-manifest (no --package/--workspace), so we -# build a name->manifest map from cargo metadata, filter to lib crates, -# intersect with the affected set ($include), and call the tool once per -# crate. Hard-fails on errors (no tolerance, unlike semver-check). +# cargo-each resolves the affected selection, filters it to library crates, +# substitutes each manifest path, and preserves aggregate failure reporting. # Check public APIs for unapproved external types. [script("pwsh", "-NoProfile")] anvil-external-types: anvil-external-types-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - $include = (& "{{ just_executable() }}" _anvil-impact-include affected) - if ($include -eq '--skip') { - Write-Host 'anvil-external-types: no affected packages; skipping' - exit 0 - } - $pkg = @(if ($include) { -split $include } else { '--workspace' }) - # Build pkg-name -> manifest-path map, restricted to library crates. - $libPkgs = @{} - $metadataJson = & cargo {{_anvil_stable_toolchain_args}} metadata --no-deps --format-version 1 + $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + Write-Host "anvil-external-types: catalog tool minimum cargo-check-external-types {{ cargo_check_external_types_version }}; rustdoc toolchain {{ rust_nightly_external_types }}" + & cargo {{_anvil_stable_toolchain_args}} each @selection --filter lib --keep-going '--' cargo '+{{ rust_nightly_external_types }}' check-external-types --manifest-path '{manifest}' if ($LASTEXITCODE -ne 0) { - Write-Error 'anvil-external-types: cargo metadata failed' + Write-Error "anvil-external-types: one or more crates failed. The installed tool may be newer than the catalog minimum; it must remain compatible with rustdoc from {{ rust_nightly_external_types }}." exit $LASTEXITCODE } - try { - $meta = $metadataJson | ConvertFrom-Json - } catch { - Write-Error "anvil-external-types: could not parse cargo metadata output: $_" - exit 1 - } - foreach ($p in $meta.packages) { - if ($p.targets | Where-Object { $_.kind -contains 'lib' }) { - $libPkgs[$p.name] = $p.manifest_path - } - } - # Decide which packages to check. - $packages = @() - if ($pkg -contains '--workspace') { - $packages = $libPkgs.Keys - } else { - for ($i = 0; $i -lt $pkg.Count; $i++) { - if ($pkg[$i] -eq '--package' -and ($i + 1) -lt $pkg.Count) { - # Impact emits version-qualified specs (name@version); - # strip the @version to recover the bare package name - # this manifest-path map keys on. - $name = ($pkg[$i + 1] -split '@', 2)[0] - if ($libPkgs.ContainsKey($name)) { $packages += $name } - $i++ - } - } - } - if ($packages.Count -eq 0) { - Write-Host 'anvil-external-types: no affected library crates; skipping' - exit 0 - } - Write-Host "anvil-external-types: catalog tool minimum cargo-check-external-types {{ cargo_check_external_types_version }}; rustdoc toolchain {{ rust_nightly_external_types }}" - $failed = $false - foreach ($p in $packages) { - Write-Host "anvil-external-types: $p" - # cargo-check-external-types requires nightly rustdoc (uses - # unstable -Z flags) AND pins a specific rustdoc-types schema - # version. We pin nightly narrowly to the schema this tool - # version expects via `rust_nightly_external_types` in - # versions.just — bump that pin alongside any cargo-check- - # external-types upgrade. No tolerance for schema mismatches: - # if it fails, the pin or the tool needs to move. - & cargo '+{{ rust_nightly_external_types }}' check-external-types --manifest-path $libPkgs[$p] - if ($LASTEXITCODE -ne 0) { - Write-Error "anvil-external-types: '$p' failed. The installed tool may be newer than the catalog minimum; it must remain compatible with rustdoc from {{ rust_nightly_external_types }}." - $failed = $true - } - } - if ($failed) { exit 1 } # Install prerequisites for the `anvil-external-types` recipe. [group("anvil-setup")] -anvil-external-types-setup installer="install": anvil-toolchain-nightly-external-types-install (anvil-tool-cargo-check-external-types-install installer) +anvil-external-types-setup installer="install": anvil-toolchain-nightly-external-types-install (anvil-tool-cargo-check-external-types-install installer) (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-external-types` recipe. [group("anvil-setup")] -anvil-external-types-validate-prereqs: anvil-toolchain-nightly-external-types-validate-prereqs anvil-tool-cargo-check-external-types-validate-prereqs +anvil-external-types-validate-prereqs: anvil-toolchain-nightly-external-types-validate-prereqs anvil-tool-cargo-check-external-types-validate-prereqs anvil-tool-cargo-each-validate-prereqs diff --git a/justfiles/anvil/checks/fmt.just b/justfiles/anvil/checks/fmt.just index 48291c5e4..50864e4e6 100644 --- a/justfiles/anvil/checks/fmt.just +++ b/justfiles/anvil/checks/fmt.just @@ -21,9 +21,9 @@ [script("pwsh", "-NoProfile")] anvil-fmt: anvil-fmt-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - $include = (& "{{ just_executable() }}" _anvil-impact-include modified) - if ($include -eq '--skip') { Write-Host 'anvil-fmt: no modified packages; skipping'; exit 0 } - cargo {{_anvil_stable_toolchain_args}} each --workspace --keep-going '--' cargo '+{{ rust_nightly }}' fmt --manifest-path '{manifest}' --check + $selection = @(& "{{ just_executable() }}" _anvil-impact-include modified) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' 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 fb794c32e..10b0a77b7 100644 --- a/justfiles/anvil/checks/license-headers.just +++ b/justfiles/anvil/checks/license-headers.just @@ -12,15 +12,15 @@ [script("pwsh", "-NoProfile")] 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 {{_anvil_stable_toolchain_args}} heather + $selection = @(& "{{ just_executable() }}" _anvil-impact-include modified) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo {{_anvil_stable_toolchain_args}} heather if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-license-headers` recipe. [group("anvil-setup")] -anvil-license-headers-setup installer="install": (anvil-tool-cargo-heather-install installer) +anvil-license-headers-setup installer="install": (anvil-tool-cargo-heather-install installer) (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-license-headers` recipe. [group("anvil-setup")] -anvil-license-headers-validate-prereqs: anvil-tool-cargo-heather-validate-prereqs +anvil-license-headers-validate-prereqs: anvil-tool-cargo-heather-validate-prereqs anvil-tool-cargo-each-validate-prereqs diff --git a/justfiles/anvil/checks/llvm-cov.just b/justfiles/anvil/checks/llvm-cov.just index 1096d2743..946a86a69 100644 --- a/justfiles/anvil/checks/llvm-cov.just +++ b/justfiles/anvil/checks/llvm-cov.just @@ -18,12 +18,13 @@ [script("pwsh", "-NoProfile")] anvil-llvm-cov: anvil-llvm-cov-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - $include = (& "{{ just_executable() }}" _anvil-impact-include affected) - if ($include -eq '--skip') { + $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + if ($selection -contains '--none') { Write-Host 'anvil-llvm-cov: no affected packages; skipping' exit 0 } - $pkg = @(if ($include) { -split $include } else { '--workspace' }) + $pkg = $selection $allTestPkg = $pkg # Separate coverage-opted-out packages from the measured set. A crate that # is entirely `#![cfg_attr(coverage_nightly, coverage(off))]` (e.g. a @@ -50,11 +51,7 @@ anvil-llvm-cov: anvil-llvm-cov-validate-prereqs anvil-impact if ($optedOut.Count -gt 0) { $filtered = [System.Collections.Generic.List[string]]::new() for ($i = 0; $i -lt $pkg.Count; $i++) { - # Impact emits version-qualified specs (name@version); - # compare the bare name against the opt-out set, but keep - # the full name@version token in $pkg so the llvm-cov - # measurement below resolves unambiguously. - if ($pkg[$i] -eq '--package' -and ($i + 1) -lt $pkg.Count -and $optedOut -contains (($pkg[$i + 1] -split '@', 2)[0])) { + if ($pkg[$i] -eq '--package' -and ($i + 1) -lt $pkg.Count -and $optedOut -contains $pkg[$i + 1]) { $testOnlyPkg.Add('--package') $testOnlyPkg.Add($pkg[$i + 1]) $i++ @@ -155,14 +152,12 @@ anvil-llvm-cov: anvil-llvm-cov-validate-prereqs anvil-impact # report NO DATA and fail the gate. When unscoped, $pkg is `--workspace` # (no gate flag); drop it so the gate defaults to every member. # - # cargo-coverage-gate keys on bare package names from cargo metadata, so - # strip the @version qualifier the measurement used for disambiguation. $gateArgs = [System.Collections.Generic.List[string]]::new() for ($i = 0; $i -lt $pkg.Count; $i++) { if ($pkg[$i] -eq '--workspace') { continue } if ($pkg[$i] -eq '--package' -and ($i + 1) -lt $pkg.Count) { $gateArgs.Add('--package') - $gateArgs.Add(($pkg[$i + 1] -split '@', 2)[0]) + $gateArgs.Add($pkg[$i + 1]) $i++ continue } diff --git a/justfiles/anvil/checks/loom.just b/justfiles/anvil/checks/loom.just index 4703a71f3..c94791e1b 100644 --- a/justfiles/anvil/checks/loom.just +++ b/justfiles/anvil/checks/loom.just @@ -52,8 +52,9 @@ [script("pwsh", "-NoProfile")] anvil-loom: anvil-loom-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - $include = (& "{{ just_executable() }}" _anvil-impact-include affected) - if ($include -eq '--skip') { + $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + if ($selection -contains '--none') { Write-Host 'anvil-loom: no affected packages; skipping' exit 0 } @@ -61,17 +62,17 @@ anvil-loom: anvil-loom-validate-prereqs anvil-impact if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Optional impact-scoping: recover bare package names from the - # impact step's version-qualified `--package name@version` pairs. + # impact step's `--package name` pairs. # Full-workspace modes (`--workspace` -- e.g. the scheduled tier, # ANVIL_IMPACT=off, or no impact cache) carry no `--package` tokens, # so $affected stays $null and the package set is left unfiltered # (filtering on an empty set would wrongly skip every loom target). $affected = $null - $tokens = @(-split $include) + $tokens = $selection for ($i = 0; $i -lt $tokens.Count; $i++) { if ($tokens[$i] -eq '--package' -and ($i + 1) -lt $tokens.Count) { if ($null -eq $affected) { $affected = @() } - $affected += ($tokens[$i + 1] -split '@', 2)[0] + $affected += $tokens[$i + 1] $i++ } } diff --git a/justfiles/anvil/checks/miri-race-coverage.just b/justfiles/anvil/checks/miri-race-coverage.just index 4851799b1..29f89f71a 100644 --- a/justfiles/anvil/checks/miri-race-coverage.just +++ b/justfiles/anvil/checks/miri-race-coverage.just @@ -17,22 +17,21 @@ [script("pwsh", "-NoProfile")] anvil-miri-race-coverage: anvil-miri-race-coverage-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - $include = (& "{{ just_executable() }}" _anvil-impact-include affected) - if ($include -eq '--skip') { Write-Host 'anvil-miri-race-coverage: no affected packages; skipping'; exit 0 } - $pkg = @(if ($include) { -split $include } else { '--workspace' }) + $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } $day = (Get-Date).Day $low = (2 * $day) - 1 $high = (2 * $day) + 1 $env:MIRIFLAGS = "-Zmiri-many-seeds=$low..$high $($env:MIRIFLAGS)".Trim() $env:RUSTFLAGS = "--cfg miri_race_coverage $($env:RUSTFLAGS)".Trim() Write-Host "anvil-miri-race-coverage: seed window $low..$high (day $day)" - & cargo '+{{ rust_nightly }}' miri test --all-features --tests @pkg + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo '+{{ rust_nightly }}' miri test --all-features --tests '{packages}' if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-miri-race-coverage` recipe. [group("anvil-setup")] -anvil-miri-race-coverage-setup installer="install": anvil-component-nightly-miri-install anvil-component-nightly-rust-src-install +anvil-miri-race-coverage-setup installer="install": anvil-component-nightly-miri-install anvil-component-nightly-rust-src-install (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-miri-race-coverage` recipe. [group("anvil-setup")] -anvil-miri-race-coverage-validate-prereqs: anvil-component-nightly-miri-validate-prereqs anvil-component-nightly-rust-src-validate-prereqs +anvil-miri-race-coverage-validate-prereqs: anvil-component-nightly-miri-validate-prereqs anvil-component-nightly-rust-src-validate-prereqs anvil-tool-cargo-each-validate-prereqs diff --git a/justfiles/anvil/checks/miri-strict-provenance.just b/justfiles/anvil/checks/miri-strict-provenance.just index 60f36c278..26a492a4a 100644 --- a/justfiles/anvil/checks/miri-strict-provenance.just +++ b/justfiles/anvil/checks/miri-strict-provenance.just @@ -10,18 +10,17 @@ [script("pwsh", "-NoProfile")] anvil-miri-strict-provenance: anvil-miri-strict-provenance-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - $include = (& "{{ just_executable() }}" _anvil-impact-include affected) - if ($include -eq '--skip') { Write-Host 'anvil-miri-strict-provenance: no affected packages; skipping'; exit 0 } - $pkg = @(if ($include) { -split $include } else { '--workspace' }) + $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } $env:MIRIFLAGS = "-Zmiri-strict-provenance $($env:MIRIFLAGS)".Trim() $env:RUSTFLAGS = "--cfg miri_strict_provenance $($env:RUSTFLAGS)".Trim() - & cargo '+{{ rust_nightly }}' miri test --all-features --tests @pkg + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo '+{{ rust_nightly }}' miri test --all-features --tests '{packages}' if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-miri-strict-provenance` recipe. [group("anvil-setup")] -anvil-miri-strict-provenance-setup installer="install": anvil-component-nightly-miri-install anvil-component-nightly-rust-src-install +anvil-miri-strict-provenance-setup installer="install": anvil-component-nightly-miri-install anvil-component-nightly-rust-src-install (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-miri-strict-provenance` recipe. [group("anvil-setup")] -anvil-miri-strict-provenance-validate-prereqs: anvil-component-nightly-miri-validate-prereqs anvil-component-nightly-rust-src-validate-prereqs +anvil-miri-strict-provenance-validate-prereqs: anvil-component-nightly-miri-validate-prereqs anvil-component-nightly-rust-src-validate-prereqs anvil-tool-cargo-each-validate-prereqs diff --git a/justfiles/anvil/checks/miri-tree-borrows.just b/justfiles/anvil/checks/miri-tree-borrows.just index c1786354b..ef8487b36 100644 --- a/justfiles/anvil/checks/miri-tree-borrows.just +++ b/justfiles/anvil/checks/miri-tree-borrows.just @@ -18,18 +18,17 @@ # that tier sets ANVIL_IMPACT=off, so _anvil-impact-include resolves # to --workspace. A direct local invocation is impact-scoped like any # other affected-tier check -- it reads the anvil-impact cache and -# honors --skip. +# honors an empty selection. # Run Miri tests with the Tree Borrows model. [script("pwsh", "-NoProfile")] anvil-miri-tree-borrows: anvil-miri-tree-borrows-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - $include = (& "{{ just_executable() }}" _anvil-impact-include affected) - if ($include -eq '--skip') { Write-Host 'anvil-miri-tree-borrows: no affected packages; skipping'; exit 0 } - $pkg = @(if ($include) { -split $include } else { '--workspace' }) + $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } $env:MIRIFLAGS = "-Zmiri-tree-borrows $($env:MIRIFLAGS)".Trim() $env:RUSTFLAGS = "--cfg miri_tree_borrows $($env:RUSTFLAGS)".Trim() - & cargo '+{{ rust_nightly }}' miri test --all-features --tests @pkg + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo '+{{ rust_nightly }}' miri test --all-features --tests '{packages}' if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # The three nightly-miri profiles share the same toolchain prereqs @@ -39,8 +38,8 @@ anvil-miri-tree-borrows: anvil-miri-tree-borrows-validate-prereqs anvil-impact # Install prerequisites for the `anvil-miri-tree-borrows` recipe. [group("anvil-setup")] -anvil-miri-tree-borrows-setup installer="install": anvil-component-nightly-miri-install anvil-component-nightly-rust-src-install +anvil-miri-tree-borrows-setup installer="install": anvil-component-nightly-miri-install anvil-component-nightly-rust-src-install (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-miri-tree-borrows` recipe. [group("anvil-setup")] -anvil-miri-tree-borrows-validate-prereqs: anvil-component-nightly-miri-validate-prereqs anvil-component-nightly-rust-src-validate-prereqs +anvil-miri-tree-borrows-validate-prereqs: anvil-component-nightly-miri-validate-prereqs anvil-component-nightly-rust-src-validate-prereqs anvil-tool-cargo-each-validate-prereqs diff --git a/justfiles/anvil/checks/miri.just b/justfiles/anvil/checks/miri.just index 630e2c792..7bc5d258f 100644 --- a/justfiles/anvil/checks/miri.just +++ b/justfiles/anvil/checks/miri.just @@ -16,8 +16,8 @@ [script("pwsh", "-NoProfile")] anvil-miri: anvil-miri-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - $include = (& "{{ just_executable() }}" _anvil-impact-include affected) - if ($include -eq '--skip') { Write-Host 'anvil-miri: no affected packages; skipping'; exit 0 } + $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Run miri via libtest (`cargo miri test`), NOT `cargo miri nextest # run`. nextest uses a process-per-test model; under miri every # process pays the full cost of re-interpreting std initialization, @@ -43,15 +43,15 @@ anvil-miri: anvil-miri-validate-prereqs anvil-impact # whose tests are all `#[cfg_attr(miri, ignore)]` -- the canonical # opt-out for build-tooling / CLI crates), so the old nextest # `--no-tests=pass` workaround is no longer needed. - & cargo '+{{ rust_nightly }}' miri test --all-features --tests @(if ($include) { -split $include } else { '--workspace' }) + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo '+{{ rust_nightly }}' miri test --all-features --tests '{packages}' if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # --- pr-runtime-analysis members --- # Install prerequisites for the `anvil-miri` recipe. [group("anvil-setup")] -anvil-miri-setup installer="install": anvil-component-nightly-miri-install anvil-component-nightly-rust-src-install +anvil-miri-setup installer="install": anvil-component-nightly-miri-install anvil-component-nightly-rust-src-install (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-miri` recipe. [group("anvil-setup")] -anvil-miri-validate-prereqs: anvil-component-nightly-miri-validate-prereqs anvil-component-nightly-rust-src-validate-prereqs +anvil-miri-validate-prereqs: anvil-component-nightly-miri-validate-prereqs anvil-component-nightly-rust-src-validate-prereqs anvil-tool-cargo-each-validate-prereqs diff --git a/justfiles/anvil/checks/msrv-test.just b/justfiles/anvil/checks/msrv-test.just index f95b38cff..a3eb020fa 100644 --- a/justfiles/anvil/checks/msrv-test.just +++ b/justfiles/anvil/checks/msrv-test.just @@ -10,10 +10,10 @@ [script("pwsh", "-NoProfile")] anvil-msrv-test: anvil-msrv-test-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - $include = (& "{{ just_executable() }}" _anvil-impact-include affected) + $selection = @(& "{{ 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 } + if ($selection -contains '--none') { 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 } @@ -22,23 +22,22 @@ anvil-msrv-test: anvil-msrv-test-validate-prereqs anvil-impact 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 + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo "+$toolchain" test '{packages}' --all-targets --all-features --locked if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - & cargo "+$toolchain" test @pkg --all-targets --locked + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo "+$toolchain" test '{packages}' --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": +anvil-msrv-test-setup installer="install": (anvil-tool-cargo-each-install installer) & "{{ 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: +anvil-msrv-test-validate-prereqs: anvil-tool-cargo-each-validate-prereqs $toolchainOutput = & "{{ just_executable() }}" _anvil-resolve-stable msrv | Out-String $resolverExit = $LASTEXITCODE if ($resolverExit -ne 0) { exit $resolverExit } diff --git a/justfiles/anvil/checks/mutants-diff.just b/justfiles/anvil/checks/mutants-diff.just index e4efc812f..3fdbe5c00 100644 --- a/justfiles/anvil/checks/mutants-diff.just +++ b/justfiles/anvil/checks/mutants-diff.just @@ -20,12 +20,13 @@ [script("pwsh", "-NoProfile")] anvil-mutants-diff: anvil-mutants-diff-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - $include = (& "{{ just_executable() }}" _anvil-impact-include affected) + $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } if ($IsWindows -and ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64' -or $env:PROCESSOR_ARCHITEW6432 -eq 'ARM64')) { Write-Host 'anvil-mutants-diff: anvil-mutants-diff-validate-prereqs aarch64-pc-windows-msvc -- cargo-mutants does not build here (winapi); skipping' exit 0 } - if ($include -eq '--skip') { + if ($selection -contains '--none') { Write-Host 'anvil-mutants-diff: anvil-mutants-diff-validate-prereqs no affected packages; skipping' exit 0 } @@ -50,15 +51,15 @@ 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 {{_anvil_stable_toolchain_args}} mutants --in-diff $diff_path --no-shuffle --jobs 0 + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo {{_anvil_stable_toolchain_args}} mutants --in-diff $diff_path --no-shuffle --jobs 0 if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # --- pr-mutants members --- # Install prerequisites for the `anvil-mutants-diff` recipe. [group("anvil-setup")] -anvil-mutants-diff-setup installer="install": (anvil-tool-cargo-mutants-install installer) +anvil-mutants-diff-setup installer="install": (anvil-tool-cargo-mutants-install installer) (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-mutants-diff` recipe. [group("anvil-setup")] -anvil-mutants-diff-validate-prereqs: anvil-tool-cargo-mutants-validate-prereqs +anvil-mutants-diff-validate-prereqs: anvil-tool-cargo-mutants-validate-prereqs anvil-tool-cargo-each-validate-prereqs diff --git a/justfiles/anvil/checks/semver-check.just b/justfiles/anvil/checks/semver-check.just index 5dbd16128..53ffb81a6 100644 --- a/justfiles/anvil/checks/semver-check.just +++ b/justfiles/anvil/checks/semver-check.just @@ -39,7 +39,7 @@ # TODO(anvil-runner): even after a `cargo ox-run` helper absorbs the # skip/splat preamble, this recipe stays multi-step: cargo-metadata # filter to publishable library crates (cargo-semver-checks --workspace -# fails on bin-only workspaces), intersect the affected set ($include) with that +# fails on bin-only workspaces), intersect the affected selection with that # set, verify the PR branch baseline is present, skip newly-added # manifest paths, then invoke per-crate against that baseline with # special handling for renamed crates ("package `` not found in @@ -50,14 +50,15 @@ anvil-semver-check: anvil-semver-check-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $stableArgs = {{_anvil_stable_toolchain_args}} - $include = (& "{{ just_executable() }}" _anvil-impact-include affected) + $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } $commentFile = 'target/anvil/comments/semver.md' - if ($include -eq '--skip') { + if ($selection -contains '--none') { Write-Host 'anvil-semver-check: no affected packages; skipping' Remove-Item -LiteralPath $commentFile -ErrorAction SilentlyContinue exit 0 } - $pkg = @(if ($include) { -split $include } else { '--workspace' }) + $pkg = $selection # Build the publishable candidate package list. Keep each package's metadata so # its manifest path can also be checked in the baseline. Always # iterate per-package over library crates only -- cargo-semver-checks @@ -92,9 +93,7 @@ anvil-semver-check: anvil-semver-check-validate-prereqs anvil-impact $packages = @() for ($i = 0; $i -lt $pkg.Count; $i++) { if ($pkg[$i] -eq '--package' -and ($i + 1) -lt $pkg.Count) { - # Impact emits version-qualified specs (name@version); - # strip the @version to recover the bare package name. - $name = ($pkg[$i + 1] -split '@', 2)[0] + $name = $pkg[$i + 1] if ($libPkgs[$name]) { $packages += $name } $i++ } diff --git a/justfiles/anvil/checks/udeps.just b/justfiles/anvil/checks/udeps.just index 83770a4c9..27caed7dc 100644 --- a/justfiles/anvil/checks/udeps.just +++ b/justfiles/anvil/checks/udeps.just @@ -28,21 +28,20 @@ [script("pwsh", "-NoProfile")] anvil-udeps: anvil-udeps-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - $include = (& "{{ just_executable() }}" _anvil-impact-include required) - if ($include -eq '--skip') { Write-Host 'anvil-udeps: no affected packages; skipping'; exit 0 } - $pkg = @(if ($include) { -split $include } else { '--workspace' }) + $selection = @(& "{{ just_executable() }}" _anvil-impact-include required) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Pass 1: default targets (lib + bins) -- surfaces [dependencies] that # are only used by tests/benches/examples (should be dev-deps). - & cargo '+{{ rust_nightly }}' udeps @pkg --all-features + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo '+{{ rust_nightly }}' udeps '{packages}' --all-features if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Pass 2: --all-targets -- surfaces unused [dev-dependencies]. - & cargo '+{{ rust_nightly }}' udeps @pkg --all-features --all-targets + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo '+{{ rust_nightly }}' udeps '{packages}' --all-features --all-targets if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-udeps` recipe. [group("anvil-setup")] -anvil-udeps-setup installer="install": anvil-toolchain-nightly-install (anvil-tool-cargo-udeps-install installer) +anvil-udeps-setup installer="install": anvil-toolchain-nightly-install (anvil-tool-cargo-udeps-install installer) (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-udeps` recipe. [group("anvil-setup")] -anvil-udeps-validate-prereqs: anvil-toolchain-nightly-validate-prereqs anvil-tool-cargo-udeps-validate-prereqs +anvil-udeps-validate-prereqs: anvil-toolchain-nightly-validate-prereqs anvil-tool-cargo-udeps-validate-prereqs anvil-tool-cargo-each-validate-prereqs diff --git a/justfiles/anvil/helpers.just b/justfiles/anvil/helpers.just index c18dd37ac..ffac2ed96 100644 --- a/justfiles/anvil/helpers.just +++ b/justfiles/anvil/helpers.just @@ -35,30 +35,26 @@ # unscoped by design. # # Each scoped check depends on `anvil-impact` (which produces the -# target/anvil/impact cache) and captures its category's scope into a local -# variable by calling the shared resolver: +# target/anvil/impact cache) and captures its category's selector tokens by +# calling the shared resolver: # -# $include = (& "{{ just_executable() }}" _anvil-impact-include ) +# $selection = @(& "{{ just_executable() }}" _anvil-impact-include ) # # _anvil-impact-include reads that cache both locally and in cloud workflows # (where the group job downloaded the target/anvil/impact artifact), so the # same code path runs everywhere -- no scoping is threaded between jobs via -# environment variables. `$include` is one of: +# environment variables. `$selection` is one of: # -# * "--workspace" (affected/required) or "" (modified) - the tier is -# unscoped: ANVIL_IMPACT=off, or no impact cache is present. Recipes splat -# "--workspace" (an empty string is NOT $null, so recipes test truthiness -# rather than `?? "--workspace"`, which would splat an empty arg cargo -# rejects). -# * "--package A@v --package B@v" - the tier has members. Packages are -# version-qualified cargo specs so they resolve uniquely even when a -# like-named crate is also a transitive dependency. -# * "--skip" - the tier is empty for this change (recipe exits 0). +# * "--workspace" - the tier is unscoped: ANVIL_IMPACT=off, or no impact +# cache is present. +# * "--package", "A", "--package", "B" - the tier has members. One token is +# emitted per line so PowerShell captures a ready-to-splat array. +# * "--none" - the tier is empty; cargo-each exits successfully without +# invoking the child command. # -# This keeps the per-recipe boilerplate to a single capture, a one-line skip -# guard, and the cargo invocation. Modified-tier recipes never forward -# `$include`; they only check the skip sentinel and then let their command -# determine its normal input domain. +# Ordinary recipes pass the array to cargo-each. Modified-tier recipes use +# --once without {packages}, so the selector admits or skips the command while +# the command retains its normal input domain. # # Every recipe whose body uses multi-line conditionals or array-splat # splicing is annotated with [script("pwsh", "-NoProfile")]. pwsh is preinstalled on diff --git a/justfiles/anvil/impact.just b/justfiles/anvil/impact.just index c9e7db74e..f97a1a4d5 100644 --- a/justfiles/anvil/impact.just +++ b/justfiles/anvil/impact.just @@ -14,7 +14,7 @@ # it, so the checks read the cache instead of recomputing. # # The base ref is resolved by the shared _anvil-base-ref helper (helpers.just, -# also reused by anvil-mutants-diff). The impact -> `--package name@version` +# also reused by anvil-mutants-diff). The impact -> cargo selector # tier projection is _anvil-impact-format, defined in THIS file next to its # sole caller; this recipe orchestrates the snapshots + caching around both so # the exact same projection is used locally and in CI. @@ -324,11 +324,11 @@ _anvil-impact-snapshot: } # Public. Computes the impact set from the two snapshots and projects each -# tier into a pre-built `--package name@version ...` string (or the sentinel -# `--skip` when the tier is empty) via the shared _anvil-impact-format -# helper, written to include_.txt. Per-crate checks depend on this -# recipe and read those files via _anvil-impact-include. Cache-aware: no-ops -# when the snapshots haven't changed. +# tier into tokenized `--package name` selectors (or `--none` when the tier is +# empty) via the shared _anvil-impact-format helper, written to +# include_.txt. Per-crate checks depend on this recipe and read those +# files via _anvil-impact-include. Cache-aware: no-ops when the snapshots +# haven't changed. # Compute the impact set and write the target/anvil/impact cache. [group("anvil")] @@ -387,7 +387,7 @@ anvil-impact: _anvil-impact-snapshot if ($dirty.Count -gt 0) { Write-Host "anvil-impact: warning: $($dirty.Count) uncommitted change(s) in the working tree are invisible to cargo-delta (it scopes on the committed diff vs the base ref); widening all tiers to --workspace so nothing you are editing is skipped -- commit to scope by impact, or set ANVIL_IMPACT=off to disable scoping." -ForegroundColor Yellow [System.IO.Directory]::CreateDirectory($impactDir) | Out-Null - Set-Content -LiteralPath (Join-Path $impactDir 'include_modified.txt') -Value '' -Encoding UTF8 -NoNewline + Set-Content -LiteralPath (Join-Path $impactDir 'include_modified.txt') -Value '--workspace' -Encoding UTF8 -NoNewline Set-Content -LiteralPath (Join-Path $impactDir 'include_affected.txt') -Value '--workspace' -Encoding UTF8 -NoNewline Set-Content -LiteralPath (Join-Path $impactDir 'include_required.txt') -Value '--workspace' -Encoding UTF8 -NoNewline Set-Content -LiteralPath $impactJson -Value '{}' -Encoding UTF8 @@ -411,11 +411,11 @@ anvil-impact: _anvil-impact-snapshot # First-time anvil adoption: the base predates the cargo workspace, so # there is no baseline to diff against. Fall back to full-workspace - # validation -- modified runs unconditionally (empty), affected/required - # default to --workspace -- rather than failing the impact computation. + # validation -- every tier defaults to --workspace rather than failing the + # impact computation. if (Test-Path (Join-Path $snapDir 'baseline-no-workspace')) { Write-Host 'anvil-impact: baseline has no workspace; defaulting all tiers to full-workspace' - Set-Content -LiteralPath (Join-Path $impactDir 'include_modified.txt') -Value '' -Encoding UTF8 -NoNewline + Set-Content -LiteralPath (Join-Path $impactDir 'include_modified.txt') -Value '--workspace' -Encoding UTF8 -NoNewline Set-Content -LiteralPath (Join-Path $impactDir 'include_affected.txt') -Value '--workspace' -Encoding UTF8 -NoNewline Set-Content -LiteralPath (Join-Path $impactDir 'include_required.txt') -Value '--workspace' -Encoding UTF8 -NoNewline Set-Content -LiteralPath $impactJson -Value '{}' -Encoding UTF8 @@ -440,20 +440,19 @@ anvil-impact: _anvil-impact-snapshot if ([string]::IsNullOrWhiteSpace($impactOut)) { $impactOut = '{}' } Set-Content -LiteralPath $impactJson -Value $impactOut.Trim() -Encoding UTF8 - # Project each tier into its `--package name@version ...` / `--skip` - # string via the shared formatter (version-qualified specs + lib-name -> - # package-name translation live there, used identically by the cloud - # impact steps). + # Project each tier into one selector token per line. The formatter owns + # lib-name -> package-name translation; cargo-each resolves package + # versions from live metadata before invoking child commands. foreach ($tier in @('modified', 'affected', 'required')) { - $value = (& "{{ just_executable() }}" _anvil-impact-format $tier $impactJson) + $value = @(& "{{ just_executable() }}" _anvil-impact-format $tier $impactJson) if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - Set-Content -LiteralPath (Join-Path $impactDir "include_$tier.txt") -Value $value.Trim() -Encoding UTF8 -NoNewline - Write-Host "anvil-impact: $tier -> $($value.Trim())" + Set-Content -LiteralPath (Join-Path $impactDir "include_$tier.txt") -Value $value -Encoding UTF8 + Write-Host "anvil-impact: $tier -> $($value -join ' ')" } Set-Content -LiteralPath $impactStateFile -Value $wantState -Encoding UTF8 -NoNewline # _anvil-impact-format: turns a cargo-delta impact report into the -# `--package ...` string the scoped checks consume, projected per +# selector tokens the scoped checks consume, projected per # tier into include_.txt by `anvil-impact` above -- its only caller, # next to which it now lives (impact analysis stays in one place). # @@ -463,14 +462,11 @@ anvil-impact: _anvil-impact-snapshot # impactJson - path to the file the caller produced via # `cargo delta impact --format json`. # -# Output (stdout, exactly one line so the caller can capture it): -# * "--package name@version --package name2@version2 ..." when the -# tier has members. Each package is a version-qualified cargo spec: -# the version (read from `cargo metadata --no-deps`, i.e. workspace -# members only) disambiguates a workspace member from a like-named -# transitive dependency pulled in at a different version, which a -# bare `-p name` cannot do ("multiple packages named name"). -# * "--skip" when the tier is empty (recipes treat this as a no-op). +# Output (stdout, one token per line so PowerShell captures a ready-to-splat array): +# * repeated "--package" / bare workspace package-name pairs when the +# tier has members. cargo-each resolves those names from live metadata +# and emits version-qualified specs to child Cargo commands. +# * "--none" when the tier is empty (cargo-each treats this as a no-op). # Metadata failures and unknown/ambiguous package names are written to # STDERR and fail the recipe, so callers never continue with an incomplete # package set (which would silently under-scope and skip a check). @@ -555,7 +551,7 @@ _anvil-impact-format tier impactJson: } $impact = Get-Content -LiteralPath $impactJson -Raw | ConvertFrom-Json $names = @($impact.$tierKey) - $specs = [System.Collections.Generic.List[string]]::new() + $selectors = [System.Collections.Generic.List[string]]::new() foreach ($name in $names) { if ([string]::IsNullOrWhiteSpace($name)) { continue } # Union candidates from every namespace, de-duplicated by package id @@ -591,20 +587,21 @@ _anvil-impact-format tier impactJson: exit 1 } $p = $candidates[0] - $specs.Add("--package $($p.name)@$($p.version)") + $selectors.Add('--package') + $selectors.Add($p.name) } - if ($specs.Count -eq 0) { - Write-Output '--skip' + if ($selectors.Count -eq 0) { + Write-Output '--none' } else { - Write-Output ($specs -join ' ') + Write-Output $selectors } # Private. Resolves an impact category's scope for a check recipe and echoes it on stdout. # 1. ANVIL_IMPACT=off -> tier default (full-workspace / run). -# 2. include_.txt present -> its contents (a --package list or --skip). +# 2. include_.txt present -> its selector tokens. # 3. otherwise -> tier default. -# Tier default: --workspace for affected/required; empty (run) for modified. -# Each scoped check calls this directly (into a local $include variable) to +# Tier default: --workspace for every tier. +# Each scoped check calls this directly (into a local $selection array) to # read its category's scope from the target/anvil/impact cache -- the same cache # both locally and in cloud workflows (where the group job downloaded it). @@ -613,7 +610,7 @@ _anvil-impact-format tier impactJson: _anvil-impact-include tier: $ErrorActionPreference = 'Stop' $tier = '{{ tier }}' - $default = if ($tier -eq 'modified') { '' } else { '--workspace' } + $default = '--workspace' # Exit 2 = invalid caller configuration (bad ANVIL_IMPACT), distinct from # the generic 1 used for operational failures -- see _anvil-impact-snapshot. if ($env:ANVIL_IMPACT -and $env:ANVIL_IMPACT -notin @('off', 'consume')) { @@ -623,7 +620,7 @@ _anvil-impact-include tier: if ($env:ANVIL_IMPACT -eq 'off') { Write-Output $default; exit 0 } $file = "target/anvil/impact/include_$tier.txt" if (Test-Path $file) { - Write-Output ("$(Get-Content -Raw $file)".Trim()) + Get-Content -LiteralPath $file | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } } else { Write-Output $default } \ No newline at end of file From 2abf2d098e55cd09e2e858ab6ded1d29ef68b367 Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Thu, 3 Sep 2026 10:29:43 +0200 Subject: [PATCH 02/15] fix(anvil): preserve workspace-wide formatting Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .anvil.lock | 4 ++-- crates/cargo-anvil/docs/design/checks.md | 5 +++-- crates/cargo-anvil/docs/design/local.md | 3 ++- crates/cargo-anvil/templates/justfiles/anvil/checks/fmt.just | 3 ++- crates/cargo-anvil/tests/recipe_contracts.rs | 4 +--- .../cargo-anvil/tests/snapshots/snapshots__ado_backend.snap | 3 ++- .../tests/snapshots/snapshots__github_backend.snap | 3 ++- .../cargo-anvil/tests/snapshots/snapshots__local_only.snap | 3 ++- justfiles/anvil/checks/fmt.just | 3 ++- 9 files changed, 18 insertions(+), 13 deletions(-) diff --git a/.anvil.lock b/.anvil.lock index 5eaec2e2c..74986a154 100644 --- a/.anvil.lock +++ b/.anvil.lock @@ -1,7 +1,7 @@ version = 1 tool = "anvil" tool_version = "0.6.0" -catalog_checksum = "sha256:f3ec246c9989f64ba76cd207360839c87349028dbfe3d38ab7ad3e594f046dcd" +catalog_checksum = "sha256:84d19f9fd43ffe070b2f8a1204a11658ae3ec5dd829e1c9f2ab696a4abe853a5" [[file]] path = ".anvil/container/Dockerfile.dockerignore" @@ -105,7 +105,7 @@ checksum = "sha256:33f28dd471646d8544be836dd61543c254a71ea2b11deac1a0870a94da1fc [[file]] path = "justfiles/anvil/checks/fmt.just" -checksum = "sha256:f0834d42e9df5c41c9b2ac8ad2db7d96ebeb61d1d6e304fcde6d726496f039a5" +checksum = "sha256:01cf50c6acf69fc1433da564aaa62bc336fb4f52166d2f114f20876e95032e27" [[file]] path = "justfiles/anvil/checks/license-headers.just" diff --git a/crates/cargo-anvil/docs/design/checks.md b/crates/cargo-anvil/docs/design/checks.md index b1f091730..323c9aa6d 100644 --- a/crates/cargo-anvil/docs/design/checks.md +++ b/crates/cargo-anvil/docs/design/checks.md @@ -376,8 +376,9 @@ Every **impact-scoped** check depends on `anvil-impact` and resolves its categor calling `_anvil-impact-include `. Ordinary checks splat those tokens directly into `cargo each`; its empty-set success behavior replaces recipe-specific skip guards, and `{packages}` injects the resolved package set into a single child Cargo invocation. -Orchestration-heavy checks capture the same token array and handle `--none` before their -domain-specific work. The **same** cache is read in cloud workflows — the impact job +Checks that must perform work before or around cargo-each capture the same token array +and handle `--none` first; this includes `fmt`, whose modified selector admits a separate +full-workspace per-manifest fan-out. The **same** cache is read in cloud workflows — the impact job uploads `target/anvil/impact/` as an artifact and each group job downloads it — so the identical code path runs locally and in CI, with no scoping threaded through environment variables. Scoping is on by default both locally and in CI; it is disabled only by diff --git a/crates/cargo-anvil/docs/design/local.md b/crates/cargo-anvil/docs/design/local.md index e5c1f5806..7ee44944b 100644 --- a/crates/cargo-anvil/docs/design/local.md +++ b/crates/cargo-anvil/docs/design/local.md @@ -595,7 +595,8 @@ resolve scope themselves; each underlying check reads what it needs. (typically a docs-only PR, or a PR touching only files cargo-delta's `file_exclude_patterns` ignore). Ordinary recipes pass it directly to cargo-each, which exits successfully without spawning the child command. Orchestration-heavy recipes -detect it before performing setup that would otherwise have side effects. +detect it before performing setup that would otherwise have side effects or, for `fmt`, +before starting its separate full-workspace per-manifest cargo-each fan-out. ### 4.3 Disabling scoping and the escape hatch diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/fmt.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/fmt.just index 50864e4e6..c7a49e492 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/fmt.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/fmt.just @@ -23,7 +23,8 @@ anvil-fmt: anvil-fmt-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $selection = @(& "{{ just_executable() }}" _anvil-impact-include modified) if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo {{_anvil_stable_toolchain_args}} each --workspace --keep-going '--' cargo '+{{ rust_nightly }}' fmt --manifest-path '{manifest}' --check + if ($selection -contains '--none') { Write-Host 'anvil-fmt: no modified packages; skipping'; exit 0 } + cargo {{_anvil_stable_toolchain_args}} each --workspace --keep-going '--' cargo '+{{ rust_nightly }}' fmt --manifest-path '{manifest}' --check if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Per-check setup + validate-prereqs diff --git a/crates/cargo-anvil/tests/recipe_contracts.rs b/crates/cargo-anvil/tests/recipe_contracts.rs index 60a8fb5eb..bd80cf622 100644 --- a/crates/cargo-anvil/tests/recipe_contracts.rs +++ b/crates/cargo-anvil/tests/recipe_contracts.rs @@ -921,9 +921,7 @@ fn fmt_delegates_workspace_iteration_to_cargo_each() { ); let commands = fs::read_to_string(&log).unwrap(); assert!( - commands.contains( - "each --workspace --once -- cargo each --workspace --keep-going -- cargo +nightly-test fmt --manifest-path {manifest} --check" - ), + commands.contains("each --workspace --keep-going -- cargo +nightly-test fmt --manifest-path {manifest} --check"), "unexpected cargo invocation: {commands}" ); assert!(!commands.contains("fmt --all")); diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap index 919bb2074..c01d37657 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap @@ -2161,7 +2161,8 @@ anvil-fmt: anvil-fmt-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $selection = @(& "{{ just_executable() }}" _anvil-impact-include modified) if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo {{_anvil_stable_toolchain_args}} each --workspace --keep-going '--' cargo '+{{ rust_nightly }}' fmt --manifest-path '{manifest}' --check + if ($selection -contains '--none') { Write-Host 'anvil-fmt: no modified packages; skipping'; exit 0 } + cargo {{_anvil_stable_toolchain_args}} each --workspace --keep-going '--' cargo '+{{ rust_nightly }}' fmt --manifest-path '{manifest}' --check if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Per-check setup + validate-prereqs diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap index 63059724f..3bbdb6fbc 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap @@ -2147,7 +2147,8 @@ anvil-fmt: anvil-fmt-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $selection = @(& "{{ just_executable() }}" _anvil-impact-include modified) if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo {{_anvil_stable_toolchain_args}} each --workspace --keep-going '--' cargo '+{{ rust_nightly }}' fmt --manifest-path '{manifest}' --check + if ($selection -contains '--none') { Write-Host 'anvil-fmt: no modified packages; skipping'; exit 0 } + cargo {{_anvil_stable_toolchain_args}} each --workspace --keep-going '--' cargo '+{{ rust_nightly }}' fmt --manifest-path '{manifest}' --check if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Per-check setup + validate-prereqs diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap index ac448efad..3c94d05a0 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap @@ -1031,7 +1031,8 @@ anvil-fmt: anvil-fmt-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $selection = @(& "{{ just_executable() }}" _anvil-impact-include modified) if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo {{_anvil_stable_toolchain_args}} each --workspace --keep-going '--' cargo '+{{ rust_nightly }}' fmt --manifest-path '{manifest}' --check + if ($selection -contains '--none') { Write-Host 'anvil-fmt: no modified packages; skipping'; exit 0 } + cargo {{_anvil_stable_toolchain_args}} each --workspace --keep-going '--' cargo '+{{ rust_nightly }}' fmt --manifest-path '{manifest}' --check if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Per-check setup + validate-prereqs diff --git a/justfiles/anvil/checks/fmt.just b/justfiles/anvil/checks/fmt.just index 50864e4e6..c7a49e492 100644 --- a/justfiles/anvil/checks/fmt.just +++ b/justfiles/anvil/checks/fmt.just @@ -23,7 +23,8 @@ anvil-fmt: anvil-fmt-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $selection = @(& "{{ just_executable() }}" _anvil-impact-include modified) if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo {{_anvil_stable_toolchain_args}} each --workspace --keep-going '--' cargo '+{{ rust_nightly }}' fmt --manifest-path '{manifest}' --check + if ($selection -contains '--none') { Write-Host 'anvil-fmt: no modified packages; skipping'; exit 0 } + cargo {{_anvil_stable_toolchain_args}} each --workspace --keep-going '--' cargo '+{{ rust_nightly }}' fmt --manifest-path '{manifest}' --check if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Per-check setup + validate-prereqs From 728034f15480439636ddda64038b0dc607717633 Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Thu, 3 Sep 2026 10:32:04 +0200 Subject: [PATCH 03/15] fix(anvil): version impact selector cache Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .anvil.lock | 4 ++-- crates/cargo-anvil/docs/implementation.md | 4 +++- .../templates/justfiles/anvil/impact.just | 4 +++- crates/cargo-anvil/tests/impact.rs | 18 ++++++++++++++++-- .../snapshots/snapshots__ado_backend.snap | 4 +++- .../snapshots/snapshots__github_backend.snap | 4 +++- .../tests/snapshots/snapshots__local_only.snap | 4 +++- justfiles/anvil/impact.just | 4 +++- 8 files changed, 36 insertions(+), 10 deletions(-) diff --git a/.anvil.lock b/.anvil.lock index 74986a154..a5d40186d 100644 --- a/.anvil.lock +++ b/.anvil.lock @@ -1,7 +1,7 @@ version = 1 tool = "anvil" tool_version = "0.6.0" -catalog_checksum = "sha256:84d19f9fd43ffe070b2f8a1204a11658ae3ec5dd829e1c9f2ab696a4abe853a5" +catalog_checksum = "sha256:aa1c165305941851dae5e47f29219344fcf8ed6e9103bd156d0c785ec70f2c32" [[file]] path = ".anvil/container/Dockerfile.dockerignore" @@ -217,7 +217,7 @@ checksum = "sha256:c1859557980f733777253e9380d90ef710512fde1748f27795cd498501998 [[file]] path = "justfiles/anvil/impact.just" -checksum = "sha256:1a38f46d55f058d6ca823a08d86c928727b230e9141aa15de20d2395aa552803" +checksum = "sha256:9d1f9df6948e1e7cdfcc96f7a04f2fd86bf7af3d152113b46ce5abcadfbe41ce" [[file]] path = "justfiles/anvil/mod.just" diff --git a/crates/cargo-anvil/docs/implementation.md b/crates/cargo-anvil/docs/implementation.md index cb4b85bd6..2fb472c04 100644 --- a/crates/cargo-anvil/docs/implementation.md +++ b/crates/cargo-anvil/docs/implementation.md @@ -161,7 +161,9 @@ Ordinal (case-sensitive) lookups (package name, lib/proc-macro target, manifest- a reported identifier resolves to **zero** packages (an unmapped gap) or **more than one** (an ambiguity), the recipe fails hard rather than guessing — under-scoping would silently skip affected work. cargo-each resolves those names and version-qualifies package arguments -before invoking child Cargo commands. Complementarily, a dirty working tree (any uncommitted change outside `target/`, +before invoking child Cargo commands. The impact state key includes a projection-format +version, so the first run after an include-format change regenerates cached selectors +instead of accepting incompatible files from an older catalog. Complementarily, a dirty working tree (any uncommitted change outside `target/`, detected via a git `:(exclude)` pathspec) widens *every* tier to `--workspace` locally, because cargo-delta scopes on the committed diff and cannot see working-tree edits. Cloud checkouts are clean, so this only affects local runs. diff --git a/crates/cargo-anvil/templates/justfiles/anvil/impact.just b/crates/cargo-anvil/templates/justfiles/anvil/impact.just index f97a1a4d5..dda73d637 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/impact.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/impact.just @@ -399,7 +399,9 @@ anvil-impact: _anvil-impact-snapshot # Content cache key: regenerate the projection only when a snapshot moved # or an output is missing. Keyed on the snapshot markers, not mtimes, so a # downstream cloud-workflow job that downloaded the artifact recognizes it. - $wantState = ("$(Get-Content -Raw $baselineKeyFile)".Trim()) + ' ' + ("$(Get-Content -Raw $currentKeyFile)".Trim()) + # Prefix the projection format so an Anvil upgrade that changes the include + # representation cannot accept stale files written by an older catalog. + $wantState = 'projection-v2 ' + ("$(Get-Content -Raw $baselineKeyFile)".Trim()) + ' ' + ("$(Get-Content -Raw $currentKeyFile)".Trim()) $includeFiles = @('modified', 'affected', 'required') | ForEach-Object { Join-Path $impactDir "include_$_.txt" } $fresh = (Test-Path $impactJson) -and (Test-Path $impactStateFile) -and ` ("$(Get-Content -Raw $impactStateFile)".Trim() -eq $wantState) -and ` diff --git a/crates/cargo-anvil/tests/impact.rs b/crates/cargo-anvil/tests/impact.rs index 0b3d531f9..85acd09b8 100644 --- a/crates/cargo-anvil/tests/impact.rs +++ b/crates/cargo-anvil/tests/impact.rs @@ -314,7 +314,21 @@ fn impact_cache_regenerates_per_key_and_reuses_when_unchanged() { ); assert!(noop.contains("cache hit"), "no-op run should report an impact cache hit:\n{noop}"); - // --- 3. HEAD moves (a new commit): only `current.json` is regenerated. --- + // --- 3. An old projection format forces include regeneration. --- + let state_file = impact_dir.join("impact.state"); + let current_state = fs::read_to_string(&state_file).unwrap(); + fs::write(&state_file, current_state.trim_start_matches("projection-v2 ")).unwrap(); + let migrated = run_impact(root); + assert!( + !migrated.contains("impact set up to date (cache hit)"), + "an old projection format must not reuse incompatible include files:\n{migrated}" + ); + assert!( + fs::read_to_string(&state_file).unwrap().starts_with("projection-v2 "), + "regeneration must persist the current projection format" + ); + + // --- 4. HEAD moves (a new commit): only `current.json` is regenerated. --- // A committed change advances HEAD without moving the base ref // (origin/master), so current.key changes while the baseline key does // not. The tree stays clean, so scoping is NOT widened. @@ -335,7 +349,7 @@ fn impact_cache_regenerates_per_key_and_reuses_when_unchanged() { "a new commit moves HEAD, so the current snapshot is regenerated:\n{edited}" ); - // --- 4. Base ref moves: only `baseline.json` is regenerated. --- + // --- 5. Base ref moves: only `baseline.json` is regenerated. --- // Advance origin/master to a NEW commit without moving HEAD (commit on a // throwaway branch, repoint the ref, return to main). The tree stays // clean, so `current` is untouched and only the baseline regenerates. diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap index c01d37657..49fec21cb 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap @@ -5522,7 +5522,9 @@ anvil-impact: _anvil-impact-snapshot # Content cache key: regenerate the projection only when a snapshot moved # or an output is missing. Keyed on the snapshot markers, not mtimes, so a # downstream cloud-workflow job that downloaded the artifact recognizes it. - $wantState = ("$(Get-Content -Raw $baselineKeyFile)".Trim()) + ' ' + ("$(Get-Content -Raw $currentKeyFile)".Trim()) + # Prefix the projection format so an Anvil upgrade that changes the include + # representation cannot accept stale files written by an older catalog. + $wantState = 'projection-v2 ' + ("$(Get-Content -Raw $baselineKeyFile)".Trim()) + ' ' + ("$(Get-Content -Raw $currentKeyFile)".Trim()) $includeFiles = @('modified', 'affected', 'required') | ForEach-Object { Join-Path $impactDir "include_$_.txt" } $fresh = (Test-Path $impactJson) -and (Test-Path $impactStateFile) -and ` ("$(Get-Content -Raw $impactStateFile)".Trim() -eq $wantState) -and ` diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap index 3bbdb6fbc..46a7525c1 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap @@ -5508,7 +5508,9 @@ anvil-impact: _anvil-impact-snapshot # Content cache key: regenerate the projection only when a snapshot moved # or an output is missing. Keyed on the snapshot markers, not mtimes, so a # downstream cloud-workflow job that downloaded the artifact recognizes it. - $wantState = ("$(Get-Content -Raw $baselineKeyFile)".Trim()) + ' ' + ("$(Get-Content -Raw $currentKeyFile)".Trim()) + # Prefix the projection format so an Anvil upgrade that changes the include + # representation cannot accept stale files written by an older catalog. + $wantState = 'projection-v2 ' + ("$(Get-Content -Raw $baselineKeyFile)".Trim()) + ' ' + ("$(Get-Content -Raw $currentKeyFile)".Trim()) $includeFiles = @('modified', 'affected', 'required') | ForEach-Object { Join-Path $impactDir "include_$_.txt" } $fresh = (Test-Path $impactJson) -and (Test-Path $impactStateFile) -and ` ("$(Get-Content -Raw $impactStateFile)".Trim() -eq $wantState) -and ` diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap index 3c94d05a0..c1439252b 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap @@ -4392,7 +4392,9 @@ anvil-impact: _anvil-impact-snapshot # Content cache key: regenerate the projection only when a snapshot moved # or an output is missing. Keyed on the snapshot markers, not mtimes, so a # downstream cloud-workflow job that downloaded the artifact recognizes it. - $wantState = ("$(Get-Content -Raw $baselineKeyFile)".Trim()) + ' ' + ("$(Get-Content -Raw $currentKeyFile)".Trim()) + # Prefix the projection format so an Anvil upgrade that changes the include + # representation cannot accept stale files written by an older catalog. + $wantState = 'projection-v2 ' + ("$(Get-Content -Raw $baselineKeyFile)".Trim()) + ' ' + ("$(Get-Content -Raw $currentKeyFile)".Trim()) $includeFiles = @('modified', 'affected', 'required') | ForEach-Object { Join-Path $impactDir "include_$_.txt" } $fresh = (Test-Path $impactJson) -and (Test-Path $impactStateFile) -and ` ("$(Get-Content -Raw $impactStateFile)".Trim() -eq $wantState) -and ` diff --git a/justfiles/anvil/impact.just b/justfiles/anvil/impact.just index f97a1a4d5..dda73d637 100644 --- a/justfiles/anvil/impact.just +++ b/justfiles/anvil/impact.just @@ -399,7 +399,9 @@ anvil-impact: _anvil-impact-snapshot # Content cache key: regenerate the projection only when a snapshot moved # or an output is missing. Keyed on the snapshot markers, not mtimes, so a # downstream cloud-workflow job that downloaded the artifact recognizes it. - $wantState = ("$(Get-Content -Raw $baselineKeyFile)".Trim()) + ' ' + ("$(Get-Content -Raw $currentKeyFile)".Trim()) + # Prefix the projection format so an Anvil upgrade that changes the include + # representation cannot accept stale files written by an older catalog. + $wantState = 'projection-v2 ' + ("$(Get-Content -Raw $baselineKeyFile)".Trim()) + ' ' + ("$(Get-Content -Raw $currentKeyFile)".Trim()) $includeFiles = @('modified', 'affected', 'required') | ForEach-Object { Join-Path $impactDir "include_$_.txt" } $fresh = (Test-Path $impactJson) -and (Test-Path $impactStateFile) -and ` ("$(Get-Content -Raw $impactStateFile)".Trim() -eq $wantState) -and ` From 162ee94d4afd983a475c5a2acbd6450dfbd6c9bf Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Thu, 3 Sep 2026 11:12:12 +0200 Subject: [PATCH 04/15] fix(anvil): skip doctests for bin-only packages Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .anvil.lock | 4 +- crates/cargo-anvil/docs/design/checks.md | 2 +- .../justfiles/anvil/checks/doc-test.just | 44 +++++++++++++++- crates/cargo-anvil/tests/recipe_contracts.rs | 50 ++++++++++++++++++- .../snapshots/snapshots__ado_backend.snap | 44 +++++++++++++++- .../snapshots/snapshots__github_backend.snap | 44 +++++++++++++++- .../snapshots/snapshots__local_only.snap | 44 +++++++++++++++- justfiles/anvil/checks/doc-test.just | 44 +++++++++++++++- 8 files changed, 261 insertions(+), 15 deletions(-) diff --git a/.anvil.lock b/.anvil.lock index a5d40186d..b2d852457 100644 --- a/.anvil.lock +++ b/.anvil.lock @@ -1,7 +1,7 @@ version = 1 tool = "anvil" tool_version = "0.6.0" -catalog_checksum = "sha256:aa1c165305941851dae5e47f29219344fcf8ed6e9103bd156d0c785ec70f2c32" +catalog_checksum = "sha256:f24feb751bab242b8b29c27b4fe77e1a4ba5e25def1c0f49f7ddc4a81b6949c4" [[file]] path = ".anvil/container/Dockerfile.dockerignore" @@ -85,7 +85,7 @@ checksum = "sha256:d2f6a9536d62030db5505c88d510c4114cb0aab81a7af0e995ba8da5ce3a1 [[file]] path = "justfiles/anvil/checks/doc-test.just" -checksum = "sha256:d44faa962fd3422edac2090b982a9839f0ce6b3f14a7aed83d1630dd8bfc9040" +checksum = "sha256:983f97606d211a0718aaa2a49ac39b1b38d7f66f50632ec5acd49524c18152c8" [[file]] path = "justfiles/anvil/checks/ensure-no-cyclic-deps.just" diff --git a/crates/cargo-anvil/docs/design/checks.md b/crates/cargo-anvil/docs/design/checks.md index 323c9aa6d..7551b2ffe 100644 --- a/crates/cargo-anvil/docs/design/checks.md +++ b/crates/cargo-anvil/docs/design/checks.md @@ -206,7 +206,7 @@ matrix overhead. | Check | Invocation | Source | |--------------|-----------------------------------------------------------------------------|--------| | `llvm-cov` | Runs tests for every affected package under both feature configurations. Packages with a positive coverage threshold run through self-contained `cargo + llvm-cov nextest --no-report` invocations and produce per-config lcov/cobertura reports. Packages declaring `min-lines-percent = 0` still run through plain `cargo nextest`; the opt-out disables measurement and gating, never tests. Per-config reports avoid Windows command-line overflow and are reconciled downstream by cargo-coverage-gate, Codecov, and ADO. Codecov is display-only; the local coverage gate is authoritative. | oxidizer, oxidizer-github; gate via [`cargo-coverage-gate`](../../../cargo-coverage-gate) | -| `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 | +| `doc-test` | Two cargo-test runs over affected library and proc-macro packages: `cargo test --doc --all-features --locked` and `cargo test --doc --locked` (default features). Bin-only packages are removed because Cargo errors when they are the only selected packages and they have no rustdoc tests. Running both feature modes catches doctests that only compile under one configuration. nextest does not run doctests, so this stays separate. | 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 | #### `pr-msrv` (minimum-version tests) 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 d7e0f9f58..60de891d9 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/doc-test.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/doc-test.just @@ -12,6 +12,10 @@ # doctests that only compile when those features are off. The # duplication is cheap because doctests are already a small fraction # of total compile/test time. +# +# Cargo errors when `cargo test --doc` selects only bin-only packages. Build +# the documentation-capable subset (plain libraries and proc macros) from +# metadata before delegating the two invocations to cargo-each. # Run documentation tests for affected workspace packages. [script("pwsh", "-NoProfile")] @@ -19,9 +23,45 @@ anvil-doc-test: anvil-doc-test-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo {{_anvil_stable_toolchain_args}} test --doc '{packages}' --all-features --locked + if ($selection -contains '--none') { Write-Host 'anvil-doc-test: no affected packages; skipping'; exit 0 } + $metadataJson = & cargo {{_anvil_stable_toolchain_args}} metadata --no-deps --format-version 1 + if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-doc-test: cargo metadata failed'; exit $LASTEXITCODE } + try { + $meta = $metadataJson | ConvertFrom-Json + } catch { + Write-Error "anvil-doc-test: could not parse cargo metadata output: $_" + exit 1 + } + $docPackages = @{} + foreach ($package in $meta.packages) { + if ($package.targets | Where-Object { ($_.kind -contains 'lib') -or ($_.kind -contains 'proc-macro') }) { + $docPackages[$package.name] = $true + } + } + $names = if ($selection -contains '--workspace') { + @($docPackages.Keys) + } else { + @( + for ($i = 0; $i -lt $selection.Count; $i++) { + if ($selection[$i] -eq '--package' -and ($i + 1) -lt $selection.Count) { + $name = $selection[++$i] + if ($docPackages.ContainsKey($name)) { $name } + } + } + ) + } + if ($names.Count -eq 0) { + Write-Host 'anvil-doc-test: no affected library or proc-macro packages; skipping' + exit 0 + } + $docSelection = [System.Collections.Generic.List[string]]::new() + foreach ($name in $names) { + $docSelection.Add('--package') + $docSelection.Add($name) + } + & cargo {{_anvil_stable_toolchain_args}} each @docSelection --once '--' cargo {{_anvil_stable_toolchain_args}} test --doc '{packages}' --all-features --locked if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo {{_anvil_stable_toolchain_args}} test --doc '{packages}' --locked + & cargo {{_anvil_stable_toolchain_args}} each @docSelection --once '--' cargo {{_anvil_stable_toolchain_args}} test --doc '{packages}' --locked if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-doc-test` recipe. diff --git a/crates/cargo-anvil/tests/recipe_contracts.rs b/crates/cargo-anvil/tests/recipe_contracts.rs index bd80cf622..d880cf4dd 100644 --- a/crates/cargo-anvil/tests/recipe_contracts.rs +++ b/crates/cargo-anvil/tests/recipe_contracts.rs @@ -20,6 +20,7 @@ const HELPERS: &str = include_str!("../templates/justfiles/anvil/helpers.just"); const IMPACT: &str = include_str!("../templates/justfiles/anvil/impact.just"); const BOLERO: &str = include_str!("../templates/justfiles/anvil/checks/bolero.just"); const FMT: &str = include_str!("../templates/justfiles/anvil/checks/fmt.just"); +const DOC_TEST: &str = include_str!("../templates/justfiles/anvil/checks/doc-test.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"); @@ -129,7 +130,10 @@ if ($args -contains 'metadata') { version = '0.1.0' id = "$($env:FAKE_SECOND_PACKAGE_NAME) 0.1.0" manifest_path = [System.IO.Path]::Combine($root, 'nested', $secondDirLeaf, 'Cargo.toml') - targets = @([pscustomobject]@{ name = $env:FAKE_SECOND_PACKAGE_NAME; kind = @('lib') }) + targets = @([pscustomobject]@{ + name = $env:FAKE_SECOND_PACKAGE_NAME + kind = if ($env:FAKE_SECOND_BIN_ONLY) { @('bin') } else { @('lib') } + }) publish = $null metadata = [pscustomobject]@{} } @@ -145,7 +149,10 @@ if ($args -contains 'metadata') { $env:FAKE_THIRD_PACKAGE_NAME, 'Cargo.toml' ) - targets = @([pscustomobject]@{ name = $env:FAKE_THIRD_PACKAGE_NAME; kind = @('lib') }) + targets = @([pscustomobject]@{ + name = $env:FAKE_THIRD_PACKAGE_NAME + kind = if ($env:FAKE_THIRD_PROC_MACRO) { @('proc-macro') } else { @('lib') } + }) publish = @('private-registry') metadata = [pscustomobject]@{} } @@ -950,6 +957,45 @@ fn fmt_propagates_cargo_each_failure() { assert_failed(&output, "anvil-fmt cargo-each failure"); } +#[test] +fn doc_test_selects_libraries_and_proc_macros_but_not_bin_only_packages() { + if !tools_available() { + return; + } + let tmp = fixture( + &[("doc-test.just", DOC_TEST), ("impact.just", IMPACT)], + &[ + "anvil-tool-rustc-validate-prereqs", + "anvil-toolchain-stable-install", + "anvil-impact", + ], + ); + seed_include(tmp.path(), "affected", "--workspace"); + let log = tmp.path().join("cargo.log"); + let output = run_just( + tmp.path(), + &["anvil-doc-test"], + &[ + ("FAKE_CARGO_LOG", log.as_os_str()), + ("FAKE_SECOND_PACKAGE_NAME", OsStr::new("bin-only")), + ("FAKE_SECOND_BIN_ONLY", OsStr::new("1")), + ("FAKE_THIRD_PACKAGE_NAME", OsStr::new("macro-package")), + ("FAKE_THIRD_PROC_MACRO", OsStr::new("1")), + ], + ); + assert!( + output.status.success(), + "doc-test selection failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + let commands = fs::read_to_string(log).unwrap(); + assert_eq!(commands.matches(" test --doc ").count(), 2, "commands:\n{commands}"); + assert!(commands.contains("--package fixture"), "commands:\n{commands}"); + assert!(commands.contains("--package macro-package"), "commands:\n{commands}"); + assert!(!commands.contains("--package bin-only"), "commands:\n{commands}"); +} + #[test] fn external_types_delegates_library_selection_and_iteration_to_cargo_each() { if !tools_available() { diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap index 49fec21cb..f6707c840 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap @@ -1992,6 +1992,10 @@ anvil-doc-build-validate-prereqs: anvil-tool-rustc-validate-prereqs anvil-tool-c # doctests that only compile when those features are off. The # duplication is cheap because doctests are already a small fraction # of total compile/test time. +# +# Cargo errors when `cargo test --doc` selects only bin-only packages. Build +# the documentation-capable subset (plain libraries and proc macros) from +# metadata before delegating the two invocations to cargo-each. # Run documentation tests for affected workspace packages. [script("pwsh", "-NoProfile")] @@ -1999,9 +2003,45 @@ anvil-doc-test: anvil-doc-test-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo {{_anvil_stable_toolchain_args}} test --doc '{packages}' --all-features --locked + if ($selection -contains '--none') { Write-Host 'anvil-doc-test: no affected packages; skipping'; exit 0 } + $metadataJson = & cargo {{_anvil_stable_toolchain_args}} metadata --no-deps --format-version 1 + if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-doc-test: cargo metadata failed'; exit $LASTEXITCODE } + try { + $meta = $metadataJson | ConvertFrom-Json + } catch { + Write-Error "anvil-doc-test: could not parse cargo metadata output: $_" + exit 1 + } + $docPackages = @{} + foreach ($package in $meta.packages) { + if ($package.targets | Where-Object { ($_.kind -contains 'lib') -or ($_.kind -contains 'proc-macro') }) { + $docPackages[$package.name] = $true + } + } + $names = if ($selection -contains '--workspace') { + @($docPackages.Keys) + } else { + @( + for ($i = 0; $i -lt $selection.Count; $i++) { + if ($selection[$i] -eq '--package' -and ($i + 1) -lt $selection.Count) { + $name = $selection[++$i] + if ($docPackages.ContainsKey($name)) { $name } + } + } + ) + } + if ($names.Count -eq 0) { + Write-Host 'anvil-doc-test: no affected library or proc-macro packages; skipping' + exit 0 + } + $docSelection = [System.Collections.Generic.List[string]]::new() + foreach ($name in $names) { + $docSelection.Add('--package') + $docSelection.Add($name) + } + & cargo {{_anvil_stable_toolchain_args}} each @docSelection --once '--' cargo {{_anvil_stable_toolchain_args}} test --doc '{packages}' --all-features --locked if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo {{_anvil_stable_toolchain_args}} test --doc '{packages}' --locked + & cargo {{_anvil_stable_toolchain_args}} each @docSelection --once '--' cargo {{_anvil_stable_toolchain_args}} test --doc '{packages}' --locked if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-doc-test` recipe. diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap index 46a7525c1..0d2b9bd27 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap @@ -1978,6 +1978,10 @@ anvil-doc-build-validate-prereqs: anvil-tool-rustc-validate-prereqs anvil-tool-c # doctests that only compile when those features are off. The # duplication is cheap because doctests are already a small fraction # of total compile/test time. +# +# Cargo errors when `cargo test --doc` selects only bin-only packages. Build +# the documentation-capable subset (plain libraries and proc macros) from +# metadata before delegating the two invocations to cargo-each. # Run documentation tests for affected workspace packages. [script("pwsh", "-NoProfile")] @@ -1985,9 +1989,45 @@ anvil-doc-test: anvil-doc-test-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo {{_anvil_stable_toolchain_args}} test --doc '{packages}' --all-features --locked + if ($selection -contains '--none') { Write-Host 'anvil-doc-test: no affected packages; skipping'; exit 0 } + $metadataJson = & cargo {{_anvil_stable_toolchain_args}} metadata --no-deps --format-version 1 + if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-doc-test: cargo metadata failed'; exit $LASTEXITCODE } + try { + $meta = $metadataJson | ConvertFrom-Json + } catch { + Write-Error "anvil-doc-test: could not parse cargo metadata output: $_" + exit 1 + } + $docPackages = @{} + foreach ($package in $meta.packages) { + if ($package.targets | Where-Object { ($_.kind -contains 'lib') -or ($_.kind -contains 'proc-macro') }) { + $docPackages[$package.name] = $true + } + } + $names = if ($selection -contains '--workspace') { + @($docPackages.Keys) + } else { + @( + for ($i = 0; $i -lt $selection.Count; $i++) { + if ($selection[$i] -eq '--package' -and ($i + 1) -lt $selection.Count) { + $name = $selection[++$i] + if ($docPackages.ContainsKey($name)) { $name } + } + } + ) + } + if ($names.Count -eq 0) { + Write-Host 'anvil-doc-test: no affected library or proc-macro packages; skipping' + exit 0 + } + $docSelection = [System.Collections.Generic.List[string]]::new() + foreach ($name in $names) { + $docSelection.Add('--package') + $docSelection.Add($name) + } + & cargo {{_anvil_stable_toolchain_args}} each @docSelection --once '--' cargo {{_anvil_stable_toolchain_args}} test --doc '{packages}' --all-features --locked if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo {{_anvil_stable_toolchain_args}} test --doc '{packages}' --locked + & cargo {{_anvil_stable_toolchain_args}} each @docSelection --once '--' cargo {{_anvil_stable_toolchain_args}} test --doc '{packages}' --locked if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-doc-test` recipe. diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap index c1439252b..b8533cfd3 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap @@ -862,6 +862,10 @@ anvil-doc-build-validate-prereqs: anvil-tool-rustc-validate-prereqs anvil-tool-c # doctests that only compile when those features are off. The # duplication is cheap because doctests are already a small fraction # of total compile/test time. +# +# Cargo errors when `cargo test --doc` selects only bin-only packages. Build +# the documentation-capable subset (plain libraries and proc macros) from +# metadata before delegating the two invocations to cargo-each. # Run documentation tests for affected workspace packages. [script("pwsh", "-NoProfile")] @@ -869,9 +873,45 @@ anvil-doc-test: anvil-doc-test-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo {{_anvil_stable_toolchain_args}} test --doc '{packages}' --all-features --locked + if ($selection -contains '--none') { Write-Host 'anvil-doc-test: no affected packages; skipping'; exit 0 } + $metadataJson = & cargo {{_anvil_stable_toolchain_args}} metadata --no-deps --format-version 1 + if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-doc-test: cargo metadata failed'; exit $LASTEXITCODE } + try { + $meta = $metadataJson | ConvertFrom-Json + } catch { + Write-Error "anvil-doc-test: could not parse cargo metadata output: $_" + exit 1 + } + $docPackages = @{} + foreach ($package in $meta.packages) { + if ($package.targets | Where-Object { ($_.kind -contains 'lib') -or ($_.kind -contains 'proc-macro') }) { + $docPackages[$package.name] = $true + } + } + $names = if ($selection -contains '--workspace') { + @($docPackages.Keys) + } else { + @( + for ($i = 0; $i -lt $selection.Count; $i++) { + if ($selection[$i] -eq '--package' -and ($i + 1) -lt $selection.Count) { + $name = $selection[++$i] + if ($docPackages.ContainsKey($name)) { $name } + } + } + ) + } + if ($names.Count -eq 0) { + Write-Host 'anvil-doc-test: no affected library or proc-macro packages; skipping' + exit 0 + } + $docSelection = [System.Collections.Generic.List[string]]::new() + foreach ($name in $names) { + $docSelection.Add('--package') + $docSelection.Add($name) + } + & cargo {{_anvil_stable_toolchain_args}} each @docSelection --once '--' cargo {{_anvil_stable_toolchain_args}} test --doc '{packages}' --all-features --locked if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo {{_anvil_stable_toolchain_args}} test --doc '{packages}' --locked + & cargo {{_anvil_stable_toolchain_args}} each @docSelection --once '--' cargo {{_anvil_stable_toolchain_args}} test --doc '{packages}' --locked if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-doc-test` recipe. diff --git a/justfiles/anvil/checks/doc-test.just b/justfiles/anvil/checks/doc-test.just index d7e0f9f58..60de891d9 100644 --- a/justfiles/anvil/checks/doc-test.just +++ b/justfiles/anvil/checks/doc-test.just @@ -12,6 +12,10 @@ # doctests that only compile when those features are off. The # duplication is cheap because doctests are already a small fraction # of total compile/test time. +# +# Cargo errors when `cargo test --doc` selects only bin-only packages. Build +# the documentation-capable subset (plain libraries and proc macros) from +# metadata before delegating the two invocations to cargo-each. # Run documentation tests for affected workspace packages. [script("pwsh", "-NoProfile")] @@ -19,9 +23,45 @@ anvil-doc-test: anvil-doc-test-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo {{_anvil_stable_toolchain_args}} test --doc '{packages}' --all-features --locked + if ($selection -contains '--none') { Write-Host 'anvil-doc-test: no affected packages; skipping'; exit 0 } + $metadataJson = & cargo {{_anvil_stable_toolchain_args}} metadata --no-deps --format-version 1 + if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-doc-test: cargo metadata failed'; exit $LASTEXITCODE } + try { + $meta = $metadataJson | ConvertFrom-Json + } catch { + Write-Error "anvil-doc-test: could not parse cargo metadata output: $_" + exit 1 + } + $docPackages = @{} + foreach ($package in $meta.packages) { + if ($package.targets | Where-Object { ($_.kind -contains 'lib') -or ($_.kind -contains 'proc-macro') }) { + $docPackages[$package.name] = $true + } + } + $names = if ($selection -contains '--workspace') { + @($docPackages.Keys) + } else { + @( + for ($i = 0; $i -lt $selection.Count; $i++) { + if ($selection[$i] -eq '--package' -and ($i + 1) -lt $selection.Count) { + $name = $selection[++$i] + if ($docPackages.ContainsKey($name)) { $name } + } + } + ) + } + if ($names.Count -eq 0) { + Write-Host 'anvil-doc-test: no affected library or proc-macro packages; skipping' + exit 0 + } + $docSelection = [System.Collections.Generic.List[string]]::new() + foreach ($name in $names) { + $docSelection.Add('--package') + $docSelection.Add($name) + } + & cargo {{_anvil_stable_toolchain_args}} each @docSelection --once '--' cargo {{_anvil_stable_toolchain_args}} test --doc '{packages}' --all-features --locked if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo {{_anvil_stable_toolchain_args}} test --doc '{packages}' --locked + & cargo {{_anvil_stable_toolchain_args}} each @docSelection --once '--' cargo {{_anvil_stable_toolchain_args}} test --doc '{packages}' --locked if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-doc-test` recipe. From c488544fb6c06d265440b5c221ef47282da10691 Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Thu, 3 Sep 2026 12:36:44 +0200 Subject: [PATCH 05/15] docs(anvil): align selector example with contract Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/cargo-anvil/docs/design/local.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/cargo-anvil/docs/design/local.md b/crates/cargo-anvil/docs/design/local.md index 7ee44944b..8d6bf469e 100644 --- a/crates/cargo-anvil/docs/design/local.md +++ b/crates/cargo-anvil/docs/design/local.md @@ -532,7 +532,9 @@ category's selector tokens directly to `cargo-each`: [script("pwsh", "-NoProfile")] anvil-clippy: anvil-clippy-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - & cargo each @(& "{{ just_executable() }}" _anvil-impact-include affected) --once '--' cargo clippy '{packages}' --all-targets --all-features --locked '--' '-D' 'warnings' + $selection = @(& "{{ just_executable() }}" _anvil-impact-include affected) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & cargo each @selection --once '--' cargo clippy '{packages}' --all-targets --all-features --locked '--' '-D' 'warnings' if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } ``` From 6663842b7471356aa3472c3186587971f07d1b5f Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Thu, 3 Sep 2026 12:46:19 +0200 Subject: [PATCH 06/15] fix(anvil): qualify scoped coverage packages Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .anvil.lock | 4 +- .../justfiles/anvil/checks/llvm-cov.just | 51 +++++++--- crates/cargo-anvil/tests/recipe_contracts.rs | 94 ++++++++++++++++++- .../snapshots/snapshots__ado_backend.snap | 51 +++++++--- .../snapshots/snapshots__github_backend.snap | 51 +++++++--- .../snapshots/snapshots__local_only.snap | 51 +++++++--- justfiles/anvil/checks/llvm-cov.just | 51 +++++++--- 7 files changed, 279 insertions(+), 74 deletions(-) diff --git a/.anvil.lock b/.anvil.lock index 4f224aea6..9c8fd6d8b 100644 --- a/.anvil.lock +++ b/.anvil.lock @@ -1,7 +1,7 @@ version = 1 tool = "anvil" tool_version = "0.6.0" -catalog_checksum = "sha256:8278372817468160ef43c934e73778b9b8848d0d9c24c6a7b4d6721af004a655" +catalog_checksum = "sha256:088371593c360ee572074a23405d279bee24f13d725dcb015ca8f45fd014fe62" [[file]] path = ".anvil/container/Dockerfile.dockerignore" @@ -117,7 +117,7 @@ checksum = "sha256:7069b83328f81b280187e45df5f8240152e6bf9245035b7683673e22e2b94 [[file]] path = "justfiles/anvil/checks/llvm-cov.just" -checksum = "sha256:a0d056607294e44f41d1820d25da77913da99d962a92a6de2404010a81ce0856" +checksum = "sha256:90d3da053e40b41cb399f02f2f19274e1b5b8b6da5aaa2d8fcad448883aad847" [[file]] path = "justfiles/anvil/checks/loom.just" 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 946a86a69..8e83e8cd5 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/llvm-cov.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/llvm-cov.just @@ -43,24 +43,46 @@ anvil-llvm-cov: anvil-llvm-cov-validate-prereqs anvil-impact Write-Error 'anvil-llvm-cov: cargo metadata failed' exit $LASTEXITCODE } + try { + $metadata = $metadataJson | ConvertFrom-Json + } catch { + Write-Error "anvil-llvm-cov: could not parse cargo metadata output: $_" + exit 1 + } + $specsByName = @{} + foreach ($package in $metadata.packages) { + $specsByName[$package.name] = "$($package.name)@$($package.version)" + } $optedOut = @( - ($metadataJson | ConvertFrom-Json).packages | + $metadata.packages | Where-Object { $null -ne $_.metadata.'coverage-gate'.'min-lines-percent' -and [double]$_.metadata.'coverage-gate'.'min-lines-percent' -eq 0 } | ForEach-Object { $_.name } ) - if ($optedOut.Count -gt 0) { - $filtered = [System.Collections.Generic.List[string]]::new() - for ($i = 0; $i -lt $pkg.Count; $i++) { - if ($pkg[$i] -eq '--package' -and ($i + 1) -lt $pkg.Count -and $optedOut -contains $pkg[$i + 1]) { - $testOnlyPkg.Add('--package') - $testOnlyPkg.Add($pkg[$i + 1]) - $i++ - continue - } - $filtered.Add($pkg[$i]) + $filtered = [System.Collections.Generic.List[string]]::new() + $qualifiedAll = [System.Collections.Generic.List[string]]::new() + for ($i = 0; $i -lt $pkg.Count; $i++) { + if ($pkg[$i] -ne '--package' -or ($i + 1) -ge $pkg.Count) { + Write-Error "anvil-llvm-cov: unsupported affected-package arguments: $($pkg -join ' ')" + exit 1 + } + $name = $pkg[++$i] + if (-not $specsByName.ContainsKey($name)) { + Write-Error "anvil-llvm-cov: affected package '$name' is absent from workspace metadata" + exit 1 + } + $spec = $specsByName[$name] + $qualifiedAll.Add('--package') + $qualifiedAll.Add($spec) + if ($optedOut -contains $name) { + $testOnlyPkg.Add('--package') + $testOnlyPkg.Add($spec) + } else { + $filtered.Add('--package') + $filtered.Add($spec) } - $pkg = $filtered.ToArray() } + $pkg = $filtered.ToArray() + $allTestPkg = $qualifiedAll.ToArray() } $configs = @( @{ name = 'all-features'; flag = '--all-features' }, @@ -151,13 +173,14 @@ anvil-llvm-cov: anvil-llvm-cov-validate-prereqs anvil-impact # that were actually instrumented -- otherwise unmeasured members would # report NO DATA and fail the gate. When unscoped, $pkg is `--workspace` # (no gate flag); drop it so the gate defaults to every member. - # + # cargo-coverage-gate keys on bare workspace package names, so strip the + # version qualifier used to disambiguate the direct Cargo commands. $gateArgs = [System.Collections.Generic.List[string]]::new() for ($i = 0; $i -lt $pkg.Count; $i++) { if ($pkg[$i] -eq '--workspace') { continue } if ($pkg[$i] -eq '--package' -and ($i + 1) -lt $pkg.Count) { $gateArgs.Add('--package') - $gateArgs.Add($pkg[$i + 1]) + $gateArgs.Add(($pkg[$i + 1] -split '@', 2)[0]) $i++ continue } diff --git a/crates/cargo-anvil/tests/recipe_contracts.rs b/crates/cargo-anvil/tests/recipe_contracts.rs index d880cf4dd..daeba19cc 100644 --- a/crates/cargo-anvil/tests/recipe_contracts.rs +++ b/crates/cargo-anvil/tests/recipe_contracts.rs @@ -115,7 +115,13 @@ if ($args -contains 'metadata') { $null } metadata = [pscustomobject]@{ - 'coverage-gate' = [pscustomobject]@{ 'min-lines-percent' = 0 } + 'coverage-gate' = [pscustomobject]@{ + 'min-lines-percent' = if ($env:FAKE_COVERAGE_THRESHOLD) { + [double]$env:FAKE_COVERAGE_THRESHOLD + } else { + 0 + } + } } } ) @@ -989,11 +995,49 @@ fn doc_test_selects_libraries_and_proc_macros_but_not_bin_only_packages() { String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr) ); - let commands = fs::read_to_string(log).unwrap(); + let commands = fs::read_to_string(&log).unwrap(); assert_eq!(commands.matches(" test --doc ").count(), 2, "commands:\n{commands}"); assert!(commands.contains("--package fixture"), "commands:\n{commands}"); assert!(commands.contains("--package macro-package"), "commands:\n{commands}"); assert!(!commands.contains("--package bin-only"), "commands:\n{commands}"); + + seed_include(tmp.path(), "affected", "--package\nfixture\n--package\nbin-only"); + fs::write(&log, "").unwrap(); + let scoped = run_just( + tmp.path(), + &["anvil-doc-test"], + &[ + ("FAKE_CARGO_LOG", log.as_os_str()), + ("FAKE_SECOND_PACKAGE_NAME", OsStr::new("bin-only")), + ("FAKE_SECOND_BIN_ONLY", OsStr::new("1")), + ], + ); + assert!(scoped.status.success(), "scoped doc-test selection failed"); + let scoped_commands = fs::read_to_string(&log).unwrap(); + assert_eq!(scoped_commands.matches(" test --doc ").count(), 2); + assert!(scoped_commands.contains("--package fixture")); + assert!(!scoped_commands.contains("--package bin-only")); + + seed_include(tmp.path(), "affected", "--package\nbin-only"); + fs::write(&log, "").unwrap(); + let bin_only = run_just( + tmp.path(), + &["anvil-doc-test"], + &[ + ("FAKE_CARGO_LOG", log.as_os_str()), + ("FAKE_SECOND_PACKAGE_NAME", OsStr::new("bin-only")), + ("FAKE_SECOND_BIN_ONLY", OsStr::new("1")), + ], + ); + assert!(bin_only.status.success(), "bin-only doc-test selection must skip cleanly"); + assert!( + String::from_utf8_lossy(&bin_only.stdout).contains("no affected library or proc-macro packages"), + "bin-only skip must explain why no doctests ran" + ); + assert!( + !fs::read_to_string(&log).unwrap().lines().any(|line| line.contains("each ")), + "bin-only selection must not invoke cargo-each" + ); } #[test] @@ -1097,6 +1141,52 @@ fn semver_includes_libraries_restricted_to_named_registries() { ); } +#[test] +fn llvm_cov_qualifies_direct_cargo_selectors_but_not_gate_selectors() { + if !tools_available() { + return; + } + let tmp = fixture( + &[("llvm-cov.just", LLVM_COV), ("impact.just", IMPACT)], + &[ + "anvil-component-nightly-llvm-tools-validate-prereqs", + "anvil-tool-cargo-llvm-cov-validate-prereqs", + "anvil-tool-cargo-nextest-validate-prereqs", + "anvil-tool-cargo-coverage-gate-validate-prereqs", + "anvil-component-nightly-llvm-tools-install", + "anvil-tool-cargo-llvm-cov-install installer", + "anvil-tool-cargo-nextest-install installer", + "anvil-tool-cargo-coverage-gate-install installer", + "anvil-impact", + ], + ); + let log = tmp.path().join("cargo.log"); + seed_include(tmp.path(), "affected", "--package\nfixture"); + let output = run_just( + tmp.path(), + &["anvil-llvm-cov"], + &[("FAKE_CARGO_LOG", log.as_os_str()), ("FAKE_COVERAGE_THRESHOLD", OsStr::new("100"))], + ); + assert!( + output.status.success(), + "qualified coverage selection failed:\n{}", + String::from_utf8_lossy(&output.stderr) + ); + let calls = fs::read_to_string(log).unwrap(); + assert!( + calls.contains("llvm-cov nextest --package fixture@0.1.0"), + "direct Cargo selectors must be version-qualified:\n{calls}" + ); + assert!( + calls.contains("coverage-gate --package fixture --lcov"), + "coverage-gate selectors must remain bare workspace names:\n{calls}" + ); + assert!( + !calls.contains("coverage-gate --package fixture@0.1.0"), + "coverage-gate does not accept version-qualified names:\n{calls}" + ); +} + #[test] fn all_coverage_opted_out_packages_run_both_test_configurations() { if !tools_available() { diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap index f6707c840..8e965f5b9 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap @@ -2302,24 +2302,46 @@ anvil-llvm-cov: anvil-llvm-cov-validate-prereqs anvil-impact Write-Error 'anvil-llvm-cov: cargo metadata failed' exit $LASTEXITCODE } + try { + $metadata = $metadataJson | ConvertFrom-Json + } catch { + Write-Error "anvil-llvm-cov: could not parse cargo metadata output: $_" + exit 1 + } + $specsByName = @{} + foreach ($package in $metadata.packages) { + $specsByName[$package.name] = "$($package.name)@$($package.version)" + } $optedOut = @( - ($metadataJson | ConvertFrom-Json).packages | + $metadata.packages | Where-Object { $null -ne $_.metadata.'coverage-gate'.'min-lines-percent' -and [double]$_.metadata.'coverage-gate'.'min-lines-percent' -eq 0 } | ForEach-Object { $_.name } ) - if ($optedOut.Count -gt 0) { - $filtered = [System.Collections.Generic.List[string]]::new() - for ($i = 0; $i -lt $pkg.Count; $i++) { - if ($pkg[$i] -eq '--package' -and ($i + 1) -lt $pkg.Count -and $optedOut -contains $pkg[$i + 1]) { - $testOnlyPkg.Add('--package') - $testOnlyPkg.Add($pkg[$i + 1]) - $i++ - continue - } - $filtered.Add($pkg[$i]) + $filtered = [System.Collections.Generic.List[string]]::new() + $qualifiedAll = [System.Collections.Generic.List[string]]::new() + for ($i = 0; $i -lt $pkg.Count; $i++) { + if ($pkg[$i] -ne '--package' -or ($i + 1) -ge $pkg.Count) { + Write-Error "anvil-llvm-cov: unsupported affected-package arguments: $($pkg -join ' ')" + exit 1 + } + $name = $pkg[++$i] + if (-not $specsByName.ContainsKey($name)) { + Write-Error "anvil-llvm-cov: affected package '$name' is absent from workspace metadata" + exit 1 + } + $spec = $specsByName[$name] + $qualifiedAll.Add('--package') + $qualifiedAll.Add($spec) + if ($optedOut -contains $name) { + $testOnlyPkg.Add('--package') + $testOnlyPkg.Add($spec) + } else { + $filtered.Add('--package') + $filtered.Add($spec) } - $pkg = $filtered.ToArray() } + $pkg = $filtered.ToArray() + $allTestPkg = $qualifiedAll.ToArray() } $configs = @( @{ name = 'all-features'; flag = '--all-features' }, @@ -2410,13 +2432,14 @@ anvil-llvm-cov: anvil-llvm-cov-validate-prereqs anvil-impact # that were actually instrumented -- otherwise unmeasured members would # report NO DATA and fail the gate. When unscoped, $pkg is `--workspace` # (no gate flag); drop it so the gate defaults to every member. - # + # cargo-coverage-gate keys on bare workspace package names, so strip the + # version qualifier used to disambiguate the direct Cargo commands. $gateArgs = [System.Collections.Generic.List[string]]::new() for ($i = 0; $i -lt $pkg.Count; $i++) { if ($pkg[$i] -eq '--workspace') { continue } if ($pkg[$i] -eq '--package' -and ($i + 1) -lt $pkg.Count) { $gateArgs.Add('--package') - $gateArgs.Add($pkg[$i + 1]) + $gateArgs.Add(($pkg[$i + 1] -split '@', 2)[0]) $i++ continue } diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap index c58325759..2649f1c49 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap @@ -2439,24 +2439,46 @@ anvil-llvm-cov: anvil-llvm-cov-validate-prereqs anvil-impact Write-Error 'anvil-llvm-cov: cargo metadata failed' exit $LASTEXITCODE } + try { + $metadata = $metadataJson | ConvertFrom-Json + } catch { + Write-Error "anvil-llvm-cov: could not parse cargo metadata output: $_" + exit 1 + } + $specsByName = @{} + foreach ($package in $metadata.packages) { + $specsByName[$package.name] = "$($package.name)@$($package.version)" + } $optedOut = @( - ($metadataJson | ConvertFrom-Json).packages | + $metadata.packages | Where-Object { $null -ne $_.metadata.'coverage-gate'.'min-lines-percent' -and [double]$_.metadata.'coverage-gate'.'min-lines-percent' -eq 0 } | ForEach-Object { $_.name } ) - if ($optedOut.Count -gt 0) { - $filtered = [System.Collections.Generic.List[string]]::new() - for ($i = 0; $i -lt $pkg.Count; $i++) { - if ($pkg[$i] -eq '--package' -and ($i + 1) -lt $pkg.Count -and $optedOut -contains $pkg[$i + 1]) { - $testOnlyPkg.Add('--package') - $testOnlyPkg.Add($pkg[$i + 1]) - $i++ - continue - } - $filtered.Add($pkg[$i]) + $filtered = [System.Collections.Generic.List[string]]::new() + $qualifiedAll = [System.Collections.Generic.List[string]]::new() + for ($i = 0; $i -lt $pkg.Count; $i++) { + if ($pkg[$i] -ne '--package' -or ($i + 1) -ge $pkg.Count) { + Write-Error "anvil-llvm-cov: unsupported affected-package arguments: $($pkg -join ' ')" + exit 1 + } + $name = $pkg[++$i] + if (-not $specsByName.ContainsKey($name)) { + Write-Error "anvil-llvm-cov: affected package '$name' is absent from workspace metadata" + exit 1 + } + $spec = $specsByName[$name] + $qualifiedAll.Add('--package') + $qualifiedAll.Add($spec) + if ($optedOut -contains $name) { + $testOnlyPkg.Add('--package') + $testOnlyPkg.Add($spec) + } else { + $filtered.Add('--package') + $filtered.Add($spec) } - $pkg = $filtered.ToArray() } + $pkg = $filtered.ToArray() + $allTestPkg = $qualifiedAll.ToArray() } $configs = @( @{ name = 'all-features'; flag = '--all-features' }, @@ -2547,13 +2569,14 @@ anvil-llvm-cov: anvil-llvm-cov-validate-prereqs anvil-impact # that were actually instrumented -- otherwise unmeasured members would # report NO DATA and fail the gate. When unscoped, $pkg is `--workspace` # (no gate flag); drop it so the gate defaults to every member. - # + # cargo-coverage-gate keys on bare workspace package names, so strip the + # version qualifier used to disambiguate the direct Cargo commands. $gateArgs = [System.Collections.Generic.List[string]]::new() for ($i = 0; $i -lt $pkg.Count; $i++) { if ($pkg[$i] -eq '--workspace') { continue } if ($pkg[$i] -eq '--package' -and ($i + 1) -lt $pkg.Count) { $gateArgs.Add('--package') - $gateArgs.Add($pkg[$i + 1]) + $gateArgs.Add(($pkg[$i + 1] -split '@', 2)[0]) $i++ continue } diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap index b8533cfd3..bcd5faf1a 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap @@ -1172,24 +1172,46 @@ anvil-llvm-cov: anvil-llvm-cov-validate-prereqs anvil-impact Write-Error 'anvil-llvm-cov: cargo metadata failed' exit $LASTEXITCODE } + try { + $metadata = $metadataJson | ConvertFrom-Json + } catch { + Write-Error "anvil-llvm-cov: could not parse cargo metadata output: $_" + exit 1 + } + $specsByName = @{} + foreach ($package in $metadata.packages) { + $specsByName[$package.name] = "$($package.name)@$($package.version)" + } $optedOut = @( - ($metadataJson | ConvertFrom-Json).packages | + $metadata.packages | Where-Object { $null -ne $_.metadata.'coverage-gate'.'min-lines-percent' -and [double]$_.metadata.'coverage-gate'.'min-lines-percent' -eq 0 } | ForEach-Object { $_.name } ) - if ($optedOut.Count -gt 0) { - $filtered = [System.Collections.Generic.List[string]]::new() - for ($i = 0; $i -lt $pkg.Count; $i++) { - if ($pkg[$i] -eq '--package' -and ($i + 1) -lt $pkg.Count -and $optedOut -contains $pkg[$i + 1]) { - $testOnlyPkg.Add('--package') - $testOnlyPkg.Add($pkg[$i + 1]) - $i++ - continue - } - $filtered.Add($pkg[$i]) + $filtered = [System.Collections.Generic.List[string]]::new() + $qualifiedAll = [System.Collections.Generic.List[string]]::new() + for ($i = 0; $i -lt $pkg.Count; $i++) { + if ($pkg[$i] -ne '--package' -or ($i + 1) -ge $pkg.Count) { + Write-Error "anvil-llvm-cov: unsupported affected-package arguments: $($pkg -join ' ')" + exit 1 + } + $name = $pkg[++$i] + if (-not $specsByName.ContainsKey($name)) { + Write-Error "anvil-llvm-cov: affected package '$name' is absent from workspace metadata" + exit 1 + } + $spec = $specsByName[$name] + $qualifiedAll.Add('--package') + $qualifiedAll.Add($spec) + if ($optedOut -contains $name) { + $testOnlyPkg.Add('--package') + $testOnlyPkg.Add($spec) + } else { + $filtered.Add('--package') + $filtered.Add($spec) } - $pkg = $filtered.ToArray() } + $pkg = $filtered.ToArray() + $allTestPkg = $qualifiedAll.ToArray() } $configs = @( @{ name = 'all-features'; flag = '--all-features' }, @@ -1280,13 +1302,14 @@ anvil-llvm-cov: anvil-llvm-cov-validate-prereqs anvil-impact # that were actually instrumented -- otherwise unmeasured members would # report NO DATA and fail the gate. When unscoped, $pkg is `--workspace` # (no gate flag); drop it so the gate defaults to every member. - # + # cargo-coverage-gate keys on bare workspace package names, so strip the + # version qualifier used to disambiguate the direct Cargo commands. $gateArgs = [System.Collections.Generic.List[string]]::new() for ($i = 0; $i -lt $pkg.Count; $i++) { if ($pkg[$i] -eq '--workspace') { continue } if ($pkg[$i] -eq '--package' -and ($i + 1) -lt $pkg.Count) { $gateArgs.Add('--package') - $gateArgs.Add($pkg[$i + 1]) + $gateArgs.Add(($pkg[$i + 1] -split '@', 2)[0]) $i++ continue } diff --git a/justfiles/anvil/checks/llvm-cov.just b/justfiles/anvil/checks/llvm-cov.just index 946a86a69..8e83e8cd5 100644 --- a/justfiles/anvil/checks/llvm-cov.just +++ b/justfiles/anvil/checks/llvm-cov.just @@ -43,24 +43,46 @@ anvil-llvm-cov: anvil-llvm-cov-validate-prereqs anvil-impact Write-Error 'anvil-llvm-cov: cargo metadata failed' exit $LASTEXITCODE } + try { + $metadata = $metadataJson | ConvertFrom-Json + } catch { + Write-Error "anvil-llvm-cov: could not parse cargo metadata output: $_" + exit 1 + } + $specsByName = @{} + foreach ($package in $metadata.packages) { + $specsByName[$package.name] = "$($package.name)@$($package.version)" + } $optedOut = @( - ($metadataJson | ConvertFrom-Json).packages | + $metadata.packages | Where-Object { $null -ne $_.metadata.'coverage-gate'.'min-lines-percent' -and [double]$_.metadata.'coverage-gate'.'min-lines-percent' -eq 0 } | ForEach-Object { $_.name } ) - if ($optedOut.Count -gt 0) { - $filtered = [System.Collections.Generic.List[string]]::new() - for ($i = 0; $i -lt $pkg.Count; $i++) { - if ($pkg[$i] -eq '--package' -and ($i + 1) -lt $pkg.Count -and $optedOut -contains $pkg[$i + 1]) { - $testOnlyPkg.Add('--package') - $testOnlyPkg.Add($pkg[$i + 1]) - $i++ - continue - } - $filtered.Add($pkg[$i]) + $filtered = [System.Collections.Generic.List[string]]::new() + $qualifiedAll = [System.Collections.Generic.List[string]]::new() + for ($i = 0; $i -lt $pkg.Count; $i++) { + if ($pkg[$i] -ne '--package' -or ($i + 1) -ge $pkg.Count) { + Write-Error "anvil-llvm-cov: unsupported affected-package arguments: $($pkg -join ' ')" + exit 1 + } + $name = $pkg[++$i] + if (-not $specsByName.ContainsKey($name)) { + Write-Error "anvil-llvm-cov: affected package '$name' is absent from workspace metadata" + exit 1 + } + $spec = $specsByName[$name] + $qualifiedAll.Add('--package') + $qualifiedAll.Add($spec) + if ($optedOut -contains $name) { + $testOnlyPkg.Add('--package') + $testOnlyPkg.Add($spec) + } else { + $filtered.Add('--package') + $filtered.Add($spec) } - $pkg = $filtered.ToArray() } + $pkg = $filtered.ToArray() + $allTestPkg = $qualifiedAll.ToArray() } $configs = @( @{ name = 'all-features'; flag = '--all-features' }, @@ -151,13 +173,14 @@ anvil-llvm-cov: anvil-llvm-cov-validate-prereqs anvil-impact # that were actually instrumented -- otherwise unmeasured members would # report NO DATA and fail the gate. When unscoped, $pkg is `--workspace` # (no gate flag); drop it so the gate defaults to every member. - # + # cargo-coverage-gate keys on bare workspace package names, so strip the + # version qualifier used to disambiguate the direct Cargo commands. $gateArgs = [System.Collections.Generic.List[string]]::new() for ($i = 0; $i -lt $pkg.Count; $i++) { if ($pkg[$i] -eq '--workspace') { continue } if ($pkg[$i] -eq '--package' -and ($i + 1) -lt $pkg.Count) { $gateArgs.Add('--package') - $gateArgs.Add($pkg[$i + 1]) + $gateArgs.Add(($pkg[$i + 1] -split '@', 2)[0]) $i++ continue } From 2725b7ddc4496492803320bec04822af5cc75f8b Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Thu, 3 Sep 2026 12:57:47 +0200 Subject: [PATCH 07/15] test(anvil): account for ARM coverage fallback Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/cargo-anvil/tests/recipe_contracts.rs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/crates/cargo-anvil/tests/recipe_contracts.rs b/crates/cargo-anvil/tests/recipe_contracts.rs index daeba19cc..466d9eb18 100644 --- a/crates/cargo-anvil/tests/recipe_contracts.rs +++ b/crates/cargo-anvil/tests/recipe_contracts.rs @@ -1174,9 +1174,21 @@ fn llvm_cov_qualifies_direct_cargo_selectors_but_not_gate_selectors() { ); let calls = fs::read_to_string(log).unwrap(); assert!( - calls.contains("llvm-cov nextest --package fixture@0.1.0"), + calls.contains("--package fixture@0.1.0"), "direct Cargo selectors must be version-qualified:\n{calls}" ); + if cfg!(all(windows, target_arch = "aarch64")) { + assert!( + calls.contains("nextest run"), + "Windows ARM must use the plain-nextest fallback:\n{calls}" + ); + assert!( + !calls.contains("coverage-gate"), + "Windows ARM does not measure or gate coverage:\n{calls}" + ); + return; + } + assert!(calls.contains("llvm-cov nextest"), "measured platforms must run llvm-cov:\n{calls}"); assert!( calls.contains("coverage-gate --package fixture --lcov"), "coverage-gate selectors must remain bare workspace names:\n{calls}" From 91bda8eb5d5ff6314ffe262cd9816e216e13646b Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Thu, 3 Sep 2026 13:16:53 +0200 Subject: [PATCH 08/15] docs(gamma): gate internal doctest examples Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/cargo-gamma-lib/src/cfg/features.rs | 3 +++ crates/cargo-gamma-lib/src/cfg/mod.rs | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/crates/cargo-gamma-lib/src/cfg/features.rs b/crates/cargo-gamma-lib/src/cfg/features.rs index b8ceec371..b9fc44c0e 100644 --- a/crates/cargo-gamma-lib/src/cfg/features.rs +++ b/crates/cargo-gamma-lib/src/cfg/features.rs @@ -41,8 +41,11 @@ use crate::{HashMap, HashSet}; /// interest, and guessing at them would cost a full resolve. /// /// ```rust,no_run +/// # #[cfg(feature = "internals")] /// # use cargo_gamma_lib::internals::cfg::features::enabled; +/// # #[cfg(feature = "internals")] /// # use cargo_gamma_lib::internals::commands::FeatureArgs; +/// # #[cfg(feature = "internals")] /// # fn example(metadata: &cargo_metadata::Metadata) { /// let features = enabled(metadata, &FeatureArgs::default()); /// diff --git a/crates/cargo-gamma-lib/src/cfg/mod.rs b/crates/cargo-gamma-lib/src/cfg/mod.rs index 324173c8b..36879b0f5 100644 --- a/crates/cargo-gamma-lib/src/cfg/mod.rs +++ b/crates/cargo-gamma-lib/src/cfg/mod.rs @@ -19,6 +19,8 @@ //! `cfg_attr` attributes, and answers whether the resulting `#[cfg(...)]` attributes hold: //! //! ```rust +//! # #[cfg(feature = "internals")] +//! # fn example() { //! # use cargo_gamma_lib::internals::cfg::CfgSet; //! let set = CfgSet::parse("unix\ntarget_arch=\"x86_64\"\n").with_features(["std".to_owned()]); //! @@ -29,6 +31,7 @@ //! assert!(set.holds_str("any(unix, windows)")); //! assert!(set.holds_str("not(windows)")); //! assert!(!set.holds_str("all(unix, feature = \"stats\")")); +//! # } //! ``` //! //! The names and values come from `rustc --print cfg`, asked about the build cargo will actually @@ -53,6 +56,8 @@ //! the code mutable: //! //! ```rust +//! # #[cfg(feature = "internals")] +//! # fn example() { //! # use cargo_gamma_lib::internals::cfg::CfgSet; //! let set = CfgSet::parse("unix\n"); //! @@ -61,6 +66,7 @@ //! //! // And a set that was never resolved holds everything. //! assert!(CfgSet::unconditional().holds_str("windows")); +//! # } //! ``` mod build; From 1afd163d12da3cb28356548e4730a97f520ed45c Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Thu, 3 Sep 2026 13:25:30 +0200 Subject: [PATCH 09/15] test(anvil): handle qualified cargo probes on Unix Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/cargo-anvil/tests/impact.rs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/crates/cargo-anvil/tests/impact.rs b/crates/cargo-anvil/tests/impact.rs index 85acd09b8..441292eb0 100644 --- a/crates/cargo-anvil/tests/impact.rs +++ b/crates/cargo-anvil/tests/impact.rs @@ -928,7 +928,7 @@ fn fake_cargo(dir: &Path, log: &Path) { #[cfg(unix)] { let script = format!( - "#!/bin/sh\nif [ \"$1 $2\" = \"install --list\" ]; then printf '%s\\n' 'cargo-each v0.1.0:'; exit 0; fi\nprintf '%s\\n' \"$*\" >> '{}'\nexit 0\n", + "#!/bin/sh\nall_args=\"$*\"\ncase \"$1\" in +*) shift ;; esac\nif [ \"$1 $2\" = \"install --list\" ]; then printf '%s\\n' 'cargo-each v0.1.0:'; exit 0; fi\nprintf '%s\\n' \"$all_args\" >> '{}'\nexit 0\n", log.display() ); let path = dir.join("cargo"); @@ -937,6 +937,21 @@ fn fake_cargo(dir: &Path, log: &Path) { } } +#[cfg(unix)] +#[test] +fn fake_cargo_recognizes_toolchain_qualified_install_list() { + let tmp = TempDir::new().unwrap(); + let log = tmp.path().join("cargo.log"); + fake_cargo(tmp.path(), &log); + let output = Command::new(tmp.path().join("cargo")) + .args(["+1.97", "install", "--list"]) + .output() + .unwrap(); + assert!(output.status.success()); + assert!(String::from_utf8_lossy(&output.stdout).contains("cargo-each v0.1.0:")); + assert!(!log.exists(), "tool discovery probes should not be logged as check invocations"); +} + /// The process `PATH` with `dir` prepended, so an executable in `dir` shadows /// the same-named tool elsewhere on PATH. fn path_with_prefix(dir: &Path) -> OsString { From ace1e876c67d85692a5b7bff86f1ad160ae2cf74 Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Thu, 3 Sep 2026 13:40:10 +0200 Subject: [PATCH 10/15] fix(anvil): validate consumed impact selectors Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .anvil.lock | 4 +- crates/cargo-anvil/docs/design/local.md | 6 +++ .../templates/justfiles/anvil/impact.just | 37 +++++++++++++++++-- crates/cargo-anvil/tests/impact.rs | 26 +++++++++++++ .../snapshots/snapshots__ado_backend.snap | 37 +++++++++++++++++-- .../snapshots/snapshots__github_backend.snap | 37 +++++++++++++++++-- .../snapshots/snapshots__local_only.snap | 37 +++++++++++++++++-- crates/cargo-gamma-lib/src/cfg/mod.rs | 4 ++ justfiles/anvil/impact.just | 37 +++++++++++++++++-- 9 files changed, 208 insertions(+), 17 deletions(-) diff --git a/.anvil.lock b/.anvil.lock index 9c8fd6d8b..c2bc6fb63 100644 --- a/.anvil.lock +++ b/.anvil.lock @@ -1,7 +1,7 @@ version = 1 tool = "anvil" tool_version = "0.6.0" -catalog_checksum = "sha256:088371593c360ee572074a23405d279bee24f13d725dcb015ca8f45fd014fe62" +catalog_checksum = "sha256:0eb5564699b740da80fa7aaee3a3aec01dbb5ca17630cebb0d3800fae3200ba5" [[file]] path = ".anvil/container/Dockerfile.dockerignore" @@ -221,7 +221,7 @@ checksum = "sha256:c1859557980f733777253e9380d90ef710512fde1748f27795cd498501998 [[file]] path = "justfiles/anvil/impact.just" -checksum = "sha256:9d1f9df6948e1e7cdfcc96f7a04f2fd86bf7af3d152113b46ce5abcadfbe41ce" +checksum = "sha256:8b7d20f2e5fcd85cc9e8edf3cda108976fba4c24563b0997c1029bbaa9e40ecf" [[file]] path = "justfiles/anvil/mod.just" diff --git a/crates/cargo-anvil/docs/design/local.md b/crates/cargo-anvil/docs/design/local.md index 8d6bf469e..59853537f 100644 --- a/crates/cargo-anvil/docs/design/local.md +++ b/crates/cargo-anvil/docs/design/local.md @@ -563,6 +563,12 @@ without string parsing. `cargo-each` resolves the names against live workspace m when `{packages}` is used, it forwards version-qualified specs so child Cargo commands remain unambiguous. +The helper validates this grammar before returning any cached selector. Empty files, +mixed single-value and package forms, incomplete pairs, whitespace-bearing names, and +unknown flag positions fail closed. `ANVIL_IMPACT=consume` validates all three downloaded +selector files before accepting the artifact, so a malformed cache cannot fall through +to cargo-each's default-member selection. + `_anvil-impact-format` **fails the recipe** (non-zero exit, aborting `anvil-impact`) when at least one name cargo-delta reported cannot be resolved to exactly one workspace package. The helper resolves each reported name against three namespaces: the exact cargo diff --git a/crates/cargo-anvil/templates/justfiles/anvil/impact.just b/crates/cargo-anvil/templates/justfiles/anvil/impact.just index dda73d637..dbfce846e 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/impact.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/impact.just @@ -343,8 +343,9 @@ anvil-impact: _anvil-impact-snapshot } if ($env:ANVIL_IMPACT -eq 'off') { exit 0 } if ($env:ANVIL_IMPACT -eq 'consume') { - # consume = trust an already-downloaded cache verbatim, so assert the - # cache is actually present. If the CI download ever stops landing at + # consume = trust an already-downloaded cache, so assert the cache is + # present and each selector file follows the token grammar. If the CI + # download ever stops landing at # target/anvil/impact/ (renamed artifact, a missing download step, or # an adopter invoking the group action directly), every scoped check # would otherwise silently widen to --workspace and the pipeline would @@ -356,6 +357,10 @@ anvil-impact: _anvil-impact-snapshot Write-Error "anvil-impact: ANVIL_IMPACT=consume but the downloaded impact cache is missing ($($missing -join ', ') include file(s) absent under target/anvil/impact/). The impact artifact must be downloaded there before scoped checks run." exit 1 } + foreach ($tier in @('modified', 'affected', 'required')) { + & "{{ just_executable() }}" _anvil-impact-include $tier | Out-Null + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + } exit 0 } @@ -622,7 +627,33 @@ _anvil-impact-include tier: if ($env:ANVIL_IMPACT -eq 'off') { Write-Output $default; exit 0 } $file = "target/anvil/impact/include_$tier.txt" if (Test-Path $file) { - Get-Content -LiteralPath $file | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } + $tokens = @(Get-Content -LiteralPath $file) + $valid = $false + if ($tokens.Count -eq 1 -and $tokens[0] -in @('--workspace', '--none')) { + $valid = $true + } elseif ($tokens.Count -gt 0 -and ($tokens.Count % 2) -eq 0) { + $valid = $true + for ($i = 0; $i -lt $tokens.Count; $i += 2) { + $name = $tokens[$i + 1] + if ($tokens[$i] -ne '--package' -or + [string]::IsNullOrWhiteSpace($name) -or + $name -ne $name.Trim() -or + $name -match '\s' -or + $name.StartsWith('--')) { + $valid = $false + break + } + } + } + if (-not $valid) { + Write-Error "anvil-impact: selector file '$file' is empty or malformed. Expected exactly --workspace, exactly --none, or one token per line as repeated --package/name pairs. Recompute or re-download the impact cache." + exit 1 + } + Write-Output $tokens } else { + if ($env:ANVIL_IMPACT -eq 'consume') { + Write-Error "anvil-impact: ANVIL_IMPACT=consume but selector file '$file' is missing" + exit 1 + } Write-Output $default } \ No newline at end of file diff --git a/crates/cargo-anvil/tests/impact.rs b/crates/cargo-anvil/tests/impact.rs index 441292eb0..912779be4 100644 --- a/crates/cargo-anvil/tests/impact.rs +++ b/crates/cargo-anvil/tests/impact.rs @@ -1328,6 +1328,32 @@ fn consume_without_downloaded_cache_fails_loudly() { "consume with a present cache must succeed as a no-op:\n{ok_combined}" ); + // Presence is not enough: an empty or malformed selector would otherwise + // become an empty argv and make cargo-each fall back to default-members. + write(&cache.join("include_affected.txt"), ""); + let empty = consume(&["anvil-impact"]); + let empty_combined = format!( + "{}{}", + String::from_utf8_lossy(&empty.stdout), + String::from_utf8_lossy(&empty.stderr) + ); + assert_ne!(empty.status.code(), Some(0), "empty selector cache must fail:\n{empty_combined}"); + assert!(empty_combined.contains("empty or malformed") && empty_combined.contains("affected")); + + write(&cache.join("include_affected.txt"), "--package\nalpha\n--workspace"); + let malformed = consume(&["anvil-impact"]); + let malformed_combined = format!( + "{}{}", + String::from_utf8_lossy(&malformed.stdout), + String::from_utf8_lossy(&malformed.stderr) + ); + assert_ne!( + malformed.status.code(), + Some(0), + "malformed selector cache must fail:\n{malformed_combined}" + ); + assert!(malformed_combined.contains("empty or malformed") && malformed_combined.contains("affected")); + // A partially downloaded cache -- one tier's include file missing -- must // also fail loudly and name the missing tier. This guards the // all-three-files contract against regressing to a directory or diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap index 8e965f5b9..066e21f23 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap @@ -5529,8 +5529,9 @@ anvil-impact: _anvil-impact-snapshot } if ($env:ANVIL_IMPACT -eq 'off') { exit 0 } if ($env:ANVIL_IMPACT -eq 'consume') { - # consume = trust an already-downloaded cache verbatim, so assert the - # cache is actually present. If the CI download ever stops landing at + # consume = trust an already-downloaded cache, so assert the cache is + # present and each selector file follows the token grammar. If the CI + # download ever stops landing at # target/anvil/impact/ (renamed artifact, a missing download step, or # an adopter invoking the group action directly), every scoped check # would otherwise silently widen to --workspace and the pipeline would @@ -5542,6 +5543,10 @@ anvil-impact: _anvil-impact-snapshot Write-Error "anvil-impact: ANVIL_IMPACT=consume but the downloaded impact cache is missing ($($missing -join ', ') include file(s) absent under target/anvil/impact/). The impact artifact must be downloaded there before scoped checks run." exit 1 } + foreach ($tier in @('modified', 'affected', 'required')) { + & "{{ just_executable() }}" _anvil-impact-include $tier | Out-Null + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + } exit 0 } @@ -5808,8 +5813,34 @@ _anvil-impact-include tier: if ($env:ANVIL_IMPACT -eq 'off') { Write-Output $default; exit 0 } $file = "target/anvil/impact/include_$tier.txt" if (Test-Path $file) { - Get-Content -LiteralPath $file | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } + $tokens = @(Get-Content -LiteralPath $file) + $valid = $false + if ($tokens.Count -eq 1 -and $tokens[0] -in @('--workspace', '--none')) { + $valid = $true + } elseif ($tokens.Count -gt 0 -and ($tokens.Count % 2) -eq 0) { + $valid = $true + for ($i = 0; $i -lt $tokens.Count; $i += 2) { + $name = $tokens[$i + 1] + if ($tokens[$i] -ne '--package' -or + [string]::IsNullOrWhiteSpace($name) -or + $name -ne $name.Trim() -or + $name -match '\s' -or + $name.StartsWith('--')) { + $valid = $false + break + } + } + } + if (-not $valid) { + Write-Error "anvil-impact: selector file '$file' is empty or malformed. Expected exactly --workspace, exactly --none, or one token per line as repeated --package/name pairs. Recompute or re-download the impact cache." + exit 1 + } + Write-Output $tokens } else { + if ($env:ANVIL_IMPACT -eq 'consume') { + Write-Error "anvil-impact: ANVIL_IMPACT=consume but selector file '$file' is missing" + exit 1 + } Write-Output $default } diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap index 2649f1c49..161baed52 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap @@ -5666,8 +5666,9 @@ anvil-impact: _anvil-impact-snapshot } if ($env:ANVIL_IMPACT -eq 'off') { exit 0 } if ($env:ANVIL_IMPACT -eq 'consume') { - # consume = trust an already-downloaded cache verbatim, so assert the - # cache is actually present. If the CI download ever stops landing at + # consume = trust an already-downloaded cache, so assert the cache is + # present and each selector file follows the token grammar. If the CI + # download ever stops landing at # target/anvil/impact/ (renamed artifact, a missing download step, or # an adopter invoking the group action directly), every scoped check # would otherwise silently widen to --workspace and the pipeline would @@ -5679,6 +5680,10 @@ anvil-impact: _anvil-impact-snapshot Write-Error "anvil-impact: ANVIL_IMPACT=consume but the downloaded impact cache is missing ($($missing -join ', ') include file(s) absent under target/anvil/impact/). The impact artifact must be downloaded there before scoped checks run." exit 1 } + foreach ($tier in @('modified', 'affected', 'required')) { + & "{{ just_executable() }}" _anvil-impact-include $tier | Out-Null + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + } exit 0 } @@ -5945,8 +5950,34 @@ _anvil-impact-include tier: if ($env:ANVIL_IMPACT -eq 'off') { Write-Output $default; exit 0 } $file = "target/anvil/impact/include_$tier.txt" if (Test-Path $file) { - Get-Content -LiteralPath $file | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } + $tokens = @(Get-Content -LiteralPath $file) + $valid = $false + if ($tokens.Count -eq 1 -and $tokens[0] -in @('--workspace', '--none')) { + $valid = $true + } elseif ($tokens.Count -gt 0 -and ($tokens.Count % 2) -eq 0) { + $valid = $true + for ($i = 0; $i -lt $tokens.Count; $i += 2) { + $name = $tokens[$i + 1] + if ($tokens[$i] -ne '--package' -or + [string]::IsNullOrWhiteSpace($name) -or + $name -ne $name.Trim() -or + $name -match '\s' -or + $name.StartsWith('--')) { + $valid = $false + break + } + } + } + if (-not $valid) { + Write-Error "anvil-impact: selector file '$file' is empty or malformed. Expected exactly --workspace, exactly --none, or one token per line as repeated --package/name pairs. Recompute or re-download the impact cache." + exit 1 + } + Write-Output $tokens } else { + if ($env:ANVIL_IMPACT -eq 'consume') { + Write-Error "anvil-impact: ANVIL_IMPACT=consume but selector file '$file' is missing" + exit 1 + } Write-Output $default } diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap index bcd5faf1a..bdcdf0e97 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap @@ -4399,8 +4399,9 @@ anvil-impact: _anvil-impact-snapshot } if ($env:ANVIL_IMPACT -eq 'off') { exit 0 } if ($env:ANVIL_IMPACT -eq 'consume') { - # consume = trust an already-downloaded cache verbatim, so assert the - # cache is actually present. If the CI download ever stops landing at + # consume = trust an already-downloaded cache, so assert the cache is + # present and each selector file follows the token grammar. If the CI + # download ever stops landing at # target/anvil/impact/ (renamed artifact, a missing download step, or # an adopter invoking the group action directly), every scoped check # would otherwise silently widen to --workspace and the pipeline would @@ -4412,6 +4413,10 @@ anvil-impact: _anvil-impact-snapshot Write-Error "anvil-impact: ANVIL_IMPACT=consume but the downloaded impact cache is missing ($($missing -join ', ') include file(s) absent under target/anvil/impact/). The impact artifact must be downloaded there before scoped checks run." exit 1 } + foreach ($tier in @('modified', 'affected', 'required')) { + & "{{ just_executable() }}" _anvil-impact-include $tier | Out-Null + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + } exit 0 } @@ -4678,8 +4683,34 @@ _anvil-impact-include tier: if ($env:ANVIL_IMPACT -eq 'off') { Write-Output $default; exit 0 } $file = "target/anvil/impact/include_$tier.txt" if (Test-Path $file) { - Get-Content -LiteralPath $file | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } + $tokens = @(Get-Content -LiteralPath $file) + $valid = $false + if ($tokens.Count -eq 1 -and $tokens[0] -in @('--workspace', '--none')) { + $valid = $true + } elseif ($tokens.Count -gt 0 -and ($tokens.Count % 2) -eq 0) { + $valid = $true + for ($i = 0; $i -lt $tokens.Count; $i += 2) { + $name = $tokens[$i + 1] + if ($tokens[$i] -ne '--package' -or + [string]::IsNullOrWhiteSpace($name) -or + $name -ne $name.Trim() -or + $name -match '\s' -or + $name.StartsWith('--')) { + $valid = $false + break + } + } + } + if (-not $valid) { + Write-Error "anvil-impact: selector file '$file' is empty or malformed. Expected exactly --workspace, exactly --none, or one token per line as repeated --package/name pairs. Recompute or re-download the impact cache." + exit 1 + } + Write-Output $tokens } else { + if ($env:ANVIL_IMPACT -eq 'consume') { + Write-Error "anvil-impact: ANVIL_IMPACT=consume but selector file '$file' is missing" + exit 1 + } Write-Output $default } diff --git a/crates/cargo-gamma-lib/src/cfg/mod.rs b/crates/cargo-gamma-lib/src/cfg/mod.rs index 36879b0f5..585003823 100644 --- a/crates/cargo-gamma-lib/src/cfg/mod.rs +++ b/crates/cargo-gamma-lib/src/cfg/mod.rs @@ -32,6 +32,8 @@ //! assert!(set.holds_str("not(windows)")); //! assert!(!set.holds_str("all(unix, feature = \"stats\")")); //! # } +//! # #[cfg(feature = "internals")] +//! # example(); //! ``` //! //! The names and values come from `rustc --print cfg`, asked about the build cargo will actually @@ -67,6 +69,8 @@ //! // And a set that was never resolved holds everything. //! assert!(CfgSet::unconditional().holds_str("windows")); //! # } +//! # #[cfg(feature = "internals")] +//! # example(); //! ``` mod build; diff --git a/justfiles/anvil/impact.just b/justfiles/anvil/impact.just index dda73d637..dbfce846e 100644 --- a/justfiles/anvil/impact.just +++ b/justfiles/anvil/impact.just @@ -343,8 +343,9 @@ anvil-impact: _anvil-impact-snapshot } if ($env:ANVIL_IMPACT -eq 'off') { exit 0 } if ($env:ANVIL_IMPACT -eq 'consume') { - # consume = trust an already-downloaded cache verbatim, so assert the - # cache is actually present. If the CI download ever stops landing at + # consume = trust an already-downloaded cache, so assert the cache is + # present and each selector file follows the token grammar. If the CI + # download ever stops landing at # target/anvil/impact/ (renamed artifact, a missing download step, or # an adopter invoking the group action directly), every scoped check # would otherwise silently widen to --workspace and the pipeline would @@ -356,6 +357,10 @@ anvil-impact: _anvil-impact-snapshot Write-Error "anvil-impact: ANVIL_IMPACT=consume but the downloaded impact cache is missing ($($missing -join ', ') include file(s) absent under target/anvil/impact/). The impact artifact must be downloaded there before scoped checks run." exit 1 } + foreach ($tier in @('modified', 'affected', 'required')) { + & "{{ just_executable() }}" _anvil-impact-include $tier | Out-Null + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + } exit 0 } @@ -622,7 +627,33 @@ _anvil-impact-include tier: if ($env:ANVIL_IMPACT -eq 'off') { Write-Output $default; exit 0 } $file = "target/anvil/impact/include_$tier.txt" if (Test-Path $file) { - Get-Content -LiteralPath $file | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } + $tokens = @(Get-Content -LiteralPath $file) + $valid = $false + if ($tokens.Count -eq 1 -and $tokens[0] -in @('--workspace', '--none')) { + $valid = $true + } elseif ($tokens.Count -gt 0 -and ($tokens.Count % 2) -eq 0) { + $valid = $true + for ($i = 0; $i -lt $tokens.Count; $i += 2) { + $name = $tokens[$i + 1] + if ($tokens[$i] -ne '--package' -or + [string]::IsNullOrWhiteSpace($name) -or + $name -ne $name.Trim() -or + $name -match '\s' -or + $name.StartsWith('--')) { + $valid = $false + break + } + } + } + if (-not $valid) { + Write-Error "anvil-impact: selector file '$file' is empty or malformed. Expected exactly --workspace, exactly --none, or one token per line as repeated --package/name pairs. Recompute or re-download the impact cache." + exit 1 + } + Write-Output $tokens } else { + if ($env:ANVIL_IMPACT -eq 'consume') { + Write-Error "anvil-impact: ANVIL_IMPACT=consume but selector file '$file' is missing" + exit 1 + } Write-Output $default } \ No newline at end of file From 27b81e8c5aea86486b9a7ae0e746341ed7c09732 Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Thu, 3 Sep 2026 15:28:58 +0200 Subject: [PATCH 11/15] fix(anvil): select doctests by target capability Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .anvil.lock | 6 +++--- crates/cargo-anvil/docs/design/checks.md | 2 +- .../justfiles/anvil/checks/doc-test.just | 9 +++++---- .../templates/justfiles/anvil/impact.just | 6 ++++-- crates/cargo-anvil/tests/recipe_contracts.rs | 13 ++++++++++--- .../tests/snapshots/snapshots__ado_backend.snap | 15 +++++++++------ .../snapshots/snapshots__github_backend.snap | 15 +++++++++------ .../tests/snapshots/snapshots__local_only.snap | 15 +++++++++------ justfiles/anvil/checks/doc-test.just | 9 +++++---- justfiles/anvil/impact.just | 6 ++++-- 10 files changed, 59 insertions(+), 37 deletions(-) diff --git a/.anvil.lock b/.anvil.lock index 308e33b6c..605b0d008 100644 --- a/.anvil.lock +++ b/.anvil.lock @@ -1,7 +1,7 @@ version = 1 tool = "anvil" tool_version = "0.6.0" -catalog_checksum = "sha256:2eb0b749d88bf7d322497c498a62c5c99e9cc6c0a30c1bececa8efbd772769fd" +catalog_checksum = "sha256:aecead1dba65f691c184186bb82773b50336b1f29ca1b287c28b59bc14db6419" [[file]] path = ".anvil/container/Dockerfile.dockerignore" @@ -89,7 +89,7 @@ checksum = "sha256:d2f6a9536d62030db5505c88d510c4114cb0aab81a7af0e995ba8da5ce3a1 [[file]] path = "justfiles/anvil/checks/doc-test.just" -checksum = "sha256:983f97606d211a0718aaa2a49ac39b1b38d7f66f50632ec5acd49524c18152c8" +checksum = "sha256:094506c8f0d7a0ba7631d640ba2d22fbd53ad5efd273477b4a268296d1007c36" [[file]] path = "justfiles/anvil/checks/ensure-no-cyclic-deps.just" @@ -221,7 +221,7 @@ checksum = "sha256:c1859557980f733777253e9380d90ef710512fde1748f27795cd498501998 [[file]] path = "justfiles/anvil/impact.just" -checksum = "sha256:8b7d20f2e5fcd85cc9e8edf3cda108976fba4c24563b0997c1029bbaa9e40ecf" +checksum = "sha256:5e8b96d77ccc17c7184a4be0f3c1a1fbf2a525b43d2dd49cf48a011a4a867df7" [[file]] path = "justfiles/anvil/mod.just" diff --git a/crates/cargo-anvil/docs/design/checks.md b/crates/cargo-anvil/docs/design/checks.md index d13da954f..dc4a654f0 100644 --- a/crates/cargo-anvil/docs/design/checks.md +++ b/crates/cargo-anvil/docs/design/checks.md @@ -206,7 +206,7 @@ matrix overhead. | Check | Invocation | Source | |--------------|-----------------------------------------------------------------------------|--------| | `llvm-cov` | Runs tests for every affected package under both feature configurations. Packages with a positive coverage threshold run through self-contained `cargo + llvm-cov nextest --no-report` invocations and produce per-config lcov/cobertura reports. Packages declaring `min-lines-percent = 0` still run through plain `cargo nextest`; the opt-out disables measurement and gating, never tests. Per-config reports avoid Windows command-line overflow and are reconciled downstream by cargo-coverage-gate, Codecov, and ADO. Codecov is display-only; the local coverage gate is authoritative. | oxidizer, oxidizer-github; gate via [`cargo-coverage-gate`](../../../cargo-coverage-gate) | -| `doc-test` | Two cargo-test runs over affected library and proc-macro packages: `cargo test --doc --all-features --locked` and `cargo test --doc --locked` (default features). Bin-only packages are removed because Cargo errors when they are the only selected packages and they have no rustdoc tests. Running both feature modes catches doctests that only compile under one configuration. nextest does not run doctests, so this stays separate. | oxidizer, oxidizer-github | +| `doc-test` | Two cargo-test runs over affected packages with at least one Cargo target marked `doctest = true`: `cargo test --doc --all-features --locked` and `cargo test --doc --locked` (default features). This metadata capability includes explicit library crate types such as `rlib` and proc macros while excluding bin-only packages, which make Cargo error when they are the only selection. Running both feature modes catches doctests that only compile under one configuration. nextest does not run doctests, so this stays separate. | 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 | #### `pr-msrv` (minimum-version tests) 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 60de891d9..f5f945647 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/doc-test.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/doc-test.just @@ -14,8 +14,9 @@ # of total compile/test time. # # Cargo errors when `cargo test --doc` selects only bin-only packages. Build -# the documentation-capable subset (plain libraries and proc macros) from -# metadata before delegating the two invocations to cargo-each. +# the subset with at least one Cargo target marked `doctest = true` before +# delegating the two invocations to cargo-each. This includes explicit library +# crate types such as rlib/cdylib as well as plain libraries and proc macros. # Run documentation tests for affected workspace packages. [script("pwsh", "-NoProfile")] @@ -34,7 +35,7 @@ anvil-doc-test: anvil-doc-test-validate-prereqs anvil-impact } $docPackages = @{} foreach ($package in $meta.packages) { - if ($package.targets | Where-Object { ($_.kind -contains 'lib') -or ($_.kind -contains 'proc-macro') }) { + if ($package.targets | Where-Object { $_.doctest -eq $true }) { $docPackages[$package.name] = $true } } @@ -51,7 +52,7 @@ anvil-doc-test: anvil-doc-test-validate-prereqs anvil-impact ) } if ($names.Count -eq 0) { - Write-Host 'anvil-doc-test: no affected library or proc-macro packages; skipping' + Write-Host 'anvil-doc-test: no affected doctest-capable packages; skipping' exit 0 } $docSelection = [System.Collections.Generic.List[string]]::new() diff --git a/crates/cargo-anvil/templates/justfiles/anvil/impact.just b/crates/cargo-anvil/templates/justfiles/anvil/impact.just index dbfce846e..2ffe8fb04 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/impact.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/impact.just @@ -605,8 +605,10 @@ _anvil-impact-format tier impactJson: # Private. Resolves an impact category's scope for a check recipe and echoes it on stdout. # 1. ANVIL_IMPACT=off -> tier default (full-workspace / run). -# 2. include_.txt present -> its selector tokens. -# 3. otherwise -> tier default. +# 2. include_.txt present -> validated selector tokens; malformed +# content fails closed. +# 3. file missing -> tier default, except consume mode fails +# because its downloaded cache is incomplete. # Tier default: --workspace for every tier. # Each scoped check calls this directly (into a local $selection array) to # read its category's scope from the target/anvil/impact cache -- the same cache diff --git a/crates/cargo-anvil/tests/recipe_contracts.rs b/crates/cargo-anvil/tests/recipe_contracts.rs index 466d9eb18..6bb7c7e91 100644 --- a/crates/cargo-anvil/tests/recipe_contracts.rs +++ b/crates/cargo-anvil/tests/recipe_contracts.rs @@ -107,7 +107,11 @@ if ($args -contains 'metadata') { version = '0.1.0' id = "$packageName 0.1.0" manifest_path = $manifestPath - targets = @([pscustomobject]@{ name = $libName; kind = @('lib') }) + targets = @([pscustomobject]@{ + name = $libName + kind = if ($env:FAKE_FIRST_RLIB) { @('rlib') } else { @('lib') } + doctest = $true + }) publish = if ($env:FAKE_PUBLISH_FALSE) { # Preserve the empty array through expression output so JSON emits [] rather than null. Write-Output -NoEnumerate @() @@ -139,6 +143,7 @@ if ($args -contains 'metadata') { targets = @([pscustomobject]@{ name = $env:FAKE_SECOND_PACKAGE_NAME kind = if ($env:FAKE_SECOND_BIN_ONLY) { @('bin') } else { @('lib') } + doctest = -not [bool]$env:FAKE_SECOND_BIN_ONLY }) publish = $null metadata = [pscustomobject]@{} @@ -158,6 +163,7 @@ if ($args -contains 'metadata') { targets = @([pscustomobject]@{ name = $env:FAKE_THIRD_PACKAGE_NAME kind = if ($env:FAKE_THIRD_PROC_MACRO) { @('proc-macro') } else { @('lib') } + doctest = $true }) publish = @('private-registry') metadata = [pscustomobject]@{} @@ -964,7 +970,7 @@ fn fmt_propagates_cargo_each_failure() { } #[test] -fn doc_test_selects_libraries_and_proc_macros_but_not_bin_only_packages() { +fn doc_test_selects_doctest_capable_targets_but_not_bin_only_packages() { if !tools_available() { return; } @@ -983,6 +989,7 @@ fn doc_test_selects_libraries_and_proc_macros_but_not_bin_only_packages() { &["anvil-doc-test"], &[ ("FAKE_CARGO_LOG", log.as_os_str()), + ("FAKE_FIRST_RLIB", OsStr::new("1")), ("FAKE_SECOND_PACKAGE_NAME", OsStr::new("bin-only")), ("FAKE_SECOND_BIN_ONLY", OsStr::new("1")), ("FAKE_THIRD_PACKAGE_NAME", OsStr::new("macro-package")), @@ -1031,7 +1038,7 @@ fn doc_test_selects_libraries_and_proc_macros_but_not_bin_only_packages() { ); assert!(bin_only.status.success(), "bin-only doc-test selection must skip cleanly"); assert!( - String::from_utf8_lossy(&bin_only.stdout).contains("no affected library or proc-macro packages"), + String::from_utf8_lossy(&bin_only.stdout).contains("no affected doctest-capable packages"), "bin-only skip must explain why no doctests ran" ); assert!( diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap index b86233a5e..72ae63b00 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap @@ -1994,8 +1994,9 @@ anvil-doc-build-validate-prereqs: anvil-tool-rustc-validate-prereqs anvil-tool-c # of total compile/test time. # # Cargo errors when `cargo test --doc` selects only bin-only packages. Build -# the documentation-capable subset (plain libraries and proc macros) from -# metadata before delegating the two invocations to cargo-each. +# the subset with at least one Cargo target marked `doctest = true` before +# delegating the two invocations to cargo-each. This includes explicit library +# crate types such as rlib/cdylib as well as plain libraries and proc macros. # Run documentation tests for affected workspace packages. [script("pwsh", "-NoProfile")] @@ -2014,7 +2015,7 @@ anvil-doc-test: anvil-doc-test-validate-prereqs anvil-impact } $docPackages = @{} foreach ($package in $meta.packages) { - if ($package.targets | Where-Object { ($_.kind -contains 'lib') -or ($_.kind -contains 'proc-macro') }) { + if ($package.targets | Where-Object { $_.doctest -eq $true }) { $docPackages[$package.name] = $true } } @@ -2031,7 +2032,7 @@ anvil-doc-test: anvil-doc-test-validate-prereqs anvil-impact ) } if ($names.Count -eq 0) { - Write-Host 'anvil-doc-test: no affected library or proc-macro packages; skipping' + Write-Host 'anvil-doc-test: no affected doctest-capable packages; skipping' exit 0 } $docSelection = [System.Collections.Generic.List[string]]::new() @@ -5796,8 +5797,10 @@ _anvil-impact-format tier impactJson: # Private. Resolves an impact category's scope for a check recipe and echoes it on stdout. # 1. ANVIL_IMPACT=off -> tier default (full-workspace / run). -# 2. include_.txt present -> its selector tokens. -# 3. otherwise -> tier default. +# 2. include_.txt present -> validated selector tokens; malformed +# content fails closed. +# 3. file missing -> tier default, except consume mode fails +# because its downloaded cache is incomplete. # Tier default: --workspace for every tier. # Each scoped check calls this directly (into a local $selection array) to # read its category's scope from the target/anvil/impact cache -- the same cache diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap index d291f0222..f47bbfd9a 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap @@ -2131,8 +2131,9 @@ anvil-doc-build-validate-prereqs: anvil-tool-rustc-validate-prereqs anvil-tool-c # of total compile/test time. # # Cargo errors when `cargo test --doc` selects only bin-only packages. Build -# the documentation-capable subset (plain libraries and proc macros) from -# metadata before delegating the two invocations to cargo-each. +# the subset with at least one Cargo target marked `doctest = true` before +# delegating the two invocations to cargo-each. This includes explicit library +# crate types such as rlib/cdylib as well as plain libraries and proc macros. # Run documentation tests for affected workspace packages. [script("pwsh", "-NoProfile")] @@ -2151,7 +2152,7 @@ anvil-doc-test: anvil-doc-test-validate-prereqs anvil-impact } $docPackages = @{} foreach ($package in $meta.packages) { - if ($package.targets | Where-Object { ($_.kind -contains 'lib') -or ($_.kind -contains 'proc-macro') }) { + if ($package.targets | Where-Object { $_.doctest -eq $true }) { $docPackages[$package.name] = $true } } @@ -2168,7 +2169,7 @@ anvil-doc-test: anvil-doc-test-validate-prereqs anvil-impact ) } if ($names.Count -eq 0) { - Write-Host 'anvil-doc-test: no affected library or proc-macro packages; skipping' + Write-Host 'anvil-doc-test: no affected doctest-capable packages; skipping' exit 0 } $docSelection = [System.Collections.Generic.List[string]]::new() @@ -5933,8 +5934,10 @@ _anvil-impact-format tier impactJson: # Private. Resolves an impact category's scope for a check recipe and echoes it on stdout. # 1. ANVIL_IMPACT=off -> tier default (full-workspace / run). -# 2. include_.txt present -> its selector tokens. -# 3. otherwise -> tier default. +# 2. include_.txt present -> validated selector tokens; malformed +# content fails closed. +# 3. file missing -> tier default, except consume mode fails +# because its downloaded cache is incomplete. # Tier default: --workspace for every tier. # Each scoped check calls this directly (into a local $selection array) to # read its category's scope from the target/anvil/impact cache -- the same cache diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap index 12f169e22..57565af73 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap @@ -864,8 +864,9 @@ anvil-doc-build-validate-prereqs: anvil-tool-rustc-validate-prereqs anvil-tool-c # of total compile/test time. # # Cargo errors when `cargo test --doc` selects only bin-only packages. Build -# the documentation-capable subset (plain libraries and proc macros) from -# metadata before delegating the two invocations to cargo-each. +# the subset with at least one Cargo target marked `doctest = true` before +# delegating the two invocations to cargo-each. This includes explicit library +# crate types such as rlib/cdylib as well as plain libraries and proc macros. # Run documentation tests for affected workspace packages. [script("pwsh", "-NoProfile")] @@ -884,7 +885,7 @@ anvil-doc-test: anvil-doc-test-validate-prereqs anvil-impact } $docPackages = @{} foreach ($package in $meta.packages) { - if ($package.targets | Where-Object { ($_.kind -contains 'lib') -or ($_.kind -contains 'proc-macro') }) { + if ($package.targets | Where-Object { $_.doctest -eq $true }) { $docPackages[$package.name] = $true } } @@ -901,7 +902,7 @@ anvil-doc-test: anvil-doc-test-validate-prereqs anvil-impact ) } if ($names.Count -eq 0) { - Write-Host 'anvil-doc-test: no affected library or proc-macro packages; skipping' + Write-Host 'anvil-doc-test: no affected doctest-capable packages; skipping' exit 0 } $docSelection = [System.Collections.Generic.List[string]]::new() @@ -4666,8 +4667,10 @@ _anvil-impact-format tier impactJson: # Private. Resolves an impact category's scope for a check recipe and echoes it on stdout. # 1. ANVIL_IMPACT=off -> tier default (full-workspace / run). -# 2. include_.txt present -> its selector tokens. -# 3. otherwise -> tier default. +# 2. include_.txt present -> validated selector tokens; malformed +# content fails closed. +# 3. file missing -> tier default, except consume mode fails +# because its downloaded cache is incomplete. # Tier default: --workspace for every tier. # Each scoped check calls this directly (into a local $selection array) to # read its category's scope from the target/anvil/impact cache -- the same cache diff --git a/justfiles/anvil/checks/doc-test.just b/justfiles/anvil/checks/doc-test.just index 60de891d9..f5f945647 100644 --- a/justfiles/anvil/checks/doc-test.just +++ b/justfiles/anvil/checks/doc-test.just @@ -14,8 +14,9 @@ # of total compile/test time. # # Cargo errors when `cargo test --doc` selects only bin-only packages. Build -# the documentation-capable subset (plain libraries and proc macros) from -# metadata before delegating the two invocations to cargo-each. +# the subset with at least one Cargo target marked `doctest = true` before +# delegating the two invocations to cargo-each. This includes explicit library +# crate types such as rlib/cdylib as well as plain libraries and proc macros. # Run documentation tests for affected workspace packages. [script("pwsh", "-NoProfile")] @@ -34,7 +35,7 @@ anvil-doc-test: anvil-doc-test-validate-prereqs anvil-impact } $docPackages = @{} foreach ($package in $meta.packages) { - if ($package.targets | Where-Object { ($_.kind -contains 'lib') -or ($_.kind -contains 'proc-macro') }) { + if ($package.targets | Where-Object { $_.doctest -eq $true }) { $docPackages[$package.name] = $true } } @@ -51,7 +52,7 @@ anvil-doc-test: anvil-doc-test-validate-prereqs anvil-impact ) } if ($names.Count -eq 0) { - Write-Host 'anvil-doc-test: no affected library or proc-macro packages; skipping' + Write-Host 'anvil-doc-test: no affected doctest-capable packages; skipping' exit 0 } $docSelection = [System.Collections.Generic.List[string]]::new() diff --git a/justfiles/anvil/impact.just b/justfiles/anvil/impact.just index dbfce846e..2ffe8fb04 100644 --- a/justfiles/anvil/impact.just +++ b/justfiles/anvil/impact.just @@ -605,8 +605,10 @@ _anvil-impact-format tier impactJson: # Private. Resolves an impact category's scope for a check recipe and echoes it on stdout. # 1. ANVIL_IMPACT=off -> tier default (full-workspace / run). -# 2. include_.txt present -> its selector tokens. -# 3. otherwise -> tier default. +# 2. include_.txt present -> validated selector tokens; malformed +# content fails closed. +# 3. file missing -> tier default, except consume mode fails +# because its downloaded cache is incomplete. # Tier default: --workspace for every tier. # Each scoped check calls this directly (into a local $selection array) to # read its category's scope from the target/anvil/impact cache -- the same cache From df61f3688f6f5902e43caf92c39b3973bf0e2923 Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Thu, 3 Sep 2026 15:37:11 +0200 Subject: [PATCH 12/15] fix(anvil): sort doctest package selection Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .anvil.lock | 4 ++-- .../templates/justfiles/anvil/checks/doc-test.just | 2 +- crates/cargo-anvil/tests/recipe_contracts.rs | 4 ++++ .../cargo-anvil/tests/snapshots/snapshots__ado_backend.snap | 2 +- .../tests/snapshots/snapshots__github_backend.snap | 2 +- crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap | 2 +- justfiles/anvil/checks/doc-test.just | 2 +- 7 files changed, 11 insertions(+), 7 deletions(-) diff --git a/.anvil.lock b/.anvil.lock index 605b0d008..c043aa90b 100644 --- a/.anvil.lock +++ b/.anvil.lock @@ -1,7 +1,7 @@ version = 1 tool = "anvil" tool_version = "0.6.0" -catalog_checksum = "sha256:aecead1dba65f691c184186bb82773b50336b1f29ca1b287c28b59bc14db6419" +catalog_checksum = "sha256:52be5f78afe4b3be333e0bbebbeb91b726092c85d24f3011dd8546806d8477c5" [[file]] path = ".anvil/container/Dockerfile.dockerignore" @@ -89,7 +89,7 @@ checksum = "sha256:d2f6a9536d62030db5505c88d510c4114cb0aab81a7af0e995ba8da5ce3a1 [[file]] path = "justfiles/anvil/checks/doc-test.just" -checksum = "sha256:094506c8f0d7a0ba7631d640ba2d22fbd53ad5efd273477b4a268296d1007c36" +checksum = "sha256:b02381d3d8d33aece84557eecfa510a2be12b2d091c7726b4f63cbcc431eee88" [[file]] path = "justfiles/anvil/checks/ensure-no-cyclic-deps.just" 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 f5f945647..b9ad14927 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/doc-test.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/doc-test.just @@ -40,7 +40,7 @@ anvil-doc-test: anvil-doc-test-validate-prereqs anvil-impact } } $names = if ($selection -contains '--workspace') { - @($docPackages.Keys) + @($docPackages.Keys | Sort-Object) } else { @( for ($i = 0; $i -lt $selection.Count; $i++) { diff --git a/crates/cargo-anvil/tests/recipe_contracts.rs b/crates/cargo-anvil/tests/recipe_contracts.rs index 6bb7c7e91..f5d04917b 100644 --- a/crates/cargo-anvil/tests/recipe_contracts.rs +++ b/crates/cargo-anvil/tests/recipe_contracts.rs @@ -1006,6 +1006,10 @@ fn doc_test_selects_doctest_capable_targets_but_not_bin_only_packages() { assert_eq!(commands.matches(" test --doc ").count(), 2, "commands:\n{commands}"); assert!(commands.contains("--package fixture"), "commands:\n{commands}"); assert!(commands.contains("--package macro-package"), "commands:\n{commands}"); + assert!( + commands.contains("--package fixture --package macro-package"), + "workspace doctest packages must be sorted by name:\n{commands}" + ); assert!(!commands.contains("--package bin-only"), "commands:\n{commands}"); seed_include(tmp.path(), "affected", "--package\nfixture\n--package\nbin-only"); diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap index 72ae63b00..87bb8b85b 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap @@ -2020,7 +2020,7 @@ anvil-doc-test: anvil-doc-test-validate-prereqs anvil-impact } } $names = if ($selection -contains '--workspace') { - @($docPackages.Keys) + @($docPackages.Keys | Sort-Object) } else { @( for ($i = 0; $i -lt $selection.Count; $i++) { diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap index f47bbfd9a..a02054c59 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap @@ -2157,7 +2157,7 @@ anvil-doc-test: anvil-doc-test-validate-prereqs anvil-impact } } $names = if ($selection -contains '--workspace') { - @($docPackages.Keys) + @($docPackages.Keys | Sort-Object) } else { @( for ($i = 0; $i -lt $selection.Count; $i++) { diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap index 57565af73..22d479537 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap @@ -890,7 +890,7 @@ anvil-doc-test: anvil-doc-test-validate-prereqs anvil-impact } } $names = if ($selection -contains '--workspace') { - @($docPackages.Keys) + @($docPackages.Keys | Sort-Object) } else { @( for ($i = 0; $i -lt $selection.Count; $i++) { diff --git a/justfiles/anvil/checks/doc-test.just b/justfiles/anvil/checks/doc-test.just index f5f945647..b9ad14927 100644 --- a/justfiles/anvil/checks/doc-test.just +++ b/justfiles/anvil/checks/doc-test.just @@ -40,7 +40,7 @@ anvil-doc-test: anvil-doc-test-validate-prereqs anvil-impact } } $names = if ($selection -contains '--workspace') { - @($docPackages.Keys) + @($docPackages.Keys | Sort-Object) } else { @( for ($i = 0; $i -lt $selection.Count; $i++) { From 5fea6dea67244ecc5484cccff2fca888835a3a6f Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Thu, 3 Sep 2026 16:22:21 +0200 Subject: [PATCH 13/15] docs(gamma): unify internal example gating Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/cargo-gamma-lib/src/cfg/features.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/cargo-gamma-lib/src/cfg/features.rs b/crates/cargo-gamma-lib/src/cfg/features.rs index b9fc44c0e..2434a958e 100644 --- a/crates/cargo-gamma-lib/src/cfg/features.rs +++ b/crates/cargo-gamma-lib/src/cfg/features.rs @@ -42,11 +42,9 @@ use crate::{HashMap, HashSet}; /// /// ```rust,no_run /// # #[cfg(feature = "internals")] +/// # fn example(metadata: &cargo_metadata::Metadata) { /// # use cargo_gamma_lib::internals::cfg::features::enabled; -/// # #[cfg(feature = "internals")] /// # use cargo_gamma_lib::internals::commands::FeatureArgs; -/// # #[cfg(feature = "internals")] -/// # fn example(metadata: &cargo_metadata::Metadata) { /// let features = enabled(metadata, &FeatureArgs::default()); /// /// // Every member is present, even one with no features at all. From 9dbd0e3f3269ee33dcd1064c3676814835f488e5 Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Thu, 3 Sep 2026 16:45:23 +0200 Subject: [PATCH 14/15] fix(anvil): preserve MSRV target coverage Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .anvil.lock | 4 +-- crates/cargo-anvil/docs/design/checks.md | 26 ++++++++++--------- .../cargo-anvil/templates/ado/pr-stages.yml | 2 +- .../justfiles/anvil/checks/msrv-test.just | 6 +++++ crates/cargo-anvil/tests/impact.rs | 11 ++++---- .../snapshots/snapshots__ado_backend.snap | 8 +++++- .../snapshots/snapshots__github_backend.snap | 6 +++++ .../snapshots/snapshots__local_only.snap | 6 +++++ justfiles/anvil/checks/msrv-test.just | 6 +++++ 9 files changed, 54 insertions(+), 21 deletions(-) diff --git a/.anvil.lock b/.anvil.lock index 1ae3a1a29..6c14aa291 100644 --- a/.anvil.lock +++ b/.anvil.lock @@ -1,7 +1,7 @@ version = 1 tool = "anvil" tool_version = "0.7.0" -catalog_checksum = "sha256:52be5f78afe4b3be333e0bbebbeb91b726092c85d24f3011dd8546806d8477c5" +catalog_checksum = "sha256:fc542b4d14f46331aa8124b51ce8c9679e0139cc2274be3a03649aa34f214dda" [[file]] path = ".anvil/container/Dockerfile.dockerignore" @@ -141,7 +141,7 @@ checksum = "sha256:8db76d984b84f209e39bad9297eff29bae7fec434eddf12cf5b2074934110 [[file]] path = "justfiles/anvil/checks/msrv-test.just" -checksum = "sha256:c1325da8e479b773310c6399c9ec895f2d5d3271ffabc46ac3713a116290687c" +checksum = "sha256:c0511437a1543f46e212dae50abb1f10158f4a79bed14184f60782fee1aa7125" [[file]] path = "justfiles/anvil/checks/mutants-diff.just" diff --git a/crates/cargo-anvil/docs/design/checks.md b/crates/cargo-anvil/docs/design/checks.md index dc4a654f0..16a41ccd3 100644 --- a/crates/cargo-anvil/docs/design/checks.md +++ b/crates/cargo-anvil/docs/design/checks.md @@ -118,7 +118,7 @@ jobs/stages. Locally, `just anvil-pr-slow` invokes those groups in order, and |--------------------|---------------------------------------|----------------------------------------------------------------------------------------------------------------------| | `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-msrv` | Same default as `pr-test` | Affected-package tests plus compile-only bench/example checks 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. | @@ -211,13 +211,15 @@ matrix overhead. #### `pr-msrv` (minimum-version tests) -When the root manifest declares an MSRV, Anvil runs `cargo test --tests` -for affected packages under that compiler. `--tests` selects every target that -carries `test = true` -- library and binary unit tests, and integration tests. -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 test execution at the minimum supported compiler; +When the root manifest declares an MSRV, Anvil runs `cargo test --tests` plus +`cargo check --benches --examples` for affected packages under that compiler. +`--tests` selects library and binary unit tests and integration tests; the +separate check preserves compile-only MSRV coverage for benches and examples. +Anvil runs both commands in 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 test and compile coverage 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 suite, so an MSRV fallback does not make `pr-msrv` a duplicate. @@ -232,10 +234,10 @@ run to a separate driver binary -- criterion's, or a profiler runner such as `gungraun-runner` driving Valgrind -- and `anvil-msrv-test-setup` installs only the MSRV toolchain, so that driver is absent and the group fails on a prerequisite it never declares. That failure says nothing about the minimum -supported version. It also matches the repository-wide policy that benches and -examples are compiled but never run: `bench` uses `cargo bench --no-run` and -`examples` uses `cargo build --examples`, and both keep that compile coverage on -the selected stable compiler. `--tests` is preferred over the equivalent +supported version. The separate `cargo check --benches --examples` invocations +type-check those targets under the MSRV without linking or executing their +harnesses. This matches the repository-wide policy that benches and examples +are compiled but never run. `--tests` is preferred over the equivalent `--lib --bins --tests` because `--lib` errors with "no library targets found" on a bin-only affected package under impact scoping, the same reason `miri` uses it. No doctest coverage is lost, since `--all-targets` suppresses doctests too and diff --git a/crates/cargo-anvil/templates/ado/pr-stages.yml b/crates/cargo-anvil/templates/ado/pr-stages.yml index 37600d142..b6b77a05d 100644 --- a/crates/cargo-anvil/templates/ado/pr-stages.yml +++ b/crates/cargo-anvil/templates/ado/pr-stages.yml @@ -70,7 +70,7 @@ stages: # Each pr-* job downloads its OS's `anvil-impact-` artifact into # target/anvil/impact/ (job.yml `inputArtifacts`); the group's scoped checks # then read that cache via their `anvil-impact` dependency. An empty tier - # resolves to `--skip` and a first-time-adoption fallback to `--workspace`, + # resolves to `--none` and a first-time-adoption fallback to `--workspace`, # exactly as locally. - stage: pr_fast displayName: anvil pr-fast diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/msrv-test.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/msrv-test.just index 66af03e68..908db4a7d 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/msrv-test.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/msrv-test.just @@ -31,6 +31,12 @@ anvil-msrv-test: anvil-msrv-test-validate-prereqs anvil-impact if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo "+$toolchain" test '{packages}' --tests --locked if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + # Preserve MSRV compile coverage for benches and examples without using + # `cargo test --all-targets`, which would execute harness-free benches. + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo "+$toolchain" check '{packages}' --benches --examples --all-features --locked + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo "+$toolchain" check '{packages}' --benches --examples --locked + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install the declared MSRV toolchain only when this check is required. [group("anvil-setup")] diff --git a/crates/cargo-anvil/tests/impact.rs b/crates/cargo-anvil/tests/impact.rs index 97d960ba1..d1bd4ed87 100644 --- a/crates/cargo-anvil/tests/impact.rs +++ b/crates/cargo-anvil/tests/impact.rs @@ -1061,15 +1061,16 @@ fn msrv_test_uses_affected_packages_for_both_feature_modes_and_skips_without_msr for expected in [ format!("each {affected} --once -- cargo +1.97 test {{packages}} --tests --all-features --locked"), format!("each {affected} --once -- cargo +1.97 test {{packages}} --tests --locked"), + format!("each {affected} --once -- cargo +1.97 check {{packages}} --benches --examples --all-features --locked"), + format!("each {affected} --once -- cargo +1.97 check {{packages}} --benches --examples --locked"), ] { assert!(argv.contains(&expected), "missing MSRV invocation '{expected}' in:\n{argv}"); } - // The MSRV check must not build or execute bench targets: `--all-targets` - // expands to include `--benches`, and a `harness = false` bench then runs - // through a driver binary the msrv setup chain never installs. + // Tests must not execute bench targets; the separate cargo-check commands + // retain compile-only MSRV coverage for benches and examples. assert!( - !argv.contains("--all-targets") && !argv.contains("--benches"), - "MSRV invocations must not select bench targets; captured argv:\n{argv}" + !argv.lines().any(|line| line.contains(" test ") && line.contains("--benches")), + "MSRV test invocations must not select bench targets; captured argv:\n{argv}" ); let manifest = fs::read_to_string(root.join("Cargo.toml")).unwrap(); diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap index 87bb8b85b..dda2b158d 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap @@ -345,7 +345,7 @@ stages: # Each pr-* job downloads its OS's `anvil-impact-` artifact into # target/anvil/impact/ (job.yml `inputArtifacts`); the group's scoped checks # then read that cache via their `anvil-impact` dependency. An empty tier - # resolves to `--skip` and a first-time-adoption fallback to `--workspace`, + # resolves to `--none` and a first-time-adoption fallback to `--workspace`, # exactly as locally. - stage: pr_fast displayName: anvil pr-fast @@ -2796,6 +2796,12 @@ anvil-msrv-test: anvil-msrv-test-validate-prereqs anvil-impact if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo "+$toolchain" test '{packages}' --tests --locked if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + # Preserve MSRV compile coverage for benches and examples without using + # `cargo test --all-targets`, which would execute harness-free benches. + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo "+$toolchain" check '{packages}' --benches --examples --all-features --locked + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo "+$toolchain" check '{packages}' --benches --examples --locked + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install the declared MSRV toolchain only when this check is required. [group("anvil-setup")] diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap index a02054c59..597e27f9a 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap @@ -2933,6 +2933,12 @@ anvil-msrv-test: anvil-msrv-test-validate-prereqs anvil-impact if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo "+$toolchain" test '{packages}' --tests --locked if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + # Preserve MSRV compile coverage for benches and examples without using + # `cargo test --all-targets`, which would execute harness-free benches. + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo "+$toolchain" check '{packages}' --benches --examples --all-features --locked + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo "+$toolchain" check '{packages}' --benches --examples --locked + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install the declared MSRV toolchain only when this check is required. [group("anvil-setup")] diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap index 22d479537..ef61f3edd 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap @@ -1666,6 +1666,12 @@ anvil-msrv-test: anvil-msrv-test-validate-prereqs anvil-impact if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo "+$toolchain" test '{packages}' --tests --locked if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + # Preserve MSRV compile coverage for benches and examples without using + # `cargo test --all-targets`, which would execute harness-free benches. + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo "+$toolchain" check '{packages}' --benches --examples --all-features --locked + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo "+$toolchain" check '{packages}' --benches --examples --locked + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install the declared MSRV toolchain only when this check is required. [group("anvil-setup")] diff --git a/justfiles/anvil/checks/msrv-test.just b/justfiles/anvil/checks/msrv-test.just index 66af03e68..908db4a7d 100644 --- a/justfiles/anvil/checks/msrv-test.just +++ b/justfiles/anvil/checks/msrv-test.just @@ -31,6 +31,12 @@ anvil-msrv-test: anvil-msrv-test-validate-prereqs anvil-impact if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo "+$toolchain" test '{packages}' --tests --locked if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + # Preserve MSRV compile coverage for benches and examples without using + # `cargo test --all-targets`, which would execute harness-free benches. + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo "+$toolchain" check '{packages}' --benches --examples --all-features --locked + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & cargo {{_anvil_stable_toolchain_args}} each @selection --once '--' cargo "+$toolchain" check '{packages}' --benches --examples --locked + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install the declared MSRV toolchain only when this check is required. [group("anvil-setup")] From adab0005797cc2bf9d4655ac92d62ea39c326899 Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Thu, 3 Sep 2026 17:32:42 +0200 Subject: [PATCH 15/15] test(anvil): prove scoped doctest intersection Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/cargo-anvil/tests/recipe_contracts.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/cargo-anvil/tests/recipe_contracts.rs b/crates/cargo-anvil/tests/recipe_contracts.rs index f5d04917b..941a4e364 100644 --- a/crates/cargo-anvil/tests/recipe_contracts.rs +++ b/crates/cargo-anvil/tests/recipe_contracts.rs @@ -1021,6 +1021,8 @@ fn doc_test_selects_doctest_capable_targets_but_not_bin_only_packages() { ("FAKE_CARGO_LOG", log.as_os_str()), ("FAKE_SECOND_PACKAGE_NAME", OsStr::new("bin-only")), ("FAKE_SECOND_BIN_ONLY", OsStr::new("1")), + ("FAKE_THIRD_PACKAGE_NAME", OsStr::new("unselected-doc")), + ("FAKE_THIRD_PROC_MACRO", OsStr::new("1")), ], ); assert!(scoped.status.success(), "scoped doc-test selection failed"); @@ -1028,6 +1030,7 @@ fn doc_test_selects_doctest_capable_targets_but_not_bin_only_packages() { assert_eq!(scoped_commands.matches(" test --doc ").count(), 2); assert!(scoped_commands.contains("--package fixture")); assert!(!scoped_commands.contains("--package bin-only")); + assert!(!scoped_commands.contains("--package unselected-doc")); seed_include(tmp.path(), "affected", "--package\nbin-only"); fs::write(&log, "").unwrap();