From 59be759fefe8a4103575721483a0529cb1fbd525 Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Wed, 26 Aug 2026 11:35:25 +0200 Subject: [PATCH 01/17] fix(anvil): harden workspace checks Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/cargo-anvil/docs/design/checks.md | 6 +- crates/cargo-anvil/docs/design/local.md | 8 +- .../justfiles/anvil/checks/cargo-sort.just | 14 +- .../anvil/checks/external-types.just | 12 +- .../templates/justfiles/anvil/checks/fmt.just | 26 +++- crates/cargo-anvil/tests/recipe_contracts.rs | 141 +++++++++++++++++- .../snapshots/snapshots__ado_backend.snap | 52 +++++-- .../snapshots/snapshots__github_backend.snap | 52 +++++-- .../snapshots/snapshots__local_only.snap | 52 +++++-- 9 files changed, 290 insertions(+), 73 deletions(-) diff --git a/crates/cargo-anvil/docs/design/checks.md b/crates/cargo-anvil/docs/design/checks.md index 69d2be0a..e63508c2 100644 --- a/crates/cargo-anvil/docs/design/checks.md +++ b/crates/cargo-anvil/docs/design/checks.md @@ -157,9 +157,9 @@ that provided the strongest version of the check. | Check | Invocation | Source | |--------------------------------|-----------------------------------------------------------|--------| -| `fmt` | `cargo + fmt --all --check` | all | +| `fmt` | `cargo + fmt --manifest-path --check` for each workspace package. Manifest-scoped invocations avoid platform command-line limits, remain valid for nested packages with independent workspace membership, and need no separate fallback path. | all | | `clippy` | `cargo clippy --workspace --all-targets --all-features --locked -- -D warnings` | all | -| `cargo-sort` | `cargo sort --workspace --grouped --check --check-format` | oxidizer-github | +| `cargo-sort` | `cargo sort --workspace --grouped --check --no-format`. Anvil checks dependency ordering but leaves general TOML formatting alone so formatter reordering cannot detach managed-region sentinels. | oxidizer-github | | `license-headers` | `cargo heather --workspace` | oxidizer (`heather`), oxidizer-github | | `ensure-no-cyclic-deps` | `cargo ensure-no-cyclic-deps --workspace` | oxidizer-github (sibling crate in `ox-tools-gh`) | | `ensure-no-default-features` | `cargo ensure-no-default-features --workspace` | oxidizer-github | @@ -171,7 +171,7 @@ that provided the strongest version of the check. | `audit` | `cargo audit` | oxidizer | | `udeps` | `cargo + udeps --workspace --all-features` run **twice** — once with default targets (lib + bins) and once with `--all-targets`. cargo-udeps only analyzes the targets it's told to, and each run catches a variant the other masks: the default-targets run surfaces a dep in `[dependencies]` referenced only by tests/benches/examples (it should be a dev-dep; `--all-targets` would see it as "used"), while the `--all-targets` run surfaces unused `[dev-dependencies]` (never compiled by the default-targets run). Together they cover unused deps, unused dev-deps, and deps that should be dev-deps. | oxidizer, oxidizer-github | | `semver-check` | `cargo semver-checks --baseline-rev ` per affected library crate. The PR target is the baseline. Exit 100 is a completed check with deny-level findings; exit 101 or another nonzero status means the comparison was inconclusive. Both outcomes write `target/anvil/comments/semver.md` and remain advisory, matching the repository's native `semver` job (`continue-on-error: true`). Proven rename and bin→lib transitions with no comparable baseline are skipped without a comment. Anvil preflight failures such as invalid current-workspace metadata or an unavailable baseline ref still fail because the recipe cannot establish what to compare. | oxidizer-github | -| `external-types` | `cargo + check-external-types --manifest-path` per library crate (per-manifest because the tool has no `--workspace`/`--package`; bin-only crates have no public API surface and are skipped). Setup installs the catalog version but validation accepts newer installed tools. The selected nightly is tested with the catalog version; an incompatible newer tool fails closed with a tool/nightly compatibility diagnostic rather than silently selecting a different schema. | oxidizer-github | +| `external-types` | `cargo + check-external-types --manifest-path` per publishable library crate (per-manifest because the tool has no `--workspace`/`--package`; bin-only and `publish = false` crates have no registry consumer contract and are skipped). Setup installs the catalog version but validation accepts newer installed tools. The selected nightly is tested with the catalog version; an incompatible newer tool fails closed with a tool/nightly compatibility diagnostic rather than silently selecting a different schema. | oxidizer-github | ### `pr-slow` diff --git a/crates/cargo-anvil/docs/design/local.md b/crates/cargo-anvil/docs/design/local.md index 69db4dbd..4899fc7c 100644 --- a/crates/cargo-anvil/docs/design/local.md +++ b/crates/cargo-anvil/docs/design/local.md @@ -456,15 +456,17 @@ anvil-clippy: cargo clippy ${ANVIL_INCLUDE_AFFECTED:---workspace} --all-targets --all-features --locked -- -D warnings ``` -A typical modified-tier recipe (the tool is workspace-wide, so there's nothing to -splice — only the skip guard matters): +A typical modified-tier recipe checks the whole workspace while keeping each +rustfmt child command bounded: ```just anvil-fmt: @if [ "$ANVIL_INCLUDE_MODIFIED" = "--skip" ]; then \ echo "anvil-fmt: no modified packages; skipping"; exit 0; \ fi; \ - cargo fmt --all --check + for manifest in $(cargo metadata ...); do \ + cargo fmt --manifest-path "$manifest" --check; \ + done ``` The mapping from check to bucket is fixed in the catalog (see 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 0d6315fc..90b24156 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/cargo-sort.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/cargo-sort.just @@ -8,23 +8,19 @@ # Modified tier. # -# `--check-format` makes the formatting diff fail the check, not just the -# sort order. cargo-sort 2.1.2+ downgraded formatting-only diffs to -# warnings by default (PR #129); without this flag cargo-sort 2.1.2+ -# would pass on formatting drift. The flag preserves the intended -# check behavior across the supported range. +# `--no-format` limits this check to dependency ordering. General TOML +# formatting can move tables and dotted keys independently of comments, +# detaching Anvil's managed-region sentinels from the content they delimit. # # `--grouped` keeps the blank lines between groups of key/value pairs -# (e.g. blank-line-separated dependency blocks) intact, matching -# oxidizer-github's convention; without it cargo-sort would flag those -# blank lines as a formatting diff and fail the check. +# (e.g. blank-line-separated dependency blocks) intact. # Check that Cargo.toml dependency tables are sorted. [script("pwsh", "-NoProfile")] anvil-cargo-sort: anvil-cargo-sort-validate-prereqs $ErrorActionPreference = 'Stop' if ($env:ANVIL_INCLUDE_MODIFIED -eq '--skip') { exit 0 } - cargo sort --workspace --grouped --check --check-format + cargo sort --workspace --grouped --check --no-format if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-cargo-sort` recipe. diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/external-types.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/external-types.just index b78320ca..4502994f 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/external-types.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/external-types.just @@ -9,9 +9,10 @@ # Affected tier (lints public API of changed crates and rev-deps). # # cargo-check-external-types is per-manifest: no --package/--workspace, -# 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. +# only --manifest-path. Iterate the affected publishable library crates and run +# the tool once each, pointing at the crate's Cargo.toml. Bin-only crates have +# no public API surface, while `publish = false` crates have no registry +# consumer contract; both 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 @@ -28,7 +29,7 @@ anvil-external-types: anvil-external-types-validate-prereqs exit 0 } $pkg = @(if ($env:ANVIL_INCLUDE_AFFECTED) { -split $env:ANVIL_INCLUDE_AFFECTED } else { '--workspace' }) - # Build pkg-name -> manifest-path map, restricted to library crates. + # Build pkg-name -> manifest-path map, restricted to publishable library crates. $libPkgs = @{} $metadataJson = & cargo metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { @@ -42,7 +43,8 @@ anvil-external-types: anvil-external-types-validate-prereqs exit 1 } foreach ($p in $meta.packages) { - if ($p.targets | Where-Object { $_.kind -contains 'lib' }) { + if (($p.targets | Where-Object { $_.kind -contains 'lib' }) -and + -not ($p.publish -is [array] -and $p.publish.Count -eq 0)) { $libPkgs[$p.name] = $p.manifest_path } } diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/fmt.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/fmt.just index 2177dd2f..15c51f5f 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/fmt.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/fmt.just @@ -18,8 +18,30 @@ anvil-fmt: anvil-fmt-validate-prereqs $ErrorActionPreference = 'Stop' if ($env:ANVIL_INCLUDE_MODIFIED -eq '--skip') { exit 0 } - cargo '+{{ rust_nightly }}' fmt --all --check - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + $metadataJson = & cargo metadata --no-deps --format-version 1 + if ($LASTEXITCODE -ne 0) { + Write-Error 'anvil-fmt: cargo metadata failed' + exit $LASTEXITCODE + } + try { + $metadata = $metadataJson | ConvertFrom-Json + } catch { + Write-Error "anvil-fmt: could not parse cargo metadata output: $_" + exit 1 + } + $workspaceMembers = @{} + foreach ($id in $metadata.workspace_members) { + $workspaceMembers[$id] = $true + } + $packages = @( + $metadata.packages | + Where-Object { $workspaceMembers.ContainsKey($_.id) } | + Sort-Object name, version + ) + foreach ($package in $packages) { + & cargo '+{{ rust_nightly }}' fmt --manifest-path $package.manifest_path --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 52925110..800146b5 100644 --- a/crates/cargo-anvil/tests/recipe_contracts.rs +++ b/crates/cargo-anvil/tests/recipe_contracts.rs @@ -18,6 +18,7 @@ use tempfile::TempDir; const HELPERS: &str = include_str!("../templates/justfiles/anvil/helpers.just"); #[cfg(target_os = "linux")] const BOLERO: &str = include_str!("../templates/justfiles/anvil/checks/bolero.just"); +const FMT: &str = include_str!("../templates/justfiles/anvil/checks/fmt.just"); const LLVM_COV: &str = include_str!("../templates/justfiles/anvil/checks/llvm-cov.just"); const SEMVER: &str = include_str!("../templates/justfiles/anvil/checks/semver-check.just"); const EXTERNAL_TYPES: &str = include_str!("../templates/justfiles/anvil/checks/external-types.just"); @@ -49,6 +50,11 @@ if ($args -contains 'metadata') { id = "$packageName 0.1.0" manifest_path = $manifestPath targets = @([pscustomobject]@{ name = $libName; kind = @('lib') }) + publish = if ($env:FAKE_PUBLISH_FALSE) { + Write-Output -NoEnumerate @() + } else { + $null + } metadata = [pscustomobject]@{ 'coverage-gate' = [pscustomobject]@{ 'min-lines-percent' = 0 } } @@ -66,12 +72,20 @@ if ($args -contains 'metadata') { 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') }) + publish = $null metadata = [pscustomobject]@{} } } $metadata = [pscustomobject]@{ workspace_root = $root - workspace_members = @($packages | ForEach-Object { $_.id }) + workspace_members = @( + $packages | + Where-Object { + -not ($env:FAKE_SECOND_PACKAGE_NON_MEMBER -and + $_.name -eq $env:FAKE_SECOND_PACKAGE_NAME) + } | + ForEach-Object { $_.id } + ) packages = $packages } $metadata | ConvertTo-Json -Depth 8 -Compress @@ -117,6 +131,8 @@ fn fixture(imports: &[(&str, &str)], dependency_recipes: &[&str]) -> TempDir { // already defines it and Just rejects duplicate definitions. if !imports.iter().any(|(name, _)| *name == "versions.just") { justfile.push_str("rust_nightly := \"nightly-test\"\n\n"); + justfile.push_str("rust_nightly_external_types := \"nightly-test\"\n\n"); + justfile.push_str("cargo_check_external_types_version := \"0.0.0-test\"\n\n"); } for (name, contents) in imports { write(&tmp.path().join(name), contents); @@ -577,9 +593,9 @@ fn public_api_checks_fail_when_metadata_discovery_fails() { "anvil-external-types", &[ "anvil-tool-cargo-check-external-types-validate-prereqs", - "anvil-toolchain-external-types-validate-prereqs", + "anvil-toolchain-nightly-external-types-validate-prereqs", "anvil-tool-cargo-check-external-types-install installer", - "anvil-toolchain-external-types-install", + "anvil-toolchain-nightly-external-types-install", ][..], ), ] { @@ -606,6 +622,125 @@ fn public_api_checks_fail_when_metadata_discovery_fails() { } } +#[test] +fn fmt_formats_workspace_packages_individually() { + if !tools_available() { + return; + } + let tmp = fixture( + &[("fmt.just", FMT)], + &[ + "anvil-component-nightly-rustfmt-validate-prereqs", + "anvil-component-nightly-rustfmt-install", + ], + ); + let log = tmp.path().join("cargo.log"); + let output = run_just( + tmp.path(), + &["anvil-fmt"], + &[ + ("FAKE_CARGO_LOG", log.as_os_str()), + ("FAKE_SECOND_PACKAGE_NAME", OsStr::new("second")), + ], + ); + assert!( + output.status.success(), + "per-package formatting failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + let commands = std::fs::read_to_string(&log).unwrap(); + assert!(commands.contains("metadata --no-deps --format-version 1")); + assert!(commands.contains(&format!( + "+nightly-test fmt --manifest-path {} --check", + tmp.path().join("Cargo.toml").display() + ))); + assert!(commands.contains(&format!( + "+nightly-test fmt --manifest-path {} --check", + tmp.path().join("nested").join("Cargo.toml").display() + ))); + assert!(!commands.contains("fmt --all")); + + std::fs::write(&log, "").unwrap(); + let output = run_just( + tmp.path(), + &["anvil-fmt"], + &[ + ("FAKE_CARGO_LOG", log.as_os_str()), + ("FAKE_SECOND_PACKAGE_NAME", OsStr::new("second")), + ("FAKE_SECOND_PACKAGE_NON_MEMBER", OsStr::new("1")), + ], + ); + assert!( + output.status.success(), + "workspace-member filtering failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + let commands = std::fs::read_to_string(log).unwrap(); + assert!(commands.contains(&format!( + "+nightly-test fmt --manifest-path {} --check", + tmp.path().join("Cargo.toml").display() + ))); + assert!(!commands.contains(&format!( + "fmt --manifest-path {}", + tmp.path().join("nested").join("Cargo.toml").display() + ))); +} + +#[test] +fn fmt_fails_when_package_discovery_fails() { + if !tools_available() { + return; + } + let tmp = fixture( + &[("fmt.just", FMT)], + &[ + "anvil-component-nightly-rustfmt-validate-prereqs", + "anvil-component-nightly-rustfmt-install", + ], + ); + for environment in [("FAKE_METADATA_EXIT", OsStr::new("23")), ("FAKE_METADATA_INVALID", OsStr::new("1"))] { + let output = run_just(tmp.path(), &["anvil-fmt"], &[environment]); + assert_failed(&output, "anvil-fmt package discovery failure"); + } +} + +#[test] +fn external_types_skips_non_publishable_libraries() { + if !tools_available() { + return; + } + let tmp = fixture( + &[("external-types.just", 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", + ], + ); + let log = tmp.path().join("cargo.log"); + let output = run_just( + tmp.path(), + &["anvil-external-types"], + &[ + ("ANVIL_INCLUDE_AFFECTED", OsStr::new("--package fixture@0.1.0")), + ("FAKE_CARGO_LOG", log.as_os_str()), + ("FAKE_PUBLISH_FALSE", OsStr::new("1")), + ], + ); + assert!( + output.status.success(), + "private library filtering failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + let commands = std::fs::read_to_string(log).unwrap(); + assert!(commands.contains("metadata --no-deps --format-version 1")); + assert!(!commands.contains("check-external-types --manifest-path")); +} + #[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 7d6f37c4..74018375 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap @@ -3033,23 +3033,19 @@ anvil-cargo-hack-validate-prereqs: anvil-tool-cargo-hack-validate-prereqs # Modified tier. # -# `--check-format` makes the formatting diff fail the check, not just the -# sort order. cargo-sort 2.1.2+ downgraded formatting-only diffs to -# warnings by default (PR #129); without this flag cargo-sort 2.1.2+ -# would pass on formatting drift. The flag preserves the intended -# check behavior across the supported range. +# `--no-format` limits this check to dependency ordering. General TOML +# formatting can move tables and dotted keys independently of comments, +# detaching Anvil's managed-region sentinels from the content they delimit. # # `--grouped` keeps the blank lines between groups of key/value pairs -# (e.g. blank-line-separated dependency blocks) intact, matching -# oxidizer-github's convention; without it cargo-sort would flag those -# blank lines as a formatting diff and fail the check. +# (e.g. blank-line-separated dependency blocks) intact. # Check that Cargo.toml dependency tables are sorted. [script("pwsh", "-NoProfile")] anvil-cargo-sort: anvil-cargo-sort-validate-prereqs $ErrorActionPreference = 'Stop' if ($env:ANVIL_INCLUDE_MODIFIED -eq '--skip') { exit 0 } - cargo sort --workspace --grouped --check --check-format + cargo sort --workspace --grouped --check --no-format if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-cargo-sort` recipe. @@ -3282,9 +3278,10 @@ anvil-examples-validate-prereqs: anvil-tool-rustc-validate-prereqs # Affected tier (lints public API of changed crates and rev-deps). # # cargo-check-external-types is per-manifest: no --package/--workspace, -# 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. +# only --manifest-path. Iterate the affected publishable library crates and run +# the tool once each, pointing at the crate's Cargo.toml. Bin-only crates have +# no public API surface, while `publish = false` crates have no registry +# consumer contract; both 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 @@ -3301,7 +3298,7 @@ anvil-external-types: anvil-external-types-validate-prereqs exit 0 } $pkg = @(if ($env:ANVIL_INCLUDE_AFFECTED) { -split $env:ANVIL_INCLUDE_AFFECTED } else { '--workspace' }) - # Build pkg-name -> manifest-path map, restricted to library crates. + # Build pkg-name -> manifest-path map, restricted to publishable library crates. $libPkgs = @{} $metadataJson = & cargo metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { @@ -3315,7 +3312,8 @@ anvil-external-types: anvil-external-types-validate-prereqs exit 1 } foreach ($p in $meta.packages) { - if ($p.targets | Where-Object { $_.kind -contains 'lib' }) { + if (($p.targets | Where-Object { $_.kind -contains 'lib' }) -and + -not ($p.publish -is [array] -and $p.publish.Count -eq 0)) { $libPkgs[$p.name] = $p.manifest_path } } @@ -3387,8 +3385,30 @@ anvil-external-types-validate-prereqs: anvil-toolchain-nightly-external-types-va anvil-fmt: anvil-fmt-validate-prereqs $ErrorActionPreference = 'Stop' if ($env:ANVIL_INCLUDE_MODIFIED -eq '--skip') { exit 0 } - cargo '+{{ rust_nightly }}' fmt --all --check - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + $metadataJson = & cargo metadata --no-deps --format-version 1 + if ($LASTEXITCODE -ne 0) { + Write-Error 'anvil-fmt: cargo metadata failed' + exit $LASTEXITCODE + } + try { + $metadata = $metadataJson | ConvertFrom-Json + } catch { + Write-Error "anvil-fmt: could not parse cargo metadata output: $_" + exit 1 + } + $workspaceMembers = @{} + foreach ($id in $metadata.workspace_members) { + $workspaceMembers[$id] = $true + } + $packages = @( + $metadata.packages | + Where-Object { $workspaceMembers.ContainsKey($_.id) } | + Sort-Object name, version + ) + foreach ($package in $packages) { + & cargo '+{{ rust_nightly }}' fmt --manifest-path $package.manifest_path --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 0fe700aa..7a6c8d36 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap @@ -2932,23 +2932,19 @@ anvil-cargo-hack-validate-prereqs: anvil-tool-cargo-hack-validate-prereqs # Modified tier. # -# `--check-format` makes the formatting diff fail the check, not just the -# sort order. cargo-sort 2.1.2+ downgraded formatting-only diffs to -# warnings by default (PR #129); without this flag cargo-sort 2.1.2+ -# would pass on formatting drift. The flag preserves the intended -# check behavior across the supported range. +# `--no-format` limits this check to dependency ordering. General TOML +# formatting can move tables and dotted keys independently of comments, +# detaching Anvil's managed-region sentinels from the content they delimit. # # `--grouped` keeps the blank lines between groups of key/value pairs -# (e.g. blank-line-separated dependency blocks) intact, matching -# oxidizer-github's convention; without it cargo-sort would flag those -# blank lines as a formatting diff and fail the check. +# (e.g. blank-line-separated dependency blocks) intact. # Check that Cargo.toml dependency tables are sorted. [script("pwsh", "-NoProfile")] anvil-cargo-sort: anvil-cargo-sort-validate-prereqs $ErrorActionPreference = 'Stop' if ($env:ANVIL_INCLUDE_MODIFIED -eq '--skip') { exit 0 } - cargo sort --workspace --grouped --check --check-format + cargo sort --workspace --grouped --check --no-format if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-cargo-sort` recipe. @@ -3181,9 +3177,10 @@ anvil-examples-validate-prereqs: anvil-tool-rustc-validate-prereqs # Affected tier (lints public API of changed crates and rev-deps). # # cargo-check-external-types is per-manifest: no --package/--workspace, -# 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. +# only --manifest-path. Iterate the affected publishable library crates and run +# the tool once each, pointing at the crate's Cargo.toml. Bin-only crates have +# no public API surface, while `publish = false` crates have no registry +# consumer contract; both 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 @@ -3200,7 +3197,7 @@ anvil-external-types: anvil-external-types-validate-prereqs exit 0 } $pkg = @(if ($env:ANVIL_INCLUDE_AFFECTED) { -split $env:ANVIL_INCLUDE_AFFECTED } else { '--workspace' }) - # Build pkg-name -> manifest-path map, restricted to library crates. + # Build pkg-name -> manifest-path map, restricted to publishable library crates. $libPkgs = @{} $metadataJson = & cargo metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { @@ -3214,7 +3211,8 @@ anvil-external-types: anvil-external-types-validate-prereqs exit 1 } foreach ($p in $meta.packages) { - if ($p.targets | Where-Object { $_.kind -contains 'lib' }) { + if (($p.targets | Where-Object { $_.kind -contains 'lib' }) -and + -not ($p.publish -is [array] -and $p.publish.Count -eq 0)) { $libPkgs[$p.name] = $p.manifest_path } } @@ -3286,8 +3284,30 @@ anvil-external-types-validate-prereqs: anvil-toolchain-nightly-external-types-va anvil-fmt: anvil-fmt-validate-prereqs $ErrorActionPreference = 'Stop' if ($env:ANVIL_INCLUDE_MODIFIED -eq '--skip') { exit 0 } - cargo '+{{ rust_nightly }}' fmt --all --check - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + $metadataJson = & cargo metadata --no-deps --format-version 1 + if ($LASTEXITCODE -ne 0) { + Write-Error 'anvil-fmt: cargo metadata failed' + exit $LASTEXITCODE + } + try { + $metadata = $metadataJson | ConvertFrom-Json + } catch { + Write-Error "anvil-fmt: could not parse cargo metadata output: $_" + exit 1 + } + $workspaceMembers = @{} + foreach ($id in $metadata.workspace_members) { + $workspaceMembers[$id] = $true + } + $packages = @( + $metadata.packages | + Where-Object { $workspaceMembers.ContainsKey($_.id) } | + Sort-Object name, version + ) + foreach ($package in $packages) { + & cargo '+{{ rust_nightly }}' fmt --manifest-path $package.manifest_path --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 078a7888..b3921356 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap @@ -1773,23 +1773,19 @@ anvil-cargo-hack-validate-prereqs: anvil-tool-cargo-hack-validate-prereqs # Modified tier. # -# `--check-format` makes the formatting diff fail the check, not just the -# sort order. cargo-sort 2.1.2+ downgraded formatting-only diffs to -# warnings by default (PR #129); without this flag cargo-sort 2.1.2+ -# would pass on formatting drift. The flag preserves the intended -# check behavior across the supported range. +# `--no-format` limits this check to dependency ordering. General TOML +# formatting can move tables and dotted keys independently of comments, +# detaching Anvil's managed-region sentinels from the content they delimit. # # `--grouped` keeps the blank lines between groups of key/value pairs -# (e.g. blank-line-separated dependency blocks) intact, matching -# oxidizer-github's convention; without it cargo-sort would flag those -# blank lines as a formatting diff and fail the check. +# (e.g. blank-line-separated dependency blocks) intact. # Check that Cargo.toml dependency tables are sorted. [script("pwsh", "-NoProfile")] anvil-cargo-sort: anvil-cargo-sort-validate-prereqs $ErrorActionPreference = 'Stop' if ($env:ANVIL_INCLUDE_MODIFIED -eq '--skip') { exit 0 } - cargo sort --workspace --grouped --check --check-format + cargo sort --workspace --grouped --check --no-format if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-cargo-sort` recipe. @@ -2022,9 +2018,10 @@ anvil-examples-validate-prereqs: anvil-tool-rustc-validate-prereqs # Affected tier (lints public API of changed crates and rev-deps). # # cargo-check-external-types is per-manifest: no --package/--workspace, -# 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. +# only --manifest-path. Iterate the affected publishable library crates and run +# the tool once each, pointing at the crate's Cargo.toml. Bin-only crates have +# no public API surface, while `publish = false` crates have no registry +# consumer contract; both 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 @@ -2041,7 +2038,7 @@ anvil-external-types: anvil-external-types-validate-prereqs exit 0 } $pkg = @(if ($env:ANVIL_INCLUDE_AFFECTED) { -split $env:ANVIL_INCLUDE_AFFECTED } else { '--workspace' }) - # Build pkg-name -> manifest-path map, restricted to library crates. + # Build pkg-name -> manifest-path map, restricted to publishable library crates. $libPkgs = @{} $metadataJson = & cargo metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { @@ -2055,7 +2052,8 @@ anvil-external-types: anvil-external-types-validate-prereqs exit 1 } foreach ($p in $meta.packages) { - if ($p.targets | Where-Object { $_.kind -contains 'lib' }) { + if (($p.targets | Where-Object { $_.kind -contains 'lib' }) -and + -not ($p.publish -is [array] -and $p.publish.Count -eq 0)) { $libPkgs[$p.name] = $p.manifest_path } } @@ -2127,8 +2125,30 @@ anvil-external-types-validate-prereqs: anvil-toolchain-nightly-external-types-va anvil-fmt: anvil-fmt-validate-prereqs $ErrorActionPreference = 'Stop' if ($env:ANVIL_INCLUDE_MODIFIED -eq '--skip') { exit 0 } - cargo '+{{ rust_nightly }}' fmt --all --check - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + $metadataJson = & cargo metadata --no-deps --format-version 1 + if ($LASTEXITCODE -ne 0) { + Write-Error 'anvil-fmt: cargo metadata failed' + exit $LASTEXITCODE + } + try { + $metadata = $metadataJson | ConvertFrom-Json + } catch { + Write-Error "anvil-fmt: could not parse cargo metadata output: $_" + exit 1 + } + $workspaceMembers = @{} + foreach ($id in $metadata.workspace_members) { + $workspaceMembers[$id] = $true + } + $packages = @( + $metadata.packages | + Where-Object { $workspaceMembers.ContainsKey($_.id) } | + Sort-Object name, version + ) + foreach ($package in $packages) { + & cargo '+{{ rust_nightly }}' fmt --manifest-path $package.manifest_path --check + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + } # Per-check setup + validate-prereqs # From 0b8698dfb4b9c945b7af87cd5b106d301f5dc32b Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Wed, 26 Aug 2026 12:01:42 +0200 Subject: [PATCH 02/17] docs(anvil): clarify cargo-sort format scope Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/cargo-anvil/docs/design/checks.md | 2 +- .../templates/justfiles/anvil/checks/cargo-sort.just | 8 +++++--- .../tests/snapshots/snapshots__ado_backend.snap | 8 +++++--- .../tests/snapshots/snapshots__github_backend.snap | 8 +++++--- .../tests/snapshots/snapshots__local_only.snap | 8 +++++--- 5 files changed, 21 insertions(+), 13 deletions(-) diff --git a/crates/cargo-anvil/docs/design/checks.md b/crates/cargo-anvil/docs/design/checks.md index e63508c2..35362018 100644 --- a/crates/cargo-anvil/docs/design/checks.md +++ b/crates/cargo-anvil/docs/design/checks.md @@ -159,7 +159,7 @@ that provided the strongest version of the check. |--------------------------------|-----------------------------------------------------------|--------| | `fmt` | `cargo + fmt --manifest-path --check` for each workspace package. Manifest-scoped invocations avoid platform command-line limits, remain valid for nested packages with independent workspace membership, and need no separate fallback path. | all | | `clippy` | `cargo clippy --workspace --all-targets --all-features --locked -- -D warnings` | all | -| `cargo-sort` | `cargo sort --workspace --grouped --check --no-format`. Anvil checks dependency ordering but leaves general TOML formatting alone so formatter reordering cannot detach managed-region sentinels. | oxidizer-github | +| `cargo-sort` | `cargo sort --workspace --grouped --check --no-format`. Anvil owns dependency-order validation, not whole-manifest TOML style. In check mode `--check-format` is read-only but makes cargo-sort formatting differences fail, while omitting both format flags still emits formatting warnings; `--no-format` keeps the contract and diagnostics limited to ordering. | oxidizer-github | | `license-headers` | `cargo heather --workspace` | oxidizer (`heather`), oxidizer-github | | `ensure-no-cyclic-deps` | `cargo ensure-no-cyclic-deps --workspace` | oxidizer-github (sibling crate in `ox-tools-gh`) | | `ensure-no-default-features` | `cargo ensure-no-default-features --workspace` | oxidizer-github | 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 90b24156..dbd51bdb 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/cargo-sort.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/cargo-sort.just @@ -8,9 +8,11 @@ # Modified tier. # -# `--no-format` limits this check to dependency ordering. General TOML -# formatting can move tables and dotted keys independently of comments, -# detaching Anvil's managed-region sentinels from the content they delimit. +# `--no-format` limits this check to dependency ordering. In check mode, +# `--check-format` does not rewrite manifests, but it does make cargo-sort's +# general TOML style part of the pass/fail contract. Anvil does not own +# whole-manifest formatting, and running the formatter without +# `--check-format` would still emit formatting warnings. # # `--grouped` keeps the blank lines between groups of key/value pairs # (e.g. blank-line-separated dependency blocks) intact. diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap index 74018375..90742376 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap @@ -3033,9 +3033,11 @@ anvil-cargo-hack-validate-prereqs: anvil-tool-cargo-hack-validate-prereqs # Modified tier. # -# `--no-format` limits this check to dependency ordering. General TOML -# formatting can move tables and dotted keys independently of comments, -# detaching Anvil's managed-region sentinels from the content they delimit. +# `--no-format` limits this check to dependency ordering. In check mode, +# `--check-format` does not rewrite manifests, but it does make cargo-sort's +# general TOML style part of the pass/fail contract. Anvil does not own +# whole-manifest formatting, and running the formatter without +# `--check-format` would still emit formatting warnings. # # `--grouped` keeps the blank lines between groups of key/value pairs # (e.g. blank-line-separated dependency blocks) intact. diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap index 7a6c8d36..8088d897 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap @@ -2932,9 +2932,11 @@ anvil-cargo-hack-validate-prereqs: anvil-tool-cargo-hack-validate-prereqs # Modified tier. # -# `--no-format` limits this check to dependency ordering. General TOML -# formatting can move tables and dotted keys independently of comments, -# detaching Anvil's managed-region sentinels from the content they delimit. +# `--no-format` limits this check to dependency ordering. In check mode, +# `--check-format` does not rewrite manifests, but it does make cargo-sort's +# general TOML style part of the pass/fail contract. Anvil does not own +# whole-manifest formatting, and running the formatter without +# `--check-format` would still emit formatting warnings. # # `--grouped` keeps the blank lines between groups of key/value pairs # (e.g. blank-line-separated dependency blocks) intact. diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap index b3921356..b7d5da66 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap @@ -1773,9 +1773,11 @@ anvil-cargo-hack-validate-prereqs: anvil-tool-cargo-hack-validate-prereqs # Modified tier. # -# `--no-format` limits this check to dependency ordering. General TOML -# formatting can move tables and dotted keys independently of comments, -# detaching Anvil's managed-region sentinels from the content they delimit. +# `--no-format` limits this check to dependency ordering. In check mode, +# `--check-format` does not rewrite manifests, but it does make cargo-sort's +# general TOML style part of the pass/fail contract. Anvil does not own +# whole-manifest formatting, and running the formatter without +# `--check-format` would still emit formatting warnings. # # `--grouped` keeps the blank lines between groups of key/value pairs # (e.g. blank-line-separated dependency blocks) intact. From e4a64ed5b14d313c9b17006fa7c575eff26987c5 Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Wed, 26 Aug 2026 12:47:37 +0200 Subject: [PATCH 03/17] fix(anvil): retain cargo-sort formatting Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/cargo-anvil/docs/design/checks.md | 2 +- .../templates/justfiles/anvil/checks/cargo-sort.just | 9 +++------ .../tests/snapshots/snapshots__ado_backend.snap | 9 +++------ .../tests/snapshots/snapshots__github_backend.snap | 9 +++------ .../tests/snapshots/snapshots__local_only.snap | 9 +++------ 5 files changed, 13 insertions(+), 25 deletions(-) diff --git a/crates/cargo-anvil/docs/design/checks.md b/crates/cargo-anvil/docs/design/checks.md index 35362018..b6c5b16c 100644 --- a/crates/cargo-anvil/docs/design/checks.md +++ b/crates/cargo-anvil/docs/design/checks.md @@ -159,7 +159,7 @@ that provided the strongest version of the check. |--------------------------------|-----------------------------------------------------------|--------| | `fmt` | `cargo + fmt --manifest-path --check` for each workspace package. Manifest-scoped invocations avoid platform command-line limits, remain valid for nested packages with independent workspace membership, and need no separate fallback path. | all | | `clippy` | `cargo clippy --workspace --all-targets --all-features --locked -- -D warnings` | all | -| `cargo-sort` | `cargo sort --workspace --grouped --check --no-format`. Anvil owns dependency-order validation, not whole-manifest TOML style. In check mode `--check-format` is read-only but makes cargo-sort formatting differences fail, while omitting both format flags still emits formatting warnings; `--no-format` keeps the contract and diagnostics limited to ordering. | oxidizer-github | +| `cargo-sort` | `cargo sort --workspace --grouped --check --check-format`. Dependency ordering and Cargo manifest formatting are both enforced; `--grouped` preserves intentional blank-line-separated dependency groups. | oxidizer-github | | `license-headers` | `cargo heather --workspace` | oxidizer (`heather`), oxidizer-github | | `ensure-no-cyclic-deps` | `cargo ensure-no-cyclic-deps --workspace` | oxidizer-github (sibling crate in `ox-tools-gh`) | | `ensure-no-default-features` | `cargo ensure-no-default-features --workspace` | oxidizer-github | 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 dbd51bdb..65dee980 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/cargo-sort.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/cargo-sort.just @@ -8,11 +8,8 @@ # Modified tier. # -# `--no-format` limits this check to dependency ordering. In check mode, -# `--check-format` does not rewrite manifests, but it does make cargo-sort's -# general TOML style part of the pass/fail contract. Anvil does not own -# whole-manifest formatting, and running the formatter without -# `--check-format` would still emit formatting warnings. +# `--check-format` makes formatting differences fail instead of producing +# warnings, keeping dependency ordering and Cargo manifest style deterministic. # # `--grouped` keeps the blank lines between groups of key/value pairs # (e.g. blank-line-separated dependency blocks) intact. @@ -22,7 +19,7 @@ anvil-cargo-sort: anvil-cargo-sort-validate-prereqs $ErrorActionPreference = 'Stop' if ($env:ANVIL_INCLUDE_MODIFIED -eq '--skip') { exit 0 } - cargo sort --workspace --grouped --check --no-format + cargo sort --workspace --grouped --check --check-format if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-cargo-sort` recipe. diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap index 90742376..55d2c9c9 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap @@ -3033,11 +3033,8 @@ anvil-cargo-hack-validate-prereqs: anvil-tool-cargo-hack-validate-prereqs # Modified tier. # -# `--no-format` limits this check to dependency ordering. In check mode, -# `--check-format` does not rewrite manifests, but it does make cargo-sort's -# general TOML style part of the pass/fail contract. Anvil does not own -# whole-manifest formatting, and running the formatter without -# `--check-format` would still emit formatting warnings. +# `--check-format` makes formatting differences fail instead of producing +# warnings, keeping dependency ordering and Cargo manifest style deterministic. # # `--grouped` keeps the blank lines between groups of key/value pairs # (e.g. blank-line-separated dependency blocks) intact. @@ -3047,7 +3044,7 @@ anvil-cargo-hack-validate-prereqs: anvil-tool-cargo-hack-validate-prereqs anvil-cargo-sort: anvil-cargo-sort-validate-prereqs $ErrorActionPreference = 'Stop' if ($env:ANVIL_INCLUDE_MODIFIED -eq '--skip') { exit 0 } - cargo sort --workspace --grouped --check --no-format + cargo sort --workspace --grouped --check --check-format if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-cargo-sort` recipe. diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap index 8088d897..0c43fb54 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap @@ -2932,11 +2932,8 @@ anvil-cargo-hack-validate-prereqs: anvil-tool-cargo-hack-validate-prereqs # Modified tier. # -# `--no-format` limits this check to dependency ordering. In check mode, -# `--check-format` does not rewrite manifests, but it does make cargo-sort's -# general TOML style part of the pass/fail contract. Anvil does not own -# whole-manifest formatting, and running the formatter without -# `--check-format` would still emit formatting warnings. +# `--check-format` makes formatting differences fail instead of producing +# warnings, keeping dependency ordering and Cargo manifest style deterministic. # # `--grouped` keeps the blank lines between groups of key/value pairs # (e.g. blank-line-separated dependency blocks) intact. @@ -2946,7 +2943,7 @@ anvil-cargo-hack-validate-prereqs: anvil-tool-cargo-hack-validate-prereqs anvil-cargo-sort: anvil-cargo-sort-validate-prereqs $ErrorActionPreference = 'Stop' if ($env:ANVIL_INCLUDE_MODIFIED -eq '--skip') { exit 0 } - cargo sort --workspace --grouped --check --no-format + cargo sort --workspace --grouped --check --check-format if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-cargo-sort` recipe. diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap index b7d5da66..40171cd1 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap @@ -1773,11 +1773,8 @@ anvil-cargo-hack-validate-prereqs: anvil-tool-cargo-hack-validate-prereqs # Modified tier. # -# `--no-format` limits this check to dependency ordering. In check mode, -# `--check-format` does not rewrite manifests, but it does make cargo-sort's -# general TOML style part of the pass/fail contract. Anvil does not own -# whole-manifest formatting, and running the formatter without -# `--check-format` would still emit formatting warnings. +# `--check-format` makes formatting differences fail instead of producing +# warnings, keeping dependency ordering and Cargo manifest style deterministic. # # `--grouped` keeps the blank lines between groups of key/value pairs # (e.g. blank-line-separated dependency blocks) intact. @@ -1787,7 +1784,7 @@ anvil-cargo-hack-validate-prereqs: anvil-tool-cargo-hack-validate-prereqs anvil-cargo-sort: anvil-cargo-sort-validate-prereqs $ErrorActionPreference = 'Stop' if ($env:ANVIL_INCLUDE_MODIFIED -eq '--skip') { exit 0 } - cargo sort --workspace --grouped --check --no-format + cargo sort --workspace --grouped --check --check-format if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-cargo-sort` recipe. From 1429fd671fa2117c970fd954a9f2fb1dcda0336d Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Wed, 26 Aug 2026 13:16:11 +0200 Subject: [PATCH 04/17] refactor(anvil): use cargo-each for formatting Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/cargo-anvil/docs/design/checks.md | 2 +- crates/cargo-anvil/docs/design/local.md | 5 +- .../src/anvil/artifacts/justfile.rs | 1 + .../templates/justfiles/anvil/checks/fmt.just | 30 ++------- .../templates/justfiles/anvil/tools.just | 8 +++ .../templates/justfiles/anvil/versions.just | 1 + crates/cargo-anvil/tests/recipe_contracts.rs | 63 +++++-------------- .../snapshots/snapshots__ado_backend.snap | 39 ++++-------- .../snapshots/snapshots__github_backend.snap | 39 ++++-------- .../snapshots/snapshots__local_only.snap | 39 ++++-------- 10 files changed, 71 insertions(+), 156 deletions(-) diff --git a/crates/cargo-anvil/docs/design/checks.md b/crates/cargo-anvil/docs/design/checks.md index b6c5b16c..01cbd6f1 100644 --- a/crates/cargo-anvil/docs/design/checks.md +++ b/crates/cargo-anvil/docs/design/checks.md @@ -157,7 +157,7 @@ that provided the strongest version of the check. | Check | Invocation | Source | |--------------------------------|-----------------------------------------------------------|--------| -| `fmt` | `cargo + fmt --manifest-path --check` for each workspace package. Manifest-scoped invocations avoid platform command-line limits, remain valid for nested packages with independent workspace membership, and need no separate fallback path. | all | +| `fmt` | `cargo each --workspace -- cargo + fmt --manifest-path {manifest} --check`. `cargo-each` resolves workspace membership and invokes rustfmt once per manifest, keeping child commands bounded on every platform. | all | | `clippy` | `cargo clippy --workspace --all-targets --all-features --locked -- -D warnings` | all | | `cargo-sort` | `cargo sort --workspace --grouped --check --check-format`. Dependency ordering and Cargo manifest formatting are both enforced; `--grouped` preserves intentional blank-line-separated dependency groups. | oxidizer-github | | `license-headers` | `cargo heather --workspace` | oxidizer (`heather`), oxidizer-github | diff --git a/crates/cargo-anvil/docs/design/local.md b/crates/cargo-anvil/docs/design/local.md index 4899fc7c..e8b91ae5 100644 --- a/crates/cargo-anvil/docs/design/local.md +++ b/crates/cargo-anvil/docs/design/local.md @@ -464,9 +464,8 @@ anvil-fmt: @if [ "$ANVIL_INCLUDE_MODIFIED" = "--skip" ]; then \ echo "anvil-fmt: no modified packages; skipping"; exit 0; \ fi; \ - for manifest in $(cargo metadata ...); do \ - cargo fmt --manifest-path "$manifest" --check; \ - done + cargo each --workspace -- \ + cargo fmt --manifest-path '{manifest}' --check ``` The mapping from check to bucket is fixed in the catalog (see diff --git a/crates/cargo-anvil/src/anvil/artifacts/justfile.rs b/crates/cargo-anvil/src/anvil/artifacts/justfile.rs index 7b8d9d29..87bbd88d 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/justfile.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/justfile.rs @@ -446,6 +446,7 @@ mod tests { "cargo_nextest_version", "cargo_llvm_cov_version", "cargo_deny_version", + "cargo_each_version", "cargo_mutants_version", ] { assert!(VERSIONS_JUST.contains(needle), "versions.just missing variable '{needle}'"); diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/fmt.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/fmt.just index 15c51f5f..55d092b2 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/fmt.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/fmt.just @@ -18,30 +18,8 @@ anvil-fmt: anvil-fmt-validate-prereqs $ErrorActionPreference = 'Stop' if ($env:ANVIL_INCLUDE_MODIFIED -eq '--skip') { exit 0 } - $metadataJson = & cargo metadata --no-deps --format-version 1 - if ($LASTEXITCODE -ne 0) { - Write-Error 'anvil-fmt: cargo metadata failed' - exit $LASTEXITCODE - } - try { - $metadata = $metadataJson | ConvertFrom-Json - } catch { - Write-Error "anvil-fmt: could not parse cargo metadata output: $_" - exit 1 - } - $workspaceMembers = @{} - foreach ($id in $metadata.workspace_members) { - $workspaceMembers[$id] = $true - } - $packages = @( - $metadata.packages | - Where-Object { $workspaceMembers.ContainsKey($_.id) } | - Sort-Object name, version - ) - foreach ($package in $packages) { - & cargo '+{{ rust_nightly }}' fmt --manifest-path $package.manifest_path --check - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - } + cargo each --workspace '--' cargo '+{{ rust_nightly }}' fmt --manifest-path '{manifest}' --check + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Per-check setup + validate-prereqs # @@ -60,8 +38,8 @@ anvil-fmt: anvil-fmt-validate-prereqs # Install prerequisites for the `anvil-fmt` recipe. [group("anvil-setup")] -anvil-fmt-setup installer="install": anvil-component-nightly-rustfmt-install +anvil-fmt-setup installer="install": anvil-component-nightly-rustfmt-install (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-fmt` recipe. [group("anvil-setup")] -anvil-fmt-validate-prereqs: anvil-component-nightly-rustfmt-validate-prereqs +anvil-fmt-validate-prereqs: anvil-component-nightly-rustfmt-validate-prereqs anvil-tool-cargo-each-validate-prereqs diff --git a/crates/cargo-anvil/templates/justfiles/anvil/tools.just b/crates/cargo-anvil/templates/justfiles/anvil/tools.just index cb6aab14..d7062aa7 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/tools.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/tools.just @@ -631,6 +631,14 @@ anvil-tool-cargo-doc2readme-install installer="install": (_install-tool "cargo-d [group("anvil-setup")] anvil-tool-cargo-doc2readme-validate-prereqs: (_check-tool "cargo-doc2readme" cargo_doc2readme_version) +# Install the pinned `cargo-each` tool. +[group("anvil-setup")] +anvil-tool-cargo-each-install installer="install": (_install-tool "cargo-each" cargo_each_version installer) + +# Validate that the pinned `cargo-each` tool is available. +[group("anvil-setup")] +anvil-tool-cargo-each-validate-prereqs: (_check-tool "cargo-each" cargo_each_version) + # Install the pinned `cargo-ensure-no-cyclic-deps` tool. [group("anvil-setup")] anvil-tool-cargo-ensure-no-cyclic-deps-install installer="install": (_install-tool "cargo-ensure-no-cyclic-deps" cargo_ensure_no_cyclic_deps_version installer) diff --git a/crates/cargo-anvil/templates/justfiles/anvil/versions.just b/crates/cargo-anvil/templates/justfiles/anvil/versions.just index 564a3b90..32fd53cc 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/versions.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/versions.just @@ -58,6 +58,7 @@ cargo_coverage_gate_version := "0.2.0" cargo_delta_version := "0.3.1" cargo_deny_version := "0.19.8" cargo_doc2readme_version := "0.7.2" +cargo_each_version := "0.1.0" cargo_ensure_no_cyclic_deps_version := "0.2.0" cargo_ensure_no_default_features_version := "1.1.0" cargo_hack_version := "0.6.45" diff --git a/crates/cargo-anvil/tests/recipe_contracts.rs b/crates/cargo-anvil/tests/recipe_contracts.rs index 800146b5..d7493758 100644 --- a/crates/cargo-anvil/tests/recipe_contracts.rs +++ b/crates/cargo-anvil/tests/recipe_contracts.rs @@ -29,6 +29,9 @@ $joined = $args -join ' ' if ($env:FAKE_CARGO_LOG) { Add-Content -LiteralPath $env:FAKE_CARGO_LOG -Value $joined } +if ($args -contains 'each') { + exit [int]$env:FAKE_EACH_EXIT +} if ($args -contains 'metadata') { if ($env:FAKE_METADATA_EXIT) { exit [int]$env:FAKE_METADATA_EXIT } if ($env:FAKE_METADATA_INVALID) { @@ -623,7 +626,7 @@ fn public_api_checks_fail_when_metadata_discovery_fails() { } #[test] -fn fmt_formats_workspace_packages_individually() { +fn fmt_delegates_workspace_iteration_to_cargo_each() { if !tools_available() { return; } @@ -632,17 +635,12 @@ fn fmt_formats_workspace_packages_individually() { &[ "anvil-component-nightly-rustfmt-validate-prereqs", "anvil-component-nightly-rustfmt-install", + "anvil-tool-cargo-each-validate-prereqs", + "anvil-tool-cargo-each-install installer", ], ); let log = tmp.path().join("cargo.log"); - let output = run_just( - tmp.path(), - &["anvil-fmt"], - &[ - ("FAKE_CARGO_LOG", log.as_os_str()), - ("FAKE_SECOND_PACKAGE_NAME", OsStr::new("second")), - ], - ); + let output = run_just(tmp.path(), &["anvil-fmt"], &[("FAKE_CARGO_LOG", log.as_os_str())]); assert!( output.status.success(), "per-package formatting failed\nstdout:\n{}\nstderr:\n{}", @@ -650,46 +648,15 @@ fn fmt_formats_workspace_packages_individually() { String::from_utf8_lossy(&output.stderr) ); let commands = std::fs::read_to_string(&log).unwrap(); - assert!(commands.contains("metadata --no-deps --format-version 1")); - assert!(commands.contains(&format!( - "+nightly-test fmt --manifest-path {} --check", - tmp.path().join("Cargo.toml").display() - ))); - assert!(commands.contains(&format!( - "+nightly-test fmt --manifest-path {} --check", - tmp.path().join("nested").join("Cargo.toml").display() - ))); - assert!(!commands.contains("fmt --all")); - - std::fs::write(&log, "").unwrap(); - let output = run_just( - tmp.path(), - &["anvil-fmt"], - &[ - ("FAKE_CARGO_LOG", log.as_os_str()), - ("FAKE_SECOND_PACKAGE_NAME", OsStr::new("second")), - ("FAKE_SECOND_PACKAGE_NON_MEMBER", OsStr::new("1")), - ], - ); assert!( - output.status.success(), - "workspace-member filtering failed\nstdout:\n{}\nstderr:\n{}", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) + commands.contains("each --workspace -- cargo +nightly-test fmt --manifest-path {manifest} --check"), + "unexpected cargo invocation: {commands}" ); - let commands = std::fs::read_to_string(log).unwrap(); - assert!(commands.contains(&format!( - "+nightly-test fmt --manifest-path {} --check", - tmp.path().join("Cargo.toml").display() - ))); - assert!(!commands.contains(&format!( - "fmt --manifest-path {}", - tmp.path().join("nested").join("Cargo.toml").display() - ))); + assert!(!commands.contains("fmt --all")); } #[test] -fn fmt_fails_when_package_discovery_fails() { +fn fmt_propagates_cargo_each_failure() { if !tools_available() { return; } @@ -698,12 +665,12 @@ fn fmt_fails_when_package_discovery_fails() { &[ "anvil-component-nightly-rustfmt-validate-prereqs", "anvil-component-nightly-rustfmt-install", + "anvil-tool-cargo-each-validate-prereqs", + "anvil-tool-cargo-each-install installer", ], ); - for environment in [("FAKE_METADATA_EXIT", OsStr::new("23")), ("FAKE_METADATA_INVALID", OsStr::new("1"))] { - let output = run_just(tmp.path(), &["anvil-fmt"], &[environment]); - assert_failed(&output, "anvil-fmt package discovery failure"); - } + let output = run_just(tmp.path(), &["anvil-fmt"], &[("FAKE_EACH_EXIT", OsStr::new("23"))]); + assert_failed(&output, "anvil-fmt cargo-each failure"); } #[test] diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap index 55d2c9c9..82b2483a 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap @@ -3384,30 +3384,8 @@ anvil-external-types-validate-prereqs: anvil-toolchain-nightly-external-types-va anvil-fmt: anvil-fmt-validate-prereqs $ErrorActionPreference = 'Stop' if ($env:ANVIL_INCLUDE_MODIFIED -eq '--skip') { exit 0 } - $metadataJson = & cargo metadata --no-deps --format-version 1 - if ($LASTEXITCODE -ne 0) { - Write-Error 'anvil-fmt: cargo metadata failed' - exit $LASTEXITCODE - } - try { - $metadata = $metadataJson | ConvertFrom-Json - } catch { - Write-Error "anvil-fmt: could not parse cargo metadata output: $_" - exit 1 - } - $workspaceMembers = @{} - foreach ($id in $metadata.workspace_members) { - $workspaceMembers[$id] = $true - } - $packages = @( - $metadata.packages | - Where-Object { $workspaceMembers.ContainsKey($_.id) } | - Sort-Object name, version - ) - foreach ($package in $packages) { - & cargo '+{{ rust_nightly }}' fmt --manifest-path $package.manifest_path --check - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - } + cargo each --workspace '--' cargo '+{{ rust_nightly }}' fmt --manifest-path '{manifest}' --check + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Per-check setup + validate-prereqs # @@ -3426,11 +3404,11 @@ anvil-fmt: anvil-fmt-validate-prereqs # Install prerequisites for the `anvil-fmt` recipe. [group("anvil-setup")] -anvil-fmt-setup installer="install": anvil-component-nightly-rustfmt-install +anvil-fmt-setup installer="install": anvil-component-nightly-rustfmt-install (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-fmt` recipe. [group("anvil-setup")] -anvil-fmt-validate-prereqs: anvil-component-nightly-rustfmt-validate-prereqs +anvil-fmt-validate-prereqs: anvil-component-nightly-rustfmt-validate-prereqs anvil-tool-cargo-each-validate-prereqs === justfiles/anvil/checks/license-headers.just === # Copyright (c) Microsoft Corporation. @@ -6108,6 +6086,14 @@ anvil-tool-cargo-doc2readme-install installer="install": (_install-tool "cargo-d [group("anvil-setup")] anvil-tool-cargo-doc2readme-validate-prereqs: (_check-tool "cargo-doc2readme" cargo_doc2readme_version) +# Install the pinned `cargo-each` tool. +[group("anvil-setup")] +anvil-tool-cargo-each-install installer="install": (_install-tool "cargo-each" cargo_each_version installer) + +# Validate that the pinned `cargo-each` tool is available. +[group("anvil-setup")] +anvil-tool-cargo-each-validate-prereqs: (_check-tool "cargo-each" cargo_each_version) + # Install the pinned `cargo-ensure-no-cyclic-deps` tool. [group("anvil-setup")] anvil-tool-cargo-ensure-no-cyclic-deps-install installer="install": (_install-tool "cargo-ensure-no-cyclic-deps" cargo_ensure_no_cyclic_deps_version installer) @@ -6278,6 +6264,7 @@ cargo_coverage_gate_version := "0.2.0" cargo_delta_version := "0.3.1" cargo_deny_version := "0.19.8" cargo_doc2readme_version := "0.7.2" +cargo_each_version := "0.1.0" cargo_ensure_no_cyclic_deps_version := "0.2.0" cargo_ensure_no_default_features_version := "1.1.0" cargo_hack_version := "0.6.45" diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap index 0c43fb54..d122a7bf 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap @@ -3283,30 +3283,8 @@ anvil-external-types-validate-prereqs: anvil-toolchain-nightly-external-types-va anvil-fmt: anvil-fmt-validate-prereqs $ErrorActionPreference = 'Stop' if ($env:ANVIL_INCLUDE_MODIFIED -eq '--skip') { exit 0 } - $metadataJson = & cargo metadata --no-deps --format-version 1 - if ($LASTEXITCODE -ne 0) { - Write-Error 'anvil-fmt: cargo metadata failed' - exit $LASTEXITCODE - } - try { - $metadata = $metadataJson | ConvertFrom-Json - } catch { - Write-Error "anvil-fmt: could not parse cargo metadata output: $_" - exit 1 - } - $workspaceMembers = @{} - foreach ($id in $metadata.workspace_members) { - $workspaceMembers[$id] = $true - } - $packages = @( - $metadata.packages | - Where-Object { $workspaceMembers.ContainsKey($_.id) } | - Sort-Object name, version - ) - foreach ($package in $packages) { - & cargo '+{{ rust_nightly }}' fmt --manifest-path $package.manifest_path --check - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - } + cargo each --workspace '--' cargo '+{{ rust_nightly }}' fmt --manifest-path '{manifest}' --check + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Per-check setup + validate-prereqs # @@ -3325,11 +3303,11 @@ anvil-fmt: anvil-fmt-validate-prereqs # Install prerequisites for the `anvil-fmt` recipe. [group("anvil-setup")] -anvil-fmt-setup installer="install": anvil-component-nightly-rustfmt-install +anvil-fmt-setup installer="install": anvil-component-nightly-rustfmt-install (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-fmt` recipe. [group("anvil-setup")] -anvil-fmt-validate-prereqs: anvil-component-nightly-rustfmt-validate-prereqs +anvil-fmt-validate-prereqs: anvil-component-nightly-rustfmt-validate-prereqs anvil-tool-cargo-each-validate-prereqs === justfiles/anvil/checks/license-headers.just === # Copyright (c) Microsoft Corporation. @@ -6007,6 +5985,14 @@ anvil-tool-cargo-doc2readme-install installer="install": (_install-tool "cargo-d [group("anvil-setup")] anvil-tool-cargo-doc2readme-validate-prereqs: (_check-tool "cargo-doc2readme" cargo_doc2readme_version) +# Install the pinned `cargo-each` tool. +[group("anvil-setup")] +anvil-tool-cargo-each-install installer="install": (_install-tool "cargo-each" cargo_each_version installer) + +# Validate that the pinned `cargo-each` tool is available. +[group("anvil-setup")] +anvil-tool-cargo-each-validate-prereqs: (_check-tool "cargo-each" cargo_each_version) + # Install the pinned `cargo-ensure-no-cyclic-deps` tool. [group("anvil-setup")] anvil-tool-cargo-ensure-no-cyclic-deps-install installer="install": (_install-tool "cargo-ensure-no-cyclic-deps" cargo_ensure_no_cyclic_deps_version installer) @@ -6177,6 +6163,7 @@ cargo_coverage_gate_version := "0.2.0" cargo_delta_version := "0.3.1" cargo_deny_version := "0.19.8" cargo_doc2readme_version := "0.7.2" +cargo_each_version := "0.1.0" cargo_ensure_no_cyclic_deps_version := "0.2.0" cargo_ensure_no_default_features_version := "1.1.0" cargo_hack_version := "0.6.45" diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap index 40171cd1..5e19cbac 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap @@ -2124,30 +2124,8 @@ anvil-external-types-validate-prereqs: anvil-toolchain-nightly-external-types-va anvil-fmt: anvil-fmt-validate-prereqs $ErrorActionPreference = 'Stop' if ($env:ANVIL_INCLUDE_MODIFIED -eq '--skip') { exit 0 } - $metadataJson = & cargo metadata --no-deps --format-version 1 - if ($LASTEXITCODE -ne 0) { - Write-Error 'anvil-fmt: cargo metadata failed' - exit $LASTEXITCODE - } - try { - $metadata = $metadataJson | ConvertFrom-Json - } catch { - Write-Error "anvil-fmt: could not parse cargo metadata output: $_" - exit 1 - } - $workspaceMembers = @{} - foreach ($id in $metadata.workspace_members) { - $workspaceMembers[$id] = $true - } - $packages = @( - $metadata.packages | - Where-Object { $workspaceMembers.ContainsKey($_.id) } | - Sort-Object name, version - ) - foreach ($package in $packages) { - & cargo '+{{ rust_nightly }}' fmt --manifest-path $package.manifest_path --check - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - } + cargo each --workspace '--' cargo '+{{ rust_nightly }}' fmt --manifest-path '{manifest}' --check + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Per-check setup + validate-prereqs # @@ -2166,11 +2144,11 @@ anvil-fmt: anvil-fmt-validate-prereqs # Install prerequisites for the `anvil-fmt` recipe. [group("anvil-setup")] -anvil-fmt-setup installer="install": anvil-component-nightly-rustfmt-install +anvil-fmt-setup installer="install": anvil-component-nightly-rustfmt-install (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-fmt` recipe. [group("anvil-setup")] -anvil-fmt-validate-prereqs: anvil-component-nightly-rustfmt-validate-prereqs +anvil-fmt-validate-prereqs: anvil-component-nightly-rustfmt-validate-prereqs anvil-tool-cargo-each-validate-prereqs === justfiles/anvil/checks/license-headers.just === # Copyright (c) Microsoft Corporation. @@ -4848,6 +4826,14 @@ anvil-tool-cargo-doc2readme-install installer="install": (_install-tool "cargo-d [group("anvil-setup")] anvil-tool-cargo-doc2readme-validate-prereqs: (_check-tool "cargo-doc2readme" cargo_doc2readme_version) +# Install the pinned `cargo-each` tool. +[group("anvil-setup")] +anvil-tool-cargo-each-install installer="install": (_install-tool "cargo-each" cargo_each_version installer) + +# Validate that the pinned `cargo-each` tool is available. +[group("anvil-setup")] +anvil-tool-cargo-each-validate-prereqs: (_check-tool "cargo-each" cargo_each_version) + # Install the pinned `cargo-ensure-no-cyclic-deps` tool. [group("anvil-setup")] anvil-tool-cargo-ensure-no-cyclic-deps-install installer="install": (_install-tool "cargo-ensure-no-cyclic-deps" cargo_ensure_no_cyclic_deps_version installer) @@ -5018,6 +5004,7 @@ cargo_coverage_gate_version := "0.2.0" cargo_delta_version := "0.3.1" cargo_deny_version := "0.19.8" cargo_doc2readme_version := "0.7.2" +cargo_each_version := "0.1.0" cargo_ensure_no_cyclic_deps_version := "0.2.0" cargo_ensure_no_default_features_version := "1.1.0" cargo_hack_version := "0.6.45" From 3fdad6f36d91f466331868a160315eb9847e2f43 Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Wed, 26 Aug 2026 16:05:15 +0200 Subject: [PATCH 05/17] chore(anvil): regenerate repository state Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .anvil.lock | 12 ++++++------ justfiles/anvil/checks/cargo-sort.just | 11 +++-------- justfiles/anvil/checks/external-types.just | 12 +++++++----- justfiles/anvil/checks/fmt.just | 6 +++--- justfiles/anvil/tools.just | 8 ++++++++ justfiles/anvil/versions.just | 1 + 6 files changed, 28 insertions(+), 22 deletions(-) diff --git a/.anvil.lock b/.anvil.lock index e3aa0ecd..b41d722a 100644 --- a/.anvil.lock +++ b/.anvil.lock @@ -1,7 +1,7 @@ version = 1 tool = "anvil" tool_version = "0.5.0" -catalog_checksum = "sha256:64436c7a20508d0413fa5659e80cd5afe3d272096be6e892c46c33b169af5d0c" +catalog_checksum = "sha256:d8b29120429d9e60e7fdcd2a0833ce3acc4217d81805b9545014f4dad4102ae7" [[file]] path = ".anvil/container/Containerfile" @@ -97,7 +97,7 @@ checksum = "sha256:3e0f0ab3826c4d259fc0bee8465cff17ab390796b584a3b6500f2ef606cfe [[file]] path = "justfiles/anvil/checks/cargo-sort.just" -checksum = "sha256:7b5c824fe4ac01d317692d971d314cee1eeb9bdc957d73df990edebb1eace94f" +checksum = "sha256:bb80567fbe3c9c082b73992971931a0316154aafce703252f2eb0cfb5a19ec27" [[file]] path = "justfiles/anvil/checks/clippy.just" @@ -129,11 +129,11 @@ checksum = "sha256:fd34ede436d8f990f16ef3f5c42e99324e62b39412c199c01501c868a1523 [[file]] path = "justfiles/anvil/checks/external-types.just" -checksum = "sha256:df84846c336b5bcafff2a13bd2c02cb8eb571962dd29c492cf725f31f0439f46" +checksum = "sha256:8b906cc383b812ac1a3a14a2450854d8c9b44860a8ab9b6e712c41c644aad794" [[file]] path = "justfiles/anvil/checks/fmt.just" -checksum = "sha256:fc2cfea98e5a31348d4fafc47bf020928c525aa9962523aded8882dde475bb39" +checksum = "sha256:735a444aa74023f2c02fdc9abca09d35e1c35b3b9309bf9f3ddbc20303aa99b0" [[file]] path = "justfiles/anvil/checks/license-headers.just" @@ -249,11 +249,11 @@ checksum = "sha256:713c5a2ae28b6b5aa20dd38226278b3b7f71bbc5e6b84a16eaf5244713f27 [[file]] path = "justfiles/anvil/tools.just" -checksum = "sha256:a1e44ca16f172b487afa3997f102512733d3b65a4418cf894cbd749a3abc17dc" +checksum = "sha256:c1f5181d14a734cde8ccc32bbf5acece485791573b7a95cfff61b63d0f1fcb97" [[file]] path = "justfiles/anvil/versions.just" -checksum = "sha256:acbea93d5117db747537f4f7b9a5eb90b7d3e0dd3e8684cc0e4dc1dcb15ac93e" +checksum = "sha256:699fe1053c28257d33516de880caf3224b23365f33dd8c9885c9bb96a0e3f94c" [[region]] host = ".delta.toml" diff --git a/justfiles/anvil/checks/cargo-sort.just b/justfiles/anvil/checks/cargo-sort.just index 0d6315fc..65dee980 100644 --- a/justfiles/anvil/checks/cargo-sort.just +++ b/justfiles/anvil/checks/cargo-sort.just @@ -8,16 +8,11 @@ # Modified tier. # -# `--check-format` makes the formatting diff fail the check, not just the -# sort order. cargo-sort 2.1.2+ downgraded formatting-only diffs to -# warnings by default (PR #129); without this flag cargo-sort 2.1.2+ -# would pass on formatting drift. The flag preserves the intended -# check behavior across the supported range. +# `--check-format` makes formatting differences fail instead of producing +# warnings, keeping dependency ordering and Cargo manifest style deterministic. # # `--grouped` keeps the blank lines between groups of key/value pairs -# (e.g. blank-line-separated dependency blocks) intact, matching -# oxidizer-github's convention; without it cargo-sort would flag those -# blank lines as a formatting diff and fail the check. +# (e.g. blank-line-separated dependency blocks) intact. # Check that Cargo.toml dependency tables are sorted. [script("pwsh", "-NoProfile")] diff --git a/justfiles/anvil/checks/external-types.just b/justfiles/anvil/checks/external-types.just index b78320ca..4502994f 100644 --- a/justfiles/anvil/checks/external-types.just +++ b/justfiles/anvil/checks/external-types.just @@ -9,9 +9,10 @@ # Affected tier (lints public API of changed crates and rev-deps). # # cargo-check-external-types is per-manifest: no --package/--workspace, -# 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. +# only --manifest-path. Iterate the affected publishable library crates and run +# the tool once each, pointing at the crate's Cargo.toml. Bin-only crates have +# no public API surface, while `publish = false` crates have no registry +# consumer contract; both 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 @@ -28,7 +29,7 @@ anvil-external-types: anvil-external-types-validate-prereqs exit 0 } $pkg = @(if ($env:ANVIL_INCLUDE_AFFECTED) { -split $env:ANVIL_INCLUDE_AFFECTED } else { '--workspace' }) - # Build pkg-name -> manifest-path map, restricted to library crates. + # Build pkg-name -> manifest-path map, restricted to publishable library crates. $libPkgs = @{} $metadataJson = & cargo metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { @@ -42,7 +43,8 @@ anvil-external-types: anvil-external-types-validate-prereqs exit 1 } foreach ($p in $meta.packages) { - if ($p.targets | Where-Object { $_.kind -contains 'lib' }) { + if (($p.targets | Where-Object { $_.kind -contains 'lib' }) -and + -not ($p.publish -is [array] -and $p.publish.Count -eq 0)) { $libPkgs[$p.name] = $p.manifest_path } } diff --git a/justfiles/anvil/checks/fmt.just b/justfiles/anvil/checks/fmt.just index 2177dd2f..55d092b2 100644 --- a/justfiles/anvil/checks/fmt.just +++ b/justfiles/anvil/checks/fmt.just @@ -18,7 +18,7 @@ anvil-fmt: anvil-fmt-validate-prereqs $ErrorActionPreference = 'Stop' if ($env:ANVIL_INCLUDE_MODIFIED -eq '--skip') { exit 0 } - cargo '+{{ rust_nightly }}' fmt --all --check + cargo each --workspace '--' cargo '+{{ rust_nightly }}' fmt --manifest-path '{manifest}' --check if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Per-check setup + validate-prereqs @@ -38,8 +38,8 @@ anvil-fmt: anvil-fmt-validate-prereqs # Install prerequisites for the `anvil-fmt` recipe. [group("anvil-setup")] -anvil-fmt-setup installer="install": anvil-component-nightly-rustfmt-install +anvil-fmt-setup installer="install": anvil-component-nightly-rustfmt-install (anvil-tool-cargo-each-install installer) # Validate prerequisites for the `anvil-fmt` recipe. [group("anvil-setup")] -anvil-fmt-validate-prereqs: anvil-component-nightly-rustfmt-validate-prereqs +anvil-fmt-validate-prereqs: anvil-component-nightly-rustfmt-validate-prereqs anvil-tool-cargo-each-validate-prereqs diff --git a/justfiles/anvil/tools.just b/justfiles/anvil/tools.just index cb6aab14..d7062aa7 100644 --- a/justfiles/anvil/tools.just +++ b/justfiles/anvil/tools.just @@ -631,6 +631,14 @@ anvil-tool-cargo-doc2readme-install installer="install": (_install-tool "cargo-d [group("anvil-setup")] anvil-tool-cargo-doc2readme-validate-prereqs: (_check-tool "cargo-doc2readme" cargo_doc2readme_version) +# Install the pinned `cargo-each` tool. +[group("anvil-setup")] +anvil-tool-cargo-each-install installer="install": (_install-tool "cargo-each" cargo_each_version installer) + +# Validate that the pinned `cargo-each` tool is available. +[group("anvil-setup")] +anvil-tool-cargo-each-validate-prereqs: (_check-tool "cargo-each" cargo_each_version) + # Install the pinned `cargo-ensure-no-cyclic-deps` tool. [group("anvil-setup")] anvil-tool-cargo-ensure-no-cyclic-deps-install installer="install": (_install-tool "cargo-ensure-no-cyclic-deps" cargo_ensure_no_cyclic_deps_version installer) diff --git a/justfiles/anvil/versions.just b/justfiles/anvil/versions.just index 564a3b90..32fd53cc 100644 --- a/justfiles/anvil/versions.just +++ b/justfiles/anvil/versions.just @@ -58,6 +58,7 @@ cargo_coverage_gate_version := "0.2.0" cargo_delta_version := "0.3.1" cargo_deny_version := "0.19.8" cargo_doc2readme_version := "0.7.2" +cargo_each_version := "0.1.0" cargo_ensure_no_cyclic_deps_version := "0.2.0" cargo_ensure_no_default_features_version := "1.1.0" cargo_hack_version := "0.6.45" From f6d8a24eb745fd444aae3c9d3b122f1d18abb0f4 Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Thu, 27 Aug 2026 19:21:42 +0200 Subject: [PATCH 06/17] fix(anvil): address workspace check review Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4112f005-775f-4c4b-88ed-db8f0c5d1cf1 --- .anvil.lock | 12 +-- crates/cargo-anvil/docs/design/checks.md | 8 +- crates/cargo-anvil/docs/design/local.md | 9 +- .../justfiles/anvil/checks/cargo-sort.just | 5 +- .../anvil/checks/external-types.just | 24 ++++- .../templates/justfiles/anvil/checks/fmt.just | 6 +- .../justfiles/anvil/checks/semver-check.just | 9 +- .../templates/justfiles/anvil/helpers.just | 14 +-- crates/cargo-anvil/tests/recipe_contracts.rs | 91 ++++++++++++++++--- .../snapshots/snapshots__ado_backend.snap | 58 ++++++++---- .../snapshots/snapshots__github_backend.snap | 58 ++++++++---- .../snapshots/snapshots__local_only.snap | 58 ++++++++---- justfiles/anvil/checks/cargo-sort.just | 5 +- justfiles/anvil/checks/external-types.just | 24 ++++- justfiles/anvil/checks/fmt.just | 6 +- justfiles/anvil/checks/semver-check.just | 9 +- justfiles/anvil/helpers.just | 14 +-- 17 files changed, 289 insertions(+), 121 deletions(-) diff --git a/.anvil.lock b/.anvil.lock index b41d722a..710247f9 100644 --- a/.anvil.lock +++ b/.anvil.lock @@ -1,7 +1,7 @@ version = 1 tool = "anvil" tool_version = "0.5.0" -catalog_checksum = "sha256:d8b29120429d9e60e7fdcd2a0833ce3acc4217d81805b9545014f4dad4102ae7" +catalog_checksum = "sha256:97099270927c3a7f1aa8e7362a116a1fb19e3992d3a3160e70482e0c947206d6" [[file]] path = ".anvil/container/Containerfile" @@ -97,7 +97,7 @@ checksum = "sha256:3e0f0ab3826c4d259fc0bee8465cff17ab390796b584a3b6500f2ef606cfe [[file]] path = "justfiles/anvil/checks/cargo-sort.just" -checksum = "sha256:bb80567fbe3c9c082b73992971931a0316154aafce703252f2eb0cfb5a19ec27" +checksum = "sha256:0ecfb1cfb5109495fbceb36b47845113f7530dc12db422ddf2972698cef409b9" [[file]] path = "justfiles/anvil/checks/clippy.just" @@ -129,11 +129,11 @@ checksum = "sha256:fd34ede436d8f990f16ef3f5c42e99324e62b39412c199c01501c868a1523 [[file]] path = "justfiles/anvil/checks/external-types.just" -checksum = "sha256:8b906cc383b812ac1a3a14a2450854d8c9b44860a8ab9b6e712c41c644aad794" +checksum = "sha256:40bd7af54f9e828cfd7846568439d6157cd50d588b7172398e854de198300330" [[file]] path = "justfiles/anvil/checks/fmt.just" -checksum = "sha256:735a444aa74023f2c02fdc9abca09d35e1c35b3b9309bf9f3ddbc20303aa99b0" +checksum = "sha256:f46d8205e9747e283040aa85a56fce6bf71d2270c83a3c6c40a20f59af470191" [[file]] path = "justfiles/anvil/checks/license-headers.just" @@ -181,7 +181,7 @@ checksum = "sha256:fb395f5413de48848f10ab3e4069599c0093a790cb31224776ba41f90c8d5 [[file]] path = "justfiles/anvil/checks/semver-check.just" -checksum = "sha256:b4376371bcf2bbb0563a74fb38adebd108c6c7ae9247ae3b9f5877ff1becd8f8" +checksum = "sha256:22f72ad1180f46aca346ecc349b57ca46c1337cacb05463e29b0c78a96892137" [[file]] path = "justfiles/anvil/checks/spellcheck.just" @@ -233,7 +233,7 @@ checksum = "sha256:f77629bbc1c73a849d9e2dada2eb5d01a8d3dcade51035c76c1061f513cf5 [[file]] path = "justfiles/anvil/helpers.just" -checksum = "sha256:cf6b30b8f4fd10eeb5bb5660c8e60512434fd94405a4fd8ea7399ababa089d3f" +checksum = "sha256:ce8ca656cc35c6b78e676971d874e267ada82311acfcfa0f8df302e535a8ff62" [[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 01cbd6f1..3dea3732 100644 --- a/crates/cargo-anvil/docs/design/checks.md +++ b/crates/cargo-anvil/docs/design/checks.md @@ -157,9 +157,9 @@ that provided the strongest version of the check. | Check | Invocation | Source | |--------------------------------|-----------------------------------------------------------|--------| -| `fmt` | `cargo each --workspace -- cargo + fmt --manifest-path {manifest} --check`. `cargo-each` resolves workspace membership and invokes rustfmt once per manifest, keeping child commands bounded on every platform. | all | +| `fmt` | `cargo each --workspace --keep-going -- cargo + fmt --manifest-path {manifest} --check`. `cargo-each` resolves workspace membership and invokes rustfmt once per manifest, keeping child commands bounded on every platform while reporting every failing member. Unlike `cargo fmt --all`, local path dependencies outside the workspace are not included. | all | | `clippy` | `cargo clippy --workspace --all-targets --all-features --locked -- -D warnings` | all | -| `cargo-sort` | `cargo sort --workspace --grouped --check --check-format`. Dependency ordering and Cargo manifest formatting are both enforced; `--grouped` preserves intentional blank-line-separated dependency groups. | oxidizer-github | +| `cargo-sort` | `cargo sort --workspace --grouped --check --check-format`. Since cargo-sort 2.1.2, formatting-only differences are warnings unless `--check-format` is set; Anvil keeps it load-bearing so dependency ordering and Cargo manifest formatting are both enforced. `--grouped` preserves intentional blank-line-separated dependency groups. | oxidizer-github | | `license-headers` | `cargo heather --workspace` | oxidizer (`heather`), oxidizer-github | | `ensure-no-cyclic-deps` | `cargo ensure-no-cyclic-deps --workspace` | oxidizer-github (sibling crate in `ox-tools-gh`) | | `ensure-no-default-features` | `cargo ensure-no-default-features --workspace` | oxidizer-github | @@ -170,7 +170,7 @@ that provided the strongest version of the check. | `deny` | `cargo deny check` | all | | `audit` | `cargo audit` | oxidizer | | `udeps` | `cargo + udeps --workspace --all-features` run **twice** — once with default targets (lib + bins) and once with `--all-targets`. cargo-udeps only analyzes the targets it's told to, and each run catches a variant the other masks: the default-targets run surfaces a dep in `[dependencies]` referenced only by tests/benches/examples (it should be a dev-dep; `--all-targets` would see it as "used"), while the `--all-targets` run surfaces unused `[dev-dependencies]` (never compiled by the default-targets run). Together they cover unused deps, unused dev-deps, and deps that should be dev-deps. | oxidizer, oxidizer-github | -| `semver-check` | `cargo semver-checks --baseline-rev ` per affected library crate. The PR target is the baseline. Exit 100 is a completed check with deny-level findings; exit 101 or another nonzero status means the comparison was inconclusive. Both outcomes write `target/anvil/comments/semver.md` and remain advisory, matching the repository's native `semver` job (`continue-on-error: true`). Proven rename and bin→lib transitions with no comparable baseline are skipped without a comment. Anvil preflight failures such as invalid current-workspace metadata or an unavailable baseline ref still fail because the recipe cannot establish what to compare. | oxidizer-github | +| `semver-check` | `cargo semver-checks --baseline-rev ` per affected publishable library crate. Private and bin-only crates have no registry consumer contract and are skipped. The PR target is the baseline. Exit 100 is a completed check with deny-level findings; exit 101 or another nonzero status means the comparison was inconclusive. Both outcomes write `target/anvil/comments/semver.md` and remain advisory, matching the repository's native `semver` job (`continue-on-error: true`). Proven rename and bin→lib transitions with no comparable baseline are skipped without a comment. Anvil preflight failures such as invalid current-workspace metadata or an unavailable baseline ref still fail because the recipe cannot establish what to compare. | oxidizer-github | | `external-types` | `cargo + check-external-types --manifest-path` per publishable library crate (per-manifest because the tool has no `--workspace`/`--package`; bin-only and `publish = false` crates have no registry consumer contract and are skipped). Setup installs the catalog version but validation accepts newer installed tools. The selected nightly is tested with the catalog version; an incompatible newer tool fails closed with a tool/nightly compatibility diagnostic rather than silently selecting a different schema. | oxidizer-github | ### `pr-slow` @@ -335,7 +335,7 @@ Each catalog check is tagged with one of four buckets: | Bucket | Env var consumed | Behavior in cloud workflows | Behavior locally (env unset) | |-----------|-------------------------------|-----------------------------------------------------------------------------|--------------------------------------| -| modified | `ANVIL_INCLUDE_MODIFIED` | If `--skip`: exit 0. Otherwise run unconditionally (tool is workspace-wide). | Run unconditionally. | +| modified | `ANVIL_INCLUDE_MODIFIED` | If `--skip`: exit 0. Otherwise run the check's complete selected scope without package splicing. | Run the complete selected scope. | | affected | `ANVIL_INCLUDE_AFFECTED` | If `--skip`: exit 0. Otherwise splice the value into the cargo invocation. | Default to `--workspace`. | | required | `ANVIL_INCLUDE_REQUIRED` | If `--skip`: exit 0. Otherwise splice the value into the cargo invocation. | Default to `--workspace`. | | unscoped | *(none)* | Always run. | Always run. | diff --git a/crates/cargo-anvil/docs/design/local.md b/crates/cargo-anvil/docs/design/local.md index e8b91ae5..8d495c09 100644 --- a/crates/cargo-anvil/docs/design/local.md +++ b/crates/cargo-anvil/docs/design/local.md @@ -436,7 +436,7 @@ such env vars, one per cargo-delta tier: | Env var | Bucket | What recipes do with it | |------------------------------|-----------|------------------------------------------------------------------------------------------------| -| `ANVIL_INCLUDE_MODIFIED` | modified | `--skip` → recipe exits 0. Otherwise: run unconditionally (modified-tier tools are workspace-wide). | +| `ANVIL_INCLUDE_MODIFIED` | modified | `--skip` → recipe exits 0. Otherwise: run the check's complete selected scope without splicing package arguments. | | `ANVIL_INCLUDE_AFFECTED` | affected | `--skip` → recipe exits 0. Otherwise: splice the value into the cargo invocation, defaulting to `--workspace` when unset. | | `ANVIL_INCLUDE_REQUIRED` | required | Same semantics as `ANVIL_INCLUDE_AFFECTED`, but consumed by recipes that need transitive dep graph in scope (doc-build, cargo-hack, udeps). | @@ -456,15 +456,16 @@ anvil-clippy: cargo clippy ${ANVIL_INCLUDE_AFFECTED:---workspace} --all-targets --all-features --locked -- -D warnings ``` -A typical modified-tier recipe checks the whole workspace while keeping each -rustfmt child command bounded: +A typical modified-tier recipe checks every workspace member while keeping each +rustfmt child command bounded. Unlike `cargo fmt --all`, this intentionally does +not discover non-member local path dependencies: ```just anvil-fmt: @if [ "$ANVIL_INCLUDE_MODIFIED" = "--skip" ]; then \ echo "anvil-fmt: no modified packages; skipping"; exit 0; \ fi; \ - cargo each --workspace -- \ + cargo each --workspace --keep-going -- \ cargo fmt --manifest-path '{manifest}' --check ``` 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 65dee980..dfe0f1dd 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/cargo-sort.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/cargo-sort.just @@ -8,8 +8,9 @@ # Modified tier. # -# `--check-format` makes formatting differences fail instead of producing -# warnings, keeping dependency ordering and Cargo manifest style deterministic. +# cargo-sort 2.1.2 and newer report formatting-only differences as warnings +# unless `--check-format` is set. Keep it enabled so dependency ordering and +# Cargo manifest style are both enforced. # # `--grouped` keeps the blank lines between groups of key/value pairs # (e.g. blank-line-separated dependency blocks) intact. 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 4502994f..1c0b1b79 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/external-types.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/external-types.just @@ -29,8 +29,9 @@ anvil-external-types: anvil-external-types-validate-prereqs exit 0 } $pkg = @(if ($env:ANVIL_INCLUDE_AFFECTED) { -split $env:ANVIL_INCLUDE_AFFECTED } else { '--workspace' }) - # Build pkg-name -> manifest-path map, restricted to publishable library crates. + # Build pkg-name -> manifest-path maps for publishable and private libraries. $libPkgs = @{} + $privateLibPkgs = @{} $metadataJson = & cargo metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-external-types: cargo metadata failed' @@ -43,15 +44,20 @@ anvil-external-types: anvil-external-types-validate-prereqs exit 1 } foreach ($p in $meta.packages) { - if (($p.targets | Where-Object { $_.kind -contains 'lib' }) -and - -not ($p.publish -is [array] -and $p.publish.Count -eq 0)) { - $libPkgs[$p.name] = $p.manifest_path + if ($p.targets | Where-Object { $_.kind -contains 'lib' }) { + if ($p.publish -is [array] -and $p.publish.Count -eq 0) { + $privateLibPkgs[$p.name] = $p.manifest_path + } else { + $libPkgs[$p.name] = $p.manifest_path + } } } # Decide which packages to check. $packages = @() + $privatePackages = @() if ($pkg -contains '--workspace') { $packages = $libPkgs.Keys + $privatePackages = $privateLibPkgs.Keys } else { for ($i = 0; $i -lt $pkg.Count; $i++) { if ($pkg[$i] -eq '--package' -and ($i + 1) -lt $pkg.Count) { @@ -60,14 +66,22 @@ anvil-external-types: anvil-external-types-validate-prereqs # this manifest-path map keys on. $name = ($pkg[$i + 1] -split '@', 2)[0] if ($libPkgs.ContainsKey($name)) { $packages += $name } + if ($privateLibPkgs.ContainsKey($name)) { $privatePackages += $name } $i++ } } } if ($packages.Count -eq 0) { - Write-Host 'anvil-external-types: no affected library crates; skipping' + if ($privatePackages.Count -gt 0) { + Write-Host "anvil-external-types: no affected publishable library crates; skipping ($($privatePackages.Count) non-publishable library crate(s) excluded)" + } else { + Write-Host 'anvil-external-types: no affected library crates; skipping' + } exit 0 } + if ($privatePackages.Count -gt 0) { + Write-Host "anvil-external-types: excluding $($privatePackages.Count) non-publishable library crate(s)" + } 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) { diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/fmt.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/fmt.just index 55d092b2..7b9e448e 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/fmt.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/fmt.just @@ -12,13 +12,17 @@ # format_code_in_doc_comments). Floating nightly would mean # format-drift breaking cloud workflows on rustup updates — the same trap we # explicitly avoid for udeps/miri/careful/external-types. +# +# Iterate workspace members rather than every local path dependency that +# `cargo fmt --all` discovers. `--keep-going` reports formatting failures from +# every member while retaining a bounded rustfmt command line per invocation. # Check Rust source formatting. [script("pwsh", "-NoProfile")] anvil-fmt: anvil-fmt-validate-prereqs $ErrorActionPreference = 'Stop' if ($env:ANVIL_INCLUDE_MODIFIED -eq '--skip') { exit 0 } - cargo each --workspace '--' cargo '+{{ rust_nightly }}' fmt --manifest-path '{manifest}' --check + cargo 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/semver-check.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/semver-check.just index faa150ca..ec930579 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/semver-check.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/semver-check.just @@ -55,14 +55,14 @@ anvil-semver-check: anvil-semver-check-validate-prereqs exit 0 } $pkg = @(if ($env:ANVIL_INCLUDE_AFFECTED) { -split $env:ANVIL_INCLUDE_AFFECTED } else { '--workspace' }) - # Build the candidate package list. Keep each package's metadata so + # 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 # --workspace would fail on workspaces that contain bin-only crates # (reported as a missing lib target), and we want the same tolerance for # new / renamed / bin->lib-transition crates regardless of whether # we got here via impact-scoping (cloud workflows) or full-workspace - # fallback (local). + # fallback (local). Private crates have no registry consumer contract. $libPkgs = @{} $metadataJson = & cargo metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { @@ -76,7 +76,8 @@ anvil-semver-check: anvil-semver-check-validate-prereqs exit 1 } foreach ($p in $meta.packages) { - if ($p.targets | Where-Object { $_.kind -contains 'lib' }) { + if (($p.targets | Where-Object { $_.kind -contains 'lib' }) -and + -not ($p.publish -is [array] -and $p.publish.Count -eq 0)) { $libPkgs[$p.name] = $p } } @@ -95,7 +96,7 @@ anvil-semver-check: anvil-semver-check-validate-prereqs } } if ($packages.Count -eq 0) { - Write-Host 'anvil-semver-check: no affected library crates; skipping' + Write-Host 'anvil-semver-check: no affected publishable library crates; skipping' Remove-Item -LiteralPath $commentFile -ErrorAction SilentlyContinue exit 0 } diff --git a/crates/cargo-anvil/templates/justfiles/anvil/helpers.just b/crates/cargo-anvil/templates/justfiles/anvil/helpers.just index 720a1f4d..b4deea62 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/helpers.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/helpers.just @@ -12,10 +12,10 @@ # in cloud workflows: # # - "modified": only run when at least one package's source files -# changed in the diff. The check's underlying tool is workspace-wide -# or directory-scoped (cargo fmt --all, cargo heather, cargo -# spellcheck), so it doesn't take --package; we short-circuit on -# the ANVIL_INCLUDE_MODIFIED == "--skip" sentinel. +# changed in the diff. The tier is only a skip gate: once admitted, +# the check runs its complete selected scope rather than splicing +# package arguments. That scope may be workspace members (cargo each), +# the workspace (cargo heather), or a directory (cargo spellcheck). # # - "affected": run on the affected set (modified ∪ reverse-deps # within the workspace). The check's underlying tool takes @@ -37,7 +37,7 @@ # Local invocation (no impact wiring): all three env vars are unset or # empty (recipes fall back to "--workspace" via the truthiness check # below); modified-tier recipes simply skip the splice and run their -# workspace-wide tool; affected/required-tier recipes splat +# complete selected scope; affected/required-tier recipes splat # "--workspace" when the env var is empty or unset. # # Preparation contract: when a recipe reaches the cargo call, the @@ -64,8 +64,8 @@ # and reduces the per-recipe boilerplate to a single one-line skip # guard plus the cargo invocation. # -# Modified-tier recipes never splice the env var into cargo (their -# tools are workspace-wide); they only check the skip sentinel. +# Modified-tier recipes never splice the env var into their command; +# they only check the skip sentinel. # # Every recipe whose body uses multi-line conditionals or env-var # splicing is annotated with [script("pwsh", "-NoProfile")]. pwsh is preinstalled on diff --git a/crates/cargo-anvil/tests/recipe_contracts.rs b/crates/cargo-anvil/tests/recipe_contracts.rs index d7493758..707a1d05 100644 --- a/crates/cargo-anvil/tests/recipe_contracts.rs +++ b/crates/cargo-anvil/tests/recipe_contracts.rs @@ -8,6 +8,7 @@ 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::path::Path; @@ -79,16 +80,25 @@ if ($args -contains 'metadata') { metadata = [pscustomobject]@{} } } + if ($env:FAKE_THIRD_PACKAGE_NAME) { + $packages += [pscustomobject]@{ + name = $env:FAKE_THIRD_PACKAGE_NAME + version = '0.1.0' + id = "$($env:FAKE_THIRD_PACKAGE_NAME) 0.1.0" + manifest_path = [System.IO.Path]::Combine( + $root, + 'nested', + $env:FAKE_THIRD_PACKAGE_NAME, + 'Cargo.toml' + ) + targets = @([pscustomobject]@{ name = $env:FAKE_THIRD_PACKAGE_NAME; kind = @('lib') }) + publish = @('private-registry') + metadata = [pscustomobject]@{} + } + } $metadata = [pscustomobject]@{ workspace_root = $root - workspace_members = @( - $packages | - Where-Object { - -not ($env:FAKE_SECOND_PACKAGE_NON_MEMBER -and - $_.name -eq $env:FAKE_SECOND_PACKAGE_NAME) - } | - ForEach-Object { $_.id } - ) + workspace_members = @($packages | ForEach-Object { $_.id }) packages = $packages } $metadata | ConvertTo-Json -Depth 8 -Compress @@ -649,7 +659,7 @@ fn fmt_delegates_workspace_iteration_to_cargo_each() { ); let commands = std::fs::read_to_string(&log).unwrap(); assert!( - commands.contains("each --workspace -- 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")); @@ -674,7 +684,7 @@ fn fmt_propagates_cargo_each_failure() { } #[test] -fn external_types_skips_non_publishable_libraries() { +fn external_types_checks_every_publishable_library_and_reports_private_ones() { if !tools_available() { return; } @@ -692,9 +702,12 @@ fn external_types_skips_non_publishable_libraries() { tmp.path(), &["anvil-external-types"], &[ - ("ANVIL_INCLUDE_AFFECTED", OsStr::new("--package fixture@0.1.0")), + ("ANVIL_INCLUDE_AFFECTED", OsStr::new("--workspace")), ("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("public-registry")), ], ); assert!( @@ -705,7 +718,61 @@ fn external_types_skips_non_publishable_libraries() { ); let commands = std::fs::read_to_string(log).unwrap(); assert!(commands.contains("metadata --no-deps --format-version 1")); - assert!(!commands.contains("check-external-types --manifest-path")); + let expected_manifests = [ + tmp.path() + .join("nested") + .join("public-default") + .join("Cargo.toml") + .to_string_lossy() + .into_owned(), + tmp.path() + .join("nested") + .join("public-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!(String::from_utf8_lossy(&output.stdout).contains("excluding 1 non-publishable library crate")); +} + +#[test] +fn semver_skips_non_publishable_libraries() { + if !tools_available() { + return; + } + let tmp = fixture( + &[("helpers.just", HELPERS), ("semver.just", SEMVER)], + &[ + "anvil-tool-cargo-semver-checks-validate-prereqs", + "anvil-tool-cargo-semver-checks-install installer", + ], + ); + let log = tmp.path().join("cargo.log"); + let output = run_just( + tmp.path(), + &["anvil-semver-check"], + &[ + ("ANVIL_INCLUDE_AFFECTED", OsStr::new("--package fixture@0.1.0")), + ("FAKE_CARGO_LOG", log.as_os_str()), + ("FAKE_PUBLISH_FALSE", OsStr::new("1")), + ], + ); + assert!( + output.status.success(), + "private semver filtering failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + assert!(!std::fs::read_to_string(log).unwrap().contains("semver-checks")); + assert!(String::from_utf8_lossy(&output.stdout).contains("no affected publishable library crates")); } #[test] diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap index 82b2483a..708815aa 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap @@ -3033,8 +3033,9 @@ anvil-cargo-hack-validate-prereqs: anvil-tool-cargo-hack-validate-prereqs # Modified tier. # -# `--check-format` makes formatting differences fail instead of producing -# warnings, keeping dependency ordering and Cargo manifest style deterministic. +# cargo-sort 2.1.2 and newer report formatting-only differences as warnings +# unless `--check-format` is set. Keep it enabled so dependency ordering and +# Cargo manifest style are both enforced. # # `--grouped` keeps the blank lines between groups of key/value pairs # (e.g. blank-line-separated dependency blocks) intact. @@ -3297,8 +3298,9 @@ anvil-external-types: anvil-external-types-validate-prereqs exit 0 } $pkg = @(if ($env:ANVIL_INCLUDE_AFFECTED) { -split $env:ANVIL_INCLUDE_AFFECTED } else { '--workspace' }) - # Build pkg-name -> manifest-path map, restricted to publishable library crates. + # Build pkg-name -> manifest-path maps for publishable and private libraries. $libPkgs = @{} + $privateLibPkgs = @{} $metadataJson = & cargo metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-external-types: cargo metadata failed' @@ -3311,15 +3313,20 @@ anvil-external-types: anvil-external-types-validate-prereqs exit 1 } foreach ($p in $meta.packages) { - if (($p.targets | Where-Object { $_.kind -contains 'lib' }) -and - -not ($p.publish -is [array] -and $p.publish.Count -eq 0)) { - $libPkgs[$p.name] = $p.manifest_path + if ($p.targets | Where-Object { $_.kind -contains 'lib' }) { + if ($p.publish -is [array] -and $p.publish.Count -eq 0) { + $privateLibPkgs[$p.name] = $p.manifest_path + } else { + $libPkgs[$p.name] = $p.manifest_path + } } } # Decide which packages to check. $packages = @() + $privatePackages = @() if ($pkg -contains '--workspace') { $packages = $libPkgs.Keys + $privatePackages = $privateLibPkgs.Keys } else { for ($i = 0; $i -lt $pkg.Count; $i++) { if ($pkg[$i] -eq '--package' -and ($i + 1) -lt $pkg.Count) { @@ -3328,14 +3335,22 @@ anvil-external-types: anvil-external-types-validate-prereqs # this manifest-path map keys on. $name = ($pkg[$i + 1] -split '@', 2)[0] if ($libPkgs.ContainsKey($name)) { $packages += $name } + if ($privateLibPkgs.ContainsKey($name)) { $privatePackages += $name } $i++ } } } if ($packages.Count -eq 0) { - Write-Host 'anvil-external-types: no affected library crates; skipping' + if ($privatePackages.Count -gt 0) { + Write-Host "anvil-external-types: no affected publishable library crates; skipping ($($privatePackages.Count) non-publishable library crate(s) excluded)" + } else { + Write-Host 'anvil-external-types: no affected library crates; skipping' + } exit 0 } + if ($privatePackages.Count -gt 0) { + Write-Host "anvil-external-types: excluding $($privatePackages.Count) non-publishable library crate(s)" + } 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) { @@ -3378,13 +3393,17 @@ anvil-external-types-validate-prereqs: anvil-toolchain-nightly-external-types-va # format_code_in_doc_comments). Floating nightly would mean # format-drift breaking cloud workflows on rustup updates — the same trap we # explicitly avoid for udeps/miri/careful/external-types. +# +# Iterate workspace members rather than every local path dependency that +# `cargo fmt --all` discovers. `--keep-going` reports formatting failures from +# every member while retaining a bounded rustfmt command line per invocation. # Check Rust source formatting. [script("pwsh", "-NoProfile")] anvil-fmt: anvil-fmt-validate-prereqs $ErrorActionPreference = 'Stop' if ($env:ANVIL_INCLUDE_MODIFIED -eq '--skip') { exit 0 } - cargo each --workspace '--' cargo '+{{ rust_nightly }}' fmt --manifest-path '{manifest}' --check + cargo each --workspace --keep-going '--' cargo '+{{ rust_nightly }}' fmt --manifest-path '{manifest}' --check if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Per-check setup + validate-prereqs @@ -4275,14 +4294,14 @@ anvil-semver-check: anvil-semver-check-validate-prereqs exit 0 } $pkg = @(if ($env:ANVIL_INCLUDE_AFFECTED) { -split $env:ANVIL_INCLUDE_AFFECTED } else { '--workspace' }) - # Build the candidate package list. Keep each package's metadata so + # 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 # --workspace would fail on workspaces that contain bin-only crates # (reported as a missing lib target), and we want the same tolerance for # new / renamed / bin->lib-transition crates regardless of whether # we got here via impact-scoping (cloud workflows) or full-workspace - # fallback (local). + # fallback (local). Private crates have no registry consumer contract. $libPkgs = @{} $metadataJson = & cargo metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { @@ -4296,7 +4315,8 @@ anvil-semver-check: anvil-semver-check-validate-prereqs exit 1 } foreach ($p in $meta.packages) { - if ($p.targets | Where-Object { $_.kind -contains 'lib' }) { + if (($p.targets | Where-Object { $_.kind -contains 'lib' }) -and + -not ($p.publish -is [array] -and $p.publish.Count -eq 0)) { $libPkgs[$p.name] = $p } } @@ -4315,7 +4335,7 @@ anvil-semver-check: anvil-semver-check-validate-prereqs } } if ($packages.Count -eq 0) { - Write-Host 'anvil-semver-check: no affected library crates; skipping' + Write-Host 'anvil-semver-check: no affected publishable library crates; skipping' Remove-Item -LiteralPath $commentFile -ErrorAction SilentlyContinue exit 0 } @@ -4944,10 +4964,10 @@ anvil-scheduled-test-validate-prereqs: \ # in cloud workflows: # # - "modified": only run when at least one package's source files -# changed in the diff. The check's underlying tool is workspace-wide -# or directory-scoped (cargo fmt --all, cargo heather, cargo -# spellcheck), so it doesn't take --package; we short-circuit on -# the ANVIL_INCLUDE_MODIFIED == "--skip" sentinel. +# changed in the diff. The tier is only a skip gate: once admitted, +# the check runs its complete selected scope rather than splicing +# package arguments. That scope may be workspace members (cargo each), +# the workspace (cargo heather), or a directory (cargo spellcheck). # # - "affected": run on the affected set (modified ∪ reverse-deps # within the workspace). The check's underlying tool takes @@ -4969,7 +4989,7 @@ anvil-scheduled-test-validate-prereqs: \ # Local invocation (no impact wiring): all three env vars are unset or # empty (recipes fall back to "--workspace" via the truthiness check # below); modified-tier recipes simply skip the splice and run their -# workspace-wide tool; affected/required-tier recipes splat +# complete selected scope; affected/required-tier recipes splat # "--workspace" when the env var is empty or unset. # # Preparation contract: when a recipe reaches the cargo call, the @@ -4996,8 +5016,8 @@ anvil-scheduled-test-validate-prereqs: \ # and reduces the per-recipe boilerplate to a single one-line skip # guard plus the cargo invocation. # -# Modified-tier recipes never splice the env var into cargo (their -# tools are workspace-wide); they only check the skip sentinel. +# Modified-tier recipes never splice the env var into their command; +# they only check the skip sentinel. # # Every recipe whose body uses multi-line conditionals or env-var # splicing is annotated with [script("pwsh", "-NoProfile")]. pwsh is preinstalled on diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap index d122a7bf..62928733 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap @@ -2932,8 +2932,9 @@ anvil-cargo-hack-validate-prereqs: anvil-tool-cargo-hack-validate-prereqs # Modified tier. # -# `--check-format` makes formatting differences fail instead of producing -# warnings, keeping dependency ordering and Cargo manifest style deterministic. +# cargo-sort 2.1.2 and newer report formatting-only differences as warnings +# unless `--check-format` is set. Keep it enabled so dependency ordering and +# Cargo manifest style are both enforced. # # `--grouped` keeps the blank lines between groups of key/value pairs # (e.g. blank-line-separated dependency blocks) intact. @@ -3196,8 +3197,9 @@ anvil-external-types: anvil-external-types-validate-prereqs exit 0 } $pkg = @(if ($env:ANVIL_INCLUDE_AFFECTED) { -split $env:ANVIL_INCLUDE_AFFECTED } else { '--workspace' }) - # Build pkg-name -> manifest-path map, restricted to publishable library crates. + # Build pkg-name -> manifest-path maps for publishable and private libraries. $libPkgs = @{} + $privateLibPkgs = @{} $metadataJson = & cargo metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-external-types: cargo metadata failed' @@ -3210,15 +3212,20 @@ anvil-external-types: anvil-external-types-validate-prereqs exit 1 } foreach ($p in $meta.packages) { - if (($p.targets | Where-Object { $_.kind -contains 'lib' }) -and - -not ($p.publish -is [array] -and $p.publish.Count -eq 0)) { - $libPkgs[$p.name] = $p.manifest_path + if ($p.targets | Where-Object { $_.kind -contains 'lib' }) { + if ($p.publish -is [array] -and $p.publish.Count -eq 0) { + $privateLibPkgs[$p.name] = $p.manifest_path + } else { + $libPkgs[$p.name] = $p.manifest_path + } } } # Decide which packages to check. $packages = @() + $privatePackages = @() if ($pkg -contains '--workspace') { $packages = $libPkgs.Keys + $privatePackages = $privateLibPkgs.Keys } else { for ($i = 0; $i -lt $pkg.Count; $i++) { if ($pkg[$i] -eq '--package' -and ($i + 1) -lt $pkg.Count) { @@ -3227,14 +3234,22 @@ anvil-external-types: anvil-external-types-validate-prereqs # this manifest-path map keys on. $name = ($pkg[$i + 1] -split '@', 2)[0] if ($libPkgs.ContainsKey($name)) { $packages += $name } + if ($privateLibPkgs.ContainsKey($name)) { $privatePackages += $name } $i++ } } } if ($packages.Count -eq 0) { - Write-Host 'anvil-external-types: no affected library crates; skipping' + if ($privatePackages.Count -gt 0) { + Write-Host "anvil-external-types: no affected publishable library crates; skipping ($($privatePackages.Count) non-publishable library crate(s) excluded)" + } else { + Write-Host 'anvil-external-types: no affected library crates; skipping' + } exit 0 } + if ($privatePackages.Count -gt 0) { + Write-Host "anvil-external-types: excluding $($privatePackages.Count) non-publishable library crate(s)" + } 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) { @@ -3277,13 +3292,17 @@ anvil-external-types-validate-prereqs: anvil-toolchain-nightly-external-types-va # format_code_in_doc_comments). Floating nightly would mean # format-drift breaking cloud workflows on rustup updates — the same trap we # explicitly avoid for udeps/miri/careful/external-types. +# +# Iterate workspace members rather than every local path dependency that +# `cargo fmt --all` discovers. `--keep-going` reports formatting failures from +# every member while retaining a bounded rustfmt command line per invocation. # Check Rust source formatting. [script("pwsh", "-NoProfile")] anvil-fmt: anvil-fmt-validate-prereqs $ErrorActionPreference = 'Stop' if ($env:ANVIL_INCLUDE_MODIFIED -eq '--skip') { exit 0 } - cargo each --workspace '--' cargo '+{{ rust_nightly }}' fmt --manifest-path '{manifest}' --check + cargo each --workspace --keep-going '--' cargo '+{{ rust_nightly }}' fmt --manifest-path '{manifest}' --check if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Per-check setup + validate-prereqs @@ -4174,14 +4193,14 @@ anvil-semver-check: anvil-semver-check-validate-prereqs exit 0 } $pkg = @(if ($env:ANVIL_INCLUDE_AFFECTED) { -split $env:ANVIL_INCLUDE_AFFECTED } else { '--workspace' }) - # Build the candidate package list. Keep each package's metadata so + # 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 # --workspace would fail on workspaces that contain bin-only crates # (reported as a missing lib target), and we want the same tolerance for # new / renamed / bin->lib-transition crates regardless of whether # we got here via impact-scoping (cloud workflows) or full-workspace - # fallback (local). + # fallback (local). Private crates have no registry consumer contract. $libPkgs = @{} $metadataJson = & cargo metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { @@ -4195,7 +4214,8 @@ anvil-semver-check: anvil-semver-check-validate-prereqs exit 1 } foreach ($p in $meta.packages) { - if ($p.targets | Where-Object { $_.kind -contains 'lib' }) { + if (($p.targets | Where-Object { $_.kind -contains 'lib' }) -and + -not ($p.publish -is [array] -and $p.publish.Count -eq 0)) { $libPkgs[$p.name] = $p } } @@ -4214,7 +4234,7 @@ anvil-semver-check: anvil-semver-check-validate-prereqs } } if ($packages.Count -eq 0) { - Write-Host 'anvil-semver-check: no affected library crates; skipping' + Write-Host 'anvil-semver-check: no affected publishable library crates; skipping' Remove-Item -LiteralPath $commentFile -ErrorAction SilentlyContinue exit 0 } @@ -4843,10 +4863,10 @@ anvil-scheduled-test-validate-prereqs: \ # in cloud workflows: # # - "modified": only run when at least one package's source files -# changed in the diff. The check's underlying tool is workspace-wide -# or directory-scoped (cargo fmt --all, cargo heather, cargo -# spellcheck), so it doesn't take --package; we short-circuit on -# the ANVIL_INCLUDE_MODIFIED == "--skip" sentinel. +# changed in the diff. The tier is only a skip gate: once admitted, +# the check runs its complete selected scope rather than splicing +# package arguments. That scope may be workspace members (cargo each), +# the workspace (cargo heather), or a directory (cargo spellcheck). # # - "affected": run on the affected set (modified ∪ reverse-deps # within the workspace). The check's underlying tool takes @@ -4868,7 +4888,7 @@ anvil-scheduled-test-validate-prereqs: \ # Local invocation (no impact wiring): all three env vars are unset or # empty (recipes fall back to "--workspace" via the truthiness check # below); modified-tier recipes simply skip the splice and run their -# workspace-wide tool; affected/required-tier recipes splat +# complete selected scope; affected/required-tier recipes splat # "--workspace" when the env var is empty or unset. # # Preparation contract: when a recipe reaches the cargo call, the @@ -4895,8 +4915,8 @@ anvil-scheduled-test-validate-prereqs: \ # and reduces the per-recipe boilerplate to a single one-line skip # guard plus the cargo invocation. # -# Modified-tier recipes never splice the env var into cargo (their -# tools are workspace-wide); they only check the skip sentinel. +# Modified-tier recipes never splice the env var into their command; +# they only check the skip sentinel. # # Every recipe whose body uses multi-line conditionals or env-var # splicing is annotated with [script("pwsh", "-NoProfile")]. pwsh is preinstalled on diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap index 5e19cbac..fdd73ae1 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap @@ -1773,8 +1773,9 @@ anvil-cargo-hack-validate-prereqs: anvil-tool-cargo-hack-validate-prereqs # Modified tier. # -# `--check-format` makes formatting differences fail instead of producing -# warnings, keeping dependency ordering and Cargo manifest style deterministic. +# cargo-sort 2.1.2 and newer report formatting-only differences as warnings +# unless `--check-format` is set. Keep it enabled so dependency ordering and +# Cargo manifest style are both enforced. # # `--grouped` keeps the blank lines between groups of key/value pairs # (e.g. blank-line-separated dependency blocks) intact. @@ -2037,8 +2038,9 @@ anvil-external-types: anvil-external-types-validate-prereqs exit 0 } $pkg = @(if ($env:ANVIL_INCLUDE_AFFECTED) { -split $env:ANVIL_INCLUDE_AFFECTED } else { '--workspace' }) - # Build pkg-name -> manifest-path map, restricted to publishable library crates. + # Build pkg-name -> manifest-path maps for publishable and private libraries. $libPkgs = @{} + $privateLibPkgs = @{} $metadataJson = & cargo metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-external-types: cargo metadata failed' @@ -2051,15 +2053,20 @@ anvil-external-types: anvil-external-types-validate-prereqs exit 1 } foreach ($p in $meta.packages) { - if (($p.targets | Where-Object { $_.kind -contains 'lib' }) -and - -not ($p.publish -is [array] -and $p.publish.Count -eq 0)) { - $libPkgs[$p.name] = $p.manifest_path + if ($p.targets | Where-Object { $_.kind -contains 'lib' }) { + if ($p.publish -is [array] -and $p.publish.Count -eq 0) { + $privateLibPkgs[$p.name] = $p.manifest_path + } else { + $libPkgs[$p.name] = $p.manifest_path + } } } # Decide which packages to check. $packages = @() + $privatePackages = @() if ($pkg -contains '--workspace') { $packages = $libPkgs.Keys + $privatePackages = $privateLibPkgs.Keys } else { for ($i = 0; $i -lt $pkg.Count; $i++) { if ($pkg[$i] -eq '--package' -and ($i + 1) -lt $pkg.Count) { @@ -2068,14 +2075,22 @@ anvil-external-types: anvil-external-types-validate-prereqs # this manifest-path map keys on. $name = ($pkg[$i + 1] -split '@', 2)[0] if ($libPkgs.ContainsKey($name)) { $packages += $name } + if ($privateLibPkgs.ContainsKey($name)) { $privatePackages += $name } $i++ } } } if ($packages.Count -eq 0) { - Write-Host 'anvil-external-types: no affected library crates; skipping' + if ($privatePackages.Count -gt 0) { + Write-Host "anvil-external-types: no affected publishable library crates; skipping ($($privatePackages.Count) non-publishable library crate(s) excluded)" + } else { + Write-Host 'anvil-external-types: no affected library crates; skipping' + } exit 0 } + if ($privatePackages.Count -gt 0) { + Write-Host "anvil-external-types: excluding $($privatePackages.Count) non-publishable library crate(s)" + } 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) { @@ -2118,13 +2133,17 @@ anvil-external-types-validate-prereqs: anvil-toolchain-nightly-external-types-va # format_code_in_doc_comments). Floating nightly would mean # format-drift breaking cloud workflows on rustup updates — the same trap we # explicitly avoid for udeps/miri/careful/external-types. +# +# Iterate workspace members rather than every local path dependency that +# `cargo fmt --all` discovers. `--keep-going` reports formatting failures from +# every member while retaining a bounded rustfmt command line per invocation. # Check Rust source formatting. [script("pwsh", "-NoProfile")] anvil-fmt: anvil-fmt-validate-prereqs $ErrorActionPreference = 'Stop' if ($env:ANVIL_INCLUDE_MODIFIED -eq '--skip') { exit 0 } - cargo each --workspace '--' cargo '+{{ rust_nightly }}' fmt --manifest-path '{manifest}' --check + cargo each --workspace --keep-going '--' cargo '+{{ rust_nightly }}' fmt --manifest-path '{manifest}' --check if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Per-check setup + validate-prereqs @@ -3015,14 +3034,14 @@ anvil-semver-check: anvil-semver-check-validate-prereqs exit 0 } $pkg = @(if ($env:ANVIL_INCLUDE_AFFECTED) { -split $env:ANVIL_INCLUDE_AFFECTED } else { '--workspace' }) - # Build the candidate package list. Keep each package's metadata so + # 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 # --workspace would fail on workspaces that contain bin-only crates # (reported as a missing lib target), and we want the same tolerance for # new / renamed / bin->lib-transition crates regardless of whether # we got here via impact-scoping (cloud workflows) or full-workspace - # fallback (local). + # fallback (local). Private crates have no registry consumer contract. $libPkgs = @{} $metadataJson = & cargo metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { @@ -3036,7 +3055,8 @@ anvil-semver-check: anvil-semver-check-validate-prereqs exit 1 } foreach ($p in $meta.packages) { - if ($p.targets | Where-Object { $_.kind -contains 'lib' }) { + if (($p.targets | Where-Object { $_.kind -contains 'lib' }) -and + -not ($p.publish -is [array] -and $p.publish.Count -eq 0)) { $libPkgs[$p.name] = $p } } @@ -3055,7 +3075,7 @@ anvil-semver-check: anvil-semver-check-validate-prereqs } } if ($packages.Count -eq 0) { - Write-Host 'anvil-semver-check: no affected library crates; skipping' + Write-Host 'anvil-semver-check: no affected publishable library crates; skipping' Remove-Item -LiteralPath $commentFile -ErrorAction SilentlyContinue exit 0 } @@ -3684,10 +3704,10 @@ anvil-scheduled-test-validate-prereqs: \ # in cloud workflows: # # - "modified": only run when at least one package's source files -# changed in the diff. The check's underlying tool is workspace-wide -# or directory-scoped (cargo fmt --all, cargo heather, cargo -# spellcheck), so it doesn't take --package; we short-circuit on -# the ANVIL_INCLUDE_MODIFIED == "--skip" sentinel. +# changed in the diff. The tier is only a skip gate: once admitted, +# the check runs its complete selected scope rather than splicing +# package arguments. That scope may be workspace members (cargo each), +# the workspace (cargo heather), or a directory (cargo spellcheck). # # - "affected": run on the affected set (modified ∪ reverse-deps # within the workspace). The check's underlying tool takes @@ -3709,7 +3729,7 @@ anvil-scheduled-test-validate-prereqs: \ # Local invocation (no impact wiring): all three env vars are unset or # empty (recipes fall back to "--workspace" via the truthiness check # below); modified-tier recipes simply skip the splice and run their -# workspace-wide tool; affected/required-tier recipes splat +# complete selected scope; affected/required-tier recipes splat # "--workspace" when the env var is empty or unset. # # Preparation contract: when a recipe reaches the cargo call, the @@ -3736,8 +3756,8 @@ anvil-scheduled-test-validate-prereqs: \ # and reduces the per-recipe boilerplate to a single one-line skip # guard plus the cargo invocation. # -# Modified-tier recipes never splice the env var into cargo (their -# tools are workspace-wide); they only check the skip sentinel. +# Modified-tier recipes never splice the env var into their command; +# they only check the skip sentinel. # # Every recipe whose body uses multi-line conditionals or env-var # splicing is annotated with [script("pwsh", "-NoProfile")]. pwsh is preinstalled on diff --git a/justfiles/anvil/checks/cargo-sort.just b/justfiles/anvil/checks/cargo-sort.just index 65dee980..dfe0f1dd 100644 --- a/justfiles/anvil/checks/cargo-sort.just +++ b/justfiles/anvil/checks/cargo-sort.just @@ -8,8 +8,9 @@ # Modified tier. # -# `--check-format` makes formatting differences fail instead of producing -# warnings, keeping dependency ordering and Cargo manifest style deterministic. +# cargo-sort 2.1.2 and newer report formatting-only differences as warnings +# unless `--check-format` is set. Keep it enabled so dependency ordering and +# Cargo manifest style are both enforced. # # `--grouped` keeps the blank lines between groups of key/value pairs # (e.g. blank-line-separated dependency blocks) intact. diff --git a/justfiles/anvil/checks/external-types.just b/justfiles/anvil/checks/external-types.just index 4502994f..1c0b1b79 100644 --- a/justfiles/anvil/checks/external-types.just +++ b/justfiles/anvil/checks/external-types.just @@ -29,8 +29,9 @@ anvil-external-types: anvil-external-types-validate-prereqs exit 0 } $pkg = @(if ($env:ANVIL_INCLUDE_AFFECTED) { -split $env:ANVIL_INCLUDE_AFFECTED } else { '--workspace' }) - # Build pkg-name -> manifest-path map, restricted to publishable library crates. + # Build pkg-name -> manifest-path maps for publishable and private libraries. $libPkgs = @{} + $privateLibPkgs = @{} $metadataJson = & cargo metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-external-types: cargo metadata failed' @@ -43,15 +44,20 @@ anvil-external-types: anvil-external-types-validate-prereqs exit 1 } foreach ($p in $meta.packages) { - if (($p.targets | Where-Object { $_.kind -contains 'lib' }) -and - -not ($p.publish -is [array] -and $p.publish.Count -eq 0)) { - $libPkgs[$p.name] = $p.manifest_path + if ($p.targets | Where-Object { $_.kind -contains 'lib' }) { + if ($p.publish -is [array] -and $p.publish.Count -eq 0) { + $privateLibPkgs[$p.name] = $p.manifest_path + } else { + $libPkgs[$p.name] = $p.manifest_path + } } } # Decide which packages to check. $packages = @() + $privatePackages = @() if ($pkg -contains '--workspace') { $packages = $libPkgs.Keys + $privatePackages = $privateLibPkgs.Keys } else { for ($i = 0; $i -lt $pkg.Count; $i++) { if ($pkg[$i] -eq '--package' -and ($i + 1) -lt $pkg.Count) { @@ -60,14 +66,22 @@ anvil-external-types: anvil-external-types-validate-prereqs # this manifest-path map keys on. $name = ($pkg[$i + 1] -split '@', 2)[0] if ($libPkgs.ContainsKey($name)) { $packages += $name } + if ($privateLibPkgs.ContainsKey($name)) { $privatePackages += $name } $i++ } } } if ($packages.Count -eq 0) { - Write-Host 'anvil-external-types: no affected library crates; skipping' + if ($privatePackages.Count -gt 0) { + Write-Host "anvil-external-types: no affected publishable library crates; skipping ($($privatePackages.Count) non-publishable library crate(s) excluded)" + } else { + Write-Host 'anvil-external-types: no affected library crates; skipping' + } exit 0 } + if ($privatePackages.Count -gt 0) { + Write-Host "anvil-external-types: excluding $($privatePackages.Count) non-publishable library crate(s)" + } 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) { diff --git a/justfiles/anvil/checks/fmt.just b/justfiles/anvil/checks/fmt.just index 55d092b2..7b9e448e 100644 --- a/justfiles/anvil/checks/fmt.just +++ b/justfiles/anvil/checks/fmt.just @@ -12,13 +12,17 @@ # format_code_in_doc_comments). Floating nightly would mean # format-drift breaking cloud workflows on rustup updates — the same trap we # explicitly avoid for udeps/miri/careful/external-types. +# +# Iterate workspace members rather than every local path dependency that +# `cargo fmt --all` discovers. `--keep-going` reports formatting failures from +# every member while retaining a bounded rustfmt command line per invocation. # Check Rust source formatting. [script("pwsh", "-NoProfile")] anvil-fmt: anvil-fmt-validate-prereqs $ErrorActionPreference = 'Stop' if ($env:ANVIL_INCLUDE_MODIFIED -eq '--skip') { exit 0 } - cargo each --workspace '--' cargo '+{{ rust_nightly }}' fmt --manifest-path '{manifest}' --check + cargo 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/semver-check.just b/justfiles/anvil/checks/semver-check.just index faa150ca..ec930579 100644 --- a/justfiles/anvil/checks/semver-check.just +++ b/justfiles/anvil/checks/semver-check.just @@ -55,14 +55,14 @@ anvil-semver-check: anvil-semver-check-validate-prereqs exit 0 } $pkg = @(if ($env:ANVIL_INCLUDE_AFFECTED) { -split $env:ANVIL_INCLUDE_AFFECTED } else { '--workspace' }) - # Build the candidate package list. Keep each package's metadata so + # 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 # --workspace would fail on workspaces that contain bin-only crates # (reported as a missing lib target), and we want the same tolerance for # new / renamed / bin->lib-transition crates regardless of whether # we got here via impact-scoping (cloud workflows) or full-workspace - # fallback (local). + # fallback (local). Private crates have no registry consumer contract. $libPkgs = @{} $metadataJson = & cargo metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { @@ -76,7 +76,8 @@ anvil-semver-check: anvil-semver-check-validate-prereqs exit 1 } foreach ($p in $meta.packages) { - if ($p.targets | Where-Object { $_.kind -contains 'lib' }) { + if (($p.targets | Where-Object { $_.kind -contains 'lib' }) -and + -not ($p.publish -is [array] -and $p.publish.Count -eq 0)) { $libPkgs[$p.name] = $p } } @@ -95,7 +96,7 @@ anvil-semver-check: anvil-semver-check-validate-prereqs } } if ($packages.Count -eq 0) { - Write-Host 'anvil-semver-check: no affected library crates; skipping' + Write-Host 'anvil-semver-check: no affected publishable library crates; skipping' Remove-Item -LiteralPath $commentFile -ErrorAction SilentlyContinue exit 0 } diff --git a/justfiles/anvil/helpers.just b/justfiles/anvil/helpers.just index 720a1f4d..b4deea62 100644 --- a/justfiles/anvil/helpers.just +++ b/justfiles/anvil/helpers.just @@ -12,10 +12,10 @@ # in cloud workflows: # # - "modified": only run when at least one package's source files -# changed in the diff. The check's underlying tool is workspace-wide -# or directory-scoped (cargo fmt --all, cargo heather, cargo -# spellcheck), so it doesn't take --package; we short-circuit on -# the ANVIL_INCLUDE_MODIFIED == "--skip" sentinel. +# changed in the diff. The tier is only a skip gate: once admitted, +# the check runs its complete selected scope rather than splicing +# package arguments. That scope may be workspace members (cargo each), +# the workspace (cargo heather), or a directory (cargo spellcheck). # # - "affected": run on the affected set (modified ∪ reverse-deps # within the workspace). The check's underlying tool takes @@ -37,7 +37,7 @@ # Local invocation (no impact wiring): all three env vars are unset or # empty (recipes fall back to "--workspace" via the truthiness check # below); modified-tier recipes simply skip the splice and run their -# workspace-wide tool; affected/required-tier recipes splat +# complete selected scope; affected/required-tier recipes splat # "--workspace" when the env var is empty or unset. # # Preparation contract: when a recipe reaches the cargo call, the @@ -64,8 +64,8 @@ # and reduces the per-recipe boilerplate to a single one-line skip # guard plus the cargo invocation. # -# Modified-tier recipes never splice the env var into cargo (their -# tools are workspace-wide); they only check the skip sentinel. +# Modified-tier recipes never splice the env var into their command; +# they only check the skip sentinel. # # Every recipe whose body uses multi-line conditionals or env-var # splicing is annotated with [script("pwsh", "-NoProfile")]. pwsh is preinstalled on From 8cf9243aca03c278f5b2f87f2f5aa056e2c637dd Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Fri, 28 Aug 2026 10:20:03 +0200 Subject: [PATCH 07/17] docs(anvil): clarify private semver scope Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4112f005-775f-4c4b-88ed-db8f0c5d1cf1 --- .anvil.lock | 4 ++-- .../templates/justfiles/anvil/checks/semver-check.just | 7 ++++--- .../tests/snapshots/snapshots__ado_backend.snap | 7 ++++--- .../tests/snapshots/snapshots__github_backend.snap | 7 ++++--- .../cargo-anvil/tests/snapshots/snapshots__local_only.snap | 7 ++++--- justfiles/anvil/checks/semver-check.just | 7 ++++--- 6 files changed, 22 insertions(+), 17 deletions(-) diff --git a/.anvil.lock b/.anvil.lock index 710247f9..09d11d40 100644 --- a/.anvil.lock +++ b/.anvil.lock @@ -1,7 +1,7 @@ version = 1 tool = "anvil" tool_version = "0.5.0" -catalog_checksum = "sha256:97099270927c3a7f1aa8e7362a116a1fb19e3992d3a3160e70482e0c947206d6" +catalog_checksum = "sha256:b898d3c10529ba7d413e496329162cf752fd71554b73f9becb39a97658f7d596" [[file]] path = ".anvil/container/Containerfile" @@ -181,7 +181,7 @@ checksum = "sha256:fb395f5413de48848f10ab3e4069599c0093a790cb31224776ba41f90c8d5 [[file]] path = "justfiles/anvil/checks/semver-check.just" -checksum = "sha256:22f72ad1180f46aca346ecc349b57ca46c1337cacb05463e29b0c78a96892137" +checksum = "sha256:2fec1c1fc4a23e060a404b7b38c9712ea4e6b796e3d0c2c2b27238248afe57ef" [[file]] path = "justfiles/anvil/checks/spellcheck.just" 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 ec930579..6d1fe060 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/semver-check.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/semver-check.just @@ -11,13 +11,14 @@ # # Several real-world conditions aren't actually SemVer violations: # - bin-only crates have no API to compare (reported as a missing lib target). +# - `publish = false` crates have no registry consumer contract. # - crates newly added since the PR base have no manifest at their # current workspace-relative path in that baseline. # - crates renamed since the PR base have no matching package in that # baseline ("package `` not found in ..."). # - crates where the branch baseline lacks a lib target the # current source has (bin -> bin+lib transition). -# We pre-filter to library-bearing crates from cargo metadata, skip +# We pre-filter to publishable library-bearing crates from cargo metadata, skip # packages whose current manifest path is absent from the baseline, # then run cargo-semver-checks per-package and tolerate the remaining # "no-comparable-baseline" failure modes. @@ -37,8 +38,8 @@ # # TODO(anvil-runner): even after a `cargo ox-run` helper absorbs the # skip/splat preamble, this recipe stays multi-step: cargo-metadata -# filter to library crates (cargo-semver-checks --workspace fails on -# bin-only workspaces), intersect ANVIL_INCLUDE_AFFECTED with that +# filter to publishable library crates (cargo-semver-checks --workspace +# fails on bin-only workspaces), intersect ANVIL_INCLUDE_AFFECTED 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 diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap index 708815aa..da078ab4 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap @@ -4250,13 +4250,14 @@ anvil-readme-check-validate-prereqs: anvil-tool-cargo-doc2readme-validate-prereq # # Several real-world conditions aren't actually SemVer violations: # - bin-only crates have no API to compare (reported as a missing lib target). +# - `publish = false` crates have no registry consumer contract. # - crates newly added since the PR base have no manifest at their # current workspace-relative path in that baseline. # - crates renamed since the PR base have no matching package in that # baseline ("package `` not found in ..."). # - crates where the branch baseline lacks a lib target the # current source has (bin -> bin+lib transition). -# We pre-filter to library-bearing crates from cargo metadata, skip +# We pre-filter to publishable library-bearing crates from cargo metadata, skip # packages whose current manifest path is absent from the baseline, # then run cargo-semver-checks per-package and tolerate the remaining # "no-comparable-baseline" failure modes. @@ -4276,8 +4277,8 @@ 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 library crates (cargo-semver-checks --workspace fails on -# bin-only workspaces), intersect ANVIL_INCLUDE_AFFECTED with that +# filter to publishable library crates (cargo-semver-checks --workspace +# fails on bin-only workspaces), intersect ANVIL_INCLUDE_AFFECTED 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 diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap index 62928733..53dc25c1 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap @@ -4149,13 +4149,14 @@ anvil-readme-check-validate-prereqs: anvil-tool-cargo-doc2readme-validate-prereq # # Several real-world conditions aren't actually SemVer violations: # - bin-only crates have no API to compare (reported as a missing lib target). +# - `publish = false` crates have no registry consumer contract. # - crates newly added since the PR base have no manifest at their # current workspace-relative path in that baseline. # - crates renamed since the PR base have no matching package in that # baseline ("package `` not found in ..."). # - crates where the branch baseline lacks a lib target the # current source has (bin -> bin+lib transition). -# We pre-filter to library-bearing crates from cargo metadata, skip +# We pre-filter to publishable library-bearing crates from cargo metadata, skip # packages whose current manifest path is absent from the baseline, # then run cargo-semver-checks per-package and tolerate the remaining # "no-comparable-baseline" failure modes. @@ -4175,8 +4176,8 @@ 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 library crates (cargo-semver-checks --workspace fails on -# bin-only workspaces), intersect ANVIL_INCLUDE_AFFECTED with that +# filter to publishable library crates (cargo-semver-checks --workspace +# fails on bin-only workspaces), intersect ANVIL_INCLUDE_AFFECTED 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 diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap index fdd73ae1..6209eadc 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap @@ -2990,13 +2990,14 @@ anvil-readme-check-validate-prereqs: anvil-tool-cargo-doc2readme-validate-prereq # # Several real-world conditions aren't actually SemVer violations: # - bin-only crates have no API to compare (reported as a missing lib target). +# - `publish = false` crates have no registry consumer contract. # - crates newly added since the PR base have no manifest at their # current workspace-relative path in that baseline. # - crates renamed since the PR base have no matching package in that # baseline ("package `` not found in ..."). # - crates where the branch baseline lacks a lib target the # current source has (bin -> bin+lib transition). -# We pre-filter to library-bearing crates from cargo metadata, skip +# We pre-filter to publishable library-bearing crates from cargo metadata, skip # packages whose current manifest path is absent from the baseline, # then run cargo-semver-checks per-package and tolerate the remaining # "no-comparable-baseline" failure modes. @@ -3016,8 +3017,8 @@ 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 library crates (cargo-semver-checks --workspace fails on -# bin-only workspaces), intersect ANVIL_INCLUDE_AFFECTED with that +# filter to publishable library crates (cargo-semver-checks --workspace +# fails on bin-only workspaces), intersect ANVIL_INCLUDE_AFFECTED 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 diff --git a/justfiles/anvil/checks/semver-check.just b/justfiles/anvil/checks/semver-check.just index ec930579..6d1fe060 100644 --- a/justfiles/anvil/checks/semver-check.just +++ b/justfiles/anvil/checks/semver-check.just @@ -11,13 +11,14 @@ # # Several real-world conditions aren't actually SemVer violations: # - bin-only crates have no API to compare (reported as a missing lib target). +# - `publish = false` crates have no registry consumer contract. # - crates newly added since the PR base have no manifest at their # current workspace-relative path in that baseline. # - crates renamed since the PR base have no matching package in that # baseline ("package `` not found in ..."). # - crates where the branch baseline lacks a lib target the # current source has (bin -> bin+lib transition). -# We pre-filter to library-bearing crates from cargo metadata, skip +# We pre-filter to publishable library-bearing crates from cargo metadata, skip # packages whose current manifest path is absent from the baseline, # then run cargo-semver-checks per-package and tolerate the remaining # "no-comparable-baseline" failure modes. @@ -37,8 +38,8 @@ # # TODO(anvil-runner): even after a `cargo ox-run` helper absorbs the # skip/splat preamble, this recipe stays multi-step: cargo-metadata -# filter to library crates (cargo-semver-checks --workspace fails on -# bin-only workspaces), intersect ANVIL_INCLUDE_AFFECTED with that +# filter to publishable library crates (cargo-semver-checks --workspace +# fails on bin-only workspaces), intersect ANVIL_INCLUDE_AFFECTED 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 From 5800b4789717d371b0e049893a521997fe626bea Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Fri, 28 Aug 2026 10:34:33 +0200 Subject: [PATCH 08/17] docs(anvil): clarify publishability terminology Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4112f005-775f-4c4b-88ed-db8f0c5d1cf1 --- .anvil.lock | 6 ++--- crates/cargo-anvil/docs/design/checks.md | 2 +- crates/cargo-anvil/docs/design/local.md | 2 +- .../anvil/checks/external-types.just | 20 ++++++++--------- .../justfiles/anvil/checks/semver-check.just | 2 +- crates/cargo-anvil/tests/recipe_contracts.rs | 6 ++--- .../snapshots/snapshots__ado_backend.snap | 22 +++++++++---------- .../snapshots/snapshots__github_backend.snap | 22 +++++++++---------- .../snapshots/snapshots__local_only.snap | 22 +++++++++---------- justfiles/anvil/checks/external-types.just | 20 ++++++++--------- justfiles/anvil/checks/semver-check.just | 2 +- 11 files changed, 63 insertions(+), 63 deletions(-) diff --git a/.anvil.lock b/.anvil.lock index f7d10e65..64f46680 100644 --- a/.anvil.lock +++ b/.anvil.lock @@ -1,7 +1,7 @@ version = 1 tool = "anvil" tool_version = "0.5.0" -catalog_checksum = "sha256:4284f8be486bd1db5e53f4a468eec6addb09fb132c45229c8596ba140b3b3bea" +catalog_checksum = "sha256:074664f94a7fc0b87f7d29966452b7c9d957ca65771bf3e65724e3b8814da86a" [[file]] path = ".anvil/container/Containerfile" @@ -129,7 +129,7 @@ checksum = "sha256:0e84a7b43ff07608c624ece03d46ddd58c629ebd5f6a5684f3b43b7190026 [[file]] path = "justfiles/anvil/checks/external-types.just" -checksum = "sha256:ad758777a3d6d55ef1d86f7f62d5a685b55fdf265f460e92d4048ab7113408e7" +checksum = "sha256:46411f5c538c2544889054bb69d9ca78b04db59a82d61d39a1a8dca9ebec6839" [[file]] path = "justfiles/anvil/checks/fmt.just" @@ -181,7 +181,7 @@ checksum = "sha256:d346399f288570066e53fd123baf05f7d4a57f17da8ee687a6d881204c5ba [[file]] path = "justfiles/anvil/checks/semver-check.just" -checksum = "sha256:13f314e04193bdebbe3d2a20ac15897a9bad705b88f6b361623345db4b0463c1" +checksum = "sha256:0e2aaef4acd81363f2e0ce2c6b6c22bd5adc164e7dc597006d78006c6d42422e" [[file]] path = "justfiles/anvil/checks/spellcheck.just" diff --git a/crates/cargo-anvil/docs/design/checks.md b/crates/cargo-anvil/docs/design/checks.md index 40756f07..ef205fad 100644 --- a/crates/cargo-anvil/docs/design/checks.md +++ b/crates/cargo-anvil/docs/design/checks.md @@ -170,7 +170,7 @@ that provided the strongest version of the check. | `deny` | `cargo deny check` | all | | `audit` | `cargo audit` | oxidizer | | `udeps` | `cargo + udeps --workspace --all-features` run **twice** — once with default targets (lib + bins) and once with `--all-targets`. cargo-udeps only analyzes the targets it's told to, and each run catches a variant the other masks: the default-targets run surfaces a dep in `[dependencies]` referenced only by tests/benches/examples (it should be a dev-dep; `--all-targets` would see it as "used"), while the `--all-targets` run surfaces unused `[dev-dependencies]` (never compiled by the default-targets run). Together they cover unused deps, unused dev-deps, and deps that should be dev-deps. | oxidizer, oxidizer-github | -| `semver-check` | `cargo semver-checks --baseline-rev ` per affected publishable library crate. Private and bin-only crates have no registry consumer contract and are skipped. The PR target is the baseline. Exit 100 is a completed check with deny-level findings; exit 101 or another nonzero status means the comparison was inconclusive. Both outcomes write `target/anvil/comments/semver.md` and remain advisory, matching the repository's native `semver` job (`continue-on-error: true`). Proven rename and bin→lib transitions with no comparable baseline are skipped without a comment. Anvil preflight failures such as invalid current-workspace metadata or an unavailable baseline ref still fail because the recipe cannot establish what to compare. | oxidizer-github | +| `semver-check` | `cargo semver-checks --baseline-rev ` per affected publishable library crate. Crates with `publish = false` and bin-only crates have no registry consumer contract and are skipped. The PR target is the baseline. Exit 100 is a completed check with deny-level findings; exit 101 or another nonzero status means the comparison was inconclusive. Both outcomes write `target/anvil/comments/semver.md` and remain advisory, matching the repository's native `semver` job (`continue-on-error: true`). Proven rename and bin→lib transitions with no comparable baseline are skipped without a comment. Anvil preflight failures such as invalid current-workspace metadata or an unavailable baseline ref still fail because the recipe cannot establish what to compare. | oxidizer-github | | `external-types` | `cargo + check-external-types --manifest-path` per publishable library crate (per-manifest because the tool has no `--workspace`/`--package`; bin-only and `publish = false` crates have no registry consumer contract and are skipped). Setup installs the catalog version but validation accepts newer installed tools. The selected nightly is tested with the catalog version; an incompatible newer tool fails closed with a tool/nightly compatibility diagnostic rather than silently selecting a different schema. | oxidizer-github | ### `pr-slow` diff --git a/crates/cargo-anvil/docs/design/local.md b/crates/cargo-anvil/docs/design/local.md index 339dd051..0d774355 100644 --- a/crates/cargo-anvil/docs/design/local.md +++ b/crates/cargo-anvil/docs/design/local.md @@ -523,7 +523,7 @@ anvil-fmt: $include = (& "{{ just_executable() }}" _anvil-impact-include modified) if ($include -eq '--skip') { exit 0 } cargo each --workspace --keep-going '--' \ - cargo fmt --manifest-path '{manifest}' --check + cargo '+{{ rust_nightly }}' fmt --manifest-path '{manifest}' --check ``` The mapping from check to bucket is fixed in the catalog (see 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 22fcd95d..e50c2d8d 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/external-types.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/external-types.just @@ -30,9 +30,9 @@ anvil-external-types: anvil-external-types-validate-prereqs anvil-impact exit 0 } $pkg = @(if ($include) { -split $include } else { '--workspace' }) - # Build pkg-name -> manifest-path maps for publishable and private libraries. + # Build pkg-name -> manifest-path maps for publishable and non-publishable libraries. $libPkgs = @{} - $privateLibPkgs = @{} + $nonPublishableLibPkgs = @{} $metadataJson = & cargo metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-external-types: cargo metadata failed' @@ -47,7 +47,7 @@ anvil-external-types: anvil-external-types-validate-prereqs anvil-impact foreach ($p in $meta.packages) { if ($p.targets | Where-Object { $_.kind -contains 'lib' }) { if ($p.publish -is [array] -and $p.publish.Count -eq 0) { - $privateLibPkgs[$p.name] = $p.manifest_path + $nonPublishableLibPkgs[$p.name] = $p.manifest_path } else { $libPkgs[$p.name] = $p.manifest_path } @@ -55,10 +55,10 @@ anvil-external-types: anvil-external-types-validate-prereqs anvil-impact } # Decide which packages to check. $packages = @() - $privatePackages = @() + $nonPublishablePackages = @() if ($pkg -contains '--workspace') { $packages = $libPkgs.Keys - $privatePackages = $privateLibPkgs.Keys + $nonPublishablePackages = $nonPublishableLibPkgs.Keys } else { for ($i = 0; $i -lt $pkg.Count; $i++) { if ($pkg[$i] -eq '--package' -and ($i + 1) -lt $pkg.Count) { @@ -67,21 +67,21 @@ anvil-external-types: anvil-external-types-validate-prereqs anvil-impact # this manifest-path map keys on. $name = ($pkg[$i + 1] -split '@', 2)[0] if ($libPkgs.ContainsKey($name)) { $packages += $name } - if ($privateLibPkgs.ContainsKey($name)) { $privatePackages += $name } + if ($nonPublishableLibPkgs.ContainsKey($name)) { $nonPublishablePackages += $name } $i++ } } } if ($packages.Count -eq 0) { - if ($privatePackages.Count -gt 0) { - Write-Host "anvil-external-types: no affected publishable library crates; skipping ($($privatePackages.Count) non-publishable library crate(s) excluded)" + if ($nonPublishablePackages.Count -gt 0) { + Write-Host "anvil-external-types: no affected publishable library crates; skipping ($($nonPublishablePackages.Count) non-publishable library crate(s) excluded)" } else { Write-Host 'anvil-external-types: no affected library crates; skipping' } exit 0 } - if ($privatePackages.Count -gt 0) { - Write-Host "anvil-external-types: excluding $($privatePackages.Count) non-publishable library crate(s)" + if ($nonPublishablePackages.Count -gt 0) { + Write-Host "anvil-external-types: excluding $($nonPublishablePackages.Count) non-publishable library crate(s)" } 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 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 4cdc0adf..5d7dd3e4 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/semver-check.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/semver-check.just @@ -64,7 +64,7 @@ anvil-semver-check: anvil-semver-check-validate-prereqs anvil-impact # (reported as a missing lib target), and we want the same tolerance for # new / renamed / bin->lib-transition crates regardless of whether # we got here via impact-scoping (cloud workflows) or full-workspace - # fallback (local). Private crates have no registry consumer contract. + # fallback (local). Crates with `publish = false` have no registry consumer contract. $libPkgs = @{} $metadataJson = & cargo metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { diff --git a/crates/cargo-anvil/tests/recipe_contracts.rs b/crates/cargo-anvil/tests/recipe_contracts.rs index 73eab401..6d375483 100644 --- a/crates/cargo-anvil/tests/recipe_contracts.rs +++ b/crates/cargo-anvil/tests/recipe_contracts.rs @@ -769,7 +769,7 @@ fn fmt_propagates_cargo_each_failure() { } #[test] -fn external_types_checks_every_publishable_library_and_reports_private_ones() { +fn external_types_checks_every_publishable_library_and_reports_non_publishable_ones() { if !tools_available() { return; } @@ -798,7 +798,7 @@ fn external_types_checks_every_publishable_library_and_reports_private_ones() { ); assert!( output.status.success(), - "private library filtering failed\nstdout:\n{}\nstderr:\n{}", + "non-publishable library filtering failed\nstdout:\n{}\nstderr:\n{}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr) ); @@ -851,7 +851,7 @@ fn semver_skips_non_publishable_libraries() { ); assert!( output.status.success(), - "private semver filtering failed\nstdout:\n{}\nstderr:\n{}", + "non-publishable semver filtering failed\nstdout:\n{}\nstderr:\n{}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr) ); diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap index e98904b8..5e30ef95 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap @@ -3297,9 +3297,9 @@ anvil-external-types: anvil-external-types-validate-prereqs anvil-impact exit 0 } $pkg = @(if ($include) { -split $include } else { '--workspace' }) - # Build pkg-name -> manifest-path maps for publishable and private libraries. + # Build pkg-name -> manifest-path maps for publishable and non-publishable libraries. $libPkgs = @{} - $privateLibPkgs = @{} + $nonPublishableLibPkgs = @{} $metadataJson = & cargo metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-external-types: cargo metadata failed' @@ -3314,7 +3314,7 @@ anvil-external-types: anvil-external-types-validate-prereqs anvil-impact foreach ($p in $meta.packages) { if ($p.targets | Where-Object { $_.kind -contains 'lib' }) { if ($p.publish -is [array] -and $p.publish.Count -eq 0) { - $privateLibPkgs[$p.name] = $p.manifest_path + $nonPublishableLibPkgs[$p.name] = $p.manifest_path } else { $libPkgs[$p.name] = $p.manifest_path } @@ -3322,10 +3322,10 @@ anvil-external-types: anvil-external-types-validate-prereqs anvil-impact } # Decide which packages to check. $packages = @() - $privatePackages = @() + $nonPublishablePackages = @() if ($pkg -contains '--workspace') { $packages = $libPkgs.Keys - $privatePackages = $privateLibPkgs.Keys + $nonPublishablePackages = $nonPublishableLibPkgs.Keys } else { for ($i = 0; $i -lt $pkg.Count; $i++) { if ($pkg[$i] -eq '--package' -and ($i + 1) -lt $pkg.Count) { @@ -3334,21 +3334,21 @@ anvil-external-types: anvil-external-types-validate-prereqs anvil-impact # this manifest-path map keys on. $name = ($pkg[$i + 1] -split '@', 2)[0] if ($libPkgs.ContainsKey($name)) { $packages += $name } - if ($privateLibPkgs.ContainsKey($name)) { $privatePackages += $name } + if ($nonPublishableLibPkgs.ContainsKey($name)) { $nonPublishablePackages += $name } $i++ } } } if ($packages.Count -eq 0) { - if ($privatePackages.Count -gt 0) { - Write-Host "anvil-external-types: no affected publishable library crates; skipping ($($privatePackages.Count) non-publishable library crate(s) excluded)" + if ($nonPublishablePackages.Count -gt 0) { + Write-Host "anvil-external-types: no affected publishable library crates; skipping ($($nonPublishablePackages.Count) non-publishable library crate(s) excluded)" } else { Write-Host 'anvil-external-types: no affected library crates; skipping' } exit 0 } - if ($privatePackages.Count -gt 0) { - Write-Host "anvil-external-types: excluding $($privatePackages.Count) non-publishable library crate(s)" + if ($nonPublishablePackages.Count -gt 0) { + Write-Host "anvil-external-types: excluding $($nonPublishablePackages.Count) non-publishable library crate(s)" } 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 @@ -4296,7 +4296,7 @@ anvil-semver-check: anvil-semver-check-validate-prereqs anvil-impact # (reported as a missing lib target), and we want the same tolerance for # new / renamed / bin->lib-transition crates regardless of whether # we got here via impact-scoping (cloud workflows) or full-workspace - # fallback (local). Private crates have no registry consumer contract. + # fallback (local). Crates with `publish = false` have no registry consumer contract. $libPkgs = @{} $metadataJson = & cargo metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap index 4f7ed4fc..28901807 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap @@ -3176,9 +3176,9 @@ anvil-external-types: anvil-external-types-validate-prereqs anvil-impact exit 0 } $pkg = @(if ($include) { -split $include } else { '--workspace' }) - # Build pkg-name -> manifest-path maps for publishable and private libraries. + # Build pkg-name -> manifest-path maps for publishable and non-publishable libraries. $libPkgs = @{} - $privateLibPkgs = @{} + $nonPublishableLibPkgs = @{} $metadataJson = & cargo metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-external-types: cargo metadata failed' @@ -3193,7 +3193,7 @@ anvil-external-types: anvil-external-types-validate-prereqs anvil-impact foreach ($p in $meta.packages) { if ($p.targets | Where-Object { $_.kind -contains 'lib' }) { if ($p.publish -is [array] -and $p.publish.Count -eq 0) { - $privateLibPkgs[$p.name] = $p.manifest_path + $nonPublishableLibPkgs[$p.name] = $p.manifest_path } else { $libPkgs[$p.name] = $p.manifest_path } @@ -3201,10 +3201,10 @@ anvil-external-types: anvil-external-types-validate-prereqs anvil-impact } # Decide which packages to check. $packages = @() - $privatePackages = @() + $nonPublishablePackages = @() if ($pkg -contains '--workspace') { $packages = $libPkgs.Keys - $privatePackages = $privateLibPkgs.Keys + $nonPublishablePackages = $nonPublishableLibPkgs.Keys } else { for ($i = 0; $i -lt $pkg.Count; $i++) { if ($pkg[$i] -eq '--package' -and ($i + 1) -lt $pkg.Count) { @@ -3213,21 +3213,21 @@ anvil-external-types: anvil-external-types-validate-prereqs anvil-impact # this manifest-path map keys on. $name = ($pkg[$i + 1] -split '@', 2)[0] if ($libPkgs.ContainsKey($name)) { $packages += $name } - if ($privateLibPkgs.ContainsKey($name)) { $privatePackages += $name } + if ($nonPublishableLibPkgs.ContainsKey($name)) { $nonPublishablePackages += $name } $i++ } } } if ($packages.Count -eq 0) { - if ($privatePackages.Count -gt 0) { - Write-Host "anvil-external-types: no affected publishable library crates; skipping ($($privatePackages.Count) non-publishable library crate(s) excluded)" + if ($nonPublishablePackages.Count -gt 0) { + Write-Host "anvil-external-types: no affected publishable library crates; skipping ($($nonPublishablePackages.Count) non-publishable library crate(s) excluded)" } else { Write-Host 'anvil-external-types: no affected library crates; skipping' } exit 0 } - if ($privatePackages.Count -gt 0) { - Write-Host "anvil-external-types: excluding $($privatePackages.Count) non-publishable library crate(s)" + if ($nonPublishablePackages.Count -gt 0) { + Write-Host "anvil-external-types: excluding $($nonPublishablePackages.Count) non-publishable library crate(s)" } 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 @@ -4175,7 +4175,7 @@ anvil-semver-check: anvil-semver-check-validate-prereqs anvil-impact # (reported as a missing lib target), and we want the same tolerance for # new / renamed / bin->lib-transition crates regardless of whether # we got here via impact-scoping (cloud workflows) or full-workspace - # fallback (local). Private crates have no registry consumer contract. + # fallback (local). Crates with `publish = false` have no registry consumer contract. $libPkgs = @{} $metadataJson = & cargo metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap index 4a65dd7f..19a2a96d 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap @@ -2049,9 +2049,9 @@ anvil-external-types: anvil-external-types-validate-prereqs anvil-impact exit 0 } $pkg = @(if ($include) { -split $include } else { '--workspace' }) - # Build pkg-name -> manifest-path maps for publishable and private libraries. + # Build pkg-name -> manifest-path maps for publishable and non-publishable libraries. $libPkgs = @{} - $privateLibPkgs = @{} + $nonPublishableLibPkgs = @{} $metadataJson = & cargo metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-external-types: cargo metadata failed' @@ -2066,7 +2066,7 @@ anvil-external-types: anvil-external-types-validate-prereqs anvil-impact foreach ($p in $meta.packages) { if ($p.targets | Where-Object { $_.kind -contains 'lib' }) { if ($p.publish -is [array] -and $p.publish.Count -eq 0) { - $privateLibPkgs[$p.name] = $p.manifest_path + $nonPublishableLibPkgs[$p.name] = $p.manifest_path } else { $libPkgs[$p.name] = $p.manifest_path } @@ -2074,10 +2074,10 @@ anvil-external-types: anvil-external-types-validate-prereqs anvil-impact } # Decide which packages to check. $packages = @() - $privatePackages = @() + $nonPublishablePackages = @() if ($pkg -contains '--workspace') { $packages = $libPkgs.Keys - $privatePackages = $privateLibPkgs.Keys + $nonPublishablePackages = $nonPublishableLibPkgs.Keys } else { for ($i = 0; $i -lt $pkg.Count; $i++) { if ($pkg[$i] -eq '--package' -and ($i + 1) -lt $pkg.Count) { @@ -2086,21 +2086,21 @@ anvil-external-types: anvil-external-types-validate-prereqs anvil-impact # this manifest-path map keys on. $name = ($pkg[$i + 1] -split '@', 2)[0] if ($libPkgs.ContainsKey($name)) { $packages += $name } - if ($privateLibPkgs.ContainsKey($name)) { $privatePackages += $name } + if ($nonPublishableLibPkgs.ContainsKey($name)) { $nonPublishablePackages += $name } $i++ } } } if ($packages.Count -eq 0) { - if ($privatePackages.Count -gt 0) { - Write-Host "anvil-external-types: no affected publishable library crates; skipping ($($privatePackages.Count) non-publishable library crate(s) excluded)" + if ($nonPublishablePackages.Count -gt 0) { + Write-Host "anvil-external-types: no affected publishable library crates; skipping ($($nonPublishablePackages.Count) non-publishable library crate(s) excluded)" } else { Write-Host 'anvil-external-types: no affected library crates; skipping' } exit 0 } - if ($privatePackages.Count -gt 0) { - Write-Host "anvil-external-types: excluding $($privatePackages.Count) non-publishable library crate(s)" + if ($nonPublishablePackages.Count -gt 0) { + Write-Host "anvil-external-types: excluding $($nonPublishablePackages.Count) non-publishable library crate(s)" } 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 @@ -3048,7 +3048,7 @@ anvil-semver-check: anvil-semver-check-validate-prereqs anvil-impact # (reported as a missing lib target), and we want the same tolerance for # new / renamed / bin->lib-transition crates regardless of whether # we got here via impact-scoping (cloud workflows) or full-workspace - # fallback (local). Private crates have no registry consumer contract. + # fallback (local). Crates with `publish = false` have no registry consumer contract. $libPkgs = @{} $metadataJson = & cargo metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { diff --git a/justfiles/anvil/checks/external-types.just b/justfiles/anvil/checks/external-types.just index 22fcd95d..e50c2d8d 100644 --- a/justfiles/anvil/checks/external-types.just +++ b/justfiles/anvil/checks/external-types.just @@ -30,9 +30,9 @@ anvil-external-types: anvil-external-types-validate-prereqs anvil-impact exit 0 } $pkg = @(if ($include) { -split $include } else { '--workspace' }) - # Build pkg-name -> manifest-path maps for publishable and private libraries. + # Build pkg-name -> manifest-path maps for publishable and non-publishable libraries. $libPkgs = @{} - $privateLibPkgs = @{} + $nonPublishableLibPkgs = @{} $metadataJson = & cargo metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-external-types: cargo metadata failed' @@ -47,7 +47,7 @@ anvil-external-types: anvil-external-types-validate-prereqs anvil-impact foreach ($p in $meta.packages) { if ($p.targets | Where-Object { $_.kind -contains 'lib' }) { if ($p.publish -is [array] -and $p.publish.Count -eq 0) { - $privateLibPkgs[$p.name] = $p.manifest_path + $nonPublishableLibPkgs[$p.name] = $p.manifest_path } else { $libPkgs[$p.name] = $p.manifest_path } @@ -55,10 +55,10 @@ anvil-external-types: anvil-external-types-validate-prereqs anvil-impact } # Decide which packages to check. $packages = @() - $privatePackages = @() + $nonPublishablePackages = @() if ($pkg -contains '--workspace') { $packages = $libPkgs.Keys - $privatePackages = $privateLibPkgs.Keys + $nonPublishablePackages = $nonPublishableLibPkgs.Keys } else { for ($i = 0; $i -lt $pkg.Count; $i++) { if ($pkg[$i] -eq '--package' -and ($i + 1) -lt $pkg.Count) { @@ -67,21 +67,21 @@ anvil-external-types: anvil-external-types-validate-prereqs anvil-impact # this manifest-path map keys on. $name = ($pkg[$i + 1] -split '@', 2)[0] if ($libPkgs.ContainsKey($name)) { $packages += $name } - if ($privateLibPkgs.ContainsKey($name)) { $privatePackages += $name } + if ($nonPublishableLibPkgs.ContainsKey($name)) { $nonPublishablePackages += $name } $i++ } } } if ($packages.Count -eq 0) { - if ($privatePackages.Count -gt 0) { - Write-Host "anvil-external-types: no affected publishable library crates; skipping ($($privatePackages.Count) non-publishable library crate(s) excluded)" + if ($nonPublishablePackages.Count -gt 0) { + Write-Host "anvil-external-types: no affected publishable library crates; skipping ($($nonPublishablePackages.Count) non-publishable library crate(s) excluded)" } else { Write-Host 'anvil-external-types: no affected library crates; skipping' } exit 0 } - if ($privatePackages.Count -gt 0) { - Write-Host "anvil-external-types: excluding $($privatePackages.Count) non-publishable library crate(s)" + if ($nonPublishablePackages.Count -gt 0) { + Write-Host "anvil-external-types: excluding $($nonPublishablePackages.Count) non-publishable library crate(s)" } 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 diff --git a/justfiles/anvil/checks/semver-check.just b/justfiles/anvil/checks/semver-check.just index 4cdc0adf..5d7dd3e4 100644 --- a/justfiles/anvil/checks/semver-check.just +++ b/justfiles/anvil/checks/semver-check.just @@ -64,7 +64,7 @@ anvil-semver-check: anvil-semver-check-validate-prereqs anvil-impact # (reported as a missing lib target), and we want the same tolerance for # new / renamed / bin->lib-transition crates regardless of whether # we got here via impact-scoping (cloud workflows) or full-workspace - # fallback (local). Private crates have no registry consumer contract. + # fallback (local). Crates with `publish = false` have no registry consumer contract. $libPkgs = @{} $metadataJson = & cargo metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { From 9d686d25ad860cb8c081e814b668a040e6ccd823 Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Fri, 28 Aug 2026 12:17:07 +0200 Subject: [PATCH 09/17] docs(anvil): restore semver filter rationale Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4112f005-775f-4c4b-88ed-db8f0c5d1cf1 --- .anvil.lock | 4 ++-- .../templates/justfiles/anvil/checks/semver-check.just | 2 +- .../cargo-anvil/tests/snapshots/snapshots__ado_backend.snap | 2 +- .../tests/snapshots/snapshots__github_backend.snap | 2 +- crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap | 2 +- justfiles/anvil/checks/semver-check.just | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.anvil.lock b/.anvil.lock index 64f46680..803e53fd 100644 --- a/.anvil.lock +++ b/.anvil.lock @@ -1,7 +1,7 @@ version = 1 tool = "anvil" tool_version = "0.5.0" -catalog_checksum = "sha256:074664f94a7fc0b87f7d29966452b7c9d957ca65771bf3e65724e3b8814da86a" +catalog_checksum = "sha256:2717eebd54ddfc2e1d1f8454dd0f901d7e634032a642334484b159f8b7e88274" [[file]] path = ".anvil/container/Containerfile" @@ -181,7 +181,7 @@ checksum = "sha256:d346399f288570066e53fd123baf05f7d4a57f17da8ee687a6d881204c5ba [[file]] path = "justfiles/anvil/checks/semver-check.just" -checksum = "sha256:0e2aaef4acd81363f2e0ce2c6b6c22bd5adc164e7dc597006d78006c6d42422e" +checksum = "sha256:13fcb4569e132b8922dc1c111a58ca081121125f29b0c855207bc818684ef2b5" [[file]] path = "justfiles/anvil/checks/spellcheck.just" 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 5d7dd3e4..98f4ca35 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 -# bin-only workspaces), intersect the affected set ($include) with that +# fails on bin-only workspaces), intersect the affected set ($include) 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 diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap index 5e30ef95..84e4f4c3 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap @@ -4271,7 +4271,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 -# bin-only workspaces), intersect the affected set ($include) with that +# fails on bin-only workspaces), intersect the affected set ($include) 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 diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap index 28901807..850f2da8 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap @@ -4150,7 +4150,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 -# bin-only workspaces), intersect the affected set ($include) with that +# fails on bin-only workspaces), intersect the affected set ($include) 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 diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap index 19a2a96d..ff0825cf 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap @@ -3023,7 +3023,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 -# bin-only workspaces), intersect the affected set ($include) with that +# fails on bin-only workspaces), intersect the affected set ($include) 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 diff --git a/justfiles/anvil/checks/semver-check.just b/justfiles/anvil/checks/semver-check.just index 5d7dd3e4..98f4ca35 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 -# bin-only workspaces), intersect the affected set ($include) with that +# fails on bin-only workspaces), intersect the affected set ($include) 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 From b754fa6fb26196b8c20f7759a9b27a139d68b85a Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Fri, 28 Aug 2026 13:47:03 +0200 Subject: [PATCH 10/17] fix(anvil): check non-publishable external types Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4112f005-775f-4c4b-88ed-db8f0c5d1cf1 --- .anvil.lock | 6 ++-- crates/cargo-anvil/docs/design/checks.md | 2 +- .../anvil/checks/external-types.just | 28 ++++------------ .../templates/justfiles/anvil/helpers.just | 4 +-- crates/cargo-anvil/tests/recipe_contracts.rs | 6 ++-- .../snapshots/snapshots__ado_backend.snap | 32 +++++-------------- .../snapshots/snapshots__github_backend.snap | 32 +++++-------------- .../snapshots/snapshots__local_only.snap | 32 +++++-------------- justfiles/anvil/checks/external-types.just | 28 ++++------------ justfiles/anvil/helpers.just | 4 +-- 10 files changed, 47 insertions(+), 127 deletions(-) diff --git a/.anvil.lock b/.anvil.lock index 803e53fd..258b02dd 100644 --- a/.anvil.lock +++ b/.anvil.lock @@ -1,7 +1,7 @@ version = 1 tool = "anvil" tool_version = "0.5.0" -catalog_checksum = "sha256:2717eebd54ddfc2e1d1f8454dd0f901d7e634032a642334484b159f8b7e88274" +catalog_checksum = "sha256:dd1853eac724c9550e1eb62b5e67c3b816e15a3be8017652239da5d71813d3ad" [[file]] path = ".anvil/container/Containerfile" @@ -129,7 +129,7 @@ checksum = "sha256:0e84a7b43ff07608c624ece03d46ddd58c629ebd5f6a5684f3b43b7190026 [[file]] path = "justfiles/anvil/checks/external-types.just" -checksum = "sha256:46411f5c538c2544889054bb69d9ca78b04db59a82d61d39a1a8dca9ebec6839" +checksum = "sha256:d003f1d82898706c136f42c3156d5ecc8f288c18f125c964b9dca1d24fa93d67" [[file]] path = "justfiles/anvil/checks/fmt.just" @@ -233,7 +233,7 @@ checksum = "sha256:a00153eda6b55d4db33e8019fa74f4a905f9c80b10b0db10955179c0af3e9 [[file]] path = "justfiles/anvil/helpers.just" -checksum = "sha256:57462cd3805fb5d2892d206ee8ccb704b6ded8b2953ff240264d18d4467dc7a5" +checksum = "sha256:72a7b89f85b5bdc55f8c8c4699b78e877a6c78d0bb39e57b57518f5e289858fa" [[file]] path = "justfiles/anvil/impact.just" diff --git a/crates/cargo-anvil/docs/design/checks.md b/crates/cargo-anvil/docs/design/checks.md index ef205fad..45eb1452 100644 --- a/crates/cargo-anvil/docs/design/checks.md +++ b/crates/cargo-anvil/docs/design/checks.md @@ -171,7 +171,7 @@ that provided the strongest version of the check. | `audit` | `cargo audit` | oxidizer | | `udeps` | `cargo + udeps --workspace --all-features` run **twice** — once with default targets (lib + bins) and once with `--all-targets`. cargo-udeps only analyzes the targets it's told to, and each run catches a variant the other masks: the default-targets run surfaces a dep in `[dependencies]` referenced only by tests/benches/examples (it should be a dev-dep; `--all-targets` would see it as "used"), while the `--all-targets` run surfaces unused `[dev-dependencies]` (never compiled by the default-targets run). Together they cover unused deps, unused dev-deps, and deps that should be dev-deps. | oxidizer, oxidizer-github | | `semver-check` | `cargo semver-checks --baseline-rev ` per affected publishable library crate. Crates with `publish = false` and bin-only crates have no registry consumer contract and are skipped. The PR target is the baseline. Exit 100 is a completed check with deny-level findings; exit 101 or another nonzero status means the comparison was inconclusive. Both outcomes write `target/anvil/comments/semver.md` and remain advisory, matching the repository's native `semver` job (`continue-on-error: true`). Proven rename and bin→lib transitions with no comparable baseline are skipped without a comment. Anvil preflight failures such as invalid current-workspace metadata or an unavailable baseline ref still fail because the recipe cannot establish what to compare. | oxidizer-github | -| `external-types` | `cargo + check-external-types --manifest-path` per publishable library crate (per-manifest because the tool has no `--workspace`/`--package`; bin-only and `publish = false` crates have no registry consumer contract and are skipped). Setup installs the catalog version but validation accepts newer installed tools. The selected nightly is tested with the catalog version; an incompatible newer tool fails closed with a tool/nightly compatibility diagnostic rather than silently selecting a different schema. | oxidizer-github | +| `external-types` | `cargo + check-external-types --manifest-path` per library crate (per-manifest because the tool has no `--workspace`/`--package`; bin-only crates have no public API surface and are skipped). Setup installs the catalog version but validation accepts newer installed tools. The selected nightly is tested with the catalog version; an incompatible newer tool fails closed with a tool/nightly compatibility diagnostic rather than silently selecting a different schema. | oxidizer-github | ### `pr-slow` 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 e50c2d8d..33c0593d 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/external-types.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/external-types.just @@ -9,10 +9,9 @@ # Affected tier (lints public API of changed crates and reverse dependencies). # # cargo-check-external-types is per-manifest: no --package/--workspace, -# only --manifest-path. Iterate the affected publishable library crates and run -# the tool once each, pointing at the crate's Cargo.toml. Bin-only crates have -# no public API surface, while `publish = false` crates have no registry -# consumer contract; both are skipped. +# 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 @@ -30,9 +29,8 @@ anvil-external-types: anvil-external-types-validate-prereqs anvil-impact exit 0 } $pkg = @(if ($include) { -split $include } else { '--workspace' }) - # Build pkg-name -> manifest-path maps for publishable and non-publishable libraries. + # Build pkg-name -> manifest-path map, restricted to library crates. $libPkgs = @{} - $nonPublishableLibPkgs = @{} $metadataJson = & cargo metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-external-types: cargo metadata failed' @@ -46,19 +44,13 @@ anvil-external-types: anvil-external-types-validate-prereqs anvil-impact } foreach ($p in $meta.packages) { if ($p.targets | Where-Object { $_.kind -contains 'lib' }) { - if ($p.publish -is [array] -and $p.publish.Count -eq 0) { - $nonPublishableLibPkgs[$p.name] = $p.manifest_path - } else { - $libPkgs[$p.name] = $p.manifest_path - } + $libPkgs[$p.name] = $p.manifest_path } } # Decide which packages to check. $packages = @() - $nonPublishablePackages = @() if ($pkg -contains '--workspace') { $packages = $libPkgs.Keys - $nonPublishablePackages = $nonPublishableLibPkgs.Keys } else { for ($i = 0; $i -lt $pkg.Count; $i++) { if ($pkg[$i] -eq '--package' -and ($i + 1) -lt $pkg.Count) { @@ -67,22 +59,14 @@ anvil-external-types: anvil-external-types-validate-prereqs anvil-impact # this manifest-path map keys on. $name = ($pkg[$i + 1] -split '@', 2)[0] if ($libPkgs.ContainsKey($name)) { $packages += $name } - if ($nonPublishableLibPkgs.ContainsKey($name)) { $nonPublishablePackages += $name } $i++ } } } if ($packages.Count -eq 0) { - if ($nonPublishablePackages.Count -gt 0) { - Write-Host "anvil-external-types: no affected publishable library crates; skipping ($($nonPublishablePackages.Count) non-publishable library crate(s) excluded)" - } else { - Write-Host 'anvil-external-types: no affected library crates; skipping' - } + Write-Host 'anvil-external-types: no affected library crates; skipping' exit 0 } - if ($nonPublishablePackages.Count -gt 0) { - Write-Host "anvil-external-types: excluding $($nonPublishablePackages.Count) non-publishable library crate(s)" - } 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) { diff --git a/crates/cargo-anvil/templates/justfiles/anvil/helpers.just b/crates/cargo-anvil/templates/justfiles/anvil/helpers.just index 78cea80a..57253121 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/helpers.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/helpers.just @@ -13,8 +13,8 @@ # - "modified": only run when at least one package's source files # changed in the diff. The tier is only a skip gate: once admitted, # the check runs its complete selected scope rather than splicing -# package arguments. That scope may be workspace members (cargo each), -# the workspace (cargo heather), or a directory (cargo spellcheck). +# package arguments. That scope may be workspace members (cargo each) +# or the whole workspace (cargo heather, cargo sort). # # - "affected": run on the affected set (modified ∪ reverse-deps within the # workspace). The tool takes --package; the recipe splices the resolved diff --git a/crates/cargo-anvil/tests/recipe_contracts.rs b/crates/cargo-anvil/tests/recipe_contracts.rs index 6d375483..2d691d1f 100644 --- a/crates/cargo-anvil/tests/recipe_contracts.rs +++ b/crates/cargo-anvil/tests/recipe_contracts.rs @@ -769,7 +769,7 @@ fn fmt_propagates_cargo_each_failure() { } #[test] -fn external_types_checks_every_publishable_library_and_reports_non_publishable_ones() { +fn external_types_checks_every_library_including_non_publishable_ones() { if !tools_available() { return; } @@ -798,13 +798,14 @@ fn external_types_checks_every_publishable_library_and_reports_non_publishable_o ); assert!( output.status.success(), - "non-publishable library filtering failed\nstdout:\n{}\nstderr:\n{}", + "library selection failed\nstdout:\n{}\nstderr:\n{}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr) ); let commands = std::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") @@ -826,7 +827,6 @@ fn external_types_checks_every_publishable_library_and_reports_non_publishable_o .collect::>(), expected_manifests.into_iter().collect() ); - assert!(String::from_utf8_lossy(&output.stdout).contains("excluding 1 non-publishable library crate")); } #[test] diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap index 84e4f4c3..3d69e668 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap @@ -3276,10 +3276,9 @@ anvil-examples-validate-prereqs: anvil-tool-rustc-validate-prereqs # Affected tier (lints public API of changed crates and reverse dependencies). # # cargo-check-external-types is per-manifest: no --package/--workspace, -# only --manifest-path. Iterate the affected publishable library crates and run -# the tool once each, pointing at the crate's Cargo.toml. Bin-only crates have -# no public API surface, while `publish = false` crates have no registry -# consumer contract; both are skipped. +# 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 @@ -3297,9 +3296,8 @@ anvil-external-types: anvil-external-types-validate-prereqs anvil-impact exit 0 } $pkg = @(if ($include) { -split $include } else { '--workspace' }) - # Build pkg-name -> manifest-path maps for publishable and non-publishable libraries. + # Build pkg-name -> manifest-path map, restricted to library crates. $libPkgs = @{} - $nonPublishableLibPkgs = @{} $metadataJson = & cargo metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-external-types: cargo metadata failed' @@ -3313,19 +3311,13 @@ anvil-external-types: anvil-external-types-validate-prereqs anvil-impact } foreach ($p in $meta.packages) { if ($p.targets | Where-Object { $_.kind -contains 'lib' }) { - if ($p.publish -is [array] -and $p.publish.Count -eq 0) { - $nonPublishableLibPkgs[$p.name] = $p.manifest_path - } else { - $libPkgs[$p.name] = $p.manifest_path - } + $libPkgs[$p.name] = $p.manifest_path } } # Decide which packages to check. $packages = @() - $nonPublishablePackages = @() if ($pkg -contains '--workspace') { $packages = $libPkgs.Keys - $nonPublishablePackages = $nonPublishableLibPkgs.Keys } else { for ($i = 0; $i -lt $pkg.Count; $i++) { if ($pkg[$i] -eq '--package' -and ($i + 1) -lt $pkg.Count) { @@ -3334,22 +3326,14 @@ anvil-external-types: anvil-external-types-validate-prereqs anvil-impact # this manifest-path map keys on. $name = ($pkg[$i + 1] -split '@', 2)[0] if ($libPkgs.ContainsKey($name)) { $packages += $name } - if ($nonPublishableLibPkgs.ContainsKey($name)) { $nonPublishablePackages += $name } $i++ } } } if ($packages.Count -eq 0) { - if ($nonPublishablePackages.Count -gt 0) { - Write-Host "anvil-external-types: no affected publishable library crates; skipping ($($nonPublishablePackages.Count) non-publishable library crate(s) excluded)" - } else { - Write-Host 'anvil-external-types: no affected library crates; skipping' - } + Write-Host 'anvil-external-types: no affected library crates; skipping' exit 0 } - if ($nonPublishablePackages.Count -gt 0) { - Write-Host "anvil-external-types: excluding $($nonPublishablePackages.Count) non-publishable library crate(s)" - } 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) { @@ -5005,8 +4989,8 @@ anvil-scheduled-test-validate-prereqs: \ # - "modified": only run when at least one package's source files # changed in the diff. The tier is only a skip gate: once admitted, # the check runs its complete selected scope rather than splicing -# package arguments. That scope may be workspace members (cargo each), -# the workspace (cargo heather), or a directory (cargo spellcheck). +# package arguments. That scope may be workspace members (cargo each) +# or the whole workspace (cargo heather, cargo sort). # # - "affected": run on the affected set (modified ∪ reverse-deps within the # workspace). The tool takes --package; the recipe splices the resolved diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap index 850f2da8..f00e00d9 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap @@ -3155,10 +3155,9 @@ anvil-examples-validate-prereqs: anvil-tool-rustc-validate-prereqs # Affected tier (lints public API of changed crates and reverse dependencies). # # cargo-check-external-types is per-manifest: no --package/--workspace, -# only --manifest-path. Iterate the affected publishable library crates and run -# the tool once each, pointing at the crate's Cargo.toml. Bin-only crates have -# no public API surface, while `publish = false` crates have no registry -# consumer contract; both are skipped. +# 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 @@ -3176,9 +3175,8 @@ anvil-external-types: anvil-external-types-validate-prereqs anvil-impact exit 0 } $pkg = @(if ($include) { -split $include } else { '--workspace' }) - # Build pkg-name -> manifest-path maps for publishable and non-publishable libraries. + # Build pkg-name -> manifest-path map, restricted to library crates. $libPkgs = @{} - $nonPublishableLibPkgs = @{} $metadataJson = & cargo metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-external-types: cargo metadata failed' @@ -3192,19 +3190,13 @@ anvil-external-types: anvil-external-types-validate-prereqs anvil-impact } foreach ($p in $meta.packages) { if ($p.targets | Where-Object { $_.kind -contains 'lib' }) { - if ($p.publish -is [array] -and $p.publish.Count -eq 0) { - $nonPublishableLibPkgs[$p.name] = $p.manifest_path - } else { - $libPkgs[$p.name] = $p.manifest_path - } + $libPkgs[$p.name] = $p.manifest_path } } # Decide which packages to check. $packages = @() - $nonPublishablePackages = @() if ($pkg -contains '--workspace') { $packages = $libPkgs.Keys - $nonPublishablePackages = $nonPublishableLibPkgs.Keys } else { for ($i = 0; $i -lt $pkg.Count; $i++) { if ($pkg[$i] -eq '--package' -and ($i + 1) -lt $pkg.Count) { @@ -3213,22 +3205,14 @@ anvil-external-types: anvil-external-types-validate-prereqs anvil-impact # this manifest-path map keys on. $name = ($pkg[$i + 1] -split '@', 2)[0] if ($libPkgs.ContainsKey($name)) { $packages += $name } - if ($nonPublishableLibPkgs.ContainsKey($name)) { $nonPublishablePackages += $name } $i++ } } } if ($packages.Count -eq 0) { - if ($nonPublishablePackages.Count -gt 0) { - Write-Host "anvil-external-types: no affected publishable library crates; skipping ($($nonPublishablePackages.Count) non-publishable library crate(s) excluded)" - } else { - Write-Host 'anvil-external-types: no affected library crates; skipping' - } + Write-Host 'anvil-external-types: no affected library crates; skipping' exit 0 } - if ($nonPublishablePackages.Count -gt 0) { - Write-Host "anvil-external-types: excluding $($nonPublishablePackages.Count) non-publishable library crate(s)" - } 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) { @@ -4884,8 +4868,8 @@ anvil-scheduled-test-validate-prereqs: \ # - "modified": only run when at least one package's source files # changed in the diff. The tier is only a skip gate: once admitted, # the check runs its complete selected scope rather than splicing -# package arguments. That scope may be workspace members (cargo each), -# the workspace (cargo heather), or a directory (cargo spellcheck). +# package arguments. That scope may be workspace members (cargo each) +# or the whole workspace (cargo heather, cargo sort). # # - "affected": run on the affected set (modified ∪ reverse-deps within the # workspace). The tool takes --package; the recipe splices the resolved diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap index ff0825cf..f89f2946 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap @@ -2028,10 +2028,9 @@ anvil-examples-validate-prereqs: anvil-tool-rustc-validate-prereqs # Affected tier (lints public API of changed crates and reverse dependencies). # # cargo-check-external-types is per-manifest: no --package/--workspace, -# only --manifest-path. Iterate the affected publishable library crates and run -# the tool once each, pointing at the crate's Cargo.toml. Bin-only crates have -# no public API surface, while `publish = false` crates have no registry -# consumer contract; both are skipped. +# 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 @@ -2049,9 +2048,8 @@ anvil-external-types: anvil-external-types-validate-prereqs anvil-impact exit 0 } $pkg = @(if ($include) { -split $include } else { '--workspace' }) - # Build pkg-name -> manifest-path maps for publishable and non-publishable libraries. + # Build pkg-name -> manifest-path map, restricted to library crates. $libPkgs = @{} - $nonPublishableLibPkgs = @{} $metadataJson = & cargo metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-external-types: cargo metadata failed' @@ -2065,19 +2063,13 @@ anvil-external-types: anvil-external-types-validate-prereqs anvil-impact } foreach ($p in $meta.packages) { if ($p.targets | Where-Object { $_.kind -contains 'lib' }) { - if ($p.publish -is [array] -and $p.publish.Count -eq 0) { - $nonPublishableLibPkgs[$p.name] = $p.manifest_path - } else { - $libPkgs[$p.name] = $p.manifest_path - } + $libPkgs[$p.name] = $p.manifest_path } } # Decide which packages to check. $packages = @() - $nonPublishablePackages = @() if ($pkg -contains '--workspace') { $packages = $libPkgs.Keys - $nonPublishablePackages = $nonPublishableLibPkgs.Keys } else { for ($i = 0; $i -lt $pkg.Count; $i++) { if ($pkg[$i] -eq '--package' -and ($i + 1) -lt $pkg.Count) { @@ -2086,22 +2078,14 @@ anvil-external-types: anvil-external-types-validate-prereqs anvil-impact # this manifest-path map keys on. $name = ($pkg[$i + 1] -split '@', 2)[0] if ($libPkgs.ContainsKey($name)) { $packages += $name } - if ($nonPublishableLibPkgs.ContainsKey($name)) { $nonPublishablePackages += $name } $i++ } } } if ($packages.Count -eq 0) { - if ($nonPublishablePackages.Count -gt 0) { - Write-Host "anvil-external-types: no affected publishable library crates; skipping ($($nonPublishablePackages.Count) non-publishable library crate(s) excluded)" - } else { - Write-Host 'anvil-external-types: no affected library crates; skipping' - } + Write-Host 'anvil-external-types: no affected library crates; skipping' exit 0 } - if ($nonPublishablePackages.Count -gt 0) { - Write-Host "anvil-external-types: excluding $($nonPublishablePackages.Count) non-publishable library crate(s)" - } 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) { @@ -3757,8 +3741,8 @@ anvil-scheduled-test-validate-prereqs: \ # - "modified": only run when at least one package's source files # changed in the diff. The tier is only a skip gate: once admitted, # the check runs its complete selected scope rather than splicing -# package arguments. That scope may be workspace members (cargo each), -# the workspace (cargo heather), or a directory (cargo spellcheck). +# package arguments. That scope may be workspace members (cargo each) +# or the whole workspace (cargo heather, cargo sort). # # - "affected": run on the affected set (modified ∪ reverse-deps within the # workspace). The tool takes --package; the recipe splices the resolved diff --git a/justfiles/anvil/checks/external-types.just b/justfiles/anvil/checks/external-types.just index e50c2d8d..33c0593d 100644 --- a/justfiles/anvil/checks/external-types.just +++ b/justfiles/anvil/checks/external-types.just @@ -9,10 +9,9 @@ # Affected tier (lints public API of changed crates and reverse dependencies). # # cargo-check-external-types is per-manifest: no --package/--workspace, -# only --manifest-path. Iterate the affected publishable library crates and run -# the tool once each, pointing at the crate's Cargo.toml. Bin-only crates have -# no public API surface, while `publish = false` crates have no registry -# consumer contract; both are skipped. +# 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 @@ -30,9 +29,8 @@ anvil-external-types: anvil-external-types-validate-prereqs anvil-impact exit 0 } $pkg = @(if ($include) { -split $include } else { '--workspace' }) - # Build pkg-name -> manifest-path maps for publishable and non-publishable libraries. + # Build pkg-name -> manifest-path map, restricted to library crates. $libPkgs = @{} - $nonPublishableLibPkgs = @{} $metadataJson = & cargo metadata --no-deps --format-version 1 if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-external-types: cargo metadata failed' @@ -46,19 +44,13 @@ anvil-external-types: anvil-external-types-validate-prereqs anvil-impact } foreach ($p in $meta.packages) { if ($p.targets | Where-Object { $_.kind -contains 'lib' }) { - if ($p.publish -is [array] -and $p.publish.Count -eq 0) { - $nonPublishableLibPkgs[$p.name] = $p.manifest_path - } else { - $libPkgs[$p.name] = $p.manifest_path - } + $libPkgs[$p.name] = $p.manifest_path } } # Decide which packages to check. $packages = @() - $nonPublishablePackages = @() if ($pkg -contains '--workspace') { $packages = $libPkgs.Keys - $nonPublishablePackages = $nonPublishableLibPkgs.Keys } else { for ($i = 0; $i -lt $pkg.Count; $i++) { if ($pkg[$i] -eq '--package' -and ($i + 1) -lt $pkg.Count) { @@ -67,22 +59,14 @@ anvil-external-types: anvil-external-types-validate-prereqs anvil-impact # this manifest-path map keys on. $name = ($pkg[$i + 1] -split '@', 2)[0] if ($libPkgs.ContainsKey($name)) { $packages += $name } - if ($nonPublishableLibPkgs.ContainsKey($name)) { $nonPublishablePackages += $name } $i++ } } } if ($packages.Count -eq 0) { - if ($nonPublishablePackages.Count -gt 0) { - Write-Host "anvil-external-types: no affected publishable library crates; skipping ($($nonPublishablePackages.Count) non-publishable library crate(s) excluded)" - } else { - Write-Host 'anvil-external-types: no affected library crates; skipping' - } + Write-Host 'anvil-external-types: no affected library crates; skipping' exit 0 } - if ($nonPublishablePackages.Count -gt 0) { - Write-Host "anvil-external-types: excluding $($nonPublishablePackages.Count) non-publishable library crate(s)" - } 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) { diff --git a/justfiles/anvil/helpers.just b/justfiles/anvil/helpers.just index 78cea80a..57253121 100644 --- a/justfiles/anvil/helpers.just +++ b/justfiles/anvil/helpers.just @@ -13,8 +13,8 @@ # - "modified": only run when at least one package's source files # changed in the diff. The tier is only a skip gate: once admitted, # the check runs its complete selected scope rather than splicing -# package arguments. That scope may be workspace members (cargo each), -# the workspace (cargo heather), or a directory (cargo spellcheck). +# package arguments. That scope may be workspace members (cargo each) +# or the whole workspace (cargo heather, cargo sort). # # - "affected": run on the affected set (modified ∪ reverse-deps within the # workspace). The tool takes --package; the recipe splices the resolved From 0594fd4903c428dd343313a2d2ada1df4f026a15 Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Sat, 29 Aug 2026 01:33:34 +0200 Subject: [PATCH 11/17] fix(anvil): bound loom exploration in CI Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4112f005-775f-4c4b-88ed-db8f0c5d1cf1 --- .anvil.lock | 46 +++++++++++++++++-- crates/cargo-anvil/docs/design/checks.md | 2 +- .../justfiles/anvil/checks/loom.just | 7 +++ crates/cargo-anvil/tests/recipe_contracts.rs | 14 ++++++ justfiles/anvil/checks/loom.just | 7 +++ 5 files changed, 72 insertions(+), 4 deletions(-) diff --git a/.anvil.lock b/.anvil.lock index 258b02dd..1ff82a82 100644 --- a/.anvil.lock +++ b/.anvil.lock @@ -1,7 +1,7 @@ version = 1 tool = "anvil" tool_version = "0.5.0" -catalog_checksum = "sha256:dd1853eac724c9550e1eb62b5e67c3b816e15a3be8017652239da5d71813d3ad" +catalog_checksum = "sha256:00ad3a3081a344bf215dfc3a94a4d7216b4784dbc002887585294d46327405f4" [[file]] path = ".anvil/container/Containerfile" @@ -145,7 +145,7 @@ checksum = "sha256:689f7d5b1eaea32672f8d0b9fff0994cc03bc56fb8ef282a4286f930fc503 [[file]] path = "justfiles/anvil/checks/loom.just" -checksum = "sha256:4bf509bcca3507aafd9848ce19ea3691ab2cd77de05b344e042f3ba2c300e35b" +checksum = "sha256:8feb9bcb11c314f4c7825854cc7dd10b184d91ad520dd736addd098d3c7bb1bf" [[file]] path = "justfiles/anvil/checks/miri-race-coverage.just" @@ -181,7 +181,7 @@ checksum = "sha256:d346399f288570066e53fd123baf05f7d4a57f17da8ee687a6d881204c5ba [[file]] path = "justfiles/anvil/checks/semver-check.just" -checksum = "sha256:13fcb4569e132b8922dc1c111a58ca081121125f29b0c855207bc818684ef2b5" +checksum = "sha256:52b31bec3c5d271b25f46d9b1dd6aca4b84e394fd8cf46bfa5386a3df7ed3999" [[file]] path = "justfiles/anvil/checks/spellcheck.just" @@ -324,6 +324,46 @@ host = "crates/cargo-ensure-no-default-features/Cargo.toml" id = "anvil-lints" checksum = "sha256:2dd7c0f21339fd17092b8dedfe924aa86732c3520baab84f914c2d8f4103ac40" +[[region]] +host = "crates/cargo-gamma-attrs-impl/Cargo.toml" +id = "anvil-lints" +checksum = "sha256:2dd7c0f21339fd17092b8dedfe924aa86732c3520baab84f914c2d8f4103ac40" + +[[region]] +host = "crates/cargo-gamma-attrs/Cargo.toml" +id = "anvil-lints" +checksum = "sha256:2dd7c0f21339fd17092b8dedfe924aa86732c3520baab84f914c2d8f4103ac40" + +[[region]] +host = "crates/cargo-gamma-engine/Cargo.toml" +id = "anvil-lints" +checksum = "sha256:2dd7c0f21339fd17092b8dedfe924aa86732c3520baab84f914c2d8f4103ac40" + +[[region]] +host = "crates/cargo-gamma-lib/Cargo.toml" +id = "anvil-lints" +checksum = "sha256:2dd7c0f21339fd17092b8dedfe924aa86732c3520baab84f914c2d8f4103ac40" + +[[region]] +host = "crates/cargo-gamma-process/Cargo.toml" +id = "anvil-lints" +checksum = "sha256:2dd7c0f21339fd17092b8dedfe924aa86732c3520baab84f914c2d8f4103ac40" + +[[region]] +host = "crates/cargo-gamma-rt/Cargo.toml" +id = "anvil-lints" +checksum = "sha256:2dd7c0f21339fd17092b8dedfe924aa86732c3520baab84f914c2d8f4103ac40" + +[[region]] +host = "crates/cargo-gamma-unsafe/Cargo.toml" +id = "anvil-lints" +checksum = "sha256:2dd7c0f21339fd17092b8dedfe924aa86732c3520baab84f914c2d8f4103ac40" + +[[region]] +host = "crates/cargo-gamma/Cargo.toml" +id = "anvil-lints" +checksum = "sha256:2dd7c0f21339fd17092b8dedfe924aa86732c3520baab84f914c2d8f4103ac40" + [[region]] host = "crates/cargo-heather/Cargo.toml" id = "anvil-lints" diff --git a/crates/cargo-anvil/docs/design/checks.md b/crates/cargo-anvil/docs/design/checks.md index 48a3c1ad..bb9ff396 100644 --- a/crates/cargo-anvil/docs/design/checks.md +++ b/crates/cargo-anvil/docs/design/checks.md @@ -198,7 +198,7 @@ This is the same set of checks that used to live in the standalone `pr-test` gro |-----------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------| | `miri` | `cargo + miri test --all-features --tests` over the impact-affected packages. Uses libtest (one process per test binary), **not** `cargo miri nextest run`: under miri, nextest's process-per-test model pays miri's expensive std-initialization re-interpretation for *every* test and roughly doubles wall-time on a large suite (the dominant cost on the PR critical path). `--tests` runs lib/bin unit tests and integration tests (the same target set nextest ran) while excluding doctests, which miri can't run; it is used in preference to `--lib --tests` because `--lib` errors with "no library targets found" on a bin-only affected package under impact scoping. Slow tests opt out per-test with `#[cfg_attr(miri, ignore)]` -- anvil doesn't pass exotic `MIRIFLAGS`; the per-test opt-out is the canonical mechanism. libtest exits 0 when a binary's tests are all skipped, so no `--no-tests=pass` workaround is needed. The recipe reads its scope from the `target/anvil/impact/` cache via `_anvil-impact-include`; because it depends on `anvil-impact`, a clean direct or PR invocation is impact-scoped (unaffected packages are skipped). It runs the full workspace only when scoping is off — the scheduled/full tiers set `ANVIL_IMPACT=off`, and a dirty local tree widens for safety. | oxidizer, oxidizer-github | | `careful` | `cargo + careful test --all-features --locked` over the impact-affected packages. cargo-careful uses a debug-instrumented std in a stable cache path. Because Cargo fingerprints the sysroot path rather than its contents, the recipe records the actual `rustc -vV` and SHA-256 of the resolved `cargo-careful` executable in `target/anvil/careful-sysroot.id`; either changing triggers `cargo clean`. The executable hash is used because cargo-careful rejects version-only invocations. This remains correct when validation accepts a newer installed cargo-careful. | oxidizer-github | -| `loom` | For each `[[test]]` target that declares `required-features = ["loom"]`, `cargo test -p --release --all-features --locked --test -- --test-threads=1` with `RUSTFLAGS="--cfg loom"`. [`loom`](https://crates.io/crates/loom) is a permutation-based concurrency model checker that explores thread interleavings. Targets are detected **structurally** from `cargo metadata` (a test target whose `kind` contains `test` and whose `required-features` contains `loom`) -- not via a filename/cfg/comment heuristic -- and only those targets run, so loom never touches a crate's ordinary tests. The `loom` feature selects the target (`required-features`); `--cfg loom` activates loom (source swaps std↔loom atomics on `#[cfg(loom)]`, and `[target.'cfg(loom)'.dependencies] loom` links only under the cfg) -- both are required. Scoped per-package with `-p` (never `--workspace`) so the global cfg never leaks into deps reachable only through other members. **Fail-loud**: a crate that declares loom support (a `loom` feature or a `cfg(loom)` dependency) but exposes no such test target errors out rather than silently no-opping. When no crate ships a loom target the recipe skips (exit 0). | oxidizer-github | +| `loom` | For each `[[test]]` target that declares `required-features = ["loom"]`, `cargo test -p --release --all-features --locked --test -- --test-threads=1` with `RUSTFLAGS="--cfg loom"`. [`loom`](https://crates.io/crates/loom) is a permutation-based concurrency model checker that explores thread interleavings. CI uses Loom's documented two-preemption bound by default, preventing one large model from consuming the whole job while retaining systematic coverage; adopters can override `LOOM_MAX_PREEMPTIONS` for deeper or exhaustive runs. Targets are detected **structurally** from `cargo metadata` (a test target whose `kind` contains `test` and whose `required-features` contains `loom`) -- not via a filename/cfg/comment heuristic -- and only those targets run, so loom never touches a crate's ordinary tests. The `loom` feature selects the target (`required-features`); `--cfg loom` activates loom (source swaps std↔loom atomics on `#[cfg(loom)]`, and `[target.'cfg(loom)'.dependencies] loom` links only under the cfg) -- both are required. Scoped per-package with `-p` (never `--workspace`) so the global cfg never leaks into deps reachable only through other members. **Fail-loud**: a crate that declares loom support (a `loom` feature or a `cfg(loom)` dependency) but exposes no such test target errors out rather than silently no-opping. When no crate ships a loom target the recipe skips (exit 0). | oxidizer-github | | `bolero` | Uses the catalog nightly and release profile consistently to discover targets one package at a time, then runs each affected libfuzzer target for 60 seconds on Linux. Explicitly selecting `release` avoids cargo-bolero's implicit, adopter-defined `fuzz` profile and matches target execution. Per-package discovery is required because `cargo-bolero list` accepts only one `--package`; local whole-workspace runs enumerate workspace members before discovery. A successful empty discovery is a no-op; metadata, discovery, or parsing failure fails the check. Non-Linux hosts skip because cargo-bolero's native dependencies are unsupported there, while harnesses still run as ordinary tests. | oxidizer-github | #### `pr-mutants` (mutation testing) diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/loom.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/loom.just index 0f88aba6..8ecd3e9e 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/loom.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/loom.just @@ -33,6 +33,9 @@ # only through *other* members (e.g. concurrent-queue), and run # single-threaded (loom drives its own scheduling). --release per the # loom docs: permutation exploration is wall-clock dominated by the SUT. +# CI defaults Loom to its documented two-preemption bound so one large +# state space cannot consume the whole job. Adopters can override the +# bound (including with a higher value) through LOOM_MAX_PREEMPTIONS. # # Fail-loud guard: a crate that *declares* loom support -- a `loom` # feature or a `cfg(loom)`-gated dependency -- but exposes no @@ -106,6 +109,10 @@ anvil-loom: anvil-loom-validate-prereqs anvil-impact } $env:RUSTFLAGS = "--cfg loom $($env:RUSTFLAGS)".Trim() + if ([string]::IsNullOrWhiteSpace($env:LOOM_MAX_PREEMPTIONS)) { + $env:LOOM_MAX_PREEMPTIONS = '2' + } + Write-Host "anvil-loom: maximum preemptions per execution: $env:LOOM_MAX_PREEMPTIONS" foreach ($lt in $loomTargets) { Write-Host "anvil-loom: $($lt.pkg) :: $($lt.target)" & cargo test -p $lt.pkg --release --all-features --locked --test $lt.target -- --test-threads=1 diff --git a/crates/cargo-anvil/tests/recipe_contracts.rs b/crates/cargo-anvil/tests/recipe_contracts.rs index 591c6a6a..22900ed6 100644 --- a/crates/cargo-anvil/tests/recipe_contracts.rs +++ b/crates/cargo-anvil/tests/recipe_contracts.rs @@ -23,10 +23,24 @@ 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 LLVM_COV: &str = include_str!("../templates/justfiles/anvil/checks/llvm-cov.just"); +const LOOM: &str = include_str!("../templates/justfiles/anvil/checks/loom.just"); const SEMVER: &str = include_str!("../templates/justfiles/anvil/checks/semver-check.just"); const EXTERNAL_TYPES: &str = include_str!("../templates/justfiles/anvil/checks/external-types.just"); const TOOLS: &str = include_str!("../templates/justfiles/anvil/tools.just"); const VERSIONS: &str = include_str!("../templates/justfiles/anvil/versions.just"); + +#[test] +fn loom_has_an_overridable_ci_preemption_bound() { + assert!( + LOOM.contains("IsNullOrWhiteSpace($env:LOOM_MAX_PREEMPTIONS)"), + "loom must preserve an adopter-provided exploration bound" + ); + assert!( + LOOM.contains("$env:LOOM_MAX_PREEMPTIONS = '2'"), + "loom must default CI to its documented two-preemption bound" + ); +} + const FAKE_CARGO_PS1: &str = r#" $joined = $args -join ' ' if ($env:FAKE_CARGO_LOG) { diff --git a/justfiles/anvil/checks/loom.just b/justfiles/anvil/checks/loom.just index 0f88aba6..8ecd3e9e 100644 --- a/justfiles/anvil/checks/loom.just +++ b/justfiles/anvil/checks/loom.just @@ -33,6 +33,9 @@ # only through *other* members (e.g. concurrent-queue), and run # single-threaded (loom drives its own scheduling). --release per the # loom docs: permutation exploration is wall-clock dominated by the SUT. +# CI defaults Loom to its documented two-preemption bound so one large +# state space cannot consume the whole job. Adopters can override the +# bound (including with a higher value) through LOOM_MAX_PREEMPTIONS. # # Fail-loud guard: a crate that *declares* loom support -- a `loom` # feature or a `cfg(loom)`-gated dependency -- but exposes no @@ -106,6 +109,10 @@ anvil-loom: anvil-loom-validate-prereqs anvil-impact } $env:RUSTFLAGS = "--cfg loom $($env:RUSTFLAGS)".Trim() + if ([string]::IsNullOrWhiteSpace($env:LOOM_MAX_PREEMPTIONS)) { + $env:LOOM_MAX_PREEMPTIONS = '2' + } + Write-Host "anvil-loom: maximum preemptions per execution: $env:LOOM_MAX_PREEMPTIONS" foreach ($lt in $loomTargets) { Write-Host "anvil-loom: $($lt.pkg) :: $($lt.target)" & cargo test -p $lt.pkg --release --all-features --locked --test $lt.target -- --test-threads=1 From d913346af06fc72bc84c62a88568065771b1559b Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Sat, 29 Aug 2026 01:41:25 +0200 Subject: [PATCH 12/17] test(anvil): refresh loom recipe snapshots Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4112f005-775f-4c4b-88ed-db8f0c5d1cf1 --- .../tests/snapshots/snapshots__ado_backend.snap | 7 +++++++ .../tests/snapshots/snapshots__github_backend.snap | 7 +++++++ .../cargo-anvil/tests/snapshots/snapshots__local_only.snap | 7 +++++++ 3 files changed, 21 insertions(+) diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap index 921e379d..ec9df779 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap @@ -3661,6 +3661,9 @@ anvil-llvm-cov-validate-prereqs: anvil-component-nightly-llvm-tools-validate-pre # only through *other* members (e.g. concurrent-queue), and run # single-threaded (loom drives its own scheduling). --release per the # loom docs: permutation exploration is wall-clock dominated by the SUT. +# CI defaults Loom to its documented two-preemption bound so one large +# state space cannot consume the whole job. Adopters can override the +# bound (including with a higher value) through LOOM_MAX_PREEMPTIONS. # # Fail-loud guard: a crate that *declares* loom support -- a `loom` # feature or a `cfg(loom)`-gated dependency -- but exposes no @@ -3734,6 +3737,10 @@ anvil-loom: anvil-loom-validate-prereqs anvil-impact } $env:RUSTFLAGS = "--cfg loom $($env:RUSTFLAGS)".Trim() + if ([string]::IsNullOrWhiteSpace($env:LOOM_MAX_PREEMPTIONS)) { + $env:LOOM_MAX_PREEMPTIONS = '2' + } + Write-Host "anvil-loom: maximum preemptions per execution: $env:LOOM_MAX_PREEMPTIONS" foreach ($lt in $loomTargets) { Write-Host "anvil-loom: $($lt.pkg) :: $($lt.target)" & cargo test -p $lt.pkg --release --all-features --locked --test $lt.target -- --test-threads=1 diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap index b4247c2b..e9a0ec2f 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap @@ -3540,6 +3540,9 @@ anvil-llvm-cov-validate-prereqs: anvil-component-nightly-llvm-tools-validate-pre # only through *other* members (e.g. concurrent-queue), and run # single-threaded (loom drives its own scheduling). --release per the # loom docs: permutation exploration is wall-clock dominated by the SUT. +# CI defaults Loom to its documented two-preemption bound so one large +# state space cannot consume the whole job. Adopters can override the +# bound (including with a higher value) through LOOM_MAX_PREEMPTIONS. # # Fail-loud guard: a crate that *declares* loom support -- a `loom` # feature or a `cfg(loom)`-gated dependency -- but exposes no @@ -3613,6 +3616,10 @@ anvil-loom: anvil-loom-validate-prereqs anvil-impact } $env:RUSTFLAGS = "--cfg loom $($env:RUSTFLAGS)".Trim() + if ([string]::IsNullOrWhiteSpace($env:LOOM_MAX_PREEMPTIONS)) { + $env:LOOM_MAX_PREEMPTIONS = '2' + } + Write-Host "anvil-loom: maximum preemptions per execution: $env:LOOM_MAX_PREEMPTIONS" foreach ($lt in $loomTargets) { Write-Host "anvil-loom: $($lt.pkg) :: $($lt.target)" & cargo test -p $lt.pkg --release --all-features --locked --test $lt.target -- --test-threads=1 diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap index 73beb512..e01f4f74 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap @@ -2413,6 +2413,9 @@ anvil-llvm-cov-validate-prereqs: anvil-component-nightly-llvm-tools-validate-pre # only through *other* members (e.g. concurrent-queue), and run # single-threaded (loom drives its own scheduling). --release per the # loom docs: permutation exploration is wall-clock dominated by the SUT. +# CI defaults Loom to its documented two-preemption bound so one large +# state space cannot consume the whole job. Adopters can override the +# bound (including with a higher value) through LOOM_MAX_PREEMPTIONS. # # Fail-loud guard: a crate that *declares* loom support -- a `loom` # feature or a `cfg(loom)`-gated dependency -- but exposes no @@ -2486,6 +2489,10 @@ anvil-loom: anvil-loom-validate-prereqs anvil-impact } $env:RUSTFLAGS = "--cfg loom $($env:RUSTFLAGS)".Trim() + if ([string]::IsNullOrWhiteSpace($env:LOOM_MAX_PREEMPTIONS)) { + $env:LOOM_MAX_PREEMPTIONS = '2' + } + Write-Host "anvil-loom: maximum preemptions per execution: $env:LOOM_MAX_PREEMPTIONS" foreach ($lt in $loomTargets) { Write-Host "anvil-loom: $($lt.pkg) :: $($lt.target)" & cargo test -p $lt.pkg --release --all-features --locked --test $lt.target -- --test-threads=1 From 29d5b3571855654da76ca42fa302b15b8eec4aaa Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Sat, 29 Aug 2026 02:16:14 +0200 Subject: [PATCH 13/17] fix(anvil): use supported bolero profile option Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4112f005-775f-4c4b-88ed-db8f0c5d1cf1 --- .anvil.lock | 4 ++-- .../templates/justfiles/anvil/checks/bolero.just | 2 +- crates/cargo-anvil/tests/recipe_contracts.rs | 13 ++++++++++++- .../tests/snapshots/snapshots__ado_backend.snap | 2 +- .../tests/snapshots/snapshots__github_backend.snap | 2 +- .../tests/snapshots/snapshots__local_only.snap | 2 +- justfiles/anvil/checks/bolero.just | 2 +- 7 files changed, 19 insertions(+), 8 deletions(-) diff --git a/.anvil.lock b/.anvil.lock index 1ff82a82..97540079 100644 --- a/.anvil.lock +++ b/.anvil.lock @@ -1,7 +1,7 @@ version = 1 tool = "anvil" tool_version = "0.5.0" -catalog_checksum = "sha256:00ad3a3081a344bf215dfc3a94a4d7216b4784dbc002887585294d46327405f4" +catalog_checksum = "sha256:f56108d6b5d2931341b900cb2fb06d3397dd7e379aa905bd4bffbf1f83e2817a" [[file]] path = ".anvil/container/Containerfile" @@ -85,7 +85,7 @@ checksum = "sha256:50f04b4ea6c99df8ad7434d320f34dccdb6db6a77b83e3090e35de0ca3a15 [[file]] path = "justfiles/anvil/checks/bolero.just" -checksum = "sha256:003032f39c781851b880c1d7e63b93573f5252cca9f785b98b04dfa2e858aff3" +checksum = "sha256:754827bb664723169b8d48a6f1e69f9122b5440ba0b94a91622f4d23313e2503" [[file]] path = "justfiles/anvil/checks/careful.just" diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/bolero.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/bolero.just index 37f7d21b..d23fb3a9 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/bolero.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/bolero.just @@ -103,7 +103,7 @@ anvil-bolero: anvil-bolero-validate-prereqs anvil-impact exit 1 } Write-Host "anvil-bolero: ${packageName}::${testName}" - & cargo '+{{ rust_nightly }}' bolero test --release --engine libfuzzer -T 60s --package $packageName $testName + & cargo '+{{ rust_nightly }}' bolero test --profile release --engine libfuzzer -T 60s --package $packageName $testName if ($LASTEXITCODE -ne 0) { $failed = $true } } if ($failed) { exit 1 } diff --git a/crates/cargo-anvil/tests/recipe_contracts.rs b/crates/cargo-anvil/tests/recipe_contracts.rs index 22900ed6..9d67eac6 100644 --- a/crates/cargo-anvil/tests/recipe_contracts.rs +++ b/crates/cargo-anvil/tests/recipe_contracts.rs @@ -19,7 +19,6 @@ use tempfile::TempDir; const HELPERS: &str = include_str!("../templates/justfiles/anvil/helpers.just"); const IMPACT: &str = include_str!("../templates/justfiles/anvil/impact.just"); -#[cfg(target_os = "linux")] const BOLERO: &str = include_str!("../templates/justfiles/anvil/checks/bolero.just"); const FMT: &str = include_str!("../templates/justfiles/anvil/checks/fmt.just"); const LLVM_COV: &str = include_str!("../templates/justfiles/anvil/checks/llvm-cov.just"); @@ -41,6 +40,18 @@ fn loom_has_an_overridable_ci_preemption_bound() { ); } +#[test] +fn bolero_uses_its_supported_release_profile_option() { + assert!( + BOLERO.contains("bolero test --profile release"), + "bolero execution must select the release profile with cargo-bolero's supported option" + ); + assert!( + !BOLERO.contains("bolero test --release"), + "cargo-bolero 0.13.4 does not accept Cargo's --release shorthand" + ); +} + const FAKE_CARGO_PS1: &str = r#" $joined = $args -join ' ' if ($env:FAKE_CARGO_LOG) { diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap index ec9df779..31635d13 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap @@ -2887,7 +2887,7 @@ anvil-bolero: anvil-bolero-validate-prereqs anvil-impact exit 1 } Write-Host "anvil-bolero: ${packageName}::${testName}" - & cargo '+{{ rust_nightly }}' bolero test --release --engine libfuzzer -T 60s --package $packageName $testName + & cargo '+{{ rust_nightly }}' bolero test --profile release --engine libfuzzer -T 60s --package $packageName $testName if ($LASTEXITCODE -ne 0) { $failed = $true } } if ($failed) { exit 1 } diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap index e9a0ec2f..52812e6d 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap @@ -2766,7 +2766,7 @@ anvil-bolero: anvil-bolero-validate-prereqs anvil-impact exit 1 } Write-Host "anvil-bolero: ${packageName}::${testName}" - & cargo '+{{ rust_nightly }}' bolero test --release --engine libfuzzer -T 60s --package $packageName $testName + & cargo '+{{ rust_nightly }}' bolero test --profile release --engine libfuzzer -T 60s --package $packageName $testName if ($LASTEXITCODE -ne 0) { $failed = $true } } if ($failed) { exit 1 } diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap index e01f4f74..78e19f16 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap @@ -1639,7 +1639,7 @@ anvil-bolero: anvil-bolero-validate-prereqs anvil-impact exit 1 } Write-Host "anvil-bolero: ${packageName}::${testName}" - & cargo '+{{ rust_nightly }}' bolero test --release --engine libfuzzer -T 60s --package $packageName $testName + & cargo '+{{ rust_nightly }}' bolero test --profile release --engine libfuzzer -T 60s --package $packageName $testName if ($LASTEXITCODE -ne 0) { $failed = $true } } if ($failed) { exit 1 } diff --git a/justfiles/anvil/checks/bolero.just b/justfiles/anvil/checks/bolero.just index 37f7d21b..d23fb3a9 100644 --- a/justfiles/anvil/checks/bolero.just +++ b/justfiles/anvil/checks/bolero.just @@ -103,7 +103,7 @@ anvil-bolero: anvil-bolero-validate-prereqs anvil-impact exit 1 } Write-Host "anvil-bolero: ${packageName}::${testName}" - & cargo '+{{ rust_nightly }}' bolero test --release --engine libfuzzer -T 60s --package $packageName $testName + & cargo '+{{ rust_nightly }}' bolero test --profile release --engine libfuzzer -T 60s --package $packageName $testName if ($LASTEXITCODE -ne 0) { $failed = $true } } if ($failed) { exit 1 } From d8a7d95b3daa9260bb6fa2dd87b0207bcb58dcaa Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Sat, 29 Aug 2026 03:02:25 +0200 Subject: [PATCH 14/17] fix(gamma): enable bolero libfuzzer support Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4112f005-775f-4c4b-88ed-db8f0c5d1cf1 --- Cargo.lock | 3 +++ Cargo.toml | 2 +- crates/cargo-anvil/docs/design/checks.md | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d88661cc..084b8503 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -310,7 +310,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98a5782f2650f80d533f58ec339c6dce4cc5428f9c2755894f98156f52af81f2" dependencies = [ "bolero-generator-derive", + "either", + "getrandom 0.3.4", "rand_core 0.9.5", + "rand_xoshiro", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 67ee14a9..28cff03c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,7 @@ anyhow = { version = "1.0.100", default-features = false } assert_cmd = { version = "2.2.0", default-features = false } async-once-cell = { version = "0.5", default-features = false } blake3 = { version = "1.8.5", default-features = false } -bolero = { version = "0.13.4", default-features = false } +bolero = { version = "0.13.4", default-features = false, features = ["std"] } bytes = { version = "1.11.1", default-features = false } camino = { version = "1.2.5", default-features = false } # local dependencies diff --git a/crates/cargo-anvil/docs/design/checks.md b/crates/cargo-anvil/docs/design/checks.md index bb9ff396..72cc45f5 100644 --- a/crates/cargo-anvil/docs/design/checks.md +++ b/crates/cargo-anvil/docs/design/checks.md @@ -199,7 +199,7 @@ This is the same set of checks that used to live in the standalone `pr-test` gro | `miri` | `cargo + miri test --all-features --tests` over the impact-affected packages. Uses libtest (one process per test binary), **not** `cargo miri nextest run`: under miri, nextest's process-per-test model pays miri's expensive std-initialization re-interpretation for *every* test and roughly doubles wall-time on a large suite (the dominant cost on the PR critical path). `--tests` runs lib/bin unit tests and integration tests (the same target set nextest ran) while excluding doctests, which miri can't run; it is used in preference to `--lib --tests` because `--lib` errors with "no library targets found" on a bin-only affected package under impact scoping. Slow tests opt out per-test with `#[cfg_attr(miri, ignore)]` -- anvil doesn't pass exotic `MIRIFLAGS`; the per-test opt-out is the canonical mechanism. libtest exits 0 when a binary's tests are all skipped, so no `--no-tests=pass` workaround is needed. The recipe reads its scope from the `target/anvil/impact/` cache via `_anvil-impact-include`; because it depends on `anvil-impact`, a clean direct or PR invocation is impact-scoped (unaffected packages are skipped). It runs the full workspace only when scoping is off — the scheduled/full tiers set `ANVIL_IMPACT=off`, and a dirty local tree widens for safety. | oxidizer, oxidizer-github | | `careful` | `cargo + careful test --all-features --locked` over the impact-affected packages. cargo-careful uses a debug-instrumented std in a stable cache path. Because Cargo fingerprints the sysroot path rather than its contents, the recipe records the actual `rustc -vV` and SHA-256 of the resolved `cargo-careful` executable in `target/anvil/careful-sysroot.id`; either changing triggers `cargo clean`. The executable hash is used because cargo-careful rejects version-only invocations. This remains correct when validation accepts a newer installed cargo-careful. | oxidizer-github | | `loom` | For each `[[test]]` target that declares `required-features = ["loom"]`, `cargo test -p --release --all-features --locked --test -- --test-threads=1` with `RUSTFLAGS="--cfg loom"`. [`loom`](https://crates.io/crates/loom) is a permutation-based concurrency model checker that explores thread interleavings. CI uses Loom's documented two-preemption bound by default, preventing one large model from consuming the whole job while retaining systematic coverage; adopters can override `LOOM_MAX_PREEMPTIONS` for deeper or exhaustive runs. Targets are detected **structurally** from `cargo metadata` (a test target whose `kind` contains `test` and whose `required-features` contains `loom`) -- not via a filename/cfg/comment heuristic -- and only those targets run, so loom never touches a crate's ordinary tests. The `loom` feature selects the target (`required-features`); `--cfg loom` activates loom (source swaps std↔loom atomics on `#[cfg(loom)]`, and `[target.'cfg(loom)'.dependencies] loom` links only under the cfg) -- both are required. Scoped per-package with `-p` (never `--workspace`) so the global cfg never leaks into deps reachable only through other members. **Fail-loud**: a crate that declares loom support (a `loom` feature or a `cfg(loom)` dependency) but exposes no such test target errors out rather than silently no-opping. When no crate ships a loom target the recipe skips (exit 0). | oxidizer-github | -| `bolero` | Uses the catalog nightly and release profile consistently to discover targets one package at a time, then runs each affected libfuzzer target for 60 seconds on Linux. Explicitly selecting `release` avoids cargo-bolero's implicit, adopter-defined `fuzz` profile and matches target execution. Per-package discovery is required because `cargo-bolero list` accepts only one `--package`; local whole-workspace runs enumerate workspace members before discovery. A successful empty discovery is a no-op; metadata, discovery, or parsing failure fails the check. Non-Linux hosts skip because cargo-bolero's native dependencies are unsupported there, while harnesses still run as ordinary tests. | oxidizer-github | +| `bolero` | Uses the catalog nightly and release profile consistently to discover targets one package at a time, then runs each affected libfuzzer target for 60 seconds on Linux. Explicitly selecting `release` avoids cargo-bolero's implicit, adopter-defined `fuzz` profile and matches target execution. Adopters that disable `bolero`'s default features must enable its `std` feature; that feature supplies the compile-time `bolero-engine/any` support required by the libfuzzer adapter. Per-package discovery is required because `cargo-bolero list` accepts only one `--package`; local whole-workspace runs enumerate workspace members before discovery. A successful empty discovery is a no-op; metadata, discovery, or parsing failure fails the check. Non-Linux hosts skip because cargo-bolero's native dependencies are unsupported there, while harnesses still run as ordinary tests. | oxidizer-github | #### `pr-mutants` (mutation testing) From 2a31cec3aafbd1e3ae3d591c918aa215ffa3c39e Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Mon, 31 Aug 2026 16:03:54 +0200 Subject: [PATCH 15/17] fix(gamma): keep loom exploration exhaustive Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4112f005-775f-4c4b-88ed-db8f0c5d1cf1 --- .anvil.lock | 4 +- crates/cargo-anvil/docs/design/checks.md | 2 +- .../justfiles/anvil/checks/loom.just | 9 +-- crates/cargo-anvil/tests/recipe_contracts.rs | 10 +-- .../snapshots/snapshots__ado_backend.snap | 9 +-- .../snapshots/snapshots__github_backend.snap | 9 +-- .../snapshots/snapshots__local_only.snap | 9 +-- .../cargo-gamma-lib/src/exec/verdict/hubs.rs | 75 ++++++++----------- justfiles/anvil/checks/loom.just | 9 +-- 9 files changed, 46 insertions(+), 90 deletions(-) diff --git a/.anvil.lock b/.anvil.lock index 97540079..6af5c2d1 100644 --- a/.anvil.lock +++ b/.anvil.lock @@ -1,7 +1,7 @@ version = 1 tool = "anvil" tool_version = "0.5.0" -catalog_checksum = "sha256:f56108d6b5d2931341b900cb2fb06d3397dd7e379aa905bd4bffbf1f83e2817a" +catalog_checksum = "sha256:ac4415039f5416ddaa044cbf80a00d328f8014a08fe0aaa68223fd9ef2d3c37f" [[file]] path = ".anvil/container/Containerfile" @@ -145,7 +145,7 @@ checksum = "sha256:689f7d5b1eaea32672f8d0b9fff0994cc03bc56fb8ef282a4286f930fc503 [[file]] path = "justfiles/anvil/checks/loom.just" -checksum = "sha256:8feb9bcb11c314f4c7825854cc7dd10b184d91ad520dd736addd098d3c7bb1bf" +checksum = "sha256:a919429e9693c230ba8c9a194c41127708acf2cb1bfd196049a1386e7bb7b4e4" [[file]] path = "justfiles/anvil/checks/miri-race-coverage.just" diff --git a/crates/cargo-anvil/docs/design/checks.md b/crates/cargo-anvil/docs/design/checks.md index 72cc45f5..afc9a899 100644 --- a/crates/cargo-anvil/docs/design/checks.md +++ b/crates/cargo-anvil/docs/design/checks.md @@ -198,7 +198,7 @@ This is the same set of checks that used to live in the standalone `pr-test` gro |-----------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------| | `miri` | `cargo + miri test --all-features --tests` over the impact-affected packages. Uses libtest (one process per test binary), **not** `cargo miri nextest run`: under miri, nextest's process-per-test model pays miri's expensive std-initialization re-interpretation for *every* test and roughly doubles wall-time on a large suite (the dominant cost on the PR critical path). `--tests` runs lib/bin unit tests and integration tests (the same target set nextest ran) while excluding doctests, which miri can't run; it is used in preference to `--lib --tests` because `--lib` errors with "no library targets found" on a bin-only affected package under impact scoping. Slow tests opt out per-test with `#[cfg_attr(miri, ignore)]` -- anvil doesn't pass exotic `MIRIFLAGS`; the per-test opt-out is the canonical mechanism. libtest exits 0 when a binary's tests are all skipped, so no `--no-tests=pass` workaround is needed. The recipe reads its scope from the `target/anvil/impact/` cache via `_anvil-impact-include`; because it depends on `anvil-impact`, a clean direct or PR invocation is impact-scoped (unaffected packages are skipped). It runs the full workspace only when scoping is off — the scheduled/full tiers set `ANVIL_IMPACT=off`, and a dirty local tree widens for safety. | oxidizer, oxidizer-github | | `careful` | `cargo + careful test --all-features --locked` over the impact-affected packages. cargo-careful uses a debug-instrumented std in a stable cache path. Because Cargo fingerprints the sysroot path rather than its contents, the recipe records the actual `rustc -vV` and SHA-256 of the resolved `cargo-careful` executable in `target/anvil/careful-sysroot.id`; either changing triggers `cargo clean`. The executable hash is used because cargo-careful rejects version-only invocations. This remains correct when validation accepts a newer installed cargo-careful. | oxidizer-github | -| `loom` | For each `[[test]]` target that declares `required-features = ["loom"]`, `cargo test -p --release --all-features --locked --test -- --test-threads=1` with `RUSTFLAGS="--cfg loom"`. [`loom`](https://crates.io/crates/loom) is a permutation-based concurrency model checker that explores thread interleavings. CI uses Loom's documented two-preemption bound by default, preventing one large model from consuming the whole job while retaining systematic coverage; adopters can override `LOOM_MAX_PREEMPTIONS` for deeper or exhaustive runs. Targets are detected **structurally** from `cargo metadata` (a test target whose `kind` contains `test` and whose `required-features` contains `loom`) -- not via a filename/cfg/comment heuristic -- and only those targets run, so loom never touches a crate's ordinary tests. The `loom` feature selects the target (`required-features`); `--cfg loom` activates loom (source swaps std↔loom atomics on `#[cfg(loom)]`, and `[target.'cfg(loom)'.dependencies] loom` links only under the cfg) -- both are required. Scoped per-package with `-p` (never `--workspace`) so the global cfg never leaks into deps reachable only through other members. **Fail-loud**: a crate that declares loom support (a `loom` feature or a `cfg(loom)` dependency) but exposes no such test target errors out rather than silently no-opping. When no crate ships a loom target the recipe skips (exit 0). | oxidizer-github | +| `loom` | For each `[[test]]` target that declares `required-features = ["loom"]`, `cargo test -p --release --all-features --locked --test -- --test-threads=1` with `RUSTFLAGS="--cfg loom"`. [`loom`](https://crates.io/crates/loom) is a permutation-based concurrency model checker that explores thread interleavings. Anvil does not impose a global exploration bound: each model owns its topology and must remain tractable under exhaustive exploration, avoiding redundant symmetric participants that multiply equivalent schedules. Targets are detected **structurally** from `cargo metadata` (a test target whose `kind` contains `test` and whose `required-features` contains `loom`) -- not via a filename/cfg/comment heuristic -- and only those targets run, so loom never touches a crate's ordinary tests. The `loom` feature selects the target (`required-features`); `--cfg loom` activates loom (source swaps std↔loom atomics on `#[cfg(loom)]`, and `[target.'cfg(loom)'.dependencies] loom` links only under the cfg) -- both are required. Scoped per-package with `-p` (never `--workspace`) so the global cfg never leaks into deps reachable only through other members. **Fail-loud**: a crate that declares loom support (a `loom` feature or a `cfg(loom)` dependency) but exposes no such test target errors out rather than silently no-opping. When no crate ships a loom target the recipe skips (exit 0). | oxidizer-github | | `bolero` | Uses the catalog nightly and release profile consistently to discover targets one package at a time, then runs each affected libfuzzer target for 60 seconds on Linux. Explicitly selecting `release` avoids cargo-bolero's implicit, adopter-defined `fuzz` profile and matches target execution. Adopters that disable `bolero`'s default features must enable its `std` feature; that feature supplies the compile-time `bolero-engine/any` support required by the libfuzzer adapter. Per-package discovery is required because `cargo-bolero list` accepts only one `--package`; local whole-workspace runs enumerate workspace members before discovery. A successful empty discovery is a no-op; metadata, discovery, or parsing failure fails the check. Non-Linux hosts skip because cargo-bolero's native dependencies are unsupported there, while harnesses still run as ordinary tests. | oxidizer-github | #### `pr-mutants` (mutation testing) diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/loom.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/loom.just index 8ecd3e9e..3db72eac 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/loom.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/loom.just @@ -33,9 +33,8 @@ # only through *other* members (e.g. concurrent-queue), and run # single-threaded (loom drives its own scheduling). --release per the # loom docs: permutation exploration is wall-clock dominated by the SUT. -# CI defaults Loom to its documented two-preemption bound so one large -# state space cannot consume the whole job. Adopters can override the -# bound (including with a higher value) through LOOM_MAX_PREEMPTIONS. +# Anvil does not impose a global exploration bound: each model owns its +# topology and must remain tractable under exhaustive exploration. # # Fail-loud guard: a crate that *declares* loom support -- a `loom` # feature or a `cfg(loom)`-gated dependency -- but exposes no @@ -109,10 +108,6 @@ anvil-loom: anvil-loom-validate-prereqs anvil-impact } $env:RUSTFLAGS = "--cfg loom $($env:RUSTFLAGS)".Trim() - if ([string]::IsNullOrWhiteSpace($env:LOOM_MAX_PREEMPTIONS)) { - $env:LOOM_MAX_PREEMPTIONS = '2' - } - Write-Host "anvil-loom: maximum preemptions per execution: $env:LOOM_MAX_PREEMPTIONS" foreach ($lt in $loomTargets) { Write-Host "anvil-loom: $($lt.pkg) :: $($lt.target)" & cargo test -p $lt.pkg --release --all-features --locked --test $lt.target -- --test-threads=1 diff --git a/crates/cargo-anvil/tests/recipe_contracts.rs b/crates/cargo-anvil/tests/recipe_contracts.rs index 9d67eac6..f1fc024f 100644 --- a/crates/cargo-anvil/tests/recipe_contracts.rs +++ b/crates/cargo-anvil/tests/recipe_contracts.rs @@ -29,14 +29,10 @@ const TOOLS: &str = include_str!("../templates/justfiles/anvil/tools.just"); const VERSIONS: &str = include_str!("../templates/justfiles/anvil/versions.just"); #[test] -fn loom_has_an_overridable_ci_preemption_bound() { +fn loom_does_not_globally_limit_exploration() { assert!( - LOOM.contains("IsNullOrWhiteSpace($env:LOOM_MAX_PREEMPTIONS)"), - "loom must preserve an adopter-provided exploration bound" - ); - assert!( - LOOM.contains("$env:LOOM_MAX_PREEMPTIONS = '2'"), - "loom must default CI to its documented two-preemption bound" + !LOOM.contains("LOOM_MAX_PREEMPTIONS"), + "loom models must own their exploration scope rather than inheriting a global preemption cap" ); } diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap index 31635d13..11f0e034 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap @@ -3661,9 +3661,8 @@ anvil-llvm-cov-validate-prereqs: anvil-component-nightly-llvm-tools-validate-pre # only through *other* members (e.g. concurrent-queue), and run # single-threaded (loom drives its own scheduling). --release per the # loom docs: permutation exploration is wall-clock dominated by the SUT. -# CI defaults Loom to its documented two-preemption bound so one large -# state space cannot consume the whole job. Adopters can override the -# bound (including with a higher value) through LOOM_MAX_PREEMPTIONS. +# Anvil does not impose a global exploration bound: each model owns its +# topology and must remain tractable under exhaustive exploration. # # Fail-loud guard: a crate that *declares* loom support -- a `loom` # feature or a `cfg(loom)`-gated dependency -- but exposes no @@ -3737,10 +3736,6 @@ anvil-loom: anvil-loom-validate-prereqs anvil-impact } $env:RUSTFLAGS = "--cfg loom $($env:RUSTFLAGS)".Trim() - if ([string]::IsNullOrWhiteSpace($env:LOOM_MAX_PREEMPTIONS)) { - $env:LOOM_MAX_PREEMPTIONS = '2' - } - Write-Host "anvil-loom: maximum preemptions per execution: $env:LOOM_MAX_PREEMPTIONS" foreach ($lt in $loomTargets) { Write-Host "anvil-loom: $($lt.pkg) :: $($lt.target)" & cargo test -p $lt.pkg --release --all-features --locked --test $lt.target -- --test-threads=1 diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap index 52812e6d..1ba939ba 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap @@ -3540,9 +3540,8 @@ anvil-llvm-cov-validate-prereqs: anvil-component-nightly-llvm-tools-validate-pre # only through *other* members (e.g. concurrent-queue), and run # single-threaded (loom drives its own scheduling). --release per the # loom docs: permutation exploration is wall-clock dominated by the SUT. -# CI defaults Loom to its documented two-preemption bound so one large -# state space cannot consume the whole job. Adopters can override the -# bound (including with a higher value) through LOOM_MAX_PREEMPTIONS. +# Anvil does not impose a global exploration bound: each model owns its +# topology and must remain tractable under exhaustive exploration. # # Fail-loud guard: a crate that *declares* loom support -- a `loom` # feature or a `cfg(loom)`-gated dependency -- but exposes no @@ -3616,10 +3615,6 @@ anvil-loom: anvil-loom-validate-prereqs anvil-impact } $env:RUSTFLAGS = "--cfg loom $($env:RUSTFLAGS)".Trim() - if ([string]::IsNullOrWhiteSpace($env:LOOM_MAX_PREEMPTIONS)) { - $env:LOOM_MAX_PREEMPTIONS = '2' - } - Write-Host "anvil-loom: maximum preemptions per execution: $env:LOOM_MAX_PREEMPTIONS" foreach ($lt in $loomTargets) { Write-Host "anvil-loom: $($lt.pkg) :: $($lt.target)" & cargo test -p $lt.pkg --release --all-features --locked --test $lt.target -- --test-threads=1 diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap index 78e19f16..5e92acee 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap @@ -2413,9 +2413,8 @@ anvil-llvm-cov-validate-prereqs: anvil-component-nightly-llvm-tools-validate-pre # only through *other* members (e.g. concurrent-queue), and run # single-threaded (loom drives its own scheduling). --release per the # loom docs: permutation exploration is wall-clock dominated by the SUT. -# CI defaults Loom to its documented two-preemption bound so one large -# state space cannot consume the whole job. Adopters can override the -# bound (including with a higher value) through LOOM_MAX_PREEMPTIONS. +# Anvil does not impose a global exploration bound: each model owns its +# topology and must remain tractable under exhaustive exploration. # # Fail-loud guard: a crate that *declares* loom support -- a `loom` # feature or a `cfg(loom)`-gated dependency -- but exposes no @@ -2489,10 +2488,6 @@ anvil-loom: anvil-loom-validate-prereqs anvil-impact } $env:RUSTFLAGS = "--cfg loom $($env:RUSTFLAGS)".Trim() - if ([string]::IsNullOrWhiteSpace($env:LOOM_MAX_PREEMPTIONS)) { - $env:LOOM_MAX_PREEMPTIONS = '2' - } - Write-Host "anvil-loom: maximum preemptions per execution: $env:LOOM_MAX_PREEMPTIONS" foreach ($lt in $loomTargets) { Write-Host "anvil-loom: $($lt.pkg) :: $($lt.target)" & cargo test -p $lt.pkg --release --all-features --locked --test $lt.target -- --test-threads=1 diff --git a/crates/cargo-gamma-lib/src/exec/verdict/hubs.rs b/crates/cargo-gamma-lib/src/exec/verdict/hubs.rs index b5df5ee8..ba28d269 100644 --- a/crates/cargo-gamma-lib/src/exec/verdict/hubs.rs +++ b/crates/cargo-gamma-lib/src/exec/verdict/hubs.rs @@ -218,18 +218,12 @@ mod loom_models { use core::time::Duration; use loom::sync::Arc; + use loom::sync::atomic::AtomicUsize; use super::{Pulse, Readers}; - /// Two simultaneous binaries, each with stdout and stderr readers. - /// - /// This is the smallest production topology that exercises both contention between jobs and - /// contention between the two streams of one job. The atomic operations are identical for - /// additional jobs, so these four streams expose every distinct participant role while keeping - /// the state space tractable in CI. - const CONCURRENT_JOBS: usize = 2; - const STREAMS_PER_JOB: usize = 2; - const CONCURRENT_READER_STREAMS: usize = CONCURRENT_JOBS * STREAMS_PER_JOB; + /// The smallest topology that exercises a race between identical reader operations. + const CONCURRENT_READERS: usize = 2; /// A wakeup is never lost, whatever order the waiter and the notifier run in. /// @@ -269,28 +263,27 @@ mod loom_models { }); } - /// The live count returns to exactly zero after all four production reader roles finish. + /// The live count returns to exactly zero after concurrent readers finish. /// - /// The coordinator first establishes the four-reader high-water mark, then all four independent - /// reader threads finish concurrently. The contended `live` observation also models diagnostics - /// racing abandoned readers as they finally close. Every increment must still have exactly one - /// decrement. + /// Two participants are the smallest topology that exercises the decrement race. Additional + /// reader streams perform the same atomic operation and only multiply equivalent permutations. + /// The contended `live` observation also models diagnostics racing abandoned readers as they + /// finally close. Every increment must still have exactly one decrement. pub(super) fn a_readers_gauge_returns_to_exactly_zero_under_any_interleaving() { loom::model(|| { - let readers = Arc::new(Readers::new()); - let mut finishers = Vec::with_capacity(CONCURRENT_READER_STREAMS); + let readers = Arc::new(Readers { + live: AtomicUsize::new(CONCURRENT_READERS), + peak: AtomicUsize::new(CONCURRENT_READERS), + }); + let mut finishers = Vec::with_capacity(CONCURRENT_READERS); - for _stream in 0..CONCURRENT_READER_STREAMS { - readers.started(); - } - - for _stream in 0..CONCURRENT_READER_STREAMS { + for _reader in 0..CONCURRENT_READERS { let readers = Arc::clone(&readers); finishers.push(loom::thread::spawn(move || readers.finished())); } assert!( - readers.live() <= CONCURRENT_READER_STREAMS, + readers.live() <= CONCURRENT_READERS, "a contended live read exceeded the number of started readers" ); @@ -298,41 +291,33 @@ mod loom_models { finisher.join().unwrap(); } - assert_eq!( - readers.peak(), - CONCURRENT_READER_STREAMS, - "counting a started reader failed to raise the peak" - ); assert_eq!(readers.live(), 0, "a decrement was lost or double-counted"); }); } - /// The peak never understates four readers started concurrently by two jobs. + /// The peak never understates readers started concurrently. /// - /// Two worker-side participants each start the stdout and stderr readers of one simultaneous - /// job. None finishes, so all four streams are genuinely live. The observations before the joins - /// race the `fetch_add` and `fetch_max` operations, exercising the production loads under - /// contention; the observations after the joins must see the exact final count and peak. - pub(super) fn a_readers_peak_never_understates_four_concurrent_starts() { + /// Two participants are the smallest topology that exercises the increment and peak-update + /// races. Additional readers perform the same atomic operations and only multiply equivalent + /// permutations. Neither participant finishes, so both are genuinely live. The observations + /// before the joins race the `fetch_add` and `fetch_max` operations, exercising the production + /// loads under contention; the observations after the joins must see the exact count and peak. + pub(super) fn a_readers_peak_never_understates_concurrent_starts() { loom::model(|| { let readers = Arc::new(Readers::new()); - let mut workers = Vec::with_capacity(CONCURRENT_JOBS); + let mut workers = Vec::with_capacity(CONCURRENT_READERS); - for _job in 0..CONCURRENT_JOBS { + for _reader in 0..CONCURRENT_READERS { let readers = Arc::clone(&readers); - workers.push(loom::thread::spawn(move || { - for _stream in 0..STREAMS_PER_JOB { - readers.started(); - } - })); + workers.push(loom::thread::spawn(move || readers.started())); } assert!( - readers.live() <= CONCURRENT_READER_STREAMS, + readers.live() <= CONCURRENT_READERS, "a contended live read exceeded the number of starters" ); assert!( - readers.peak() <= CONCURRENT_READER_STREAMS, + readers.peak() <= CONCURRENT_READERS, "a contended peak read exceeded the number of starters" ); @@ -340,8 +325,8 @@ mod loom_models { worker.join().unwrap(); } - assert_eq!(readers.live(), CONCURRENT_READER_STREAMS, "a concurrent increment was lost"); - assert_eq!(readers.peak(), CONCURRENT_READER_STREAMS, "the peak understated four live readers"); + assert_eq!(readers.live(), CONCURRENT_READERS, "a concurrent increment was lost"); + assert_eq!(readers.peak(), CONCURRENT_READERS, "the peak understated concurrent readers"); }); } } @@ -350,5 +335,5 @@ mod loom_models { pub(crate) fn run_loom_models() { loom_models::a_pulse_wakeup_is_never_lost_under_any_interleaving(); loom_models::a_readers_gauge_returns_to_exactly_zero_under_any_interleaving(); - loom_models::a_readers_peak_never_understates_four_concurrent_starts(); + loom_models::a_readers_peak_never_understates_concurrent_starts(); } diff --git a/justfiles/anvil/checks/loom.just b/justfiles/anvil/checks/loom.just index 8ecd3e9e..3db72eac 100644 --- a/justfiles/anvil/checks/loom.just +++ b/justfiles/anvil/checks/loom.just @@ -33,9 +33,8 @@ # only through *other* members (e.g. concurrent-queue), and run # single-threaded (loom drives its own scheduling). --release per the # loom docs: permutation exploration is wall-clock dominated by the SUT. -# CI defaults Loom to its documented two-preemption bound so one large -# state space cannot consume the whole job. Adopters can override the -# bound (including with a higher value) through LOOM_MAX_PREEMPTIONS. +# Anvil does not impose a global exploration bound: each model owns its +# topology and must remain tractable under exhaustive exploration. # # Fail-loud guard: a crate that *declares* loom support -- a `loom` # feature or a `cfg(loom)`-gated dependency -- but exposes no @@ -109,10 +108,6 @@ anvil-loom: anvil-loom-validate-prereqs anvil-impact } $env:RUSTFLAGS = "--cfg loom $($env:RUSTFLAGS)".Trim() - if ([string]::IsNullOrWhiteSpace($env:LOOM_MAX_PREEMPTIONS)) { - $env:LOOM_MAX_PREEMPTIONS = '2' - } - Write-Host "anvil-loom: maximum preemptions per execution: $env:LOOM_MAX_PREEMPTIONS" foreach ($lt in $loomTargets) { Write-Host "anvil-loom: $($lt.pkg) :: $($lt.target)" & cargo test -p $lt.pkg --release --all-features --locked --test $lt.target -- --test-threads=1 From c42bf9e46794966f7483d3281949848a51ef6fae Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Mon, 31 Aug 2026 17:36:17 +0200 Subject: [PATCH 16/17] fix(anvil): detect manifest-only drift Make dry-run project and compare the complete lockfile state, and run the dogfood regeneration gate on every pull request. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4112f005-775f-4c4b-88ed-db8f0c5d1cf1 --- .github/workflows/regenerate-check.yml | 7 +- crates/cargo-anvil/docs/design/updates.md | 8 +- crates/cargo-anvil/docs/verification.md | 15 +- crates/cargo-anvil/src/plan.rs | 173 ++++++++----------- crates/cargo-anvil/src/run.rs | 91 ++++++++-- crates/cargo-anvil/tests/cli.rs | 21 +++ crates/cargo-anvil/tests/recipe_contracts.rs | 10 ++ 7 files changed, 199 insertions(+), 126 deletions(-) diff --git a/.github/workflows/regenerate-check.yml b/.github/workflows/regenerate-check.yml index f1e21836..82af4818 100644 --- a/.github/workflows/regenerate-check.yml +++ b/.github/workflows/regenerate-check.yml @@ -6,7 +6,7 @@ # # 1. Builds cargo-anvil from the PR branch. # 2. Runs `cargo anvil --dry-run` against the repo root. -# 3. Fails iff the binary would write or propose anything. +# 3. Fails iff the binary would change generated content or `.anvil.lock`. # # This is the primary dogfooding mechanism described in # crates/cargo-anvil/docs/verification.md. @@ -17,10 +17,7 @@ name: regenerate-check on: - pull_request: - paths: - - "crates/cargo-anvil/**" - - ".github/workflows/regenerate-check.yml" + pull_request: {} merge_group: {} workflow_dispatch: {} diff --git a/crates/cargo-anvil/docs/design/updates.md b/crates/cargo-anvil/docs/design/updates.md index 8da95c8f..3a8eddc8 100644 --- a/crates/cargo-anvil/docs/design/updates.md +++ b/crates/cargo-anvil/docs/design/updates.md @@ -407,11 +407,11 @@ work — provided they hadn't customized it. ### Exit codes -- `--dry-run` exit code 0: the run would change nothing on disk and every artifact was - safely inspected. +- `--dry-run` exit code 0: the run would change nothing on disk, including + `.anvil.lock`, and every artifact was safely inspected. - `--dry-run` exit code 1: the run would change something on disk, including writing a - `.anvil-proposed` sibling, or Anvil refused to manage an artifact it could not safely - inspect. + `.anvil-proposed` sibling or refreshing manifest checksums and inventory, or Anvil + refused to manage an artifact it could not safely inspect. The same partitioning is printed at the end of every non-`--dry-run` `update`. diff --git a/crates/cargo-anvil/docs/verification.md b/crates/cargo-anvil/docs/verification.md index 829ade0f..e2823bc0 100644 --- a/crates/cargo-anvil/docs/verification.md +++ b/crates/cargo-anvil/docs/verification.md @@ -150,12 +150,11 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: dtolnay/rust-toolchain@stable - run: cargo build --locked -p cargo-anvil - - name: Regenerate emitted files - run: ./target/debug/cargo-anvil anvil - name: Assert no drift run: | - if ! git diff --exit-code; then - echo "::error::cargo-anvil changed files. Run 'cargo anvil' locally and commit the diff." + if ! ./target/debug/cargo-anvil anvil --dry-run; then + echo "::error::cargo-anvil would change generated files or .anvil.lock." \ + "Run 'cargo anvil' locally and commit the diff." exit 1 fi @@ -164,10 +163,10 @@ jobs: uses: ./.github/workflows/anvil-pr-impl.yml ``` -The `regenerate-check` job runs first. If a PR changes the catalog or emitter without -also committing the regenerated output, this fails with an actionable message. After -that, the standard `anvil-pr-impl.yml` reusable workflow runs every group, exactly as -in any consumer repo. +The `regenerate-check` job runs on every PR. If a change leaves either emitted +content or `.anvil.lock` out of date, this fails with an actionable message. +After that, the standard `anvil-pr-impl.yml` reusable workflow runs every group, +exactly as in any consumer repo. The wrapper workflow above is the **one** hand-written workflow in ox-tools — it bootstraps the dogfood loop. Every other cloud workflows artifact is regenerated. diff --git a/crates/cargo-anvil/src/plan.rs b/crates/cargo-anvil/src/plan.rs index 32b3771a..65b7e84f 100644 --- a/crates/cargo-anvil/src/plan.rs +++ b/crates/cargo-anvil/src/plan.rs @@ -238,6 +238,7 @@ pub struct Plan { items: Vec, notes: Vec, refusals: Vec, + manifest_update_required: bool, } impl Plan { @@ -284,10 +285,15 @@ impl Plan { &mut self.items } + /// Record whether applying this plan would change `.anvil.lock`. + pub(crate) fn set_manifest_update_required(&mut self, required: bool) { + self.manifest_update_required = required; + } + /// Whether the plan would change anything on disk if applied. #[must_use] pub fn has_changes(&self) -> bool { - self.items.iter().any(|i| i.decision.writes()) + self.manifest_update_required || self.items.iter().any(|i| i.decision.writes()) } /// Exit code for `--dry-run`: 0 if everything is in sync and every @@ -350,6 +356,10 @@ impl Plan { write_section(&mut out, "Orphaned (customized; transferring ownership)", &orphans_kept); write_section(&mut out, "Will leave alone (silent)", &leave_alones); + if self.manifest_update_required { + let _ = writeln!(out, "Manifest update required: 1 item(s)"); + let _ = writeln!(out, " - .anvil.lock"); + } if !in_syncs.is_empty() { let _ = writeln!(out, "Unchanged: {} item(s)", in_syncs.len()); } @@ -379,8 +389,8 @@ impl Plan { /// subsequent runs see the divergence as resolved /// (`LeaveAlone`) until the template moves again — see /// [`updates.md §5`](../../docs/design/updates.md). - /// - `InSync`, `LeaveAlone` items preserve their existing - /// manifest entries from `previous_manifest`. + /// - `InSync` items refresh their manifest checksum; `LeaveAlone` + /// items preserve their existing manifest entries. /// - Stale entries — items present in `previous_manifest` but not /// in this plan (e.g. a removed workspace member, a backend the /// user disabled) — are purged from the returned manifest. @@ -396,120 +406,70 @@ impl Plan { /// items must carry a spliced host. These invariants are enforced /// by the `PlanItem::*` constructors, so violations only happen if /// callers build `PlanItem` directly with inconsistent fields. + pub fn apply(&self, repo_root: &Path, previous_manifest: &Manifest) -> Result { + self.apply_files(repo_root)?; + Ok(self.projected_manifest(previous_manifest)) + } + + /// Apply only the owned-file and managed-region changes. + /// + /// Manifest projection is deliberately separate so dry-run and real + /// application calculate the same next `.anvil.lock`. #[expect( - clippy::too_many_lines, clippy::expect_used, - reason = "single dispatch site covering every (target × decision) pair; the expects encode constructor-enforced invariants" + reason = "the expects encode invariants enforced by the PlanItem constructors" )] - pub fn apply(&self, repo_root: &Path, previous_manifest: &Manifest) -> Result { - let mut next = Manifest { - tool: previous_manifest.tool.clone(), - tool_version: previous_manifest.tool_version.clone(), - catalog_checksum: previous_manifest.catalog_checksum.clone(), - files: previous_manifest.files.clone(), - regions: previous_manifest.regions.clone(), - }; - + pub(crate) fn apply_files(&self, repo_root: &Path) -> Result<(), AppError> { for item in &self.items { match (&item.target, item.decision) { (Target::File { path }, Decision::Write) => { let content = item.rendered.as_ref().expect("Write decision must carry rendered content"); - let abs = repo_root.join(path); - write_file(&abs, content)?; - if let Some(checksum) = &item.rendered_checksum { - next.files.insert(path.clone(), checksum.clone()); - } + write_file(&repo_root.join(path), content)?; } (Target::File { path }, Decision::Propose) => { let content = item.rendered.as_ref().expect("Propose decision must carry rendered content"); - let abs = repo_root.join(format!("{path}.anvil-proposed")); - write_file(&abs, content)?; - if let Some(checksum) = &item.rendered_checksum { - // Bump L to the new T so subsequent runs see the - // divergence as resolved (LeaveAlone). The user's - // .anvil-proposed sibling stays on disk for - // review; deleting or accepting it is the user's - // job. - next.files.insert(path.clone(), checksum.clone()); - } + write_file(&repo_root.join(format!("{path}.anvil-proposed")), content)?; } - (Target::Region { host, id }, Decision::Write) => { + (Target::Region { host, .. }, Decision::Write) => { let spliced = item.spliced_host.as_ref().expect("region Write must carry spliced host"); - let abs = repo_root.join(host); - write_file(&abs, spliced)?; - if let Some(checksum) = &item.rendered_checksum { - next.regions.insert( - RegionKey { - host: host.clone(), - id: id.clone(), - }, - checksum.clone(), - ); - } + write_file(&repo_root.join(host), spliced)?; } - (Target::Region { host, id }, Decision::Propose) => { + (Target::Region { host, .. }, Decision::Propose) => { let spliced = item.spliced_host.as_ref().expect("region Propose must carry spliced host"); - let abs = repo_root.join(format!("{host}.anvil-proposed")); - write_file(&abs, spliced)?; - if let Some(checksum) = &item.rendered_checksum { - // Same rationale as the File/Propose branch: bump - // L = T so subsequent runs see LeaveAlone until - // the template moves again. - next.regions.insert( - RegionKey { - host: host.clone(), - id: id.clone(), - }, - checksum.clone(), - ); - } + write_file(&repo_root.join(format!("{host}.anvil-proposed")), spliced)?; } (Target::File { path }, Decision::Remove) => { - // Untouched orphan file: delete and drop the - // manifest entry. If the file is already missing - // (race / external delete), absorb the error so - // the result is idempotent. let abs = repo_root.join(path); if let Err(e) = std::fs::remove_file(&abs) && e.kind() != std::io::ErrorKind::NotFound { - return Err::(e).into_app_err_with(|| format!("failed to remove {}", abs.display())); + return Err::<(), _>(e).into_app_err_with(|| format!("failed to remove {}", abs.display())); } - next.files.remove(path); } - (Target::Region { host, id }, Decision::Remove) => { - // Untouched orphan region: splice the markers + body - // out of the host file and drop the manifest entry. + (Target::Region { host, .. }, Decision::Remove) => { let spliced = item.spliced_host.as_ref().expect("region Remove must carry spliced host"); - let abs = repo_root.join(host); - write_file(&abs, spliced)?; - next.regions.remove(&RegionKey { - host: host.clone(), - id: id.clone(), - }); - } - (Target::File { path }, Decision::OrphanedKept) => { - // Customized orphan: leave the file in place, - // transfer ownership by dropping the manifest entry. - next.files.remove(path); - } - (Target::Region { host, id }, Decision::OrphanedKept) => { - // Customized orphan region: leave the host file - // and the in-region content in place, transfer - // ownership by dropping the manifest entry. - next.regions.remove(&RegionKey { - host: host.clone(), - id: id.clone(), - }); + write_file(&repo_root.join(host), spliced)?; } - (_, Decision::InSync) => { - // Disk content matches the current template. No - // file write needed. We DO refresh the manifest L - // to the current template checksum if the plan - // item carries one — this self-heals stale-L - // values left over from older binary versions - // whose hash function differed (e.g. before - // line-ending normalization). + (_, Decision::InSync | Decision::LeaveAlone | Decision::OrphanedKept) => {} + } + } + Ok(()) + } + + /// Calculate the manifest produced by this plan without touching disk. + #[must_use] + pub(crate) fn projected_manifest(&self, previous_manifest: &Manifest) -> Manifest { + let mut next = Manifest { + tool: previous_manifest.tool.clone(), + tool_version: previous_manifest.tool_version.clone(), + catalog_checksum: previous_manifest.catalog_checksum.clone(), + files: previous_manifest.files.clone(), + regions: previous_manifest.regions.clone(), + }; + + for item in &self.items { + match item.decision { + Decision::Write | Decision::Propose | Decision::InSync => { if let Some(checksum) = &item.rendered_checksum { match &item.target { Target::File { path } => { @@ -527,13 +487,22 @@ impl Plan { } } } - (_, Decision::LeaveAlone) => { - // No-op; manifest entry already preserved. - } + Decision::Remove | Decision::OrphanedKept => match &item.target { + Target::File { path } => { + next.files.remove(path); + } + Target::Region { host, id } => { + next.regions.remove(&RegionKey { + host: host.clone(), + id: id.clone(), + }); + } + }, + Decision::LeaveAlone => {} } } - Ok(next) + next } } @@ -600,6 +569,16 @@ mod tests { assert!(!plan.has_changes()); } + #[test] + fn manifest_only_update_is_reported_as_a_change() { + let mut plan = Plan::default(); + plan.set_manifest_update_required(true); + + assert!(plan.has_changes()); + assert_eq!(plan.dry_run_exit_code(), 1); + assert!(plan.summary(None).contains(".anvil.lock")); + } + #[test] fn summary_categorizes_items() { let mut plan = Plan::default(); diff --git a/crates/cargo-anvil/src/run.rs b/crates/cargo-anvil/src/run.rs index cb03b78e..a29e67c5 100644 --- a/crates/cargo-anvil/src/run.rs +++ b/crates/cargo-anvil/src/run.rs @@ -98,19 +98,21 @@ pub fn run_update(catalog: &Catalog, args: &Cli, start_dir: &Path) -> Result Date: Mon, 31 Aug 2026 17:37:28 +0200 Subject: [PATCH 17/17] docs: clarify bolero feature rationale Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4112f005-775f-4c4b-88ed-db8f0c5d1cf1 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 6ce708ac..6d98ab1d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,8 +27,8 @@ anyhow = { version = "1.0.104", default-features = false } assert_cmd = { version = "2.2.2", default-features = false } async-once-cell = { version = "0.5", default-features = false } blake3 = { version = "1.8.7", default-features = false } +# bolero-libfuzzer 0.13.0 calls bolero_engine::any; bolero/std enables that API for its host build. bolero = { version = "0.13.4", default-features = false, features = ["std"] } -# bolero-libfuzzer 0.13.0 calls bolero_engine::any without enabling the feature itself. bolero-engine = { version = "0.13.4", default-features = false } bytes = { version = "1.12.1", default-features = false } camino = { version = "1.2.5", default-features = false }