diff --git a/.anvil.lock b/.anvil.lock index 91687c7f9..987ab3fb6 100644 --- a/.anvil.lock +++ b/.anvil.lock @@ -1,249 +1,274 @@ version = 1 tool = "anvil" -tool_version = "0.3.0" -catalog_checksum = "sha256:61b5b505464579f54039ba1f3ba7df78efe6e34e9e72a3e4a7d169754537d276" +tool_version = "0.6.0" +catalog_checksum = "sha256:88e449f0c37963a26628d00274c09e03ec29e8c262f65a694930f180851b7fc2" [[file]] -path = ".github/actions/anvil-impact/action.yml" -checksum = "sha256:84581a820fe96b4087ac07201ca65c3d2d3c4b00560ba9f046e7d84ebb9ea059" - -[[file]] -path = ".github/actions/anvil-pr-fast/action.yml" -checksum = "sha256:0cfc6f4b8d729f412023cdedb906fa40ef3d19e8adafe8c947f4e85166f5bdda" - -[[file]] -path = ".github/actions/anvil-pr-mutants/action.yml" -checksum = "sha256:d4f445a18079b20390307545b558bb42e3d2ceabd5aa42426996ddce4593dac6" +path = ".anvil/container/Dockerfile.dockerignore" +checksum = "sha256:9c7906c20415ca3b832afb075c21e79f191ef14ee2fb4b6a5a95394e8b2153c1" [[file]] -path = ".github/actions/anvil-pr-runtime-analysis/action.yml" -checksum = "sha256:1e0a7089541508d7f7979f9d4af8c307db6ff6b8323adb8a8064a3adce1e9e62" - -[[file]] -path = ".github/actions/anvil-pr-test/action.yml" -checksum = "sha256:3fff3e46365b6684a7855c9bb61bd4599e2b15452d8d06c7b7ec3f304ea60f91" - -[[file]] -path = ".github/actions/anvil-scheduled-advisories/action.yml" -checksum = "sha256:2bdbacd4cfa1626ae6ea9e99921a827bc14b5cd805ce00f49b1326ac00cea98b" +path = ".github/actions/anvil-impact/action.yml" +checksum = "sha256:76be01852dc1f7e01905c4a8ad384ca57eefc15e2ed74c5c00f57dddefae9b6f" [[file]] -path = ".github/actions/anvil-scheduled-exhaustive/action.yml" -checksum = "sha256:0e61edc0c6dfcd2cf3a6ad3c498804da681032d0a4da7bd3dd2419dc77e11a7f" +path = ".github/actions/anvil-report-status/action.yml" +checksum = "sha256:9940d1947482150ac08fcb9b4150da99f5ae60642f4caeea137577ce0e709e08" [[file]] -path = ".github/actions/anvil-scheduled-runtime-analysis/action.yml" -checksum = "sha256:30dae425f3a11b0a322162bbcbecb848a9030453a8318aeb898b32f10688ede4" +path = ".github/actions/anvil-run-group/action.yml" +checksum = "sha256:ff8def6c0786b6e146c4b633dfe38cb9b8ede398345516cca32bbcd5586087af" [[file]] -path = ".github/actions/anvil-scheduled-test/action.yml" -checksum = "sha256:26ee7e618e4b0cdef2587baa7e6b3e0ab8ed15c027fdc4965b25deabd90075bc" +path = ".github/actions/anvil-setup/action.yml" +checksum = "sha256:e1029f7ff95a5966184b4c04ccbed04ee8ba93bfc93bc9ac307d3dcef31374fb" [[file]] -path = ".github/actions/anvil-setup/action.yml" -checksum = "sha256:d0cb05b4f1931e798335f581fd5401543b01f1785a3a1d82b9693233cfd5b44f" +path = ".github/actions/anvil-setup/just-problem-matcher.json" +checksum = "sha256:ea44d5e1a2cb1471cf2cef05eceab846d8afa45cf691be800f21fec1218da120" [[file]] path = ".github/workflows/anvil-pr-impl.yml" -checksum = "sha256:e2039c553064d1eea584d5b57203c39755a79a2a90686fea8896366cd4275c31" +checksum = "sha256:4481b76085cda73c49d061ec493d467d50815667b81700e5724119170427aec8" [[file]] path = ".github/workflows/anvil-pr.yml" -checksum = "sha256:cace8cf38c1ce2dc85d054c39a14de67d100b9bb10c1a3ec8c70ad5bcad32d41" +checksum = "sha256:0c2530d9a38e6a74e0a7fd4f999b4a1790f97de30b58b68c6c2344600da196f2" [[file]] path = ".github/workflows/anvil-scheduled-impl.yml" -checksum = "sha256:37467df6bf06ed5cbd7178266c08905172edd9cb66ef49057e83bdfccf6d4f40" +checksum = "sha256:ac70061acf594c8c212c45ed97c3b653e7b8de68f4e1dcc9695a2628e4e2596d" [[file]] path = ".github/workflows/anvil-scheduled.yml" -checksum = "sha256:b5e0a82b87adcd3be7b4522716af733a3746c3d24def2aa97c422f916cbe0c37" +checksum = "sha256:d4d3bd645a5586e9a1cc3a5fc27e38c93e59b1e29f83ede0ccd610273eec00f4" [[file]] path = "justfiles/anvil/checks/aprz.just" -checksum = "sha256:7a6143381d5712efec78432b73094a99220cec3d0063829bb57fa2be42db3d90" +checksum = "sha256:e10edbbe60358ec930241cd5d4e681a84f30f9d5278e652696e2f149e112d066" [[file]] path = "justfiles/anvil/checks/audit.just" -checksum = "sha256:c5735ada01381c6f6524178d42cbd500d7adfbefd4b9da8f82ab354280e5de2e" +checksum = "sha256:ef33da620683628208b4c5ad51f96f5f6424fe1d56c2d0475487228b1f1aec23" [[file]] path = "justfiles/anvil/checks/bench.just" -checksum = "sha256:a8fa651a85f36a6ee8519c4b1271eb79664ca447313612a582c71a54186b5285" +checksum = "sha256:850793443ada2151a895ee2728e81f03da5430facde16522bcc2e0c314a28577" [[file]] path = "justfiles/anvil/checks/bolero.just" -checksum = "sha256:933d52d3f2d21488925d1ad0d0d6adacbf5f0d639de632b34b1c89371ecce079" +checksum = "sha256:5a1767c4ff911cec3b0870d675cbefb563193146cedd3a1cbfe1667215bdb264" [[file]] path = "justfiles/anvil/checks/careful.just" -checksum = "sha256:54bfb8ea85c410baf716f8150f381f099edef112fbd6189f04bc06ad7fe31a88" +checksum = "sha256:ea06cf7ebd7d5955b409db500a9ab193311af584437412d7bf0429fa70e43fac" [[file]] path = "justfiles/anvil/checks/cargo-hack.just" -checksum = "sha256:e98a5d5fe98e429616938a6e665b6986601d45d422a51931fbc3c6592d39dc06" +checksum = "sha256:4eca78f197c2cf428b693c49cf0af4afb735b9c6caa8c83a71e5f5fd312a9bc1" [[file]] path = "justfiles/anvil/checks/cargo-sort.just" -checksum = "sha256:c58d7926245c360733f0055c66423a5cbb70714aae65328394ba80e92ed016b5" +checksum = "sha256:ab342c014199ffb58a9346b7b9fcea88201b5b60a07b06cbc9c4e5ef03bd18f5" [[file]] path = "justfiles/anvil/checks/clippy.just" -checksum = "sha256:ddd426e896d8c0bb5ca9cb4aaf688f859fb90a0e15e0678b8c2417236c504eef" +checksum = "sha256:945a1bf3bb1372b2bfbbfec00d520a97da56ebc5575b1e6292c421b52cc77243" [[file]] path = "justfiles/anvil/checks/deny.just" -checksum = "sha256:626e75d506d3b327db177ce6165bc76b8f0bf92d2918ffc1ff09e3ca574a8a84" +checksum = "sha256:c5604e8cb0eaa2baf4e2e2c70032d037d21873090375ffcf1ee99983965f6635" [[file]] path = "justfiles/anvil/checks/doc-build.just" -checksum = "sha256:2e76bf30af5fe740790b76b1b60978991c7eb1b74334106ec48dc3542290e2cf" +checksum = "sha256:9e187f61cb1ef9d11fab2e402af8a882b77f6ed17ca332afb9ae0b1d2112e95b" [[file]] path = "justfiles/anvil/checks/doc-test.just" -checksum = "sha256:9053c76d4c2b2fc98796bd5399ac117cf778d4c8162d0df1b1983289a6a7b07f" +checksum = "sha256:0e54c767de8e57a523a8b1b589a88ab6aca3169ac97d157c42d509d429f4b024" [[file]] path = "justfiles/anvil/checks/ensure-no-cyclic-deps.just" -checksum = "sha256:1a6d4c1ebae829113c6040c5da88970b55f7da16f821bbcde0680f3bd293ff6c" +checksum = "sha256:2d19930c33a93b49d0c9e9672a31d85e1ad4fb92faecc0fb599803139b11c18f" [[file]] path = "justfiles/anvil/checks/ensure-no-default-features.just" -checksum = "sha256:660fa9586acf35ec3445634a8d1395b89021a97faf6634fc6918c0c6d6650553" +checksum = "sha256:38e089afe176aea92c93ac6758154b206b6fc8940931035c0d059449fa824d6c" [[file]] path = "justfiles/anvil/checks/examples.just" -checksum = "sha256:079f35434bfbedcba2df08973380b4809bd463641bf96cdcc4b4402d60187ac8" +checksum = "sha256:8d01fa3e303a9f2b768213636f12c68cc8b41932b68549239e9f10d3be43507b" [[file]] path = "justfiles/anvil/checks/external-types.just" -checksum = "sha256:372ee3b46f2298e333b3d8795dfe9b2b664b32c9dd366f4073247c2d24f1e81d" +checksum = "sha256:7e67d90a9d8bd8cd5c8adb68baf41fdd4625bff4d425b5bf18b75baee62892a8" [[file]] path = "justfiles/anvil/checks/fmt.just" -checksum = "sha256:9d02c97c8d56e2d2a6751d45cce15699e6c80ea96db02caa02e4b0ab9b450c0a" +checksum = "sha256:0434e1a8720453a4bc635ea56af1d19875ac79e6fdd76349052cd57e70392eb8" [[file]] path = "justfiles/anvil/checks/license-headers.just" -checksum = "sha256:535737e127e5315a348709b172b42a87f2db33c8946db913e6ead5c0f1762fa1" +checksum = "sha256:7aa0560dc9088b33e80cd55aee0a25090cbd3d3610652afd3734d0ca52ee7b47" [[file]] path = "justfiles/anvil/checks/llvm-cov.just" -checksum = "sha256:4a24cac22cdb37c116009d987e4e256895b6f1f70b09bc8a344b1f787f6c6567" +checksum = "sha256:cb23283ac3d377b1219c904f4de93b05b437b48c93629fec617307914bba6296" [[file]] path = "justfiles/anvil/checks/loom.just" -checksum = "sha256:fa2495adaae5e3b5bb4dfdb5cc79f1b08c14d8a799d787a7bfa7b5dd2b3add1f" +checksum = "sha256:f4822a8f9cb282bc426790355ba83b63e778db9327e986ba3c86ae6205cfdabb" [[file]] path = "justfiles/anvil/checks/miri-race-coverage.just" -checksum = "sha256:36c64b1f9d0032d13b253f03853f2ae725a45ef635daacc0b4a25442152db214" +checksum = "sha256:f8435d359f4495a3a1b3693069b0bab371bf94c3698389d9749acf95704a3c45" [[file]] path = "justfiles/anvil/checks/miri-strict-provenance.just" -checksum = "sha256:9b7cb431e4ee9761edc141550076a7a2d2bde79740ce5f14109a4edd680954dd" +checksum = "sha256:2e3fafdb060f10635a0dc34b447db4f06d7dd1549e585440611a5018a1e4ef5c" [[file]] path = "justfiles/anvil/checks/miri-tree-borrows.just" -checksum = "sha256:0651c7608246b0a155bfb1c478139de80aefd5f3f562281217e9400fc1692423" +checksum = "sha256:c82f586402f2cb05397a40eec6fe95c7590df2cc52f0dd9da6879de08517561c" [[file]] path = "justfiles/anvil/checks/miri.just" -checksum = "sha256:de57ac72f74a2c5baaba359138bbe3dbf3336c286312acef7e0e8a404542a2e0" +checksum = "sha256:9e9d0cbfef1e1e1586c2af4e9c387719c2f017ea1203d326baacc3ae25df80e2" + +[[file]] +path = "justfiles/anvil/checks/msrv-test.just" +checksum = "sha256:9c25b140dcaa4b38db7701fd627dd9a2d5ad5f9bb83d4490c146058bf4328cc8" [[file]] path = "justfiles/anvil/checks/mutants-diff.just" -checksum = "sha256:98d48d992bcc57401953ef09340ff449802ddbe52533cc1c38a81673814b7cc3" +checksum = "sha256:e8d23688dcb86f82bf537e82c35ab8af9296d5530898645ddfe95132f9192bd0" [[file]] path = "justfiles/anvil/checks/mutants-full.just" -checksum = "sha256:cc4914341feb0625caa62addfe64e6964c06ca88d20fd5d07d129a363ad22dcc" +checksum = "sha256:604dfb6aa2742695ff638a7d2db6811a1bcbf27da73660027baacc86b2fe1509" [[file]] path = "justfiles/anvil/checks/pr-title.just" -checksum = "sha256:81c133f3339be449ab6e05813da7584f2ceeb94d30c69186eb41f91f224a1425" +checksum = "sha256:28734aa77526ca5c53d89a32c3e20ae6b42d2032c73ab6a1dbc9831f25cb0f76" [[file]] path = "justfiles/anvil/checks/readme-check.just" -checksum = "sha256:e509d7c5dde5242742b3d9649d4cad18dbb83f3227fd3670be168ba426066855" +checksum = "sha256:4993e662875561ddf976ec94c69ec0d647a5c957bfb8f5174f79162a640ed892" [[file]] path = "justfiles/anvil/checks/semver-check.just" -checksum = "sha256:40306ff4ea1aaa176dcd3670fd7add92571643c8366cb5bb1473e16498294eaf" +checksum = "sha256:be0430a53007301db00602d4bc9296f51ad9b2f011583084d069c51b30939b74" [[file]] path = "justfiles/anvil/checks/spellcheck.just" -checksum = "sha256:b2ef03bbad22479982bf4041ba6cb93344f614095bad75d8eb88d00f0fc61731" +checksum = "sha256:2b2063f7e494dba51f805534bce5235132be1eeabde3a118d42d87ce49d51fd6" [[file]] path = "justfiles/anvil/checks/udeps.just" -checksum = "sha256:9c16366a4cb46e1e7efe7531180f7297d8a37b12c521c7f9f3c4323138d2c944" +checksum = "sha256:6efd7378a2cd0f5d86519bd32fd86f2055a60191187dd77a8842b374b8eb7102" + +[[file]] +path = "justfiles/anvil/container.just" +checksum = "sha256:3363992c6c006c649eae3732b2a7b04c2a4925eff369179b128d43380e2800d4" [[file]] path = "justfiles/anvil/groups/pr-fast.just" -checksum = "sha256:09efab1892f90b61427662220203e68985cd5432a04f53a21fd8cb961380cc27" +checksum = "sha256:29a219b7d8b2eaa605b0444838f05ffc7538ab8bccc43d1613c555f7562a731f" + +[[file]] +path = "justfiles/anvil/groups/pr-msrv.just" +checksum = "sha256:5c862a1c7775742377e75d6848650070d11e9ce3facd9ab304b03dbca1c4b1d2" [[file]] path = "justfiles/anvil/groups/pr-mutants.just" -checksum = "sha256:2d056111f7c370c2aff4d3f8ac2c66e2201cc3c8c285eec15e10775c44c203aa" +checksum = "sha256:19a5fb032f680eee2d2e4eca56dece0ced80ac15bd2332b8135915d884f804a1" [[file]] path = "justfiles/anvil/groups/pr-runtime-analysis.just" -checksum = "sha256:de0c35176228d82ef18454ee116a619f2ac38d80e659340c7907137166750916" +checksum = "sha256:9a94b6ae2d212f91d324e401ebdb2238805f3450d95d4d45df791d7f1679a1bd" [[file]] path = "justfiles/anvil/groups/pr-slow.just" -checksum = "sha256:1ca8867bda2ef7f7213502e805a025b91742320eab09332e5b7f40589a3adfdb" +checksum = "sha256:c3281f2044494dcfcfa7544b022ce3d751444241d93d3e8f136669d72d102528" [[file]] path = "justfiles/anvil/groups/pr-test.just" -checksum = "sha256:c309a7b9f1f2df31a988d5acd51508dc9f44cee5077789892ddd3c9f566bae80" +checksum = "sha256:ea96d29e261b454a585c0ba3dc7954a35d0c726e9e94f6bb7c82f15261531330" [[file]] path = "justfiles/anvil/groups/scheduled-advisories.just" -checksum = "sha256:2f0ed3b45c1431f5f5e425696385cbf06ec8f4e99d7232dc2d2794e33325ac5c" +checksum = "sha256:4f9940bb54fd7cd1d622f3207f7c43f38f95232a370f7537b15546271d88805d" [[file]] path = "justfiles/anvil/groups/scheduled-exhaustive.just" -checksum = "sha256:61fa2fc759fd979232dffd4d379be9cf40353ce27a285992b19c45f1a6bb7624" +checksum = "sha256:0b9023c614ae400c30f7131fc939b318bf6ee2ba086b18d45491fa30691694e7" [[file]] path = "justfiles/anvil/groups/scheduled-runtime-analysis.just" -checksum = "sha256:194c84124be3648f529f33ac166f47f54fa555a53af32c03c694a06f0ee4d00b" +checksum = "sha256:51d43ac7399183467cb137f655fa0f1f0c6cf4980aab019dc5898a40ad18259d" [[file]] path = "justfiles/anvil/groups/scheduled-test.just" -checksum = "sha256:9ca50508c39f0fa1b153fd832312a435e46c857a5516cf3613ccfd784815387c" +checksum = "sha256:04679222579a090769403f5aae7c9ffabbf5bd559ddb1f17b5a0655152172846" [[file]] path = "justfiles/anvil/helpers.just" -checksum = "sha256:6f8ed97d9d60f6844fe37427b0e282b701b960219adf7b2a29fa9814c3a185fb" +checksum = "sha256:dc42a4b2ed4a25e3a37b322dad209df34d7adca88bf3c4de60b2f8c1b396e23b" + +[[file]] +path = "justfiles/anvil/impact.just" +checksum = "sha256:0b3a5c96dd33384b4c86ac02b17396bc1a9fe9c82edbc516a2ceea7f4fe3833d" [[file]] path = "justfiles/anvil/mod.just" -checksum = "sha256:705bed028b6999c1e0cf98d0b5320be1495cb3e9e3464122b3d0e38b893ae039" +checksum = "sha256:4892e7a17d5f576241834041c89916708b23e88503156721af3048b2a358538f" [[file]] path = "justfiles/anvil/tiers.just" -checksum = "sha256:d9dceafb51b39173c5d669c101f6173a32a7081536b58e8652aa03c4cc2e8daf" +checksum = "sha256:00453a12cbb34811ee6a2c083dade5f6198575e3b0610f49e4743366326cdd18" [[file]] path = "justfiles/anvil/tools.just" -checksum = "sha256:1c22f38c093e836c83e5ef4da35b04a1ff6a8a0d0355310f5605cf52bfb95cd7" +checksum = "sha256:48b887481a7eb8dc7615367347c93b8a02dd7baafb0ef80ece883f03600285dd" [[file]] path = "justfiles/anvil/versions.just" -checksum = "sha256:403b9abf954d0f7479144d6890e1db36dfa1b199704741c7087b48355ea593ba" +checksum = "sha256:ae6676fb883aa58adb0995384adef477957ad7df10fc43439517cfb617fd92d1" + +[[region]] +host = ".anvil/container/Dockerfile" +id = "anvil-container-base" +checksum = "sha256:734e21d8ae8ce8c0a00f54a36a3a9f15a02b52eff11c27f3de4f7cd60bb95449" + +[[region]] +host = ".anvil/container/Dockerfile" +id = "anvil-container-base-image" +checksum = "sha256:64fa253bd48baecd440ae02cb13d2626e17be6e2bb0e1b19e3345baeade04abb" + +[[region]] +host = ".anvil/container/Dockerfile" +id = "anvil-container-entry" +checksum = "sha256:7b409a9b560c214e10b50f74330fb6f8c0c12c3d83494e0dcf016f2411b50365" + +[[region]] +host = ".anvil/container/Dockerfile" +id = "anvil-container-setup" +checksum = "sha256:3788845ea3d4c483917954ddf9bccf918bf69ab7c64bbf559baf59d5781c444f" + +[[region]] +host = ".anvil/container/Dockerfile" +id = "anvil-container-tools" +checksum = "sha256:ba7456ca88da39f72024dd0616e56d49d1aef7a5d26b81a8b33efccb13321379" [[region]] host = ".delta.toml" id = "anvil-delta" -checksum = "sha256:ef049abc4bba5e6dac7dfc07a4852d821de4682fe6ad46aace7ddfb2455cb597" +checksum = "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" [[region]] host = ".gitattributes" id = "anvil-gitattributes" -checksum = "sha256:7d9051a56a2935a3fce2e4d8585fa5bb4ab8a89ace988cbdde77b52b0c4bcc9f" +checksum = "sha256:b91854c7f0e6d14c74751f607d2c0e3dfcefef55f5178bbafae59d47e577aa1a" [[region]] host = "Cargo.toml" @@ -255,6 +280,11 @@ host = "clippy.toml" id = "anvil-clippy" checksum = "sha256:aba0733632eac4cb54c4768db578fe1f7b7cfe730aa0d7dc13e2828c9062d67d" +[[region]] +host = "crates/allocation_hints/Cargo.toml" +id = "anvil-lints" +checksum = "sha256:2dd7c0f21339fd17092b8dedfe924aa86732c3520baab84f914c2d8f4103ac40" + [[region]] host = "crates/anyspawn/Cargo.toml" id = "anvil-lints" @@ -265,11 +295,21 @@ host = "crates/anyspawn_azure/Cargo.toml" id = "anvil-lints" checksum = "sha256:2dd7c0f21339fd17092b8dedfe924aa86732c3520baab84f914c2d8f4103ac40" +[[region]] +host = "crates/arty_executor/Cargo.toml" +id = "anvil-lints" +checksum = "sha256:2dd7c0f21339fd17092b8dedfe924aa86732c3520baab84f914c2d8f4103ac40" + [[region]] host = "crates/automation/Cargo.toml" id = "anvil-lints" checksum = "sha256:2dd7c0f21339fd17092b8dedfe924aa86732c3520baab84f914c2d8f4103ac40" +[[region]] +host = "crates/benchmarking/Cargo.toml" +id = "anvil-lints" +checksum = "sha256:2dd7c0f21339fd17092b8dedfe924aa86732c3520baab84f914c2d8f4103ac40" + [[region]] host = "crates/bytesbuf/Cargo.toml" id = "anvil-lints" @@ -345,6 +385,16 @@ host = "crates/fetch_tls/Cargo.toml" id = "anvil-lints" checksum = "sha256:2dd7c0f21339fd17092b8dedfe924aa86732c3520baab84f914c2d8f4103ac40" +[[region]] +host = "crates/fetch_winhttp/Cargo.toml" +id = "anvil-lints" +checksum = "sha256:2dd7c0f21339fd17092b8dedfe924aa86732c3520baab84f914c2d8f4103ac40" + +[[region]] +host = "crates/fetch_winhttp_impl/Cargo.toml" +id = "anvil-lints" +checksum = "sha256:2dd7c0f21339fd17092b8dedfe924aa86732c3520baab84f914c2d8f4103ac40" + [[region]] host = "crates/fundle/Cargo.toml" id = "anvil-lints" @@ -370,16 +420,76 @@ host = "crates/http_path_template/Cargo.toml" id = "anvil-lints" checksum = "sha256:2dd7c0f21339fd17092b8dedfe924aa86732c3520baab84f914c2d8f4103ac40" +[[region]] +host = "crates/internity/Cargo.toml" +id = "anvil-lints" +checksum = "sha256:2dd7c0f21339fd17092b8dedfe924aa86732c3520baab84f914c2d8f4103ac40" + +[[region]] +host = "crates/internity_macros/Cargo.toml" +id = "anvil-lints" +checksum = "sha256:2dd7c0f21339fd17092b8dedfe924aa86732c3520baab84f914c2d8f4103ac40" + +[[region]] +host = "crates/internity_macros_impl/Cargo.toml" +id = "anvil-lints" +checksum = "sha256:2dd7c0f21339fd17092b8dedfe924aa86732c3520baab84f914c2d8f4103ac40" + [[region]] host = "crates/layered/Cargo.toml" id = "anvil-lints" checksum = "sha256:2dd7c0f21339fd17092b8dedfe924aa86732c3520baab84f914c2d8f4103ac40" +[[region]] +host = "crates/msvc_spectre_libs/Cargo.toml" +id = "anvil-lints" +checksum = "sha256:2dd7c0f21339fd17092b8dedfe924aa86732c3520baab84f914c2d8f4103ac40" + +[[region]] +host = "crates/msvc_spectre_libs_build/Cargo.toml" +id = "anvil-lints" +checksum = "sha256:2dd7c0f21339fd17092b8dedfe924aa86732c3520baab84f914c2d8f4103ac40" + [[region]] host = "crates/multitude/Cargo.toml" id = "anvil-lints" checksum = "sha256:2dd7c0f21339fd17092b8dedfe924aa86732c3520baab84f914c2d8f4103ac40" +[[region]] +host = "crates/multitude_macros/Cargo.toml" +id = "anvil-lints" +checksum = "sha256:2dd7c0f21339fd17092b8dedfe924aa86732c3520baab84f914c2d8f4103ac40" + +[[region]] +host = "crates/multitude_macros_impl/Cargo.toml" +id = "anvil-lints" +checksum = "sha256:2dd7c0f21339fd17092b8dedfe924aa86732c3520baab84f914c2d8f4103ac40" + +[[region]] +host = "crates/observed/Cargo.toml" +id = "anvil-lints" +checksum = "sha256:2dd7c0f21339fd17092b8dedfe924aa86732c3520baab84f914c2d8f4103ac40" + +[[region]] +host = "crates/observed_macros/Cargo.toml" +id = "anvil-lints" +checksum = "sha256:2dd7c0f21339fd17092b8dedfe924aa86732c3520baab84f914c2d8f4103ac40" + +[[region]] +host = "crates/observed_macros_impl/Cargo.toml" +id = "anvil-lints" +checksum = "sha256:2dd7c0f21339fd17092b8dedfe924aa86732c3520baab84f914c2d8f4103ac40" + +[[region]] +host = "crates/observed_testing/Cargo.toml" +id = "anvil-lints" +checksum = "sha256:2dd7c0f21339fd17092b8dedfe924aa86732c3520baab84f914c2d8f4103ac40" + +[[region]] +host = "crates/observed_utils/Cargo.toml" +id = "anvil-lints" +checksum = "sha256:2dd7c0f21339fd17092b8dedfe924aa86732c3520baab84f914c2d8f4103ac40" + [[region]] host = "crates/ohno/Cargo.toml" id = "anvil-lints" @@ -400,11 +510,61 @@ host = "crates/plurality/Cargo.toml" id = "anvil-lints" checksum = "sha256:2dd7c0f21339fd17092b8dedfe924aa86732c3520baab84f914c2d8f4103ac40" +[[region]] +host = "crates/rallocator/Cargo.toml" +id = "anvil-lints" +checksum = "sha256:2dd7c0f21339fd17092b8dedfe924aa86732c3520baab84f914c2d8f4103ac40" + +[[region]] +host = "crates/rallocator_cli/Cargo.toml" +id = "anvil-lints" +checksum = "sha256:2dd7c0f21339fd17092b8dedfe924aa86732c3520baab84f914c2d8f4103ac40" + +[[region]] +host = "crates/rallocator_telemetry/Cargo.toml" +id = "anvil-lints" +checksum = "sha256:2dd7c0f21339fd17092b8dedfe924aa86732c3520baab84f914c2d8f4103ac40" + +[[region]] +host = "crates/rallocator_wire/Cargo.toml" +id = "anvil-lints" +checksum = "sha256:2dd7c0f21339fd17092b8dedfe924aa86732c3520baab84f914c2d8f4103ac40" + [[region]] host = "crates/recoverable/Cargo.toml" id = "anvil-lints" checksum = "sha256:2dd7c0f21339fd17092b8dedfe924aa86732c3520baab84f914c2d8f4103ac40" +[[region]] +host = "crates/rest_over_grpc/Cargo.toml" +id = "anvil-lints" +checksum = "sha256:2dd7c0f21339fd17092b8dedfe924aa86732c3520baab84f914c2d8f4103ac40" + +[[region]] +host = "crates/rest_over_grpc_examples/Cargo.toml" +id = "anvil-lints" +checksum = "sha256:2dd7c0f21339fd17092b8dedfe924aa86732c3520baab84f914c2d8f4103ac40" + +[[region]] +host = "crates/rest_over_grpc_tests/Cargo.toml" +id = "anvil-lints" +checksum = "sha256:2dd7c0f21339fd17092b8dedfe924aa86732c3520baab84f914c2d8f4103ac40" + +[[region]] +host = "crates/routerama/Cargo.toml" +id = "anvil-lints" +checksum = "sha256:2dd7c0f21339fd17092b8dedfe924aa86732c3520baab84f914c2d8f4103ac40" + +[[region]] +host = "crates/routerama_build/Cargo.toml" +id = "anvil-lints" +checksum = "sha256:2dd7c0f21339fd17092b8dedfe924aa86732c3520baab84f914c2d8f4103ac40" + +[[region]] +host = "crates/routerama_macros/Cargo.toml" +id = "anvil-lints" +checksum = "sha256:2dd7c0f21339fd17092b8dedfe924aa86732c3520baab84f914c2d8f4103ac40" + [[region]] host = "crates/seatbelt/Cargo.toml" id = "anvil-lints" @@ -440,6 +600,16 @@ host = "crates/thread_aware/Cargo.toml" id = "anvil-lints" checksum = "sha256:2dd7c0f21339fd17092b8dedfe924aa86732c3520baab84f914c2d8f4103ac40" +[[region]] +host = "crates/thread_aware_benchmarking/Cargo.toml" +id = "anvil-lints" +checksum = "sha256:2dd7c0f21339fd17092b8dedfe924aa86732c3520baab84f914c2d8f4103ac40" + +[[region]] +host = "crates/thread_aware_core/Cargo.toml" +id = "anvil-lints" +checksum = "sha256:2dd7c0f21339fd17092b8dedfe924aa86732c3520baab84f914c2d8f4103ac40" + [[region]] host = "crates/thread_aware_macros/Cargo.toml" id = "anvil-lints" diff --git a/.anvil/container/Dockerfile b/.anvil/container/Dockerfile new file mode 100644 index 000000000..c5ab21688 --- /dev/null +++ b/.anvil/container/Dockerfile @@ -0,0 +1,112 @@ +# syntax=docker/dockerfile:1 + +# >>> anvil-managed: anvil-container-base-image +# Prebuilt binaries installed by `anvil-setup binstall` link against this +# image's glibc, so it tracks the Linux runner the generated workflows use. +# Digest-pinned: a floating tag moves content under a reference that claims to +# name fixed content. +# +# Re-declare BASE_IMAGE in the gap below to build on another base; a later ARG +# wins, and the pins anvil maintains stay current. +ARG BASE_IMAGE=docker.io/library/ubuntu:24.04@sha256:561618e2c15bf2397621dd04f96926663a3b5616c189cf7e38db7e82f5c538ea +# <<< anvil-managed: anvil-container-base-image + +# >>> anvil-managed: anvil-container-base +FROM ${BASE_IMAGE} + +ARG JUST_VERSION=1.56.0 +ARG JUST_SHA256=fa2a8ec1015d9df5330941ade12437488fc40d33f9c9f8cd4eb70a26de11b639 +ARG POWERSHELL_VERSION=7.6.3 +ARG POWERSHELL_SHA256=856d0765d2332377f9d7a4aea76efdfde4de51446e7738dde2dfda41dba9e2a7 +ARG RUSTUP_VERSION=1.29.0 +ARG RUSTUP_SHA256=4acc9acc76d5079515b46346a485974457b5a79893cfb01112423c89aeb5aa10 +ARG CARGO_BINSTALL_VERSION=1.21.1 +ARG CARGO_BINSTALL_SHA256=630c8f8803a686aa6779497f0f0fb51d49822fb5fc3c514d8ced33b34e338e6e + +ENV DEBIAN_FRONTEND=noninteractive \ + CARGO_HOME=/usr/local/cargo \ + RUSTUP_HOME=/usr/local/rustup \ + RUSTUP_NO_UPDATE_CHECK=1 \ + PATH=/usr/local/cargo/bin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin +# <<< anvil-managed: anvil-container-base + +# >>> anvil-managed: anvil-container-tools +# clang/libclang are required by cargo-spellcheck; the rest is the usual Rust +# link-time set. A bare base has no C runtime development files, so every link +# step fails without build-essential. +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + build-essential ca-certificates clang libclang-dev curl git libicu-dev \ + libssl-dev pkg-config tar \ + && rm -rf /var/lib/apt/lists/* + +# pwsh is not optional: every generated anvil recipe is a `script("pwsh", +# "-NoProfile")` recipe. +RUN curl -fsSLo /tmp/powershell.tar.gz \ + "https://github.com/PowerShell/PowerShell/releases/download/v${POWERSHELL_VERSION}/powershell-${POWERSHELL_VERSION}-linux-x64.tar.gz" \ + && echo "${POWERSHELL_SHA256} /tmp/powershell.tar.gz" | sha256sum -c - \ + && mkdir -p /opt/microsoft/powershell/7 \ + && tar -xzf /tmp/powershell.tar.gz -C /opt/microsoft/powershell/7 \ + && chmod 755 /opt/microsoft/powershell/7/pwsh \ + && ln -s /opt/microsoft/powershell/7/pwsh /usr/local/bin/pwsh \ + && rm /tmp/powershell.tar.gz + +RUN curl -fsSLo /tmp/just.tar.gz \ + "https://github.com/casey/just/releases/download/${JUST_VERSION}/just-${JUST_VERSION}-x86_64-unknown-linux-musl.tar.gz" \ + && echo "${JUST_SHA256} /tmp/just.tar.gz" | sha256sum -c - \ + && tar -xzf /tmp/just.tar.gz -C /usr/local/bin just \ + && chmod 755 /usr/local/bin/just \ + && rm /tmp/just.tar.gz + +RUN curl --proto '=https' --tlsv1.2 -fsSLo /tmp/rustup-init \ + "https://static.rust-lang.org/rustup/archive/${RUSTUP_VERSION}/x86_64-unknown-linux-gnu/rustup-init" \ + && echo "${RUSTUP_SHA256} /tmp/rustup-init" | sha256sum -c - \ + && chmod 755 /tmp/rustup-init \ + && /tmp/rustup-init -y --profile minimal --default-toolchain none --no-modify-path \ + && rm /tmp/rustup-init + +RUN curl -fsSLo /tmp/cargo-binstall.tgz \ + "https://github.com/cargo-bins/cargo-binstall/releases/download/v${CARGO_BINSTALL_VERSION}/cargo-binstall-x86_64-unknown-linux-musl.tgz" \ + && echo "${CARGO_BINSTALL_SHA256} /tmp/cargo-binstall.tgz" | sha256sum -c - \ + && mkdir -p "${CARGO_HOME}/bin" \ + && tar -xzf /tmp/cargo-binstall.tgz -C "${CARGO_HOME}/bin" cargo-binstall \ + && chmod 755 "${CARGO_HOME}/bin/cargo-binstall" \ + && rm /tmp/cargo-binstall.tgz +# <<< anvil-managed: anvil-container-tools + +# >>> anvil-managed: anvil-container-setup +# The whole recipe tree is copied because `just` parses it to reach the install +# recipes. +# +# The credential files are removed in the same layer that used them: a build +# secret never lands in a layer, but anything the install *writes* with it is +# ordinary content, and the `chmod` below would publish it world-readable. A +# later `RUN` cannot undo that, because the earlier layer keeps them. +# +# `HOME` is not set by this image and `docker build` does not set it either, so +# `${HOME}/.netrc` would expand to `/.netrc` and leave the file root actually +# uses. Both spellings are named. +# +# `registry` and `git` must exist before the `chmod`. The run mounts a named +# volume over each, and an engine seeds a new volume from the image path it +# covers; a path that does not exist seeds as root-owned 0755, which the +# `--user` mapping cannot write, so the first cargo fetch fails with EACCES. +WORKDIR /opt/anvil +COPY justfiles ./justfiles +COPY rust-toolchain.toml ./ +RUN printf "import 'justfiles/anvil/mod.just'\n" > Justfile \ + && just anvil-setup binstall \ + && rm -rf "${CARGO_HOME}/registry/cache" "${CARGO_HOME}/registry/src" \ + && rm -f "${CARGO_HOME}/credentials" "${CARGO_HOME}/credentials.toml" "${HOME:-/root}/.netrc" /root/.netrc \ + && mkdir -p "${CARGO_HOME}/registry" "${CARGO_HOME}/git" \ + && chmod -R a+rwX "${CARGO_HOME}" "${RUSTUP_HOME}" +# <<< anvil-managed: anvil-container-setup + +# >>> anvil-managed: anvil-container-entry +# Consumed by `anvil-container` itself: a nested invocation from inside the +# image runs the recipe natively instead of launching another container. +ENV ANVIL_IN_CONTAINER=1 + +WORKDIR /workspace +CMD ["bash"] +# <<< anvil-managed: anvil-container-entry diff --git a/.anvil/container/Dockerfile.dockerignore b/.anvil/container/Dockerfile.dockerignore new file mode 100644 index 000000000..577907f0e --- /dev/null +++ b/.anvil/container/Dockerfile.dockerignore @@ -0,0 +1,37 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# GENERATED BY cargo-anvil. DO NOT EDIT DIRECTLY. +# Update the corresponding template in the cargo-anvil crate. +# +# BuildKit reads `.dockerignore` in preference to a root +# `.dockerignore`, so this scopes the exec-image build context without the +# repository having to own a root ignore file or having one silently overridden. +# +# The build context is the repository root but the image only needs two things. +# Excluding everything else keeps a cold build from streaming the whole +# worktree (and every stale `target/`) to the daemon. +# +# The context is narrowed to `justfiles/anvil/` rather than all of `justfiles/` +# so that a cold build does not stream unrelated trees to the daemon. The +# recipes are copied to drive `just anvil-setup`, which needs the whole tree to +# parse, and the whole tree is hashed into the image tag: the tier, group and +# check recipes decide which tools `anvil-setup` reaches, not just the catalog. +# +# `.anvil/container/` is admitted because the Dockerfile is composed: the gaps +# between anvil's regions exist for a repository to add its own instructions, +# and the headline case -- `COPY`ing a corporate root CA in before the first +# download -- needs the file to be in the context. Denying it would leave the +# gap documented but unusable for anything but `RUN`. It is also the directory +# the image tag digests, so what the context admits and what the tag covers stay +# the same set -- including the `.anvil-proposed` siblings both exclude, which +# are anvil's review artifacts rather than build inputs. +* +!justfiles +justfiles/* +!justfiles/anvil +justfiles/anvil/**/*.anvil-proposed +!.anvil +.anvil/* +!.anvil/container +.anvil/container/**/*.anvil-proposed +!rust-toolchain.toml diff --git a/.delta.toml b/.delta.toml index 739cb27a8..50ce9cc0b 100644 --- a/.delta.toml +++ b/.delta.toml @@ -1,3 +1,6 @@ +# >>> anvil-managed: anvil-delta +# <<< anvil-managed: anvil-delta + # cargo-delta configuration file # Patterns for files and folders to exclude from analysis. @@ -82,14 +85,3 @@ assume_patterns = [ # The remote branch to compare against for determining changed files # If not specified, uses the default branch detection remote_branch = "origin/main" - -# >>> anvil-managed: anvil-delta -[delta] -# Include the workspace root files that should invalidate every member's -# impact analysis when changed (lockfile, root manifest, toolchain). -root-files = [ - "Cargo.lock", - "Cargo.toml", - "rust-toolchain.toml", -] -# <<< anvil-managed: anvil-delta diff --git a/.gitattributes b/.gitattributes index 845e29f8a..970a26ba3 100644 --- a/.gitattributes +++ b/.gitattributes @@ -30,4 +30,5 @@ # Force LF line endings for Rust sources regardless of the checkout # platform, so rustfmt and other tooling see consistent newlines. *.rs text eol=lf +*.sh text eol=lf # <<< anvil-managed: anvil-gitattributes diff --git a/.github/actions/anvil-impact/action.yml b/.github/actions/anvil-impact/action.yml index 13612c4e1..d5f41291b 100644 --- a/.github/actions/anvil-impact/action.yml +++ b/.github/actions/anvil-impact/action.yml @@ -1,44 +1,25 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -# Managed by cargo-anvil. Update the corresponding template in the cargo-anvil -# crate unless the change is repository-specific. +# GENERATED BY cargo-anvil. DO NOT EDIT DIRECTLY. +# Update cargo-anvil and regenerate; repository-specific edits stop automatic updates. # Update behaviour: https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/updates.md name: anvil-impact description: | - Compute the cargo-delta impact set for this PR and emit per-tier - include lists. + Compute the cargo-delta impact set for this PR and publish it as the + `anvil-impact-` workflow artifact. - Outputs: - include_modified - "--package X@ver --package Y@ver" string for crates - whose source files changed in the diff, or "--skip" - if the modified set is empty. Each package is a - version-qualified cargo spec (name@version) so it - resolves unambiguously even when a like-named crate - is also a different-versioned transitive dependency. - include_affected - same shape, for crates in the affected set - (modified ∪ rev-deps). - include_required - same shape, for crates in the required set - (affected ∪ workspace-internal transitive deps). + This runs the same `anvil-impact` recipe adopters run locally: it snapshots + the base ref and the working tree, runs `cargo delta impact`, and + writes the durable cache under `target/anvil/impact/` (the per-tier + `include_.txt` lists, `impact.json`, and the `snapshots/`). The whole + directory is uploaded so each downstream group job can download it and read + the cache exactly as a local run does -- rather than threading the include + lists through job outputs / environment variables. This keeps CI and local + execution identical by construction. - Recipes in checks.just interpret each variable per their tier: - modified-tier recipes (fmt, license-headers, spellcheck, ...) short- - circuit on "--skip"; affected-tier recipes (clippy, tests, ...) and - required-tier recipes (doc, cargo-hack, udeps) splice their include - list into the cargo invocation, defaulting to --workspace when unset - (local runs without impact wiring). - - Unscoped recipes (deny, audit, aprz, pr-title) ignore all three - variables and always run unconditionally. -outputs: - include_modified: - description: Pre-formatted --package args for the modified tier. - value: ${{ steps.compute.outputs.include_modified }} - include_affected: - description: Pre-formatted --package args for the affected tier. - value: ${{ steps.compute.outputs.include_affected }} - include_required: - description: Pre-formatted --package args for the required tier. - value: ${{ steps.compute.outputs.include_required }} + Computed once per OS family (see anvil-pr-impl.yml) because an + OS-conditional dependency changes the reverse-dep set only in that host's + cargo-metadata graph. runs: using: composite steps: @@ -53,68 +34,31 @@ runs: - name: Install cargo-delta shell: bash run: just anvil-tool-cargo-delta-install binstall - - id: compute - name: Compute impact + - name: Compute impact shell: bash - run: | - set -euo pipefail - # Resolve the baseline ref via the shared anvil resolver (single - # source of truth; see the _anvil-base-ref recipe). On GitHub PRs it - # resolves GITHUB_BASE_REF to origin/; BASE_REF overrides. - base="$(just _anvil-base-ref)" - # The baseline worktree is checked out at the merge target, whose - # rust-toolchain.toml may pin a toolchain that is NOT installed on the - # runner (any PR that bumps rust-toolchain.toml hits this). The - # snapshot is metadata-only (file presence + content hashes), so run - # the baseline under the runner's *active* toolchain via - # RUSTUP_TOOLCHAIN, which overrides the worktree's rust-toolchain.toml. - # Capture it here, in the PR checkout, where it is resolved + installed. - active_toolchain="$(rustup show active-toolchain | head -n1 | cut -d' ' -f1)" - # cargo delta has no --base flag; the flow is two snapshots - # (baseline at the merge target + current at HEAD) compared by - # `cargo delta impact`. We use a temporary worktree to snapshot - # the baseline without disturbing the checked-out tree. - cargo delta snapshot > "$RUNNER_TEMP/anvil-current.json" - git worktree add --detach "$RUNNER_TEMP/anvil-baseline" "$base" - # When anvil is first introduced (this PR adds the cargo workspace), - # the baseline branch has no root Cargo.toml, so there is no - # workspace for cargo-delta to snapshot. Detect that explicitly and - # fall back to full-workspace validation (empty include_* -> recipes - # default to --workspace) rather than running cargo-delta. This is a - # targeted check: any *other* cargo-delta failure below is a real - # error and is allowed to fail the impact job (and the gated pr-* jobs). - if [ ! -f "$RUNNER_TEMP/anvil-baseline/Cargo.toml" ] ; then - echo "anvil impact: baseline '$base' has no root Cargo.toml (not a cargo workspace yet -- first-time anvil adoption?). Falling back to full-workspace validation." >&2 - git worktree remove --force "$RUNNER_TEMP/anvil-baseline" - { - echo "include_modified=" - echo "include_affected=" - echo "include_required=" - } >> "$GITHUB_OUTPUT" - exit 0 - fi - ( cd "$RUNNER_TEMP/anvil-baseline" \ - && RUSTUP_TOOLCHAIN="$active_toolchain" cargo delta snapshot ) \ - > "$RUNNER_TEMP/anvil-baseline.json" - git worktree remove --force "$RUNNER_TEMP/anvil-baseline" - cargo delta impact \ - --baseline "$RUNNER_TEMP/anvil-baseline.json" \ - --current "$RUNNER_TEMP/anvil-current.json" \ - --format json \ - > "$RUNNER_TEMP/anvil-impact.json" - # Format each tier into the `--package name@version ...` shape the - # checks.just recipes consume (or the "--skip" sentinel when a - # tier is empty), via the shared _anvil-impact-format recipe -- - # single source of truth across the GitHub and ADO impact steps. - # See that recipe in checks.just for the version-qualification and - # lib-name translation rationale. Capture each formatted value into - # a variable first, then write plain `name=value` lines, so the - # quoted JSON-path argument never sits inside the output `echo`. - modified="$(just _anvil-impact-format modified "$RUNNER_TEMP/anvil-impact.json")" - affected="$(just _anvil-impact-format affected "$RUNNER_TEMP/anvil-impact.json")" - required="$(just _anvil-impact-format required "$RUNNER_TEMP/anvil-impact.json")" - { - echo "include_modified=$modified" - echo "include_affected=$affected" - echo "include_required=$required" - } >> "$GITHUB_OUTPUT" + # Run the shared anvil-impact recipe -- the same impact building block + # adopters run locally. It resolves the base ref (_anvil-base-ref), + # snapshots the base ref in a throwaway worktree and the working + # tree, runs `cargo delta impact`, and writes the cache under + # target/anvil/impact/. This is the only job that runs cargo-delta to + # compute the impact set; group jobs install it as a setup prereq (for + # local recompute-capable runs) but consume the downloaded artifact + # instead of recomputing. + run: just anvil-impact + - name: Upload impact artifact + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + # Per-OS name so a downstream leg downloads the impact set computed on + # its own host (Linux / Windows). The two arm legs reuse their OS + # family's artifact. + name: anvil-impact-${{ runner.os }} + path: target/anvil/impact + # Match GitHub's default 30-day workflow-rerun window. Consumer group + # jobs (pr-fast, pr-slow, ...) unconditionally download this artifact + # and run under ANVIL_IMPACT=consume, so a rerun triggered within that + # window must still find the impact set. A shorter lifetime (e.g. 1 day) + # would silently cap the effective rerun window at that lifetime -- the + # download step would fail once the artifact expired even though GitHub + # still offers the rerun. The set is small (a few cache files), so the + # storage cost of the full window is negligible. + retention-days: 30 diff --git a/.github/actions/anvil-pr-fast/action.yml b/.github/actions/anvil-pr-fast/action.yml deleted file mode 100644 index b811ad1c7..000000000 --- a/.github/actions/anvil-pr-fast/action.yml +++ /dev/null @@ -1,55 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# Managed by cargo-anvil. Update the corresponding template in the cargo-anvil -# crate unless the change is repository-specific. -# Update behaviour: https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/updates.md -# The token pr-fast is substituted by cargo-anvil at emit time with -# the concrete check-group name (pr-fast, pr-test, scheduled-runtime-analysis, ...). -name: anvil-pr-fast -description: Run the pr-fast check group. -inputs: - include_modified: - description: | - Pre-formatted --package args (e.g. "--package alpha@1.0.0 --package - beta@0.2.0") for the modified tier, or the sentinel "--skip" when - nothing modified. Packages are version-qualified cargo specs so they - resolve uniquely even when a like-named crate is also a transitive - dependency. Local invocations leave it unset; recipes default to - --workspace. - default: "" - required: false - include_affected: - description: | - Same shape as include_modified, but for the affected tier - (modified ∪ rev-deps). - default: "" - required: false - include_required: - description: | - Same shape as include_modified, but for the required tier - (affected ∪ workspace-internal transitive deps). - default: "" - required: false - free-disk-space: - description: Remove unused toolchains from GitHub-hosted runners before setup. - default: "false" - required: false -runs: - using: composite - steps: - - uses: ./.github/actions/anvil-setup - with: - group: pr-fast - free-disk-space: ${{ inputs.free-disk-space }} - - name: Run just anvil-pr-fast - shell: bash - env: - ANVIL_INCLUDE_MODIFIED: ${{ inputs.include_modified }} - ANVIL_INCLUDE_AFFECTED: ${{ inputs.include_affected }} - ANVIL_INCLUDE_REQUIRED: ${{ inputs.include_required }} - # Some checks (e.g. cargo-aprz, run only by groups that include it) - # hit the GitHub API; pass the built-in token so they use the - # authenticated quota (1000 vs 60 req/hr). Harmless for groups whose - # checks never read it. - GITHUB_TOKEN: ${{ github.token }} - run: just anvil-pr-fast diff --git a/.github/actions/anvil-pr-mutants/action.yml b/.github/actions/anvil-pr-mutants/action.yml deleted file mode 100644 index 48ea1d843..000000000 --- a/.github/actions/anvil-pr-mutants/action.yml +++ /dev/null @@ -1,55 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# Managed by cargo-anvil. Update the corresponding template in the cargo-anvil -# crate unless the change is repository-specific. -# Update behaviour: https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/updates.md -# The token pr-mutants is substituted by cargo-anvil at emit time with -# the concrete check-group name (pr-fast, pr-test, scheduled-runtime-analysis, ...). -name: anvil-pr-mutants -description: Run the pr-mutants check group. -inputs: - include_modified: - description: | - Pre-formatted --package args (e.g. "--package alpha@1.0.0 --package - beta@0.2.0") for the modified tier, or the sentinel "--skip" when - nothing modified. Packages are version-qualified cargo specs so they - resolve uniquely even when a like-named crate is also a transitive - dependency. Local invocations leave it unset; recipes default to - --workspace. - default: "" - required: false - include_affected: - description: | - Same shape as include_modified, but for the affected tier - (modified ∪ rev-deps). - default: "" - required: false - include_required: - description: | - Same shape as include_modified, but for the required tier - (affected ∪ workspace-internal transitive deps). - default: "" - required: false - free-disk-space: - description: Remove unused toolchains from GitHub-hosted runners before setup. - default: "false" - required: false -runs: - using: composite - steps: - - uses: ./.github/actions/anvil-setup - with: - group: pr-mutants - free-disk-space: ${{ inputs.free-disk-space }} - - name: Run just anvil-pr-mutants - shell: bash - env: - ANVIL_INCLUDE_MODIFIED: ${{ inputs.include_modified }} - ANVIL_INCLUDE_AFFECTED: ${{ inputs.include_affected }} - ANVIL_INCLUDE_REQUIRED: ${{ inputs.include_required }} - # Some checks (e.g. cargo-aprz, run only by groups that include it) - # hit the GitHub API; pass the built-in token so they use the - # authenticated quota (1000 vs 60 req/hr). Harmless for groups whose - # checks never read it. - GITHUB_TOKEN: ${{ github.token }} - run: just anvil-pr-mutants diff --git a/.github/actions/anvil-pr-runtime-analysis/action.yml b/.github/actions/anvil-pr-runtime-analysis/action.yml deleted file mode 100644 index 1e34dd904..000000000 --- a/.github/actions/anvil-pr-runtime-analysis/action.yml +++ /dev/null @@ -1,55 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# Managed by cargo-anvil. Update the corresponding template in the cargo-anvil -# crate unless the change is repository-specific. -# Update behaviour: https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/updates.md -# The token pr-runtime-analysis is substituted by cargo-anvil at emit time with -# the concrete check-group name (pr-fast, pr-test, scheduled-runtime-analysis, ...). -name: anvil-pr-runtime-analysis -description: Run the pr-runtime-analysis check group. -inputs: - include_modified: - description: | - Pre-formatted --package args (e.g. "--package alpha@1.0.0 --package - beta@0.2.0") for the modified tier, or the sentinel "--skip" when - nothing modified. Packages are version-qualified cargo specs so they - resolve uniquely even when a like-named crate is also a transitive - dependency. Local invocations leave it unset; recipes default to - --workspace. - default: "" - required: false - include_affected: - description: | - Same shape as include_modified, but for the affected tier - (modified ∪ rev-deps). - default: "" - required: false - include_required: - description: | - Same shape as include_modified, but for the required tier - (affected ∪ workspace-internal transitive deps). - default: "" - required: false - free-disk-space: - description: Remove unused toolchains from GitHub-hosted runners before setup. - default: "false" - required: false -runs: - using: composite - steps: - - uses: ./.github/actions/anvil-setup - with: - group: pr-runtime-analysis - free-disk-space: ${{ inputs.free-disk-space }} - - name: Run just anvil-pr-runtime-analysis - shell: bash - env: - ANVIL_INCLUDE_MODIFIED: ${{ inputs.include_modified }} - ANVIL_INCLUDE_AFFECTED: ${{ inputs.include_affected }} - ANVIL_INCLUDE_REQUIRED: ${{ inputs.include_required }} - # Some checks (e.g. cargo-aprz, run only by groups that include it) - # hit the GitHub API; pass the built-in token so they use the - # authenticated quota (1000 vs 60 req/hr). Harmless for groups whose - # checks never read it. - GITHUB_TOKEN: ${{ github.token }} - run: just anvil-pr-runtime-analysis diff --git a/.github/actions/anvil-pr-test/action.yml b/.github/actions/anvil-pr-test/action.yml deleted file mode 100644 index 3c6cecb3e..000000000 --- a/.github/actions/anvil-pr-test/action.yml +++ /dev/null @@ -1,55 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# Managed by cargo-anvil. Update the corresponding template in the cargo-anvil -# crate unless the change is repository-specific. -# Update behaviour: https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/updates.md -# The token pr-test is substituted by cargo-anvil at emit time with -# the concrete check-group name (pr-fast, pr-test, scheduled-runtime-analysis, ...). -name: anvil-pr-test -description: Run the pr-test check group. -inputs: - include_modified: - description: | - Pre-formatted --package args (e.g. "--package alpha@1.0.0 --package - beta@0.2.0") for the modified tier, or the sentinel "--skip" when - nothing modified. Packages are version-qualified cargo specs so they - resolve uniquely even when a like-named crate is also a transitive - dependency. Local invocations leave it unset; recipes default to - --workspace. - default: "" - required: false - include_affected: - description: | - Same shape as include_modified, but for the affected tier - (modified ∪ rev-deps). - default: "" - required: false - include_required: - description: | - Same shape as include_modified, but for the required tier - (affected ∪ workspace-internal transitive deps). - default: "" - required: false - free-disk-space: - description: Remove unused toolchains from GitHub-hosted runners before setup. - default: "false" - required: false -runs: - using: composite - steps: - - uses: ./.github/actions/anvil-setup - with: - group: pr-test - free-disk-space: ${{ inputs.free-disk-space }} - - name: Run just anvil-pr-test - shell: bash - env: - ANVIL_INCLUDE_MODIFIED: ${{ inputs.include_modified }} - ANVIL_INCLUDE_AFFECTED: ${{ inputs.include_affected }} - ANVIL_INCLUDE_REQUIRED: ${{ inputs.include_required }} - # Some checks (e.g. cargo-aprz, run only by groups that include it) - # hit the GitHub API; pass the built-in token so they use the - # authenticated quota (1000 vs 60 req/hr). Harmless for groups whose - # checks never read it. - GITHUB_TOKEN: ${{ github.token }} - run: just anvil-pr-test diff --git a/.github/actions/anvil-report-status/action.yml b/.github/actions/anvil-report-status/action.yml new file mode 100644 index 000000000..298dcbe15 --- /dev/null +++ b/.github/actions/anvil-report-status/action.yml @@ -0,0 +1,137 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# GENERATED BY cargo-anvil. DO NOT EDIT DIRECTLY. +# Update cargo-anvil and regenerate; repository-specific edits stop automatic updates. +# Update behaviour: https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/updates.md +name: anvil-report-status +description: Manage supplemental Anvil failure commit statuses on a pull-request head. +inputs: + group: + description: Anvil group recipe name without the anvil- prefix. + required: true + setup_outcome: + description: Outcome of the Anvil setup step. + required: true + exit_code: + description: Exit code returned by the group recipe, if it ran. + required: false + default: "" + failed_recipe: + description: Terminal failed recipe reported by Just, if any. + required: false + default: "" +runs: + using: composite + steps: + - name: Publish commit status + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + ANVIL_GROUP: ${{ inputs.group }} + ANVIL_SETUP_OUTCOME: ${{ inputs.setup_outcome }} + ANVIL_EXIT_CODE: ${{ inputs.exit_code }} + ANVIL_FAILED_RECIPE: ${{ inputs.failed_recipe }} + with: + github-token: ${{ github.token }} + script: | + const group = process.env.ANVIL_GROUP; + const setupOutcome = process.env.ANVIL_SETUP_OUTCOME; + const exitCode = process.env.ANVIL_EXIT_CODE; + const failedRecipe = process.env.ANVIL_FAILED_RECIPE || `anvil-${group}`; + const displayRecipe = failedRecipe.replace(/^anvil-/, ""); + const runner = `${process.env.RUNNER_OS}-${process.env.RUNNER_ARCH}`.toLowerCase(); + if (!context.payload.pull_request?.head?.sha) { + throw new Error( + "anvil-report-status requires a pull_request event with a head SHA", + ); + } + const headSha = context.payload.pull_request.head.sha; + const groupMarker = `#anvil-group=${encodeURIComponent(group)}`; + const targetUrl = + `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}` + + `/actions/runs/${process.env.GITHUB_RUN_ID}${groupMarker}`; + // GitHub's commit-status API limits descriptions to 140 characters + // and contexts to 100 characters: + // https://docs.github.com/rest/commits/statuses + const MAX_DESCRIPTION_LENGTH = 140; + const MAX_CONTEXT_LENGTH = 100; + const MAX_STATUS_PAGE_SIZE = 100; + const MIN_FAILURE_LABEL_LENGTH = 1; + const statusPrefix = "Anvil / "; + // Include the group in the persistent identity so two groups that + // reach the same recipe on one runner cannot overwrite each other. + const statusSuffix = ` [${group}] (${runner})`; + + let state; + let description; + let failureLabel; + if (setupOutcome !== "success") { + state = "error"; + description = `setup failed before ${group} ran`; + failureLabel = `${group} setup`; + } else if (exitCode === "0") { + state = "success"; + description = `all ${group} recipes passed`; + } else if (exitCode === "") { + state = "error"; + description = `${group} did not report a result`; + failureLabel = `${group} no result`; + } else { + state = "failure"; + description = `${displayRecipe} failed`; + failureLabel = displayRecipe; + } + + const history = await github.paginate( + github.rest.repos.listCommitStatusesForRef, + { ...context.repo, ref: headSha, per_page: MAX_STATUS_PAGE_SIZE }, + ); + const seen = new Set(); + const activePriorContexts = []; + for (const status of history) { + if (seen.has(status.context)) { + continue; + } + seen.add(status.context); + if ( + status.context.startsWith(statusPrefix) && + status.context.endsWith(statusSuffix) && + status.target_url?.endsWith(groupMarker) + ) { + if (status.state !== "success") { + activePriorContexts.push(status.context); + } + } + } + + const publish = (statusContext, statusState, statusDescription) => + github.rest.repos.createCommitStatus({ + ...context.repo, + sha: headSha, + state: statusState, + context: statusContext, + description: statusDescription.slice(0, MAX_DESCRIPTION_LENGTH), + target_url: targetUrl, + }); + + let currentContext; + if (state !== "success") { + const availableLabelLength = Math.max( + MIN_FAILURE_LABEL_LENGTH, + MAX_CONTEXT_LENGTH - statusPrefix.length - statusSuffix.length, + ); + currentContext = + statusPrefix + failureLabel.slice(0, availableLabelLength) + statusSuffix; + await publish(currentContext, state, description); + } + + for (const priorContext of activePriorContexts) { + if (priorContext !== currentContext) { + await publish( + priorContext, + "success", + state === "success" + ? `superseded: all ${group} recipes passed` + : `superseded by ${failureLabel}`, + ); + } + } diff --git a/.github/actions/anvil-run-group/action.yml b/.github/actions/anvil-run-group/action.yml new file mode 100644 index 000000000..ce3c159fe --- /dev/null +++ b/.github/actions/anvil-run-group/action.yml @@ -0,0 +1,91 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# GENERATED BY cargo-anvil. DO NOT EDIT DIRECTLY. +# Update cargo-anvil and regenerate; repository-specific edits stop automatic updates. +# Update behaviour: https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/updates.md +name: anvil-run-group +description: Run an Anvil Just group and report its result. +inputs: + group: + description: Anvil group recipe name without the anvil- prefix. + required: true + impact_mode: + description: | + Impact-scoping mode exported as ANVIL_IMPACT before the group runs. + Fixed by tier at the call site, never probed from a runtime marker: + - "consume" (PR groups): trust the impact cache the caller downloaded + into target/anvil/impact/. The group's `anvil-impact` dependency + no-ops (no snapshot, cargo-delta, or base-ref resolution) and the + scoped check recipes read the downloaded cache via + `_anvil-impact-include`, exactly as a local run does. + - "off" (scheduled groups): every tier runs full-workspace, so no + leftover cache on the runner can wrongly narrow the backstop. + Defaults to "off" so an un-wired caller never silently scopes checks out. + default: "off" + required: false + free-disk-space: + description: Remove unused toolchains from GitHub-hosted runners before setup. + default: "false" + required: false + publish_commit_statuses: + description: >- + Best-effort management of supplemental failure commit statuses for + same-repository pull requests. The caller must grant statuses: write. + Clean runs only supersede prior failures. + default: "false" + required: false +runs: + using: composite + steps: + - id: setup + uses: ./.github/actions/anvil-setup + with: + group: ${{ inputs.group }} + free-disk-space: ${{ inputs.free-disk-space }} + + - name: Run Anvil group + id: run + if: steps.setup.outcome == 'success' + shell: bash + env: + ANVIL_GROUP: ${{ inputs.group }} + # The impact set reaches scoped checks through the downloaded + # target/anvil/impact cache (read via `_anvil-impact-include`), not + # threaded --package strings. This action only fixes the mode. + ANVIL_IMPACT: ${{ inputs.impact_mode }} + # Some checks (e.g. cargo-aprz) call GitHub's API. The built-in token + # gives them the authenticated quota without adding group knowledge + # to this action. + GITHUB_TOKEN: ${{ github.token }} + run: | + # Capture the group result before propagating failure so the + # best-effort reporter can consume both outputs. + log="$RUNNER_TEMP/anvil-$ANVIL_GROUP.log" + set +e + just "anvil-$ANVIL_GROUP" 2>&1 | tee "$log" + # Preserve Just's status rather than tee's pipeline status. + status=${PIPESTATUS[0]} + set -e + + # The final terminal Just diagnostic identifies the concrete failure; + # fall back to the group when a tool exits without that diagnostic. + failed_recipe="$(sed -n 's/^error: recipe `\([^`]*\)` failed\( on line [0-9][0-9]*\)\{0,1\} with exit code [0-9][0-9]*$/\1/p' "$log" | tail -n 1)" + echo "failed_recipe=${failed_recipe:-anvil-$ANVIL_GROUP}" >> "$GITHUB_OUTPUT" + echo "exit_code=$status" >> "$GITHUB_OUTPUT" + + # Reporting is supplemental: run after success or failure, but never let + # an API outage determine the authoritative workflow-job result. + - name: Publish supplemental Anvil commit status + if: always() && inputs.publish_commit_statuses == 'true' && github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository + continue-on-error: true + uses: ./.github/actions/anvil-report-status + with: + group: ${{ inputs.group }} + setup_outcome: ${{ steps.setup.outcome }} + exit_code: ${{ steps.run.outputs.exit_code }} + failed_recipe: ${{ steps.run.outputs.failed_recipe }} + + - name: "Failed Just recipe: ${{ steps.run.outputs.failed_recipe }}" + if: always() && steps.run.outputs.exit_code != '' && steps.run.outputs.exit_code != '0' + shell: bash + run: exit 1 diff --git a/.github/actions/anvil-scheduled-advisories/action.yml b/.github/actions/anvil-scheduled-advisories/action.yml deleted file mode 100644 index 532ea3f63..000000000 --- a/.github/actions/anvil-scheduled-advisories/action.yml +++ /dev/null @@ -1,55 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# Managed by cargo-anvil. Update the corresponding template in the cargo-anvil -# crate unless the change is repository-specific. -# Update behaviour: https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/updates.md -# The token scheduled-advisories is substituted by cargo-anvil at emit time with -# the concrete check-group name (pr-fast, pr-test, scheduled-runtime-analysis, ...). -name: anvil-scheduled-advisories -description: Run the scheduled-advisories check group. -inputs: - include_modified: - description: | - Pre-formatted --package args (e.g. "--package alpha@1.0.0 --package - beta@0.2.0") for the modified tier, or the sentinel "--skip" when - nothing modified. Packages are version-qualified cargo specs so they - resolve uniquely even when a like-named crate is also a transitive - dependency. Local invocations leave it unset; recipes default to - --workspace. - default: "" - required: false - include_affected: - description: | - Same shape as include_modified, but for the affected tier - (modified ∪ rev-deps). - default: "" - required: false - include_required: - description: | - Same shape as include_modified, but for the required tier - (affected ∪ workspace-internal transitive deps). - default: "" - required: false - free-disk-space: - description: Remove unused toolchains from GitHub-hosted runners before setup. - default: "false" - required: false -runs: - using: composite - steps: - - uses: ./.github/actions/anvil-setup - with: - group: scheduled-advisories - free-disk-space: ${{ inputs.free-disk-space }} - - name: Run just anvil-scheduled-advisories - shell: bash - env: - ANVIL_INCLUDE_MODIFIED: ${{ inputs.include_modified }} - ANVIL_INCLUDE_AFFECTED: ${{ inputs.include_affected }} - ANVIL_INCLUDE_REQUIRED: ${{ inputs.include_required }} - # Some checks (e.g. cargo-aprz, run only by groups that include it) - # hit the GitHub API; pass the built-in token so they use the - # authenticated quota (1000 vs 60 req/hr). Harmless for groups whose - # checks never read it. - GITHUB_TOKEN: ${{ github.token }} - run: just anvil-scheduled-advisories diff --git a/.github/actions/anvil-scheduled-exhaustive/action.yml b/.github/actions/anvil-scheduled-exhaustive/action.yml deleted file mode 100644 index 1f992d571..000000000 --- a/.github/actions/anvil-scheduled-exhaustive/action.yml +++ /dev/null @@ -1,55 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# Managed by cargo-anvil. Update the corresponding template in the cargo-anvil -# crate unless the change is repository-specific. -# Update behaviour: https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/updates.md -# The token scheduled-exhaustive is substituted by cargo-anvil at emit time with -# the concrete check-group name (pr-fast, pr-test, scheduled-runtime-analysis, ...). -name: anvil-scheduled-exhaustive -description: Run the scheduled-exhaustive check group. -inputs: - include_modified: - description: | - Pre-formatted --package args (e.g. "--package alpha@1.0.0 --package - beta@0.2.0") for the modified tier, or the sentinel "--skip" when - nothing modified. Packages are version-qualified cargo specs so they - resolve uniquely even when a like-named crate is also a transitive - dependency. Local invocations leave it unset; recipes default to - --workspace. - default: "" - required: false - include_affected: - description: | - Same shape as include_modified, but for the affected tier - (modified ∪ rev-deps). - default: "" - required: false - include_required: - description: | - Same shape as include_modified, but for the required tier - (affected ∪ workspace-internal transitive deps). - default: "" - required: false - free-disk-space: - description: Remove unused toolchains from GitHub-hosted runners before setup. - default: "false" - required: false -runs: - using: composite - steps: - - uses: ./.github/actions/anvil-setup - with: - group: scheduled-exhaustive - free-disk-space: ${{ inputs.free-disk-space }} - - name: Run just anvil-scheduled-exhaustive - shell: bash - env: - ANVIL_INCLUDE_MODIFIED: ${{ inputs.include_modified }} - ANVIL_INCLUDE_AFFECTED: ${{ inputs.include_affected }} - ANVIL_INCLUDE_REQUIRED: ${{ inputs.include_required }} - # Some checks (e.g. cargo-aprz, run only by groups that include it) - # hit the GitHub API; pass the built-in token so they use the - # authenticated quota (1000 vs 60 req/hr). Harmless for groups whose - # checks never read it. - GITHUB_TOKEN: ${{ github.token }} - run: just anvil-scheduled-exhaustive diff --git a/.github/actions/anvil-scheduled-runtime-analysis/action.yml b/.github/actions/anvil-scheduled-runtime-analysis/action.yml deleted file mode 100644 index b9c831ee8..000000000 --- a/.github/actions/anvil-scheduled-runtime-analysis/action.yml +++ /dev/null @@ -1,55 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# Managed by cargo-anvil. Update the corresponding template in the cargo-anvil -# crate unless the change is repository-specific. -# Update behaviour: https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/updates.md -# The token scheduled-runtime-analysis is substituted by cargo-anvil at emit time with -# the concrete check-group name (pr-fast, pr-test, scheduled-runtime-analysis, ...). -name: anvil-scheduled-runtime-analysis -description: Run the scheduled-runtime-analysis check group. -inputs: - include_modified: - description: | - Pre-formatted --package args (e.g. "--package alpha@1.0.0 --package - beta@0.2.0") for the modified tier, or the sentinel "--skip" when - nothing modified. Packages are version-qualified cargo specs so they - resolve uniquely even when a like-named crate is also a transitive - dependency. Local invocations leave it unset; recipes default to - --workspace. - default: "" - required: false - include_affected: - description: | - Same shape as include_modified, but for the affected tier - (modified ∪ rev-deps). - default: "" - required: false - include_required: - description: | - Same shape as include_modified, but for the required tier - (affected ∪ workspace-internal transitive deps). - default: "" - required: false - free-disk-space: - description: Remove unused toolchains from GitHub-hosted runners before setup. - default: "false" - required: false -runs: - using: composite - steps: - - uses: ./.github/actions/anvil-setup - with: - group: scheduled-runtime-analysis - free-disk-space: ${{ inputs.free-disk-space }} - - name: Run just anvil-scheduled-runtime-analysis - shell: bash - env: - ANVIL_INCLUDE_MODIFIED: ${{ inputs.include_modified }} - ANVIL_INCLUDE_AFFECTED: ${{ inputs.include_affected }} - ANVIL_INCLUDE_REQUIRED: ${{ inputs.include_required }} - # Some checks (e.g. cargo-aprz, run only by groups that include it) - # hit the GitHub API; pass the built-in token so they use the - # authenticated quota (1000 vs 60 req/hr). Harmless for groups whose - # checks never read it. - GITHUB_TOKEN: ${{ github.token }} - run: just anvil-scheduled-runtime-analysis diff --git a/.github/actions/anvil-scheduled-test/action.yml b/.github/actions/anvil-scheduled-test/action.yml deleted file mode 100644 index d79289c87..000000000 --- a/.github/actions/anvil-scheduled-test/action.yml +++ /dev/null @@ -1,55 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# Managed by cargo-anvil. Update the corresponding template in the cargo-anvil -# crate unless the change is repository-specific. -# Update behaviour: https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/updates.md -# The token scheduled-test is substituted by cargo-anvil at emit time with -# the concrete check-group name (pr-fast, pr-test, scheduled-runtime-analysis, ...). -name: anvil-scheduled-test -description: Run the scheduled-test check group. -inputs: - include_modified: - description: | - Pre-formatted --package args (e.g. "--package alpha@1.0.0 --package - beta@0.2.0") for the modified tier, or the sentinel "--skip" when - nothing modified. Packages are version-qualified cargo specs so they - resolve uniquely even when a like-named crate is also a transitive - dependency. Local invocations leave it unset; recipes default to - --workspace. - default: "" - required: false - include_affected: - description: | - Same shape as include_modified, but for the affected tier - (modified ∪ rev-deps). - default: "" - required: false - include_required: - description: | - Same shape as include_modified, but for the required tier - (affected ∪ workspace-internal transitive deps). - default: "" - required: false - free-disk-space: - description: Remove unused toolchains from GitHub-hosted runners before setup. - default: "false" - required: false -runs: - using: composite - steps: - - uses: ./.github/actions/anvil-setup - with: - group: scheduled-test - free-disk-space: ${{ inputs.free-disk-space }} - - name: Run just anvil-scheduled-test - shell: bash - env: - ANVIL_INCLUDE_MODIFIED: ${{ inputs.include_modified }} - ANVIL_INCLUDE_AFFECTED: ${{ inputs.include_affected }} - ANVIL_INCLUDE_REQUIRED: ${{ inputs.include_required }} - # Some checks (e.g. cargo-aprz, run only by groups that include it) - # hit the GitHub API; pass the built-in token so they use the - # authenticated quota (1000 vs 60 req/hr). Harmless for groups whose - # checks never read it. - GITHUB_TOKEN: ${{ github.token }} - run: just anvil-scheduled-test diff --git a/.github/actions/anvil-setup/action.yml b/.github/actions/anvil-setup/action.yml index b317d36a2..0c5c0077a 100644 --- a/.github/actions/anvil-setup/action.yml +++ b/.github/actions/anvil-setup/action.yml @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -# Managed by cargo-anvil. Update the corresponding template in the cargo-anvil -# crate unless the change is repository-specific. +# GENERATED BY cargo-anvil. DO NOT EDIT DIRECTLY. +# Update cargo-anvil and regenerate; repository-specific edits stop automatic updates. # Update behaviour: https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/updates.md name: anvil-setup description: Install `just` and the anvil tools/components needed by a specific group (or everything if omitted). @@ -13,10 +13,11 @@ inputs: - "" (default): install the full catalog via `just anvil-setup` -- use for local "give me everything" flows. - - "none": skip tool installation entirely; just bootstrap the - rust toolchain + just + binstall + cache. Used by + - "none": skip tool installation entirely; just restore the + Cargo cache and bootstrap just + binstall. Used by anvil-impact, which installs only cargo-delta afterwards. - - anything else: install only that group's prerequisites via + - a name containing only lowercase letters, digits, and hyphens: + install only that group's prerequisites via `just anvil--setup`. default: "" required: false @@ -30,6 +31,13 @@ inputs: runs: using: composite steps: + # Just reports the exact failing dependency recipe on stderr. GitHub + # otherwise reduces that to "Process completed with exit code 1", so + # promote Just's diagnostic to a workflow annotation. + - name: Register Just problem matcher + shell: bash + run: echo "::add-matcher::$GITHUB_ACTION_PATH/just-problem-matcher.json" + # Standard GitHub-hosted runners guarantee only 14 GB of SSD space. Heavy # build and test jobs can exceed that with target artifacts alone, so this # opt-in cleanup removes toolchains unused by anvil's Rust checks. @@ -50,7 +58,7 @@ runs: - name: Free disk space (Windows) if: inputs.free-disk-space == 'true' && runner.environment == 'github-hosted' && runner.os == 'Windows' - shell: pwsh + shell: pwsh -NoProfile -Command ". '{0}'" run: | function Get-FreeDiskGiB { [math]::Round((Get-CimInstance Win32_LogicalDisk -Filter "DeviceID='C:'").FreeSpace / 1GB, 1) @@ -69,90 +77,39 @@ runs: } "Free after cleanup (GiB): $(Get-FreeDiskGiB)" - - id: rustc-version - shell: bash - run: echo "version=$(rustc --version | awk '{print $2}')" >> "$GITHUB_OUTPUT" + # Restore before bootstrapping Just so a warm job can reuse the cached + # executable. Repository toolchain files intentionally define a fresh + # cache generation, bounding registry growth without invalidating on + # routine Cargo.toml or Cargo.lock edits. - name: Restore cargo cache id: cargo-cache uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: - # Key on OS + arch + rustc version + lockfile hashes + catalog - # hash so a Rust toolchain bump, a cross-arch matrix leg, or a - # tool-versions update all invalidate the cache cleanly. - # runner.arch resolves to X64 / ARM64 / X86, which keeps the - # x86_64 and aarch64 legs of the same OS from colliding on - # arch-incompatible target/ contents. - # - # ${{ github.job }} discriminates by workflow-job-id (`pr-fast`, - # `pr-test`, `pr-slow`, etc.) so concurrent matrix legs that - # share OS+arch (e.g. pr-fast linux + pr-test linux) don't race - # on the same cache key. Without this, the first-to-finish job - # reserves the key, the others get "Unable to reserve cache: - # another job may be creating this cache" and silently skip - # the save -- leaving the cache empty forever. The restore-keys - # fall back across jobs so the install work is still shared - # across sibling legs on subsequent runs. - key: anvil-v1-${{ runner.os }}-${{ runner.arch }}-rust${{ steps.rustc-version.outputs.version }}-${{ hashFiles('Cargo.lock', '.cargo/config.toml', 'rust-toolchain.toml', 'justfiles/anvil/versions.just') }}-${{ github.job }} + # The job suffix prevents concurrent matrix jobs from racing to save + # one key. The prefix shares a completed cache across sibling jobs. + # There is deliberately no fallback across toolchain/catalog changes: + # those inputs are the cache-pruning boundary. + key: anvil-v2-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('.cargo/config.toml', 'rust-toolchain', 'rust-toolchain.toml', 'justfiles/anvil/versions.just') }}-${{ github.job }} restore-keys: | - anvil-v1-${{ runner.os }}-${{ runner.arch }}-rust${{ steps.rustc-version.outputs.version }}-${{ hashFiles('Cargo.lock', '.cargo/config.toml', 'rust-toolchain.toml', 'justfiles/anvil/versions.just') }}- - anvil-v1-${{ runner.os }}-${{ runner.arch }}-rust${{ steps.rustc-version.outputs.version }}- - anvil-v1-${{ runner.os }}-${{ runner.arch }}- + anvil-v2-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('.cargo/config.toml', 'rust-toolchain', 'rust-toolchain.toml', 'justfiles/anvil/versions.just') }}- # `.crates.toml` and `.crates2.json` track which cargo-installed # tools and versions live in ~/.cargo/bin/. Without them in the # cache, `cargo install --list` and (downstream) the # tool-install recipes' early-skip on "installed >= pin" don't - # see the cached binaries — every run then tries to reinstall - # on top of them and fails with "binary X already exists in - # destination". + # see the cached binaries. + # + # target/ is deliberately excluded: per-job target trees dwarf the + # tool cache and could overwrite the downloaded impact cache. path: | ~/.cargo/registry/cache/ ~/.cargo/registry/index/ ~/.cargo/bin/ ~/.cargo/.crates.toml ~/.cargo/.crates2.json - target/ - - # System dependencies for the catalog's source builds. - # - # cargo-spellcheck's build script (clang-sys) needs libclang. The - # Ubuntu runner images don't ship it on PATH by default, so the - # source compile fails with "couldn't execute llvm-config". On - # macOS, libclang is bundled with Xcode CLT (always present on - # GH-hosted macos-*). On Windows, the Visual Studio install on - # the windows-* images includes a usable LLVM, so no install is - # needed. - - name: Install libclang (Linux) - if: runner.os == 'Linux' - shell: bash - run: sudo apt-get update && sudo apt-get install -y libclang-dev - - # rustup auto-installs the toolchain from rust-toolchain.toml on - # first cargo invocation, but it doesn't pull profile/component - # extras. The `anvil-setup` recipe in step "Install anvil - # toolchains + tools" below handles that exhaustively (the - # per-component install recipes in tools.just install - # default-toolchain components and pinned-nightly components for - # miri/careful/etc.) -- we don't add components inline here anymore. - # This step exists only to ensure rustup itself has the default - # toolchain set up so subsequent cargo / just calls work. - - name: Ensure default toolchain is installed - shell: bash - run: rustup show active-toolchain || rustup default stable - # The catalog recipe `anvil-setup` (or `anvil--setup` - # when a group is specified via the `group` input) needs `just` to - # run. We bootstrap just here (chicken-and-egg), then hand off - # everything else to it. The setup recipes are idempotent and - # short-circuit on tools already installed at or above the pinned - # version, so re-running on cache-hit runs is cheap. - # Install a prebuilt cargo-binstall binary in seconds. Without this, - # the first `_install-tool` recipe that needs binstall bootstraps it - # via `cargo install --locked cargo-binstall`, which takes ~4 min of - # source compilation on every cold-cache job. The official action - # downloads the release binary from cargo-bins/cargo-binstall, so - # the bootstrap branch in `tools.just` becomes a no-op on GH. + # cargo-binstall keeps the cold bootstrap path fast. - name: Install cargo-binstall - uses: cargo-bins/cargo-binstall@ead08b90bd7b2e6d81963fb9cf0b7239f66d5db4 # v1.21.0 + uses: cargo-bins/cargo-binstall@v1.21.0 # immutable release, the tag cannot be moved - name: Install just shell: bash @@ -161,8 +118,18 @@ runs: cargo binstall --no-confirm --locked just || cargo install --locked just fi + # The catalog recipe `anvil-setup` (or `anvil--setup` + # when a group is specified via the `group` input) uses the Just executable + # bootstrapped above, then handles everything else. The setup recipes are idempotent and + # short-circuit on tools already installed at or above the pinned + # version, so re-running on cache-hit runs is cheap. - name: Install anvil toolchains + tools shell: bash + # Carry action data through the environment instead of interpolating it + # into shell source. Validation below occurs before path or recipe-name + # composition. + env: + ANVIL_GROUP: ${{ inputs.group }} # When `group` is empty (the default), installs the full catalog # via `just anvil-setup binstall`. When `group` is "none", # skips tool installation entirely (used by anvil-impact, which @@ -176,10 +143,16 @@ runs: # for ADO pipelines, so the ADO backend uses the default `install` # path; GH uses `binstall`. run: | - case "${{ inputs.group }}" in + if [[ -n "$ANVIL_GROUP" && "$ANVIL_GROUP" != "none" && ! "$ANVIL_GROUP" =~ ^[a-z0-9-]+$ ]]; then + echo "::error::Invalid Anvil group; expected lowercase letters, digits, and hyphens." + # Distinguish invalid action input from a setup-recipe failure. + exit 2 + fi + + case "$ANVIL_GROUP" in none) echo "anvil-setup: group=none, skipping tool install" ;; "") just anvil-setup binstall ;; - *) just "anvil-${{ inputs.group }}-setup" binstall ;; + *) just "anvil-$ANVIL_GROUP-setup" binstall ;; esac # Save the cache as the LAST step of setup, regardless of whether @@ -202,10 +175,11 @@ runs: uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: key: ${{ steps.cargo-cache.outputs.cache-primary-key }} + # target/ deliberately excluded from the save path too -- see the + # restore step above for why (cache-quota / impact-cache-clobber). path: | ~/.cargo/registry/cache/ ~/.cargo/registry/index/ ~/.cargo/bin/ ~/.cargo/.crates.toml ~/.cargo/.crates2.json - target/ diff --git a/.github/actions/anvil-setup/just-problem-matcher.json b/.github/actions/anvil-setup/just-problem-matcher.json new file mode 100644 index 000000000..68dfa6884 --- /dev/null +++ b/.github/actions/anvil-setup/just-problem-matcher.json @@ -0,0 +1,15 @@ +{ + "_generated": "GENERATED BY cargo-anvil. DO NOT EDIT DIRECTLY.", + "problemMatcher": [ + { + "owner": "anvil-just", + "severity": "error", + "pattern": [ + { + "regexp": "^(error: recipe `[^`]+` failed( on line \\d+)? with exit code \\d+)$", + "message": 1 + } + ] + } + ] +} diff --git a/.github/workflows/anvil-pr-impl.yml b/.github/workflows/anvil-pr-impl.yml index 542fda0b8..9d696cc4a 100644 --- a/.github/workflows/anvil-pr-impl.yml +++ b/.github/workflows/anvil-pr-impl.yml @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -# Managed by cargo-anvil. Update the corresponding template in the cargo-anvil -# crate unless the change is repository-specific. +# GENERATED BY cargo-anvil. DO NOT EDIT DIRECTLY. +# Update cargo-anvil and regenerate; repository-specific edits stop automatic updates. # Update behaviour: https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/updates.md name: anvil-pr-impl @@ -24,6 +24,16 @@ on: description: Runner label for aarch64 Windows jobs. type: string default: windows-11-arm + base_ref: + description: Event-specific target commit SHA used by base-dependent checks. + type: string + required: true + publish_commit_statuses: + description: >- + Best-effort management of supplemental failure commit statuses for + same-repository pull requests. Requires statuses: write. + type: boolean + default: false secrets: CODECOV_TOKEN: description: | @@ -31,6 +41,9 @@ on: configured at Codecov; required for private repos. required: false +# Event-specific callers select permissions because a called workflow cannot +# elevate beyond its caller. This implementation declares no broader scopes. + # Note on matrices: every multi-OS job below hardcodes its OS axis as # an inline YAML array. Per-leg runner *labels* are inputs (so adopters # can swap in self-hosted runners), but the OS axis itself is part of @@ -51,32 +64,41 @@ jobs: # cfg gates are rare enough that paying for 4 impact jobs isn't # justified; arm legs reuse their OS counterpart's impact set. impact-linux: + name: "Preparation: Impact Analysis (linux)" runs-on: ${{ inputs.linux_runner }} - outputs: - include_modified: ${{ steps.delta.outputs.include_modified }} - include_affected: ${{ steps.delta.outputs.include_affected }} - include_required: ${{ steps.delta.outputs.include_required }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: + # No lfs: true -- the impact job only runs cargo-delta snapshot/impact + # + cargo metadata over paths and dependency metadata; it never reads + # LFS payload contents (the baseline worktree even disables LFS + # smudging). Hydrating LFS assets here would add two large downloads + # to the critical path for no impact-selection benefit. Downstream + # group jobs keep lfs: true because they run arbitrary checks. fetch-depth: 0 - - id: delta - uses: ./.github/actions/anvil-impact + # anvil-impact runs `just anvil-impact` and uploads the resulting + # target/anvil/impact cache as the anvil-impact- artifact. + - uses: ./.github/actions/anvil-impact + env: + BASE_REF: ${{ inputs.base_ref }} impact-windows: + name: "Preparation: Impact Analysis (windows)" runs-on: ${{ inputs.windows_runner }} - outputs: - include_modified: ${{ steps.delta.outputs.include_modified }} - include_affected: ${{ steps.delta.outputs.include_affected }} - include_required: ${{ steps.delta.outputs.include_required }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: + # See impact-linux: impact needs only path/metadata inputs, not LFS + # payloads, so it checks out without lfs: true. fetch-depth: 0 - - id: delta - uses: ./.github/actions/anvil-impact + - uses: ./.github/actions/anvil-impact + env: + BASE_REF: ${{ inputs.base_ref }} + # These job display names form repository-ruleset contexts. Treat cosmetic + # changes as compatibility changes and keep PR and merge-group callers aligned. pr-fast: + name: "Check Group: Fast Checks (${{ matrix.os }})" # Cross-OS / cross-arch because pr-fast contains compile-sensitive # checks (clippy, doc-build, udeps, semver-check, external-types) # whose results can differ across host for crates that use @@ -93,6 +115,7 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: + lfs: true # anvil-semver-check compares the affected crates' current API # against the PR's target branch (origin/) via # `cargo semver-checks --baseline-rev`. That resolves the @@ -102,12 +125,22 @@ jobs: # origin/ absent. Full history makes the baseline # resolvable; the other pr-fast checks only touch the HEAD tree. fetch-depth: 0 - - uses: ./.github/actions/anvil-pr-fast + - name: Download impact artifact + # Pull the impact cache computed on this leg's OS family into + # target/anvil/impact/ so the group's scoped checks read it via their + # `anvil-impact` dependency -- the same code path as a local run. arm + # legs reuse their OS family's artifact. + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - include_modified: ${{ startsWith(matrix.os, 'linux') && needs.impact-linux.outputs.include_modified || needs.impact-windows.outputs.include_modified }} - include_affected: ${{ startsWith(matrix.os, 'linux') && needs.impact-linux.outputs.include_affected || needs.impact-windows.outputs.include_affected }} - include_required: ${{ startsWith(matrix.os, 'linux') && needs.impact-linux.outputs.include_required || needs.impact-windows.outputs.include_required }} + name: anvil-impact-${{ startsWith(matrix.os, 'linux') && 'Linux' || 'Windows' }} + path: target/anvil/impact + - uses: ./.github/actions/anvil-run-group + with: + group: pr-fast + publish_commit_statuses: ${{ inputs.publish_commit_statuses }} + impact_mode: consume env: + BASE_REF: ${{ inputs.base_ref }} PR_TITLE: ${{ github.event.pull_request.title }} # Advisory PR comments. Recipes that surface non-blocking findings # (e.g. cargo-semver-checks) write a markdown body to @@ -121,18 +154,19 @@ jobs: # write tokens). - name: Upsert anvil-semver advisory if: always() && github.event_name == 'pull_request' && matrix.os == 'linux' && github.event.pull_request.head.repo.full_name == github.repository && hashFiles('target/anvil/comments/semver.md') != '' - uses: marocchino/sticky-pull-request-comment@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v3.0.5 + uses: marocchino/sticky-pull-request-comment@v3.0.5 # immutable release, the tag cannot be moved with: header: anvil-semver path: target/anvil/comments/semver.md - name: Clear anvil-semver advisory if: always() && github.event_name == 'pull_request' && matrix.os == 'linux' && github.event.pull_request.head.repo.full_name == github.repository && hashFiles('target/anvil/comments/semver.md') == '' - uses: marocchino/sticky-pull-request-comment@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v3.0.5 + uses: marocchino/sticky-pull-request-comment@v3.0.5 # immutable release, the tag cannot be moved with: header: anvil-semver delete: true pr-test: + name: "Check Group: Tests and Coverage (${{ matrix.os }})" # Tests + coverage: llvm-cov / doc-test / examples. # 4-leg matrix -- compile and runtime behaviour can differ across # OS and arch for cfg-gated code, so we exercise tests on every leg. @@ -149,12 +183,21 @@ jobs: || inputs.windows_arm_runner }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: ./.github/actions/anvil-pr-test with: + lfs: true + - name: Download impact artifact + # See pr-fast: pull the OS-family impact cache so scoped checks read it + # via their `anvil-impact` dependency. + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: anvil-impact-${{ startsWith(matrix.os, 'linux') && 'Linux' || 'Windows' }} + path: target/anvil/impact + - uses: ./.github/actions/anvil-run-group + with: + group: pr-test + publish_commit_statuses: ${{ inputs.publish_commit_statuses }} free-disk-space: true - include_modified: ${{ startsWith(matrix.os, 'linux') && needs.impact-linux.outputs.include_modified || needs.impact-windows.outputs.include_modified }} - include_affected: ${{ startsWith(matrix.os, 'linux') && needs.impact-linux.outputs.include_affected || needs.impact-windows.outputs.include_affected }} - include_required: ${{ startsWith(matrix.os, 'linux') && needs.impact-linux.outputs.include_required || needs.impact-windows.outputs.include_required }} + impact_mode: consume - name: Upload coverage to Codecov # Upload from every leg except windows-11-arm. OS/arch-gated # code only gets exercised on its native target, so a single- @@ -169,16 +212,45 @@ jobs: # lcov-*.info are produced by the anvil-llvm-cov recipe inside # anvil-pr-test (one per feature config); if the affected set was # empty the recipe no-ops and there is no file to upload, so we gate - # on the impact output. Codecov coalesces the two per-config files. - if: matrix.os != 'windows-arm' && ((startsWith(matrix.os, 'linux') && needs.impact-linux.outputs.include_affected != '--skip') || (matrix.os == 'windows' && needs.impact-windows.outputs.include_affected != '--skip')) - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 + # on the files existing (impact scoping lives in the downloaded cache + # now, not a job output). Codecov coalesces the two per-config files. + if: matrix.os != 'windows-arm' && hashFiles('target/coverage/lcov-all-features.info', 'target/coverage/lcov-no-default.info') != '' + uses: codecov/codecov-action@v7.0.0 # immutable release, the tag cannot be moved with: files: target/coverage/lcov-all-features.info,target/coverage/lcov-no-default.info flags: ${{ matrix.os }} token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: false + pr-msrv: + name: "Check Group: MSRV Tests (${{ matrix.os }})" + needs: [impact-linux, impact-windows] + strategy: + fail-fast: false + matrix: + os: [linux, windows, linux-arm, windows-arm] + runs-on: ${{ matrix.os == 'linux' && inputs.linux_runner + || matrix.os == 'windows' && inputs.windows_runner + || matrix.os == 'linux-arm' && inputs.linux_arm_runner + || inputs.windows_arm_runner }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + lfs: true + - name: Download impact artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: anvil-impact-${{ startsWith(matrix.os, 'linux') && 'Linux' || 'Windows' }} + path: target/anvil/impact + - uses: ./.github/actions/anvil-run-group + with: + group: pr-msrv + publish_commit_statuses: ${{ inputs.publish_commit_statuses }} + free-disk-space: true + impact_mode: consume + pr-runtime-analysis: + name: "Check Group: Runtime Analysis (${{ matrix.os }})" # Stricter-runtime correctness: miri + careful. # 4-leg matrix -- both checks compile per host target and can # surface OS/arch-specific UB. Both are impact-scoped so the @@ -194,13 +266,23 @@ jobs: || inputs.windows_arm_runner }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: ./.github/actions/anvil-pr-runtime-analysis with: - include_modified: ${{ startsWith(matrix.os, 'linux') && needs.impact-linux.outputs.include_modified || needs.impact-windows.outputs.include_modified }} - include_affected: ${{ startsWith(matrix.os, 'linux') && needs.impact-linux.outputs.include_affected || needs.impact-windows.outputs.include_affected }} - include_required: ${{ startsWith(matrix.os, 'linux') && needs.impact-linux.outputs.include_required || needs.impact-windows.outputs.include_required }} + lfs: true + - name: Download impact artifact + # See pr-fast: pull the OS-family impact cache so scoped checks read it + # via their `anvil-impact` dependency. + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: anvil-impact-${{ startsWith(matrix.os, 'linux') && 'Linux' || 'Windows' }} + path: target/anvil/impact + - uses: ./.github/actions/anvil-run-group + with: + group: pr-runtime-analysis + publish_commit_statuses: ${{ inputs.publish_commit_statuses }} + impact_mode: consume pr-mutants: + name: "Check Group: Mutation Testing (${{ matrix.os }})" # Mutation testing: cargo mutants (diff-scoped against the PR base). # 4-leg matrix. cargo-mutants doesn't build on aarch64-pc-windows-msvc # (upstream winapi crate incompat); the anvil-mutants-diff recipe @@ -218,11 +300,19 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: + lfs: true fetch-depth: 0 - - uses: ./.github/actions/anvil-pr-mutants + - name: Download impact artifact + # See pr-fast: pull the OS-family impact cache so scoped checks read it + # via their `anvil-impact` dependency. + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: anvil-impact-${{ startsWith(matrix.os, 'linux') && 'Linux' || 'Windows' }} + path: target/anvil/impact + - uses: ./.github/actions/anvil-run-group with: - include_modified: ${{ startsWith(matrix.os, 'linux') && needs.impact-linux.outputs.include_modified || needs.impact-windows.outputs.include_modified }} - include_affected: ${{ startsWith(matrix.os, 'linux') && needs.impact-linux.outputs.include_affected || needs.impact-windows.outputs.include_affected }} - include_required: ${{ startsWith(matrix.os, 'linux') && needs.impact-linux.outputs.include_required || needs.impact-windows.outputs.include_required }} + group: pr-mutants + publish_commit_statuses: ${{ inputs.publish_commit_statuses }} + impact_mode: consume env: - BASE_REF: ${{ github.event.pull_request.base.sha }} + BASE_REF: ${{ inputs.base_ref }} diff --git a/.github/workflows/anvil-pr.yml b/.github/workflows/anvil-pr.yml index 0e414c3fb..0055c3a77 100644 --- a/.github/workflows/anvil-pr.yml +++ b/.github/workflows/anvil-pr.yml @@ -1,9 +1,11 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -# Managed by cargo-anvil. Update the corresponding template in the cargo-anvil -# crate unless the change is repository-specific. +# GENERATED BY cargo-anvil. DO NOT EDIT DIRECTLY. +# Update cargo-anvil and regenerate; repository-specific edits stop automatic updates. # Update behaviour: https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/updates.md -name: anvil-pr +name: Anvil +# Workflow and caller display names contribute to GitHub's check hierarchy. +# Treat changes as compatibility changes for repository rulesets. on: pull_request: {} @@ -17,12 +19,35 @@ concurrency: cancel-in-progress: true jobs: - anvil-pr: + validation: + name: PR Job + if: github.event_name == 'pull_request' uses: ./.github/workflows/anvil-pr-impl.yml + # A called workflow cannot elevate beyond its caller. Grant only the + # capabilities required by same-repository pull-request presentation. permissions: contents: read + # Publish supplemental failure commit statuses so the PR checks rollup identifies the + # concrete failed Just recipe before the aggregate workflow job. + statuses: write # Write needed so the pr-fast job can upsert/clear the sticky PR # comment carrying the cargo-semver-checks advisory (and any # future advisory checks that emit target/anvil/comments/*). pull-requests: write + with: + base_ref: ${{ github.event.pull_request.base.sha }} + publish_commit_statuses: true + secrets: inherit + + merge-validation: + # Keep the called-job check contexts identical to pull-request runs so one + # branch-protection configuration works for PRs and the merge queue. + name: PR Job + if: github.event_name == 'merge_group' + uses: ./.github/workflows/anvil-pr-impl.yml + # Synthetic merge-group code needs validation but no presentation writes. + permissions: + contents: read + with: + base_ref: ${{ github.event.merge_group.base_sha }} secrets: inherit diff --git a/.github/workflows/anvil-scheduled-impl.yml b/.github/workflows/anvil-scheduled-impl.yml index 01858bc48..982dc1ae6 100644 --- a/.github/workflows/anvil-scheduled-impl.yml +++ b/.github/workflows/anvil-scheduled-impl.yml @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -# Managed by cargo-anvil. Update the corresponding template in the cargo-anvil -# crate unless the change is repository-specific. +# GENERATED BY cargo-anvil. DO NOT EDIT DIRECTLY. +# Update cargo-anvil and regenerate; repository-specific edits stop automatic updates. # Update behaviour: https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/updates.md name: anvil-scheduled-impl @@ -31,9 +31,20 @@ on: configured at Codecov; required for private repos. required: false +# A called workflow cannot elevate beyond its caller. Reset ordinary jobs here, +# then restore only the publisher's issue scope below. +permissions: + contents: read + # Note on matrices: see pr-impl-workflow.yml for the rationale. OS # matrices are hardcoded; per-leg runner labels are inputs. +# Disable cargo incremental compilation for every job/step (see +# pr-impl-workflow.yml for the rationale): the incremental dir is not +# cached, so on CI it is pure cost. +env: + CARGO_INCREMENTAL: "0" + jobs: scheduled-test: strategy: @@ -46,8 +57,11 @@ jobs: || inputs.windows_arm_runner }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: ./.github/actions/anvil-scheduled-test with: + lfs: true + - uses: ./.github/actions/anvil-run-group + with: + group: scheduled-test free-disk-space: true - name: Upload coverage to Codecov # Upload from every leg except windows-11-arm (see the matching @@ -56,7 +70,7 @@ jobs: # the Codecov UI can distinguish PR-tier uploads from scheduled # uploads while still tracking each platform separately. if: matrix.os != 'windows-arm' - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 + uses: codecov/codecov-action@v7.0.0 # immutable release, the tag cannot be moved with: files: target/coverage/lcov-all-features.info,target/coverage/lcov-no-default.info flags: scheduled,${{ matrix.os }} @@ -77,7 +91,11 @@ jobs: || inputs.windows_arm_runner }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: ./.github/actions/anvil-scheduled-advisories + with: + lfs: true + - uses: ./.github/actions/anvil-run-group + with: + group: scheduled-advisories scheduled-runtime-analysis: # Full-workspace counterpart of pr-runtime-analysis. OS matrix @@ -88,8 +106,8 @@ jobs: # miri profiles (base miri + the three stricter ones) sequentially # within the leg. Parallelism is across OS legs, not across profiles: # running the profiles sequentially lets them share toolchain setup - # and the target/ cache, which is the right trade given each profile - # already costs hours. + # and the compiled target/ artifacts within the leg, which is the + # right trade given each profile already costs hours. strategy: fail-fast: false matrix: @@ -100,7 +118,11 @@ jobs: || inputs.windows_arm_runner }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: ./.github/actions/anvil-scheduled-runtime-analysis + with: + lfs: true + - uses: ./.github/actions/anvil-run-group + with: + group: scheduled-runtime-analysis scheduled-exhaustive: # x86_64-only by design: this group includes mutants-full (which @@ -113,4 +135,76 @@ jobs: runs-on: ${{ matrix.os == 'linux' && inputs.linux_runner || inputs.windows_runner }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: ./.github/actions/anvil-scheduled-exhaustive + with: + lfs: true + - uses: ./.github/actions/anvil-run-group + with: + group: scheduled-exhaustive + + publish-failure: + name: Publish scheduled failure + needs: + - scheduled-test + - scheduled-advisories + - scheduled-runtime-analysis + - scheduled-exhaustive + if: ${{ always() && vars.ANVIL_PUBLISH_FAILURE_ISSUE != 'false' + && contains(needs.*.result, 'failure') }} + runs-on: ${{ inputs.linux_runner }} + permissions: + issues: write + steps: + - name: Create or update failure issue + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + ANVIL_JOB_RESULTS: ${{ toJSON(needs) }} + with: + script: | + const title = "[Anvil] Scheduled checks failed"; + const marker = ""; + const runUrl = + `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}` + + `/actions/runs/${context.runId}`; + const results = JSON.parse(process.env.ANVIL_JOB_RESULTS); + const failedJobs = Object.entries(results) + .filter(([, job]) => job.result === "failure") + .map(([job]) => `- \`${job}\``) + .join("\n"); + const body = [ + marker, + "", + "The Anvil scheduled workflow failed.", + "", + "Failed jobs:", + failedJobs, + "", + `[View workflow run](${runUrl})`, + ].join("\n"); + + const query = + `repo:${context.repo.owner}/${context.repo.repo} ` + + `is:issue is:open in:body "anvil scheduled failure"`; + const { data: search } = + await github.rest.search.issuesAndPullRequests({ + q: query, + per_page: 100, + }); + const existing = search.items.find( + issue => issue.body?.includes(marker), + ); + + if (existing) { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: existing.number, + body, + }); + } else { + await github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title, + body, + }); + } diff --git a/.github/workflows/anvil-scheduled.yml b/.github/workflows/anvil-scheduled.yml index ecee2c3ea..06a001521 100644 --- a/.github/workflows/anvil-scheduled.yml +++ b/.github/workflows/anvil-scheduled.yml @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -# Managed by cargo-anvil. Update the corresponding template in the cargo-anvil -# crate unless the change is repository-specific. +# GENERATED BY cargo-anvil. DO NOT EDIT DIRECTLY. +# Update cargo-anvil and regenerate; repository-specific edits stop automatic updates. # Update behaviour: https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/updates.md name: anvil-scheduled @@ -16,6 +16,10 @@ permissions: jobs: anvil-scheduled: uses: ./.github/workflows/anvil-scheduled-impl.yml + # A called workflow cannot elevate beyond its caller, so the root grants + # issues:write while the implementation resets ordinary jobs to read-only + # and restores that scope only on publish-failure. permissions: contents: read + issues: write secrets: inherit diff --git a/constants.env b/constants.env index abc4e5164..e786d3975 100644 --- a/constants.env +++ b/constants.env @@ -20,7 +20,7 @@ CARGO_CAREFUL_VERSION=0.4.10 CARGO_CHECK_EXTERNAL_TYPES_VERSION=0.5.0 CARGO_DELTA_VERSION=0.3.1 CARGO_DENY_VERSION=0.19.8 -CARGO_DOC2README_VERSION=0.7.2 +CARGO_DOC2README_VERSION=0.7.3 CARGO_ENSURE_NO_CYCLIC_DEPS_VERSION=0.2.0 CARGO_ENSURE_NO_DEFAULT_FEATURES_VERSION=1.1.0 CARGO_HACK_VERSION=0.6.45 @@ -29,7 +29,7 @@ CARGO_LLVM_COV_VERSION=0.8.7 CARGO_MACHETE_VERSION=0.9.2 CARGO_MUTANTS_VERSION=27.0.0 CARGO_NEXTEST_VERSION=0.9.137 -CARGO_SEMVER_CHECKS_VERSION=0.48.0 +CARGO_SEMVER_CHECKS_VERSION=0.50.0 CARGO_SORT_VERSION=2.1.4 CARGO_SPELLCHECK_VERSION=0.15.1 CARGO_UDEPS_VERSION=0.1.61 diff --git a/crates/allocation_hints/Cargo.toml b/crates/allocation_hints/Cargo.toml index 3a1d26aac..9ba1c2d9a 100644 --- a/crates/allocation_hints/Cargo.toml +++ b/crates/allocation_hints/Cargo.toml @@ -22,5 +22,7 @@ links = "allocation_hints_backend" mutants.workspace = true static_assertions.workspace = true +# >>> anvil-managed: anvil-lints [lints] workspace = true +# <<< anvil-managed: anvil-lints diff --git a/crates/fetch_winhttp/Cargo.toml b/crates/fetch_winhttp/Cargo.toml index 15aa21a1c..23e361b36 100644 --- a/crates/fetch_winhttp/Cargo.toml +++ b/crates/fetch_winhttp/Cargo.toml @@ -92,5 +92,7 @@ harness = false name = "fw_client" harness = false +# >>> anvil-managed: anvil-lints [lints] workspace = true +# <<< anvil-managed: anvil-lints diff --git a/crates/fetch_winhttp_impl/Cargo.toml b/crates/fetch_winhttp_impl/Cargo.toml index b5d33112e..405ee3db8 100644 --- a/crates/fetch_winhttp_impl/Cargo.toml +++ b/crates/fetch_winhttp_impl/Cargo.toml @@ -113,5 +113,7 @@ static_assertions = { workspace = true } testing_aids = { path = "../testing_aids" } tick = { path = "../tick", features = ["test-util"] } +# >>> anvil-managed: anvil-lints [lints] workspace = true +# <<< anvil-managed: anvil-lints diff --git a/crates/http_extensions/README.md b/crates/http_extensions/README.md index a64e05eb1..c7e403685 100644 --- a/crates/http_extensions/README.md +++ b/crates/http_extensions/README.md @@ -170,7 +170,7 @@ for future requests. This makes the crate particularly efficient for high-throug This crate was developed as part of The Oxidizer Project. Browse this crate's source code. - [__cargo_doc2readme_dependencies_info]: ggGmYW0CYXZlMC43LjJhdIQb11VxC_uAPOQbtUn4Wx2-BfAbid3Nt1Y27Pobprn8Z6FjFy9hYvRhcoQb8ws5BCyJXrMbtKOqwMaB35YbTeUgtnVoxOMbl-5qQusDAothZIWCZWJ5dGVzZjEuMTIuMIJoYnl0ZXNidWZlMC45LjCCZGh0dHBlMS40LjKCaWh0dHBfYm9keWUxLjAuMYJvaHR0cF9leHRlbnNpb25zZjAuMTAuMA + [__cargo_doc2readme_dependencies_info]: ggGmYW0CYXZlMC43LjNhdIQb11VxC_uAPOQbtUn4Wx2-BfAbid3Nt1Y27Pobprn8Z6FjFy9hYvRhcoQb8ws5BCyJXrMbtKOqwMaB35YbTeUgtnVoxOMbl-5qQusDAothZIWCZWJ5dGVzZjEuMTIuMIJoYnl0ZXNidWZlMC45LjCCZGh0dHBlMS40LjKDaWh0dHAtYm9keWUxLjAuMWlodHRwX2JvZHmCb2h0dHBfZXh0ZW5zaW9uc2YwLjEwLjA [__link0]: https://crates.io/crates/http/1.4.2 [__link1]: https://docs.rs/http_extensions/0.10.0/http_extensions/type.HttpRequest.html [__link10]: https://docs.rs/http_extensions/0.10.0/http_extensions/?search=StatusExt @@ -193,7 +193,7 @@ This crate was developed as part of >> anvil-managed: anvil-lints [lints] workspace = true +# <<< anvil-managed: anvil-lints diff --git a/crates/internity_macros/Cargo.toml b/crates/internity_macros/Cargo.toml index 01f0226f2..20f7bc842 100644 --- a/crates/internity_macros/Cargo.toml +++ b/crates/internity_macros/Cargo.toml @@ -41,5 +41,7 @@ syn = { workspace = true, features = ["full", "derive", "printing", "parsing", " [dev-dependencies] mutants = { workspace = true } +# >>> anvil-managed: anvil-lints [lints] workspace = true +# <<< anvil-managed: anvil-lints diff --git a/crates/observed/Cargo.toml b/crates/observed/Cargo.toml index 80f834b02..a7951362c 100644 --- a/crates/observed/Cargo.toml +++ b/crates/observed/Cargo.toml @@ -59,10 +59,12 @@ static_assertions = { workspace = true } tick = { workspace = true, features = ["test-util", "tokio"] } tokio = { workspace = true, features = ["rt-multi-thread", "macros", "sync"] } -[lints] -workspace = true - [[bench]] name = "observed_benchmarks" harness = false required-features = ["test-util"] + +# >>> anvil-managed: anvil-lints +[lints] +workspace = true +# <<< anvil-managed: anvil-lints diff --git a/crates/observed_macros/Cargo.toml b/crates/observed_macros/Cargo.toml index 20e667a90..6c66cb8e0 100644 --- a/crates/observed_macros/Cargo.toml +++ b/crates/observed_macros/Cargo.toml @@ -40,5 +40,7 @@ observed_macros_impl = { workspace = true } [dev-dependencies] mutants = { workspace = true } +# >>> anvil-managed: anvil-lints [lints] workspace = true +# <<< anvil-managed: anvil-lints diff --git a/crates/observed_macros_impl/Cargo.toml b/crates/observed_macros_impl/Cargo.toml index b8ac2cd03..b2e56486b 100644 --- a/crates/observed_macros_impl/Cargo.toml +++ b/crates/observed_macros_impl/Cargo.toml @@ -43,5 +43,7 @@ syn = { workspace = true, features = [ insta = { workspace = true } testing_aids = { workspace = true } +# >>> anvil-managed: anvil-lints [lints] workspace = true +# <<< anvil-managed: anvil-lints diff --git a/crates/observed_testing/Cargo.toml b/crates/observed_testing/Cargo.toml index 1f607c35c..f0c26511f 100644 --- a/crates/observed_testing/Cargo.toml +++ b/crates/observed_testing/Cargo.toml @@ -36,5 +36,7 @@ tick = { workspace = true, features = ["test-util"] } testing_aids = { workspace = true } tokio = { workspace = true, features = ["rt-multi-thread", "macros", "sync"] } +# >>> anvil-managed: anvil-lints [lints] workspace = true +# <<< anvil-managed: anvil-lints diff --git a/crates/observed_utils/Cargo.toml b/crates/observed_utils/Cargo.toml index dcf8bd932..e11216f4f 100644 --- a/crates/observed_utils/Cargo.toml +++ b/crates/observed_utils/Cargo.toml @@ -44,5 +44,7 @@ opentelemetry = { workspace = true, features = ["logs"] } [dev-dependencies] tick = { workspace = true } +# >>> anvil-managed: anvil-lints [lints] workspace = true +# <<< anvil-managed: anvil-lints diff --git a/crates/rallocator/Cargo.toml b/crates/rallocator/Cargo.toml index 8a6b4010b..fa6f7b5c8 100644 --- a/crates/rallocator/Cargo.toml +++ b/crates/rallocator/Cargo.toml @@ -43,9 +43,6 @@ hashbrown = { workspace = true, features = ["default-hasher"] } mimalloc.workspace = true multitude = { workspace = true, features = ["hashbrown"] } -[lints] -workspace = true - [[bench]] name = "mimalloc" harness = false @@ -101,3 +98,8 @@ harness = false [[bench]] name = "tracking_all_callers" harness = false + +# >>> anvil-managed: anvil-lints +[lints] +workspace = true +# <<< anvil-managed: anvil-lints diff --git a/crates/rallocator_cli/Cargo.toml b/crates/rallocator_cli/Cargo.toml index 42abae486..fd02d39e0 100644 --- a/crates/rallocator_cli/Cargo.toml +++ b/crates/rallocator_cli/Cargo.toml @@ -26,5 +26,7 @@ clap = { workspace = true, features = ["color", "derive", "error-context", "help rallocator_telemetry.workspace = true same-file.workspace = true +# >>> anvil-managed: anvil-lints [lints] workspace = true +# <<< anvil-managed: anvil-lints diff --git a/crates/rallocator_telemetry/Cargo.toml b/crates/rallocator_telemetry/Cargo.toml index 4a56b9c19..290d2940a 100644 --- a/crates/rallocator_telemetry/Cargo.toml +++ b/crates/rallocator_telemetry/Cargo.toml @@ -23,5 +23,7 @@ allowed_external_types = ["rallocator_wire::*"] [dependencies] rallocator_wire.workspace = true +# >>> anvil-managed: anvil-lints [lints] workspace = true +# <<< anvil-managed: anvil-lints diff --git a/crates/rallocator_wire/Cargo.toml b/crates/rallocator_wire/Cargo.toml index 6fad07b3c..b158fa81f 100644 --- a/crates/rallocator_wire/Cargo.toml +++ b/crates/rallocator_wire/Cargo.toml @@ -20,5 +20,7 @@ repository = "https://github.com/microsoft/oxidizer/tree/main/crates/rallocator_ [lib] path = "src/lib.rs" +# >>> anvil-managed: anvil-lints [lints] workspace = true +# <<< anvil-managed: anvil-lints diff --git a/crates/rest_over_grpc/Cargo.toml b/crates/rest_over_grpc/Cargo.toml index c34389732..383c2f671 100644 --- a/crates/rest_over_grpc/Cargo.toml +++ b/crates/rest_over_grpc/Cargo.toml @@ -144,5 +144,7 @@ required-features = ["serving"] name = "generate_service" required-features = ["build"] +# >>> anvil-managed: anvil-lints [lints] workspace = true +# <<< anvil-managed: anvil-lints diff --git a/crates/rest_over_grpc/README.md b/crates/rest_over_grpc/README.md index df56d8725..341fb554c 100644 --- a/crates/rest_over_grpc/README.md +++ b/crates/rest_over_grpc/README.md @@ -218,7 +218,7 @@ as an Axum fallback service. This crate was developed as part of The Oxidizer Project. Browse this crate's source code. - [__cargo_doc2readme_dependencies_info]: ggGmYW0CYXZlMC43LjJhdIQb11VxC_uAPOQbtUn4Wx2-BfAbid3Nt1Y27Pobprn8Z6FjFy9hYvRhcoQb4yyDbhLmywUbUgoeDyjY0hYb_gBd7xtnrJEbm_ruDQCrgu9hZIOCZ2xheWVyZWRlMC4zLjaCbnJlc3Rfb3Zlcl9ncnBjZTAuMi4wgm10b3dlcl9zZXJ2aWNlZTAuMy4z + [__cargo_doc2readme_dependencies_info]: ggGmYW0CYXZlMC43LjNhdIQb11VxC_uAPOQbtUn4Wx2-BfAbid3Nt1Y27Pobprn8Z6FjFy9hYvRhcoQb4yyDbhLmywUbUgoeDyjY0hYb_gBd7xtnrJEbm_ruDQCrgu9hZIOCZ2xheWVyZWRlMC4zLjaCbnJlc3Rfb3Zlcl9ncnBjZTAuMi4wg210b3dlci1zZXJ2aWNlZTAuMy4zbXRvd2VyX3NlcnZpY2U [__link0]: https://docs.rs/rest_over_grpc/0.2.0/rest_over_grpc/?search=handling::Status [__link1]: https://docs.rs/rest_over_grpc/0.2.0/rest_over_grpc/?search=serving::RestService::new [__link10]: https://docs.rs/rest_over_grpc/0.2.0/rest_over_grpc/?search=transcoding::Transcode::try_transcode @@ -231,7 +231,7 @@ This crate was developed as part of >> anvil-managed: anvil-lints +[lints] +workspace = true +# <<< anvil-managed: anvil-lints diff --git a/crates/rest_over_grpc_tests/Cargo.toml b/crates/rest_over_grpc_tests/Cargo.toml index dda3868fa..d9b4e52d5 100644 --- a/crates/rest_over_grpc_tests/Cargo.toml +++ b/crates/rest_over_grpc_tests/Cargo.toml @@ -72,9 +72,6 @@ tower-service = { workspace = true } gungraun = { workspace = true, features = ["default"] } matchit = { workspace = true } -[lints] -workspace = true - [[bench]] name = "rog_router_cg" harness = false @@ -82,3 +79,8 @@ harness = false [[bench]] name = "rog_transcode_cg" harness = false + +# >>> anvil-managed: anvil-lints +[lints] +workspace = true +# <<< anvil-managed: anvil-lints diff --git a/crates/routerama/Cargo.toml b/crates/routerama/Cargo.toml index c63167dd7..1ece8d950 100644 --- a/crates/routerama/Cargo.toml +++ b/crates/routerama/Cargo.toml @@ -62,9 +62,6 @@ tokio = { workspace = true, features = ["rt-multi-thread", "macros", "net"] } [target.'cfg(target_os = "linux")'.dev-dependencies] gungraun = { workspace = true, features = ["default"] } -[lints] -workspace = true - [[bench]] name = "criterion_routers" harness = false @@ -122,3 +119,8 @@ required-features = ["query"] [[test]] name = "query_allocations" required-features = ["query"] + +# >>> anvil-managed: anvil-lints +[lints] +workspace = true +# <<< anvil-managed: anvil-lints diff --git a/crates/routerama_build/Cargo.toml b/crates/routerama_build/Cargo.toml index ac6b7ad2c..ab4d475a7 100644 --- a/crates/routerama_build/Cargo.toml +++ b/crates/routerama_build/Cargo.toml @@ -48,10 +48,12 @@ proc-macro2 = { workspace = true } quote = { workspace = true } syn = { workspace = true, features = ["clone-impls", "parsing", "proc-macro", "printing", "derive", "full", "visit"] } -[lints] -workspace = true - [[bench]] name = "generator_scaling" harness = false required-features = ["codegen"] + +# >>> anvil-managed: anvil-lints +[lints] +workspace = true +# <<< anvil-managed: anvil-lints diff --git a/crates/routerama_macros/Cargo.toml b/crates/routerama_macros/Cargo.toml index 6bfe075c4..d79763c28 100644 --- a/crates/routerama_macros/Cargo.toml +++ b/crates/routerama_macros/Cargo.toml @@ -26,5 +26,7 @@ routerama_build = { workspace = true, default-features = false, features = ["cod [dev-dependencies] mutants = { workspace = true } +# >>> anvil-managed: anvil-lints [lints] workspace = true +# <<< anvil-managed: anvil-lints diff --git a/crates/seatbelt/README.md b/crates/seatbelt/README.md index cb9c71a76..a04c6f28b 100644 --- a/crates/seatbelt/README.md +++ b/crates/seatbelt/README.md @@ -199,7 +199,7 @@ This crate provides several optional features that can be enabled in your `Cargo This crate was developed as part of The Oxidizer Project. Browse this crate's source code. - [__cargo_doc2readme_dependencies_info]: ggGmYW0CYXZlMC43LjJhdIQb11VxC_uAPOQbtUn4Wx2-BfAbid3Nt1Y27Pobprn8Z6FjFy9hYvRhcoQbYvFWrvv8310bVH74l03n0WQbkisnDrQ6OyIbDoEzq0lVXmRhZIWCZ2xheWVyZWRlMC4zLjaCa3JlY292ZXJhYmxlZTAuMi4wgmhzZWF0YmVsdGUwLjguMIJkdGlja2UwLjYuMIJtdG93ZXJfc2VydmljZWUwLjMuMw + [__cargo_doc2readme_dependencies_info]: ggGmYW0CYXZlMC43LjNhdIQb11VxC_uAPOQbtUn4Wx2-BfAbid3Nt1Y27Pobprn8Z6FjFy9hYvRhcoQbYvFWrvv8310bVH74l03n0WQbkisnDrQ6OyIbDoEzq0lVXmRhZIWCZ2xheWVyZWRlMC4zLjaCa3JlY292ZXJhYmxlZTAuMi4wgmhzZWF0YmVsdGUwLjguMIJkdGlja2UwLjYuMINtdG93ZXItc2VydmljZWUwLjMuM210b3dlcl9zZXJ2aWNl [__link0]: https://crates.io/crates/layered/0.3.6 [__link1]: https://docs.rs/layered/0.3.6/layered/?search=Stack [__link10]: https://docs.rs/seatbelt/0.8.0/seatbelt/hedging/index.html @@ -232,7 +232,7 @@ This crate was developed as part of )") plus the careful version. - $idLabel = "$(($rustcVV -split "`n")[0].Trim()) + cargo-careful {{ cargo_careful_version }}" + # (e.g. "rustc 1.98.0-nightly ( )") plus the executable hash. + $idLabel = "$(($rustcVV -split "`n")[0].Trim()) + cargo-careful sha256:$($carefulExecutableHash.Substring(0, 12))" $marker = Join-Path 'target' 'anvil/careful-sysroot.id' $prev = if (Test-Path -LiteralPath $marker) { (Get-Content -LiteralPath $marker -Raw).Trim() } else { $null } if ($null -ne $prev -and $prev -ne $idHash) { @@ -44,10 +56,10 @@ anvil-careful: anvil-careful-validate-prereqs Write-Host ' workspace is rebuilt against the freshly-built careful std (cargo cannot detect the in-place' Write-Host ' sysroot rebuild on its own, so stale artifacts would otherwise fail with an' Write-Host ' "incompatible version of rustc" / metadata mismatch).' - Write-Host " reason : the pinned nightly toolchain and/or the cargo-careful version changed" + Write-Host " reason : the nightly toolchain and/or resolved cargo-careful executable changed" Write-Host " now : $idLabel" Write-Host " identity: $($prev.Substring(0, [Math]::Min(12, $prev.Length)))... -> $($idHash.Substring(0, 12))..." - cargo clean + & cargo '+{{ rust_nightly }}' clean if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } } # Persist the current identity (also seeds the marker on first run, when diff --git a/justfiles/anvil/checks/cargo-hack.just b/justfiles/anvil/checks/cargo-hack.just index 79ce110f1..0377fb4da 100644 --- a/justfiles/anvil/checks/cargo-hack.just +++ b/justfiles/anvil/checks/cargo-hack.just @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -# Managed by cargo-anvil. Update the corresponding template in the cargo-anvil -# crate unless the change is repository-specific. +# GENERATED BY cargo-anvil. DO NOT EDIT DIRECTLY. +# Update cargo-anvil and regenerate; repository-specific edits stop automatic updates. # Update behaviour: https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/updates.md # See https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/checks.md @@ -11,11 +11,12 @@ # is the right scope. # Check feature combinations for required workspace packages. -[script("pwsh")] -anvil-cargo-hack: anvil-cargo-hack-validate-prereqs +[script("pwsh", "-NoProfile")] +anvil-cargo-hack: anvil-cargo-hack-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - if ($env:ANVIL_INCLUDE_REQUIRED -eq '--skip') { exit 0 } - & cargo hack @(if ($env:ANVIL_INCLUDE_REQUIRED) { -split $env:ANVIL_INCLUDE_REQUIRED } else { '--workspace' }) --feature-powerset --depth 2 check + $include = (& "{{ just_executable() }}" _anvil-impact-include required) + if ($include -eq '--skip') { Write-Host 'anvil-cargo-hack: no affected packages; skipping'; exit 0 } + & cargo {{_anvil_stable_toolchain_args}} hack @(if ($include) { -split $include } else { '--workspace' }) --feature-powerset --depth 2 check if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-cargo-hack` recipe. diff --git a/justfiles/anvil/checks/cargo-sort.just b/justfiles/anvil/checks/cargo-sort.just index 18de4e2b8..9744892f0 100644 --- a/justfiles/anvil/checks/cargo-sort.just +++ b/justfiles/anvil/checks/cargo-sort.just @@ -1,31 +1,27 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -# Managed by cargo-anvil. Update the corresponding template in the cargo-anvil -# crate unless the change is repository-specific. +# GENERATED BY cargo-anvil. DO NOT EDIT DIRECTLY. +# Update cargo-anvil and regenerate; repository-specific edits stop automatic updates. # Update behaviour: https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/updates.md # See https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/checks.md # 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 a local 2.1.2+ install -# would pass on formatting drift that the pinned version still rejects. -# The flag exists in 2.0.2 too (where it is a no-op, since that version -# already errors on formatting), so it is safe across the supported range. +# 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, 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")] -anvil-cargo-sort: anvil-cargo-sort-validate-prereqs +[script("pwsh", "-NoProfile")] +anvil-cargo-sort: anvil-cargo-sort-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - if ($env:ANVIL_INCLUDE_MODIFIED -eq '--skip') { exit 0 } - cargo sort --workspace --grouped --check --check-format + $include = (& "{{ just_executable() }}" _anvil-impact-include modified) + if ($include -eq '--skip') { Write-Host 'anvil-cargo-sort: no modified packages; skipping'; exit 0 } + & cargo {{_anvil_stable_toolchain_args}} sort --workspace --grouped --check --check-format if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-cargo-sort` recipe. diff --git a/justfiles/anvil/checks/clippy.just b/justfiles/anvil/checks/clippy.just index cb8827e8d..a0354f2c4 100644 --- a/justfiles/anvil/checks/clippy.just +++ b/justfiles/anvil/checks/clippy.just @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -# Managed by cargo-anvil. Update the corresponding template in the cargo-anvil -# crate unless the change is repository-specific. +# GENERATED BY cargo-anvil. DO NOT EDIT DIRECTLY. +# Update cargo-anvil and regenerate; repository-specific edits stop automatic updates. # Update behaviour: https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/updates.md # See https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/checks.md @@ -10,15 +10,16 @@ # rather than the modified set: a change in a crate can introduce a # clippy issue in a dependent crate (e.g., trait-bound or # obviously-truthy-condition lints that key off the changed type), so -# we want downstream rev-deps to lint as well. cargo-clippy is a +# we want downstream reverse dependencies to lint as well. cargo-clippy is a # rustup component; same reasoning as fmt for the require. # Lint affected workspace packages with Clippy. -[script("pwsh")] -anvil-clippy: anvil-clippy-validate-prereqs +[script("pwsh", "-NoProfile")] +anvil-clippy: anvil-clippy-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - if ($env:ANVIL_INCLUDE_AFFECTED -eq '--skip') { exit 0 } - & cargo clippy @(if ($env:ANVIL_INCLUDE_AFFECTED) { -split $env:ANVIL_INCLUDE_AFFECTED } else { '--workspace' }) --all-targets --all-features --locked "--" '-D' 'warnings' + $include = (& "{{ just_executable() }}" _anvil-impact-include affected) + if ($include -eq '--skip') { Write-Host 'anvil-clippy: no affected packages; skipping'; exit 0 } + & cargo {{_anvil_stable_toolchain_args}} clippy @(if ($include) { -split $include } else { '--workspace' }) --all-targets --all-features --locked "--" '-D' 'warnings' if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-clippy` recipe. diff --git a/justfiles/anvil/checks/deny.just b/justfiles/anvil/checks/deny.just index 8c2072845..c798669b5 100644 --- a/justfiles/anvil/checks/deny.just +++ b/justfiles/anvil/checks/deny.just @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -# Managed by cargo-anvil. Update the corresponding template in the cargo-anvil -# crate unless the change is repository-specific. +# GENERATED BY cargo-anvil. DO NOT EDIT DIRECTLY. +# Update cargo-anvil and regenerate; repository-specific edits stop automatic updates. # Update behaviour: https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/updates.md # See https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/checks.md @@ -11,10 +11,10 @@ # depend on the adopter's default shell (absent/varying on Windows). # Check dependency licenses, bans, sources, and advisories. -[script("pwsh")] +[script("pwsh", "-NoProfile")] anvil-deny: anvil-deny-validate-prereqs $ErrorActionPreference = 'Stop' - & cargo deny check + & cargo {{_anvil_stable_toolchain_args}} deny check if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-deny` recipe. diff --git a/justfiles/anvil/checks/doc-build.just b/justfiles/anvil/checks/doc-build.just index 2f80e9533..a8edeef7c 100644 --- a/justfiles/anvil/checks/doc-build.just +++ b/justfiles/anvil/checks/doc-build.just @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -# Managed by cargo-anvil. Update the corresponding template in the cargo-anvil -# crate unless the change is repository-specific. +# GENERATED BY cargo-anvil. DO NOT EDIT DIRECTLY. +# Update cargo-anvil and regenerate; repository-specific edits stop automatic updates. # Update behaviour: https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/updates.md # See https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/checks.md @@ -11,12 +11,13 @@ # crate that wasn't itself modified. # Build documentation for required workspace packages. -[script("pwsh")] -anvil-doc-build: anvil-doc-build-validate-prereqs +[script("pwsh", "-NoProfile")] +anvil-doc-build: anvil-doc-build-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - if ($env:ANVIL_INCLUDE_REQUIRED -eq '--skip') { exit 0 } + $include = (& "{{ just_executable() }}" _anvil-impact-include required) + if ($include -eq '--skip') { Write-Host 'anvil-doc-build: no affected packages; skipping'; exit 0 } $env:RUSTDOCFLAGS = '-D warnings' - & cargo doc @(if ($env:ANVIL_INCLUDE_REQUIRED) { -split $env:ANVIL_INCLUDE_REQUIRED } else { '--workspace' }) --all-features --no-deps + & cargo {{_anvil_stable_toolchain_args}} doc @(if ($include) { -split $include } else { '--workspace' }) --all-features --no-deps if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # doc-build, examples and doc-test are pure cargo built-ins; the rust @@ -25,7 +26,7 @@ anvil-doc-build: anvil-doc-build-validate-prereqs # Install prerequisites for the `anvil-doc-build` recipe. [group("anvil-setup")] -anvil-doc-build-setup installer="install": anvil-tool-rustc-validate-prereqs +anvil-doc-build-setup installer="install": anvil-toolchain-stable-install # Validate prerequisites for the `anvil-doc-build` recipe. [group("anvil-setup")] diff --git a/justfiles/anvil/checks/doc-test.just b/justfiles/anvil/checks/doc-test.just index 5191dbd01..ca6ade8c9 100644 --- a/justfiles/anvil/checks/doc-test.just +++ b/justfiles/anvil/checks/doc-test.just @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -# Managed by cargo-anvil. Update the corresponding template in the cargo-anvil -# crate unless the change is repository-specific. +# GENERATED BY cargo-anvil. DO NOT EDIT DIRECTLY. +# Update cargo-anvil and regenerate; repository-specific edits stop automatic updates. # Update behaviour: https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/updates.md # See https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/checks.md @@ -14,19 +14,20 @@ # of total compile/test time. # Run documentation tests for affected workspace packages. -[script("pwsh")] -anvil-doc-test: anvil-doc-test-validate-prereqs +[script("pwsh", "-NoProfile")] +anvil-doc-test: anvil-doc-test-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - if ($env:ANVIL_INCLUDE_AFFECTED -eq '--skip') { exit 0 } - $pkg = @(if ($env:ANVIL_INCLUDE_AFFECTED) { -split $env:ANVIL_INCLUDE_AFFECTED } else { '--workspace' }) - & cargo test --doc @pkg --all-features --locked + $include = (& "{{ just_executable() }}" _anvil-impact-include affected) + if ($include -eq '--skip') { Write-Host 'anvil-doc-test: no affected packages; skipping'; exit 0 } + $pkg = @(if ($include) { -split $include } else { '--workspace' }) + & cargo {{_anvil_stable_toolchain_args}} test --doc @pkg --all-features --locked if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - & cargo test --doc @pkg --locked + & cargo {{_anvil_stable_toolchain_args}} test --doc @pkg --locked if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-doc-test` recipe. [group("anvil-setup")] -anvil-doc-test-setup installer="install": anvil-tool-rustc-validate-prereqs +anvil-doc-test-setup installer="install": anvil-toolchain-stable-install # Validate prerequisites for the `anvil-doc-test` recipe. [group("anvil-setup")] diff --git a/justfiles/anvil/checks/ensure-no-cyclic-deps.just b/justfiles/anvil/checks/ensure-no-cyclic-deps.just index 1e254e3e2..57f360170 100644 --- a/justfiles/anvil/checks/ensure-no-cyclic-deps.just +++ b/justfiles/anvil/checks/ensure-no-cyclic-deps.just @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -# Managed by cargo-anvil. Update the corresponding template in the cargo-anvil -# crate unless the change is repository-specific. +# GENERATED BY cargo-anvil. DO NOT EDIT DIRECTLY. +# Update cargo-anvil and regenerate; repository-specific edits stop automatic updates. # Update behaviour: https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/updates.md # See https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/checks.md @@ -9,11 +9,12 @@ # Modified tier. # Check that workspace dependencies contain no cycles. -[script("pwsh")] -anvil-ensure-no-cyclic-deps: anvil-ensure-no-cyclic-deps-validate-prereqs +[script("pwsh", "-NoProfile")] +anvil-ensure-no-cyclic-deps: anvil-ensure-no-cyclic-deps-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - if ($env:ANVIL_INCLUDE_MODIFIED -eq '--skip') { exit 0 } - cargo ensure-no-cyclic-deps + $include = (& "{{ just_executable() }}" _anvil-impact-include modified) + if ($include -eq '--skip') { Write-Host 'anvil-ensure-no-cyclic-deps: no modified packages; skipping'; exit 0 } + & cargo {{_anvil_stable_toolchain_args}} ensure-no-cyclic-deps if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-ensure-no-cyclic-deps` recipe. diff --git a/justfiles/anvil/checks/ensure-no-default-features.just b/justfiles/anvil/checks/ensure-no-default-features.just index 6514c558e..cee4d24d5 100644 --- a/justfiles/anvil/checks/ensure-no-default-features.just +++ b/justfiles/anvil/checks/ensure-no-default-features.just @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -# Managed by cargo-anvil. Update the corresponding template in the cargo-anvil -# crate unless the change is repository-specific. +# GENERATED BY cargo-anvil. DO NOT EDIT DIRECTLY. +# Update cargo-anvil and regenerate; repository-specific edits stop automatic updates. # Update behaviour: https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/updates.md # See https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/checks.md @@ -9,11 +9,12 @@ # Modified tier. # Check that workspace packages compile without default features. -[script("pwsh")] -anvil-ensure-no-default-features: anvil-ensure-no-default-features-validate-prereqs +[script("pwsh", "-NoProfile")] +anvil-ensure-no-default-features: anvil-ensure-no-default-features-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - if ($env:ANVIL_INCLUDE_MODIFIED -eq '--skip') { exit 0 } - cargo ensure-no-default-features + $include = (& "{{ just_executable() }}" _anvil-impact-include modified) + if ($include -eq '--skip') { Write-Host 'anvil-ensure-no-default-features: no modified packages; skipping'; exit 0 } + & cargo {{_anvil_stable_toolchain_args}} ensure-no-default-features if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-ensure-no-default-features` recipe. diff --git a/justfiles/anvil/checks/examples.just b/justfiles/anvil/checks/examples.just index 070ae7762..fe447b7fb 100644 --- a/justfiles/anvil/checks/examples.just +++ b/justfiles/anvil/checks/examples.just @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -# Managed by cargo-anvil. Update the corresponding template in the cargo-anvil -# crate unless the change is repository-specific. +# GENERATED BY cargo-anvil. DO NOT EDIT DIRECTLY. +# Update cargo-anvil and regenerate; repository-specific edits stop automatic updates. # Update behaviour: https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/updates.md # See https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/checks.md @@ -9,16 +9,17 @@ # Affected tier. # Build all workspace examples. -[script("pwsh")] -anvil-examples: anvil-examples-validate-prereqs +[script("pwsh", "-NoProfile")] +anvil-examples: anvil-examples-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - if ($env:ANVIL_INCLUDE_AFFECTED -eq '--skip') { exit 0 } - & cargo build @(if ($env:ANVIL_INCLUDE_AFFECTED) { -split $env:ANVIL_INCLUDE_AFFECTED } else { '--workspace' }) --examples --all-features --locked + $include = (& "{{ just_executable() }}" _anvil-impact-include affected) + if ($include -eq '--skip') { Write-Host 'anvil-examples: no affected packages; skipping'; exit 0 } + & cargo {{_anvil_stable_toolchain_args}} build @(if ($include) { -split $include } else { '--workspace' }) --examples --all-features --locked if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-examples` recipe. [group("anvil-setup")] -anvil-examples-setup installer="install": anvil-tool-rustc-validate-prereqs +anvil-examples-setup installer="install": anvil-toolchain-stable-install # Validate prerequisites for the `anvil-examples` recipe. [group("anvil-setup")] diff --git a/justfiles/anvil/checks/external-types.just b/justfiles/anvil/checks/external-types.just index 8b69e8d7a..d659a3d4d 100644 --- a/justfiles/anvil/checks/external-types.just +++ b/justfiles/anvil/checks/external-types.just @@ -1,12 +1,12 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -# Managed by cargo-anvil. Update the corresponding template in the cargo-anvil -# crate unless the change is repository-specific. +# GENERATED BY cargo-anvil. DO NOT EDIT DIRECTLY. +# Update cargo-anvil and regenerate; repository-specific edits stop automatic updates. # Update behaviour: https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/updates.md # See https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/checks.md -# Affected tier (lints public API of changed crates and rev-deps). +# 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 library crates and run @@ -16,21 +16,32 @@ # skip/splat preamble, this recipe stays multi-step: cargo-check- # external-types is per-manifest (no --package/--workspace), so we # build a name->manifest map from cargo metadata, filter to lib crates, -# intersect with ANVIL_INCLUDE_AFFECTED, and call the tool once per +# intersect with the affected set ($include), and call the tool once per # crate. Hard-fails on errors (no tolerance, unlike semver-check). # Check public APIs for unapproved external types. -[script("pwsh")] -anvil-external-types: anvil-external-types-validate-prereqs +[script("pwsh", "-NoProfile")] +anvil-external-types: anvil-external-types-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - if ($env:ANVIL_INCLUDE_AFFECTED -eq '--skip') { + $include = (& "{{ just_executable() }}" _anvil-impact-include affected) + if ($include -eq '--skip') { Write-Host 'anvil-external-types: no affected packages; skipping' exit 0 } - $pkg = @(if ($env:ANVIL_INCLUDE_AFFECTED) { -split $env:ANVIL_INCLUDE_AFFECTED } else { '--workspace' }) + $pkg = @(if ($include) { -split $include } else { '--workspace' }) # Build pkg-name -> manifest-path map, restricted to library crates. $libPkgs = @{} - $meta = cargo metadata --no-deps --format-version 1 | ConvertFrom-Json + $metadataJson = & cargo {{_anvil_stable_toolchain_args}} metadata --no-deps --format-version 1 + if ($LASTEXITCODE -ne 0) { + Write-Error 'anvil-external-types: cargo metadata failed' + exit $LASTEXITCODE + } + try { + $meta = $metadataJson | ConvertFrom-Json + } catch { + Write-Error "anvil-external-types: could not parse cargo metadata output: $_" + exit 1 + } foreach ($p in $meta.packages) { if ($p.targets | Where-Object { $_.kind -contains 'lib' }) { $libPkgs[$p.name] = $p.manifest_path @@ -56,6 +67,7 @@ anvil-external-types: anvil-external-types-validate-prereqs Write-Host 'anvil-external-types: no affected library crates; skipping' exit 0 } + Write-Host "anvil-external-types: catalog tool minimum cargo-check-external-types {{ cargo_check_external_types_version }}; rustdoc toolchain {{ rust_nightly_external_types }}" $failed = $false foreach ($p in $packages) { Write-Host "anvil-external-types: $p" @@ -67,7 +79,10 @@ anvil-external-types: anvil-external-types-validate-prereqs # external-types upgrade. No tolerance for schema mismatches: # if it fails, the pin or the tool needs to move. & cargo '+{{ rust_nightly_external_types }}' check-external-types --manifest-path $libPkgs[$p] - if ($LASTEXITCODE -ne 0) { $failed = $true } + if ($LASTEXITCODE -ne 0) { + Write-Error "anvil-external-types: '$p' failed. The installed tool may be newer than the catalog minimum; it must remain compatible with rustdoc from {{ rust_nightly_external_types }}." + $failed = $true + } } if ($failed) { exit 1 } diff --git a/justfiles/anvil/checks/fmt.just b/justfiles/anvil/checks/fmt.just index 1974c7ab4..84fda5052 100644 --- a/justfiles/anvil/checks/fmt.just +++ b/justfiles/anvil/checks/fmt.just @@ -14,7 +14,7 @@ # explicitly avoid for udeps/miri/careful/external-types. # Check Rust source formatting. -[script("pwsh")] +[script("pwsh", "-NoProfile")] anvil-fmt: anvil-fmt-validate-prereqs $ErrorActionPreference = 'Stop' if ($env:ANVIL_INCLUDE_MODIFIED -eq '--skip') { exit 0 } diff --git a/justfiles/anvil/checks/license-headers.just b/justfiles/anvil/checks/license-headers.just index 78937d77c..fb794c32e 100644 --- a/justfiles/anvil/checks/license-headers.just +++ b/justfiles/anvil/checks/license-headers.just @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -# Managed by cargo-anvil. Update the corresponding template in the cargo-anvil -# crate unless the change is repository-specific. +# GENERATED BY cargo-anvil. DO NOT EDIT DIRECTLY. +# Update cargo-anvil and regenerate; repository-specific edits stop automatic updates. # Update behaviour: https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/updates.md # See https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/checks.md @@ -9,11 +9,12 @@ # Modified tier. # Check source files for required license headers. -[script("pwsh")] -anvil-license-headers: anvil-license-headers-validate-prereqs +[script("pwsh", "-NoProfile")] +anvil-license-headers: anvil-license-headers-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - if ($env:ANVIL_INCLUDE_MODIFIED -eq '--skip') { exit 0 } - cargo heather + $include = (& "{{ just_executable() }}" _anvil-impact-include modified) + if ($include -eq '--skip') { Write-Host 'anvil-license-headers: no modified packages; skipping'; exit 0 } + & cargo {{_anvil_stable_toolchain_args}} heather if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install prerequisites for the `anvil-license-headers` recipe. diff --git a/justfiles/anvil/checks/llvm-cov.just b/justfiles/anvil/checks/llvm-cov.just index 68baa8689..1096d2743 100644 --- a/justfiles/anvil/checks/llvm-cov.just +++ b/justfiles/anvil/checks/llvm-cov.just @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -# Managed by cargo-anvil. Update the corresponding template in the cargo-anvil -# crate unless the change is repository-specific. +# GENERATED BY cargo-anvil. DO NOT EDIT DIRECTLY. +# Update cargo-anvil and regenerate; repository-specific edits stop automatic updates. # Update behaviour: https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/updates.md # See https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/checks.md @@ -15,27 +15,35 @@ # `coverage-measure` recipe, which also runs on nightly. # Measure test coverage and enforce the coverage gate. -[script("pwsh")] -anvil-llvm-cov: anvil-llvm-cov-validate-prereqs +[script("pwsh", "-NoProfile")] +anvil-llvm-cov: anvil-llvm-cov-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - if ($env:ANVIL_INCLUDE_AFFECTED -eq '--skip') { + $include = (& "{{ just_executable() }}" _anvil-impact-include affected) + if ($include -eq '--skip') { Write-Host 'anvil-llvm-cov: no affected packages; skipping' exit 0 } - $pkg = @(if ($env:ANVIL_INCLUDE_AFFECTED) { -split $env:ANVIL_INCLUDE_AFFECTED } else { '--workspace' }) - # Drop coverage-opted-out packages from the measured set. A crate that + $pkg = @(if ($include) { -split $include } else { '--workspace' }) + $allTestPkg = $pkg + # Separate coverage-opted-out packages from the measured set. A crate that # is entirely `#![cfg_attr(coverage_nightly, coverage(off))]` (e.g. a # script-only crate) compiles to a test binary with no __llvm_covmap, # and `llvm-cov export` aborts with "no coverage data found" whenever # such a package is the *only* object in the report (e.g. it is the # sole affected crate). Opt-out is declared via # [package.metadata.coverage-gate] min-lines-percent = 0 -- the same - # marker cargo-coverage-gate already treats as NO DATA -> OK. If the - # affected set is *entirely* opted-out, there is nothing to measure, - # so we skip exactly like the empty-affected ('--skip') case. + # marker cargo-coverage-gate already treats as NO DATA -> OK. Opted-out + # packages still run through plain nextest; opting out of measurement + # never opts a changed package out of tests. + $testOnlyPkg = [System.Collections.Generic.List[string]]::new() if ($pkg -contains '--package') { + $metadataJson = & cargo {{_anvil_stable_toolchain_args}} metadata --no-deps --format-version 1 + if ($LASTEXITCODE -ne 0) { + Write-Error 'anvil-llvm-cov: cargo metadata failed' + exit $LASTEXITCODE + } $optedOut = @( - (cargo metadata --no-deps --format-version 1 | ConvertFrom-Json).packages | + ($metadataJson | ConvertFrom-Json).packages | Where-Object { $null -ne $_.metadata.'coverage-gate'.'min-lines-percent' -and [double]$_.metadata.'coverage-gate'.'min-lines-percent' -eq 0 } | ForEach-Object { $_.name } ) @@ -47,6 +55,8 @@ anvil-llvm-cov: anvil-llvm-cov-validate-prereqs # the full name@version token in $pkg so the llvm-cov # measurement below resolves unambiguously. if ($pkg[$i] -eq '--package' -and ($i + 1) -lt $pkg.Count -and $optedOut -contains (($pkg[$i + 1] -split '@', 2)[0])) { + $testOnlyPkg.Add('--package') + $testOnlyPkg.Add($pkg[$i + 1]) $i++ continue } @@ -54,11 +64,11 @@ anvil-llvm-cov: anvil-llvm-cov-validate-prereqs } $pkg = $filtered.ToArray() } - if (-not ($pkg -contains '--package')) { - Write-Host 'anvil-llvm-cov: only coverage-opted-out packages affected; nothing to measure, skipping' - exit 0 - } } + $configs = @( + @{ name = 'all-features'; flag = '--all-features' }, + @{ name = 'no-default'; flag = '--no-default-features' } + ) # cargo-llvm-cov doesn't work on aarch64-pc-windows-msvc: the # llvm-profdata that ships with the rust toolchain there fails # to merge the .profraw set ("no profile can be merged"). Fall @@ -67,8 +77,22 @@ anvil-llvm-cov: anvil-llvm-cov-validate-prereqs # been used anyway (coverage upload is gated on Linux only). if ($IsWindows -and ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64' -or $env:PROCESSOR_ARCHITEW6432 -eq 'ARM64')) { Write-Host 'anvil-llvm-cov: aarch64-pc-windows-msvc -- skipping coverage; running plain nextest' - & cargo '+{{ rust_nightly }}' nextest run @pkg --all-features --locked - exit $LASTEXITCODE + foreach ($cfg in $configs) { + & cargo '+{{ rust_nightly }}' nextest run @allTestPkg $cfg.flag --no-tests=pass --locked + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + } + exit 0 + } + if ($testOnlyPkg.Count -gt 0) { + Write-Host 'anvil-llvm-cov: running tests without coverage for opted-out packages' + foreach ($cfg in $configs) { + & cargo '+{{ rust_nightly }}' nextest run @testOnlyPkg $cfg.flag --no-tests=pass --locked + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + } + } + if ($pkg.Count -eq 0 -and $testOnlyPkg.Count -gt 0) { + Write-Host 'anvil-llvm-cov: all affected packages opted out of coverage; tests completed' + exit 0 } # The *report* output directory (target/coverage/) is something we # choose and must exist before --output-path runs. @@ -102,10 +126,6 @@ anvil-llvm-cov: anvil-llvm-cov-validate-prereqs # here -- a llvm-cov run that finds no tests almost always means a # config mistake (wrong package filter, missing test target, etc.), not # a legitimate empty set. anvil-miri is the exception (see its comment). - $configs = @( - @{ name = 'all-features'; flag = '--all-features' }, - @{ name = 'no-default'; flag = '--no-default-features' } - ) foreach ($cfg in $configs) { cargo '+{{ rust_nightly }}' llvm-cov clean --workspace if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } @@ -128,7 +148,7 @@ anvil-llvm-cov: anvil-llvm-cov-validate-prereqs # threshold, so coverage regressions fail the PR locally and in cloud # workflows -- the Codecov upload stays purely for display, not the gate. # - # Scope the gate to the SAME package set we measured. When impact + # Scope the gate to the same package set we measured. When impact # scoping is active, $pkg holds `--package X@ver ...` (already opt-out # filtered above); pass those through so the gate only gates packages # that were actually instrumented -- otherwise unmeasured members would @@ -148,7 +168,7 @@ anvil-llvm-cov: anvil-llvm-cov-validate-prereqs } $gateArgs.Add($pkg[$i]) } - cargo coverage-gate @gateArgs --lcov target/coverage/lcov-all-features.info --lcov target/coverage/lcov-no-default.info + & cargo {{_anvil_stable_toolchain_args}} coverage-gate @gateArgs --lcov target/coverage/lcov-all-features.info --lcov target/coverage/lcov-no-default.info if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # --- pr-test members (shared with scheduled-test) --- diff --git a/justfiles/anvil/checks/loom.just b/justfiles/anvil/checks/loom.just index 1fb3ba192..4703a71f3 100644 --- a/justfiles/anvil/checks/loom.just +++ b/justfiles/anvil/checks/loom.just @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -# Managed by cargo-anvil. Update the corresponding template in the cargo-anvil -# crate unless the change is repository-specific. +# GENERATED BY cargo-anvil. DO NOT EDIT DIRECTLY. +# Update cargo-anvil and regenerate; repository-specific edits stop automatic updates. # Update behaviour: https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/updates.md # See https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/checks.md @@ -33,6 +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. +# 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 @@ -47,27 +49,30 @@ # invocations cleanly on both pwsh/bash. # Run Loom concurrency tests for affected workspace packages. -[script("pwsh")] -anvil-loom: anvil-loom-validate-prereqs +[script("pwsh", "-NoProfile")] +anvil-loom: anvil-loom-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - if ($env:ANVIL_INCLUDE_AFFECTED -eq '--skip') { + $include = (& "{{ just_executable() }}" _anvil-impact-include affected) + if ($include -eq '--skip') { Write-Host 'anvil-loom: no affected packages; skipping' exit 0 } - $meta = cargo metadata --no-deps --format-version 1 | ConvertFrom-Json + $meta = & cargo {{_anvil_stable_toolchain_args}} metadata --no-deps --format-version 1 | ConvertFrom-Json if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Optional impact-scoping: recover bare package names from the # impact step's version-qualified `--package name@version` pairs. + # Full-workspace modes (`--workspace` -- e.g. the scheduled tier, + # ANVIL_IMPACT=off, or no impact cache) carry no `--package` tokens, + # so $affected stays $null and the package set is left unfiltered + # (filtering on an empty set would wrongly skip every loom target). $affected = $null - if ($env:ANVIL_INCLUDE_AFFECTED) { - $tokens = @(-split $env:ANVIL_INCLUDE_AFFECTED) - $affected = @() - for ($i = 0; $i -lt $tokens.Count; $i++) { - if ($tokens[$i] -eq '--package' -and ($i + 1) -lt $tokens.Count) { - $affected += ($tokens[$i + 1] -split '@', 2)[0] - $i++ - } + $tokens = @(-split $include) + for ($i = 0; $i -lt $tokens.Count; $i++) { + if ($tokens[$i] -eq '--package' -and ($i + 1) -lt $tokens.Count) { + if ($null -eq $affected) { $affected = @() } + $affected += ($tokens[$i + 1] -split '@', 2)[0] + $i++ } } $pkgs = $meta.packages @@ -105,7 +110,7 @@ anvil-loom: anvil-loom-validate-prereqs $env:RUSTFLAGS = "--cfg loom $($env:RUSTFLAGS)".Trim() foreach ($lt in $loomTargets) { Write-Host "anvil-loom: $($lt.pkg) :: $($lt.target)" - & cargo test -p $lt.pkg --release --all-features --locked --test $lt.target -- --test-threads=1 + & cargo {{_anvil_stable_toolchain_args}} test -p $lt.pkg --release --all-features --locked --test $lt.target -- --test-threads=1 if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } } @@ -115,7 +120,7 @@ anvil-loom: anvil-loom-validate-prereqs # Install prerequisites for the `anvil-loom` recipe. [group("anvil-setup")] -anvil-loom-setup installer="install": anvil-tool-rustc-validate-prereqs +anvil-loom-setup installer="install": anvil-toolchain-stable-install # Validate prerequisites for the `anvil-loom` recipe. [group("anvil-setup")] diff --git a/justfiles/anvil/checks/miri-race-coverage.just b/justfiles/anvil/checks/miri-race-coverage.just index 504d78a98..61a2dbe3b 100644 --- a/justfiles/anvil/checks/miri-race-coverage.just +++ b/justfiles/anvil/checks/miri-race-coverage.just @@ -14,7 +14,7 @@ # per-seed crashes, so coverage matters more than depth-per-seed. # Run Miri tests with race coverage tracking. -[script("pwsh")] +[script("pwsh", "-NoProfile")] anvil-miri-race-coverage: anvil-miri-race-coverage-validate-prereqs $ErrorActionPreference = 'Stop' if ($env:ANVIL_INCLUDE_AFFECTED -eq '--skip') { exit 0 } diff --git a/justfiles/anvil/checks/miri-strict-provenance.just b/justfiles/anvil/checks/miri-strict-provenance.just index 890866ba4..d3e9f75b6 100644 --- a/justfiles/anvil/checks/miri-strict-provenance.just +++ b/justfiles/anvil/checks/miri-strict-provenance.just @@ -7,7 +7,7 @@ # See https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/checks.md # Run Miri tests with strict provenance checks. -[script("pwsh")] +[script("pwsh", "-NoProfile")] anvil-miri-strict-provenance: anvil-miri-strict-provenance-validate-prereqs $ErrorActionPreference = 'Stop' if ($env:ANVIL_INCLUDE_AFFECTED -eq '--skip') { exit 0 } diff --git a/justfiles/anvil/checks/miri-tree-borrows.just b/justfiles/anvil/checks/miri-tree-borrows.just index 97ae4e927..87770df69 100644 --- a/justfiles/anvil/checks/miri-tree-borrows.just +++ b/justfiles/anvil/checks/miri-tree-borrows.just @@ -20,7 +20,7 @@ # vars set. # Run Miri tests with the Tree Borrows model. -[script("pwsh")] +[script("pwsh", "-NoProfile")] anvil-miri-tree-borrows: anvil-miri-tree-borrows-validate-prereqs $ErrorActionPreference = 'Stop' if ($env:ANVIL_INCLUDE_AFFECTED -eq '--skip') { exit 0 } diff --git a/justfiles/anvil/checks/miri.just b/justfiles/anvil/checks/miri.just index e8bce0c98..f807b3915 100644 --- a/justfiles/anvil/checks/miri.just +++ b/justfiles/anvil/checks/miri.just @@ -22,7 +22,7 @@ # a runner class. # Run selected Miri test artifacts with CPU-based parallelism. -[script("pwsh")] +[script("pwsh", "-NoProfile")] _anvil-miri-test: $ErrorActionPreference = 'Stop' if ($env:ANVIL_INCLUDE_AFFECTED -eq '--skip') { exit 0 } @@ -213,7 +213,7 @@ _anvil-miri-test: } # Run Miri tests for affected workspace packages. -[script("pwsh")] +[script("pwsh", "-NoProfile")] anvil-miri: anvil-miri-validate-prereqs $ErrorActionPreference = 'Stop' if ($env:ANVIL_INCLUDE_AFFECTED -eq '--skip') { exit 0 } diff --git a/justfiles/anvil/checks/msrv-test.just b/justfiles/anvil/checks/msrv-test.just new file mode 100644 index 000000000..f95b38cff --- /dev/null +++ b/justfiles/anvil/checks/msrv-test.just @@ -0,0 +1,56 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# GENERATED BY cargo-anvil. DO NOT EDIT DIRECTLY. +# Update cargo-anvil and regenerate; repository-specific edits stop automatic updates. +# Update behaviour: https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/updates.md + +# See https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/checks.md + +# Run affected-package tests under the declared MSRV. +[script("pwsh", "-NoProfile")] +anvil-msrv-test: anvil-msrv-test-validate-prereqs anvil-impact + $ErrorActionPreference = 'Stop' + $include = (& "{{ just_executable() }}" _anvil-impact-include affected) + $impactExit = $LASTEXITCODE + if ($impactExit -ne 0) { exit $impactExit } + if ($include -eq '--skip') { Write-Host 'anvil-msrv-test: no affected packages; skipping'; exit 0 } + $toolchainOutput = & "{{ just_executable() }}" _anvil-resolve-stable msrv | Out-String + $resolverExit = $LASTEXITCODE + if ($resolverExit -ne 0) { exit $resolverExit } + $toolchain = $toolchainOutput.Trim() + if ([string]::IsNullOrWhiteSpace($toolchain)) { + Write-Host 'anvil-msrv-test: no root MSRV declared; skipping' + exit 0 + } + $pkg = @(if ($include) { -split $include } else { '--workspace' }) + & cargo "+$toolchain" test @pkg --all-targets --all-features --locked + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & cargo "+$toolchain" test @pkg --all-targets --locked + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + +# Install the declared MSRV toolchain only when this check is required. +[group("anvil-setup")] +[script("pwsh", "-NoProfile")] +anvil-msrv-test-setup installer="install": + & "{{ just_executable() }}" _anvil-resolve-stable install-msrv + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + +# Validate the MSRV prerequisite. +[group("anvil-setup")] +[script("pwsh", "-NoProfile")] +anvil-msrv-test-validate-prereqs: + $toolchainOutput = & "{{ just_executable() }}" _anvil-resolve-stable msrv | Out-String + $resolverExit = $LASTEXITCODE + if ($resolverExit -ne 0) { exit $resolverExit } + $toolchain = $toolchainOutput.Trim() + if ([string]::IsNullOrWhiteSpace($toolchain)) { exit 0 } + & cargo "+$toolchain" --version + if ($LASTEXITCODE -ne 0) { + $hint = if ($env:ANVIL_MSRV_TOOLCHAIN) { + 'provision ANVIL_MSRV_TOOLCHAIN or unset it to let Anvil install the declared public MSRV' + } else { + 'run: just anvil-msrv-test-setup' + } + Write-Error "anvil: MSRV toolchain '$toolchain' is unavailable; $hint" + exit $LASTEXITCODE + } diff --git a/justfiles/anvil/checks/mutants-diff.just b/justfiles/anvil/checks/mutants-diff.just index cd62dcf93..e4efc812f 100644 --- a/justfiles/anvil/checks/mutants-diff.just +++ b/justfiles/anvil/checks/mutants-diff.just @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -# Managed by cargo-anvil. Update the corresponding template in the cargo-anvil -# crate unless the change is repository-specific. +# GENERATED BY cargo-anvil. DO NOT EDIT DIRECTLY. +# Update cargo-anvil and regenerate; repository-specific edits stop automatic updates. # Update behaviour: https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/updates.md # See https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/checks.md @@ -17,14 +17,15 @@ # affected leg. Coverage on the other three legs is unchanged. # Run mutation testing for changes relative to the base branch. -[script("pwsh")] -anvil-mutants-diff: anvil-mutants-diff-validate-prereqs +[script("pwsh", "-NoProfile")] +anvil-mutants-diff: anvil-mutants-diff-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' + $include = (& "{{ just_executable() }}" _anvil-impact-include affected) if ($IsWindows -and ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64' -or $env:PROCESSOR_ARCHITEW6432 -eq 'ARM64')) { Write-Host 'anvil-mutants-diff: anvil-mutants-diff-validate-prereqs aarch64-pc-windows-msvc -- cargo-mutants does not build here (winapi); skipping' exit 0 } - if ($env:ANVIL_INCLUDE_AFFECTED -eq '--skip') { + if ($include -eq '--skip') { Write-Host 'anvil-mutants-diff: anvil-mutants-diff-validate-prereqs no affected packages; skipping' exit 0 } @@ -40,9 +41,16 @@ anvil-mutants-diff: anvil-mutants-diff-validate-prereqs if (-not $tmp_dir) { $tmp_dir = $env:AGENT_TEMPDIRECTORY } if (-not $tmp_dir) { $tmp_dir = [System.IO.Path]::GetTempPath() } $diff_path = Join-Path $tmp_dir 'anvil-mutants-diff.diff' - git diff "$base..HEAD" --output=$diff_path + # Diff the base against the WORKING TREE, not against HEAD. + # cargo-mutants validates every line of the diff against the file on + # disk and aborts when they disagree, so a commit-to-commit diff fails + # the moment anything is uncommitted -- which is the normal local state, + # since the point of running a tier locally is to check work in progress. + # CI has a clean tree, so the two forms are identical there and this is + # not a behaviour change for it. + git diff "$base" --output=$diff_path if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - cargo mutants --in-diff $diff_path --no-shuffle --jobs 0 + & cargo {{_anvil_stable_toolchain_args}} mutants --in-diff $diff_path --no-shuffle --jobs 0 if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # --- pr-mutants members --- diff --git a/justfiles/anvil/checks/mutants-full.just b/justfiles/anvil/checks/mutants-full.just index e358513da..e3e32a16b 100644 --- a/justfiles/anvil/checks/mutants-full.just +++ b/justfiles/anvil/checks/mutants-full.just @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -# Managed by cargo-anvil. Update the corresponding template in the cargo-anvil -# crate unless the change is repository-specific. +# GENERATED BY cargo-anvil. DO NOT EDIT DIRECTLY. +# Update cargo-anvil and regenerate; repository-specific edits stop automatic updates. # Update behaviour: https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/updates.md # See https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/checks.md @@ -10,10 +10,14 @@ # workspace regardless of diff. # Run exhaustive mutation testing across the workspace. -[script("pwsh")] +[script("pwsh", "-NoProfile")] anvil-mutants-full: anvil-mutants-full-validate-prereqs $ErrorActionPreference = 'Stop' - & cargo mutants --workspace --no-shuffle --jobs 0 + if ($IsWindows -and ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64' -or $env:PROCESSOR_ARCHITEW6432 -eq 'ARM64')) { + Write-Host 'anvil-mutants-full: aarch64-pc-windows-msvc -- cargo-mutants does not build here (winapi); skipping' + exit 0 + } + & cargo {{_anvil_stable_toolchain_args}} mutants --workspace --no-shuffle --jobs 0 if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # --- scheduled-exhaustive members (mutants-full reuses cargo-mutants; diff --git a/justfiles/anvil/checks/pr-title.just b/justfiles/anvil/checks/pr-title.just index 4574292b8..0b7d80ba9 100644 --- a/justfiles/anvil/checks/pr-title.just +++ b/justfiles/anvil/checks/pr-title.just @@ -1,33 +1,73 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -# Managed by cargo-anvil. Update the corresponding template in the cargo-anvil -# crate unless the change is repository-specific. +# GENERATED BY cargo-anvil. DO NOT EDIT DIRECTLY. +# Update cargo-anvil and regenerate; repository-specific edits stop automatic updates. # Update behaviour: https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/updates.md # See https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/checks.md # Unscoped (PR title is not source-related). # -# Validates that $env:PR_TITLE matches Conventional Commits when set; -# no-op when unset. Cloud workflows inject PR_TITLE explicitly: GH Actions -# from ${{ github.event.pull_request.title }}; ADO resolves it via the REST -# API in steps/group.yml (ADO has no PR-title predefined variable). Locally -# it skips silently when PR_TITLE is unset -- there is no reliable way to -# recover the PR title, so the recipe defers the check to cloud workflows. +# Validates that $env:PR_TITLE matches the accepted title syntax when it +# holds a value; no-op when unset or empty. Cloud workflows inject PR_TITLE +# explicitly: GH Actions from ${{ github.event.pull_request.title }}; ADO +# resolves it via the REST API in steps/group.yml (ADO has no PR-title +# predefined variable) and publishes an empty value only outside PR builds, +# failing closed when a known PR's title cannot be retrieved. Local runs have +# no reliable way to recover the PR title, so an unset or empty value defers +# the check rather than failing it. # It does NOT tolerate a non-empty-but-wrong value: a misconfigured cloud # variable (e.g. an unexpanded pipeline macro) is a real error and fails # the check loudly rather than silently passing. # Validate the pull request title. -[script("pwsh")] +[script("pwsh", "-NoProfile")] anvil-pr-title: anvil-pr-title-validate-prereqs $title = $env:PR_TITLE if (-not $title) { - Write-Host 'anvil-pr-title: PR_TITLE env var not set; skipping (check runs in cloud workflows)' + Write-Host 'anvil-pr-title: PR_TITLE env var is unset or empty; skipping title validation' exit 0 } - if ($title -notmatch '^(feat|fix|chore|docs|refactor|test|build|ci|perf|revert)(\([^)]+\))?!?: .+') { - Write-Error "PR title '$title' does not match Conventional Commits" + + # These human-readable formats are also the validation source of truth. + $allowedTypes = @('feat', 'fix', 'chore', 'docs', 'refactor', 'test', 'build', 'ci', 'perf', 'revert') + $allowedPatterns = @( + ': ' + '(): ' + '!: ' + '()!: ' + ) + $placeholderRegex = @{ + '' = '(?:' + (($allowedTypes | ForEach-Object { [regex]::Escape($_) }) -join '|') + ')' + '' = '[^)]+' + '' = '.+' + } + $titlePatterns = $allowedPatterns | ForEach-Object { + # Escape first, expand second: escaping turns the display syntax into + # literal regex, after which the trusted fragments above are injected. + # Expanding first would escape those fragments and match them literally. + $pattern = [regex]::Escape($_) + foreach ($placeholder in $placeholderRegex.GetEnumerator()) { + $pattern = $pattern.Replace([regex]::Escape($placeholder.Key), $placeholder.Value) + } + $pattern + } + # Anchored at both ends so a title cannot smuggle extra lines past a + # matching prefix. `$` rather than `\z` leaves a lone trailing newline + # tolerated, which costs nothing and avoids failing on a title source + # that terminates its value. + $titleRegex = '^(?:' + ($titlePatterns -join '|') + ')$' + + # The accepted forms are a deliberate subset of Conventional Commits, so the + # diagnostic names the subset rather than claiming the full grammar. + if ($title -notmatch $titleRegex) { + $diagnostic = @( + "PR title '$title' does not match the accepted Conventional Commits subset." + 'Allowed patterns:' + $allowedPatterns | ForEach-Object { " - $_" } + 'Allowed types (case-insensitive): ' + ($allowedTypes -join ', ') + ) + Write-Error ($diagnostic -join [Environment]::NewLine) exit 1 } diff --git a/justfiles/anvil/checks/readme-check.just b/justfiles/anvil/checks/readme-check.just index c018d9cf2..7ae42342d 100644 --- a/justfiles/anvil/checks/readme-check.just +++ b/justfiles/anvil/checks/readme-check.just @@ -1,12 +1,16 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -# Managed by cargo-anvil. Update the corresponding template in the cargo-anvil -# crate unless the change is repository-specific. +# GENERATED BY cargo-anvil. DO NOT EDIT DIRECTLY. +# Update cargo-anvil and regenerate; repository-specific edits stop automatic updates. # Update behaviour: https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/updates.md # See https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/checks.md -# Modified tier. +# Unscoped. +# +# A workspace-level README template (`crates/README.j2` / `README.j2`) feeds +# every crate's README but is a repo-level input cargo-delta does not map to a +# cargo package, so this check cannot be safely impact-scoped and always runs. # # cargo-doc2readme regenerates a crate's README.md from its rustdoc. # Two extension points users frequently need: @@ -18,25 +22,29 @@ # `[package.metadata.ox-gen-readme]\ndisable = true` to their # Cargo.toml. anvil skips those crates. # -# Bin-only crates have no library rustdoc to base a README on, so they -# are skipped as well (cargo doc2readme requires a library target). +# Library or proc-macro rustdoc is preferred when a crate has one; bin-only +# crates use their binary rustdoc so every publishable crate follows the same +# generation policy. # TODO(anvil-runner): even after a `cargo ox-run` helper absorbs the # skip/splat preamble, this recipe stays multi-step: per-crate -# iteration over library targets, cargo-metadata-driven opt-outs +# iteration over eligible targets, cargo-metadata-driven opt-outs # (publish=false, [package.metadata.ox-gen-readme] disable), per-crate # Push-Location into the crate dir (cargo-doc2readme is CWD-sensitive # rather than --manifest-path-driven), and per-crate template-path -# resolution. The ANVIL_INCLUDE_MODIFIED value would still need to -# be intersected with the lib-crate set rather than splatted into cargo. +# resolution. The recipe iterates the full eligible-crate set every run. -# Check that crate README files are up to date. -[script("pwsh")] -anvil-readme-check: anvil-readme-check-validate-prereqs +# Generate or check every eligible crate README with the same selection policy. +[script("pwsh", "-NoProfile")] +_anvil-readme mode: anvil-readme-check-validate-prereqs $ErrorActionPreference = 'Stop' - if ($env:ANVIL_INCLUDE_MODIFIED -eq '--skip') { - Write-Host 'anvil-readme-check: no modified packages; skipping' - exit 0 + $stableArgs = {{_anvil_stable_toolchain_args}} + $action = '{{ mode }}' + if ($action -notin @('generate', 'check')) { + [Console]::Error.WriteLine("anvil-readme: invalid mode '$action'; expected 'generate' or 'check'") + exit 2 } + $check = $action -eq 'check' + $prefix = "anvil-readme-${action}" # Detect a workspace-level README template. Two conventional # locations: crates/README.j2 (cargo-workspaces idiom) or # README.j2 at repo root. @@ -44,37 +52,28 @@ anvil-readme-check: anvil-readme-check-validate-prereqs foreach ($candidate in 'crates/README.j2', 'README.j2') { if (Test-Path $candidate) { $template = (Resolve-Path $candidate).Path; break } } - # Iterate library crates. Filter by impact set when set, then drop - # bin-only crates and opt-outs. - $pkg = @(if ($env:ANVIL_INCLUDE_MODIFIED) { -split $env:ANVIL_INCLUDE_MODIFIED } else { '--workspace' }) - $meta = cargo metadata --no-deps --format-version 1 | ConvertFrom-Json - $byName = @{} - foreach ($p in $meta.packages) { $byName[$p.name] = $p } - $candidates = if ($pkg -contains '--workspace') { - @($meta.packages | ForEach-Object { $_.name }) - } else { - $names = @() - for ($i = 0; $i -lt $pkg.Count; $i++) { - if ($pkg[$i] -eq '--package' -and ($i + 1) -lt $pkg.Count) { - # Impact emits version-qualified specs (name@version); - # strip the @version to recover the bare package name - # this metadata lookup keys on. - $names += ($pkg[$i + 1] -split '@', 2)[0]; $i++ - } - } - $names + # Iterate every crate with rustdoc, then drop private crates and opt-outs. + # Unscoped: a change to the workspace-level README template is a repo-level + # input cargo-delta cannot map to a package, so this check always runs the + # full eligible-crate set rather than an impact subset. + $metadataJson = & cargo @stableArgs metadata --no-deps --format-version 1 + if ($LASTEXITCODE -ne 0) { + Write-Error "${prefix}: cargo metadata failed" + exit $LASTEXITCODE } + $meta = $metadataJson | ConvertFrom-Json $hadFailure = $false - foreach ($name in $candidates) { - $p = $byName[$name] - if (-not $p) { continue } - if (-not ($p.targets | Where-Object { $_.kind -contains 'lib' })) { continue } + foreach ($p in $meta.packages) { + $name = $p.name + $hasLib = [bool]($p.targets | Where-Object { ($_.kind -contains 'lib') -or ($_.kind -contains 'proc-macro') }) + $hasBin = [bool]($p.targets | Where-Object { $_.kind -contains 'bin' }) + if (-not $hasLib -and -not $hasBin) { continue } # Skip private crates (publish = false). They aren't released # and rarely have a polished README. Mirrors the # `cargo workspaces exec --ignore-private` idiom adopters # commonly use. if ($p.publish -is [array] -and $p.publish.Count -eq 0) { - Write-Host "anvil-readme-check: $name (skipped: publish = false)" + Write-Host "${prefix}: $name (skipped: publish = false)" continue } $disabled = $false @@ -82,13 +81,13 @@ anvil-readme-check: anvil-readme-check-validate-prereqs $disabled = $true } if ($disabled) { - Write-Host "anvil-readme-check: $name (opted out via [package.metadata.ox-gen-readme])" + Write-Host "${prefix}: $name (opted out via [package.metadata.ox-gen-readme])" continue } - Write-Host "anvil-readme-check: $name" + Write-Host "${prefix}: $name" # cargo doc2readme writes / compares relative to its CWD (not - # --manifest-path), so chdir into the crate before invoking - # --check. We also compute a per-crate relative path to the + # --manifest-path), so chdir into the crate before invoking it. + # We also compute a per-crate relative path to the # workspace-level template so the same template file works for # every crate (parallels the cargo-workspaces idiom). $crateDir = Split-Path -Parent $p.manifest_path @@ -99,9 +98,11 @@ anvil-readme-check: anvil-readme-check-validate-prereqs } else { $null } - $args = @('doc2readme', '--check') + $target = if ($hasLib) { '--lib' } else { '--bin' } + $args = @('doc2readme', $target) + if ($check) { $args += '--check' } if ($relTemplate) { $args += @('--template', $relTemplate) } - & cargo @args + & cargo @stableArgs @args if ($LASTEXITCODE -ne 0) { $hadFailure = $true } } finally { Pop-Location @@ -109,6 +110,9 @@ anvil-readme-check: anvil-readme-check-validate-prereqs } if ($hadFailure) { exit 1 } +# Check that crate README files are up to date. +anvil-readme-check: (_anvil-readme "check") + # Install prerequisites for the `anvil-readme-check` recipe. [group("anvil-setup")] anvil-readme-check-setup installer="install": (anvil-tool-cargo-doc2readme-install installer) diff --git a/justfiles/anvil/checks/semver-check.just b/justfiles/anvil/checks/semver-check.just index 9baa66bea..5dbd16128 100644 --- a/justfiles/anvil/checks/semver-check.just +++ b/justfiles/anvil/checks/semver-check.just @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -# Managed by cargo-anvil. Update the corresponding template in the cargo-anvil -# crate unless the change is repository-specific. +# GENERATED BY cargo-anvil. DO NOT EDIT DIRECTLY. +# Update cargo-anvil and regenerate; repository-specific edits stop automatic updates. # Update behaviour: https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/updates.md # See https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/checks.md @@ -10,24 +10,27 @@ # target branch baseline; only changed crates' surface is at risk). # # Several real-world conditions aren't actually SemVer violations: -# - bin-only crates have no API to compare ("no library targets found"). +# - bin-only crates have no API to compare (reported as a missing lib target). +# - `publish = false` crates cannot be published. # - 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 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. # -# Findings policy: this recipe is *advisory*. Real SemVer findings do -# NOT fail the recipe -- breaking changes between unreleased commits -# are normal (the major-version bump happens at release time, not on -# every PR). Instead, when there are findings we write a markdown -# advisory body to `target/anvil/comments/semver.md`; when the -# tree is clean we remove that file. cloud-workflow wiring (GH: +# Findings policy: cargo-semver-checks is *advisory*. Real SemVer +# findings and operational failures do NOT fail the recipe -- breaking +# changes between unreleased commits are normal, while an unbuildable +# baseline is not evidence that the PR broke the API. Instead, either +# outcome writes a markdown advisory body to +# `target/anvil/comments/semver.md`; when the tree is clean we remove +# that file. Anvil preflight failures before cargo-semver-checks still +# fail because no comparison can be configured. cloud-workflow wiring (GH: # marocchino/sticky-pull-request-comment; ADO: pwsh + REST API) # inspects the file after the recipe and upserts / clears a sticky # PR comment accordingly. Local invocation gets the same file @@ -35,37 +38,51 @@ # # 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 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 -# selective error tolerance for renamed crates ("package `` not -# found in ...") and bin->bin+lib transitions ("no library targets -# found"). +# special handling for renamed crates ("package `` not found in +# ...") and bin->bin+lib transitions (reported as a missing lib target). # Check affected crates for semantic versioning violations. -[script("pwsh")] -anvil-semver-check: anvil-semver-check-validate-prereqs +[script("pwsh", "-NoProfile")] +anvil-semver-check: anvil-semver-check-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' + $stableArgs = {{_anvil_stable_toolchain_args}} + $include = (& "{{ just_executable() }}" _anvil-impact-include affected) $commentFile = 'target/anvil/comments/semver.md' - if ($env:ANVIL_INCLUDE_AFFECTED -eq '--skip') { + if ($include -eq '--skip') { Write-Host 'anvil-semver-check: no affected packages; skipping' Remove-Item -LiteralPath $commentFile -ErrorAction SilentlyContinue 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 + $pkg = @(if ($include) { -split $include } else { '--workspace' }) + # 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 - # ("no library targets found"), and we want the same tolerance for + # (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). Cargo metadata represents unrestricted publication as + # null, `publish = false` as an empty array, and named-registry restrictions + # as a nonempty array. Only the empty array is non-publishable. $libPkgs = @{} - $meta = cargo metadata --no-deps --format-version 1 | ConvertFrom-Json + $metadataJson = & cargo @stableArgs metadata --no-deps --format-version 1 + if ($LASTEXITCODE -ne 0) { + Write-Error 'anvil-semver-check: cargo metadata failed' + exit $LASTEXITCODE + } + try { + $meta = $metadataJson | ConvertFrom-Json + } catch { + Write-Error "anvil-semver-check: could not parse cargo metadata output: $_" + 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 } } @@ -84,7 +101,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 } @@ -112,6 +129,7 @@ anvil-semver-check: anvil-semver-check-validate-prereqs exit 1 } $findings = New-Object System.Collections.Generic.List[string] + $inconclusive = New-Object System.Collections.Generic.List[string] foreach ($p in $packages) { # A manifest absent at its current workspace-relative path did # not exist on the PR base, so this is a newly-added crate (or a @@ -129,42 +147,60 @@ anvil-semver-check: anvil-semver-check-validate-prereqs } Write-Host "anvil-semver-check: $p (baseline $base)" - $output = (& cargo semver-checks --package $p --baseline-rev $base 2>&1) | Out-String - if ($LASTEXITCODE -ne 0) { - # A crate renamed in place has a manifest at the same path but - # no matching package name in the branch baseline ("package - # `` not found in "). Tolerate that -- but NOT - # when cargo-semver-checks appends "possibly due to errors:", - # which means the baseline manifest failed to parse (a real - # problem to surface, not a missing crate). + $outputLines = @(& cargo @stableArgs semver-checks --package $p --baseline-rev $base 2>&1) + $semverExit = $LASTEXITCODE + $output = ($outputLines | Out-String) + if ($semverExit -eq 100) { + # cargo-semver-checks 0.49+ reserves exit 100 for a completed + # check with deny-level SemVer findings. These are advisory. + Write-Host $output + $findings.Add('#### `' + $p + '`') | Out-Null + $findings.Add('') | Out-Null + $findings.Add('```') | Out-Null + foreach ($line in ($output.TrimEnd() -split "`r?`n")) { + $findings.Add($line.TrimEnd()) | Out-Null + } + $findings.Add('```') | Out-Null + $findings.Add('') | Out-Null + } elseif ($semverExit -eq 101) { + # Exit 101 means the check could not complete. Some conditions + # intentionally have no usable branch baseline: a package renamed + # in place, a baseline bin becoming a library, or a dependency + # version in the baseline becoming yanked after that commit landed. $packageMissing = $output.Contains(('package `{0}` not found in' -f $p)) $manifestParseFailed = $output.Contains('possibly due to errors:') $renamedPackage = $packageMissing -and -not $manifestParseFailed - # The candidate filter above proves the current package has a - # `lib` target, but the corresponding baseline target's `kind` - # may not contain `lib` (a bin->lib transition). - $baselineLibraryMissing = $output.Contains('no library targets found') - if ($renamedPackage -or $baselineLibraryMissing) { - Write-Host " $p has no comparable branch baseline; skipping (likely renamed or bin->lib transition)" -ForegroundColor Yellow + $baselineLibraryMissing = $output.Contains('has no lib target') -or + $output.Contains('no library targets found') + $baselineDependencyYanked = $output.Contains('is yanked') -and + ($output -match 'target[\\/]+semver-checks[\\/]+git-') + if ($renamedPackage -or $baselineLibraryMissing -or $baselineDependencyYanked) { + Write-Host " $p has no usable branch baseline; skipping (renamed, bin->lib, or yanked baseline dependency)" -ForegroundColor Yellow } else { - Write-Host $output - # Append a per-crate findings block. Using one-line-at-a-time - # appends keeps the markdown free of pwsh backtick-escape - # gymnastics (single-quoted literals + the natural `n join - # produce clean LF newlines and unambiguous triple-backticks). - $findings.Add('### `' + $p + '`') | Out-Null - $findings.Add('') | Out-Null - $findings.Add('```') | Out-Null + Write-Warning "anvil-semver-check: $p could not be checked (cargo-semver-checks exit 101):`n$output" + $inconclusive.Add('#### `' + $p + '` (exit 101)') | Out-Null + $inconclusive.Add('') | Out-Null + $inconclusive.Add('```') | Out-Null foreach ($line in ($output.TrimEnd() -split "`r?`n")) { - $findings.Add($line.TrimEnd()) | Out-Null + $inconclusive.Add($line.TrimEnd()) | Out-Null } - $findings.Add('```') | Out-Null - $findings.Add('') | Out-Null + $inconclusive.Add('```') | Out-Null + $inconclusive.Add('') | Out-Null + } + } elseif ($semverExit -ne 0) { + Write-Warning "anvil-semver-check: $p failed with unexpected cargo-semver-checks exit code ${semverExit}:`n$output" + $inconclusive.Add('#### `' + $p + '` (exit ' + $semverExit + ')') | Out-Null + $inconclusive.Add('') | Out-Null + $inconclusive.Add('```') | Out-Null + foreach ($line in ($output.TrimEnd() -split "`r?`n")) { + $inconclusive.Add($line.TrimEnd()) | Out-Null } + $inconclusive.Add('```') | Out-Null + $inconclusive.Add('') | Out-Null } } [System.IO.Directory]::CreateDirectory((Split-Path -Parent $commentFile)) | Out-Null - if ($findings.Count -gt 0) { + if ($findings.Count -gt 0 -or $inconclusive.Count -gt 0) { # Body starts with an HTML-comment marker so the ADO wiring can # locate the existing thread on subsequent runs (ADO has no # native "sticky comment header"; the marker is invisible to @@ -173,11 +209,23 @@ anvil-semver-check: anvil-semver-check-validate-prereqs # single source of truth across backends. $lines = New-Object System.Collections.Generic.List[string] $lines.Add('') | Out-Null - $lines.Add('## :warning: Potential breaking changes detected') | Out-Null - $lines.Add('') | Out-Null - $lines.Add('`cargo semver-checks` flagged the following on this PR. This is **informational** -- breaking changes between commits are expected; the major-version bump happens at release time, not on every PR.') | Out-Null - $lines.Add('') | Out-Null - foreach ($f in $findings) { $lines.Add($f) | Out-Null } + $lines.Add('## :warning: SemVer check advisory') | Out-Null + if ($findings.Count -gt 0) { + $lines.Add('') | Out-Null + $lines.Add('### Potential breaking changes') | Out-Null + $lines.Add('') | Out-Null + $lines.Add('`cargo semver-checks` flagged the following on this PR. This is **informational** -- breaking changes between commits are expected; the major-version bump happens at release time, not on every PR.') | Out-Null + $lines.Add('') | Out-Null + foreach ($f in $findings) { $lines.Add($f) | Out-Null } + } + if ($inconclusive.Count -gt 0) { + $lines.Add('') | Out-Null + $lines.Add('### Inconclusive comparisons') | Out-Null + $lines.Add('') | Out-Null + $lines.Add('`cargo semver-checks` could not complete the following comparisons. These failures are **informational** because an unbuildable baseline is not evidence of a breaking API change.') | Out-Null + $lines.Add('') | Out-Null + foreach ($f in $inconclusive) { $lines.Add($f) | Out-Null } + } $body = ($lines -join "`n") + "`n" Set-Content -LiteralPath $commentFile -Value $body -Encoding UTF8 -NoNewline Write-Host '' @@ -185,7 +233,7 @@ anvil-semver-check: anvil-semver-check-validate-prereqs } else { Remove-Item -LiteralPath $commentFile -ErrorAction SilentlyContinue } - # Advisory: always exit 0. cloud-workflow wiring posts/clears the PR comment. + # cargo-semver-checks findings and operational failures are advisory. exit 0 # Install prerequisites for the `anvil-semver-check` recipe. diff --git a/justfiles/anvil/checks/spellcheck.just b/justfiles/anvil/checks/spellcheck.just index 6f8dce82d..cdf2aef95 100644 --- a/justfiles/anvil/checks/spellcheck.just +++ b/justfiles/anvil/checks/spellcheck.just @@ -1,12 +1,16 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -# Managed by cargo-anvil. Update the corresponding template in the cargo-anvil -# crate unless the change is repository-specific. +# GENERATED BY cargo-anvil. DO NOT EDIT DIRECTLY. +# Update cargo-anvil and regenerate; repository-specific edits stop automatic updates. # Update behaviour: https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/updates.md # See https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/checks.md -# Modified tier. +# Unscoped. +# +# cargo-spellcheck checks the whole workspace and reads a repo-root `.spelling` +# dictionary -- repo-level inputs that cargo-delta does not map to any cargo +# package -- so this check cannot be safely impact-scoped and always runs. # # cargo-spellcheck reads a Hunspell-compatible dictionary file at the # path configured in spellcheck.toml (typically `extra_dictionaries = @@ -21,13 +25,9 @@ # leaving the path dangling. # Check spelling in source comments and documentation. -[script("pwsh")] +[script("pwsh", "-NoProfile")] anvil-spellcheck: anvil-spellcheck-validate-prereqs $ErrorActionPreference = 'Stop' - if ($env:ANVIL_INCLUDE_MODIFIED -eq '--skip') { - Write-Host 'anvil-spellcheck: no modified packages; skipping' - exit 0 - } $output_file = 'target/spelling.dic' $filtered_lines = @() if (Test-Path '.spelling') { @@ -42,19 +42,19 @@ anvil-spellcheck: anvil-spellcheck-validate-prereqs # ignores user-curated dictionaries (`extra_dictionaries`, custom # hunspell langs, etc.). if (Test-Path 'spellcheck.toml') { - cargo spellcheck --cfg spellcheck.toml check --code 1 + & cargo {{_anvil_stable_toolchain_args}} spellcheck --cfg spellcheck.toml check --code 1 } else { - cargo spellcheck check --code 1 + & cargo {{_anvil_stable_toolchain_args}} spellcheck check --code 1 } if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } -# cargo-spellcheck has a build-time libclang dependency; the system -# deps check runs first so adopters get a clear hint instead of a -# cryptic clang-sys build error mid-install. +# cargo-spellcheck has a source-build-time libclang dependency. The +# catalog installer checks it only immediately before a source install; +# successful prebuilt installation does not require libclang. # Install prerequisites for the `anvil-spellcheck` recipe. [group("anvil-setup")] -anvil-spellcheck-setup installer="install": anvil-system-deps-check (anvil-tool-cargo-spellcheck-install installer) +anvil-spellcheck-setup installer="install": (anvil-tool-cargo-spellcheck-install installer) # Validate prerequisites for the `anvil-spellcheck` recipe. [group("anvil-setup")] diff --git a/justfiles/anvil/checks/udeps.just b/justfiles/anvil/checks/udeps.just index 8cbaf7832..83770a4c9 100644 --- a/justfiles/anvil/checks/udeps.just +++ b/justfiles/anvil/checks/udeps.just @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -# Managed by cargo-anvil. Update the corresponding template in the cargo-anvil -# crate unless the change is repository-specific. +# GENERATED BY cargo-anvil. DO NOT EDIT DIRECTLY. +# Update cargo-anvil and regenerate; repository-specific edits stop automatic updates. # Update behaviour: https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/updates.md # See https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/checks.md @@ -25,11 +25,12 @@ # of {unused dep, unused dev-dep, dep that should be a dev-dep}. # Check required workspace packages for unused dependencies. -[script("pwsh")] -anvil-udeps: anvil-udeps-validate-prereqs +[script("pwsh", "-NoProfile")] +anvil-udeps: anvil-udeps-validate-prereqs anvil-impact $ErrorActionPreference = 'Stop' - if ($env:ANVIL_INCLUDE_REQUIRED -eq '--skip') { exit 0 } - $pkg = @(if ($env:ANVIL_INCLUDE_REQUIRED) { -split $env:ANVIL_INCLUDE_REQUIRED } else { '--workspace' }) + $include = (& "{{ just_executable() }}" _anvil-impact-include required) + if ($include -eq '--skip') { Write-Host 'anvil-udeps: no affected packages; skipping'; exit 0 } + $pkg = @(if ($include) { -split $include } else { '--workspace' }) # Pass 1: default targets (lib + bins) -- surfaces [dependencies] that # are only used by tests/benches/examples (should be dev-deps). & cargo '+{{ rust_nightly }}' udeps @pkg --all-features diff --git a/justfiles/anvil/container.just b/justfiles/anvil/container.just new file mode 100644 index 000000000..6e3a91d88 --- /dev/null +++ b/justfiles/anvil/container.just @@ -0,0 +1,1150 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# GENERATED BY cargo-anvil. DO NOT EDIT DIRECTLY. +# Update cargo-anvil and regenerate; repository-specific edits stop automatic updates. +# Update behaviour: https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/updates.md +# +# Containerized execution. `just anvil-container ` runs the given +# argv inside a pinned Linux image; everything else keeps running natively. +# Anvil recipes are reached by naming `just`, like any other command. +# There is no configuration file and no transparent routing: the container is +# reached through this recipe or not at all. +# +# The image tag *is* a hash of the inputs that define it, so the presence of a +# tag is proof that its contents are current -- a changed tool pin names a tag +# that cannot already exist, and a build follows. There is nothing to keep in +# sync and no staleness to detect. +# +# See https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/containers.md + +# The container engine, `docker` or `podman`. A host property, never +# committed. Set it in your environment: a `just anvil_container_engine=...` +# override would not reach the nested invocations that resolve the engine. +anvil_container_engine := env_var_or_default("ANVIL_CONTAINER_ENGINE", "docker") + +# Where the repository is mounted inside the container. +anvil_container_workdir := "/workspace" + +# Image and cache-volume prefix, derived from the repository directory name. +# Two checkouts with the same directory name share cache volumes; that is +# harmless (the caches are content-addressed by cargo) but worth knowing before +# `anvil-container-down` removes volumes another checkout is also using. +# +# Every run of non-alphanumerics collapses to a single `-`, and a trailing one +# is trimmed, because a repository name may not end in a separator or repeat +# `.`/`_`. Without that, a checkout in `ox-tools (copy)` yields a reference the +# engine rejects as malformed, from a directory name nobody would suspect. +anvil_container_name := trim_end_matches("anvil-" + replace_regex(lowercase(file_name(justfile_directory())), '[^a-z0-9]+', "-"), "-") + +# Resolve how to invoke the engine, as a pipe-separated command. +# +# There is deliberately no probe *between* engines: presence is not +# reachability, and a silent choice between two installed engines means two +# image stores and an unexplained rebuild. We check that the requested binary +# exists and let every other failure surface the engine's own diagnostic, which +# is more accurate than anything repeated here. +# +# The one fallback is Windows-specific and unambiguous: when the engine is not +# on the Windows PATH, try it inside the default WSL distribution. Installing +# Docker in WSL without Docker Desktop is a documented, common setup, and it +# leaves no Windows CLI behind, so without this fallback a correctly installed +# engine would be unreachable. Docker Desktop and Podman both ship a Windows +# CLI and are found on PATH, so they never take this path. +[private] +[script("pwsh", "-NoProfile")] +_anvil-container-engine: + $ErrorActionPreference = 'Stop' + $engine = '{{ replace(anvil_container_engine, "'", "''") }}' + if ($engine -ne 'docker' -and $engine -ne 'podman') { + Write-Error "anvil: ANVIL_CONTAINER_ENGINE must be 'docker' or 'podman', got '$engine'" + exit 1 + } + if (Get-Command $engine -ErrorAction SilentlyContinue) { + Write-Output $engine + exit 0 + } + # --exec, not --: `wsl.exe -- ` hands the rest of the command line to + # the distribution's default shell, which expands $NAME, splits on ;, and + # eats backslashes. Every argument we forward -- the repository path and + # the recipe's own arguments -- would cross that boundary unquoted. + if ($IsWindows -and (Get-Command wsl.exe -ErrorAction SilentlyContinue)) { + & wsl.exe --exec $engine --version *> $null + if ($LASTEXITCODE -eq 0) { + Write-Output "wsl.exe|--exec|$engine" + exit 0 + } + } + Write-Error "anvil: '$engine' was not found on PATH, and is not usable in the default WSL distribution. Install it, or set ANVIL_CONTAINER_ENGINE to the other engine. Setup: https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/containers.md" + exit 1 + +# Translate a host path into what the engine sees. +# +# Identical when the engine runs on this host. When it runs in WSL, a Windows +# path has to become its /mnt/... form or the daemon silently bind-mounts an +# empty directory -- a failure that surfaces much later, as a missing file +# inside the container. +[private] +[script("pwsh", "-NoProfile")] +_anvil-container-path host_path: + $ErrorActionPreference = 'Stop' + $engine = & '{{ replace(just_executable(), "'", "''") }}' _anvil-container-engine + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + $engine = "$engine".Trim() + if (-not $engine.StartsWith('wsl.exe|')) { + Write-Output '{{ replace(host_path, "'", "''") }}' + exit 0 + } + # --exec for the reason given above. It matters most here: through a shell, + # a path holding `$` loses it, and `wslpath -a` then makes the *truncated* + # path absolute and exits 0, so the guard below never fires and the wrong + # directory is bind-mounted. + $hostPath = '{{ replace(host_path, "'", "''") }}' + $translated = & wsl.exe --exec wslpath -a -u $hostPath + if ($LASTEXITCODE -ne 0) { + Write-Error "anvil: could not translate '$hostPath' for the engine running in WSL" + exit 1 + } + Write-Output $translated.Trim() + +# Verify the composed Dockerfile is present under the name the build uses. +# +# The engine derives the ignore file's name from the Dockerfile's: BuildKit +# reads `.dockerignore` and there is no flag to point it elsewhere. +# Anvil owns that artifact at the fixed path `.anvil/container/Dockerfile.dockerignore`, +# so the two names have to agree, and only one of them can move. A case variant +# is therefore refused rather than accommodated: building from `dockerfile` +# would silently use no ignore file at all, streaming the whole worktree into +# the build context and admitting inputs the tag does not cover. +# +# Also the one place that asserts the file exists: the tag's directory walk +# cannot, because a missing Dockerfile simply contributes nothing to the hash +# and yields a confident tag for an image that can never be built. +[private] +[script("pwsh", "-NoProfile")] +_anvil-container-dockerfile: + $ErrorActionPreference = 'Stop' + $dir = Join-Path '{{ replace(justfile_directory(), "'", "''") }}' '.anvil/container' + $entries = @(Get-ChildItem -LiteralPath $dir -File -Force -ErrorAction SilentlyContinue) + # -ceq because PowerShell's -eq on strings is case-insensitive, which would + # make the exact name indistinguishable from a variant on a filesystem that + # can hold both. + if (@($entries | Where-Object { $_.Name -ceq 'Dockerfile' }).Count -eq 1) { + Write-Output '.anvil/container/Dockerfile' + exit 0 + } + $variant = @($entries | Where-Object { $_.Name -ieq 'Dockerfile' })[0] + if ($variant) { + Write-Error "anvil: the container image input must be named exactly '.anvil/container/Dockerfile', but this repository has '.anvil/container/$($variant.Name)'. The engine reads the ignore file as '.dockerignore', and anvil maintains '.anvil/container/Dockerfile.dockerignore', so a differently-cased name would build with no ignore file. Rename it." + exit 1 + } + Write-Error 'anvil: container image input is missing: .anvil/container/Dockerfile' + exit 1 + +# Print the exec image reference for the current inputs, without building it. +# +# The tag is a SHA-256 over the image's declared inputs: the Dockerfile and its +# ignore file, the pinned toolchain, the optional hook, and the whole generated +# recipe tree -- because the image installs its tools by running +# `just anvil-setup`, whose dependency chain reaches the tier, group, check and +# tool recipes alike. Editing any of them can change what the image contains, so +# any of them can rename it. +# +# This is the only recipe that computes the reference; everything else asks it. +# It is public because a publisher needs the tag before there is an image to +# inspect: a pipeline that builds the image tags the result with exactly the +# reference a consumer will later compute, which is what lets presence be +# checked without a second source of truth. + +# Print the exec image reference for the current inputs, without building it. +[group("anvil-container")] +[script("pwsh", "-NoProfile")] +anvil-container-tag: + $ErrorActionPreference = 'Stop' + $repoRoot = '{{ replace(justfile_directory(), "'", "''") }}' + $dockerfile = & '{{ replace(just_executable(), "'", "''") }}' _anvil-container-dockerfile + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + $dockerfile = "$dockerfile".Trim() + $hookRel = '.anvil/container/hooks.ps1' + + # Both named rather than left to the walk, so that either one going missing + # is a hard failure at the check below. The walk cannot notice an absent + # file, and the ignore file's absence is the worse of the two: the build + # still succeeds, but BuildKit falls back to a root ignore file or none, so + # the context widens to files the digest does not hash and the tag stops + # covering what `COPY` puts in the image. + $inputs = @('rust-toolchain.toml', "$dockerfile.dockerignore") + $links = @() + # The declared input and every directory on the way to a walk root are + # checked here, because a walk only ever reports descendants: a link that + # *is* a root, or that is a parent of one, is traversed through and never + # appears in its own output. `COPY justfiles ./justfiles` copies from + # `justfiles`, not from `justfiles/anvil`, so a link at the parent is copied + # as a link while everything here reads through it -- the same divergence + # this guard exists to refuse, reached one level up. + foreach ($declared in @('rust-toolchain.toml', '.anvil', '.anvil/container', 'justfiles', 'justfiles/anvil')) { + $item = Get-Item -LiteralPath (Join-Path $repoRoot $declared) -Force -ErrorAction SilentlyContinue + if ($item -and ($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint)) { + $links += $item.FullName + } + } + # The declared inputs are text this tree owns, so their line endings are + # normalized before hashing and a CRLF checkout agrees with an LF one. + # Everything discovered by walking a directory is treated as text only when + # it is a `.just` recipe; anything else is hashed as the bytes the build + # context actually copies. + $declaredText = [System.Collections.Generic.HashSet[string]]::new( + [string[]]$inputs, [System.StringComparer]::Ordinal) + [void]$declaredText.Add($dockerfile) + [void]$declaredText.Add("$dockerfile.dockerignore") + [void]$declaredText.Add($hookRel) + # Everything under `.anvil/container/`, not a fixed list of three files. + # The Dockerfile is composed -- anvil owns regions inside it and the + # repository owns the gaps -- and a repository that adds a `COPY` in one of + # those gaps names a file that shapes the image: a corporate root CA, an + # install script, a patch. A replacement region from a downstream catalog + # does the same. Hashing only the three files anvil happens to know about + # would let any of them change the image under a reference that already + # resolves, which is precisely the hole this digest exists to close. + # + # The hook is picked up by the same walk. Its *output* is deliberately + # never hashed: a credential must not influence a tag. + # + # `.anvil-proposed` siblings are excluded. A region proposal is anvil's own + # review artifact, written beside its host when a template moves under a + # customized region; the build cannot see it and it cannot change what the + # image contains. Digesting it would rename the image for as long as a + # proposal sat undismissed, so two checkouts of one commit would disagree + # on the tag and a published image would stop resolving. + $containerRoot = Join-Path $repoRoot '.anvil/container' + if (Test-Path -LiteralPath $containerRoot) { + foreach ($file in Get-ChildItem -LiteralPath $containerRoot -Recurse -Force | + Where-Object { -not $_.Name.EndsWith('.anvil-proposed') }) { + if ($file.Attributes -band [System.IO.FileAttributes]::ReparsePoint) { $links += $file.FullName } + elseif (-not $file.PSIsContainer) { + $inputs += [System.IO.Path]::GetRelativePath($repoRoot, $file.FullName) -replace '\\', '/' + } + } + } + # Every generated recipe file. The image installs its tools by running + # `just anvil-setup`, and that dependency chain runs through the tier, + # group and check recipes before it reaches the install recipes in + # tools.just -- so the routing decides *whether* a tool is installed just + # as surely as tools.just decides *how*. Hashing only the install + # definitions would let a group drop a `-setup` dependency, changing the + # installed set, without renaming the image. + # + # This driver is included too. It is not circular -- the tag is derived + # from file text, and no file contains the tag -- and it belongs in the set + # because it passes the build arguments, the secret mounts and the hook's + # `Anvil-BuildSecrets` output into the build, all of which shape the result. + # Every file in the generated recipe tree, not only `*.just`. The build + # context admits the whole `justfiles/anvil/` directory (see the ignore + # file), so anything an adopter drops there is copied into the image. The + # catalog refuses to *own* a non-recipe file there, but a repository can + # still add one by hand, and a file that reaches the image without reaching + # the tag is precisely the hole this digest exists to close. Hashing what + # the context copies keeps the two sets identical by construction. + # + # -Force because Get-ChildItem omits hidden entries otherwise: a + # dot-prefixed file is copied like any other, and skipping it would let its + # edits ride under an unchanged tag -- and make Windows and Unix disagree. + # + # `.anvil-proposed` siblings are excluded here for the same reason as under + # `.anvil/container/`: this driver is itself an owned artifact, so a + # repository that customizes it gets the proposal written right here. + $recipeRoot = Join-Path $repoRoot 'justfiles/anvil' + if (Test-Path -LiteralPath $recipeRoot) { + foreach ($file in Get-ChildItem -LiteralPath $recipeRoot -Recurse -Force | + Where-Object { -not $_.Name.EndsWith('.anvil-proposed') }) { + if ($file.Attributes -band [System.IO.FileAttributes]::ReparsePoint) { $links += $file.FullName } + elseif (-not $file.PSIsContainer) { + $inputs += [System.IO.Path]::GetRelativePath($repoRoot, $file.FullName) -replace '\\', '/' + } + } + } + + # A symlink is refused rather than digested. The engine copies the link + # itself while any reading of it here follows it, so a retarget changes the + # image without changing a single byte the walk can see, and a link to a + # directory is not enumerated by the walk at all. Framing link text instead + # would have to work on Windows, where git materializes a symlink as an + # ordinary file unless the checkout was privileged, so the same commit would + # digest differently per platform. Anvil never creates one under these + # trees, so refusing costs nothing and closes the whole class. + if ($links.Count -gt 0) { + $named = ($links | ForEach-Object { [System.IO.Path]::GetRelativePath($repoRoot, $_) -replace '\\', '/' }) -join ', ' + Write-Error "anvil: the container image inputs must be regular files, but these are links: $named. The engine copies a link as a link while the image tag is computed from what it points at, so the image would not match its own reference. Replace them with regular files." + exit 1 + } + + # Hash a tagged stream rather than raw concatenation, so no rearrangement of + # names and contents can collide. Line endings are normalized once, here, so + # a CRLF checkout and an LF checkout agree on the tag. Ordinal sort and dedup: + # `Sort-Object -Unique` compares case-insensitively, which would silently drop + # one of two inputs differing only in case on the case-sensitive filesystem + # where the image is actually built. + # Length-prefix the path and the content rather than relying on newlines as + # separators. A bare `file\n\n\n` stream is not + # self-delimiting: content is arbitrary, so a file whose body contains + # "file\n\n" serializes identically to two files whose bodies + # split at that point. That makes distinct input sets nameable by one tag -- + # a hook that exists versus an ignore file whose body ends in the hook's + # path and body, for instance -- and the second state would silently reuse + # the first state's image. Byte counts cannot be forged by content. + # + # Content is hashed as BYTES, not as decoded text. `ReadAllText` decodes + # UTF-8 with a replacing fallback, so every invalid sequence becomes U+FFFD + # before it is hashed: a one-byte file of 0xFF and one of 0xFE both collapse + # to the same replacement character and produce the same tag, while `COPY` + # puts their real, different bytes in the image. That was unreachable while + # only `*.just` was hashed and became reachable the moment the input set + # widened to everything the build context copies -- which is precisely the + # class of file (a stray `.png`, a `.DS_Store`, a UTF-16 fragment) that the + # widening admitted. + # + # Line endings are still normalized, but only for the text this tree owns: + # a `.just` recipe and the declared inputs, which a CRLF checkout and an LF + # checkout must agree on. Normalizing bytes generally would reintroduce the + # same collision from the other direction. + $ordered = [System.Collections.Generic.SortedSet[string]]::new( + [string[]]$inputs, [System.StringComparer]::Ordinal) + # A file's git mode is part of what `COPY` puts in the image -- the + # executable bit, and whether the entry is a regular file or a symlink -- so + # a change that leaves the bytes alone still changes the image and must + # rename the tag. Git's index is the only source of that mode which answers + # identically on every platform: Windows has no executable bit, so reading + # it from the filesystem would make two checkouts of one commit disagree on + # the tag, and a published image would stop resolving for half the people + # who use it. + # + # Because the mode comes from the index, the index has to be able to answer + # for every input. Two states are refused rather than absorbed, since both + # would otherwise frame a mode of `-` and silently rename every image: + # git being unavailable, and an input the index does not know about. A + # missing git is not a degraded mode here -- it is the difference between + # two hosts computing different tags for identical bytes. + # + # The index is authoritative only while the working tree agrees with it. A + # mode change that has not been staged would be copied by the build and + # missed by the tag, so it is refused below too. + # + # quotePath=false so a non-ASCII path arrives verbatim rather than + # backslash-escaped, which would key the map on a spelling the walk never + # produces. + # + # Ordinal, like the sort and the dedup below: PowerShell's `@{}` folds case, + # so two paths differing only in case -- which git permits and the + # case-sensitive filesystem the image is built on can hold -- would collapse + # to one entry and both would be framed with whichever mode was stored last. + # + # Scoped to exactly what the digest hashes. `justfiles` would reach recipes + # outside `justfiles/anvil`, which neither the walk hashes nor the ignore + # file admits, so an unstaged `chmod` on one of those would refuse a build + # it cannot affect. + $indexMode = [System.Collections.Generic.Dictionary[string, string]]::new([System.StringComparer]::Ordinal) + $tracked = @('.anvil/container', 'justfiles/anvil', 'rust-toolchain.toml') + if (-not (Get-Command git -ErrorAction SilentlyContinue)) { + Write-Error 'anvil: git is required to compute the container image tag, because the image inputs are framed with the file modes git records. Without it two hosts would compute different tags for identical content. Install git, or set ANVIL_CONTAINER_ENGINE aside and build the image where git is available.' + exit 1 + } + $staged = & git -c core.quotePath=false -C $repoRoot ls-files --stage -- @tracked 2>$null + if ($LASTEXITCODE -ne 0) { + Write-Error "anvil: could not read the git index for the container image inputs (git ls-files exited $LASTEXITCODE). The image tag is framed with the modes git records, so it cannot be computed without them." + exit 1 + } + foreach ($entry in $staged) { + if ($entry -match '^(\d{6}) [0-9a-f]+ \d+\t(.+)$') { + $indexMode[$Matches[2]] = $Matches[1] + } + } + # An input git does not know about is framed as an ordinary file. Its mode + # cannot be read the same way on every platform, and it is not in any + # commit, so no other checkout can reproduce it and no published image can + # depend on it -- the reproducibility the index protects is a property of + # committed content. Refusing here instead would block the first container + # run of a freshly generated repository, before anything has been staged, + # which is the most common way the feature is met. + $untrackedMode = '100644' + # `--raw` reports the working-tree mode as its second field, so a + # mode-only change is visible even though the content is identical. On + # Windows core.fileMode is normally false and git reports no drift, + # which is correct: the filesystem has no bit to disagree with. + # + # A zero working-tree mode is a deletion: the path is in neither the + # build context nor the digest, so there is nothing to disagree about. + # Every other entry is present in the context and is compared against + # the mode the digest actually framed, which comes from `ls-files + # --stage` above. The raw index-side mode is not that mode -- an + # intent-to-add entry reports zero there while `ls-files` reports a real + # placeholder -- so comparing the two raw fields would both miss an + # executable `git add -N` file and reject an ordinary one. + $drift = & git -c core.quotePath=false -C $repoRoot diff --no-ext-diff --raw -- @tracked 2>$null + if ($LASTEXITCODE -ne 0) { + Write-Error "anvil: could not compare the working tree against the git index for the container image inputs (git diff exited $LASTEXITCODE). The tag is framed with index modes, and this query is what proves the working tree still matches them, so it cannot be skipped." + exit 1 + } + foreach ($entry in $drift) { + if ($entry -match '^:\d{6} (\d{6}) [0-9a-f]+ [0-9a-f]+ \S+\t(.+)$') { + $worktreeMode, $driftPath = $Matches[1], $Matches[2] + if ($worktreeMode -ne '000000' -and $indexMode.ContainsKey($driftPath) -and $indexMode[$driftPath] -ne $worktreeMode) { + Write-Error "anvil: '$driftPath' has mode $worktreeMode in the working tree, but the image tag was computed from mode $($indexMode[$driftPath]). The build copies the working tree, so the image would not match its own reference. Stage the change (git add) and re-run." + exit 1 + } + } + } + # ComputeHash rather than the static HashData: the latter arrived in .NET 5, + # and the prerequisite check accepts any PowerShell 7, including 7.0 on + # .NET Core 3.1 where the static overload does not exist. Failing there + # would be a MethodNotFound at tag time, before anything useful happened. + $sha = [System.Security.Cryptography.SHA256]::Create() + try { + foreach ($rel in $ordered) { + $path = Join-Path $repoRoot $rel + if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { + Write-Error "anvil: container image input is missing: $rel" + exit 1 + } + if ([System.IO.Path]::GetExtension($rel) -eq '.just' -or $declaredText.Contains($rel)) { + $content = [System.Text.Encoding]::UTF8.GetBytes( + ([System.IO.File]::ReadAllText($path) -replace "`r`n", "`n")) + } else { + $content = [System.IO.File]::ReadAllBytes($path) + } + # The whole git mode, not just the executable bit: `COPY` preserves + # a symlink as a symlink, while the walk above reads through it, so + # replacing a regular file with a link to identical bytes would + # otherwise keep the tag. A link is refused before this, so an + # untracked input at this point is a regular file. + # + # An untracked file has no index entry, and `git diff --raw` does + # not report one either, so the filesystem is the only thing that + # can answer for it. Reading it there is safe precisely because the + # file is in no commit: nothing shared depends on two platforms + # agreeing about it, which is the reason tracked modes come from the + # index instead. + # + # `test -x` rather than `FileInfo.UnixMode`, which needs the + # PSUnixFileStat experimental feature and is simply absent on + # PowerShell 7.0 -- the floor this recipe supports, and the reason + # ComputeHash is used below. A missing property would silently frame + # every untracked executable as an ordinary file. + $mode = if ($indexMode.ContainsKey($rel)) { + $indexMode[$rel] + } elseif ($IsWindows) { + # No executable bit exists to read, and none can be created. + $untrackedMode + } else { + & /usr/bin/env test -x $path + if ($LASTEXITCODE -eq 0) { '100755' } else { $untrackedMode } + } + $header = [System.Text.Encoding]::UTF8.GetBytes( + 'file ' + [System.Text.Encoding]::UTF8.GetByteCount($rel) + ' ' + $rel + ' ' + $mode + ' ' + $content.Length + ' ') + [void]$sha.TransformBlock($header, 0, $header.Length, $null, 0) + if ($content.Length -gt 0) { + [void]$sha.TransformBlock($content, 0, $content.Length, $null, 0) + } + } + [void]$sha.TransformFinalBlock([byte[]]::new(0), 0, 0) + $digest = $sha.Hash + } finally { + $sha.Dispose() + } + # 16 hex characters (64 bits) is far past any practical collision risk for a + # local image set, and keeps `docker images` readable. + $imageId = -join ($digest[0..7] | ForEach-Object { $_.ToString('x2') }) + Write-Output ('{{anvil_container_name}}:' + $imageId) + +# Resolve the exec image, building it if it is neither present nor resolvable. +# +# Three steps, in order: a local image under the computed tag, then the +# optional `Anvil-ResolveImage` hook (a registry, typically), then a build. +# Resolution comes before the NO_REBUILD guard because fetching a published +# image is not building one. +# +# ANVIL_CONTAINER_NO_REBUILD=1 fails instead of building, which is how a cache +# miss is told apart from a build failure. ANVIL_CONTAINER_NO_RESOLVE=1 skips +# the hook, so a query stays a query -- resolving can mean pulling gigabytes. +# ANVIL_CONTAINER_NO_CACHE=1 rebuilds a tag that already resolves, for the cases +# a content hash cannot see: a moved upstream package, or a base layer that +# changed behind its digest. It skips the hook too -- "ignore what is cached" +# has to mean the remote cache as well, or a rebuild would be undone by the +# next pull. +[private] +[script("pwsh", "-NoProfile")] +_anvil-container-image: + $ErrorActionPreference = 'Stop' + $engine = & '{{ replace(just_executable(), "'", "''") }}' _anvil-container-engine + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + $engine = "$engine".Trim() + $engineCmd = $engine -split '\|' + $engineExe = $engineCmd[0] + $enginePrefix = @($engineCmd | Select-Object -Skip 1) + + $repoRoot = '{{ replace(justfile_directory(), "'", "''") }}' + $dockerfile = & '{{ replace(just_executable(), "'", "''") }}' _anvil-container-dockerfile + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + $dockerfile = "$dockerfile".Trim() + $hookRel = '.anvil/container/hooks.ps1' + + $image = & '{{ replace(just_executable(), "'", "''") }}' anvil-container-tag + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + $image = "$image".Trim() + + if ($env:ANVIL_CONTAINER_NO_CACHE -ne '1') { + & $engineExe @enginePrefix image inspect $image *> $null + if ($LASTEXITCODE -eq 0) { + Write-Output $image + exit 0 + } + + # Nothing local. Give the optional hook a chance to fetch a published + # image built from these same inputs -- a registry, typically. + # + # The hook returns the reference it made available, and we run that + # reference rather than re-tagging it to the local name: a local tag + # asserts "built here from these inputs", and a fetched image only + # *claims* that, since the hash is over source files and cannot be + # re-derived from layers. Whether that claim holds is a property of the + # registry (immutable tags, restricted push), not of anything this + # recipe can check, so the reference stays honest about where it came + # from. + # + # Every failure here is non-fatal: a missing image, an expired + # credential and a broken hook all fall through to a build, which is + # slower but always correct. A publisher that has not yet caught up + # with a change must not stop the developer who made it. + $hookPath = Join-Path $repoRoot $hookRel + if ($env:ANVIL_CONTAINER_NO_RESOLVE -ne '1' -and (Test-Path -LiteralPath $hookPath -PathType Leaf)) { + # Dot-sourcing is inside the try as well: a hook with a syntax error, + # or one that throws while being loaded, must cost no more than a + # hook that resolves nothing. The recipe runs under + # `$ErrorActionPreference = 'Stop'`, so leaving the load outside + # would abort the run instead of falling through to a build. + $resolved = $null + try { + . $hookPath + if (Get-Command Anvil-ResolveImage -CommandType Function -ErrorAction SilentlyContinue) { + [Console]::Error.WriteLine("anvil: running Anvil-ResolveImage from $hookRel") + $resolved = @(Anvil-ResolveImage $image | Where-Object { $_ }) | Select-Object -Last 1 + } + } catch { + [Console]::Error.WriteLine("anvil: $hookRel failed: $($_.Exception.Message)") + $resolved = $null + } + if (-not [string]::IsNullOrWhiteSpace($resolved)) { + $resolved = ([string]$resolved).Trim() + # A presence check, not a verification: `image inspect` proves + # something is tagged with that reference, not that its contents + # match the digest the tag claims. Trusting the hook is the + # contract -- this only keeps a reference the hook reported but + # never fetched from failing later, under `--pull=never`, a long + # way from the cause. + & $engineExe @enginePrefix image inspect $resolved *> $null + if ($LASTEXITCODE -eq 0) { + [Console]::Error.WriteLine("anvil: resolved $resolved") + Write-Output $resolved + exit 0 + } + [Console]::Error.WriteLine( + "anvil: Anvil-ResolveImage reported '$resolved' but no such image is present locally") + } + [Console]::Error.WriteLine("anvil: nothing resolved; building locally") + } + } + + # Checked outside the cache guard, so the two variables compose: a caller + # that has NO_CACHE exported would otherwise fall straight through to a + # from-scratch build, which is exactly what NO_REBUILD exists to prevent -- + # and `anvil-container-status`, which sets it, would spend minutes building + # from a query. + if ($env:ANVIL_CONTAINER_NO_REBUILD -eq '1') { + # Still report the reference: a caller that asked not to build is + # usually asking *which* image is missing. + Write-Output $image + [Console]::Error.WriteLine("anvil: $image is not present or not current, and ANVIL_CONTAINER_NO_REBUILD=1") + exit 1 + } + + # Build-time credentials come from the optional hook, never from a committed + # file. Values are handed to BuildKit by environment variable name, so they + # stay out of the host's process command line, and BuildKit keeps them out of + # every image layer. An empty value is fatal: BuildKit would mount an empty + # secret, the build would install a reduced tool set and exit 0, and the + # result would be tagged with the same hash a credentialed build produces -- + # so every later run would reuse the broken image. + $secretArgs = @() + $secretEnv = @() + $hookPath = Join-Path $repoRoot $hookRel + if (Test-Path -LiteralPath $hookPath -PathType Leaf) { + # Fail-closed, unlike the resolve hook: a build that cannot mint its + # credentials must stop, not proceed to produce a reduced image. The + # try exists only so the cause is named -- loading a hook with a syntax + # error would otherwise surface as a bare parser error with no hint + # that a hook was involved. + try { + . $hookPath + } catch { + Write-Error "anvil: failed to load ${hookRel}: $($_.Exception.Message)" + exit 1 + } + if (Get-Command Anvil-BuildSecrets -CommandType Function -ErrorAction SilentlyContinue) { + [Console]::Error.WriteLine("anvil: running Anvil-BuildSecrets from $hookRel") + try { + # Take the last emitted object, not the whole stream: a hook + # that writes progress with `Write-Output` would otherwise hand + # back an array whose `.Secrets` is silently $null. + $hook = @(Anvil-BuildSecrets | Where-Object { $_ }) | Select-Object -Last 1 + } catch { + Write-Error "anvil: Anvil-BuildSecrets failed: $($_.Exception.Message)" + exit 1 + } + $secrets = if ($null -ne $hook) { $hook.Secrets } else { $null } + # A defined `Anvil-BuildSecrets` that yields nothing is the hazard this + # guard exists for, not a hook opting out: secrets are the only + # thing the phase can contribute, so an empty return means the mint + # failed quietly. A hook with no build-time credentials simply does + # not define the function. + if ($null -eq $secrets -or $secrets.Keys.Count -eq 0) { + Write-Error "anvil: Anvil-BuildSecrets returned no secrets; omit the function if the build needs none" + exit 1 + } + foreach ($id in $secrets.Keys) { + if ([string]::IsNullOrWhiteSpace($secrets[$id])) { + Write-Error "anvil: Anvil-BuildSecrets returned an empty value for secret '$id'" + exit 1 + } + $name = "ANVIL_SECRET_$id" + Set-Item -LiteralPath "Env:$name" -Value $secrets[$id] + $secretEnv += $name + $secretArgs += "id=$id,env=$name" + } + [Console]::Error.WriteLine("anvil: build secrets: $($secrets.Keys -join ', ')") + } + } + + try { + # Progress goes to stderr: callers capture this recipe's stdout to learn + # the image reference, so anything else written there becomes part of it. + [Console]::Error.WriteLine("anvil: building $image (inputs changed or first run)") + # The engine may not share this host's filesystem view, so the context + # and the Dockerfile are given in its terms rather than ours. + $engineRoot = & '{{ replace(just_executable(), "'", "''") }}' _anvil-container-path $repoRoot + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + $engineRoot = "$engineRoot".Trim() + # Pinned, not inferred from the host. The Dockerfile installs amd64 + # toolchains and verifies amd64 checksums, so an arm host would resolve + # the multi-arch base to arm64 and fail late with an exec-format error. + # It also keeps the identity scheme honest: without this, two hosts of + # different architecture compute the same tag for different images. + $buildCmd = @('build', '--platform', 'linux/amd64', '--file', "$engineRoot/$dockerfile", '--tag', $image) + # BuildKit reads `.dockerignore` on its own; buildah reads + # only a context-root ignore file and needs to be pointed at ours. Named + # rather than probed, because the flag is rejected outright by the engine + # that does not take it, and an unscoped context streams the whole + # worktree -- `target/` included -- on every build. + if ($engineCmd[-1] -eq 'podman') { $buildCmd += @('--ignorefile', "$engineRoot/$dockerfile.dockerignore") } + if ($env:ANVIL_CONTAINER_NO_CACHE -eq '1') { $buildCmd += '--no-cache' } + foreach ($secret in $secretArgs) { $buildCmd += @('--secret', $secret) } + $buildCmd += $engineRoot + # BuildKit is required for --secret; docker enables it by default from + # 23.0 but an older daemon silently ignores the flag, so ask explicitly. + $env:DOCKER_BUILDKIT = '1' + # WSLENV exports the named variables into the WSL environment, which is + # where the engine reads a secret's value from when it runs there. + if ($engineExe -eq 'wsl.exe') { + $bridged = @('DOCKER_BUILDKIT/u') + ($secretEnv | ForEach-Object { "$_/u" }) + $env:WSLENV = (@($env:WSLENV) + $bridged | Where-Object { $_ }) -join ':' + } + & $engineExe @enginePrefix @buildCmd | ForEach-Object { [Console]::Error.WriteLine($_) } + if ($LASTEXITCODE -ne 0) { + # Build secrets are the part of this path engines implement least + # consistently -- podman on Windows cannot mount one at all, and + # fails with a path error that names neither the secret nor the + # engine. Say so once, rather than leaving that to be rediscovered. + if ($secretArgs.Count -gt 0) { + [Console]::Error.WriteLine( + "anvil: the build passed $($secretArgs.Count) secret(s) from $hookRel. " + + "If the failure above is about a temp file or a path, the engine may not support " + + "build secrets on this host; see docs/design/containers.md.") + } + exit $LASTEXITCODE + } + } finally { + foreach ($name in $secretEnv) { Remove-Item -LiteralPath "Env:$name" -ErrorAction SilentlyContinue } + } + + Write-Output $image + +# Run a command inside the pinned Linux image. +# +# The tokens after the recipe name are the argv, executed verbatim in the +# image. Anvil recipes are reached by naming `just` like any other command. +# +# just anvil-container just anvil-pr # a tier +# just anvil-container cargo build # any other command +# just anvil-container # interactive shell +# +# ANVIL_IN_CONTAINER is set inside the image, so a nested invocation runs the +# command on the spot and the work happens exactly once. + +# Run a command inside the pinned Linux image (no argument: a shell). +[group("anvil-container")] +[script("pwsh", "-NoProfile")] +anvil-container *command: + $ErrorActionPreference = 'Stop' + # `*command` joins its parts with spaces, so the string is split back into + # argv here. Whitespace is the only separator, so an argument containing a + # space does not survive; pass such a value through the environment. + $argv = @('{{ replace(command, "'", "''") }}' -split '\s+' | Where-Object { $_ }) + if ($env:ANVIL_IN_CONTAINER -eq '1') { + if ($argv.Count -eq 0) { + # The no-argument form asks for a shell in the image, and this is + # that shell. Nothing runs, so exiting 0 would report success for a + # request that was not carried out. + [Console]::Error.WriteLine("anvil: already inside the container; run the command directly") + exit 1 + } + # `just` resolves to the binary running this tree rather than to PATH: + # ANVIL_IN_CONTAINER is a documented control a developer can set on a + # host, and a caller who invoked `just` by absolute path with its + # directory off PATH would otherwise fail here. + $exe = if ($argv[0] -eq 'just') { '{{ replace(just_executable(), "'", "''") }}' } else { $argv[0] } + & $exe @($argv | Select-Object -Skip 1) + exit $LASTEXITCODE + } + + $engine = & '{{ replace(just_executable(), "'", "''") }}' _anvil-container-engine + + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + + $engine = "$engine".Trim() + $engineCmd = $engine -split '\|' + $engineExe = $engineCmd[0] + $enginePrefix = @($engineCmd | Select-Object -Skip 1) + $image = (& '{{ replace(just_executable(), "'", "''") }}' _anvil-container-image) | Select-Object -Last 1 + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + + $repoRoot = '{{ replace(justfile_directory(), "'", "''") }}' + $engineRoot = & '{{ replace(just_executable(), "'", "''") }}' _anvil-container-path $repoRoot + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + $engineRoot = "$engineRoot".Trim() + + # Map the caller's working directory to its in-container equivalent so + # relative paths keep working from a subdirectory. `invocation_directory()` + # would be wrong here: with `cygpath` on PATH it reports a Cygwin-style + # path, which shares no prefix with the native `justfile_directory()` above. + # `GetRelativePath` then walks out with `..` segments and the run is placed + # outside the mount, so it fails on a path that does not exist in the + # container. The `_native` form is the one that agrees with the root. + $rel = [System.IO.Path]::GetRelativePath($repoRoot, '{{ replace(invocation_directory_native(), "'", "''") }}') -replace '\\', '/' + # `-eq '..'` or a `../` prefix, not a bare `..` prefix: `GetRelativePath` + # returns a plain relative path for anything inside the root, so a directory + # named `..data` -- the shape a projected mount uses -- is inside and must + # not be refused. + if ($rel -eq '..' -or $rel.StartsWith('../')) { + Write-Error "anvil: run this from inside the repository; $rel is outside $repoRoot" + exit 1 + } + $containerCwd = if ($rel -eq '.' -or [string]::IsNullOrEmpty($rel)) { + '{{anvil_container_workdir}}' + } else { + '{{anvil_container_workdir}}/' + $rel + } + + $interactive = $argv.Count -eq 0 + $runArgs = @('run', '--rm', '--platform', 'linux/amd64') + $runArgs += $interactive ? '-it' : '-i' + $runArgs += @('-v', "${engineRoot}:{{anvil_container_workdir}}") + + # A checkout whose `.git` is a file keeps its real git directory elsewhere: + # a linked worktree points into the main clone, `--separate-git-dir` and a + # submodule point somewhere else again. The path recorded there is a host + # path that does not exist inside the container, so git resolves neither + # HEAD nor origin/main. Mount the common git directory and replace the + # checkout's `.git` with one naming that mount; the `commondir` file in a + # worktree's entry is relative, so it resolves under it. + # + # The predicate is the shape of `.git`, not whether the git directory + # differs from the common one: `--separate-git-dir` redirects without + # differing, and testing for a difference skips it and leaves git pointed at + # a path the container cannot see. + # + # The redirection lives in the checkout rather than in GIT_DIR/GIT_WORK_TREE + # so that it stays scoped to it. Those variables are ambient: every process + # in the container inherits them, and a git command run elsewhere -- `git + # init` in a test's scratch directory -- would operate on this repository + # instead of its own. + # + # An ordinary clone keeps its git directory inside the checkout, where the + # bind mount already carries it, and takes none of this. + # + # Guarded on git being present: the run path needs it only to answer this + # question, and a host with a working engine but no git on PATH keeps + # working rather than failing on a call it does not need. + $gitFile = $null + if (Get-Command git -ErrorAction SilentlyContinue) { + $gitDir = & git rev-parse --git-dir 2>$null + $gitCommon = & git rev-parse --git-common-dir 2>$null + if ($LASTEXITCODE -eq 0 -and $gitDir -and $gitCommon -and + (Test-Path -LiteralPath (Join-Path $repoRoot '.git') -PathType Leaf)) { + $gitDirAbs = (Resolve-Path -LiteralPath $gitDir).Path + $gitCommonAbs = (Resolve-Path -LiteralPath $gitCommon).Path + $rel = [System.IO.Path]::GetRelativePath($gitCommonAbs, $gitDirAbs) -replace '\\', '/' + # One mount has to carry both directories, so the git directory must + # sit under the common one. `git worktree` always places it there and + # a redirect without a separate worktree entry makes the two equal; + # anything else cannot be expressed as a single mount, and emitting a + # path that climbs out of it would fail inside the container instead. + if ($rel -eq '.') { + # git-dir equal to common-dir means there is no separate worktree + # entry: a `--separate-git-dir` clone or a submodule. A submodule + # also records `core.worktree`, resolved relative to the git + # directory, and remounting the git directory moves what that + # relative path resolves to. The value belongs to the repository, + # so it is not ours to rewrite, and git inside the container + # would otherwise report a worktree that does not exist -- which + # is why this is refused here rather than left to fail there. + $coreWorktree = & git -C $repoRoot config --get core.worktree 2>$null + if ($LASTEXITCODE -eq 0 -and $coreWorktree) { + Write-Error "anvil: this checkout sets core.worktree ($coreWorktree) and keeps its git directory outside itself, which is the shape of a submodule. The git directory has to be remounted for the container, and that moves what core.worktree resolves against, so git inside the container would not find the worktree. Run the container from the superproject, or from an ordinary clone." + exit 1 + } + $containerGitDir = '/anvil/gitdir' + } elseif ($rel.StartsWith('../') -or [System.IO.Path]::IsPathRooted($rel)) { + Write-Error "anvil: this checkout's git directory ($gitDirAbs) is not inside its common git directory ($gitCommonAbs), so the two cannot be mounted as one tree. Run the container from an ordinary clone or a git worktree checkout." + exit 1 + } else { + $containerGitDir = "/anvil/gitdir/$rel" + } + $engineGitCommon = & '{{ replace(just_executable(), "'", "''") }}' _anvil-container-path $gitCommonAbs + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + $engineGitCommon = "$engineGitCommon".Trim() + # LF and no trailing newline: git parses this file strictly. + $gitFile = Join-Path ([System.IO.Path]::GetTempPath()) "anvil-gitfile-$([System.Guid]::NewGuid().ToString('N'))" + [System.IO.File]::WriteAllText($gitFile, "gitdir: $containerGitDir`n") + $engineGitFile = & '{{ replace(just_executable(), "'", "''") }}' _anvil-container-path $gitFile + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + $engineGitFile = "$engineGitFile".Trim() + $runArgs += @('-v', "${engineGitCommon}:/anvil/gitdir") + $runArgs += @('-v', "${engineGitFile}:{{anvil_container_workdir}}/.git:ro") + } + } + + # Cache only what is content-addressed: the downloaded registry and the git + # checkouts. Deliberately NOT $CARGO_HOME or $RUSTUP_HOME themselves -- + # those hold the installed tools and toolchains, and a named volume is + # populated from the image only when it is first created. Mounting them + # would pin the first image's binaries over every later one, so a tool bump + # would change the tag, build a new image, and still run the old tools. + $runArgs += @('-v', '{{anvil_container_name}}-cargo-registry:/usr/local/cargo/registry') + $runArgs += @('-v', '{{anvil_container_name}}-cargo-git:/usr/local/cargo/git') + # Match the caller's uid/gid on Linux. Without this everything the run + # writes under the bind mount -- target/, generated files -- lands as root + # on the host, and the next native cargo build or git clean fails with + # EACCES a long way from the cause. + # + # The question is how the *selected engine* maps users, not what the host + # OS is. Docker Desktop on Windows and macOS maps ownership itself, and + # `id` is not there to ask. But an engine living inside WSL does not: the + # repository is reached over `/mnt/c`, which presents every file as the + # distribution's default user while the container runs as root, so git + # refuses the checkout as dubiously owned and every history-reading check + # fails. That engine is Linux however Windows the host is, so it takes the + # Linux path -- asking the distribution for the uid, since the host has no + # `id`. + $mapsOwnership = ($IsWindows -or $IsMacOS) -and -not $engine.StartsWith('wsl.exe|') + if (-not $mapsOwnership) { + if ($engine.StartsWith('wsl.exe|')) { + $hostUid = (& wsl.exe --exec id -u); $hostGid = (& wsl.exe --exec id -g) + } else { + $hostUid = (id -u); $hostGid = (id -g) + } + if ($LASTEXITCODE -eq 0 -and $hostUid -ne '0') { + $runArgs += @('--user', "${hostUid}:${hostGid}") + # That uid has no passwd entry, so the engine leaves HOME as `/`. + # Anything falling back to $HOME for a cache then writes to a + # read-only root and fails a long way from the cause. + $runArgs += @('-e', 'HOME=/tmp') + } + } + $runArgs += @('-e', 'ANVIL_IN_CONTAINER=1') + + # Run-time credentials come from the optional hook. Forwarded by NAME, never + # as NAME=VALUE: the engine copies the value out of the environment it + # already inherits, so a credential never appears in the host's process + # command line, where endpoint telemetry records and retains it for far + # longer than a short-lived token is meant to live. + # $forwardedEnv is every name passed with -e; $hookEnv is the subset this + # process set, and so the subset it must unset again. + $forwardedEnv = @() + $hookEnv = @() + + # anvil-aprz queries the GitHub advisory API, which allows 60 requests an + # hour unauthenticated -- less than a full tier needs. Unauthenticated is + # not a degraded-but-working mode: `cargo aprz deps` sleeps until the quota + # resets rather than failing, so a containerized tier blocks for up to an + # hour with no way to opt out. Authentication is what makes the check + # terminate, not what makes it fast. + # + # Resolve the token exactly as the recipe does natively -- GITHUB_TOKEN + # first, then the gh CLI's stored token -- so a containerized run + # authenticates for the same developers a native run does. + # + # An already-exported GITHUB_TOKEN is forwarded whatever the command is: + # that is exact parity, since a native run exposes it to every process the + # shell spawns too. Deriving one from `gh` is different -- it manufactures a + # credential the developer did not put in this environment, and PID 1's + # environment is inherited by every build script and proc macro in the + # container, where natively the recipe would mint it in its own process. So + # it is derived only when the command is known to read the variable, or when + # there is no command at all: an interactive session can run anything, and + # refusing there would reintroduce the silent hour-long stall on a tier the + # developer runs from inside the shell. + # `gh auth token` is non-interactive and never opens a prompt. + # + # The predicate is the variable itself rather than the name of a check, so + # the driver stays generic: a catalog that adds another GitHub-authenticated + # check is covered without touching this recipe. + # + # Set here and passed by NAME, so the value never reaches the host's + # process command line, and unset again with the hook's variables below. + if (-not $env:GITHUB_TOKEN -and (Get-Command gh -ErrorAction SilentlyContinue)) { + $needsToken = $argv.Count -eq 0 + # Only a `just` command can be planned, and planning is the only way to + # know whether what runs reads the variable. Anything else keeps the + # environment it was given: a manufactured credential reaches every + # process in the container, so an unknown command does not earn one. + if (-not $needsToken -and $argv[0] -eq 'just') { + # A dry run has no side effects, and a target that cannot be planned + # (a typo, a recipe needing arguments) yields nothing, so the run + # fails on its own terms rather than on a missing token. + # + # A plan covers the bodies just runs itself, not the body of a + # recipe that one of them launches as a child process. The unscoped + # tier wrapper launches its tier that way, so planning + # `anvil-scheduled` shows the wrapper and none of the checks + # underneath it. Follow each nested target a plan names, or a + # wrapped tier reads as needing nothing and runs unauthenticated. + $plan = '' + $targets = [System.Collections.Generic.List[object]]::new() + $targets.Add([string[]]@($argv | Select-Object -Skip 1)) + $planned = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal) + for ($i = 0; $i -lt $targets.Count; $i++) { + $target = [string[]]$targets[$i] + # A recipe reachable twice is planned once, and a body naming + # itself terminates. + if (-not $planned.Add(($target -join ' '))) { continue } + # The same executable that launched this tree, for the reason + # every other nested call uses it: a caller invoking `just` by + # absolute path with its directory off PATH would otherwise fail + # here. That failure is silent, because an empty plan reads as + # "does not need a token" -- so anvil-aprz would run + # unauthenticated in an image with no gh of its own and block on + # the rate limit for up to an hour. + $step = '' + try { + $step = (& '{{ replace(just_executable(), "'", "''") }}' --dry-run @target 2>&1 | + ForEach-Object { $_.ToString() }) -join "`n" + } catch { + $step = '' + } + $plan = "$plan`n$step" + # A launched recipe appears as a quoted argument to `just`. + foreach ($nested in [regex]::Matches($step, "'(_anvil-[^'\s]+)'")) { + $targets.Add([string[]]@($nested.Groups[1].Value)) + } + } + $needsToken = $plan -match 'GITHUB_TOKEN' + } + if ($needsToken) { + $ghToken = $null + try { $ghToken = (gh auth token --hostname github.com 2>$null) } catch { $ghToken = $null } + if ($ghToken -and $ghToken.Trim()) { + Set-Item -LiteralPath 'Env:GITHUB_TOKEN' -Value $ghToken.Trim() + $hookEnv += 'GITHUB_TOKEN' + } + } + } + if ($env:GITHUB_TOKEN) { + $forwardedEnv += 'GITHUB_TOKEN' + $runArgs += @('-e', 'GITHUB_TOKEN') + } + + # The recipe contract's own inputs. These are read by generated checks -- + # `anvil-pr-title` reads PR_TITLE, `_anvil-base-ref` reads BASE_REF and its + # CI equivalents, and `anvil-impact` reads ANVIL_IMPACT to decide whether to + # compute scoping, consume a downloaded cache, or skip -- so dropping them at + # the boundary makes the same command mean different things inside and out. + # anvil-pr-title is the sharp case: with PR_TITLE unset it exits 0 with a + # skip notice, so a title a native run rejects passes in a container and the + # tier still reports green. ANVIL_IMPACT is the other: a CI group job exports + # `consume`, and a container that did not inherit it would recompute scoping + # from a diff instead of trusting the artifact the group downloaded. + # + # Forwarded by name and only when set, so an unset variable stays unset + # rather than arriving as an empty string, which several of these treat as + # a value. + foreach ($name in @( + 'PR_TITLE', + 'BASE_REF', 'GITHUB_BASE_REF', 'SYSTEM_PULLREQUEST_TARGETBRANCH', + 'ANVIL_IMPACT')) { + if ((Test-Path -LiteralPath "Env:$name") -and -not [string]::IsNullOrEmpty((Get-Item -LiteralPath "Env:$name").Value)) { + $forwardedEnv += $name + $runArgs += @('-e', $name) + } + } + + $hookPath = Join-Path $repoRoot '.anvil/container/hooks.ps1' + if (Test-Path -LiteralPath $hookPath -PathType Leaf) { + # Fail-closed like Anvil-BuildSecrets: a run that cannot obtain its + # credentials fails inside the container in a far less obvious way. + try { + . $hookPath + } catch { + Write-Error "anvil: failed to load .anvil/container/hooks.ps1: $($_.Exception.Message)" + exit 1 + } + if (Get-Command Anvil-RunEnv -CommandType Function -ErrorAction SilentlyContinue) { + [Console]::Error.WriteLine("anvil: running Anvil-RunEnv from .anvil/container/hooks.ps1") + try { + $hook = @(Anvil-RunEnv | Where-Object { $_ }) | Select-Object -Last 1 + } catch { + Write-Error "anvil: Anvil-RunEnv failed: $($_.Exception.Message)" + exit 1 + } + $hookVars = if ($null -ne $hook) { $hook.Env } else { $null } + if ($null -eq $hookVars -or $hookVars.Keys.Count -eq 0) { + Write-Error "anvil: Anvil-RunEnv returned no variables; omit the function if the run needs none" + exit 1 + } + foreach ($name in $hookVars.Keys) { + if ([string]::IsNullOrWhiteSpace($hookVars[$name])) { + Write-Error "anvil: Anvil-RunEnv returned an empty value for '$name'" + exit 1 + } + Set-Item -LiteralPath "Env:$name" -Value $hookVars[$name] + $hookEnv += $name + $forwardedEnv += $name + $runArgs += @('-e', $name) + } + # Names only, never values: a hook with a broad idea of what to + # forward should be visible, since everything inside the + # container can read it -- including third-party build scripts. + [Console]::Error.WriteLine("anvil: forwarding env: $($hookVars.Keys -join ', ')") + } + } + + try { + # --pull=never: the reference names content that is already here, either + # built locally or fetched by the resolve hook, so a miss is a bug to + # surface rather than an invitation to fetch something unrelated. + $runArgs += @('--pull=never', '-w', $containerCwd, $image) + if (-not $interactive) { $runArgs += $argv } + # WSLENV exports the forwarded names into the WSL environment, which is + # where the engine reads their values from when it runs there. Without + # it, `-e NAME` reaches an engine that cannot see NAME and forwards + # nothing, leaving the variable unset inside the container. + # + # It is not restored afterwards because there is nothing to restore to: + # `just` runs a [script(...)] recipe as its own pwsh process, so this + # assignment dies with that process and never reaches the caller's + # shell. The `finally` below unsets the credential names for hygiene + # within this process, not to protect the parent. + if ($engineExe -eq 'wsl.exe' -and $forwardedEnv.Count -gt 0) { + $env:WSLENV = (@($env:WSLENV) + ($forwardedEnv | ForEach-Object { "$_/u" }) | Where-Object { $_ }) -join ':' + } + & $engineExe @enginePrefix @runArgs + exit $LASTEXITCODE + } finally { + foreach ($name in $hookEnv) { Remove-Item -LiteralPath "Env:$name" -ErrorAction SilentlyContinue } + if ($gitFile) { Remove-Item -LiteralPath $gitFile -Force -ErrorAction SilentlyContinue } + } + +# Report the engine, the exec image, and whether it is present and current. +# +# The tag embeds the hash of the image's inputs, so "absent" and "out of date" +# are the same condition and are reported as one. + +# Report the engine, the exec image, and whether it is present and current. +[group("anvil-container")] +[script("pwsh", "-NoProfile")] +anvil-container-status: + $ErrorActionPreference = 'Stop' + $engine = & '{{ replace(just_executable(), "'", "''") }}' _anvil-container-engine + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + $engine = "$engine".Trim() + Write-Output ("engine: " + ($engine -replace '\|', ' ')) + Write-Output "workdir: {{anvil_container_workdir}}" + + # NO_REBUILD turns the resolve into a pure query: report the state instead + # of silently spending several minutes building from a status command. + # NO_RESOLVE is the same argument applied to the hook, which would otherwise + # pull gigabytes to answer a question about the local machine. + # + # Compute the tag first and let it fail loudly. It is fatal for a reason a + # query cannot paper over -- a declared input is missing -- and reporting + # that as "not present locally" would be a lie: the next run cannot build + # it either. + $image = (& '{{ replace(just_executable(), "'", "''") }}' anvil-container-tag) | Select-Object -Last 1 + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + Write-Output "image: $image" + + $env:ANVIL_CONTAINER_NO_REBUILD = '1' + $env:ANVIL_CONTAINER_NO_RESOLVE = '1' + # And explicitly *not* NO_CACHE. A caller who exported it is asking the next + # build to ignore the layer cache, which is a statement about building -- + # but it also makes the resolver skip the local `image inspect` + # short-circuit, so a present image would be reported absent by a command + # that is only ever asking what is on this machine. + $env:ANVIL_CONTAINER_NO_CACHE = $null + & '{{ replace(just_executable(), "'", "''") }}' _anvil-container-image *> $null + if ($LASTEXITCODE -eq 0) { + Write-Output "status: present and current" + exit 0 + } + + # A cache miss and an unreachable daemon both make `image inspect` fail, and + # reporting the second as the first tells a developer to expect a build that + # will not start either. Ask the engine whether it is answering at all: only + # then is absence the honest reading. + $engineCmd = $engine -split '\|' + & $engineCmd[0] @($engineCmd | Select-Object -Skip 1) version *> $null + if ($LASTEXITCODE -ne 0) { + Write-Output "status: unknown -- the engine is not responding (is the daemon running?)" + exit 1 + } + Write-Output "status: not present locally (the next run resolves or builds it)" + exit 0 + +# Only the download caches are volumes, so this discards fetched crates and git +# checkouts and nothing else: the next run re-fetches them, and the image's own +# tools are untouched. To discard the image instead, set +# ANVIL_CONTAINER_NO_CACHE=1 for a single run. + +# Remove this repository's cache volumes. The image is left in place. +[group("anvil-container")] +[script("pwsh", "-NoProfile")] +anvil-container-down: + $ErrorActionPreference = 'Stop' + $engine = & '{{ replace(just_executable(), "'", "''") }}' _anvil-container-engine + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + $engine = "$engine".Trim() + $engineCmd = $engine -split '\|' + $engineExe = $engineCmd[0] + $enginePrefix = @($engineCmd | Select-Object -Skip 1) + # Report a teardown that did not happen. $ErrorActionPreference does not + # cover native commands, so a non-serving engine would otherwise print a + # connection error per volume and still exit 0 -- and this recipe is the + # only way to clear a cache volume, so a caller that scripts teardown must + # be able to tell that it failed. `-f` already exits 0 for a volume that + # does not exist, so this cannot fire spuriously. + $failed = @() + foreach ($vol in @('{{anvil_container_name}}-cargo-registry', '{{anvil_container_name}}-cargo-git')) { + & $engineExe @enginePrefix volume rm -f $vol + if ($LASTEXITCODE -ne 0) { $failed += $vol } + } + if ($failed.Count -gt 0) { + Write-Error ("anvil: could not remove: " + ($failed -join ', ')) + exit 1 + } + exit 0 diff --git a/justfiles/anvil/groups/pr-fast.just b/justfiles/anvil/groups/pr-fast.just index 1194fe733..5671b2b2b 100644 --- a/justfiles/anvil/groups/pr-fast.just +++ b/justfiles/anvil/groups/pr-fast.just @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -# Managed by cargo-anvil. Update the corresponding template in the cargo-anvil -# crate unless the change is repository-specific. +# GENERATED BY cargo-anvil. DO NOT EDIT DIRECTLY. +# Update cargo-anvil and regenerate; repository-specific edits stop automatic updates. # Update behaviour: https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/updates.md # See https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/checks.md @@ -23,8 +23,7 @@ anvil-pr-fast: anvil-pr-fast-validate-prereqs \ anvil-audit \ anvil-udeps \ anvil-semver-check \ - anvil-external-types \ - anvil-aprz + anvil-external-types # Group-level setup + validate-prereqs # @@ -35,6 +34,7 @@ anvil-pr-fast: anvil-pr-fast-validate-prereqs \ # Install prerequisites for the `anvil-pr-fast` recipe. [group("anvil-setup")] anvil-pr-fast-setup installer="install": \ + (anvil-tool-cargo-delta-install installer) \ (anvil-fmt-setup installer) \ (anvil-clippy-setup installer) \ (anvil-cargo-sort-setup installer) \ @@ -49,12 +49,12 @@ anvil-pr-fast-setup installer="install": \ (anvil-audit-setup installer) \ (anvil-udeps-setup installer) \ (anvil-semver-check-setup installer) \ - (anvil-external-types-setup installer) \ - (anvil-aprz-setup installer) + (anvil-external-types-setup installer) # Validate prerequisites for the `anvil-pr-fast` recipe. [group("anvil-setup")] anvil-pr-fast-validate-prereqs: \ + anvil-tool-cargo-delta-validate-prereqs \ anvil-fmt-validate-prereqs \ anvil-clippy-validate-prereqs \ anvil-cargo-sort-validate-prereqs \ @@ -69,5 +69,4 @@ anvil-pr-fast-validate-prereqs: \ anvil-audit-validate-prereqs \ anvil-udeps-validate-prereqs \ anvil-semver-check-validate-prereqs \ - anvil-external-types-validate-prereqs \ - anvil-aprz-validate-prereqs + anvil-external-types-validate-prereqs diff --git a/justfiles/anvil/groups/pr-msrv.just b/justfiles/anvil/groups/pr-msrv.just new file mode 100644 index 000000000..d35d01ac8 --- /dev/null +++ b/justfiles/anvil/groups/pr-msrv.just @@ -0,0 +1,23 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# GENERATED BY cargo-anvil. DO NOT EDIT DIRECTLY. +# Update cargo-anvil and regenerate; repository-specific edits stop automatic updates. +# Update behaviour: https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/updates.md + +# See https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/checks.md + +# Run pull request compatibility tests under the declared MSRV. +[group("anvil")] +anvil-pr-msrv: anvil-pr-msrv-validate-prereqs anvil-msrv-test + +# Install prerequisites for the `anvil-pr-msrv` recipe. +[group("anvil-setup")] +anvil-pr-msrv-setup installer="install": \ + (anvil-tool-cargo-delta-install installer) \ + (anvil-msrv-test-setup installer) + +# Validate prerequisites for the `anvil-pr-msrv` recipe. +[group("anvil-setup")] +anvil-pr-msrv-validate-prereqs: \ + anvil-tool-cargo-delta-validate-prereqs \ + anvil-msrv-test-validate-prereqs diff --git a/justfiles/anvil/groups/pr-mutants.just b/justfiles/anvil/groups/pr-mutants.just index b6910d0dc..ebcbc7b41 100644 --- a/justfiles/anvil/groups/pr-mutants.just +++ b/justfiles/anvil/groups/pr-mutants.just @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -# Managed by cargo-anvil. Update the corresponding template in the cargo-anvil -# crate unless the change is repository-specific. +# GENERATED BY cargo-anvil. DO NOT EDIT DIRECTLY. +# Update cargo-anvil and regenerate; repository-specific edits stop automatic updates. # Update behaviour: https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/updates.md # See https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/checks.md @@ -12,8 +12,12 @@ anvil-pr-mutants: anvil-pr-mutants-validate-prereqs anvil-mutants-diff # Install prerequisites for the `anvil-pr-mutants` recipe. [group("anvil-setup")] -anvil-pr-mutants-setup installer="install": (anvil-mutants-diff-setup installer) +anvil-pr-mutants-setup installer="install": \ + (anvil-tool-cargo-delta-install installer) \ + (anvil-mutants-diff-setup installer) # Validate prerequisites for the `anvil-pr-mutants` recipe. [group("anvil-setup")] -anvil-pr-mutants-validate-prereqs: anvil-mutants-diff-validate-prereqs +anvil-pr-mutants-validate-prereqs: \ + anvil-tool-cargo-delta-validate-prereqs \ + anvil-mutants-diff-validate-prereqs diff --git a/justfiles/anvil/groups/pr-runtime-analysis.just b/justfiles/anvil/groups/pr-runtime-analysis.just index 5f3b4ebfb..7301a8f35 100644 --- a/justfiles/anvil/groups/pr-runtime-analysis.just +++ b/justfiles/anvil/groups/pr-runtime-analysis.just @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -# Managed by cargo-anvil. Update the corresponding template in the cargo-anvil -# crate unless the change is repository-specific. +# GENERATED BY cargo-anvil. DO NOT EDIT DIRECTLY. +# Update cargo-anvil and regenerate; repository-specific edits stop automatic updates. # Update behaviour: https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/updates.md # See https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/checks.md @@ -17,6 +17,7 @@ anvil-pr-runtime-analysis: anvil-pr-runtime-analysis-validate-prereqs \ # Install prerequisites for the `anvil-pr-runtime-analysis` recipe. [group("anvil-setup")] anvil-pr-runtime-analysis-setup installer="install": \ + (anvil-tool-cargo-delta-install installer) \ (anvil-miri-setup installer) \ (anvil-careful-setup installer) \ (anvil-loom-setup installer) \ @@ -25,6 +26,7 @@ anvil-pr-runtime-analysis-setup installer="install": \ # Validate prerequisites for the `anvil-pr-runtime-analysis` recipe. [group("anvil-setup")] anvil-pr-runtime-analysis-validate-prereqs: \ + anvil-tool-cargo-delta-validate-prereqs \ anvil-miri-validate-prereqs \ anvil-careful-validate-prereqs \ anvil-loom-validate-prereqs \ diff --git a/justfiles/anvil/groups/pr-slow.just b/justfiles/anvil/groups/pr-slow.just index 2592afc66..ddce5d420 100644 --- a/justfiles/anvil/groups/pr-slow.just +++ b/justfiles/anvil/groups/pr-slow.just @@ -1,37 +1,37 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -# Managed by cargo-anvil. Update the corresponding template in the cargo-anvil -# crate unless the change is repository-specific. +# GENERATED BY cargo-anvil. DO NOT EDIT DIRECTLY. +# Update cargo-anvil and regenerate; repository-specific edits stop automatic updates. # Update behaviour: https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/updates.md # See https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/checks.md -# pr-slow is the single PR-tier group for everything that takes more -# than ~30s per crate (tests, stricter runtimes, mutation testing). -# It's internally split into three sub-recipes so individual concerns -# can be invoked locally without dragging the others along: +# pr-slow is the local umbrella for everything that takes more than ~30s +# per crate (tests, MSRV tests, stricter runtimes, mutation testing). Its +# groups can be invoked individually without invoking the other groups: # -# slow1: tests + coverage (replaces the former pr-test group) -# slow2: stricter-runtime correctness (miri, careful) -# slow3: mutation testing +# pr-test: tests + coverage +# pr-msrv: all-target tests under the declared MSRV +# pr-runtime-analysis: stricter-runtime correctness (miri, careful) +# pr-mutants: mutation testing # -# Cloud workflows run pr-slow as ONE job per OS leg -- the sub-recipes run -# sequentially within. This trades per-leg wall-clock for fewer -# orchestration jobs and a flatter PR check graph. Individual sub- -# recipes are runnable on their own locally: +# Cloud workflows run these groups as independent parallel jobs. +# The umbrella runs them sequentially only when invoked locally: # # $ just anvil-pr-test # tests + coverage only +# $ just anvil-pr-msrv # MSRV all-target tests only # $ just anvil-pr-runtime-analysis # miri + careful only # $ just anvil-pr-mutants # mutants only # Run the slow pull request checks. [group("anvil")] -anvil-pr-slow: anvil-pr-slow-validate-prereqs anvil-pr-test anvil-pr-runtime-analysis anvil-pr-mutants +anvil-pr-slow: anvil-pr-slow-validate-prereqs anvil-pr-test anvil-pr-msrv anvil-pr-runtime-analysis anvil-pr-mutants # Install prerequisites for the `anvil-pr-slow` recipe. [group("anvil-setup")] anvil-pr-slow-setup installer="install": \ (anvil-pr-test-setup installer) \ + (anvil-pr-msrv-setup installer) \ (anvil-pr-runtime-analysis-setup installer) \ (anvil-pr-mutants-setup installer) @@ -39,5 +39,6 @@ anvil-pr-slow-setup installer="install": \ [group("anvil-setup")] anvil-pr-slow-validate-prereqs: \ anvil-pr-test-validate-prereqs \ + anvil-pr-msrv-validate-prereqs \ anvil-pr-runtime-analysis-validate-prereqs \ anvil-pr-mutants-validate-prereqs diff --git a/justfiles/anvil/groups/pr-test.just b/justfiles/anvil/groups/pr-test.just index 618ac8801..508dfaa8a 100644 --- a/justfiles/anvil/groups/pr-test.just +++ b/justfiles/anvil/groups/pr-test.just @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -# Managed by cargo-anvil. Update the corresponding template in the cargo-anvil -# crate unless the change is repository-specific. +# GENERATED BY cargo-anvil. DO NOT EDIT DIRECTLY. +# Update cargo-anvil and regenerate; repository-specific edits stop automatic updates. # Update behaviour: https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/updates.md # See https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/checks.md @@ -16,6 +16,7 @@ anvil-pr-test: anvil-pr-test-validate-prereqs \ # Install prerequisites for the `anvil-pr-test` recipe. [group("anvil-setup")] anvil-pr-test-setup installer="install": \ + (anvil-tool-cargo-delta-install installer) \ (anvil-llvm-cov-setup installer) \ (anvil-doc-test-setup installer) \ (anvil-examples-setup installer) @@ -23,6 +24,7 @@ anvil-pr-test-setup installer="install": \ # Validate prerequisites for the `anvil-pr-test` recipe. [group("anvil-setup")] anvil-pr-test-validate-prereqs: \ + anvil-tool-cargo-delta-validate-prereqs \ anvil-llvm-cov-validate-prereqs \ anvil-doc-test-validate-prereqs \ anvil-examples-validate-prereqs diff --git a/justfiles/anvil/groups/scheduled-advisories.just b/justfiles/anvil/groups/scheduled-advisories.just index 4197553ff..d47875620 100644 --- a/justfiles/anvil/groups/scheduled-advisories.just +++ b/justfiles/anvil/groups/scheduled-advisories.just @@ -1,14 +1,22 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -# Managed by cargo-anvil. Update the corresponding template in the cargo-anvil -# crate unless the change is repository-specific. +# GENERATED BY cargo-anvil. DO NOT EDIT DIRECTLY. +# Update cargo-anvil and regenerate; repository-specific edits stop automatic updates. # Update behaviour: https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/updates.md # See https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/checks.md +# Full-workspace backstop: routed through _anvil-unscoped so +# ANVIL_IMPACT=off is set before the check dependencies run, regardless of how +# the group is invoked. The group does not recompute impact and therefore does +# not require cargo-delta. + # Run the scheduled advisory checks. [group("anvil")] -anvil-scheduled-advisories: anvil-scheduled-advisories-validate-prereqs \ +anvil-scheduled-advisories: (_anvil-unscoped "scheduled-advisories") + +[private] +_anvil-scheduled-advisories: anvil-scheduled-advisories-validate-prereqs \ anvil-deny \ anvil-audit \ anvil-aprz \ diff --git a/justfiles/anvil/groups/scheduled-exhaustive.just b/justfiles/anvil/groups/scheduled-exhaustive.just index c0e8b7457..9cfd91c1f 100644 --- a/justfiles/anvil/groups/scheduled-exhaustive.just +++ b/justfiles/anvil/groups/scheduled-exhaustive.just @@ -1,14 +1,22 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -# Managed by cargo-anvil. Update the corresponding template in the cargo-anvil -# crate unless the change is repository-specific. +# GENERATED BY cargo-anvil. DO NOT EDIT DIRECTLY. +# Update cargo-anvil and regenerate; repository-specific edits stop automatic updates. # Update behaviour: https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/updates.md # See https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/checks.md +# Full-workspace backstop: routed through _anvil-unscoped so +# ANVIL_IMPACT=off is set before the check dependencies run, regardless of how +# the group is invoked. The group does not recompute impact and therefore does +# not require cargo-delta. + # Run the scheduled exhaustive checks. [group("anvil")] -anvil-scheduled-exhaustive: anvil-scheduled-exhaustive-validate-prereqs \ +anvil-scheduled-exhaustive: (_anvil-unscoped "scheduled-exhaustive") + +[private] +_anvil-scheduled-exhaustive: anvil-scheduled-exhaustive-validate-prereqs \ anvil-mutants-full \ anvil-cargo-hack \ anvil-bench diff --git a/justfiles/anvil/groups/scheduled-runtime-analysis.just b/justfiles/anvil/groups/scheduled-runtime-analysis.just index 3681c469f..f443eb5c8 100644 --- a/justfiles/anvil/groups/scheduled-runtime-analysis.just +++ b/justfiles/anvil/groups/scheduled-runtime-analysis.just @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -# Managed by cargo-anvil. Update the corresponding template in the cargo-anvil -# crate unless the change is repository-specific. +# GENERATED BY cargo-anvil. DO NOT EDIT DIRECTLY. +# Update cargo-anvil and regenerate; repository-specific edits stop automatic updates. # Update behaviour: https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/updates.md # See https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/checks.md @@ -12,9 +12,17 @@ # and adds the three stricter miri profiles (tree-borrows, strict- # provenance, race-coverage) which are too expensive for PR. +# Full-workspace backstop: routed through _anvil-unscoped so +# ANVIL_IMPACT=off is set before the check dependencies run, regardless of how +# the group is invoked. The group does not recompute impact and therefore does +# not require cargo-delta. + # Run the scheduled runtime analysis. [group("anvil")] -anvil-scheduled-runtime-analysis: anvil-scheduled-runtime-analysis-validate-prereqs \ +anvil-scheduled-runtime-analysis: (_anvil-unscoped "scheduled-runtime-analysis") + +[private] +_anvil-scheduled-runtime-analysis: anvil-scheduled-runtime-analysis-validate-prereqs \ anvil-miri \ anvil-miri-tree-borrows \ anvil-miri-strict-provenance \ diff --git a/justfiles/anvil/groups/scheduled-test.just b/justfiles/anvil/groups/scheduled-test.just index 8644adae3..d60f03f5a 100644 --- a/justfiles/anvil/groups/scheduled-test.just +++ b/justfiles/anvil/groups/scheduled-test.just @@ -1,16 +1,26 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -# Managed by cargo-anvil. Update the corresponding template in the cargo-anvil -# crate unless the change is repository-specific. +# GENERATED BY cargo-anvil. DO NOT EDIT DIRECTLY. +# Update cargo-anvil and regenerate; repository-specific edits stop automatic updates. # Update behaviour: https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/updates.md # See https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/checks.md # Scheduled groups +# Scheduled groups are the full-workspace backstop for PR-tier impact scoping, +# so route through _anvil-unscoped: it exports ANVIL_IMPACT=off +# before the check dependencies run, so the group is full-workspace regardless +# of how it is invoked (CI, `just anvil-scheduled`, or +# `just anvil-scheduled-test` directly). Because these groups never recompute +# the impact set, they no longer need cargo-delta as a prerequisite. + # Run the scheduled tests. [group("anvil")] -anvil-scheduled-test: anvil-scheduled-test-validate-prereqs \ +anvil-scheduled-test: (_anvil-unscoped "scheduled-test") + +[private] +_anvil-scheduled-test: anvil-scheduled-test-validate-prereqs \ anvil-llvm-cov \ anvil-doc-test \ anvil-examples diff --git a/justfiles/anvil/helpers.just b/justfiles/anvil/helpers.just index 7a43df92c..c18dd37ac 100644 --- a/justfiles/anvil/helpers.just +++ b/justfiles/anvil/helpers.just @@ -1,84 +1,77 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -# Managed by cargo-anvil. Update the corresponding template in the cargo-anvil -# crate unless the change is repository-specific. +# GENERATED BY cargo-anvil. DO NOT EDIT DIRECTLY. +# Update cargo-anvil and regenerate; repository-specific edits stop automatic updates. # Update behaviour: https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/updates.md # See https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/checks.md # -# Each check belongs to one of four buckets, which determines how it -# interprets the impact env vars emitted by the cargo-delta impact step -# in cloud workflows: +# Each check belongs to one of four buckets, which determines how it scopes +# its work to the packages impacted by a change: # # - "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 only admits or skips the recipe; it +# does not forward package arguments. Each admitted tool owns its +# normal input domain. A recipe may invoke that tool once (cargo +# heather, cargo sort) or fan out one bounded child per workspace +# member (cargo each). # -# - "affected": run on the affected set (modified ∪ reverse-deps -# within the workspace). The check's underlying tool takes -# --package; we splice ANVIL_INCLUDE_AFFECTED into the cargo -# invocation, defaulting to --workspace for local invocations where -# no env var is set. +# - "affected": run on the affected set (modified ∪ reverse-deps within the +# workspace). The tool takes --package; the recipe splices the resolved +# package list into the cargo invocation, defaulting to --workspace when +# the tier is unscoped. # # - "required": run on the required set (affected ∪ workspace-internal -# transitive deps). Same splice/default pattern as affected, but -# keyed on ANVIL_INCLUDE_REQUIRED. Used for checks whose tool -# resolves through the dep graph (cargo doc → intra-doc links; -# cargo hack → feature powerset; cargo udeps → unused-deps). -# -# - "unscoped": always run, no env var reference. External-input -# checks (deny, audit, aprz) and PR-context checks (pr-title) live -# here. Scheduled-exhaustive recipes (mutants-full) are also unscoped -# by design. -# -# 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 -# "--workspace" when the env var is empty or unset. -# -# Preparation contract: when a recipe reaches the cargo call, the -# env var is one of: -# -# * unset or "" - local run, or a cloud workflow (e.g. -# scheduled) that leaves the tier -# unscoped; the recipe substitutes -# "--workspace". An empty string is -# NOT $null, so we test truthiness -# rather than `?? "--workspace"` -# (which would only catch $null and -# splat an empty arg cargo rejects). -# * "--package A@v --package B@v" - emitted by the cloud-workflow impact step -# when the tier has members. Packages are -# version-qualified cargo specs so they -# resolve uniquely even when a like-named -# crate is also a transitive dependency. -# * "--skip" - emitted by the cloud-workflow impact step when -# the tier is empty (recipe exits 0) -# -# This lets the simple recipes splat the var directly with -# & cargo X @(if ($env:ANVIL_INCLUDE_AFFECTED) { -split $env:ANVIL_INCLUDE_AFFECTED } else { '--workspace' }) ... -# 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. -# -# Every recipe whose body uses multi-line conditionals or env-var -# splicing is annotated with [script("pwsh")]. pwsh is preinstalled on +# transitive deps). Same splice/default as affected, for checks whose tool +# resolves through the dep graph (cargo doc -> intra-doc links; cargo hack +# -> feature powerset; cargo udeps -> unused-deps). +# +# - "unscoped": always run. External-input checks (deny, audit, aprz) and +# PR-context checks (pr-title) live here, as do checks whose inputs +# include repo-level files cargo-delta maps to no package (the +# workspace-level README template via readme-check, the root `.spelling` +# via spellcheck); scheduled-exhaustive recipes (mutants-full) are +# unscoped by design. +# +# Each scoped check depends on `anvil-impact` (which produces the +# target/anvil/impact cache) and captures its category's scope into a local +# variable by calling the shared resolver: +# +# $include = (& "{{ just_executable() }}" _anvil-impact-include ) +# +# _anvil-impact-include reads that cache both locally and in cloud workflows +# (where the group job downloaded the target/anvil/impact artifact), so the +# same code path runs everywhere -- no scoping is threaded between jobs via +# environment variables. `$include` is one of: +# +# * "--workspace" (affected/required) or "" (modified) - the tier is +# unscoped: ANVIL_IMPACT=off, or no impact cache is present. Recipes splat +# "--workspace" (an empty string is NOT $null, so recipes test truthiness +# rather than `?? "--workspace"`, which would splat an empty arg cargo +# rejects). +# * "--package A@v --package B@v" - the tier has members. Packages are +# version-qualified cargo specs so they resolve uniquely even when a +# like-named crate is also a transitive dependency. +# * "--skip" - the tier is empty for this change (recipe exits 0). +# +# This keeps the per-recipe boilerplate to a single capture, a one-line skip +# guard, and the cargo invocation. Modified-tier recipes never forward +# `$include`; they only check the skip sentinel and then let their command +# determine its normal input domain. +# +# Every recipe whose body uses multi-line conditionals or array-splat +# splicing is annotated with [script("pwsh", "-NoProfile")]. pwsh is preinstalled on # Windows (since Windows 10), on GH/ADO hosted Linux + Windows # runners, and installable on macOS via Homebrew or the upstream # installer. We chose pwsh over bash because just's shebang dispatch # requires `cygpath` on Windows (only on PATH from inside Git Bash), -# while [script("pwsh")] works from plain PowerShell with no PATH +# while [script("pwsh", "-NoProfile")] works from plain PowerShell with no PATH # augmentation. We require pwsh 7+ (enforced via _anvil-require pwsh) # for consistent semantics of the array-splat splice and the modern # operators used across these recipes. # -# Every recipe that runs a command is annotated with [script("pwsh")], +# Every recipe that runs a command is annotated with [script("pwsh", "-NoProfile")], # including single-command ones (cargo deny check, cargo audit). They do # NOT inherit the adopter's default shell: just defaults to `sh`, which is # absent on Windows and varies across environments, so relying on it makes @@ -103,9 +96,9 @@ # 3. GITHUB_BASE_REF -- GitHub PR builds. Unqualified target branch name. # 4. origin/main, then origin/master -- local / non-PR fallback. # Prints ONLY the resolved ref to stdout so callers can capture it: -# pwsh : $base = (& {{just_executable()}} _anvil-base-ref) +# pwsh : $base = (& "{{ just_executable() }}" _anvil-base-ref) # bash : base="$(just _anvil-base-ref)" -[script("pwsh")] +[script("pwsh", "-NoProfile")] _anvil-base-ref: $ErrorActionPreference = 'Stop' if ($env:BASE_REF) { @@ -130,97 +123,21 @@ _anvil-base-ref: Write-Error 'anvil-base-ref: cannot resolve a base ref. Set BASE_REF, or ensure origin/main or origin/master exists.' exit 1 -# _anvil-impact-format: single source of truth for turning a cargo-delta -# impact report into the `--package ...` string the checks.just -# recipes consume. Shared by both impact steps -# (github/impact-action.yml, ado/steps/impact.yml) so the formatting -# logic lives in exactly one place rather than being duplicated (and -# subtly diverging) across two shell dialects. -# -# Arguments: -# tier - one of "modified" / "affected" / "required". Selected -# from the cargo-delta JSON (which uses TitleCase keys). -# impactJson - path to the file the caller produced via -# `cargo delta impact --format json`. -# -# Output (stdout, exactly one line so callers can capture it): -# * "--package name@version --package name2@version2 ..." when the -# tier has members. Each package is a version-qualified cargo spec: -# the version (read from `cargo metadata --no-deps`, i.e. workspace -# members only) disambiguates a workspace member from a like-named -# transitive dependency pulled in at a different version, which a -# bare `-p name` cannot do ("multiple packages named name"). -# * "--skip" when the tier is empty (recipes treat this as a no-op). -# Diagnostics (dropped unknown names) go to STDERR so they never -# pollute the captured value. -# -# Why pwsh + ConvertFrom-Json rather than jq + read: cargo-delta and -# cargo-metadata both emit JSON, and parsing structured data through a -# tab-delimited `IFS` read is fragile -- e.g. tab is an IFS-whitespace -# character, so an empty interior field (a package with no lib target) -# coalesces with its delimiters and silently shifts every later field. -# Working with objects sidesteps that entire class of bug. -# -# Usage: -# bash : val="$(just _anvil-impact-format affected "$impact_json")" -# pwsh : $val = (& {{just_executable()}} _anvil-impact-format affected $impactJson) -[script("pwsh")] -_anvil-impact-format tier impactJson: +# Run a private recipe with impact scoping disabled. +# +# The only way to reach a whole dependency tree with an environment variable: +# `just` runs each dependency as its own process, and a dependency-only +# recipe's body executes after its dependencies, so exporting from there is +# too late. Invoking `_anvil-` as a child process makes every check +# below it inherit the setting. +# +# The justfile is named explicitly so the child resolves the same file the +# wrapper was defined in, rather than whatever an upward search from the +# working directory happens to find. +[private] +[script("pwsh", "-NoProfile")] +_anvil-unscoped name: $ErrorActionPreference = 'Stop' - $tier = '{{tier}}' - $impactJson = '{{impactJson}}' - # cargo-delta keys its impact tiers in TitleCase. - $tierKey = switch ($tier) { - 'modified' { 'Modified' } - 'affected' { 'Affected' } - 'required' { 'Required' } - default { - Write-Error "anvil impact: unknown tier '$tier' (expected modified/affected/required)" - exit 1 - } - } - # Build two lookups from workspace members: - # * $byName: exact cargo package name (may contain hyphens) -> pkg - # * $byLib: library target name (snake_case) -> pkg - # cargo-delta reports *library* names (snake_case), but for crates - # whose package name differs from the lib name (e.g. `cargo-anvil` - # vs `cargo_anvil`) we must translate back to the package name cargo - # accepts. Bin-only crates have no lib target; cargo-delta reports - # those by their package name, caught by $byName. - $meta = cargo metadata --no-deps --format-version 1 | ConvertFrom-Json - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - $byName = @{} - $byLib = @{} - foreach ($p in $meta.packages) { - $byName[$p.name] = $p - foreach ($t in $p.targets) { - if ($t.kind -contains 'lib') { $byLib[$t.name] = $p } - } - } - $impact = Get-Content -LiteralPath $impactJson -Raw | ConvertFrom-Json - $names = @($impact.$tierKey) - $specs = [System.Collections.Generic.List[string]]::new() - foreach ($name in $names) { - if ([string]::IsNullOrWhiteSpace($name)) { continue } - $p = if ($byName.ContainsKey($name)) { - $byName[$name] - } elseif ($byLib.ContainsKey($name)) { - $byLib[$name] - } else { - $null - } - if ($null -eq $p) { - # Names cargo-delta emits that aren't valid workspace - # packages (e.g. directory-leaf ambiguities like `ffi` / - # `ffi_build` in deeply nested workspaces) are dropped with a - # warning rather than failing the whole build. - [Console]::Error.WriteLine("anvil impact: dropping unknown package '$name' from $tier set") - continue - } - $specs.Add("--package $($p.name)@$($p.version)") - } - if ($specs.Count -eq 0) { - Write-Output '--skip' - } else { - Write-Output ($specs -join ' ') - } + $env:ANVIL_IMPACT = 'off' + & '{{ replace(just_executable(), "'", "''") }}' --justfile '{{ replace(justfile(), "'", "''") }}' '_anvil-{{ replace(name, "'", "''") }}' + exit $LASTEXITCODE \ No newline at end of file diff --git a/justfiles/anvil/impact.just b/justfiles/anvil/impact.just new file mode 100644 index 000000000..c9e7db74e --- /dev/null +++ b/justfiles/anvil/impact.just @@ -0,0 +1,629 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# GENERATED BY cargo-anvil. DO NOT EDIT DIRECTLY. +# Update cargo-anvil and regenerate; repository-specific edits stop automatic updates. + +# See https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/local.md +# +# Impact analysis lives in one place: the `anvil-impact` recipe. It runs +# cargo-delta against the working tree vs. the base ref and writes durable +# artifacts under target/anvil/impact/ that the per-crate check recipes +# consume via _anvil-impact-include. The same recipe runs locally and in +# cloud workflows: a cloud impact job runs `just anvil-impact` and uploads +# target/anvil/impact/ as an artifact; each downstream group job downloads +# it, so the checks read the cache instead of recomputing. +# +# The base ref is resolved by the shared _anvil-base-ref helper (helpers.just, +# also reused by anvil-mutants-diff). The impact -> `--package name@version` +# tier projection is _anvil-impact-format, defined in THIS file next to its +# sole caller; this recipe orchestrates the snapshots + caching around both so +# the exact same projection is used locally and in CI. +# +# Escape hatch: set ANVIL_IMPACT=off in the environment that invokes `just` +# to disable scoping entirely (no git, no snapshot, no cargo-delta). Because +# `just` runs dependencies in that same environment, the guard is honored +# even when these recipes run as a `: anvil-impact` dependency of a check. +# This is how the scheduled tier stays full-workspace. +# +# Set ANVIL_IMPACT=consume when an authoritative target/anvil/impact cache is +# already present and must be trusted verbatim -- e.g. a cloud-workflow group +# job that downloaded the impact artifact the impact job produced. In that +# mode anvil-impact does not snapshot or recompute (so it needs neither +# cargo-delta nor a fetched base ref, which group jobs lack); it asserts the +# downloaded include_.txt files are present -- failing loudly if any are +# missing -- and the checks read them directly. + +# Private. Produces the two cargo-delta snapshots under +# target/anvil/impact/snapshots/ with two independent cache keys: +# * baseline.json -- snapshot of the base ref. EXPENSIVE (needs a throwaway +# git worktree). Keyed on the composite of the base commit sha and the +# effective .delta.toml identity (baseline.key stores " +# "), so the worktree is recreated when the base moves OR the +# cargo-delta config changes. +# * current.json -- snapshot of the working tree. Cheap. Keyed on +# (current.key); the dirty-tree guard means a clean tree +# corresponds exactly to HEAD. + +# Snapshot the base ref and working tree for impact analysis. +[script("pwsh", "-NoProfile")] +_anvil-impact-snapshot: + $ErrorActionPreference = 'Stop' + # Exit 2 (not the generic 1 used for operational failures below) marks + # *invalid caller configuration*: an unrecognized ANVIL_IMPACT value is a + # mistake in how the recipe was invoked, not a runtime failure of the work + # it does. Keeping the two distinct lets a caller (or CI) tell a bad-mode + # misconfiguration apart from a genuine snapshot/impact failure. All three + # ANVIL_IMPACT readers (_anvil-impact-snapshot, anvil-impact, + # _anvil-impact-include) use the same 2/1 split; keep them in sync. + if ($env:ANVIL_IMPACT -and $env:ANVIL_IMPACT -notin @('off', 'consume')) { + [Console]::Error.WriteLine("anvil: ANVIL_IMPACT='$($env:ANVIL_IMPACT)' is not recognized (expected 'off', 'consume', or unset).") + exit 2 + } + if ($env:ANVIL_IMPACT -eq 'off' -or $env:ANVIL_IMPACT -eq 'consume') { exit 0 } + + # Short-circuit on a dirty working tree, BEFORE the expensive recompute + # below. anvil-impact widens every tier to --workspace when the tree is + # dirty (see its safety net) and never reads these snapshots, so computing + # them would be wasted work -- and the recompute path needs cargo-delta + + # a resolvable, non-shallow base ref, which a first-time checkout or local + # working-tree edit may lack. Without this, a dirty run that could not recompute + # would hard-fail here instead of gracefully widening. Same probe as + # anvil-impact (excluding target/, which holds our own cache artifacts). + # Let git itself exclude target/ via an :(exclude) pathspec rather than + # string-slicing porcelain records -- git then reports both sides of a + # rename and applies the checkout's real path-case rules, so a change whose + # only out-of-target/ side is a rename destination (or a case-distinct + # path) is still seen as dirty. + if (@(git status --porcelain -- . ':(exclude)target/' | Where-Object { $_ }).Count -gt 0) { + exit 0 + } + + $impactDir = 'target/anvil/impact' + $snapDir = Join-Path $impactDir 'snapshots' + $baselineJson = Join-Path $snapDir 'baseline.json' + $baselineKeyFile = Join-Path $snapDir 'baseline.key' + $currentJson = Join-Path $snapDir 'current.json' + $currentKeyFile = Join-Path $snapDir 'current.key' + $noWorkspaceMarker = Join-Path $snapDir 'baseline-no-workspace' + + # Effective cargo-delta config identity -- part of the baseline cache key. + # Resolve the repo's `.delta.toml` ONCE here, at the repo root, before the + # fast path below and before any Push-Location into the base worktree. + # (This probe -- like the one in anvil-impact -- is CWD-relative; it relies + # on `just` invoking recipes at the repo root, which the generated Justfile + # guarantees.) cargo-delta's default config lookup is `delta.toml` (no dot), + # so a repo that uses `.delta.toml` must pass `--config` explicitly or every + # snapshot/impact runs under cargo-delta's built-in defaults -- ignoring + # file_exclude / [parser] / assume_patterns / trip_wire_patterns. All three + # invocations (baseline snapshot, current snapshot, impact) share this one + # `$deltaArgs`. + # + # Because that config governs WHAT every snapshot captures, it is part of + # the baseline's identity: editing `.delta.toml` (or adding/removing it) + # must invalidate the baseline exactly like a moved base ref would. + # Otherwise a warm local cache keyed on the base sha alone would keep an + # old-config baseline while the current snapshot -- regenerated because the + # committed config edit moved HEAD -- is captured under the new config, and + # `cargo delta impact` would then diff two snapshots taken under different + # rules and silently mis-scope. So hash the resolved config (or mark its + # absence) and fold it into the composite baseline key `$baseSha $configId` + # below (which also flows into anvil-impact's projection cache via + # baseline.key). Absent a config file, `$deltaArgs` stays empty and the id + # is ``, so adopters without a `.delta.toml` are unaffected. + # + # anvil manages `.delta.toml` (the anvil-delta artifact owns its + # trip_wire_patterns region), so in an anvil-managed repo the file should + # exist whenever this impact recipe does. Its absence means cargo-delta runs + # under defaults with trip wires + file_exclude OFF, which can silently + # UNDER-scope -- the opposite of what trip wires exist to prevent. Every + # neighbouring precondition here (missing base ref, shallow clone) is a hard + # error; failing open on this one alone would be surprising, so warn loudly. + # We warn rather than fail because a repo may legitimately opt out of the + # `.delta.toml` region; the warning surfaces the reduced coverage without + # blocking. (anvil-impact re-probes but stays silent: this dependency + # recipe always runs first, so one warning per compute is enough.) + $deltaArgs = @() + $deltaConfigId = '' + if (Test-Path '.delta.toml') { + $deltaConfigPath = (Resolve-Path '.delta.toml').Path + $deltaArgs = @('--config', $deltaConfigPath) + # SHA-256 is an arbitrary but stable choice: the key only needs a + # deterministic content fingerprint to detect config edits, not any + # cryptographic property. Get-FileHash defaults to SHA-256, so this + # avoids pulling in another algorithm. Changing it only invalidates + # already-cached baselines (they recompute once); it is not a contract. + $deltaConfigId = (Get-FileHash -LiteralPath $deltaConfigPath -Algorithm SHA256).Hash + } else { + [Console]::Error.WriteLine("anvil-impact: warning: no .delta.toml found at the repo root; cargo-delta will run under its defaults (trip_wire_patterns and file_exclude_patterns inactive), which can under-scope the impact set. Restore the anvil-managed .delta.toml to get conservative scoping.") + } + + # Working-tree state key -- git only, no cargo-delta, no base ref. The + # dirty-tree guard above already exited on any non-target/ change, so by + # this point the tree is clean and the snapshot corresponds exactly to + # HEAD; the key is therefore just the HEAD sha. (An earlier version also + # appended a hash of `git diff HEAD` + porcelain status, but past the guard + # both are empty for every tracked change. Their only remaining input was + # tracked target/ content -- which `git diff HEAD` reports even though the + # guard's status probe excludes target/ -- so it could only cause spurious + # current-snapshot invalidation, never add signal. target/ must not perturb + # the key, so keying on HEAD alone is both simpler and more correct.) + $currentState = (git rev-parse HEAD).Trim() + + # Consumer fast path: reuse an already-present, matching snapshot set + # WITHOUT cargo-delta or a fetched base ref -- this short-circuits a local + # repeat run when nothing changed. (The cloud-workflow consume path never + # reaches here: ANVIL_IMPACT=consume returned at the guard above and trusts + # the downloaded artifact verbatim -- anvil-impact asserts its presence + # before any scoped check reads it.) We still detect a moved base ref when + # it is cheaply resolvable (best effort). If the base cannot be resolved + # $baseSha stays $null, the baseline is treated as not fresh, and we fall + # through to the recompute path, which fails fast with git-fetch guidance + # rather than scoping against a stale baseline. + $currentFresh = (Test-Path $currentJson) -and (Test-Path $currentKeyFile) -and ` + ("$(Get-Content -Raw $currentKeyFile)".Trim() -eq $currentState) + $baseSha = $null + $base = (& "{{ just_executable() }}" _anvil-base-ref 2>$null) + if (($LASTEXITCODE -eq 0) -and $base) { + $base = ($base | Select-Object -First 1).Trim() + git rev-parse --verify "$base^{commit}" 2>$null | Out-Null + if ($LASTEXITCODE -eq 0) { $baseSha = (git rev-parse $base).Trim() } + } + # Baseline freshness is keyed on the COMPOSITE `$baseSha $deltaConfigId` + # (see the config-identity note above): a moved base ref OR a changed + # cargo-delta config both invalidate it, so the baseline is never diffed + # against a current snapshot captured under different rules. + $baselineKey = "$baseSha $deltaConfigId" + $baselineFresh = (Test-Path $baselineKeyFile) -and ` + ((Test-Path $baselineJson) -or (Test-Path $noWorkspaceMarker)) -and ` + ("$(Get-Content -Raw $baselineKeyFile)".Trim() -eq $baselineKey) + if ($currentFresh -and $baselineFresh) { + Write-Host 'anvil-impact: snapshots up to date' + exit 0 + } + + # --- (Re)compute path: this is the only path that needs cargo-delta and a + # resolvable base ref (the impact job in cloud workflows, or a local first + # run / cache miss). --- + # cargo-delta is validated here -- inside the recompute path, after the + # off/consume guard and the fast path -- rather than as a hard `just` + # dependency of `anvil-impact`. Only recompute needs the tool; the paths + # that skip it (ANVIL_IMPACT=off/consume, or a cache hit) must be able to + # no-op without it. Groups that can actually reach recompute declare + # cargo-delta as their own setup prereq (see groups/*.just), so a local + # `just anvil-` still fails fast if it is missing; keeping it off + # anvil-impact's dependency line is what lets the no-op paths stay + # tool-agnostic. + & "{{ just_executable() }}" anvil-tool-cargo-delta-validate-prereqs + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + [System.IO.Directory]::CreateDirectory($snapDir) | Out-Null + + # The best-effort base-ref resolution above may have left `$baseSha` unset + # (unresolvable ref, or a `just`/git hiccup). Recompute cannot proceed + # without it, so resolve with fail-fast diagnostics now. We deliberately do + # NOT `git fetch`: mutating git state as a side effect of a build check is + # surprising and can race the user's own git operations. + if (-not $baseSha) { + $base = (& "{{ just_executable() }}" _anvil-base-ref) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + $base = $base.Trim() + git rev-parse --verify "$base^{commit}" 2>$null | Out-Null + if ($LASTEXITCODE -ne 0) { + $branch = ($base -replace '^origin/', '') + Write-Error "anvil-impact: base ref '$base' not found locally. Run ``git fetch origin $branch`` and retry." + exit 1 + } + $baseSha = (git rev-parse $base).Trim() + } + if ((git rev-parse --is-shallow-repository) -eq 'true') { + Write-Error 'anvil-impact: shallow clone detected; cargo-delta needs full history for the base ref. Run ``git fetch --unshallow`` and retry.' + exit 1 + } + + # --- Baseline snapshot: only when the base sha OR the config changed. --- + # Re-derive the composite key now that `$baseSha` is authoritatively + # resolved (the fast-path value above may have been $null on a cache miss). + $baselineKey = "$baseSha $deltaConfigId" + $haveBaseline = (Test-Path $baselineKeyFile) -and ` + ("$(Get-Content -Raw $baselineKeyFile)".Trim() -eq $baselineKey) -and ` + ((Test-Path $baselineJson) -or (Test-Path $noWorkspaceMarker)) + if ($haveBaseline) { + Write-Host "anvil-impact: baseline snapshot up to date (base $baseSha)" + } else { + Write-Host "anvil-impact: snapshotting baseline at $base ($baseSha)" + Remove-Item -LiteralPath $noWorkspaceMarker -ErrorAction SilentlyContinue + Remove-Item -LiteralPath $baselineJson -ErrorAction SilentlyContinue + # The baseline worktree is checked out at the merge target, whose + # rust-toolchain.toml may pin a toolchain that is NOT installed on this + # machine (any PR that bumps rust-toolchain.toml hits this). The + # snapshot is metadata-only (file presence + content hashes), so use + # the selected compiler from the current checkout to override any + # different toolchain file in the baseline worktree. Environment/MSRV + # selections are already valid rustup overrides. For a selecting + # toolchain file, use the selected rustc sysroot as an unambiguous path + # toolchain. + $stableArgs = {{_anvil_stable_toolchain_args}} + $baselineToolchain = if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { + $env:RUSTUP_TOOLCHAIN.Trim() + } elseif ($stableArgs.Count -eq 1) { + ([string] $stableArgs[0]).Substring(1) + } else { + $sysroot = @(& rustc --print sysroot) + if ($LASTEXITCODE -ne 0 -or $sysroot.Count -ne 1 -or [string]::IsNullOrWhiteSpace([string] $sysroot[0])) { + Write-Error 'anvil-impact: rustc could not report the current checkout stable toolchain sysroot' + exit 1 + } + ([string] $sysroot[0]).Trim() + } + + # Temp-root precedence follows the runner-managed scratch dir on each + # host: RUNNER_TEMP on GitHub Actions, AGENT_TEMPDIRECTORY on Azure + # Pipelines -- both are job-owned and cleaned up by the runner, so the + # throwaway worktree never leaks into the workspace or survives the job. + # Local runs have neither, so fall back to the system temp dir. The + # worktree name includes $PID so concurrent impact runs (e.g. several + # local checks, or matrix legs sharing a temp root) each get a distinct + # path and never remove or overwrite another run's baseline checkout. + $tmpRoot = $env:RUNNER_TEMP + if (-not $tmpRoot) { $tmpRoot = $env:AGENT_TEMPDIRECTORY } + if (-not $tmpRoot) { $tmpRoot = [System.IO.Path]::GetTempPath() } + $wt = Join-Path $tmpRoot "anvil-baseline-$PID" + if (Test-Path $wt) { git worktree remove --force $wt 2>$null | Out-Null } + # GIT_LFS_SKIP_SMUDGE=1: the worktree inherits the repo config but not + # the credential helper, so LFS smudge would fail. cargo-delta only + # needs file content hashes; LFS pointer files hash consistently. + $env:GIT_LFS_SKIP_SMUDGE = '1' + git worktree add --detach $wt $baseSha 2>&1 | Out-Null + if ($LASTEXITCODE -ne 0) { Write-Error "anvil-impact: failed to create baseline worktree at $wt"; exit 1 } + try { + if (-not (Test-Path (Join-Path $wt 'Cargo.toml'))) { + # First-time anvil adoption: the base predates the cargo + # workspace (no root Cargo.toml), so there is nothing for + # cargo-delta to snapshot. Record a marker; anvil-impact falls + # back to full-workspace validation rather than failing. + Write-Host "anvil-impact: base '$base' has no root Cargo.toml (first-time adoption?); falling back to full-workspace" + New-Item -ItemType File -Path $noWorkspaceMarker -Force | Out-Null + Set-Content -LiteralPath $baselineKeyFile -Value $baselineKey -Encoding UTF8 -NoNewline + } else { + Push-Location $wt + # Override only this baseline subprocess scope, then restore the + # caller's input override before returning to the current tree. + $hadToolchain = Test-Path Env:\RUSTUP_TOOLCHAIN + $priorToolchain = if ($hadToolchain) { $env:RUSTUP_TOOLCHAIN } else { $null } + try { + $env:RUSTUP_TOOLCHAIN = $baselineToolchain + $baseSnap = cargo delta @deltaArgs snapshot + if ($LASTEXITCODE -ne 0) { throw 'cargo delta snapshot (baseline) failed' } + } finally { + if ($hadToolchain) { + $env:RUSTUP_TOOLCHAIN = $priorToolchain + } else { + Remove-Item Env:\RUSTUP_TOOLCHAIN -ErrorAction SilentlyContinue + } + Pop-Location + } + Set-Content -LiteralPath $baselineJson -Value $baseSnap -Encoding UTF8 + Set-Content -LiteralPath $baselineKeyFile -Value $baselineKey -Encoding UTF8 -NoNewline + } + } finally { + git worktree remove --force $wt 2>$null | Out-Null + } + } + + # --- Current snapshot: only when the working tree changed + # ($currentState was computed at the top of this recipe). --- + $haveCurrent = (Test-Path $currentJson) -and (Test-Path $currentKeyFile) -and ` + ("$(Get-Content -Raw $currentKeyFile)".Trim() -eq $currentState) + if ($haveCurrent) { + Write-Host 'anvil-impact: current snapshot up to date' + } else { + Write-Host 'anvil-impact: snapshotting working tree' + $curSnap = & cargo {{_anvil_stable_toolchain_args}} delta @deltaArgs snapshot + if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-impact: cargo delta snapshot (current) failed'; exit 1 } + Set-Content -LiteralPath $currentJson -Value $curSnap -Encoding UTF8 + Set-Content -LiteralPath $currentKeyFile -Value $currentState -Encoding UTF8 -NoNewline + } + +# Public. Computes the impact set from the two snapshots and projects each +# tier into a pre-built `--package name@version ...` string (or the sentinel +# `--skip` when the tier is empty) via the shared _anvil-impact-format +# helper, written to include_.txt. Per-crate checks depend on this +# recipe and read those files via _anvil-impact-include. Cache-aware: no-ops +# when the snapshots haven't changed. + +# Compute the impact set and write the target/anvil/impact cache. +[group("anvil")] +[script("pwsh", "-NoProfile")] +anvil-impact: _anvil-impact-snapshot + $ErrorActionPreference = 'Stop' + # Exit 2 = invalid caller configuration (bad ANVIL_IMPACT), distinct from + # the generic 1 used for operational failures -- see _anvil-impact-snapshot. + if ($env:ANVIL_IMPACT -and $env:ANVIL_IMPACT -notin @('off', 'consume')) { + [Console]::Error.WriteLine("anvil: ANVIL_IMPACT='$($env:ANVIL_IMPACT)' is not recognized (expected 'off', 'consume', or unset).") + exit 2 + } + if ($env:ANVIL_IMPACT -eq 'off') { exit 0 } + if ($env:ANVIL_IMPACT -eq 'consume') { + # consume = trust an already-downloaded cache verbatim, so assert the + # cache is actually present. If the CI download ever stops landing at + # target/anvil/impact/ (renamed artifact, a missing download step, or + # an adopter invoking the group action directly), every scoped check + # would otherwise silently widen to --workspace and the pipeline would + # stay green. Fail loudly instead. (Presence only -- freshness stays a + # deliberate non-goal in consume mode.) + $missing = @('modified', 'affected', 'required') | + Where-Object { -not (Test-Path "target/anvil/impact/include_$_.txt") } + if ($missing.Count -gt 0) { + Write-Error "anvil-impact: ANVIL_IMPACT=consume but the downloaded impact cache is missing ($($missing -join ', ') include file(s) absent under target/anvil/impact/). The impact artifact must be downloaded there before scoped checks run." + exit 1 + } + exit 0 + } + + $impactDir = 'target/anvil/impact' + $snapDir = Join-Path $impactDir 'snapshots' + $baselineJson = Join-Path $snapDir 'baseline.json' + $currentJson = Join-Path $snapDir 'current.json' + $baselineKeyFile = Join-Path $snapDir 'baseline.key' + $currentKeyFile = Join-Path $snapDir 'current.key' + $impactJson = Join-Path $impactDir 'impact.json' + $impactStateFile = Join-Path $impactDir 'impact.state' + + # Local safety net + warning for a dirty working tree. cargo-delta scopes + # on the *committed* diff of HEAD against the base ref, so uncommitted + # changes -- a crate you are actively editing but have not committed -- are + # invisible to it and would be silently scoped out (its checks skipped). + # When the tree has any uncommitted change (tracked edits or new, + # non-ignored untracked files), warn and widen every tier to the full + # workspace so work-in-progress is never skipped; commit to scope by impact + # instead. This runs BEFORE the cache-freshness check below so the warning + # is shown on every dirty invocation, not just the first. Cloud-workflow + # checkouts are clean, so this only affects local runs. cargo-delta and + # anvil write only under target/ (snapshots, include files), and target/ + # may not be gitignored in every adopter repo, so exclude it from the probe + # -- otherwise anvil's own artifacts would make every run look dirty. git + # itself does the excluding via an :(exclude) pathspec, so renames report + # both sides and path-case follows the checkout's real behavior. + $dirty = @(git status --porcelain -- . ':(exclude)target/' | Where-Object { $_ }) + if ($dirty.Count -gt 0) { + Write-Host "anvil-impact: warning: $($dirty.Count) uncommitted change(s) in the working tree are invisible to cargo-delta (it scopes on the committed diff vs the base ref); widening all tiers to --workspace so nothing you are editing is skipped -- commit to scope by impact, or set ANVIL_IMPACT=off to disable scoping." -ForegroundColor Yellow + [System.IO.Directory]::CreateDirectory($impactDir) | Out-Null + Set-Content -LiteralPath (Join-Path $impactDir 'include_modified.txt') -Value '' -Encoding UTF8 -NoNewline + Set-Content -LiteralPath (Join-Path $impactDir 'include_affected.txt') -Value '--workspace' -Encoding UTF8 -NoNewline + Set-Content -LiteralPath (Join-Path $impactDir 'include_required.txt') -Value '--workspace' -Encoding UTF8 -NoNewline + Set-Content -LiteralPath $impactJson -Value '{}' -Encoding UTF8 + # Invalidate any prior clean-tree cache so committing recomputes. + Remove-Item -LiteralPath $impactStateFile -ErrorAction SilentlyContinue + exit 0 + } + + # Content cache key: regenerate the projection only when a snapshot moved + # or an output is missing. Keyed on the snapshot markers, not mtimes, so a + # downstream cloud-workflow job that downloaded the artifact recognizes it. + $wantState = ("$(Get-Content -Raw $baselineKeyFile)".Trim()) + ' ' + ("$(Get-Content -Raw $currentKeyFile)".Trim()) + $includeFiles = @('modified', 'affected', 'required') | ForEach-Object { Join-Path $impactDir "include_$_.txt" } + $fresh = (Test-Path $impactJson) -and (Test-Path $impactStateFile) -and ` + ("$(Get-Content -Raw $impactStateFile)".Trim() -eq $wantState) -and ` + (@($includeFiles | Where-Object { -not (Test-Path $_) }).Count -eq 0) + if ($fresh) { + Write-Host 'anvil-impact: impact set up to date (cache hit)' + exit 0 + } + + # First-time anvil adoption: the base predates the cargo workspace, so + # there is no baseline to diff against. Fall back to full-workspace + # validation -- modified runs unconditionally (empty), affected/required + # default to --workspace -- rather than failing the impact computation. + if (Test-Path (Join-Path $snapDir 'baseline-no-workspace')) { + Write-Host 'anvil-impact: baseline has no workspace; defaulting all tiers to full-workspace' + Set-Content -LiteralPath (Join-Path $impactDir 'include_modified.txt') -Value '' -Encoding UTF8 -NoNewline + Set-Content -LiteralPath (Join-Path $impactDir 'include_affected.txt') -Value '--workspace' -Encoding UTF8 -NoNewline + Set-Content -LiteralPath (Join-Path $impactDir 'include_required.txt') -Value '--workspace' -Encoding UTF8 -NoNewline + Set-Content -LiteralPath $impactJson -Value '{}' -Encoding UTF8 + Set-Content -LiteralPath $impactStateFile -Value $wantState -Encoding UTF8 -NoNewline + exit 0 + } + + # Resolve the repo's cargo-delta config for `cargo delta impact` below. This + # recipe is a separate pwsh script from `_anvil-impact-snapshot`, so it does + # not inherit that recipe's `$deltaArgs`; resolve it again here (at the repo + # root) so the impact diff honors `.delta.toml` (e.g. trip_wire_patterns). + $deltaArgs = @() + if (Test-Path '.delta.toml') { $deltaArgs = @('--config', (Resolve-Path '.delta.toml').Path) } + + # Full impact JSON -- the durable source of truth. cargo-delta prints + # nothing when no files changed between baseline and current (it "quits" + # early), so default to an empty object so impact.json always exists -- + # the freshness check above keys on its presence, and a missing file + # would force a recompute on every invocation. + $impactOut = (& cargo {{_anvil_stable_toolchain_args}} delta @deltaArgs impact --baseline $baselineJson --current $currentJson --format json | Out-String) + if ($LASTEXITCODE -ne 0) { Write-Error 'anvil-impact: cargo delta impact failed'; exit 1 } + if ([string]::IsNullOrWhiteSpace($impactOut)) { $impactOut = '{}' } + Set-Content -LiteralPath $impactJson -Value $impactOut.Trim() -Encoding UTF8 + + # Project each tier into its `--package name@version ...` / `--skip` + # string via the shared formatter (version-qualified specs + lib-name -> + # package-name translation live there, used identically by the cloud + # impact steps). + foreach ($tier in @('modified', 'affected', 'required')) { + $value = (& "{{ just_executable() }}" _anvil-impact-format $tier $impactJson) + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + Set-Content -LiteralPath (Join-Path $impactDir "include_$tier.txt") -Value $value.Trim() -Encoding UTF8 -NoNewline + Write-Host "anvil-impact: $tier -> $($value.Trim())" + } + Set-Content -LiteralPath $impactStateFile -Value $wantState -Encoding UTF8 -NoNewline + +# _anvil-impact-format: turns a cargo-delta impact report into the +# `--package ...` string the scoped checks consume, projected per +# tier into include_.txt by `anvil-impact` above -- its only caller, +# next to which it now lives (impact analysis stays in one place). +# +# Arguments: +# tier - one of "modified" / "affected" / "required". Selected +# from the cargo-delta JSON (which uses TitleCase keys). +# impactJson - path to the file the caller produced via +# `cargo delta impact --format json`. +# +# Output (stdout, exactly one line so the caller can capture it): +# * "--package name@version --package name2@version2 ..." when the +# tier has members. Each package is a version-qualified cargo spec: +# the version (read from `cargo metadata --no-deps`, i.e. workspace +# members only) disambiguates a workspace member from a like-named +# transitive dependency pulled in at a different version, which a +# bare `-p name` cannot do ("multiple packages named name"). +# * "--skip" when the tier is empty (recipes treat this as a no-op). +# Metadata failures and unknown/ambiguous package names are written to +# STDERR and fail the recipe, so callers never continue with an incomplete +# package set (which would silently under-scope and skip a check). +# +# Why pwsh + ConvertFrom-Json rather than jq + read: cargo-delta and +# cargo-metadata both emit JSON, and parsing structured data through a +# tab-delimited `IFS` read is fragile -- e.g. tab is an IFS-whitespace +# character, so an empty interior field (a package with no lib target) +# coalesces with its delimiters and silently shifts every later field. +# Working with objects sidesteps that entire class of bug. +# +# Usage: +# pwsh : $val = (& "{{ just_executable() }}" _anvil-impact-format affected $impactJson) +[script("pwsh", "-NoProfile")] +_anvil-impact-format tier impactJson: + $ErrorActionPreference = 'Stop' + $tier = '{{tier}}' + $impactJson = '{{impactJson}}' + # cargo-delta keys its impact tiers in TitleCase. + $tierKey = switch ($tier) { + 'modified' { 'Modified' } + 'affected' { 'Affected' } + 'required' { 'Required' } + default { + Write-Error "anvil-impact: unknown tier '$tier' (expected modified/affected/required)" + exit 1 + } + } + # Build three lookups from workspace members: + # * $byName: exact cargo package name (may contain hyphens) -> pkgs + # * $byLib: library / proc-macro target name (snake_case) -> pkgs + # * $byDirLeaf: manifest directory leaf -> pkgs + # cargo-delta reports *library* names (snake_case), but for crates + # whose package name differs from the lib name (e.g. `cargo-anvil` + # vs `cargo_anvil`) we must translate back to the package name cargo + # accepts. Proc-macro crates carry the same name/target skew, so their + # target is indexed too. Bin-only crates have no lib target; cargo-delta + # reports those by their package name, caught by $byName. In deeply + # nested workspaces cargo-delta can instead emit a manifest directory + # leaf; accept any reported identifier only when every matching namespace + # resolves to the *same* workspace package. + $metadataJson = & cargo {{_anvil_stable_toolchain_args}} metadata --no-deps --format-version 1 + if ($LASTEXITCODE -ne 0) { + Write-Error 'anvil-impact: cargo metadata failed' + exit $LASTEXITCODE + } + try { + $meta = $metadataJson | ConvertFrom-Json + } catch { + Write-Error "anvil-impact: could not parse cargo metadata output: $_" + exit 1 + } + # These identifiers (package name, lib/proc-macro target name, manifest + # directory leaf) are case-sensitive: both `cargo metadata` and cargo-delta + # derive them from the same on-disk workspace, so their casing agrees + # exactly. Use Ordinal dictionaries rather than PowerShell's default + # case-insensitive hashtables so that case-distinct identifiers on a + # case-sensitive checkout (`Foo` vs `foo`) stay separate instead of + # collapsing into one key and triggering a spurious ambiguous-identifier + # error. + $byName = [System.Collections.Generic.Dictionary[string, object]]::new([System.StringComparer]::Ordinal) + $byLib = [System.Collections.Generic.Dictionary[string, object]]::new([System.StringComparer]::Ordinal) + $byDirLeaf = [System.Collections.Generic.Dictionary[string, object]]::new([System.StringComparer]::Ordinal) + foreach ($p in $meta.packages) { + if (-not $byName.ContainsKey($p.name)) { + $byName[$p.name] = [System.Collections.Generic.List[object]]::new() + } + $byName[$p.name].Add($p) + foreach ($t in $p.targets) { + if (($t.kind -contains 'lib') -or ($t.kind -contains 'proc-macro')) { + if (-not $byLib.ContainsKey($t.name)) { + $byLib[$t.name] = [System.Collections.Generic.List[object]]::new() + } + $byLib[$t.name].Add($p) + } + } + $dirLeaf = Split-Path (Split-Path $p.manifest_path -Parent) -Leaf + if (-not $byDirLeaf.ContainsKey($dirLeaf)) { + $byDirLeaf[$dirLeaf] = [System.Collections.Generic.List[object]]::new() + } + $byDirLeaf[$dirLeaf].Add($p) + } + $impact = Get-Content -LiteralPath $impactJson -Raw | ConvertFrom-Json + $names = @($impact.$tierKey) + $specs = [System.Collections.Generic.List[string]]::new() + foreach ($name in $names) { + if ([string]::IsNullOrWhiteSpace($name)) { continue } + # Union candidates from every namespace, de-duplicated by package id + # (case-sensitive, so Ordinal) so that a name matching the same package + # as both a cargo name and a directory leaf collapses to one candidate. + $matches = [System.Collections.Generic.Dictionary[string, object]]::new([System.StringComparer]::Ordinal) + if ($byName.ContainsKey($name)) { + foreach ($candidate in $byName[$name]) { $matches[$candidate.id] = $candidate } + } + if ($byLib.ContainsKey($name)) { + foreach ($candidate in $byLib[$name]) { $matches[$candidate.id] = $candidate } + } + if ($byDirLeaf.ContainsKey($name)) { + foreach ($candidate in $byDirLeaf[$name]) { $matches[$candidate.id] = $candidate } + } + $candidates = @($matches.Values) + if ($candidates.Count -eq 0) { + # A name cargo-delta reported that doesn't resolve to any workspace + # package -- a gap in our reverse-mapping of the (non-unique) library + # identifier cargo-delta emits. Fail hard so the mapping gap is + # surfaced and fixed rather than silently under-scoping the tier. + Write-Error "anvil-impact: cargo-delta returned unknown package '$name' in the $tier set. Re-run with ANVIL_IMPACT=off to proceed unscoped, and report the unmapped identifier." + exit 1 + } + if ($candidates.Count -ne 1) { + # The identifier is genuinely ambiguous -- it names two or more + # distinct workspace packages across the name / lib / directory-leaf + # namespaces (e.g. `ffi` / `ffi_build` collisions in deeply nested + # workspaces). We cannot know which cargo-delta meant, so fail hard + # rather than guess and risk under-scoping the tier. + $candidateNames = ($candidates | ForEach-Object { $_.name } | Sort-Object) -join ', ' + Write-Error "anvil-impact: cargo-delta returned ambiguous package identifier '$name' in the $tier set (matches: $candidateNames). Re-run with ANVIL_IMPACT=off to proceed unscoped, and report the identifier." + exit 1 + } + $p = $candidates[0] + $specs.Add("--package $($p.name)@$($p.version)") + } + if ($specs.Count -eq 0) { + Write-Output '--skip' + } else { + Write-Output ($specs -join ' ') + } + +# Private. Resolves an impact category's scope for a check recipe and echoes it on stdout. +# 1. ANVIL_IMPACT=off -> tier default (full-workspace / run). +# 2. include_.txt present -> its contents (a --package list or --skip). +# 3. otherwise -> tier default. +# Tier default: --workspace for affected/required; empty (run) for modified. +# Each scoped check calls this directly (into a local $include variable) to +# read its category's scope from the target/anvil/impact cache -- the same cache +# both locally and in cloud workflows (where the group job downloaded it). + +# Resolve a tier's package scope from the impact cache. +[script("pwsh", "-NoProfile")] +_anvil-impact-include tier: + $ErrorActionPreference = 'Stop' + $tier = '{{ tier }}' + $default = if ($tier -eq 'modified') { '' } else { '--workspace' } + # Exit 2 = invalid caller configuration (bad ANVIL_IMPACT), distinct from + # the generic 1 used for operational failures -- see _anvil-impact-snapshot. + if ($env:ANVIL_IMPACT -and $env:ANVIL_IMPACT -notin @('off', 'consume')) { + [Console]::Error.WriteLine("anvil: ANVIL_IMPACT='$($env:ANVIL_IMPACT)' is not recognized (expected 'off', 'consume', or unset).") + exit 2 + } + if ($env:ANVIL_IMPACT -eq 'off') { Write-Output $default; exit 0 } + $file = "target/anvil/impact/include_$tier.txt" + if (Test-Path $file) { + Write-Output ("$(Get-Content -Raw $file)".Trim()) + } else { + Write-Output $default + } \ No newline at end of file diff --git a/justfiles/anvil/mod.just b/justfiles/anvil/mod.just index 932f5bfa6..0e444fef5 100644 --- a/justfiles/anvil/mod.just +++ b/justfiles/anvil/mod.just @@ -1,13 +1,13 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -# Managed by cargo-anvil. Update the corresponding template in the cargo-anvil -# crate unless the change is repository-specific. +# GENERATED BY cargo-anvil. DO NOT EDIT DIRECTLY. +# Update cargo-anvil and regenerate; repository-specific edits stop automatic updates. # Update behaviour: https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/updates.md # Entry point for the anvil just recipe tree. The user's root Justfile # imports this single file; everything else lives in sibling .just files # pulled in here. -# All multi-statement recipes in this tree use [script("pwsh")]. pwsh +# All multi-statement recipes in this tree use [script("pwsh", "-NoProfile")]. pwsh # is preinstalled on Windows (10+), GH/ADO hosted Linux + Windows # runners, and is installable on macOS/Linux via Homebrew / upstream # installer. We chose pwsh over bash because: @@ -18,7 +18,7 @@ # - just's [script("bash")] attribute passes Windows tempfile paths # to bash unescaped, and bash interprets the backslashes as escape # characters — every recipe fails with a mangled path. -# - [script("pwsh")] works from plain PowerShell with no PATH +# - [script("pwsh", "-NoProfile")] works from plain PowerShell with no PATH # augmentation and no path translation. pwsh handles Windows # paths natively. # @@ -36,6 +36,7 @@ # it, to avoid conflicting with adopters who already have it. import 'helpers.just' +import 'impact.just' import 'checks/aprz.just' import 'checks/audit.just' import 'checks/bench.just' @@ -59,6 +60,7 @@ import 'checks/miri.just' import 'checks/miri-race-coverage.just' import 'checks/miri-strict-provenance.just' import 'checks/miri-tree-borrows.just' +import 'checks/msrv-test.just' import 'checks/mutants-diff.just' import 'checks/mutants-full.just' import 'checks/pr-title.just' @@ -66,7 +68,13 @@ import 'checks/readme-check.just' import 'checks/semver-check.just' import 'checks/spellcheck.just' import 'checks/udeps.just' +# Optional: the container artifacts can be removed through `without_artifact`, +# which deletes this file. A hard import would then fail parsing for every +# recipe in the tree, not merely the container ones, so the documented opt-out +# would break the whole Justfile. +import? 'container.just' import 'groups/pr-fast.just' +import 'groups/pr-msrv.just' import 'groups/pr-slow.just' import 'groups/pr-test.just' import 'groups/pr-runtime-analysis.just' diff --git a/justfiles/anvil/tiers.just b/justfiles/anvil/tiers.just index 5c73f724c..a06e87b4f 100644 --- a/justfiles/anvil/tiers.just +++ b/justfiles/anvil/tiers.just @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -# Managed by cargo-anvil. Update the corresponding template in the cargo-anvil -# crate unless the change is repository-specific. +# GENERATED BY cargo-anvil. DO NOT EDIT DIRECTLY. +# Update cargo-anvil and regenerate; repository-specific edits stop automatic updates. # Update behaviour: https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/updates.md # See https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/checks.md @@ -20,20 +20,37 @@ anvil-pr: anvil-pr-validate-prereqs \ # without a commit (advisories, flakes) + the truly expensive # exhaustive checks that don't fit in a PR budget. Runs on a schedule # against `main`, not on PRs. +# +# The scheduled tier is deliberately NOT impact-scoped: it is the catch-all +# that backstops PR-tier scoping. Every impact-scoped check depends on +# `anvil-impact` and populates its own scope from the cache, so the tier runs +# with ANVIL_IMPACT=off, which makes `_anvil-impact-include` return each +# category's full-workspace default and the `anvil-impact` dependency no-op. +# A dependency-only recipe cannot set an environment variable for its own +# dependencies, so the public tier wraps the private one through +# `_anvil-unscoped`. # Run all scheduled checks. [group("anvil")] -anvil-scheduled: anvil-scheduled-validate-prereqs \ +anvil-scheduled: (_anvil-unscoped "scheduled") + +[private] +_anvil-scheduled: anvil-scheduled-validate-prereqs \ anvil-scheduled-test \ anvil-scheduled-advisories \ anvil-scheduled-runtime-analysis \ anvil-scheduled-exhaustive +# Full-workspace for the same reason as the scheduled tier. + # Full tier: PR + scheduled, end-to-end. Useful before tagging a release. [group("anvil")] -anvil-full: anvil-full-validate-prereqs \ +anvil-full: (_anvil-unscoped "full") + +[private] +_anvil-full: anvil-full-validate-prereqs \ anvil-pr \ - anvil-scheduled + _anvil-scheduled # Tier-level + global setup + validate-prereqs # =========================================================================== @@ -91,14 +108,15 @@ anvil-full-validate-prereqs: \ # catalog knows about; `anvil-validate-prereqs` verifies every tool # and component is present at or above its pinned version. # -# These also install / verify cargo-delta, the impact-analysis tool. -# cargo-delta is only invoked by the cloud-workflow impact step (not by -# any check), so it is deliberately NOT part of the per-group / per-tier -# setup recipes -- a CI group job installs only what its checks need, and -# the impact step installs cargo-delta itself. But the catch-all -# `anvil-setup` is "install everything the catalog knows about", so it -# includes cargo-delta too (a local `just anvil-setup` then provisions a -# complete environment, impact tool included). +# cargo-delta (the impact-analysis tool) is a prerequisite of every group +# whose checks are impact-scoped: those checks depend on `anvil-impact`, +# which invokes cargo-delta when it has to (re)compute the impact set, so +# each such group's `-setup` / `-validate-prereqs` installs / verifies it +# (fail-fast, rather than discovering it missing mid-run). In a cloud +# workflow a group job normally consumes the downloaded impact artifact and +# never actually runs cargo-delta, but the tool is guaranteed present. +# `anvil-setup` also lists it explicitly as the catch-all (harmless -- just +# dedups the fan-out). # Install every tool and component in the anvil catalog. [group("anvil-setup")] diff --git a/justfiles/anvil/tools.just b/justfiles/anvil/tools.just index 2bfad3528..8e942948d 100644 --- a/justfiles/anvil/tools.just +++ b/justfiles/anvil/tools.just @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -# Managed by cargo-anvil. Update the corresponding template in the cargo-anvil -# crate unless the change is repository-specific. +# GENERATED BY cargo-anvil. DO NOT EDIT DIRECTLY. +# Update cargo-anvil and regenerate; repository-specific edits stop automatic updates. # Update behaviour: https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/updates.md # See https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/local.md @@ -11,9 +11,8 @@ # Public: probe for system-level prerequisites that catalog tools need # to BUILD from source. The `binstall` install path downloads pre-built -# binaries and does not need these, so this check is primarily relevant -# for the source-build `install` path (used by local devs and the ADO -# backend) and is best-effort (skipped silently) for `binstall`. +# binaries and does not need these. The installer invokes this check only +# immediately before a source build, including fallback after binstall fails. # # Scope policy: only system libs that an anvil catalog tool DIRECTLY # requires. We do not try to be a general-purpose dev-env doctor. @@ -35,10 +34,10 @@ # package manager and exits non-zero. No auto-install: admin / sudo and # package-manager choice stay with the user. -# Check system-level build prerequisites for catalog tools. +# Check cargo-spellcheck source-build prerequisites. [group("anvil-setup")] -[script("pwsh")] -anvil-system-deps-check: +[script("pwsh", "-NoProfile")] +anvil-tool-cargo-spellcheck-source-deps-check: $ErrorActionPreference = 'Stop' $missing = @() @@ -129,26 +128,29 @@ anvil-system-deps-check: } # ============================================================================ -# rustc + pwsh validate-prereqs (no install -- system prereqs) +# rustc + pwsh validate-prereqs # ============================================================================ # -# rustc is installed via rustup (https://rustup.rs); pwsh is installed -# via the platform's package manager. Both are presumed present on any -# machine running anvil; the validate recipes just surface a friendly -# error if not. +# Stable rustc is provisioned by `anvil-toolchain-stable-install` below. +# pwsh is installed via the platform's package manager. The validate recipes +# are read-only and only surface a friendly error if a prerequisite is absent. -# Validate that rustc is available. +# Validate that rustc is available and workspace MSRVs are compatible. [group("anvil-setup")] -[script("pwsh")] +[script("pwsh", "-NoProfile")] anvil-tool-rustc-validate-prereqs: if (-not (Get-Command rustc -ErrorAction SilentlyContinue)) { Write-Error 'anvil: rustc not found. Install via rustup: https://rustup.rs' exit 1 } + & "{{ just_executable() }}" _anvil-resolve-stable validate-workspace-msrv + if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE + } # Validate that PowerShell Core is available. [group("anvil-setup")] -[script("pwsh")] +[script("pwsh", "-NoProfile")] anvil-tool-pwsh-validate-prereqs: if (-not (Get-Command pwsh -ErrorAction SilentlyContinue)) { $hint = if ($IsMacOS) { @@ -161,27 +163,255 @@ anvil-tool-pwsh-validate-prereqs: Write-Error "anvil: pwsh (PowerShell Core) not found. Install: $hint" exit 1 } + if ($PSVersionTable.PSVersion.Major -lt 7) { + Write-Error "anvil: PowerShell 7+ required; found $($PSVersionTable.PSVersion)" + exit 1 + } # ============================================================================ # Private helpers (install/check primitives) # ============================================================================ -# _install-tool: install a cargo subcommand at exactly the pinned version, +# Resolve or prepare the selected stable compiler without globally exporting it. +# Rustup owns toolchain-file parsing, installation, and file options. Anvil only +# handles explicit environment overrides and the root Cargo MSRV fallback. +[script("pwsh", "-NoProfile")] +_anvil-resolve-stable action="install": + $ErrorActionPreference = 'Stop' + Set-StrictMode -Version 3 + + $action = '{{action}}' + $validActions = @( + 'install', + 'validate-workspace-msrv', + 'msrv', + 'install-msrv' + ) + if ($action -notin $validActions) { + Write-Error "_anvil-resolve-stable: unknown action '$action'" + exit 2 + } + + $repoRoot = '{{replace(justfile_directory(), "'", "''")}}' + + function Test-RootToolchainFile { + return ( + (Test-Path -LiteralPath (Join-Path $repoRoot 'rust-toolchain') -PathType Leaf) -or + (Test-Path -LiteralPath (Join-Path $repoRoot 'rust-toolchain.toml') -PathType Leaf) + ) + } + + function Get-RootMsrv([switch] $AllowMissing) { + $manifestPath = Join-Path $repoRoot 'Cargo.toml' + if (-not (Test-Path -LiteralPath $manifestPath -PathType Leaf)) { + throw "anvil: Cargo.toml not found at repository root '$repoRoot'" + } + + # This bootstrap scan must choose Cargo before Cargo is available to + # parse the manifest. Keep its workspace.package-before-package + # precedence and accepted syntax synchronized with the selector in + # versions.just. + $values = @{} + $section = '' + foreach ($line in (Get-Content -LiteralPath $manifestPath)) { + if ($line -match '^\s*\[\s*([^\]]+)\s*\]\s*(?:#.*)?$') { + $section = $Matches[1].Trim() + continue + } + if ($section -in @('workspace.package', 'package') -and + $line -match '^\s*["'']?rust-version["'']?\s*=\s*(["''])([^"'']+)\1\s*(?:#.*)?$') { + $values[$section] = $Matches[2] + } + } + + if ($values.ContainsKey('workspace.package')) { + return $values['workspace.package'] + } + if ($values.ContainsKey('package')) { + return $values['package'] + } + + if ($AllowMissing) { + return $null + } + throw 'anvil: no selecting rust-toolchain(.toml) and no root [workspace.package] or [package] rust-version; declare an MSRV or select a toolchain explicitly' + } + + function Get-InstalledToolchains { + if (-not (Get-Command rustup -ErrorAction SilentlyContinue)) { + throw 'anvil: rustup not found. Install rustup from https://rustup.rs and ensure it is on PATH' + } + $installed = @(rustup toolchain list) + if ($LASTEXITCODE -ne 0) { + throw 'anvil: rustup toolchain list failed' + } + return $installed + } + + function Test-ToolchainInstalled([string] $toolchain) { + $installed = Get-InstalledToolchains + return (($installed -join "`n") -match "(?m)^$([regex]::Escape($toolchain))(?:-|$)") + } + + function Assert-WorkspaceMsrvCompatibility { + $manifestPath = Join-Path $repoRoot 'Cargo.toml' + $rootMsrv = Get-RootMsrv + if (-not (Test-ToolchainInstalled $rootMsrv)) { + throw "anvil: root MSRV toolchain '$rootMsrv' is not installed; run 'just anvil-toolchain-stable-install' before validating prerequisites" + } + $metadataText = (& cargo "+$rootMsrv" metadata --manifest-path $manifestPath --format-version 1 --no-deps 2>&1 | Out-String) + if ($LASTEXITCODE -ne 0) { + throw "anvil: cargo metadata failed while validating workspace MSRVs:`n$metadataText" + } + $metadata = $metadataText | ConvertFrom-Json + $memberIds = [Collections.Generic.HashSet[string]]::new( + [string[]] $metadata.workspace_members, + [StringComparer]::Ordinal + ) + $members = @($metadata.packages | Where-Object { $memberIds.Contains([string] $_.id) }) + $missing = @($members | Where-Object { [string]::IsNullOrWhiteSpace([string] $_.rust_version) } | ForEach-Object name) + if ($missing.Count -gt 0) { + throw "anvil: workspace packages without a resolved rust-version: $($missing -join ', '); declare one or select a toolchain explicitly" + } + + function ConvertTo-ComparableRustVersion([string] $value) { + if ($value -notmatch '^\s*(\d+)\.(\d+)(?:\.(\d+))?\s*$') { + throw "anvil: invalid resolved rust-version '$value'" + } + # Cargo treats 1.x and 1.x.0 as the same floor, while + # System.Version sorts an unspecified build component differently. + $patch = if ([string]::IsNullOrWhiteSpace($Matches[3])) { 0 } else { [int] $Matches[3] } + return [version]::new([int] $Matches[1], [int] $Matches[2], $patch) + } + + $rootVersion = ConvertTo-ComparableRustVersion $rootMsrv + $newer = @( + $members | + Where-Object { (ConvertTo-ComparableRustVersion ([string] $_.rust_version)) -gt $rootVersion } | + ForEach-Object { "$($_.name) ($($_.rust_version))" } + ) + if ($newer.Count -gt 0) { + throw "anvil: workspace packages require a compiler newer than the root MSRV $rootMsrv`: $($newer -join ', '); raise the root MSRV or select one catalog toolchain explicitly with rust-toolchain.toml" + } + } + + function Assert-CompleteInternalToolchainConfiguration { + if (-not [string]::IsNullOrWhiteSpace($env:ANVIL_MSRV_TOOLCHAIN) -and + [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN) -and + -not (Test-RootToolchainFile)) { + throw 'anvil: ANVIL_MSRV_TOOLCHAIN is set without RUSTUP_TOOLCHAIN and no repository toolchain file selects stable checks; set RUSTUP_TOOLCHAIN to the provisioned stable toolchain or unset ANVIL_MSRV_TOOLCHAIN' + } + } + + function Get-MsrvSelection { + $msrv = Get-RootMsrv -AllowMissing + if ([string]::IsNullOrWhiteSpace($msrv)) { + return $null + } + + if (-not [string]::IsNullOrWhiteSpace($env:ANVIL_MSRV_TOOLCHAIN)) { + return [pscustomobject]@{ + Value = $env:ANVIL_MSRV_TOOLCHAIN + Mapped = $true + } + } + return [pscustomobject]@{ Value = $msrv; Mapped = $false } + } + + function Install-Toolchain([string] $toolchain, [string] $label) { + if (-not (Test-ToolchainInstalled $toolchain)) { + Write-Host "anvil: installing $label toolchain '$toolchain'" + rustup toolchain install $toolchain --profile minimal + if ($LASTEXITCODE -ne 0) { + throw "anvil: failed to install $label toolchain '$toolchain'" + } + } + } + + if ($action -eq 'validate-workspace-msrv') { + Assert-CompleteInternalToolchainConfiguration + $skipWorkspaceMsrvValidation = -not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN) + $hasRootToolchainFile = Test-RootToolchainFile + if ($skipWorkspaceMsrvValidation -or $hasRootToolchainFile) { + Get-InstalledToolchains | Out-Null + } else { + Assert-WorkspaceMsrvCompatibility + } + exit 0 + } + + if ($action -in @('msrv', 'install-msrv')) { + $msrvSelection = Get-MsrvSelection + if ($null -eq $msrvSelection) { + exit 0 + } + if ($action -eq 'install-msrv') { + if ($msrvSelection.Mapped) { + & cargo "+$($msrvSelection.Value)" --version *> $null + if ($LASTEXITCODE -ne 0) { + throw "anvil: mapped MSRV toolchain '$($msrvSelection.Value)' is unavailable; provision ANVIL_MSRV_TOOLCHAIN or unset it to let Anvil install the declared public MSRV" + } + } else { + Install-Toolchain $msrvSelection.Value 'MSRV' + } + } else { + $msrvSelection.Value + } + exit 0 + } + + Assert-CompleteInternalToolchainConfiguration + if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { + Get-InstalledToolchains | Out-Null + & cargo --version *> $null + if ($LASTEXITCODE -ne 0) { + throw "anvil: selected stable toolchain '$($env:RUSTUP_TOOLCHAIN)' is unavailable; provision RUSTUP_TOOLCHAIN or unset it" + } + exit 0 + } + + if (Test-RootToolchainFile) { + Get-InstalledToolchains | Out-Null + Push-Location -LiteralPath $repoRoot + try { + & rustc --version + if ($LASTEXITCODE -ne 0) { + throw 'anvil: rustup could not provision the repository toolchain file' + } + } finally { + Pop-Location + } + exit 0 + } + + Install-Toolchain (Get-RootMsrv) 'stable' + +# Setup paths that execute stable Cargo/Rust or install Cargo tools share this +# dependency, which Just deduplicates within one setup invocation. + +# Ensure the selected stable toolchain is available while preserving native repository toolchain-file behavior. +[group("anvil-setup")] +anvil-toolchain-stable-install: (_anvil-resolve-stable "install") + +# _install-tool-core: install a cargo subcommand at the catalog version, # or no-op if it is already installed at or above that version. The # `installer` parameter selects between: # - "install" (cargo install --locked, pure-source). Default. -# - "binstall" (cargo binstall --no-confirm --locked, with cargo install -# fallback if binstall fails). Bootstraps cargo-binstall -# itself if not on PATH. -[script("pwsh")] -_install-tool name version installer: +# - "binstall" (cargo binstall --no-confirm --locked, with a controlled +# cargo install fallback for tools that declare source +# prerequisites). Bootstraps cargo-binstall itself if not on PATH. +# `source_prereq`, when set, runs immediately before a source installation, +# including a binstall fallback. +[script("pwsh", "-NoProfile")] +_install-tool-core name version installer source_prereq="": $ErrorActionPreference = 'Stop' $name = '{{name}}' $version = '{{version}}' $installer = '{{installer}}' + $sourcePrereq = '{{source_prereq}}' if ($installer -ne 'install' -and $installer -ne 'binstall') { - Write-Error "_install-tool: unknown installer '$installer' (expected 'install' or 'binstall')" + Write-Error "_install-tool-core: unknown installer '$installer' (expected 'install' or 'binstall')" exit 2 } @@ -193,7 +423,7 @@ _install-tool name version installer: # cached binaries and fail with "binary already exists in destination". $installed = $null $pattern = '^' + [regex]::Escape($name) + ' v(\S+):' - foreach ($line in (cargo install --list 2>$null)) { + foreach ($line in (& cargo {{_anvil_stable_toolchain_args}} install --list 2>$null)) { if ($line -match $pattern) { $installed = $Matches[1]; break } } if ($installed) { @@ -211,25 +441,40 @@ _install-tool name version installer: if ($installer -eq 'binstall') { if (-not (Get-Command cargo-binstall -ErrorAction SilentlyContinue)) { Write-Host ' Bootstrapping cargo-binstall' - cargo install --locked cargo-binstall + & cargo {{_anvil_stable_toolchain_args}} install --locked cargo-binstall if ($LASTEXITCODE -ne 0) { Write-Error 'cargo-binstall bootstrap failed' exit $LASTEXITCODE } } - cargo binstall --no-confirm --locked $name --version "=$version" + # Keep source builds behind Anvil's prerequisite check instead of + # allowing binstall to compile before that check can run. Tools with + # no source prerequisite may still use binstall's compile strategy. + $binstallArgs = @('binstall', '--no-confirm', '--locked') + if ($sourcePrereq) { + $binstallArgs += @('--disable-strategies', 'compile') + } + $binstallArgs += @($name, '--version', "=$version") + & cargo {{_anvil_stable_toolchain_args}} @binstallArgs if ($LASTEXITCODE -eq 0) { exit 0 } Write-Host ' binstall failed; falling back to cargo install' -ForegroundColor Yellow } - cargo install --locked $name --version "=$version" + if ($sourcePrereq) { + & "{{just_executable()}}" $sourcePrereq + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + } + & cargo {{_anvil_stable_toolchain_args}} install --locked $name --version "=$version" if ($LASTEXITCODE -ne 0) { Write-Error "$name install FAILED" exit $LASTEXITCODE } +# Provision stable before entering the Cargo tool installer. +_install-tool name version installer source_prereq="": anvil-toolchain-stable-install (_install-tool-core name version installer source_prereq) + # _check-tool: verify a cargo subcommand is installed at or above the # pinned version. Errors with an install hint on missing or too-old. -[script("pwsh")] +[script("pwsh", "-NoProfile")] _check-tool name version: $ErrorActionPreference = 'Stop' $name = '{{name}}' @@ -237,8 +482,19 @@ _check-tool name version: $installed = $null $pattern = '^' + [regex]::Escape($name) + ' v(\S+):' - foreach ($line in (cargo install --list 2>$null)) { - if ($line -match $pattern) { $installed = $Matches[1]; break } + $previousAutoInstall = $env:RUSTUP_AUTO_INSTALL + try { + # Validation must not let the rustup proxy provision a missing selection. + $env:RUSTUP_AUTO_INSTALL = '0' + foreach ($line in (& cargo {{_anvil_stable_toolchain_args}} install --list 2>$null)) { + if ($line -match $pattern) { $installed = $Matches[1]; break } + } + } finally { + if ($null -eq $previousAutoInstall) { + Remove-Item Env:RUSTUP_AUTO_INSTALL -ErrorAction SilentlyContinue + } else { + $env:RUSTUP_AUTO_INSTALL = $previousAutoInstall + } } if (-not $installed) { Write-Error "anvil: required tool '$name' not found. Install: cargo install --locked --version =$version $name" @@ -262,7 +518,7 @@ _check-tool name version: # on warm or cache-restored runners. The probe uses `rustup toolchain list` # (a pure, no-network presence check). Substring match because rustup # suffixes the host triple to the toolchain name. -[script("pwsh")] +[script("pwsh", "-NoProfile")] _install-toolchain toolchain: $ErrorActionPreference = 'Stop' $toolchain = '{{toolchain}}' @@ -279,7 +535,7 @@ _install-toolchain toolchain: # _check-toolchain: verify a rustup toolchain is installed. Uses the same # `rustup toolchain list` substring probe as _install-toolchain. -[script("pwsh")] +[script("pwsh", "-NoProfile")] _check-toolchain toolchain: $ErrorActionPreference = 'Stop' $toolchain = '{{toolchain}}' @@ -288,11 +544,12 @@ _check-toolchain toolchain: exit 1 } -# _install-component: add a component to a toolchain. The toolchain is -# either the literal "default" (current rustup default) or a specific -# pinned toolchain string (which must already be installed -- the -# per-component setup recipes ensure this via a dependency on -# anvil--install). +# _install-component: add a component to a toolchain. The toolchain is either +# the literal "default" (Anvil's selected stable toolchain) or a specific pinned +# toolchain string (which must already be installed -- the per-component setup +# recipes ensure this via a dependency on anvil--install). Explicit +# MSRV fallback selections target `rustup component add --toolchain`; native +# rustup selections use `rustup component add` without restating the selector. # # Probe-first: if the component is already available, skip the # `rustup component add`. This keeps the recipe idempotent and robust @@ -301,11 +558,19 @@ _check-toolchain toolchain: # detection (cargo subcommand / sysroot file) rather than `rustup # component list` -- so the install path uses the same detection the # check path already had. -[script("pwsh")] +[script("pwsh", "-NoProfile")] _install-component toolchain component: $ErrorActionPreference = 'Stop' $toolchain = '{{toolchain}}' $component = '{{component}}' + $stableArgs = @() + $selectedStableToolchain = $null + if ($toolchain -eq 'default') { + $stableArgs = {{_anvil_stable_toolchain_args}} + if ($stableArgs.Count -eq 1) { + $selectedStableToolchain = ([string] $stableArgs[0]).Substring(1) + } + } # Probe whether the component is already present (see _check-component # for the rationale behind each detection method and the `+toolchain` @@ -318,14 +583,22 @@ _install-component toolchain component: } if ($null -ne $subcommand) { if ($toolchain -eq 'default') { - & cargo $subcommand --version *> $null + if ($null -ne $selectedStableToolchain) { + & cargo $stableArgs[0] $subcommand --version *> $null + } else { + & cargo $subcommand --version *> $null + } } else { & cargo "+$toolchain" $subcommand --version *> $null } $present = ($LASTEXITCODE -eq 0) } else { $sysroot = if ($toolchain -eq 'default') { - & rustc --print sysroot 2>$null + if ($null -ne $selectedStableToolchain) { + & rustc $stableArgs[0] --print sysroot 2>$null + } else { + & rustc --print sysroot 2>$null + } } else { & rustc "+$toolchain" --print sysroot 2>$null } @@ -348,8 +621,11 @@ _install-component toolchain component: exit 0 } - if ($toolchain -eq 'default') { - Write-Host "rustup component add $component (default toolchain)" + if ($toolchain -eq 'default' -and $null -ne $selectedStableToolchain) { + Write-Host "rustup component add --toolchain $selectedStableToolchain $component" + rustup component add --toolchain $selectedStableToolchain $component + } elseif ($toolchain -eq 'default') { + Write-Host "rustup component add $component" rustup component add $component } else { Write-Host "rustup component add --toolchain $toolchain $component" @@ -373,11 +649,19 @@ _install-component toolchain component: # rather than via array splat: the Windows rustup proxy mis-parses a # splatted toolchain token and reports `invalid toolchain name ''`, so a # splat would spuriously flag installed components as missing. -[script("pwsh")] +[script("pwsh", "-NoProfile")] _check-component toolchain component: $ErrorActionPreference = 'Stop' $toolchain = '{{toolchain}}' $component = '{{component}}' + $stableArgs = @() + $selectedStableToolchain = $null + if ($toolchain -eq 'default') { + $stableArgs = {{_anvil_stable_toolchain_args}} + if ($stableArgs.Count -eq 1) { + $selectedStableToolchain = ([string] $stableArgs[0]).Substring(1) + } + } $subcommand = switch ($component) { 'clippy' { 'clippy' } 'rustfmt' { 'fmt' } @@ -386,14 +670,22 @@ _check-component toolchain component: } if ($null -ne $subcommand) { if ($toolchain -eq 'default') { - & cargo $subcommand --version *> $null + if ($null -ne $selectedStableToolchain) { + & cargo $stableArgs[0] $subcommand --version *> $null + } else { + & cargo $subcommand --version *> $null + } } else { & cargo "+$toolchain" $subcommand --version *> $null } $present = ($LASTEXITCODE -eq 0) } else { $sysroot = if ($toolchain -eq 'default') { - & rustc --print sysroot 2>$null + if ($null -ne $selectedStableToolchain) { + & rustc $stableArgs[0] --print sysroot 2>$null + } else { + & rustc --print sysroot 2>$null + } } else { & rustc "+$toolchain" --print sysroot 2>$null } @@ -415,7 +707,13 @@ _check-component toolchain component: } } if (-not $present) { - $cmd = if ($toolchain -eq 'default') { "rustup component add $component" } else { "rustup component add --toolchain $toolchain $component" } + $cmd = if ($null -ne $selectedStableToolchain) { + "rustup component add --toolchain $selectedStableToolchain $component" + } elseif ($toolchain -eq 'default') { + "rustup component add $component" + } else { + "rustup component add --toolchain $toolchain $component" + } Write-Error "anvil: component '$component' not installed on toolchain '$toolchain'. Run: $cmd" exit 1 } @@ -444,14 +742,13 @@ anvil-toolchain-nightly-external-types-validate-prereqs: (_check-toolchain rust_ # Rustup components # ============================================================================ # -# Default-toolchain components are installed via `rustup component add` -# (no toolchain spec). Nightly components depend on the toolchain -# being installed first (via the relevant anvil--install -# recipe) and then add the component. +# Stable components target Anvil's explicit selected toolchain. +# Nightly components depend on the toolchain being installed first (via the +# relevant anvil--install recipe) and then add the component. # Install the pinned `clippy` component for the default toolchain. [group("anvil-setup")] -anvil-component-default-clippy-install: (_install-component "default" "clippy") +anvil-component-default-clippy-install: anvil-toolchain-stable-install (_install-component "default" "clippy") # Validate that the pinned `clippy` component is available for the default toolchain. [group("anvil-setup")] @@ -459,7 +756,7 @@ anvil-component-default-clippy-validate-prereqs: (_check-component "default" "cl # Install the pinned `rustfmt` component for the default toolchain. [group("anvil-setup")] -anvil-component-default-rustfmt-install: (_install-component "default" "rustfmt") +anvil-component-default-rustfmt-install: anvil-toolchain-stable-install (_install-component "default" "rustfmt") # Validate that the pinned `rustfmt` component is available for the default toolchain. [group("anvil-setup")] @@ -545,18 +842,18 @@ anvil-tool-cargo-audit-validate-prereqs: (_check-tool "cargo-audit" cargo_audit_ # Install the pinned `cargo-bolero` tool. [group("anvil-setup")] -[script("pwsh")] -anvil-tool-cargo-bolero-install installer="install": +[script("pwsh", "-NoProfile")] +anvil-tool-cargo-bolero-install installer="install": anvil-toolchain-stable-install if (-not $IsLinux) { Write-Host 'anvil-tool-cargo-bolero-install: non-Linux host -- skipping (cargo-bolero/libfuzzer is Linux-only)' exit 0 } - & just _install-tool cargo-bolero {{cargo_bolero_version}} {{installer}} + & just _install-tool-core cargo-bolero {{cargo_bolero_version}} {{installer}} exit $LASTEXITCODE # Validate that the pinned `cargo-bolero` tool is available. [group("anvil-setup")] -[script("pwsh")] +[script("pwsh", "-NoProfile")] anvil-tool-cargo-bolero-validate-prereqs: if (-not $IsLinux) { Write-Host 'anvil-tool-cargo-bolero-validate-prereqs: non-Linux host -- skipping (cargo-bolero/libfuzzer is Linux-only)' @@ -613,6 +910,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) @@ -662,18 +967,18 @@ anvil-tool-cargo-llvm-cov-validate-prereqs: (_check-tool "cargo-llvm-cov" cargo_ # Install the pinned `cargo-mutants` tool. [group("anvil-setup")] -[script("pwsh")] -anvil-tool-cargo-mutants-install installer="install": +[script("pwsh", "-NoProfile")] +anvil-tool-cargo-mutants-install installer="install": anvil-toolchain-stable-install if ($IsWindows -and ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64' -or $env:PROCESSOR_ARCHITEW6432 -eq 'ARM64')) { Write-Host 'anvil-tool-cargo-mutants-install: aarch64-pc-windows-msvc -- skipping (winapi build incompat)' exit 0 } - & just _install-tool cargo-mutants {{cargo_mutants_version}} {{installer}} + & just _install-tool-core cargo-mutants {{cargo_mutants_version}} {{installer}} exit $LASTEXITCODE # Validate that the pinned `cargo-mutants` tool is available. [group("anvil-setup")] -[script("pwsh")] +[script("pwsh", "-NoProfile")] anvil-tool-cargo-mutants-validate-prereqs: if ($IsWindows -and ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64' -or $env:PROCESSOR_ARCHITEW6432 -eq 'ARM64')) { Write-Host 'anvil-tool-cargo-mutants-validate-prereqs: aarch64-pc-windows-msvc -- skipping (winapi build incompat)' @@ -708,7 +1013,7 @@ anvil-tool-cargo-sort-validate-prereqs: (_check-tool "cargo-sort" cargo_sort_ver # Install the pinned `cargo-spellcheck` tool. [group("anvil-setup")] -anvil-tool-cargo-spellcheck-install installer="install": (_install-tool "cargo-spellcheck" cargo_spellcheck_version installer) +anvil-tool-cargo-spellcheck-install installer="install": (_install-tool "cargo-spellcheck" cargo_spellcheck_version installer "anvil-tool-cargo-spellcheck-source-deps-check") # Validate that the pinned `cargo-spellcheck` tool is available. [group("anvil-setup")] diff --git a/justfiles/anvil/versions.just b/justfiles/anvil/versions.just index c7a32d8f1..f330945cf 100644 --- a/justfiles/anvil/versions.just +++ b/justfiles/anvil/versions.just @@ -1,20 +1,20 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -# Managed by cargo-anvil. Update the corresponding template in the cargo-anvil -# crate unless the change is repository-specific. +# GENERATED BY cargo-anvil. DO NOT EDIT DIRECTLY. +# Update cargo-anvil and regenerate; repository-specific edits stop automatic updates. # Update behaviour: https://github.com/microsoft/ox-tools/blob/main/crates/cargo-anvil/docs/design/updates.md # # Pinned versions used by the anvil check tree. + # # Everything anvil installs (cargo subcommands + rustup toolchains) # is pinned here. The pinning policy: # - On install (`-install` recipes): exactly this version (`=` for # cargo subcommands, exact ref for rustup toolchains). Pulling # "latest-matching" at install time is a cloud-workflow reproducibility risk -- -# an upstream release between yesterday's green build and today's -# PR can break things (cargo-spellcheck 0.15.7's em-dash regression -# is the canonical case). The `=` constraint locks the install to -# the version the catalog was validated against. +# an upstream release between yesterday's green build and today's PR can +# change behavior. The `=` constraint locks the install to the version the +# catalog was validated against. # - On validate-prereqs (`-validate-prereqs` recipes): the installed # version must be `>= `. A user who has manually upgraded a # tool for their own reasons (e.g. needing an unreleased bugfix) @@ -31,6 +31,61 @@ # Rustup toolchains # ============================================================================ +# Stable commands interpolate this PowerShell array expression directly at each +# command site. Rustup resolves repository toolchain files from the repository +# root; Anvil only reads Cargo.toml when it needs the root MSRV fallback. +# `RUSTUP_TOOLCHAIN` is an input override only; Anvil never exports a resolved +# value globally into unrelated recipes or helpers. +[private] +_anvil_stable_toolchain_args := trim("@(& {\n$RepoRoot = '" + replace(justfile_directory(), "'", "''") + "'\n\n" + ''' +$ErrorActionPreference = 'Stop' +Set-StrictMode -Version 3 + +if (-not [string]::IsNullOrWhiteSpace($env:RUSTUP_TOOLCHAIN)) { + return +} + +if ((Test-Path -LiteralPath (Join-Path $RepoRoot 'rust-toolchain') -PathType Leaf) -or + (Test-Path -LiteralPath (Join-Path $RepoRoot 'rust-toolchain.toml') -PathType Leaf)) { + return +} + +if (-not [string]::IsNullOrWhiteSpace($env:ANVIL_MSRV_TOOLCHAIN)) { + throw 'anvil: ANVIL_MSRV_TOOLCHAIN is set without RUSTUP_TOOLCHAIN and no repository toolchain file selects stable checks; set RUSTUP_TOOLCHAIN to the provisioned stable toolchain or unset ANVIL_MSRV_TOOLCHAIN' +} + +$manifestPath = Join-Path $RepoRoot 'Cargo.toml' +if (-not (Test-Path -LiteralPath $manifestPath -PathType Leaf)) { + throw "anvil: Cargo.toml not found at repository root '$RepoRoot'" +} + +# This bootstrap scan must choose Cargo before Cargo is available to parse the +# manifest. Keep its workspace.package-before-package precedence and accepted +# syntax synchronized with Get-RootMsrv in tools.just. +$values = @{} +$section = '' +foreach ($line in (Get-Content -LiteralPath $manifestPath)) { + if ($line -match '^\s*\[\s*([^\]]+)\s*\]\s*(?:#.*)?$') { + $section = $Matches[1].Trim() + continue + } + if ($section -in @('workspace.package', 'package') -and + $line -match '^\s*["'']?rust-version["'']?\s*=\s*(["''])([^"'']+)\1\s*(?:#.*)?$') { + $values[$section] = $Matches[2] + } +} + +$msrv = if ($values.ContainsKey('workspace.package')) { + $values['workspace.package'] +} elseif ($values.ContainsKey('package')) { + $values['package'] +} else { + throw 'anvil: no selecting rust-toolchain(.toml) and no root [workspace.package] or [package] rust-version; declare an MSRV or select a toolchain explicitly' +} +Write-Output ('+' + $msrv) +}) +''') + # General nightly used by udeps, miri, careful, and any future # nightly-dependent check. Bumped on a regular cadence (monthly is a # reasonable default) when an adopter has time to absorb formatting / @@ -50,23 +105,24 @@ rust_nightly_external_types := "nightly-2026-03-20" # Cargo subcommands # ============================================================================ -cargo_aprz_version := "1.0.0" +cargo_aprz_version := "1.1.0" cargo_audit_version := "0.22.2" cargo_bolero_version := "0.13.4" -cargo_careful_version := "0.4.9" +cargo_careful_version := "0.4.10" cargo_check_external_types_version := "0.5.0" cargo_coverage_gate_version := "0.2.0" cargo_delta_version := "0.3.1" -cargo_deny_version := "0.19.0" -cargo_doc2readme_version := "0.6.4" +cargo_deny_version := "0.20.2" +cargo_doc2readme_version := "0.7.3" +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.41" +cargo_hack_version := "0.6.45" cargo_heather_version := "0.2.1" -cargo_llvm_cov_version := "0.8.4" -cargo_mutants_version := "26.1.2" -cargo_nextest_version := "0.9.122" -cargo_semver_checks_version := "0.46.0" -cargo_sort_version := "2.0.2" +cargo_llvm_cov_version := "0.9.0" +cargo_mutants_version := "27.1.0" +cargo_nextest_version := "0.9.143" +cargo_semver_checks_version := "0.50.0" +cargo_sort_version := "2.1.4" cargo_spellcheck_version := "0.15.7" -cargo_udeps_version := "0.1.60" +cargo_udeps_version := "0.1.61"