diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ee141f9f..ccff3098 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ jobs: # os: [ubuntu-latest, windows-latest] # fail-fast: false # steps: - # - uses: actions/checkout@v6 + # - uses: actions/checkout@v7 # - uses: moonrepo/setup-rust@v1 # with: # cache: false @@ -35,7 +35,7 @@ jobs: # os: [ubuntu-latest, windows-latest] # fail-fast: false # steps: - # - uses: actions/checkout@v6 + # - uses: actions/checkout@v7 # - uses: moonrepo/setup-rust@v1 # with: # cache: false @@ -49,7 +49,7 @@ jobs: # os: [ubuntu-latest, macos-latest, windows-latest] # fail-fast: false # steps: - # - uses: actions/checkout@v6 + # - uses: actions/checkout@v7 # - uses: moonrepo/setup-rust@v1 # with: # bins: cargo-nextest @@ -66,7 +66,7 @@ jobs: job-total: ${{ steps.plan.outputs.job-total }} jobs-array: ${{ steps.plan.outputs.jobs-array }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: fetch-depth: 0 - uses: moonrepo/setup-toolchain@v0 @@ -85,7 +85,7 @@ jobs: job: ${{ fromJson(needs.plan.outputs.jobs-array) }} fail-fast: false steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: fetch-depth: 0 - uses: moonrepo/setup-toolchain@v0 @@ -97,6 +97,11 @@ jobs: cache: false # Runs out of disk space components: clippy, rustfmt targets: wasm32-wasip1 + # The dotnet toolchain's tests evaluate fixtures with a real `dotnet + # msbuild`, and `-getProperty`/`-getItem` JSON output requires SDK 8+. + - uses: actions/setup-dotnet@v5 + with: + dotnet-version: '8.0.x' - run: moon ci --color --job ${{ matrix.job }} --job-total ${{ needs.plan.outputs.job-total }} --log debug # env: # DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }} diff --git a/.github/workflows/generate-releases.yml b/.github/workflows/generate-releases.yml new file mode 100644 index 00000000..654c12aa --- /dev/null +++ b/.github/workflows/generate-releases.yml @@ -0,0 +1,31 @@ +name: Generate releases + +permissions: + contents: write + +on: + schedule: + # Every 24 hours (avoid the top of the hour, as it's high load for GitHub) + - cron: "15 4 * * *" + workflow_dispatch: + +jobs: + generate: + name: Generate + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-node@v7 + with: + node-version: 24 + - run: node ./scripts/generatePythonReleases.mjs + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - run: node ./scripts/generateRubyReleases.mjs + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Commit and push changes + uses: EndBug/add-and-commit@v10 + with: + add: 'tools/python/releases.json tools/python/releases-v2.json tools/ruby/releases.json' + message: 'chore: Update tool releases [skip ci]' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 152b333a..7c7e2159 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,7 +19,7 @@ jobs: name: Build runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: moonrepo/setup-rust@v1 with: cache: false diff --git a/.moon/workspace.yml b/.moon/workspace.yml index 33366e54..520acf5e 100644 --- a/.moon/workspace.yml +++ b/.moon/workspace.yml @@ -30,6 +30,7 @@ projects: # Toolchains bun-toolchain: toolchains/bun deno-toolchain: toolchains/deno + dotnet-toolchain: toolchains/dotnet go-toolchain: toolchains/go javascript-toolchain: toolchains/javascript node-toolchain: toolchains/node @@ -38,6 +39,7 @@ projects: python-pip-toolchain: toolchains/python-pip python-poetry-toolchain: toolchains/python-poetry python-uv-toolchain: toolchains/python-uv + ruby-toolchain: toolchains/ruby rust-toolchain: toolchains/rust system-toolchain: toolchains/system typescript-toolchain: toolchains/typescript diff --git a/Cargo.lock b/Cargo.lock index dc236ffc..ee1b653e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 4 [[package]] name = "addr2line" -version = "0.25.1" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +checksum = "59317f77929f0e679d39364702289274de2f0f0b22cbf50b2b8cff2169a0b27a" dependencies = [ "gimli", ] @@ -19,9 +19,9 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aho-corasick" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" dependencies = [ "memchr", ] @@ -49,9 +49,9 @@ checksum = "e9d4ee0d472d1cd2e28c97dfa124b3d8d992e10eb0a035f33f5d12e3a177ba3b" [[package]] name = "android_system_properties" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" dependencies = [ "libc", ] @@ -82,17 +82,17 @@ checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" [[package]] name = "asdf_backend" -version = "0.3.4" +version = "0.3.5" dependencies = [ "backend_common", "extism-pdk", "proto_pdk", - "proto_pdk_test_utils 0.47.0", + "proto_pdk_test_utils", "rustc-hash", "schematic", "serde", - "starbase_sandbox 0.11.1", - "starbase_utils 0.13.8", + "starbase_sandbox", + "starbase_utils 0.14.2", "tokio", ] @@ -127,13 +127,13 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.91" +version = "0.1.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" dependencies = [ "proc-macro2", "quote", - "syn 3.0.2", + "syn 3.0.3", ] [[package]] @@ -150,9 +150,9 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "aws-lc-rs" -version = "1.17.3" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00bdb5da18dac48ca2cc7cd4a98e533e8635a58e2361d13a1a4ee3888e0d72f1" +checksum = "ce2b2dcc879c3bae0d371e77c99f2238400ef24ec001394befa67b6e543add9e" dependencies = [ "aws-lc-sys", "untrusted 0.7.1", @@ -161,9 +161,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.43.0" +version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43103168cc76fe62678a375e722fc9cb3a0146159ac5828bc4f0dfd755c2224c" +checksum = "f09fae7be8bb3174e05c6afdb34199e6dc0c7c04ba9fa237b1967adfbde27483" dependencies = [ "cc", "cmake", @@ -191,6 +191,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + [[package]] name = "binstall-tar" version = "0.4.42" @@ -266,24 +272,24 @@ dependencies = [ [[package]] name = "bun_tool" -version = "0.16.9" +version = "0.16.10" dependencies = [ "extism-pdk", "lang_javascript_common", "nodejs_package_json", "proto_pdk", - "proto_pdk_test_utils 0.47.0", + "proto_pdk_test_utils", "schematic", "serde", - "starbase_sandbox 0.11.1", - "starbase_utils 0.13.8", + "starbase_sandbox", + "starbase_utils 0.14.2", "tokio", "tool_common", ] [[package]] name = "bun_toolchain" -version = "1.0.2" +version = "1.0.3" dependencies = [ "bun_tool", "extism-pdk", @@ -294,8 +300,8 @@ dependencies = [ "schematic", "serde", "serde_json", - "starbase_sandbox 0.11.1", - "starbase_utils 0.13.8", + "starbase_sandbox", + "starbase_utils 0.14.2", "tokio", "toolchain_common", ] @@ -438,29 +444,30 @@ dependencies = [ [[package]] name = "cargo-lock" -version = "11.0.1" +version = "11.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63585cdf8572aa7adf0e30a253f988f2b77233bfac1973d52efb6dd53a75920e" +checksum = "50524592e6bfbb1bf6f94e8184a786f637faeaf1cf37bbe65502b9a2c5c48939" dependencies = [ "semver", "serde", - "toml 0.9.12+spec-1.1.0", + "toml 1.1.4+spec-1.1.0", "url", ] [[package]] name = "cargo_backend" -version = "0.1.1" +version = "0.1.6" dependencies = [ "backend_common", "extism-pdk", + "lang_rust_common", "proto_pdk", - "proto_pdk_test_utils 0.47.0", + "proto_pdk_test_utils", "rustc-hash", "schematic", "serde", - "starbase_sandbox 0.11.1", - "starbase_utils 0.13.8", + "starbase_sandbox", + "starbase_utils 0.14.2", "tokio", ] @@ -503,9 +510,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.3.0" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c89588d05638b5b4594a3348a2d6c20277e43a7f5c5202b05cc56888475a47b8" +checksum = "5d262e149917187838d5b42777c8253bcb64500067342904e7d429499a6f277e" dependencies = [ "find-msvc-tools", "jobserver", @@ -513,12 +520,6 @@ dependencies = [ "shlex", ] -[[package]] -name = "cesu8" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" - [[package]] name = "cfg-if" version = "1.0.4" @@ -558,9 +559,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.6.3" +version = "4.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fb99565819980999fb7b4a1796046a5c949e6d4ff132cf5fadf5a641e20d776" +checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca" dependencies = [ "clap_builder", "clap_derive", @@ -568,9 +569,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.6.2" +version = "4.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f09628afdcc538b57f3c6341e9c8e9970f18e4a481690a64974d7023bd33548b" +checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889" dependencies = [ "anstyle", "clap_lex", @@ -578,14 +579,14 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.6.3" +version = "4.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f2392eae7f16557a3d727ef3a12e57b2b2ca6f98566a5f4fb41ffe305df077" +checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] @@ -615,7 +616,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" dependencies = [ - "thiserror 2.0.19", + "thiserror 2.0.20", ] [[package]] @@ -655,21 +656,6 @@ dependencies = [ "static_assertions", ] -[[package]] -name = "compact_str" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dfdd1c2274d9aa354115b09dc9a901d6c5576818cdf70d14cae2bdb47df00ab" -dependencies = [ - "castaway", - "cfg-if", - "itoa", - "rustversion", - "ryu", - "serde", - "static_assertions", -] - [[package]] name = "compact_str" version = "0.10.0" @@ -785,46 +771,48 @@ dependencies = [ [[package]] name = "cranelift-assembler-x64" -version = "0.128.4" +version = "0.130.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50a04121a197fde2fe896f8e7cac9812fc41ed6ee9c63e1906090f9f497845f6" +checksum = "adc822414b18d1f5b1b33ce1441534e311e62fef86ebb5b9d382af857d0272c9" dependencies = [ "cranelift-assembler-x64-meta", ] [[package]] name = "cranelift-assembler-x64-meta" -version = "0.128.4" +version = "0.130.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a09e699a94f477303820fb2167024f091543d6240783a2d3b01a3f21c42bc744" +checksum = "8c646808b06f4532478d8d6057d74f15c3322f10d995d9486e7dcea405bf521a" dependencies = [ "cranelift-srcgen", ] [[package]] name = "cranelift-bforest" -version = "0.128.4" +version = "0.130.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f07732c662a9755529e332d86f8c5842171f6e98ba4d5976a178043dad838654" +checksum = "7b5996f01a686b2349cdb379083ec5ad3e8cb8767fb2d495d3a4f2ee4163a18d" dependencies = [ "cranelift-entity", + "wasmtime-internal-core", ] [[package]] name = "cranelift-bitset" -version = "0.128.4" +version = "0.130.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18391da761cf362a06def7a7cf11474d79e55801dd34c2e9ba105b33dc0aef88" +checksum = "523fea83273f6a985520f57788809a4de2165794d9ab00fb1254fceb4f5aa00c" dependencies = [ "serde", "serde_derive", + "wasmtime-internal-core", ] [[package]] name = "cranelift-codegen" -version = "0.128.4" +version = "0.130.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b3a09b3042c69810d255aef59ddc3b3e4c0644d1d90ecfd6e3837798cc88a3c" +checksum = "d73d1e372730b5f64ed1a2bd9f01fe4686c8ec14a28034e3084e530c8d951878" dependencies = [ "bumpalo", "cranelift-assembler-x64", @@ -836,7 +824,8 @@ dependencies = [ "cranelift-entity", "cranelift-isle", "gimli", - "hashbrown 0.15.5", + "hashbrown 0.16.1", + "libm", "log", "pulley-interpreter", "regalloc2", @@ -844,14 +833,14 @@ dependencies = [ "serde", "smallvec", "target-lexicon", - "wasmtime-internal-math", + "wasmtime-internal-core", ] [[package]] name = "cranelift-codegen-meta" -version = "0.128.4" +version = "0.130.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75817926ec812241889208d1b190cadb7fedded4592a4bb01b8524babb9e4849" +checksum = "b0319c18165e93dc1ebf78946a8da0b1c341c95b4a39729a69574671639bdb5f" dependencies = [ "cranelift-assembler-x64-meta", "cranelift-codegen-shared", @@ -862,35 +851,36 @@ dependencies = [ [[package]] name = "cranelift-codegen-shared" -version = "0.128.4" +version = "0.130.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "859158f87a59476476eda3884d883c32e08a143cf3d315095533b362a3250a63" +checksum = "9195cd8aeecb55e401aa96b2eaa55921636e8246c127ed7908f7ef7e0d40f270" [[package]] name = "cranelift-control" -version = "0.128.4" +version = "0.130.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03b65a9aec442d715cbf54d14548b8f395476c09cef7abe03e104a378291ab88" +checksum = "8976c2154b74136322befc74222ab5c7249edd7e2604f8cbef2b94975541ffb9" dependencies = [ "arbitrary", ] [[package]] name = "cranelift-entity" -version = "0.128.4" +version = "0.130.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8334c99a7e86060c24028732efd23bac84585770dcb752329c69f135d64f2fc1" +checksum = "6038b3147c7982f4951150d5f96c7c06c1e7214b99d4b4a98607aadf8ded89d1" dependencies = [ "cranelift-bitset", "serde", "serde_derive", + "wasmtime-internal-core", ] [[package]] name = "cranelift-frontend" -version = "0.128.4" +version = "0.130.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43ac6c095aa5b3e845d7ca3461e67e2b65249eb5401477a5ff9100369b745111" +checksum = "4cbd294abe236e23cc3d907b0936226b6a8342db7636daa9c7c72be1e323420e" dependencies = [ "cranelift-codegen", "log", @@ -900,15 +890,15 @@ dependencies = [ [[package]] name = "cranelift-isle" -version = "0.128.4" +version = "0.130.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69d3d992870ed4f0f2e82e2175275cb3a123a46e9660c6558c46417b822c91fa" +checksum = "b5a90b6ed3aba84189352a87badeb93b2126d3724225a42dc67fdce53d1b139c" [[package]] name = "cranelift-native" -version = "0.128.4" +version = "0.130.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee32e36beaf80f309edb535274cfe0349e1c5cf5799ba2d9f42e828285c6b52e" +checksum = "c3ec0cc1a54e22925eacf4fc3dc815f907734d3b377899d19d52bec04863e853" dependencies = [ "cranelift-codegen", "libc", @@ -917,9 +907,9 @@ dependencies = [ [[package]] name = "cranelift-srcgen" -version = "0.128.4" +version = "0.130.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "903adeaf4938e60209a97b53a2e4326cd2d356aab9764a1934630204bae381c9" +checksum = "948865622f87f30907bb46fbb081b235ae63c1896a99a83c26a003305c1fa82d" [[package]] name = "crc32fast" @@ -1178,7 +1168,7 @@ dependencies = [ "deno_semver", "serde", "serde_json", - "thiserror 2.0.19", + "thiserror 2.0.20", ] [[package]] @@ -1194,27 +1184,27 @@ dependencies = [ "monch", "once_cell", "serde", - "thiserror 2.0.19", + "thiserror 2.0.20", "url", ] [[package]] name = "deno_tool" -version = "0.15.10" +version = "0.15.11" dependencies = [ "extism-pdk", "proto_pdk", - "proto_pdk_test_utils 0.47.0", + "proto_pdk_test_utils", "schematic", "serde", - "starbase_sandbox 0.11.1", + "starbase_sandbox", "tokio", "tool_common", ] [[package]] name = "deno_toolchain" -version = "1.1.0" +version = "1.1.1" dependencies = [ "deno_tool", "extism-pdk", @@ -1225,8 +1215,8 @@ dependencies = [ "schematic", "serde", "serde_json", - "starbase_sandbox 0.11.1", - "starbase_utils 0.13.8", + "starbase_sandbox", + "starbase_utils 0.14.2", "tokio", "toolchain_common", ] @@ -1403,13 +1393,13 @@ dependencies = [ [[package]] name = "displaydoc" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] @@ -1438,16 +1428,36 @@ version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" +[[package]] +name = "dotnet_toolchain" +version = "0.1.0" +dependencies = [ + "extism-pdk", + "moon_common", + "moon_config", + "moon_pdk", + "moon_pdk_api", + "moon_pdk_test_utils", + "moon_target", + "schematic", + "serde", + "serde_json", + "starbase_sandbox 0.11.1", + "starbase_utils 0.13.8", + "tokio", + "toolchain_common", +] + [[package]] name = "download_extension" -version = "1.0.2" +version = "1.0.3" dependencies = [ "extension_common", "extism-pdk", "moon_pdk", "moon_pdk_api", "moon_pdk_test_utils", - "starbase_sandbox 0.11.1", + "starbase_sandbox", "tokio", ] @@ -1474,9 +1484,9 @@ dependencies = [ [[package]] name = "either" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" +checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" [[package]] name = "embedded-io" @@ -1523,7 +1533,7 @@ dependencies = [ [[package]] name = "example_tool" -version = "0.0.2" +version = "0.0.3" dependencies = [ "extism-pdk", "proto_pdk", @@ -1542,14 +1552,14 @@ dependencies = [ "moon_pdk", "rustc-hash", "serde", - "starbase_utils 0.13.8", + "starbase_utils 0.14.2", ] [[package]] name = "extism" -version = "1.21.0" +version = "1.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed8c5859bdab81d2eb4cd963eeacd8031d353b1ffb2fde43ee9179a0d6295120" +checksum = "4b66cd9ac5c64b49c9bac69db3d1b10d8f9386e7caab73489c2f197ba43d5e05" dependencies = [ "anyhow", "async-trait", @@ -1638,12 +1648,6 @@ dependencies = [ "syn 2.0.119", ] -[[package]] -name = "fallible-iterator" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" - [[package]] name = "fastrand" version = "2.5.0" @@ -1673,9 +1677,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +checksum = "26b73573e6edcd2af0cdf47bd6cb58f0b3839491263c314eaad1ccf24430e1de" [[package]] name = "fixedbitset" @@ -1711,9 +1715,9 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "foldhash" -version = "0.1.5" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" [[package]] name = "foreign-types" @@ -1943,11 +1947,12 @@ dependencies = [ [[package]] name = "gimli" -version = "0.32.3" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" +checksum = "0bf7f043f89559805f8c7cacc432749b2fa0d0a0a9ee46ce47164ed5ba7f126c" dependencies = [ - "fallible-iterator", + "fnv", + "hashbrown 0.16.1", "indexmap", "stable_deref_trait", ] @@ -1960,9 +1965,9 @@ checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" [[package]] name = "globset" -version = "0.4.19" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e47d37d2ae4464254884b60ab7071be2b876a9c35b696bd018ddcc76847309cd" +checksum = "07c34a9410465b45bd9787443bc7370f37735bad04b0f0cd57ff1a3186c98988" dependencies = [ "aho-corasick", "bstr", @@ -1984,22 +1989,22 @@ dependencies = [ [[package]] name = "go_tool" -version = "0.16.7" +version = "0.16.8" dependencies = [ "extism-pdk", "proto_pdk", - "proto_pdk_api 0.32.1", - "proto_pdk_test_utils 0.47.0", + "proto_pdk_api", + "proto_pdk_test_utils", "schematic", "serde", - "starbase_sandbox 0.11.1", + "starbase_sandbox", "tokio", "tool_common", ] [[package]] name = "go_toolchain" -version = "1.4.4" +version = "1.5.0" dependencies = [ "extism-pdk", "go_tool", @@ -2012,8 +2017,8 @@ dependencies = [ "schematic", "serde", "serde_json", - "starbase_sandbox 0.11.1", - "starbase_utils 0.13.8", + "starbase_sandbox", + "starbase_utils 0.14.2", "tokio", "toolchain_common", ] @@ -2054,12 +2059,13 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.5" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ "foldhash", "serde", + "serde_core", ] [[package]] @@ -2093,9 +2099,9 @@ dependencies = [ [[package]] name = "http" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" dependencies = [ "bytes", "itoa", @@ -2135,9 +2141,9 @@ dependencies = [ [[package]] name = "http-cache" -version = "1.0.0-alpha.6" +version = "1.0.0-alpha.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d01e0b5d3afe17eadd68dad863adc0715b7ccfa887effbb9ea4de3c7c78c6c44" +checksum = "e5578f9c6c2ad8403802d209d1ffe8a2a15d3f89bfaabca9deaa977947dba241" dependencies = [ "bytes", "cacache", @@ -2150,6 +2156,7 @@ dependencies = [ "log", "pin-project-lite", "postcard", + "redb", "serde", "tokio", "url", @@ -2157,9 +2164,9 @@ dependencies = [ [[package]] name = "http-cache-reqwest" -version = "1.0.0-alpha.6" +version = "1.0.0-alpha.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff1fa00fd5b0d38c26d5f24f1cf513e286a988b57880c2bf357304669268fa3" +checksum = "8b8eff2dd41dcc485744bd8ce476cf0fe2df289ba8b49c1750b7c887ef7a07b5" dependencies = [ "anyhow", "async-trait", @@ -2216,9 +2223,9 @@ checksum = "140a09c9305e6d5e557e2ed7cbc68e05765a7d4213975b87cb04920689cc6219" [[package]] name = "hybrid-array" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" +checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" dependencies = [ "typenum", ] @@ -2439,9 +2446,9 @@ dependencies = [ [[package]] name = "ignore" -version = "0.4.31" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f8a7b8211e695a1d0cd91cace480d4d0bd57667ab10277cc412c5f7f4884f83" +checksum = "00b69833ed729dc5aa7d19541d96d6cf8e9137194207a04916d658e43168402f" dependencies = [ "crossbeam-deque", "globset", @@ -2536,9 +2543,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.12.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" +checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" [[package]] name = "is_ci" @@ -2592,23 +2599,23 @@ dependencies = [ [[package]] name = "java_tool" -version = "0.1.0" +version = "0.1.1" dependencies = [ "extism-pdk", "proto_pdk", - "proto_pdk_api 0.32.1", - "proto_pdk_test_utils 0.47.0", + "proto_pdk_api", + "proto_pdk_test_utils", "schematic", "serde", "serde_json", - "starbase_sandbox 0.11.1", + "starbase_sandbox", "tokio", "tool_common", ] [[package]] name = "javascript_toolchain" -version = "1.2.1" +version = "1.3.0" dependencies = [ "deno_lockfile", "extism-pdk", @@ -2626,29 +2633,13 @@ dependencies = [ "serde", "serde_json", "shell-words", - "starbase_sandbox 0.11.1", - "starbase_utils 0.13.8", + "starbase_sandbox", + "starbase_utils 0.14.2", "tokio", "toolchain_common", "yarn-lock-parser", ] -[[package]] -name = "jni" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" -dependencies = [ - "cesu8", - "cfg-if", - "combine", - "jni-sys 0.3.1", - "log", - "thiserror 1.0.69", - "walkdir", - "windows-sys 0.45.0", -] - [[package]] name = "jni" version = "0.22.4" @@ -2658,10 +2649,10 @@ dependencies = [ "cfg-if", "combine", "jni-macros", - "jni-sys 0.4.1", + "jni-sys", "log", "simd_cesu8", - "thiserror 2.0.19", + "thiserror 2.0.20", "walkdir", "windows-link", ] @@ -2679,15 +2670,6 @@ dependencies = [ "syn 2.0.119", ] -[[package]] -name = "jni-sys" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" -dependencies = [ - "jni-sys 0.4.1", -] - [[package]] name = "jni-sys" version = "0.4.1" @@ -2719,9 +2701,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.103" +version = "0.3.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" dependencies = [ "cfg-if", "futures-util", @@ -2730,9 +2712,9 @@ dependencies = [ [[package]] name = "json-strip-comments" -version = "3.1.1" +version = "3.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9301b34ecbe81051a62001a2dfa56d906628efdfbc68153e0a4d5eba58181ece" +checksum = "01ed659c5f428031b9c1fb7de6729af2eb491654d4bcd416a5b5b704986b9658" dependencies = [ "memchr", ] @@ -2783,10 +2765,18 @@ name = "lang_javascript_common" version = "0.1.0" dependencies = [ "nodejs_package_json", - "proto_pdk_api 0.32.1", + "proto_pdk_api", "serde", "serde_json", - "starbase_utils 0.13.8", + "starbase_utils 0.14.2", +] + +[[package]] +name = "lang_rust_common" +version = "0.1.0" +dependencies = [ + "extism-pdk", + "proto_pdk", ] [[package]] @@ -2815,24 +2805,24 @@ checksum = "34b357333733e8260735ba5894eb928c02ecc69c78715f01a8019e7fa7f2db4c" [[package]] name = "libc" -version = "0.2.188" +version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22053b6a34f84abc97f9129e61334f40174659a1b9bd18c970b83db6a9a6348b" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] name = "liblzma" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45aec2360b3933207e27908049d8e4df4e476b58180afb1e56b2a4fb72efe4ba" +checksum = "2fe0a34ca854fd4f20c07f696fc8675aec78f87d88d29f5e10257a7490a1b2e1" dependencies = [ "liblzma-sys", ] [[package]] name = "liblzma-sys" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a046c7f353ba30f810545151e04f63545833803f5b86ee3ddf1517247fe560a5" +checksum = "a0dad045e4b1b7b170be4b60b54b780cafb4490165461bac7d1cf7b703f61d5f" dependencies = [ "cc", "libc", @@ -2847,9 +2837,9 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.18" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" +checksum = "2026a5056764a10b2bf5d56488cba40da507f5493a6a429340e2004d9ed085fa" dependencies = [ "libc", ] @@ -3026,7 +3016,7 @@ dependencies = [ [[package]] name = "migrate_nx_extension" -version = "1.0.3" +version = "1.0.4" dependencies = [ "extension_common", "extism-pdk", @@ -3039,14 +3029,14 @@ dependencies = [ "rustc-hash", "serde", "serde_json", - "starbase_sandbox 0.11.1", - "starbase_utils 0.13.8", + "starbase_sandbox", + "starbase_utils 0.14.2", "tokio", ] [[package]] name = "migrate_turborepo_extension" -version = "1.0.3" +version = "1.0.4" dependencies = [ "extension_common", "extism-pdk", @@ -3058,8 +3048,8 @@ dependencies = [ "moon_target", "rustc-hash", "serde", - "starbase_sandbox 0.11.1", - "starbase_utils 0.13.8", + "starbase_sandbox", + "starbase_utils 0.14.2", "tokio", ] @@ -3111,9 +3101,9 @@ checksum = "6bcc6ad3b93f756f2532d29f7c7291b8d246a2c460a99a3611327bb726830014" [[package]] name = "moon_common" -version = "2.0.7" +version = "2.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af0ed007c51e64a32e290e10b3cd7692814d69fb37522b2bfd40439369bd5ffa" +checksum = "bae7ab089aab9153270403e5b7546c5d521e078e2c20b42c8691ae41395d1d96" dependencies = [ "dirs", "miette 7.6.0", @@ -3123,14 +3113,14 @@ dependencies = [ "serde", "starbase_id", "starbase_styles", - "thiserror 2.0.19", + "thiserror 2.0.20", ] [[package]] name = "moon_config" -version = "2.1.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "222902244f1af1e0e6c4124ac7022e46ead76cfe26379cc73a90a0e2dfe22d69" +checksum = "54782d87fbfe5f1de3fdbf22193183c6e226c77f1cdf951e6115c4122477bea0" dependencies = [ "deserialize_untagged_verbose_error", "indexmap", @@ -3141,10 +3131,9 @@ dependencies = [ "rpkl", "rustc-hash", "schematic", - "semver", "serde", "serde_json", - "version_spec 0.10.3", + "version_spec", "warpgate_api", ] @@ -3167,14 +3156,14 @@ dependencies = [ "moon_feature_flags", "serde", "starbase_utils 0.13.8", - "thiserror 2.0.19", + "thiserror 2.0.20", ] [[package]] name = "moon_pdk" -version = "2.0.4" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ebaf7567779b4ebc0f97e59d73615c28ef7124a8fec279ea70ce01202270815" +checksum = "366b913031f8f4fa3383069e7d0b49f7f89f1c1f0f2c3a32b4229748162fc763" dependencies = [ "clap", "extism-pdk", @@ -3190,16 +3179,16 @@ dependencies = [ [[package]] name = "moon_pdk_api" -version = "2.0.4" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26f7e646a6227abd2069633078cc5a896c1e199fb6622ce03f2483b9cb08ffb" +checksum = "e85fa8551e29ac31db1c19d4914a101328a8c57cc4fa5d1db3ed402899bb97fc" dependencies = [ "derive_setters", "moon_common", "moon_config", "moon_project", "moon_task", - "proto_pdk_api 0.31.13", + "proto_pdk_api", "rustc-hash", "schematic", "serde", @@ -3209,26 +3198,26 @@ dependencies = [ [[package]] name = "moon_pdk_test_utils" -version = "2.0.4" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "734fa3d893e1a09967530bf843e4ddea01e8878584b7c2fc90daeb479436e70a" +checksum = "35f11fa31154e4233527500490a68fe350716e1343cd190834feebfe4582341f" dependencies = [ "extism", "moon_pdk_api", "moon_target", - "proto_core 0.56.4", - "proto_pdk_test_utils 0.44.2", + "proto_core", + "proto_pdk_test_utils", "serde", "serde_json", - "starbase_sandbox 0.11.1", - "warpgate 0.30.5", + "starbase_sandbox", + "warpgate", ] [[package]] name = "moon_project" -version = "2.0.6" +version = "2.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f699261f45112dab6e89e61e013a606f2a68122ede96b28ee13f7ff92c4a2581" +checksum = "ffc394cf3c1729d10c891f946e49b1ab38bf9bba68dd5b9bf0489802c50ed51b" dependencies = [ "miette 7.6.0", "moon_common", @@ -3236,30 +3225,30 @@ dependencies = [ "moon_file_group", "moon_task", "serde", - "thiserror 2.0.19", + "thiserror 2.0.20", ] [[package]] name = "moon_target" -version = "3.0.0" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73fbdd8e17bb29ca8042b62a133211a163ec320536be1c4f2e12a054612c8f1a" +checksum = "d571381d1a0d8db6c87958f035e60e69fa6f5488c977c90bd5b6a917d29ea7d9" dependencies = [ - "compact_str 0.9.1", + "compact_str 0.10.0", "miette 7.6.0", "moon_common", "regex", "schematic", "serde", - "thiserror 2.0.19", + "thiserror 2.0.20", "tracing", ] [[package]] name = "moon_task" -version = "2.1.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "178fe96a4d401f97ab61367332a31bb3994da9074dbbe46349c02b97b180712c" +checksum = "21cebc233793fe0fa563680ee4b9734b9f267f42719bc93143a57be2665e6ad9" dependencies = [ "miette 7.6.0", "moon_common", @@ -3268,18 +3257,18 @@ dependencies = [ "moon_target", "rustc-hash", "serde", - "starbase_utils 0.13.8", + "starbase_utils 0.14.2", ] [[package]] name = "moon_tool" -version = "0.4.2" +version = "0.4.3" dependencies = [ "extism-pdk", "proto_pdk", - "proto_pdk_test_utils 0.47.0", + "proto_pdk_test_utils", "serde", - "starbase_sandbox 0.11.1", + "starbase_sandbox", "tokio", "tool_common", ] @@ -3303,29 +3292,29 @@ dependencies = [ [[package]] name = "node_depman_tool" -version = "0.19.0" +version = "0.20.1" dependencies = [ "extism-pdk", "lang_javascript_common", "nodejs_package_json", "npmrc-config-rs", "proto_pdk", - "proto_pdk_api 0.32.1", - "proto_pdk_test_utils 0.47.0", + "proto_pdk_api", + "proto_pdk_test_utils", "regex", "rustc-hash", "schematic", "serde", "serde_json", - "starbase_sandbox 0.11.1", - "starbase_utils 0.13.8", + "starbase_sandbox", + "starbase_utils 0.14.2", "tokio", "tool_common", ] [[package]] name = "node_depman_toolchain" -version = "1.0.3" +version = "1.1.0" dependencies = [ "extism-pdk", "moon_config", @@ -3333,37 +3322,37 @@ dependencies = [ "moon_pdk_api", "moon_pdk_test_utils", "node_depman_tool", - "proto_pdk_api 0.32.1", + "proto_pdk_api", "schematic", "serde", "serde_json", - "starbase_sandbox 0.11.1", - "starbase_utils 0.13.8", + "starbase_sandbox", + "starbase_utils 0.14.2", "tokio", "toolchain_common", ] [[package]] name = "node_tool" -version = "0.17.10" +version = "0.17.12" dependencies = [ "extism-pdk", "lang_javascript_common", "nodejs_package_json", "proto_pdk", - "proto_pdk_test_utils 0.47.0", + "proto_pdk_test_utils", "schematic", "serde", "serial_test", - "starbase_sandbox 0.11.1", - "starbase_utils 0.13.8", + "starbase_sandbox", + "starbase_utils 0.14.2", "tokio", "tool_common", ] [[package]] name = "node_toolchain" -version = "1.0.2" +version = "1.0.4" dependencies = [ "extism-pdk", "moon_common", @@ -3375,8 +3364,8 @@ dependencies = [ "schematic", "serde", "serde_json", - "starbase_sandbox 0.11.1", - "starbase_utils 0.13.8", + "starbase_sandbox", + "starbase_utils 0.14.2", "tokio", "toolchain_common", ] @@ -3393,7 +3382,7 @@ dependencies = [ "semver", "serde", "serde_json", - "thiserror 2.0.19", + "thiserror 2.0.20", ] [[package]] @@ -3432,17 +3421,17 @@ checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" [[package]] name = "npm_backend" -version = "0.1.2" +version = "0.1.3" dependencies = [ "backend_common", "extism-pdk", "proto_pdk", - "proto_pdk_test_utils 0.47.0", + "proto_pdk_test_utils", "rustc-hash", "schematic", "serde", - "starbase_sandbox 0.11.1", - "starbase_utils 0.13.8", + "starbase_sandbox", + "starbase_utils 0.14.2", "tokio", ] @@ -3455,7 +3444,7 @@ dependencies = [ "base64 0.22.1", "dirs", "regex", - "thiserror 2.0.19", + "thiserror 2.0.20", "url", "which", ] @@ -3514,42 +3503,16 @@ dependencies = [ [[package]] name = "object" -version = "0.37.3" +version = "0.38.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +checksum = "271638cd5fa9cca89c4c304675ca658efc4e64a66c716b7cfe1afb4b9611dbbc" dependencies = [ "crc32fast", - "hashbrown 0.15.5", + "hashbrown 0.16.1", "indexmap", "memchr", ] -[[package]] -name = "oci-client" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b7f8deaffcd3b0e3baf93dddcab3d18b91d46dc37d38a8b170089b234de5bb3" -dependencies = [ - "bytes", - "chrono", - "futures-util", - "http", - "http-auth", - "jsonwebtoken", - "lazy_static", - "oci-spec", - "olpc-cjson", - "regex", - "reqwest", - "serde", - "serde_json", - "sha2 0.10.9", - "thiserror 2.0.19", - "tokio", - "tracing", - "unicase", -] - [[package]] name = "oci-client" version = "0.17.0" @@ -3571,7 +3534,7 @@ dependencies = [ "serde", "serde_json", "sha2 0.11.0", - "thiserror 2.0.19", + "thiserror 2.0.20", "tokio", "tracing", "unicase", @@ -3591,7 +3554,7 @@ dependencies = [ "serde_json", "strum", "strum_macros", - "thiserror 2.0.19", + "thiserror 2.0.20", ] [[package]] @@ -3772,9 +3735,9 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" -version = "2.8.7" +version = "2.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47627dd7305c6a2d6c8c6bcd24c5a4c17dbbf425f4f9c5313e724b38fc9782e9" +checksum = "7df728be843c7070fab6ab7c328c4e9e9d78e23bf749c0669c86ee7ebfa050a2" dependencies = [ "memchr", "ucd-trie", @@ -3782,9 +3745,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.8.7" +version = "2.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b4254325ecad416ab689e27ba51da03ba01a9632bc6e108f5fe7c3c4ad29d58" +checksum = "9e2dd6fc3b26b3462ee188aac870f5a41d398f1cd5e2408d16531bd71c9591fd" dependencies = [ "pest", "pest_generator", @@ -3792,9 +3755,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.8.7" +version = "2.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c4c0e91ead7a8f7acecbca6f003fc2e8282b1dbe2dd9c9d2f16aba42995e0a7" +checksum = "6a7a9205cfb6f596a9e8b689c0a15f9ceb7a1aafae7aaf788150ac65b29975b6" dependencies = [ "pest", "pest_meta", @@ -3805,9 +3768,9 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.8.7" +version = "2.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9744bc48116fee06334924bb5f2bad41eed5e89bd26e29b0b799f9a3f82c210" +checksum = "85abd351c0de1e8384fc791a0737111a350394937e92b956b743dac12429f57c" dependencies = [ "pest", ] @@ -4003,52 +3966,9 @@ dependencies = [ [[package]] name = "proto_core" -version = "0.56.4" +version = "0.60.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34127fc437c3aacbb37ce45db87ce84037f5a35d460f06a927c26f531a4bafaf" -dependencies = [ - "convert_case 0.11.0", - "docker_credential", - "dotenvy", - "indexmap", - "iocraft", - "miette 7.6.0", - "minisign-verify", - "oci-client 0.16.1", - "once_cell", - "proto_pdk_api 0.31.13", - "proto_shim", - "regex", - "reqwest", - "rustc-hash", - "scc", - "schematic", - "semver", - "serde", - "serde_json", - "sha2 0.10.9", - "shell-words", - "starbase_archive 0.13.2", - "starbase_console", - "starbase_shell", - "starbase_styles", - "starbase_utils 0.13.8", - "system_env", - "thiserror 2.0.19", - "tokio", - "toml_edit", - "tracing", - "url", - "uuid", - "version_spec 0.10.3", - "warpgate 0.30.5", -] - -[[package]] -name = "proto_core" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a1838c7d0223c8403b5353869549e19996952a60e38079c8dcff7eb1d46a5d7" +checksum = "69a33f00c01ec2a7cb6cb33a31976213e095750db566fc7ee3ba001acb8aad4d" dependencies = [ "ai_env", "convert_case 0.11.0", @@ -4058,9 +3978,9 @@ dependencies = [ "iocraft", "miette 7.6.0", "minisign-verify", - "oci-client 0.17.0", + "oci-client", "once_cell", - "proto_pdk_api 0.32.1", + "proto_pdk_api", "proto_shim", "regex", "reqwest", @@ -4070,30 +3990,30 @@ dependencies = [ "serde", "serde_json", "shell-words", - "starbase_archive 0.14.4", + "starbase_archive", "starbase_console", "starbase_shell", "starbase_styles", - "starbase_utils 0.14.1", + "starbase_utils 0.14.2", "system_env", - "thiserror 2.0.19", + "thiserror 2.0.20", "tokio", "toml_edit", "tracing", "url", "uuid", - "version_spec 0.11.2", - "warpgate 0.34.0", + "version_spec", + "warpgate", ] [[package]] name = "proto_pdk" -version = "0.33.0" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0030c90652835da05c1839ebb5e7cb8995bfae776bf340f5869aff530a230ea" +checksum = "7c6984c7bc56c118dad0f597bfbce17429831b46f27f88c54e2367ff671fbdc3" dependencies = [ "extism-pdk", - "proto_pdk_api 0.32.1", + "proto_pdk_api", "rustc-hash", "serde", "warpgate_pdk", @@ -4101,27 +4021,9 @@ dependencies = [ [[package]] name = "proto_pdk_api" -version = "0.31.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50ac6c5862ce4f4b6ac4c6146807b8fb94b12790839005af4a91edf548e97176" -dependencies = [ - "derive_setters", - "rustc-hash", - "schematic", - "semver", - "serde", - "serde_json", - "system_env", - "thiserror 2.0.19", - "version_spec 0.10.3", - "warpgate_api", -] - -[[package]] -name = "proto_pdk_api" -version = "0.32.1" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e00f08f99b3d05d81133b6ebb9077bc6e1b8a2718182892022d657e706161f4e" +checksum = "1ab11b9f81482b9233620631855e59e7761f8c1cabb861eb9853f02b2bdaae58" dependencies = [ "derive_setters", "rustc-hash", @@ -4129,46 +4031,31 @@ dependencies = [ "serde", "serde_json", "system_env", - "thiserror 2.0.19", - "version_spec 0.11.2", + "thiserror 2.0.20", + "version_spec", "warpgate_api", ] [[package]] name = "proto_pdk_test_utils" -version = "0.44.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05136d0efc40a87d85241a7bcb6a8940b6cf21aa93f72b75ba874a016c6570a3" -dependencies = [ - "extism", - "proto_core 0.56.4", - "proto_pdk_api 0.31.13", - "serde", - "serde_json", - "starbase_sandbox 0.11.1", - "warpgate 0.30.5", -] - -[[package]] -name = "proto_pdk_test_utils" -version = "0.47.0" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7ecdd0834f5d2b54ce06beb900b7ef1ae3cd34a2ac3c3146821beb636ab0b47" +checksum = "a2876fdc4762a98c8b98d333cca84eaf46c48afb9795a89fdbca29e7163c3ce6" dependencies = [ "extism", - "proto_core 0.59.0", - "proto_pdk_api 0.32.1", + "proto_core", + "proto_pdk_api", "serde", "serde_json", - "starbase_sandbox 0.12.0", - "warpgate 0.34.0", + "starbase_sandbox", + "warpgate", ] [[package]] name = "proto_shim" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3296719b871c47a3e81b371353cf689a16bf40b8cb5e9a0f3603239218656634" +checksum = "93bf09c099fd93508eceece6742b5d68d5e6c0c0f4804fd35c883f7de5b7fb66" dependencies = [ "dirs", "windows-sys 0.61.2", @@ -4176,7 +4063,7 @@ dependencies = [ [[package]] name = "proto_tool" -version = "0.5.7" +version = "0.5.8" dependencies = [ "extism-pdk", "proto_pdk", @@ -4186,21 +4073,21 @@ dependencies = [ [[package]] name = "pulley-interpreter" -version = "41.0.4" +version = "43.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9812652c1feb63cf39f8780cecac154a32b22b3665806c733cd4072547233a4" +checksum = "7ec12fe19a9588315a49fe5704502a9c02d6a198303314b0c7c86123b06d29e5" dependencies = [ "cranelift-bitset", "log", "pulley-macros", - "wasmtime-internal-math", + "wasmtime-internal-core", ] [[package]] name = "pulley-macros" -version = "41.0.4" +version = "43.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56000349b6896e3d44286eb9c330891237f40b27fd43c1ccc84547d0b463cb40" +checksum = "36f7d5ef31ebf1b46cd7e722ffef934e670d7e462f49aa01cde07b9b76dca580" dependencies = [ "proc-macro2", "quote", @@ -4217,13 +4104,13 @@ dependencies = [ "pep440_rs", "pep508_rs", "serde", - "thiserror 2.0.19", + "thiserror 2.0.20", "toml 0.9.12+spec-1.1.0", ] [[package]] name = "python_pip_toolchain" -version = "0.1.2" +version = "0.1.4" dependencies = [ "extism-pdk", "moon_config", @@ -4235,22 +4122,22 @@ dependencies = [ [[package]] name = "python_poetry_tool" -version = "0.1.8" +version = "0.1.9" dependencies = [ "extism-pdk", "proto_pdk", - "proto_pdk_test_utils 0.47.0", + "proto_pdk_test_utils", "serde", - "starbase_sandbox 0.11.1", - "starbase_utils 0.13.8", + "starbase_sandbox", + "starbase_utils 0.14.2", "tokio", - "toml 1.1.3+spec-1.1.0", + "toml 1.1.4+spec-1.1.0", "tool_common", ] [[package]] name = "python_poetry_toolchain" -version = "0.1.0" +version = "0.1.2" dependencies = [ "extism-pdk", "moon_config", @@ -4263,21 +4150,21 @@ dependencies = [ [[package]] name = "python_tool" -version = "0.14.8" +version = "0.14.9" dependencies = [ "extism-pdk", "proto_pdk", - "proto_pdk_test_utils 0.47.0", + "proto_pdk_test_utils", "regex", "serde", - "starbase_sandbox 0.11.1", + "starbase_sandbox", "tokio", "tool_common", ] [[package]] name = "python_toolchain" -version = "0.2.0" +version = "0.3.0" dependencies = [ "extism-pdk", "moon_common", @@ -4292,29 +4179,29 @@ dependencies = [ "schematic", "serde", "serde_json", - "starbase_sandbox 0.11.1", - "starbase_utils 0.13.8", + "starbase_sandbox", + "starbase_utils 0.14.2", "tokio", "toolchain_common", ] [[package]] name = "python_uv_tool" -version = "0.3.3" +version = "0.3.4" dependencies = [ "extism-pdk", "proto_pdk", - "proto_pdk_test_utils 0.47.0", + "proto_pdk_test_utils", "serde", - "starbase_sandbox 0.11.1", + "starbase_sandbox", "tokio", - "toml 1.1.3+spec-1.1.0", + "toml 1.1.4+spec-1.1.0", "tool_common", ] [[package]] name = "python_uv_toolchain" -version = "0.1.3" +version = "0.1.5" dependencies = [ "extism-pdk", "moon_config", @@ -4339,7 +4226,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2", - "thiserror 2.0.19", + "thiserror 2.0.20", "tokio", "tracing", "web-time", @@ -4362,7 +4249,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.19", + "thiserror 2.0.20", "tinyvec", "tracing", "web-time", @@ -4482,6 +4369,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "redb" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e925444704b5f17d32bf42f5b6e2df050bceebc3dcd6e71cc73dafe8092e839" +dependencies = [ + "libc", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -4510,7 +4406,7 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.17", "libredox", - "thiserror 2.0.19", + "thiserror 2.0.20", ] [[package]] @@ -4527,13 +4423,13 @@ dependencies = [ [[package]] name = "regalloc2" -version = "0.13.5" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08effbc1fa53aaebff69521a5c05640523fab037b34a4a2c109506bc938246fa" +checksum = "757712e8e61590d6d4f5d563483755538b5aa13467837a3b41cd9832509a7f85" dependencies = [ "allocator-api2", "bumpalo", - "hashbrown 0.15.5", + "hashbrown 0.17.1", "log", "rustc-hash", "smallvec", @@ -4553,9 +4449,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.16" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" dependencies = [ "aho-corasick", "memchr", @@ -4605,7 +4501,7 @@ dependencies = [ "quinn", "rustls", "rustls-pki-types", - "rustls-platform-verifier 0.7.0", + "rustls-platform-verifier", "serde", "serde_json", "serde_urlencoded", @@ -4635,7 +4531,7 @@ dependencies = [ "http", "reqwest", "serde", - "thiserror 2.0.19", + "thiserror 2.0.20", "tower-service", ] @@ -4654,7 +4550,7 @@ dependencies = [ "reqwest", "reqwest-middleware", "retry-policies", - "thiserror 2.0.19", + "thiserror 2.0.20", "tokio", "tracing", "wasmtimer", @@ -4731,20 +4627,20 @@ checksum = "64f53e5272cd467a89ad1fa6d66273e3d720477d51875de84e2db7218e370896" [[package]] name = "ruby_tool" -version = "0.2.8" +version = "0.2.9" dependencies = [ "extism-pdk", "proto_pdk", - "proto_pdk_test_utils 0.47.0", + "proto_pdk_test_utils", "serde", - "starbase_sandbox 0.11.1", + "starbase_sandbox", "tokio", "tool_common", ] [[package]] name = "ruby_toolchain" -version = "0.1.0" +version = "0.1.1" dependencies = [ "extism-pdk", "moon_common", @@ -4757,8 +4653,8 @@ dependencies = [ "schematic", "serde", "serde_json", - "starbase_sandbox 0.11.1", - "starbase_utils 0.13.8", + "starbase_sandbox", + "starbase_utils 0.14.2", "tokio", "toolchain_common", ] @@ -4775,22 +4671,23 @@ dependencies = [ [[package]] name = "rust_tool" -version = "0.13.9" +version = "0.13.12" dependencies = [ "extism-pdk", + "lang_rust_common", "proto_pdk", - "proto_pdk_test_utils 0.47.0", + "proto_pdk_test_utils", "serde", - "starbase_sandbox 0.11.1", - "starbase_utils 0.13.8", + "starbase_sandbox", + "starbase_utils 0.14.2", "tokio", - "toml 1.1.3+spec-1.1.0", + "toml 1.1.4+spec-1.1.0", "tool_common", ] [[package]] name = "rust_toolchain" -version = "1.0.6" +version = "1.0.9" dependencies = [ "cargo-lock", "cargo_toml", @@ -4805,8 +4702,8 @@ dependencies = [ "schematic", "serde", "serde_json", - "starbase_sandbox 0.11.1", - "starbase_utils 0.13.8", + "starbase_sandbox", + "starbase_utils 0.14.2", "tokio", "toolchain_common", ] @@ -4870,9 +4767,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.42" +version = "0.23.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c54fcab019b409d04215d3a17cb438fd7fbf192ee61461f20f4fe18704bc138" +checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" dependencies = [ "aws-lc-rs", "log", @@ -4898,35 +4795,14 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.15.0" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" dependencies = [ "web-time", "zeroize", ] -[[package]] -name = "rustls-platform-verifier" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" -dependencies = [ - "core-foundation", - "core-foundation-sys", - "jni 0.21.1", - "log", - "once_cell", - "rustls", - "rustls-native-certs", - "rustls-platform-verifier-android", - "rustls-webpki", - "security-framework", - "security-framework-sys", - "webpki-root-certs", - "windows-sys 0.61.2", -] - [[package]] name = "rustls-platform-verifier" version = "0.7.0" @@ -4935,7 +4811,7 @@ checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" dependencies = [ "core-foundation", "core-foundation-sys", - "jni 0.22.4", + "jni", "log", "once_cell", "rustls", @@ -4995,9 +4871,9 @@ dependencies = [ [[package]] name = "scc" -version = "3.8.5" +version = "3.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f733aa28b85255811ad1358d559fe9a182e39327cf5470140ed9d444de86e6d5" +checksum = "f8af0b99483d1c3e59471d4f0cb58b244169436a8979c889a91a3f697075ea01" dependencies = [ "saa", "sdd", @@ -5014,15 +4890,15 @@ dependencies = [ [[package]] name = "schema_tool" -version = "0.18.1" +version = "0.18.2" dependencies = [ "extism-pdk", "proto_pdk", - "proto_pdk_test_utils 0.47.0", + "proto_pdk_test_utils", "regex", "serde", "serde_json", - "starbase_sandbox 0.11.1", + "starbase_sandbox", "tokio", "tool_common", ] @@ -5042,15 +4918,14 @@ dependencies = [ "rpkl", "schematic_macros", "schematic_types", - "semver", "serde", "serde-content", "serde_json", "serde_norway", "serde_path_to_error", "starbase_styles", - "thiserror 2.0.19", - "toml 1.1.3+spec-1.1.0", + "thiserror 2.0.20", + "toml 1.1.4+spec-1.1.0", "tracing", ] @@ -5076,11 +4951,10 @@ dependencies = [ "indexmap", "regex", "rpkl", - "semver", "serde", "serde_json", "serde_norway", - "toml 1.1.3+spec-1.1.0", + "toml 1.1.4+spec-1.1.0", "url", ] @@ -5188,7 +5062,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 3.0.2", + "syn 3.0.3", ] [[package]] @@ -5265,9 +5139,9 @@ dependencies = [ [[package]] name = "serial_test" -version = "3.5.0" +version = "4.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "699f4197115b8a7e7ff19c9a315a4bd6fffec26cc4626ef45ecaea389e081c6d" +checksum = "a6df5ed973ad8d834e09f824f9e9f449af6b9a3745f78dec7cc752770bd3bf11" dependencies = [ "futures-executor", "futures-util", @@ -5279,13 +5153,13 @@ dependencies = [ [[package]] name = "serial_test_derive" -version = "3.5.0" +version = "4.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94e153fc76e1c6a068703d6d29c508a0b15c061c4b7e43da59cc097bc342673c" +checksum = "a22144e767da4ddd8416dbf383700542ffd8a5dc493dfecedfe1fe3ad03c98ae" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] @@ -5343,9 +5217,9 @@ dependencies = [ [[package]] name = "shell-quote" -version = "0.7.2" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb502615975ae2365825521fa1529ca7648fd03ce0b0746604e0683856ecd7e4" +checksum = "6250294ab161cc8ed5d110114cec70d4cec4e0135edc58e7362d094244edf54a" [[package]] name = "shell-words" @@ -5511,26 +5385,6 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" -[[package]] -name = "starbase_archive" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a244a519b24166278276743125b76f958ca00a6cdfb488ab35411beb444801b5" -dependencies = [ - "binstall-tar", - "bzip2", - "flate2", - "liblzma", - "miette 7.6.0", - "rustc-hash", - "starbase_styles", - "starbase_utils 0.13.8", - "thiserror 2.0.19", - "tracing", - "zip", - "zstd", -] - [[package]] name = "starbase_archive" version = "0.14.4" @@ -5544,8 +5398,8 @@ dependencies = [ "miette 7.6.0", "rustc-hash", "starbase_styles", - "starbase_utils 0.14.1", - "thiserror 2.0.19", + "starbase_utils 0.14.2", + "thiserror 2.0.20", "tracing", "zip", "zstd", @@ -5553,9 +5407,9 @@ dependencies = [ [[package]] name = "starbase_console" -version = "0.6.33" +version = "0.6.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6530b0f4bd6cef5c945dcb277cc91f980dac23d322832bc0dfbd2c4457d5f600" +checksum = "fd3e97b059b90ddb6e48168e12034ef5a126eaebe656c0b172529c2b66d65adf" dependencies = [ "crossterm", "iocraft", @@ -5564,7 +5418,7 @@ dependencies = [ "serde", "serde_json", "starbase_styles", - "thiserror 2.0.19", + "thiserror 2.0.20", "tokio", "tracing", ] @@ -5580,21 +5434,7 @@ dependencies = [ "regex", "schematic", "serde", - "thiserror 2.0.19", -] - -[[package]] -name = "starbase_sandbox" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a52a223bc675989a16257e3cc30fd160ba1ac6871596009782758adca038208c" -dependencies = [ - "assert_cmd", - "assert_fs", - "insta", - "predicates", - "pretty_assertions", - "starbase_utils 0.13.8", + "thiserror 2.0.20", ] [[package]] @@ -5608,21 +5448,21 @@ dependencies = [ "insta", "predicates", "pretty_assertions", - "starbase_utils 0.14.1", + "starbase_utils 0.14.2", ] [[package]] name = "starbase_shell" -version = "0.12.7" +version = "0.12.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "429af8cac86fea24c02ec987dc305df7e86458a0b737840c6a3b5c50f6c4fd0d" +checksum = "faa99da498ff199c18eb754d315429157b2cb8ccea3f5685d883a0d4b24ec5c4" dependencies = [ - "base64 0.22.1", + "base64 0.23.1", "miette 7.6.0", "regex", "shell-quote", "sysinfo", - "thiserror 2.0.19", + "thiserror 2.0.20", "tracing", ] @@ -5644,50 +5484,43 @@ version = "0.13.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f8804a32cd7c654c3321022fc1b05314d7d2fbf6781f8d19d6e4c97488cffd8" dependencies = [ - "async-trait", "dirs", "ec4rs", - "json-strip-comments", "jwalk", "miette 7.6.0", "reflink-copy", - "reqwest", "scc", - "serde", - "serde_json", - "serde_norway", "starbase_styles", - "thiserror 2.0.19", - "tokio", - "toml 1.1.3+spec-1.1.0", + "thiserror 2.0.20", "tracing", - "url", "wax", ] [[package]] name = "starbase_utils" -version = "0.14.1" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "068a9cf0dd7252c656d26ceca3b146c4d3baf13d0a683169952e4badea60925e" +checksum = "879e06a90157702a315c6859585349cd9c5219f2bdf0288e0ac85a3bb5c9234c" dependencies = [ "async-trait", - "base64 0.22.1", + "base64 0.23.1", "dirs", + "ec4rs", "hex", "json-strip-comments", "jwalk", "miette 7.6.0", "reflink-copy", "reqwest", + "scc", "serde", "serde_json", "serde_norway", "sha2 0.11.0", "starbase_styles", - "thiserror 2.0.19", + "thiserror 2.0.20", "tokio", - "toml 1.1.3+spec-1.1.0", + "toml 1.1.4+spec-1.1.0", "tracing", "url", "wax", @@ -5761,9 +5594,9 @@ dependencies = [ [[package]] name = "syn" -version = "3.0.2" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a207d6d6a2b7fc470b80443726053f18a2481b7e1eee970597051596567987a3" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" dependencies = [ "proc-macro2", "quote", @@ -5822,21 +5655,21 @@ dependencies = [ [[package]] name = "system_env" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4d7bf944aae854a6a613b8af7a4d044def0fbbc763dcea7e1acf84e331d3527" +checksum = "325a3f70fd3a025f4b583cd8eaa90fbabf61493f690815d7148df778bb4839ac" dependencies = [ "regex", "schematic", "serde", "serde_json", "shell-words", - "thiserror 2.0.19", + "thiserror 2.0.20", ] [[package]] name = "system_toolchain" -version = "1.0.2" +version = "1.0.3" dependencies = [ "extism-pdk", "moon_pdk_api", @@ -5901,11 +5734,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.19" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" dependencies = [ - "thiserror-impl 2.0.19", + "thiserror-impl 2.0.20", ] [[package]] @@ -5921,13 +5754,13 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.19" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" dependencies = [ "proc-macro2", "quote", - "syn 3.0.2", + "syn 3.0.3", ] [[package]] @@ -5941,9 +5774,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.54" +version = "0.3.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e1d5e639ff6bab73cb6885cc7e7b1de96c3f32c68ec55f3952614bec1092244" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" dependencies = [ "deranged", "num-conv", @@ -6014,13 +5847,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.7.1" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6328af13490e73a9b4694030fafd93f8c8c6a9dede33e821c3fc63eddf8042ba" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] @@ -6045,9 +5878,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.18" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b" dependencies = [ "futures-core", "pin-project-lite", @@ -6085,9 +5918,9 @@ dependencies = [ [[package]] name = "toml" -version = "1.1.3+spec-1.1.0" +version = "1.1.4+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53c96ecdfa941c8fc4fcaed14f99ada8ebed502eef533015095a07e3301d4c3c" +checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5" dependencies = [ "indexmap", "serde_core", @@ -6131,9 +5964,9 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.1.2+spec-1.1.0" +version = "1.1.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" dependencies = [ "winnow 1.0.4", ] @@ -6285,7 +6118,7 @@ checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "typescript_toolchain" -version = "1.1.3" +version = "1.1.4" dependencies = [ "extism-pdk", "moon_common", @@ -6298,8 +6131,8 @@ dependencies = [ "schematic", "serde", "serde_json", - "starbase_sandbox 0.11.1", - "starbase_utils 0.13.8", + "starbase_sandbox", + "starbase_utils 0.14.2", "tokio", "toolchain_common", "typescript_tsconfig_json", @@ -6372,15 +6205,15 @@ checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "unpack_extension" -version = "1.0.2" +version = "1.0.3" dependencies = [ "extension_common", "extism-pdk", "moon_pdk", "moon_pdk_api", "moon_pdk_test_utils", - "starbase_sandbox 0.11.1", - "starbase_utils 0.13.8", + "starbase_sandbox", + "starbase_utils 0.14.2", "tokio", ] @@ -6416,17 +6249,17 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "ureq" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dea7109cdcd5864d4eeb1b58a1648dc9bf520360d7af16ec26d0a9354bafcfc0" +checksum = "972d7902c8735f2695410b8aed7df6ed12a47394aa1c8d7af49f0497b731a94d" dependencies = [ - "base64 0.22.1", + "base64 0.23.1", "flate2", "log", "percent-encoding", "rustls", "rustls-pki-types", - "rustls-platform-verifier 0.6.2", + "rustls-platform-verifier", "ureq-proto", "utf8-zero", "webpki-roots", @@ -6434,11 +6267,11 @@ dependencies = [ [[package]] name = "ureq-proto" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e994ba84b0bd1b1b0cf92878b7ef898a5c1760108fe7b6010327e274917a808c" +checksum = "da5f78b09e6941e1a0f2e30e695e4b120377b54d5e0aec11b594bb57b3971613" dependencies = [ - "base64 0.22.1", + "base64 0.23.1", "http", "httparse", "log", @@ -6513,21 +6346,6 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" -[[package]] -name = "version_spec" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56820e158475da461ff8492898347d5a162906917658a530da0a6879c503392b" -dependencies = [ - "compact_str 0.9.1", - "human-sort", - "regex", - "schematic", - "semver", - "serde", - "thiserror 2.0.19", -] - [[package]] name = "version_spec" version = "0.11.2" @@ -6541,7 +6359,7 @@ dependencies = [ "regex", "schematic", "serde", - "thiserror 2.0.19", + "thiserror 2.0.20", ] [[package]] @@ -6574,48 +6392,9 @@ dependencies = [ [[package]] name = "warpgate" -version = "0.30.5" +version = "0.35.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47a82380b3b99182158aa0ca78247d73a075b136c541bd452f392e332ecbc84f" -dependencies = [ - "async-trait", - "base64 0.22.1", - "compact_str 0.9.1", - "docker_credential", - "extism", - "http-cache-reqwest", - "miette 7.6.0", - "oci-client 0.16.1", - "once_cell", - "regex", - "reqwest", - "reqwest-middleware", - "reqwest-retry", - "rust-netrc", - "rustc-hash", - "scc", - "schematic", - "serde", - "serde_json", - "sha2 0.10.9", - "starbase_archive 0.13.2", - "starbase_shell", - "starbase_styles", - "starbase_utils 0.13.8", - "system_env", - "thiserror 2.0.19", - "tokio", - "tracing", - "ureq", - "url", - "warpgate_api", -] - -[[package]] -name = "warpgate" -version = "0.34.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d48f4db46676d43f6f64e6256069fa8d2fd057cc24c0b6f36dace423676f8210" +checksum = "42e431d2cb9077b3db89462904dbe61cabd246770fd8dde5e7e2cdbbaacdfba0" dependencies = [ "async-trait", "compact_str 0.10.0", @@ -6623,7 +6402,7 @@ dependencies = [ "extism", "http-cache-reqwest", "miette 7.6.0", - "oci-client 0.17.0", + "oci-client", "once_cell", "regex", "reqwest", @@ -6635,12 +6414,12 @@ dependencies = [ "schematic", "serde", "serde_json", - "starbase_archive 0.14.4", + "starbase_archive", "starbase_shell", "starbase_styles", - "starbase_utils 0.14.1", + "starbase_utils 0.14.2", "system_env", - "thiserror 2.0.19", + "thiserror 2.0.20", "tokio", "tracing", "ureq", @@ -6650,9 +6429,9 @@ dependencies = [ [[package]] name = "warpgate_api" -version = "0.17.6" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c678ea51aa280e3b4688966daa09b95ebcc7a58d6f5ace3a00708cda7aea62cf" +checksum = "6e3d85bb888170166b1bf42011090e66b4c8e48d0563ba6279e4be2cedbf710e" dependencies = [ "anyhow", "derive_setters", @@ -6662,14 +6441,14 @@ dependencies = [ "serde_json", "starbase_id", "system_env", - "thiserror 2.0.19", + "thiserror 2.0.20", ] [[package]] name = "warpgate_pdk" -version = "0.16.5" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2897a95cca4ca14002c2407a365aa161a43fe4e4935d626c170e8c34552a22a" +checksum = "e59497845a80b314477c9803eb96eaf41574df879b8f269a8c1711da46ed9184" dependencies = [ "extism-pdk", "serde", @@ -6686,11 +6465,10 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasi-common" -version = "41.0.4" +version = "43.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f49ffbbd04665d04028f66aee8f24ae7a1f46063f59a28fddfa52ca3091754a2" +checksum = "46137f5bcc41a0f002ed14688e463665388a6f3a6662a12a8c315d4b8849791c" dependencies = [ - "anyhow", "async-trait", "bitflags", "cap-fs-ext", @@ -6703,18 +6481,19 @@ dependencies = [ "log", "rustix 1.1.4", "system-interface", - "thiserror 2.0.19", + "thiserror 2.0.20", "tracing", "wasmtime", + "wasmtime-environ", "wiggle", "windows-sys 0.61.2", ] [[package]] name = "wasm-bindgen" -version = "0.2.126" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" dependencies = [ "cfg-if", "once_cell", @@ -6725,9 +6504,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.76" +version = "0.4.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" +checksum = "6b7777d5cc23d0e91404e53ce2d5e8ec7acae3026b16233dba62cd3246457950" dependencies = [ "js-sys", "wasm-bindgen", @@ -6735,9 +6514,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.126" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -6745,9 +6524,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.126" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" dependencies = [ "bumpalo", "proc-macro2", @@ -6758,18 +6537,18 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.126" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" dependencies = [ "unicode-ident", ] [[package]] name = "wasm-compose" -version = "0.243.0" +version = "0.245.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af801b6f36459023eaec63fdbaedad2fd5a4ab7dc74ecc110a8b5d375c5775e4" +checksum = "5fd23d12cc95c451c1306db5bc63075fbebb612bb70c53b4237b1ce5bc178343" dependencies = [ "anyhow", "heck", @@ -6781,29 +6560,29 @@ dependencies = [ "serde_derive", "serde_yaml", "smallvec", - "wasm-encoder 0.243.0", - "wasmparser 0.243.0", + "wasm-encoder 0.245.1", + "wasmparser 0.245.1", "wat", ] [[package]] name = "wasm-encoder" -version = "0.243.0" +version = "0.245.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c55db9c896d70bd9fa535ce83cd4e1f2ec3726b0edd2142079f594fc3be1cb35" +checksum = "3f9dca005e69bf015e45577e415b9af8c67e8ee3c0e38b5b0add5aa92581ed5c" dependencies = [ "leb128fmt", - "wasmparser 0.243.0", + "wasmparser 0.245.1", ] [[package]] name = "wasm-encoder" -version = "0.254.0" +version = "0.255.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09480d646178e5fdd12bb06e812d0af9a3a191dbc9cd697fdc86687beade7393" +checksum = "9b524283fb5df62eec102ed0574838961bdd7ba5ac9c50d38e2756c51c971a42" dependencies = [ "leb128fmt", - "wasmparser 0.254.0", + "wasmparser 0.255.0", ] [[package]] @@ -6821,12 +6600,12 @@ dependencies = [ [[package]] name = "wasmparser" -version = "0.243.0" +version = "0.245.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6d8db401b0528ec316dfbe579e6ab4152d61739cfe076706d2009127970159d" +checksum = "4f08c9adee0428b7bddf3890fc27e015ac4b761cc608c822667102b8bfd6995e" dependencies = [ "bitflags", - "hashbrown 0.15.5", + "hashbrown 0.16.1", "indexmap", "semver", "serde", @@ -6834,9 +6613,9 @@ dependencies = [ [[package]] name = "wasmparser" -version = "0.254.0" +version = "0.255.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5769a29f799fbab136aaf65b4fe5384cd7d93fe6fc9ba0dcb6c8382a1f16e27" +checksum = "e8e329ef4b5d46e73b91d3ac6924417cad55a8cbbf869c199283383427c3320b" dependencies = [ "bitflags", "indexmap", @@ -6845,23 +6624,22 @@ dependencies = [ [[package]] name = "wasmprinter" -version = "0.243.0" +version = "0.245.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb2b6035559e146114c29a909a3232928ee488d6507a1504d8934e8607b36d7b" +checksum = "5f41517a3716fbb8ccf46daa9c1325f760fcbff5168e75c7392288e410b91ac8" dependencies = [ "anyhow", "termcolor", - "wasmparser 0.243.0", + "wasmparser 0.245.1", ] [[package]] name = "wasmtime" -version = "41.0.4" +version = "43.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2a83182bf04af87571b4c642300479501684f26bab5597f68f68cded5b098fd" +checksum = "efb1ed5899dde98357cfdcf647a4614498798719793898245b4b34e663addabf" dependencies = [ "addr2line", - "anyhow", "async-trait", "bitflags", "bumpalo", @@ -6871,8 +6649,6 @@ dependencies = [ "futures", "fxprof-processed-profile", "gimli", - "hashbrown 0.15.5", - "indexmap", "ittapi", "libc", "log", @@ -6892,18 +6668,17 @@ dependencies = [ "target-lexicon", "tempfile", "wasm-compose", - "wasm-encoder 0.243.0", - "wasmparser 0.243.0", + "wasm-encoder 0.245.1", + "wasmparser 0.245.1", "wasmtime-environ", "wasmtime-internal-cache", "wasmtime-internal-component-macro", "wasmtime-internal-component-util", + "wasmtime-internal-core", "wasmtime-internal-cranelift", "wasmtime-internal-fiber", "wasmtime-internal-jit-debug", "wasmtime-internal-jit-icache-coherence", - "wasmtime-internal-math", - "wasmtime-internal-slab", "wasmtime-internal-unwinder", "wasmtime-internal-versioned-export-macros", "wasmtime-internal-winch", @@ -6913,15 +6688,17 @@ dependencies = [ [[package]] name = "wasmtime-environ" -version = "41.0.4" +version = "43.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb201c41aa23a3642365cfb2e4a183573d85127a3c9d528f56b9997c984541ab" +checksum = "4172382dcc785c31d0e862c6780a18f5dd437914d22c4691351f965ef751c821" dependencies = [ "anyhow", "cpp_demangle", + "cranelift-bforest", "cranelift-bitset", "cranelift-entity", "gimli", + "hashbrown 0.16.1", "indexmap", "log", "object", @@ -6930,19 +6707,21 @@ dependencies = [ "semver", "serde", "serde_derive", + "sha2 0.10.9", "smallvec", "target-lexicon", - "wasm-encoder 0.243.0", - "wasmparser 0.243.0", + "wasm-encoder 0.245.1", + "wasmparser 0.245.1", "wasmprinter", "wasmtime-internal-component-util", + "wasmtime-internal-core", ] [[package]] name = "wasmtime-internal-cache" -version = "41.0.4" +version = "43.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5b3069d1a67ba5969d0eb1ccd7e141367d4e713f4649aa90356c98e8f19bea" +checksum = "4ed398988226d7aa0505ac6bb576e09532ad722d702ec4e66365d78ed695c95f" dependencies = [ "base64 0.22.1", "directories-next", @@ -6960,9 +6739,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-component-macro" -version = "41.0.4" +version = "43.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c924400db7b6ca996fef1b23beb0f41d5c809836b1ec60fc25b4057e2d25d9b" +checksum = "ae5ec9fff073ff13b81732d56a9515d761c245750bcda09093827f84130ebc25" dependencies = [ "anyhow", "proc-macro2", @@ -6975,15 +6754,27 @@ dependencies = [ [[package]] name = "wasmtime-internal-component-util" -version = "41.0.4" +version = "43.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d3f65daf4bf3d74ca2fbbe20af0589c42e2b398a073486451425d94fd4afef4" +checksum = "935d9ab293ba27d1ec9aa7bc1b3a43993dbe961af2a8f23f90a11e1331b4c13f" + +[[package]] +name = "wasmtime-internal-core" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3820b174f477d2a7083209d1ad5353fcdb11eaea434b2137b8681029460dd3" +dependencies = [ + "anyhow", + "hashbrown 0.16.1", + "libm", + "serde", +] [[package]] name = "wasmtime-internal-cranelift" -version = "41.0.4" +version = "43.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "633e889cdae76829738db0114ab3b02fce51ea4a1cd9675a67a65fce92e8b418" +checksum = "d1679d205caf9766c6aa309d45bb3e7c634d7725e3164404df33824b9f7c4fb7" dependencies = [ "cfg-if", "cranelift-codegen", @@ -6998,19 +6789,19 @@ dependencies = [ "pulley-interpreter", "smallvec", "target-lexicon", - "thiserror 2.0.19", - "wasmparser 0.243.0", + "thiserror 2.0.20", + "wasmparser 0.245.1", "wasmtime-environ", - "wasmtime-internal-math", + "wasmtime-internal-core", "wasmtime-internal-unwinder", "wasmtime-internal-versioned-export-macros", ] [[package]] name = "wasmtime-internal-fiber" -version = "41.0.4" +version = "43.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "deb126adc5d0c72695cfb77260b357f1b81705a0f8fa30b3944e7c2219c17341" +checksum = "f1e505254058be5b0df458d670ee42d9eafe2349d04c1296e9dc01071dc20a85" dependencies = [ "cc", "cfg-if", @@ -7023,9 +6814,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-jit-debug" -version = "41.0.4" +version = "43.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e66ff7f90a8002187691ff6237ffd09f954a0ebb9de8b2ff7f5c62632134120" +checksum = "1c2e05b345f1773e59c20e6ad7298fd6857cdea245023d88bb659c96d8f0ea72" dependencies = [ "cc", "object", @@ -7035,36 +6826,21 @@ dependencies = [ [[package]] name = "wasmtime-internal-jit-icache-coherence" -version = "41.0.4" +version = "43.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b96df23179ae16d54fb3a420f84ffe4383ec9dd06fad3e5bc782f85f66e8e08" +checksum = "b86701b234a4643e3f111869aa792b3a05a06e02d486ee9cb6c04dae16b52dab" dependencies = [ - "anyhow", "cfg-if", "libc", + "wasmtime-internal-core", "windows-sys 0.61.2", ] -[[package]] -name = "wasmtime-internal-math" -version = "41.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86d1380926682b44c383e9a67f47e7a95e60c6d3fa8c072294dab2c7de6168a0" -dependencies = [ - "libm", -] - -[[package]] -name = "wasmtime-internal-slab" -version = "41.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b63cbea1c0192c7feb7c0dfb35f47166988a3742f29f46b585ef57246c65764" - [[package]] name = "wasmtime-internal-unwinder" -version = "41.0.4" +version = "43.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f25c392c7e5fb891a7416e3c34cfbd148849271e8c58744fda875dde4bec4d6a" +checksum = "f63558d801beb83dde9b336eb4ae049019aee26627926edb32cd119d7e4c83cd" dependencies = [ "cfg-if", "cranelift-codegen", @@ -7075,9 +6851,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-versioned-export-macros" -version = "41.0.4" +version = "43.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70f8b9796a3f0451a7b702508b303d654de640271ac80287176de222f187a237" +checksum = "737c4d956fc3a848541a064afb683dd2771132a6b125be5baaf95c4379aa47df" dependencies = [ "proc-macro2", "quote", @@ -7086,16 +6862,16 @@ dependencies = [ [[package]] name = "wasmtime-internal-winch" -version = "41.0.4" +version = "43.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0063e61f1d0b2c20e9cfc58361a6513d074a23c80b417aac3033724f51648a0" +checksum = "f599b79545e3bba0b7913406055ebede5bb0dabee9ba2015ef25a9f4c9f47807" dependencies = [ "cranelift-codegen", "gimli", "log", "object", "target-lexicon", - "wasmparser 0.243.0", + "wasmparser 0.245.1", "wasmtime-environ", "wasmtime-internal-cranelift", "winch-codegen", @@ -7103,9 +6879,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-wit-bindgen" -version = "41.0.4" +version = "43.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "587699ca7cae16b4a234ffcc834f37e75675933d533809919b52975f5609e2ef" +checksum = "2192a77a00b9a67800c2b4e1c70fb6abca79d6b529e53a2ef9dcdcc36090330d" dependencies = [ "anyhow", "bitflags", @@ -7139,24 +6915,24 @@ dependencies = [ [[package]] name = "wast" -version = "254.0.0" +version = "255.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7ed4dfc8f6b9fc38b231065e2cdfbf7359af5ab945990abf09658dcc63c3e32" +checksum = "55ffec530f199bd3d553ac442c13dd108353cad533cad8514bc41e1f1f0fe686" dependencies = [ "bumpalo", "leb128fmt", "memchr", "unicode-width 0.2.2", - "wasm-encoder 0.254.0", + "wasm-encoder 0.255.0", ] [[package]] name = "wat" -version = "1.254.0" +version = "1.255.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7127f7f9b8f127c879991cecd35f494e4628bae1b0874c681414d8d8831e952c" +checksum = "dda82c82e1486c7eed42a0465e544d80fff37abc3b39482e0d34dbaabe2fe5b1" dependencies = [ - "wast 254.0.0", + "wast 255.0.0", ] [[package]] @@ -7170,15 +6946,15 @@ dependencies = [ "nom 7.1.3", "pori", "regex", - "thiserror 2.0.19", + "thiserror 2.0.20", "walkdir", ] [[package]] name = "web-sys" -version = "0.3.103" +version = "0.3.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" +checksum = "c435338968042f4f59a557f690a253676d47ce13ceb55d70100e7facf6620a30" dependencies = [ "js-sys", "wasm-bindgen", @@ -7223,38 +6999,38 @@ dependencies = [ [[package]] name = "wiggle" -version = "41.0.4" +version = "43.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69a60bcbe1475c5dc9ec89210ade54823d44f742e283cba64f98f89697c4cec" +checksum = "9c8cfd3db2f05619c6f36f257d84327c11546e28d61e3a1c1220aaad553bc4b0" dependencies = [ - "anyhow", "bitflags", - "thiserror 2.0.19", + "thiserror 2.0.20", "tracing", "wasmtime", + "wasmtime-environ", "wiggle-macro", "witx", ] [[package]] name = "wiggle-generate" -version = "41.0.4" +version = "43.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21f3dc0fd4dcfc7736434bb216179a2147835309abc09bf226736a40d484548f" +checksum = "4bd7a197903e5b4ff5e13aef9c891960d71e92073600ecf4c86c7e795ac1c803" dependencies = [ - "anyhow", "heck", "proc-macro2", "quote", "syn 2.0.119", + "wasmtime-environ", "witx", ] [[package]] name = "wiggle-macro" -version = "41.0.4" +version = "43.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fea2aea744eded58ae092bf57110c27517dab7d5a300513ff13897325c5c5021" +checksum = "6410b86fcec207070d9372b215d3470bad67215e6bbac46981a16999c4abbc28" dependencies = [ "proc-macro2", "quote", @@ -7295,22 +7071,21 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "winch-codegen" -version = "41.0.4" +version = "43.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c55de3ac5b8bd71e5f6c87a9e511dd3ceb194bdb58183c6a7bf21cd8c0e46fbc" +checksum = "52dbb0cf07b0dfe7b7a1ca8efb8f94ba98bd0fb144c411ea1665c78f0449e958" dependencies = [ - "anyhow", "cranelift-assembler-x64", "cranelift-codegen", "gimli", "regalloc2", "smallvec", "target-lexicon", - "thiserror 2.0.19", - "wasmparser 0.243.0", + "thiserror 2.0.20", + "wasmparser 0.245.1", "wasmtime-environ", + "wasmtime-internal-core", "wasmtime-internal-cranelift", - "wasmtime-internal-math", ] [[package]] @@ -7414,22 +7189,13 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", -] - [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.6", + "windows-targets", ] [[package]] @@ -7438,7 +7204,7 @@ version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets 0.52.6", + "windows-targets", ] [[package]] @@ -7450,35 +7216,20 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", "windows_i686_gnullvm", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] [[package]] @@ -7490,36 +7241,18 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -7532,48 +7265,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -7607,11 +7316,12 @@ dependencies = [ [[package]] name = "wit-parser" -version = "0.243.0" +version = "0.245.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df983a8608e513d8997f435bb74207bf0933d0e49ca97aa9d8a6157164b9b7fc" +checksum = "330698718e82983499419494dd1e3d7811a457a9bf9f69734e8c5f07a2547929" dependencies = [ "anyhow", + "hashbrown 0.16.1", "id-arena", "indexmap", "log", @@ -7620,7 +7330,7 @@ dependencies = [ "serde_derive", "serde_json", "unicode-xid", - "wasmparser 0.243.0", + "wasmparser 0.245.1", ] [[package]] @@ -7671,7 +7381,7 @@ checksum = "f903203119ded2bc5e8ba635fa8f3c54bddcc174f89d2b39637cf1be5c3f0944" dependencies = [ "nom 8.0.0", "nom-language", - "thiserror 2.0.19", + "thiserror 2.0.20", ] [[package]] @@ -7699,18 +7409,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.55" +version = "0.8.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb" +checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.55" +version = "0.8.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb" +checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" dependencies = [ "proc-macro2", "quote", @@ -7810,9 +7520,9 @@ dependencies = [ [[package]] name = "zlib-rs" -version = "0.6.6" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b142a20ec14a91d5bc708c1dc21b080c550113d8aa77afa29635673a65dd02c5" +checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12" [[package]] name = "zmij" diff --git a/Cargo.toml b/Cargo.toml index 85b115fd..7b415e82 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,33 +5,33 @@ members = ["backends/*", "crates/*", "extensions/*", "tools/*", "toolchains/*"] [workspace.dependencies] # Common extism-pdk = { version = "1.4.1" } -regex = { version = "1.11.3", default-features = false, features = ["std"] } +regex = { version = "1.13.1", default-features = false, features = ["std"] } rustc-hash = "2.1.2" schematic = { version = "0.19.7", default-features = false, features = [ "schema", ] } serde = { version = "1.0.228", features = ["derive"] } serde_json = { version = "1.0.150", features = ["preserve_order"] } -serial_test = "3.5.0" +serial_test = "4.0.1" shell-words = "1.1.1" -starbase_sandbox = "0.11.0" -starbase_utils = { version = "0.13.6", default-features = false, features = [ +starbase_sandbox = "0.12.0" +starbase_utils = { version = "0.14.2", default-features = false, features = [ "editor-config", ] } tokio = { version = "1.52.3", features = ["full"] } -toml = { version = "1.1.2", default-features = false, features = [ +toml = { version = "1.1.4", default-features = false, features = [ "parse", "serde", ] } # moon -moon_common = { version = "2.0.7" } -moon_config = { version = "2.1.0" } -moon_pdk = { version = "2.0.4" } -moon_pdk_api = { version = "2.0.4" } -moon_pdk_test_utils = { version = "2.0.4" } -moon_project = { version = "2.0.6" } -moon_target = { version = "3.0.0" } +moon_common = { version = "2.0.8" } +moon_config = { version = "2.1.1" } +moon_pdk = { version = "2.1.0" } +moon_pdk_api = { version = "2.1.2" } +moon_pdk_test_utils = { version = "2.1.0" } +moon_project = { version = "2.0.7" } +moon_target = { version = "3.0.1" } # moon_common = { path = "../../moon/crates/common" } # moon_config = { path = "../../moon/crates/config" } # moon_pdk = { path = "../../moon/crates/pdk" } @@ -41,9 +41,9 @@ moon_target = { version = "3.0.0" } # moon_target = { path = "../../moon/crates/target" } # proto -proto_pdk = { version = "0.33.0" } -proto_pdk_api = { version = "0.32.0" } -proto_pdk_test_utils = { version = "0.47.0" } +proto_pdk = { version = "0.34.0" } +proto_pdk_api = { version = "0.33.0" } +proto_pdk_test_utils = { version = "0.48.0" } # proto_pdk = { path = "../../proto/crates/pdk" } # proto_pdk_api = { path = "../../proto/crates/pdk-api" } # proto_pdk_test_utils = { path = "../../proto/crates/pdk-test-utils" } diff --git a/backends/asdf/CHANGELOG.md b/backends/asdf/CHANGELOG.md index 741bc923..0fb90150 100644 --- a/backends/asdf/CHANGELOG.md +++ b/backends/asdf/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 0.3.5 + +#### 🚀 Updates + +- Updated to support proto v0.60 release. + ## 0.3.4 #### 🚀 Updates diff --git a/backends/asdf/Cargo.toml b/backends/asdf/Cargo.toml index 6c7030f2..d260e1e4 100644 --- a/backends/asdf/Cargo.toml +++ b/backends/asdf/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "asdf_backend" -version = "0.3.4" +version = "0.3.5" edition = "2024" description = "asdf backend WASM plugin for proto, for installing tools via asdf plugins." authors = ["Miles Johnson"] diff --git a/backends/asdf/src/config.rs b/backends/asdf/src/config.rs index 44243bcd..fb7a45fd 100644 --- a/backends/asdf/src/config.rs +++ b/backends/asdf/src/config.rs @@ -4,7 +4,6 @@ use extism_pdk::*; use proto_pdk::*; use schematic::Schematic; use serde::{Deserialize, Serialize}; -use std::path::PathBuf; #[host_fn] extern "ExtismHost" { @@ -43,14 +42,11 @@ impl AsdfToolConfig { self.get_shortname() } - pub fn get_backend_path(&self) -> AnyResult { - Ok(PathBuf::from(format!( - "/proto/backends/asdf/{}", - self.get_backend_id()? - ))) + pub fn get_backend_path(&self) -> AnyResult { + VirtualPath::create(format!("/proto/backends/asdf/{}", self.get_backend_id()?)) } - pub fn get_script_path(&self, script: &str) -> AnyResult { + pub fn get_script_path(&self, script: &str) -> AnyResult { self.get_backend_path() .map(|path| path.join("bin").join(script)) } diff --git a/backends/asdf/src/proto.rs b/backends/asdf/src/proto.rs index 401a2edd..00649ea6 100644 --- a/backends/asdf/src/proto.rs +++ b/backends/asdf/src/proto.rs @@ -5,13 +5,10 @@ use proto_pdk::*; use rustc_hash::FxHashMap; use schematic::SchemaBuilder; use starbase_utils::fs; -use std::path::{Path, PathBuf}; #[host_fn] extern "ExtismHost" { fn exec_command(input: Json) -> Json; - fn from_virtual_path(path: String) -> String; - fn to_virtual_path(path: String) -> String; fn host_log(input: Json); } @@ -40,20 +37,8 @@ fn cpu_cores() -> AnyResult { Ok(value) } -fn backend_root() -> AnyResult { - if let Some(value) = var::get::("backend_root")? { - return Ok(value.into()); - } - - let root = into_real_path("/proto/backends")?; - - var::set("backend_root", root.to_str().unwrap())?; - - Ok(root) -} - fn create_script_from_context( - virtual_script_path: &Path, + virtual_script_path: &VirtualPath, context: &PluginContext, ) -> AnyResult { create_script( @@ -65,14 +50,14 @@ fn create_script_from_context( } fn create_script_from_unresolved_context( - virtual_script_path: &Path, + virtual_script_path: &VirtualPath, _context: &PluginUnresolvedContext, ) -> AnyResult { create_script(virtual_script_path, None, None, None) } fn create_script( - virtual_script_path: &Path, + virtual_script_path: &VirtualPath, version: Option<&VersionSpec>, tool_dir: Option<&VirtualPath>, temp_dir: Option<&VirtualPath>, @@ -107,12 +92,10 @@ fn create_script( // Resolve the real path since this is executed in the console input.args.push( - match virtual_script_path.strip_prefix("/proto/backends") { - Ok(suffix) => backend_root()?.join(suffix), - Err(_) => into_real_path(virtual_script_path)?, - } - .to_string_lossy() - .to_string(), + virtual_script_path + .to_real_path()? + .expect("Invalid script path!") + .to_string(), ); if let Some(version) = version { @@ -124,16 +107,20 @@ fn create_script( .insert("ASDF_INSTALL_VERSION".into(), version.to_string()); } - if let Some(dir) = tool_dir { + if let Some(dir) = tool_dir + && let Some(dir) = dir.to_real_path()? + { input .env - .insert("ASDF_INSTALL_PATH".into(), dir.real_path_string().unwrap()); + .insert("ASDF_INSTALL_PATH".into(), dir.to_string()); } - if let Some(dir) = temp_dir { + if let Some(dir) = temp_dir + && let Some(dir) = dir.to_real_path()? + { input .env - .insert("ASDF_DOWNLOAD_PATH".into(), dir.real_path_string().unwrap()); + .insert("ASDF_DOWNLOAD_PATH".into(), dir.to_string()); } Ok(input) @@ -177,10 +164,10 @@ pub fn register_tool(Json(input): Json) -> FnResult, + /// Install using locked versions. + pub locked: bool, + /// Custom Git URL to the package. // pub git_url: Option, diff --git a/backends/cargo/src/proto.rs b/backends/cargo/src/proto.rs index 939bef42..ee01dc62 100644 --- a/backends/cargo/src/proto.rs +++ b/backends/cargo/src/proto.rs @@ -1,6 +1,7 @@ use crate::config::{CargoBackendConfig, CargoToolConfig}; use backend_common::enable_tracing; use extism_pdk::*; +use lang_rust_common::get_cargo_home; use proto_pdk::*; use schematic::SchemaBuilder; use serde::Deserialize; @@ -27,10 +28,10 @@ pub fn register_tool(Json(input): Json) -> FnResult FnResult> { })) } -fn get_cargo_home(env: &HostEnvironment) -> Result { - let dir = env.home_dir.join(".cargo"); - - match get_host_env_var("CARGO_HOME")? { - Some(value) => Ok(if value.is_empty() { - dir - } else { - into_virtual_path(value)? - }), - None => Ok(dir), - } -} - #[plugin_fn] pub fn native_install( Json(input): Json, @@ -81,7 +69,7 @@ pub fn native_install( let tool_config = get_tool_config::()?; // Detect `cargo-binstall` - let binstall_path = get_cargo_home(&env)? + let binstall_path = get_cargo_home(env)? .join("bin") .join(env.os.get_exe_name("cargo-binstall")); @@ -105,6 +93,10 @@ pub fn native_install( // What to install command.args.push(format!("{id}@{}", input.context.version)); + if tool_config.locked || backend_config.locked { + command.args.push("--locked".into()); + } + // if let Some(git) = &tool_config.git_url { // command.args.push("--git".into()); // command.args.push(git.into()); @@ -121,9 +113,13 @@ pub fn native_install( // Where to install command.args.push("--root".into()); - command - .args - .push(input.install_dir.real_path_string().unwrap()); + command.args.push( + input + .install_dir + .to_real_path()? + .expect("Invalid install directory!") + .to_string(), + ); // Other options if input.force { diff --git a/backends/cargo/tests/download_test.rs b/backends/cargo/tests/download_test.rs index 68972ad7..8e0eb2fc 100644 --- a/backends/cargo/tests/download_test.rs +++ b/backends/cargo/tests/download_test.rs @@ -21,31 +21,54 @@ mod cargo_backend_download { }); } - // https://github.com/kbknapp/cargo-outdated/blob/master/Cargo.toml - - mod features { + mod locked { use super::*; - generate_native_install_tests!("cargo:cargo-outdated", "0.17.0", None, |cfg| { + generate_native_install_tests!("cargo:eza", "0.23.1", None, |cfg| { cfg.tool_config(CargoToolConfig { - features: vec!["debug".into()], - no_default_features: true, + locked: true, ..Default::default() }); }); } - mod bin { + mod locked_without_binstall { + use super::*; + + generate_native_install_tests!("cargo:eza", "0.23.1", None, |cfg| { + cfg.backend_config(CargoBackendConfig { + locked: true, + no_binstall: true, + ..Default::default() + }); + }); + } + + // https://github.com/kbknapp/cargo-outdated/blob/master/Cargo.toml + + mod features { use super::*; generate_native_install_tests!("cargo:cargo-outdated", "0.17.0", None, |cfg| { cfg.tool_config(CargoToolConfig { - bin: Some("cargo-outdated".into()), + features: vec!["debug".into()], + no_default_features: true, ..Default::default() }); }); } + // mod bin { + // use super::*; + + // generate_native_install_tests!("cargo:cargo-outdated", "0.17.0", None, |cfg| { + // cfg.tool_config(CargoToolConfig { + // bin: Some("cargo-outdated".into()), + // ..Default::default() + // }); + // }); + // } + // mod git { // use super::*; diff --git a/backends/cargo/tests/locate_test.rs b/backends/cargo/tests/locate_test.rs index 2d2269f2..4501638b 100644 --- a/backends/cargo/tests/locate_test.rs +++ b/backends/cargo/tests/locate_test.rs @@ -1,9 +1,9 @@ use proto_pdk_test_utils::*; use std::path::PathBuf; -fn locate_input(sandbox: &ProtoWasmSandbox) -> LocateExecutablesInput { +fn locate_input(sandbox: &ProtoWasmSandbox, plugin: &WasmTestWrapper) -> LocateExecutablesInput { LocateExecutablesInput { - install_dir: VirtualPath::Real(sandbox.path().into()), + install_dir: plugin.tool.to_virtual_path(sandbox.path()), ..Default::default() } } @@ -17,7 +17,9 @@ mod cargo_backend_locate { sandbox.create_file("bin/cargo-nextest", ""); let plugin = sandbox.create_plugin("cargo:cargo-nextest").await; - let output = plugin.locate_executables(locate_input(&sandbox)).await; + let output = plugin + .locate_executables(locate_input(&sandbox, &plugin)) + .await; let exe = output.exes.get("cargo-nextest").unwrap(); assert_eq!(exe.exe_path, Some(PathBuf::from("bin/cargo-nextest"))); @@ -33,7 +35,9 @@ mod cargo_backend_locate { sandbox.create_file("bin/subdir/inner", ""); let plugin = sandbox.create_plugin("cargo:cargo-nextest").await; - let output = plugin.locate_executables(locate_input(&sandbox)).await; + let output = plugin + .locate_executables(locate_input(&sandbox, &plugin)) + .await; assert!(output.exes.contains_key("cargo-nextest")); assert!(!output.exes.contains_key("subdir")); @@ -47,7 +51,9 @@ mod cargo_backend_locate { sandbox.create_file("bin/other", ""); let plugin = sandbox.create_plugin("cargo:eza").await; - let output = plugin.locate_executables(locate_input(&sandbox)).await; + let output = plugin + .locate_executables(locate_input(&sandbox, &plugin)) + .await; let eza = output.exes.get("eza").unwrap(); assert!(eza.primary); @@ -64,7 +70,9 @@ mod cargo_backend_locate { sandbox.create_file("bin/outdated", ""); let plugin = sandbox.create_plugin("cargo:cargo-outdated").await; - let output = plugin.locate_executables(locate_input(&sandbox)).await; + let output = plugin + .locate_executables(locate_input(&sandbox, &plugin)) + .await; let exe = output.exes.get("outdated").unwrap(); assert!(exe.primary); @@ -76,7 +84,9 @@ mod cargo_backend_locate { sandbox.create_file("bin/something-else", ""); let plugin = sandbox.create_plugin("cargo:cargo-nextest").await; - let output = plugin.locate_executables(locate_input(&sandbox)).await; + let output = plugin + .locate_executables(locate_input(&sandbox, &plugin)) + .await; assert_eq!(output.exes.values().filter(|cfg| cfg.primary).count(), 1); } @@ -87,7 +97,9 @@ mod cargo_backend_locate { sandbox.create_file("bin/cargo-nextest.exe", ""); let plugin = sandbox.create_plugin("cargo:cargo-nextest").await; - let output = plugin.locate_executables(locate_input(&sandbox)).await; + let output = plugin + .locate_executables(locate_input(&sandbox, &plugin)) + .await; // Map key has `.exe` stripped, but exe_path retains it. let exe = output.exes.get("cargo-nextest").unwrap(); diff --git a/backends/npm/CHANGELOG.md b/backends/npm/CHANGELOG.md index 0c6a9f29..b11f34d8 100644 --- a/backends/npm/CHANGELOG.md +++ b/backends/npm/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 0.1.3 + +#### 🚀 Updates + +- Updated to support proto v0.60 release. + ## 0.1.2 #### 🚀 Updates diff --git a/backends/npm/Cargo.toml b/backends/npm/Cargo.toml index 4b70279a..ec944026 100644 --- a/backends/npm/Cargo.toml +++ b/backends/npm/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "npm_backend" -version = "0.1.2" +version = "0.1.3" edition = "2024" description = "npm backend WASM plugin for proto, for installing CLIs from npmjs.com." authors = ["Miles Johnson"] diff --git a/backends/npm/src/proto.rs b/backends/npm/src/proto.rs index 9b00c153..7cb0bbe5 100644 --- a/backends/npm/src/proto.rs +++ b/backends/npm/src/proto.rs @@ -33,10 +33,10 @@ pub fn register_tool(Json(input): Json) -> FnResult LocateExecutablesInput { +fn locate_input(sandbox: &ProtoWasmSandbox, plugin: &WasmTestWrapper) -> LocateExecutablesInput { LocateExecutablesInput { - install_dir: VirtualPath::Real(sandbox.path().into()), + install_dir: plugin.tool.to_virtual_path(sandbox.path()), ..Default::default() } } @@ -24,7 +24,9 @@ mod npm_backend_locate { ); let plugin = sandbox.create_plugin("npm:typescript").await; - let output = plugin.locate_executables(locate_input(&sandbox)).await; + let output = plugin + .locate_executables(locate_input(&sandbox, &plugin)) + .await; let tsc = output.exes.get("tsc").unwrap(); assert_eq!( @@ -52,7 +54,9 @@ mod npm_backend_locate { ); let plugin = sandbox.create_plugin("npm:prettier").await; - let output = plugin.locate_executables(locate_input(&sandbox)).await; + let output = plugin + .locate_executables(locate_input(&sandbox, &plugin)) + .await; let prettier = output.exes.get("prettier").unwrap(); assert_eq!( @@ -73,7 +77,9 @@ mod npm_backend_locate { ); let plugin = sandbox.create_plugin("npm:@moonrepo/cli").await; - let output = plugin.locate_executables(locate_input(&sandbox)).await; + let output = plugin + .locate_executables(locate_input(&sandbox, &plugin)) + .await; assert!(output.exes.contains_key("cli")); assert!(!output.exes.contains_key("@moonrepo/cli")); @@ -95,7 +101,9 @@ mod npm_backend_locate { ); let plugin = sandbox.create_plugin("npm:multi").await; - let output = plugin.locate_executables(locate_input(&sandbox)).await; + let output = plugin + .locate_executables(locate_input(&sandbox, &plugin)) + .await; for name in ["a", "b", "c"] { let cfg = output.exes.get(name).unwrap(); @@ -113,7 +121,9 @@ mod npm_backend_locate { ); let plugin = sandbox.create_plugin("npm:multi").await; - let output = plugin.locate_executables(locate_input(&sandbox)).await; + let output = plugin + .locate_executables(locate_input(&sandbox, &plugin)) + .await; for name in ["a", "b", "c", "d"] { let cfg = output.exes.get(name).unwrap(); @@ -131,7 +141,9 @@ mod npm_backend_locate { ); let plugin = sandbox.create_plugin("npm:typescript").await; - let output = plugin.locate_executables(locate_input(&sandbox)).await; + let output = plugin + .locate_executables(locate_input(&sandbox, &plugin)) + .await; let tsc = output.exes.get("tsc").unwrap(); assert_eq!(tsc.parent_exe_name, Some("node".into())); @@ -145,7 +157,9 @@ mod npm_backend_locate { sandbox.create_file("bin/extra", ""); let plugin = sandbox.create_plugin("npm:typescript").await; - let output = plugin.locate_executables(locate_input(&sandbox)).await; + let output = plugin + .locate_executables(locate_input(&sandbox, &plugin)) + .await; let typescript = output.exes.get("typescript").unwrap(); assert_eq!(typescript.exe_path, Some(PathBuf::from("bin/typescript"))); @@ -162,7 +176,9 @@ mod npm_backend_locate { sandbox.create_file("typescript", ""); let plugin = sandbox.create_plugin("npm:typescript").await; - let output = plugin.locate_executables(locate_input(&sandbox)).await; + let output = plugin + .locate_executables(locate_input(&sandbox, &plugin)) + .await; let typescript = output.exes.get("typescript").unwrap(); assert_eq!(typescript.exe_path, Some(PathBuf::from("typescript"))); @@ -177,7 +193,9 @@ mod npm_backend_locate { sandbox.create_file("typescript.ps1", ""); let plugin = sandbox.create_plugin("npm:typescript").await; - let output = plugin.locate_executables(locate_input(&sandbox)).await; + let output = plugin + .locate_executables(locate_input(&sandbox, &plugin)) + .await; assert!(output.exes.contains_key("typescript")); assert!(!output.exes.contains_key("typescript.cmd")); @@ -192,7 +210,9 @@ mod npm_backend_locate { sandbox.create_file("bin/bar", ""); let plugin = sandbox.create_plugin("npm:typescript").await; - let output = plugin.locate_executables(locate_input(&sandbox)).await; + let output = plugin + .locate_executables(locate_input(&sandbox, &plugin)) + .await; assert_eq!(output.exes.values().filter(|cfg| cfg.primary).count(), 1); } @@ -207,7 +227,9 @@ mod npm_backend_locate { sandbox.create_file("bin/.keep", ""); let plugin = sandbox.create_plugin("npm:typescript").await; - let output = plugin.locate_executables(locate_input(&sandbox)).await; + let output = plugin + .locate_executables(locate_input(&sandbox, &plugin)) + .await; assert!(output.exes_dirs.contains(&PathBuf::from("bin"))); } @@ -221,7 +243,9 @@ mod npm_backend_locate { ); let plugin = sandbox.create_plugin("npm:typescript").await; - let output = plugin.locate_executables(locate_input(&sandbox)).await; + let output = plugin + .locate_executables(locate_input(&sandbox, &plugin)) + .await; assert!(output.exes_dirs.contains(&PathBuf::from("."))); } @@ -243,7 +267,9 @@ mod npm_backend_locate { cfg.backend_config(NpmBackendConfig { bun: true }); }) .await; - let output = plugin.locate_executables(locate_input(&sandbox)).await; + let output = plugin + .locate_executables(locate_input(&sandbox, &plugin)) + .await; let tsc = output.exes.get("tsc").unwrap(); assert_eq!( @@ -266,7 +292,9 @@ mod npm_backend_locate { cfg.backend_config(NpmBackendConfig { bun: true }); }) .await; - let output = plugin.locate_executables(locate_input(&sandbox)).await; + let output = plugin + .locate_executables(locate_input(&sandbox, &plugin)) + .await; let cli = output.exes.get("cli").unwrap(); assert_eq!( diff --git a/crates/extension-common/src/download.rs b/crates/extension-common/src/download.rs index 6e7ede65..4bd41926 100644 --- a/crates/extension-common/src/download.rs +++ b/crates/extension-common/src/download.rs @@ -1,4 +1,3 @@ -use crate::format_virtual_path; use extism_pdk::debug; use moon_pdk::{AnyResult, VirtualPath, fetch_bytes}; use starbase_utils::fs; @@ -25,7 +24,7 @@ pub fn download_from_url, P: AsRef>( fs::create_dir_all(dir)?; fs::write_file(&file, bytes)?; - debug!("Downloaded to {}", format_virtual_path(&file)); + debug!("Downloaded to {file}"); Ok(file) } diff --git a/crates/extension-common/src/lib.rs b/crates/extension-common/src/lib.rs index 7ab9af84..646ea766 100644 --- a/crates/extension-common/src/lib.rs +++ b/crates/extension-common/src/lib.rs @@ -3,17 +3,3 @@ pub mod migrator; pub mod project_graph; pub use common::*; -use moon_pdk::VirtualPath; -use std::borrow::Cow; - -pub fn format_virtual_path(path: &VirtualPath) -> Cow<'_, str> { - if let Some(real) = path.real_path_string() { - Cow::Owned(real) - } else if let Some(rel) = path.without_prefix() { - rel.to_string_lossy() - } else if let Some(virt) = path.virtual_path_string() { - Cow::Owned(virt) - } else { - Cow::Owned(path.to_string()) - } -} diff --git a/crates/lang-javascript-common/src/package_json.rs b/crates/lang-javascript-common/src/package_json.rs index a5795bdf..5508a994 100644 --- a/crates/lang-javascript-common/src/package_json.rs +++ b/crates/lang-javascript-common/src/package_json.rs @@ -75,10 +75,7 @@ pub fn extract_engine_version(package_json: &PackageJson, key: &str) -> Option( - package_json: &'a PackageJson, - key: &str, -) -> Option<&'a str> { +pub fn extract_package_manager_version(package_json: &PackageJson, key: &str) -> Option { if let Some(pm) = &package_json.package_manager { let mut parts = pm.split('@'); let name = parts.next().unwrap_or_default(); @@ -95,7 +92,7 @@ pub fn extract_package_manager_version<'a>( "latest" }; - return Some(value); + return Some(value.into()); } } diff --git a/crates/lang-rust-common/Cargo.toml b/crates/lang-rust-common/Cargo.toml new file mode 100644 index 00000000..2d1d16bf --- /dev/null +++ b/crates/lang-rust-common/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "lang_rust_common" +version = "0.1.0" +edition = "2024" +license = "MIT" +publish = false + +[dependencies] +extism-pdk = { workspace = true } +proto_pdk = { workspace = true } + +[features] +default = [] +wasm = [] diff --git a/tools/rust/src/helpers.rs b/crates/lang-rust-common/src/helpers.rs similarity index 80% rename from tools/rust/src/helpers.rs rename to crates/lang-rust-common/src/helpers.rs index 969e128e..a8bb4f55 100644 --- a/tools/rust/src/helpers.rs +++ b/crates/lang-rust-common/src/helpers.rs @@ -4,16 +4,19 @@ use proto_pdk::*; #[host_fn] extern "ExtismHost" { fn get_env_var(name: String) -> String; - fn to_virtual_path(input: String) -> String; } fn get_home_env(key: &str) -> Result, Error> { match get_host_env_var(key)? { - Some(value) => Ok(if value.is_empty() { - None - } else { - into_virtual_path(value).ok() - }), + Some(value) => { + if value.is_empty() { + Ok(None) + } else { + // This may point to a path outside of our virtual paths, + // which is okay, but we shouldn't fail the entire plugin + Ok(VirtualPath::create(value).ok()) + } + } None => Ok(None), } } diff --git a/crates/lang-rust-common/src/lib.rs b/crates/lang-rust-common/src/lib.rs new file mode 100644 index 00000000..43d6b391 --- /dev/null +++ b/crates/lang-rust-common/src/lib.rs @@ -0,0 +1,5 @@ +#[cfg(feature = "wasm")] +mod helpers; + +#[cfg(feature = "wasm")] +pub use helpers::*; diff --git a/extensions/download/CHANGELOG.md b/extensions/download/CHANGELOG.md index 1a5e7b9b..840329b3 100644 --- a/extensions/download/CHANGELOG.md +++ b/extensions/download/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 1.0.3 + +#### 🚀 Updates + +- Updated to support moon v2.5 release. + ## 1.0.2 #### 🚀 Updates diff --git a/extensions/download/Cargo.toml b/extensions/download/Cargo.toml index 8b0f9573..a5eaf223 100644 --- a/extensions/download/Cargo.toml +++ b/extensions/download/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "download_extension" -version = "1.0.2" +version = "1.0.3" edition = "2024" description = "moon extension WASM plugin for downloading a file from a URL." authors = ["Miles Johnson"] diff --git a/extensions/download/src/download_ext.rs b/extensions/download/src/download_ext.rs index 05b6c4ff..c21215bd 100644 --- a/extensions/download/src/download_ext.rs +++ b/extensions/download/src/download_ext.rs @@ -1,5 +1,5 @@ use extension_common::download::download_from_url; -use extension_common::{enable_tracing, format_virtual_path}; +use extension_common::enable_tracing; use extism_pdk::*; use moon_pdk::*; use moon_pdk_api::{ExecuteExtensionInput, RegisterExtensionInput, RegisterExtensionOutput}; @@ -7,7 +7,6 @@ use moon_pdk_api::{ExecuteExtensionInput, RegisterExtensionInput, RegisterExtens #[host_fn] extern "ExtismHost" { fn host_log(input: Json); - fn to_virtual_path(path: String) -> String; } #[plugin_fn] @@ -46,34 +45,24 @@ pub fn execute_extension(Json(input): Json) -> FnResult<( // Determine destination directory debug!("Determining destination directory"); - let dest_dir = into_virtual_path( - input - .context - .get_absolute_path(args.dest.as_deref().unwrap_or_default()), - )?; + let dest_dir = input + .context + .get_absolute_path(args.dest.as_deref().unwrap_or_default()); if dest_dir.exists() && dest_dir.is_file() { return Err(plugin_err!( - "Destination {} must be a directory, found a file.", - format_virtual_path(&dest_dir), + "Destination {dest_dir} must be a directory, found a file.", )); } - debug!( - "Destination {} will be used", - format_virtual_path(&dest_dir), - ); + debug!("Destination {dest_dir} will be used",); // Attempt to download the file host_log!(stdout, "Downloading {}", args.url); let dest_file = download_from_url(&args.url, &dest_dir, args.name.as_deref())?; - host_log!( - stdout, - "Downloaded to {}", - format_virtual_path(&dest_file), - ); + host_log!(stdout, "Downloaded to {dest_file}",); Ok(()) } diff --git a/extensions/migrate-nx/CHANGELOG.md b/extensions/migrate-nx/CHANGELOG.md index 5a1767d7..85573e24 100644 --- a/extensions/migrate-nx/CHANGELOG.md +++ b/extensions/migrate-nx/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 1.0.4 + +#### 🚀 Updates + +- Updated to support moon v2.5 release. + ## 1.0.3 #### 🚀 Updates diff --git a/extensions/migrate-nx/Cargo.toml b/extensions/migrate-nx/Cargo.toml index 5840a6c0..76fa1497 100644 --- a/extensions/migrate-nx/Cargo.toml +++ b/extensions/migrate-nx/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "migrate_nx_extension" -version = "1.0.3" +version = "1.0.4" edition = "2024" description = "moon extension WASM plugin for migrating an Nx repository to moon." authors = ["Miles Johnson"] diff --git a/extensions/migrate-nx/src/nx_migrator.rs b/extensions/migrate-nx/src/nx_migrator.rs index 071cf7ff..d8aa1ee4 100644 --- a/extensions/migrate-nx/src/nx_migrator.rs +++ b/extensions/migrate-nx/src/nx_migrator.rs @@ -213,7 +213,7 @@ impl NxMigrator { args: Some(PartialTaskArgs::List(migrate_options_to_args( &config_options, ))), - ..PartialTaskConfig::default() + ..Default::default() }, ); } @@ -400,7 +400,7 @@ fn inject_args_into_task(nx_target: &NxTargetOptions, config: &mut PartialTaskCo fn migrate_noop_task(nx_target: &NxTargetOptions) -> AnyResult { let mut config = PartialTaskConfig { command: Some(PartialTaskArgs::String("noop".into())), - ..PartialTaskConfig::default() + ..Default::default() }; inject_args_into_task(nx_target, &mut config); @@ -412,7 +412,7 @@ fn migrate_noop_task(nx_target: &NxTargetOptions) -> AnyResult AnyResult { let mut config = PartialTaskConfig { toolchains: Some(OneOrMany::One(Id::raw("system"))), - ..PartialTaskConfig::default() + ..Default::default() }; // https://nx.dev/nx-api/nx/executors/run-commands#options @@ -510,7 +510,7 @@ fn migrate_task( } else { format!("{package} {target}") })), - ..PartialTaskConfig::default() + ..Default::default() } } } else { diff --git a/extensions/migrate-turborepo/CHANGELOG.md b/extensions/migrate-turborepo/CHANGELOG.md index 244763f6..f4405345 100644 --- a/extensions/migrate-turborepo/CHANGELOG.md +++ b/extensions/migrate-turborepo/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 1.0.4 + +#### 🚀 Updates + +- Updated to support moon v2.5 release. + ## 1.0.3 #### 🚀 Updates diff --git a/extensions/migrate-turborepo/Cargo.toml b/extensions/migrate-turborepo/Cargo.toml index 57a78626..5582b55b 100644 --- a/extensions/migrate-turborepo/Cargo.toml +++ b/extensions/migrate-turborepo/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "migrate_turborepo_extension" -version = "1.0.3" +version = "1.0.4" edition = "2024" description = "moon extension WASM plugin for migrating a Turborepo repository to moon." authors = ["Miles Johnson"] diff --git a/extensions/unpack/CHANGELOG.md b/extensions/unpack/CHANGELOG.md index 9535ce18..9e783337 100644 --- a/extensions/unpack/CHANGELOG.md +++ b/extensions/unpack/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 1.0.3 + +#### 🚀 Updates + +- Updated to support moon v2.5 release. + ## 1.0.2 #### 🚀 Updates diff --git a/extensions/unpack/Cargo.toml b/extensions/unpack/Cargo.toml index 6e1621f7..1d8c6136 100644 --- a/extensions/unpack/Cargo.toml +++ b/extensions/unpack/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "unpack_extension" -version = "1.0.2" +version = "1.0.3" edition = "2024" description = "moon extension WASM plugin for unpacking archive files (tar, zip, etc.)." authors = ["Miles Johnson"] diff --git a/extensions/unpack/src/unpack_ext.rs b/extensions/unpack/src/unpack_ext.rs index 7a7f0535..92b9ed0e 100644 --- a/extensions/unpack/src/unpack_ext.rs +++ b/extensions/unpack/src/unpack_ext.rs @@ -1,5 +1,5 @@ use extension_common::download::download_from_url; -use extension_common::{enable_tracing, format_virtual_path}; +use extension_common::enable_tracing; use extism_pdk::*; use moon_pdk::*; use moon_pdk_api::{ExecuteExtensionInput, RegisterExtensionInput, RegisterExtensionOutput}; @@ -8,7 +8,6 @@ use starbase_utils::fs; #[host_fn] extern "ExtismHost" { fn host_log(input: Json); - fn to_virtual_path(path: String) -> String; } #[plugin_fn] @@ -45,7 +44,7 @@ pub fn execute_extension(Json(input): Json) -> FnResult<( let src_file = if args.src.starts_with("http") { debug!("Received a URL as the input source"); - download_from_url(&args.src, virtual_path!("/moon/temp"), None)? + download_from_url(&args.src, VirtualPath::new("/moon/temp"), None)? } else { debug!( "Converting source {} to an absolute virtual path", @@ -57,8 +56,7 @@ pub fn execute_extension(Json(input): Json) -> FnResult<( if !src_file.exists() || !src_file.is_file() { return Err(plugin_err!( - "Source {} must be a valid file.", - format_virtual_path(&src_file), + "Source {src_file} must be a valid file.", )); } @@ -69,16 +67,11 @@ pub fn execute_extension(Json(input): Json) -> FnResult<( if dest_dir.exists() && dest_dir.is_file() { return Err(plugin_err!( - "Destination {} must be a directory, found a file.", - format_virtual_path(&dest_dir), + "Destination {dest_dir} must be a directory, found a file.", )); } - host_log!( - stdout, - "Unpacking archive to {}", - format_virtual_path(&dest_dir), - ); + host_log!(stdout, "Unpacking archive to {dest_dir}",); fs::create_dir_all(&dest_dir)?; @@ -86,9 +79,15 @@ pub fn execute_extension(Json(input): Json) -> FnResult<( exec_streamed( "unzip", [ - src_file.real_path_string().expect("Invalid source"), + src_file + .to_real_path()? + .expect("Invalid source") + .to_string(), "-d".into(), - dest_dir.real_path_string().expect("Invalid destination"), + dest_dir + .to_real_path()? + .expect("Invalid destination") + .to_string(), ], )?; } else { @@ -96,9 +95,15 @@ pub fn execute_extension(Json(input): Json) -> FnResult<( "tar", [ "-xf".into(), - src_file.real_path_string().expect("Invalid source"), + src_file + .to_real_path()? + .expect("Invalid source") + .to_string(), "-C".into(), - dest_dir.real_path_string().expect("Invalid destination"), + dest_dir + .to_real_path()? + .expect("Invalid destination") + .to_string(), ], )?; } diff --git a/scripts/generateRubyReleases.mjs b/scripts/generateRubyReleases.mjs new file mode 100644 index 00000000..9048680d --- /dev/null +++ b/scripts/generateRubyReleases.mjs @@ -0,0 +1,70 @@ +// @ts-check +import fs from "node:fs"; + +const GH_TOKEN = process.env.GITHUB_TOKEN || process.env.GH_TOKEN; +const RELEASES_PATH = "tools/ruby/releases.json"; +const data = fs.existsSync(RELEASES_PATH) + ? JSON.parse(fs.readFileSync(RELEASES_PATH, "utf8")) + : {}; + +const headers = { + Accept: "application/vnd.github+json", + ...(GH_TOKEN ? { Authorization: `Bearer ${GH_TOKEN}` } : {}), +}; + +let page = 1; + +while (true) { + console.log(`Loading page ${page}`); + + const response = await fetch( + `https://api.github.com/repos/jdx/ruby/releases?per_page=100&page=${page}`, + { headers }, + ); + + if (!response.ok) { + throw new Error(`GitHub API returned HTTP ${response.status}`); + } + + const releases = await response.json(); + + for (const release of releases) { + const version = release.tag_name; + + // Ignore immutable build revisions, such as 3.4.9-1. + if (!/^\d+\.\d+\.\d+(?:-(?:preview|rc)\d+)?$/.test(version)) { + continue; + } + + for (const asset of release.assets) { + const match = asset.name.match( + /^ruby-(.+)\.(macos|x86_64_linux|arm64_linux)\.tar\.gz$/, + ); + + if (!match || match[1] !== version) { + continue; + } + + data[version] ||= {}; + data[version][match[2]] = asset.name; + } + } + + const link = response.headers.get("link"); + + if (!link?.includes('rel="next"')) { + break; + } + + page += 1; +} + +function sortObjectKeys(value) { + return Object.fromEntries( + Object.entries(value) + .sort(([a], [b]) => a.localeCompare(b, undefined, { numeric: true })) + .map(([key, item]) => [key, typeof item === "object" ? sortObjectKeys(item) : item]), + ); +} + +fs.writeFileSync(RELEASES_PATH, `${JSON.stringify(sortObjectKeys(data), null, 2)}\n`); diff --git a/toolchains/bun/CHANGELOG.md b/toolchains/bun/CHANGELOG.md index c7510123..631befe4 100644 --- a/toolchains/bun/CHANGELOG.md +++ b/toolchains/bun/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 1.0.3 + +#### 🚀 Updates + +- Updated to support moon v2.5 release. + ## 1.0.2 #### 🚀 Updates diff --git a/toolchains/bun/Cargo.toml b/toolchains/bun/Cargo.toml index 72d42e8a..4cc958c1 100644 --- a/toolchains/bun/Cargo.toml +++ b/toolchains/bun/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bun_toolchain" -version = "1.0.2" +version = "1.0.3" edition = "2024" description = "Bun toolchain WASM plugin for moon." authors = ["Miles Johnson"] diff --git a/toolchains/deno/CHANGELOG.md b/toolchains/deno/CHANGELOG.md index 66efc014..2417193b 100644 --- a/toolchains/deno/CHANGELOG.md +++ b/toolchains/deno/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 1.1.1 + +#### 🚀 Updates + +- Updated to support moon v2.5 release. + ## 1.1.0 #### 🚀 Updates diff --git a/toolchains/deno/Cargo.toml b/toolchains/deno/Cargo.toml index cabcef0d..b9348b1b 100644 --- a/toolchains/deno/Cargo.toml +++ b/toolchains/deno/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "deno_toolchain" -version = "1.1.0" +version = "1.1.1" edition = "2024" description = "Deno toolchain WASM plugin for moon." authors = ["Miles Johnson"] diff --git a/toolchains/deno/src/tier2.rs b/toolchains/deno/src/tier2.rs index 16668e4f..83470ab0 100644 --- a/toolchains/deno/src/tier2.rs +++ b/toolchains/deno/src/tier2.rs @@ -5,7 +5,8 @@ use crate::deno_json::DenoJson; use extism_pdk::*; use moon_config::BinEntry; use moon_pdk::{ - get_host_env_var, get_host_environment, parse_toolchain_config, parse_toolchain_config_schema, + VirtualPathExt, get_host_env_var, get_host_environment, parse_toolchain_config, + parse_toolchain_config_schema, }; use moon_pdk_api::*; use std::path::PathBuf; @@ -16,7 +17,7 @@ fn gather_shared_paths( paths: &mut Vec, ) -> AnyResult<()> { if let Some(globals_dir) = globals_dir - && globals_dir.real_path().is_some() + && globals_dir.to_real_path()?.is_some() { // Avoid the host env overhead if we already // have a valid globals directory! @@ -31,7 +32,11 @@ fn gather_shared_paths( } else if let Some(value) = get_host_env_var("DENO_HOME")? { Some(PathBuf::from(value).join("bin")) } else { - env.home_dir.join(".deno").join("bin").real_path() + env.home_dir + .join(".deno") + .join("bin") + .to_real_path()? + .map(|path| path.to_path_buf()) }; if let Some(dir) = maybe_dir { @@ -58,7 +63,7 @@ pub fn extend_task_command( output.args = Some(Extend::Prepend(config.execute_args)); } - gather_shared_paths(&env, input.globals_dir.as_ref(), &mut output.paths)?; + gather_shared_paths(env, input.globals_dir.as_ref(), &mut output.paths)?; Ok(Json(output)) } @@ -70,7 +75,7 @@ pub fn extend_task_script( let mut output = ExtendTaskScriptOutput::default(); let env = get_host_environment()?; - gather_shared_paths(&env, input.globals_dir.as_ref(), &mut output.paths)?; + gather_shared_paths(env, input.globals_dir.as_ref(), &mut output.paths)?; Ok(Json(output)) } @@ -165,7 +170,8 @@ pub fn setup_environment( output.commands.push( ExecCommand::new(ExecCommandInput::new("deno", args).cwd(input.root.to_owned())) - .cache(format!("deno-bin-{name}")), + .cache(CacheStrategy::Memory) + .label(format!("deno-bin-{name}")), ); } } diff --git a/toolchains/deno/tests/tier2_test.rs b/toolchains/deno/tests/tier2_test.rs index 0aef8863..e2774366 100644 --- a/toolchains/deno/tests/tier2_test.rs +++ b/toolchains/deno/tests/tier2_test.rs @@ -57,7 +57,7 @@ mod deno_toolchain_tier2 { let output = plugin .extend_task_command(ExtendTaskCommandInput { command: "unknown".into(), - globals_dir: Some(VirtualPath::Real(sandbox.path().into())), + globals_dir: Some(VirtualPath::new(sandbox.path())), ..Default::default() }) .await; @@ -126,7 +126,7 @@ mod deno_toolchain_tier2 { let output = plugin .parse_manifest(ParseManifestInput { - path: VirtualPath::Real(sandbox.path().join("deno.json")), + path: VirtualPath::new(sandbox.path().join("deno.json")), ..Default::default() }) .await; @@ -181,7 +181,7 @@ mod deno_toolchain_tier2 { let output = plugin .parse_manifest(ParseManifestInput { - path: VirtualPath::Real(sandbox.path().join("deno-publish.json")), + path: VirtualPath::new(sandbox.path().join("deno-publish.json")), ..Default::default() }) .await; @@ -200,7 +200,7 @@ mod deno_toolchain_tier2 { let output = plugin .setup_environment(SetupEnvironmentInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "bins": [] }), @@ -218,7 +218,7 @@ mod deno_toolchain_tier2 { let output = plugin .setup_environment(SetupEnvironmentInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "bins": [ "jsr:@std/http/file-server", @@ -247,7 +247,8 @@ mod deno_toolchain_tier2 { ) .cwd(plugin.plugin.to_virtual_path(sandbox.path())) ) - .cache("deno-bin-jsr:@std/http/file-server"), + .cache(CacheStrategy::Memory) + .label("deno-bin-jsr:@std/http/file-server"), ExecCommand::new( ExecCommandInput::new( "deno", @@ -261,7 +262,8 @@ mod deno_toolchain_tier2 { ) .cwd(plugin.plugin.to_virtual_path(sandbox.path())) ) - .cache("deno-bin-https://examples.deno.land/color-logging.ts"), + .cache(CacheStrategy::Memory) + .label("deno-bin-https://examples.deno.land/color-logging.ts"), ] ); } @@ -273,7 +275,7 @@ mod deno_toolchain_tier2 { let output = plugin .setup_environment(SetupEnvironmentInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "bins": [ { @@ -303,7 +305,8 @@ mod deno_toolchain_tier2 { ) .cwd(plugin.plugin.to_virtual_path(sandbox.path())) ) - .cache("deno-bin-jsr:@std/http/file-server")] + .cache(CacheStrategy::Memory) + .label("deno-bin-jsr:@std/http/file-server")] ); } @@ -314,7 +317,7 @@ mod deno_toolchain_tier2 { let output = plugin .setup_environment(SetupEnvironmentInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "bins": [ { @@ -343,7 +346,8 @@ mod deno_toolchain_tier2 { ) .cwd(plugin.plugin.to_virtual_path(sandbox.path())) ) - .cache("deno-bin-jsr:@std/http/file-server")] + .cache(CacheStrategy::Memory) + .label("deno-bin-jsr:@std/http/file-server")] ); } @@ -361,7 +365,7 @@ mod deno_toolchain_tier2 { let output = plugin .setup_environment(SetupEnvironmentInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "bins": [ { diff --git a/toolchains/dotnet/CHANGELOG.md b/toolchains/dotnet/CHANGELOG.md new file mode 100644 index 00000000..78bc553d --- /dev/null +++ b/toolchains/dotnet/CHANGELOG.md @@ -0,0 +1,27 @@ +# Changelog + +## 0.1.0 + +#### 🚀 Updates + +- Initial release of the .NET toolchain plugin. + - Tier 1: project and task detection (`*.csproj`/`*.fsproj`/`*.vbproj`, `*.sln`/`*.slnx`, + `global.json`, `Directory.Build.*`, `Directory.Packages.props`, `nuget.config`), + config schema, and Docker metadata with restore-layer scaffold globs. + - Tier 2: dependency root location, `dotnet restore` installs with automatic + `--locked-mode` when a lock file is present, local tool manifest restore + (`.config/dotnet-tools.json`), `packages.lock.json` and `Directory.Packages.props` + parsing, project-graph dependency inference from `ProjectReference`, task inference + (`build`/`test`/`run`/`publish`), `AssemblyName` project aliases, task-content + hashing, and `DOTNET_ROOT`/`PATH` injection into task environments. + - Tier 3: .NET SDK installation via the official dotnet-install scripts when + `version` is configured. +- Dependencies and tasks are inferred from a real MSBuild evaluation rather than by + parsing project XML, so `Directory.Build.targets` imports, MSBuild properties such as + `$(SolutionDir)`, conditional references and Central Package Management all resolve the + way the SDK resolves them. Every project in the workspace is evaluated in a single + batched invocation, and the results are cached on disk for task hashing to reuse. +- Evaluation accepts additional MSBuild properties through `msbuildProperties`, for + workspaces where a conditional reference or package should resolve the way the code is + actually built rather than under the SDK defaults. They apply to evaluation only, form + part of the evaluation cache key, and are never passed to inferred task commands. diff --git a/toolchains/dotnet/Cargo.toml b/toolchains/dotnet/Cargo.toml new file mode 100644 index 00000000..7004ae13 --- /dev/null +++ b/toolchains/dotnet/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "dotnet_toolchain" +version = "0.1.0" +edition = "2024" +description = ".NET toolchain WASM plugin for moon." +authors = ["Wouter Tiben"] +license = "MIT" +repository = "https://github.com/moonrepo/plugins" +documentation = "https://github.com/moonrepo/plugins/tree/master/toolchains/dotnet" +publish = false + +[package.metadata.release] +pre-release-replacements = [ + { file = "./CHANGELOG.md", search = "Unreleased", replace = "{{version}}" }, +] + +[lib] +crate-type = ["cdylib", "lib"] + +[dependencies] +toolchain_common = { path = "../../crates/toolchain-common" } +extism-pdk = { workspace = true } +moon_common = { workspace = true } +moon_config = { workspace = true } +moon_pdk = { workspace = true, features = ["schematic"] } +moon_pdk_api = { workspace = true } +moon_target = { workspace = true } +schematic = { workspace = true, features = ["config"] } +serde = { workspace = true } +serde_json = { workspace = true } +starbase_utils = { workspace = true, features = ["yaml"] } + +[dev-dependencies] +moon_pdk_test_utils = { workspace = true } +serde_json = { workspace = true } +starbase_sandbox = { workspace = true } +tokio = { workspace = true } + +[features] +default = ["wasm"] +wasm = [] diff --git a/toolchains/dotnet/src/config.rs b/toolchains/dotnet/src/config.rs new file mode 100644 index 00000000..c900188c --- /dev/null +++ b/toolchains/dotnet/src/config.rs @@ -0,0 +1,236 @@ +use moon_pdk_api::config_struct; +use schematic::{Config, Schematic}; +use std::collections::BTreeMap; + +/// The task names the plugin can infer. +pub const INFERABLE_TASKS: &[&str] = &["build", "test", "run", "publish"]; + +/// Which tasks to infer from evaluated MSBuild properties: a boolean to +/// enable/disable all of them, or an explicit list of task names +/// (`build`, `test`, `run`, `publish`) to infer only those. +#[derive(Clone, Debug, PartialEq, Schematic, serde::Deserialize, serde::Serialize)] +#[serde( + untagged, + expecting = "expected a boolean or a list of task names (build, test, run, publish)" +)] +pub enum InferTasksSetting { + Enabled(bool), + Only(Vec), +} + +impl Default for InferTasksSetting { + fn default() -> Self { + Self::Enabled(true) + } +} + +impl InferTasksSetting { + /// Is a specific task name selected for inference? + pub fn includes(&self, task: &str) -> bool { + match self { + Self::Enabled(enabled) => *enabled, + Self::Only(list) => list.iter().any(|name| name.eq_ignore_ascii_case(task)), + } + } + + /// Is any inference enabled at all? + pub fn any_enabled(&self) -> bool { + match self { + Self::Enabled(enabled) => *enabled, + Self::Only(list) => !list.is_empty(), + } + } +} + +config_struct!( + /// Configures and enables the .NET toolchain. + #[derive(Config)] + pub struct DotnetToolchainConfig { + /// Infer moon project dependencies from MSBuild `ProjectReference` + /// items, so `moon` knows the real build order without any `dependsOn` + /// declarations. + /// + /// This runs a real MSBuild evaluation, which is what makes it see + /// references added by `Directory.Build.targets` and conditional + /// `ProjectReference`s — not just what a project file lists literally. + /// Every project in the workspace is evaluated in a single batched + /// invocation. A reference outside the moon workspace is skipped. + /// + /// Defaults to `true`. + #[setting(default = true)] + pub infer_dependencies: bool, + + /// Infer `build`, `test`, `run` and `publish` tasks from each project's + /// evaluated MSBuild properties. + /// + /// `true` infers all four, `false` infers none, and a list infers only + /// the named ones — `['build', 'test']`. Unrecognised names in the list + /// are ignored rather than rejected. Being workspace-level, one line + /// here covers every project; turning inference off never requires + /// per-project overrides. + /// + /// What gets inferred: + /// + /// - `build` for every project, with `deps: ['^:build']` and + /// `--no-dependencies`, so moon orchestrates and caches the graph + /// per project rather than delegating that to MSBuild. + /// - `test` for a project with `IsTestProject=true` or a + /// `Microsoft.NET.Test.Sdk` reference. Both VSTest and + /// Microsoft.Testing.Platform are supported; the command shape follows + /// whichever the governing `global.json` selects. + /// - `run` for `Exe`/`WinExe`, never cached and excluded from CI. + /// - `publish` for single-target-framework `Exe`/`WinExe`. Multi-TFM + /// projects get none, since `dotnet publish` needs an explicit `-f`. + /// + /// Never inferred: `pack`, `watch`, `clean`, and `restore` — moon models + /// restore as the install-dependencies action instead, which is why the + /// inferred commands all pass `--no-restore`. + /// + /// Your own tasks always win. A task of the same id in a project's + /// `moon.yml` replaces the inferred one outright, and an id defined by an + /// inherited task file (`.moon/tasks.yml`, `.moon/tasks/**/*.yml`) that + /// can apply to dotnet projects is not inferred at all — moon would + /// otherwise merge the two into a broken command. Every such suppression + /// is logged with the id and the file that claimed it. + /// + /// Inferred commands pin the evaluated `Configuration` with `-c`, because + /// `dotnet publish` defaults to Release on .NET 8+ while `build` defaults + /// to Debug, and `--no-build` needs them to agree. Task outputs come from + /// the evaluated `BaseOutputPath`/`PublishDir`, so redirected output + /// locations cache correctly; a path resolving outside the workspace + /// makes the task run uncached rather than cache the wrong directory. + /// + /// Defaults to `true`. + pub infer_tasks: InferTasksSetting, + + /// Additional arguments appended to `dotnet restore`, which moon runs as + /// its install-dependencies action rather than as a task. + /// + /// `--locked-mode` is added automatically when a `packages.lock.json` (or + /// a `packages..lock.json`) is found, so it does not need to be + /// passed here. + pub restore_args: Vec, + + /// Explicit `DOTNET_ROOT`, used both for task environments and for the + /// MSBuild evaluation behind dependency and task inference — the two must + /// agree, or the graph gets evaluated by one SDK while tasks run under + /// another. + /// + /// When unset, resolution falls back to an existing `DOTNET_ROOT` + /// environment variable, then to `~/.dotnet` when it holds a `dotnet` + /// executable *and* an SDK satisfying the workspace's `global.json` pin + /// (a leftover install there is otherwise skipped in favour of the + /// `dotnet` on `PATH`). Set explicitly, it is never second-guessed. + pub dotnet_root: Option, + + /// Additional MSBuild properties applied to every evaluation behind + /// dependency and task inference, passed as `-p:NAME=VALUE`. + /// + /// Use this when the graph must be evaluated the way the code is + /// actually deployed. The motivating case: conditional, codegen-only + /// `ProjectReference`s (`ReferenceOutputAssembly=false`, gated on a + /// property like `Condition="'$(SkipApiClientGen)' != 'true'"`) that a + /// production/Docker build disables. Without the property, evaluation + /// includes build-ordering edges the deployed build never compiles — + /// over-attributing affected projects, and doing so + /// platform-dependently when the condition involves globs. + /// + /// ```yaml + /// dotnet: + /// msbuildProperties: + /// SkipApiClientGen: 'true' + /// ``` + /// + /// These are evaluation-time only, and the boundary is deliberate: + /// neither inferred task commands nor `dotnet restore` pass them, so + /// `moon run` builds stay exactly what the project defines. Two + /// consequences worth knowing: + /// + /// - Keep these consistent with how the code is actually built. Setting + /// a property here that your real build does not set makes the graph + /// describe a build nobody runs. Inferred `build` tasks pass + /// `--no-dependencies`, so moon is the only thing ordering + /// dependencies: dropping an edge here drops that ordering too. + /// - A `PackageReference` gated on one of these properties is resolved + /// for hashing but not for restore, so the recorded package set can + /// differ from what restore installs. Harmless when the properties + /// match the build; a reason not to gate packages on them otherwise. + pub msbuild_properties: BTreeMap, + } +); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn config_schema_builds() { + let schema = schematic::SchemaBuilder::build_root::(); + let json = serde_json::to_string(&schema).unwrap(); + + assert!(json.contains("inferDependencies")); + assert!(json.contains("inferTasks")); + assert!(json.contains("restoreArgs")); + assert!(json.contains("dotnetRoot")); + assert!(json.contains("msbuildProperties")); + + // `inferTasks` must stay a `bool | string[]` union. The derive produces + // this from the untagged enum; asserting the shape means a change to the + // enum cannot silently narrow what the setting accepts. + assert!( + json.contains( + r#""operator":"AnyOf","variants_types":[{"ty":{"type":"Boolean"}},{"ty":{"type":"Array","items_type":{"ty":{"type":"String"}}}}]"# + ), + "inferTasks lost its bool | string[] union: {json}" + ); + } + + #[test] + fn config_defaults_apply() { + let config: DotnetToolchainConfig = serde_json::from_value(serde_json::json!({})).unwrap(); + + assert!(config.infer_dependencies); + assert_eq!(config.infer_tasks, InferTasksSetting::Enabled(true)); + assert!(config.infer_tasks.any_enabled()); + assert!(config.restore_args.is_empty()); + assert!(config.dotnet_root.is_none()); + assert!(config.msbuild_properties.is_empty()); + } + + #[test] + fn msbuild_properties_deserialize_from_camel_case() { + let config: DotnetToolchainConfig = serde_json::from_value(serde_json::json!({ + "msbuildProperties": { "SkipApiClientGen": "true", "Answer": "42" } + })) + .unwrap(); + + assert_eq!( + config.msbuild_properties.get("SkipApiClientGen"), + Some(&"true".to_owned()) + ); + assert_eq!( + config.msbuild_properties.get("Answer"), + Some(&"42".to_owned()) + ); + } + + #[test] + fn infer_tasks_accepts_bool_and_list() { + let config: DotnetToolchainConfig = + serde_json::from_value(serde_json::json!({ "inferTasks": false })).unwrap(); + assert!(!config.infer_tasks.any_enabled()); + assert!(!config.infer_tasks.includes("build")); + + let config: DotnetToolchainConfig = + serde_json::from_value(serde_json::json!({ "inferTasks": ["build", "Test"] })).unwrap(); + assert!(config.infer_tasks.any_enabled()); + assert!(config.infer_tasks.includes("build")); + assert!(config.infer_tasks.includes("test")); + assert!(!config.infer_tasks.includes("run")); + assert!(!config.infer_tasks.includes("publish")); + + let config: DotnetToolchainConfig = + serde_json::from_value(serde_json::json!({ "inferTasks": [] })).unwrap(); + assert!(!config.infer_tasks.any_enabled()); + } +} diff --git a/toolchains/dotnet/src/discovery.rs b/toolchains/dotnet/src/discovery.rs new file mode 100644 index 00000000..1a605bed --- /dev/null +++ b/toolchains/dotnet/src/discovery.rs @@ -0,0 +1,175 @@ +//! Locating the files this toolchain cares about, relative to a directory. +//! +//! The `find_*` helpers enumerate one directory; `walk_up` supplies the +//! workspace-bounded upward traversal they are usually driven by. Neither has a +//! PDK equivalent: `warpgate_pdk` exposes no directory listing at all, and +//! `moon_pdk::locate_root*` walks up without a bound. + +use moon_pdk_api::VirtualPath; +use starbase_utils::fs; + +/// Project file extensions this toolchain understands. +pub const PROJECT_EXTENSIONS: &[&str] = &["csproj", "fsproj", "vbproj"]; + +/// Directories never worth descending into: build output, or owned by another +/// tool. Shared with tier 3's `global.json` scan. +pub const SKIP_DIRS: &[&str] = &["bin", "obj", "node_modules", ".git", ".moon"]; + +/// Workspace-level MSBuild/NuGet config files that can change evaluation, +/// restore, or build behavior from any level between a project dir and the +/// workspace root. Matched case-insensitively: NuGet itself accepts any +/// casing of `nuget.config`, and over-matching the others merely over-hashes +/// (a spurious cache invalidation, never a stale hit). +pub const CONFIG_FILE_NAMES: &[&str] = &[ + "directory.build.props", + "directory.build.rsp", + "directory.build.targets", + "directory.packages.props", + "global.json", + "nuget.config", +]; + +/// Does a file name carry one of the given extensions (case-insensitively)? +fn has_extension(name: &str, extensions: &[&str]) -> bool { + name.rsplit_once('.').is_some_and(|(_, ext)| { + extensions + .iter() + .any(|known| known.eq_ignore_ascii_case(ext)) + }) +} + +/// Files directly inside a directory whose name satisfies `keep`, sorted by +/// name and returned as paths under `dir`. +/// +/// An unreadable directory yields nothing rather than an error. Every caller +/// treats "nothing matched" and "could not look" identically, and propagating +/// would turn a single unreadable subdirectory into a failed +/// `install_dependencies` (via `contains_lockfile`'s recursion) or force the +/// infallible digest and boolean-probe callers to change shape. Note that +/// `fs::read_dir` already maps a missing directory to an empty list, so this +/// only absorbs real I/O failures — permissions, symlink loops, WASI +/// `NotCapable`. +fn list_files(dir: &VirtualPath, keep: impl Fn(&str) -> bool) -> Vec { + let Ok(entries) = fs::read_dir(dir.any_path()) else { + return vec![]; + }; + + let mut names = entries + .into_iter() + .filter(|entry| entry.file_type().is_ok_and(|kind| kind.is_file())) + .filter_map(|entry| entry.file_name().into_string().ok()) + .filter(|name| keep(name)) + .collect::>(); + + names.sort(); + + names.into_iter().map(|name| dir.join(name)).collect() +} + +/// Names of the subdirectories directly inside a directory. Same +/// unreadable-yields-nothing contract as [`list_files`]. +fn list_dirs(dir: &VirtualPath) -> Vec { + let Ok(entries) = fs::read_dir(dir.any_path()) else { + return vec![]; + }; + + entries + .into_iter() + .filter(|entry| entry.file_type().is_ok_and(|kind| kind.is_dir())) + .filter_map(|entry| entry.file_name().into_string().ok()) + .collect() +} + +/// List MSBuild project files (*.csproj etc.) directly inside a directory +/// (non-recursive). +pub fn find_project_files(dir: &VirtualPath) -> Vec { + list_files(dir, |name| has_extension(name, PROJECT_EXTENSIONS)) +} + +/// NuGet lock file names: the default `packages.lock.json`, plus the +/// `packages..lock.json` convention used when `NuGetLockFilePath` +/// renames it (case-insensitive, NuGet accepts any casing). +pub fn is_lock_file_name(name: &str) -> bool { + let lower = name.to_ascii_lowercase(); + + // The default name needs no special case: it starts with `packages.` and + // ends with `.lock.json` like the renamed variants. + lower.starts_with("packages.") && lower.ends_with(".lock.json") +} + +/// List NuGet lock files directly inside a directory (non-recursive), sorted. +pub fn find_lock_files(dir: &VirtualPath) -> Vec { + list_files(dir, is_lock_file_name) +} + +/// List hash-relevant config files directly inside a directory +/// (non-recursive), sorted by actual file name. +pub fn find_config_files(dir: &VirtualPath) -> Vec { + list_files(dir, |name| { + CONFIG_FILE_NAMES.contains(&name.to_ascii_lowercase().as_str()) + }) +} + +/// Does a directory directly contain a solution file (*.sln / *.slnx)? +pub fn has_solution_file(dir: &VirtualPath) -> bool { + !list_files(dir, |name| has_extension(name, &["sln", "slnx"])).is_empty() +} + +/// How far below a dependencies root to look for a lock file. Lock files sit +/// next to each project file rather than at the root, and .NET repositories +/// conventionally nest them a few levels down (`src///`). +pub const LOCKFILE_SEARCH_DEPTH: u8 = 5; + +/// Depth-limited search for any NuGet lock file under a directory. +/// Lock files live next to each project file, not at the dependencies root, +/// so a root-only check would miss them. +pub fn contains_lockfile(dir: &VirtualPath, depth: u8) -> bool { + if !find_lock_files(dir).is_empty() { + return true; + } + + if depth == 0 { + return false; + } + + list_dirs(dir) + .into_iter() + .filter(|name| !SKIP_DIRS.iter().any(|skip| skip.eq_ignore_ascii_case(name))) + .any(|name| contains_lockfile(&dir.join(name), depth - 1)) +} + +/// SDK versions laid out under a .NET root (`/sdk/`). +pub fn installed_sdk_versions(root: &VirtualPath) -> Vec { + list_dirs(&root.join("sdk")) +} + +/// Directories from `start` up to and including `workspace_root`. +/// +/// Every upward search in this plugin is bounded by the workspace root, which is +/// why moon's own `locate_root*` helpers are not used: they are unbounded, so +/// `global.json` or `dotnet-tools.json` discovery could escape into `$HOME` or a +/// parent repository and pick up a file that governs nothing here. The bound +/// matters most for `VirtualPath::Real`, whose `parent()` keeps yielding host +/// directories all the way to the filesystem root. +/// +/// Stops early if `start` is not under `workspace_root`, once `parent()` runs +/// out. +pub fn walk_up( + start: &VirtualPath, + workspace_root: &VirtualPath, +) -> impl Iterator { + let root = workspace_root.any_path().to_owned(); + let mut next = Some(start.to_owned()); + + std::iter::from_fn(move || { + let dir = next.take()?; + + next = if dir.any_path() == &root { + None + } else { + dir.parent() + }; + + Some(dir) + }) +} diff --git a/toolchains/dotnet/src/dotnet_install.rs b/toolchains/dotnet/src/dotnet_install.rs new file mode 100644 index 00000000..3802f9e5 --- /dev/null +++ b/toolchains/dotnet/src/dotnet_install.rs @@ -0,0 +1,137 @@ +use moon_config::UnresolvedVersionSpec; + +/// Official Microsoft install-script endpoints (stable redirect aliases). +pub const INSTALL_SCRIPT_URL_PS1: &str = "https://dot.net/v1/dotnet-install.ps1"; +pub const INSTALL_SCRIPT_URL_SH: &str = "https://dot.net/v1/dotnet-install.sh"; + +pub fn install_script_url(windows: bool) -> &'static str { + if windows { + INSTALL_SCRIPT_URL_PS1 + } else { + INSTALL_SCRIPT_URL_SH + } +} + +pub fn install_script_file_name(windows: bool) -> &'static str { + if windows { + "dotnet-install.ps1" + } else { + "dotnet-install.sh" + } +} + +/// The exact SDK version an exact spec would install (`8.0.404`), used to +/// short-circuit when `/sdk/` already exists. Channels and +/// aliases resolve server-side, so only fully-qualified versions qualify. +pub fn exact_version(spec: &UnresolvedVersionSpec) -> Option { + match spec { + UnresolvedVersionSpec::Semantic(version) => Some(version.to_string()), + _ => None, + } +} + +/// Map a configured version spec onto dotnet-install script arguments, +/// passing through the script's native semantics: `X.Y` requirements become +/// channels, `lts`/`sts`/`preview` aliases become named channels, and +/// fully-qualified versions install pinned. +pub fn install_version_args( + spec: &UnresolvedVersionSpec, + windows: bool, +) -> Result, String> { + let channel_flag = if windows { "-Channel" } else { "--channel" }; + let version_flag = if windows { "-Version" } else { "--version" }; + + let unsupported = |value: &dyn std::fmt::Display| { + format!( + "Unsupported .NET version specification `{value}` — use a channel like `8.0`, \ + an exact version like `8.0.404`, or one of `lts`, `sts`, `preview`." + ) + }; + + match spec { + UnresolvedVersionSpec::Semantic(version) => { + Ok(vec![version_flag.into(), version.to_string()]) + } + UnresolvedVersionSpec::Alias(alias) => { + let channel = match alias.to_ascii_lowercase().as_str() { + "lts" => "LTS", + // "Current" was renamed to STS; treat "latest" the same way. + "sts" | "current" | "latest" => "STS", + "preview" => "Preview", + _ => return Err(unsupported(alias)), + }; + + Ok(vec![channel_flag.into(), channel.into()]) + } + UnresolvedVersionSpec::Req(req) => { + let Some(comparator) = req.comparators.first() else { + return Err(unsupported(req)); + }; + + // dotnet-install channels are `major.minor` feature bands. + Ok(vec![ + channel_flag.into(), + format!("{}.{}", comparator.major, comparator.minor.unwrap_or(0)), + ]) + } + other => Err(unsupported(other)), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn parse(value: &str) -> UnresolvedVersionSpec { + UnresolvedVersionSpec::parse(value).unwrap() + } + + #[test] + fn exact_version_only_for_fully_qualified() { + assert_eq!(exact_version(&parse("8.0.404")).as_deref(), Some("8.0.404")); + assert_eq!(exact_version(&parse("8.0")), None); + assert_eq!(exact_version(&parse("lts")), None); + } + + #[test] + fn exact_versions_pass_through() { + assert_eq!( + install_version_args(&parse("8.0.404"), true).unwrap(), + vec!["-Version", "8.0.404"] + ); + assert_eq!( + install_version_args(&parse("8.0.404"), false).unwrap(), + vec!["--version", "8.0.404"] + ); + } + + #[test] + fn partial_versions_become_channels() { + assert_eq!( + install_version_args(&parse("8.0"), true).unwrap(), + vec!["-Channel", "8.0"] + ); + assert_eq!( + install_version_args(&parse("9"), false).unwrap(), + vec!["--channel", "9.0"] + ); + } + + #[test] + fn aliases_become_named_channels() { + assert_eq!( + install_version_args(&parse("lts"), false).unwrap(), + vec!["--channel", "LTS"] + ); + assert_eq!( + install_version_args(&parse("latest"), true).unwrap(), + vec!["-Channel", "STS"] + ); + } + + #[test] + fn unsupported_specs_error() { + assert!(install_version_args(&parse("canary"), true).is_err()); + assert!(install_version_args(&parse("banana"), false).is_err()); + } +} diff --git a/toolchains/dotnet/src/eval_cache.rs b/toolchains/dotnet/src/eval_cache.rs new file mode 100644 index 00000000..eb433da2 --- /dev/null +++ b/toolchains/dotnet/src/eval_cache.rs @@ -0,0 +1,248 @@ +//! On-disk cache of evaluated NuGet package sets, keyed per moon project. +//! +//! Task hashing needs the same data the project graph just evaluated, but runs +//! later — often in a separate process, against an already-cached project graph +//! — so it cannot rely on in-memory state. Without this, a workspace with no +//! lock files pays one MSBuild evaluation *per project* while hashing, which is +//! exactly what batching the graph evaluation exists to avoid. + +use crate::discovery::{find_config_files, find_project_files, walk_up}; +use moon_pdk_api::VirtualPath; +use serde::{Deserialize, Serialize}; +use starbase_utils::fs; +use std::collections::BTreeMap; + +/// FNV-1a digest, rendered hex. Used only to discriminate cache keys, never +/// for integrity — a plain content hash would mean pulling sha2 into the +/// wasm binary. Deterministic across Rust versions, unlike `DefaultHasher`. +pub fn content_digest(content: &str) -> String { + let mut hash: u64 = 0xcbf2_9ce4_8422_2325; + + for byte in content.as_bytes() { + hash ^= *byte as u64; + hash = hash.wrapping_mul(0x0000_0100_0000_01b3); + } + + format!("{hash:016x}") +} + +/// Cached evaluated package set for one moon project, written by the batched +/// graph evaluation and read back by task hashing. +#[derive(Debug, Deserialize, Serialize)] +struct EvalCacheEntry { + /// Digest of every file that can change the evaluated package set, so a + /// stale entry is never used. + digest: String, + packages: BTreeMap, +} + +/// Where cached package sets live. Under `.moon/cache`, which moon already +/// treats as disposable. +fn eval_cache_file(workspace_root: &VirtualPath, project_id: &str) -> VirtualPath { + let safe_id = project_id + .chars() + .map(|char| { + if char.is_ascii_alphanumeric() || matches!(char, '-' | '_' | '.') { + char + } else { + '_' + } + }) + .collect::(); + + workspace_root + .join(".moon") + .join("cache") + .join("dotnet-toolchain") + .join("eval") + .join(format!("{safe_id}.json")) +} + +/// Append one file to the digest buffer, framed by its name and byte length. +/// +/// Framing is load-bearing. Concatenating contents directly means the same bytes +/// distributed differently across two files produce the same buffer: moving a +/// `` block from the end of `Directory.Build.props` to the start +/// of `Directory.Packages.props` — adjacent in `find_config_files`' name sort — +/// is a routine Central Package Management migration, and it left the digest, and +/// therefore every task hash, unchanged. +/// +/// The file *name* is used rather than the full path, so the digest stays +/// independent of where the workspace lives on disk. +/// +/// An unreadable file contributes an empty body rather than propagating: that +/// yields a digest that cannot match the one recorded when the file was +/// readable, i.e. a cache miss, which is the safe direction. Returning an error +/// instead would break `write_eval_cache`'s best-effort contract. +fn push_framed(buffer: &mut String, file: &VirtualPath) { + let content = fs::read_file(file).unwrap_or_default(); + let name = file + .file_name() + .map(|name| name.to_string_lossy()) + .unwrap_or_default(); + + buffer.push_str(&name); + buffer.push(':'); + buffer.push_str(&content.len().to_string()); + buffer.push(':'); + buffer.push_str(&content); +} + +/// Digest of everything that can change a project's evaluated package set: +/// its project files, every config file from the project directory up to the +/// workspace root, plus the configured `msbuildProperties` — a conditional +/// `PackageReference` gated on such a property evaluates differently when the +/// setting changes, so the properties must invalidate the cache like any file +/// edit would. +/// +/// Two things are deliberately *not* captured. Custom ``s outside the +/// `Directory.Build.*` conventions — the same caveat that already applies to +/// task hashing itself. And the identity of the SDK that produced the set, so +/// switching `dotnetRoot` or upgrading the system SDK reuses the old answer +/// until some file changes. Including it would mean resolving the SDK root +/// before the cache *read* in `hash_task_contents`, and any asymmetry between +/// the read and write keys turns the cache into a permanent miss — which is the +/// per-project-evaluation cost this cache exists to avoid. +fn eval_cache_digest( + project_root: &VirtualPath, + workspace_root: &VirtualPath, + msbuild_properties: &BTreeMap, +) -> String { + let mut buffer = String::new(); + + for file in find_project_files(project_root) { + push_framed(&mut buffer, &file); + } + + for dir in walk_up(project_root, workspace_root) { + for file in find_config_files(&dir) { + push_framed(&mut buffer, &file); + } + } + + // Framed like a file, under a name no real file can have (path separator). + for (name, value) in msbuild_properties { + buffer.push_str("msbuild-property/"); + buffer.push_str(name); + buffer.push(':'); + buffer.push_str(&value.len().to_string()); + buffer.push(':'); + buffer.push_str(value); + } + + content_digest(&buffer) +} + +/// Persist a project's evaluated package set for task hashing to reuse. +/// +/// Callers must only pass a set they evaluated *completely*: a partial set is +/// indistinguishable from a complete one once written, and it would be served +/// under a digest that keeps validating. +pub fn write_eval_cache( + workspace_root: &VirtualPath, + project_id: &str, + project_root: &VirtualPath, + msbuild_properties: &BTreeMap, + packages: BTreeMap, +) { + let file = eval_cache_file(workspace_root, project_id); + + let entry = EvalCacheEntry { + digest: eval_cache_digest(project_root, workspace_root, msbuild_properties), + packages, + }; + + // Best-effort: a failed write only costs a re-evaluation later. Two tasks + // of the same project can race here, but they write identical content and + // a torn read simply fails to parse (also a re-evaluation). + if let Some(parent) = file.parent() { + let _ = fs::create_dir_all(parent); + } + + if let Ok(json) = serde_json::to_string(&entry) { + let _ = fs::write_file(&file, json); + } +} + +/// Read a project's cached package set, if it is still current. +pub fn read_eval_cache( + workspace_root: &VirtualPath, + project_id: &str, + project_root: &VirtualPath, + msbuild_properties: &BTreeMap, +) -> Option> { + let file = eval_cache_file(workspace_root, project_id); + + if !file.exists() { + return None; + } + + let entry: EvalCacheEntry = serde_json::from_str(&fs::read_file(&file).ok()?).ok()?; + + (entry.digest == eval_cache_digest(project_root, workspace_root, msbuild_properties)) + .then_some(entry.packages) +} + +#[cfg(test)] +mod tests { + use super::*; + use starbase_sandbox::create_empty_sandbox; + + #[test] + fn framing_distinguishes_content_moved_between_config_files() { + // The collision being guarded against: these two distributions of the + // same bytes are indistinguishable once concatenated, and + // `find_config_files` sorts these two file names adjacently. + assert_eq!( + content_digest(&format!("{}{}", "", "")), + content_digest(&format!("{}{}", "", "")), + ); + + let sandbox = create_empty_sandbox(); + let root = VirtualPath::Real(sandbox.path().into()); + let no_properties = BTreeMap::new(); + + sandbox.create_file("Directory.Build.props", ""); + sandbox.create_file("Directory.Packages.props", ""); + + let before = eval_cache_digest(&root, &root, &no_properties); + + // A routine CPM migration: move the declaration to the other file. + sandbox.create_file("Directory.Build.props", ""); + sandbox.create_file("Directory.Packages.props", ""); + + assert_ne!( + before, + eval_cache_digest(&root, &root, &no_properties), + "moving a declaration between config files must change the digest" + ); + } + + #[test] + fn msbuild_properties_invalidate_the_digest() { + let sandbox = create_empty_sandbox(); + let root = VirtualPath::Real(sandbox.path().into()); + + sandbox.create_file("Directory.Build.props", ""); + + let without = eval_cache_digest(&root, &root, &BTreeMap::new()); + let with = eval_cache_digest( + &root, + &root, + &BTreeMap::from([("SkipApiClientGen".to_owned(), "true".to_owned())]), + ); + + // A conditional PackageReference gated on the property would evaluate + // differently, so a cached set from one configuration must never be + // served under the other. + assert_ne!(without, with); + + let changed_value = eval_cache_digest( + &root, + &root, + &BTreeMap::from([("SkipApiClientGen".to_owned(), "false".to_owned())]), + ); + + assert_ne!(with, changed_value); + } +} diff --git a/toolchains/dotnet/src/global_json.rs b/toolchains/dotnet/src/global_json.rs new file mode 100644 index 00000000..c1649376 --- /dev/null +++ b/toolchains/dotnet/src/global_json.rs @@ -0,0 +1,345 @@ +//! `global.json` SDK pin parsing. +//! +//! Used to avoid injecting a `DOTNET_ROOT` that cannot satisfy the SDK a +//! workspace pins: the dotnet host resolves `global.json` from the current +//! directory, so a stale root (e.g. a leftover `~/.dotnet`) makes every task +//! fail with the host's own "compatible SDK was not found" error while +//! graph evaluation, running elsewhere, succeeds. + +use serde::Deserialize; + +#[derive(Debug, Default, Deserialize)] +#[serde(default, rename_all = "camelCase")] +struct GlobalJsonFile { + sdk: Option, + test: Option, +} + +#[derive(Debug, Default, Deserialize)] +#[serde(default, rename_all = "camelCase")] +struct GlobalJsonTest { + runner: Option, +} + +#[derive(Debug, Default, Deserialize)] +#[serde(default, rename_all = "camelCase")] +struct GlobalJsonSdk { + version: Option, + roll_forward: Option, + allow_prerelease: Option, +} + +/// How far the dotnet host may roll forward from the pinned SDK version. +/// +/// The `latest*` variants differ from their plain counterparts only in *which* +/// matching SDK gets chosen, not in whether one exists, so both map to the +/// same level here — this type only answers "could any installed SDK satisfy +/// this pin?". +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RollForward { + Disable, + Patch, + Feature, + Minor, + Major, +} + +impl RollForward { + fn parse(value: Option<&str>) -> Self { + match value.map(str::to_ascii_lowercase).as_deref() { + Some("disable") => Self::Disable, + // `patch` is the documented default when rollForward is unset. + None | Some("patch") | Some("latestpatch") => Self::Patch, + Some("feature") | Some("latestfeature") => Self::Feature, + Some("minor") | Some("latestminor") => Self::Minor, + Some("major") | Some("latestmajor") => Self::Major, + // Unknown values: assume the most permissive level rather than + // wrongly reporting a pin as unsatisfiable. + Some(_) => Self::Major, + } + } +} + +/// An SDK version pinned or installed, as `major.minor.patch` plus whether it +/// carries a prerelease suffix. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct SdkVersion { + pub major: u64, + pub minor: u64, + pub patch: u64, + pub prerelease: bool, +} + +impl SdkVersion { + /// SDK feature band — the hundreds component of the patch (e.g. `201` + /// is band 2). `patch`-level roll-forward stays within one band. + fn feature_band(&self) -> u64 { + self.patch / 100 + } + + fn triple(&self) -> (u64, u64, u64) { + (self.major, self.minor, self.patch) + } + + pub fn parse(value: &str) -> Option { + let value = value.trim(); + let (numeric, prerelease) = match value.split_once('-') { + Some((numeric, _)) => (numeric, true), + None => (value, false), + }; + + let mut parts = numeric.split('.'); + let major = parts.next()?.parse().ok()?; + + // Missing components default to 0, so a lenient "10" or "10.0" pin + // still compares sensibly even though global.json requires all three. + let component = |part: Option<&str>| match part { + Some(value) => value.parse().ok(), + None => Some(0), + }; + + Some(Self { + major, + minor: component(parts.next())?, + patch: component(parts.next())?, + prerelease, + }) + } +} + +/// The SDK pin declared by a `global.json`. +#[derive(Clone, Debug)] +pub struct SdkRequirement { + /// Version string exactly as written, for error messages and + /// `rollForward: disable` comparisons. + pub version: String, + pub parsed: SdkVersion, + pub roll_forward: RollForward, + pub allow_prerelease: bool, +} + +/// Parse the `sdk` block of a `global.json`. Returns `None` when the file +/// declares no SDK version — there is nothing to satisfy then. +pub fn parse_sdk_requirement(content: &str) -> Option { + let file: GlobalJsonFile = serde_json::from_str(content).ok()?; + let sdk = file.sdk?; + let version = sdk.version?; + let parsed = SdkVersion::parse(&version)?; + + Some(SdkRequirement { + version, + parsed, + roll_forward: RollForward::parse(sdk.roll_forward.as_deref()), + // Documented default is to allow prerelease SDKs. + allow_prerelease: sdk.allow_prerelease.unwrap_or(true), + }) +} + +/// Does this `global.json` select Microsoft.Testing.Platform as the runner +/// for `dotnet test` (`{"test": {"runner": "Microsoft.Testing.Platform"}}`)? +/// +/// It changes the `dotnet test` command line, not just the runner: MTP takes +/// the project through `--project` and rejects a positional path, while +/// classic VSTest mode is the exact opposite. Verified against SDK 10.0.201. +pub fn selects_test_platform(content: &str) -> bool { + serde_json::from_str::(content) + .ok() + .and_then(|file| file.test?.runner) + .is_some_and(|runner| runner.eq_ignore_ascii_case("Microsoft.Testing.Platform")) +} + +/// Could any of these installed SDK versions satisfy the pin? +/// +/// Unparseable installed version strings are ignored; an unparseable pin +/// never reaches here (`parse_sdk_requirement` returns `None`). +pub fn satisfies(installed: &[String], requirement: &SdkRequirement) -> bool { + installed.iter().any(|version| { + if requirement.roll_forward == RollForward::Disable { + return version.trim() == requirement.version.trim(); + } + + let Some(candidate) = SdkVersion::parse(version) else { + return false; + }; + + if candidate.prerelease && !requirement.allow_prerelease { + return false; + } + + let pinned = requirement.parsed; + + match requirement.roll_forward { + RollForward::Disable => unreachable!("handled above"), + RollForward::Patch => { + candidate.major == pinned.major + && candidate.minor == pinned.minor + && candidate.feature_band() == pinned.feature_band() + && candidate.patch >= pinned.patch + } + RollForward::Feature => { + candidate.major == pinned.major + && candidate.minor == pinned.minor + && candidate.patch >= pinned.patch + } + RollForward::Minor => { + candidate.major == pinned.major && candidate.triple() >= pinned.triple() + } + RollForward::Major => candidate.triple() >= pinned.triple(), + } + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn versions(list: &[&str]) -> Vec { + list.iter().map(|value| value.to_string()).collect() + } + + #[test] + fn parses_versions_with_and_without_prerelease() { + let version = SdkVersion::parse("10.0.201").unwrap(); + assert_eq!(version.triple(), (10, 0, 201)); + assert!(!version.prerelease); + assert_eq!(version.feature_band(), 2); + + let prerelease = SdkVersion::parse("10.0.100-rc.1.25451.107").unwrap(); + assert_eq!(prerelease.triple(), (10, 0, 100)); + assert!(prerelease.prerelease); + + // Lenient about missing components. + assert_eq!(SdkVersion::parse("8").unwrap().triple(), (8, 0, 0)); + assert_eq!(SdkVersion::parse("8.0").unwrap().triple(), (8, 0, 0)); + assert!(SdkVersion::parse("").is_none()); + assert!(SdkVersion::parse("latest").is_none()); + } + + #[test] + fn parses_a_global_json_carrying_unrelated_keys() { + // Shape taken from a production repository: `msbuild-sdks` and `test` + // sit alongside `sdk`, and neither must interfere with the pin. + let requirement = parse_sdk_requirement( + r#"{ + "sdk": { + "version": "10.0.301", + "rollForward": "latestMajor", + "allowPrerelease": true + }, + "msbuild-sdks": { "Aspire.AppHost.Sdk": "13.4.6" }, + "test": { "runner": "Microsoft.Testing.Platform" } + }"#, + ) + .unwrap(); + + assert_eq!(requirement.version, "10.0.301"); + assert_eq!(requirement.roll_forward, RollForward::Major); + assert!(requirement.allow_prerelease); + } + + #[test] + fn detects_the_microsoft_testing_platform_runner() { + // Real shape from a production repo. + assert!(selects_test_platform( + r#"{ + "sdk": { "version": "10.0.301", "rollForward": "latestMajor" }, + "test": { "runner": "Microsoft.Testing.Platform" } + }"# + )); + assert!(selects_test_platform( + r#"{"test":{"runner":"microsoft.testing.platform"}}"# + )); + + assert!(!selects_test_platform(r#"{"test":{"runner":"VSTest"}}"#)); + assert!(!selects_test_platform(r#"{"test":{}}"#)); + assert!(!selects_test_platform(r#"{"sdk":{"version":"10.0.301"}}"#)); + assert!(!selects_test_platform("{}")); + assert!(!selects_test_platform("not json")); + } + + #[test] + fn no_requirement_without_a_version() { + assert!(parse_sdk_requirement("{}").is_none()); + assert!(parse_sdk_requirement(r#"{"sdk":{}}"#).is_none()); + assert!(parse_sdk_requirement(r#"{"sdk":{"rollForward":"major"}}"#).is_none()); + assert!(parse_sdk_requirement("not json").is_none()); + } + + #[test] + fn latest_major_accepts_any_newer_sdk() { + let requirement = + parse_sdk_requirement(r#"{"sdk":{"version":"10.0.301","rollForward":"latestMajor"}}"#) + .unwrap(); + + // The case this guard exists for: a leftover `~/.dotnet` holding only + // SDK 8 cannot serve a 10.x pin, so it must not be preferred over the + // `dotnet` on PATH. + assert!(!satisfies(&versions(&["8.0.423"]), &requirement)); + assert!(satisfies(&versions(&["8.0.423", "10.0.301"]), &requirement)); + assert!(satisfies(&versions(&["11.0.100"]), &requirement)); + assert!(!satisfies(&versions(&["10.0.201"]), &requirement)); + } + + #[test] + fn default_roll_forward_stays_within_the_feature_band() { + let requirement = parse_sdk_requirement(r#"{"sdk":{"version":"10.0.201"}}"#).unwrap(); + + assert_eq!(requirement.roll_forward, RollForward::Patch); + assert!(satisfies(&versions(&["10.0.201"]), &requirement)); + assert!(satisfies(&versions(&["10.0.204"]), &requirement)); + // Higher feature band (3xx) and a different minor are out of reach. + assert!(!satisfies(&versions(&["10.0.301"]), &requirement)); + assert!(!satisfies(&versions(&["10.1.201"]), &requirement)); + assert!(!satisfies(&versions(&["10.0.104"]), &requirement)); + } + + #[test] + fn feature_and_minor_levels_widen_progressively() { + let feature = + parse_sdk_requirement(r#"{"sdk":{"version":"10.0.201","rollForward":"feature"}}"#) + .unwrap(); + assert!(satisfies(&versions(&["10.0.301"]), &feature)); + assert!(!satisfies(&versions(&["10.1.100"]), &feature)); + + let minor = + parse_sdk_requirement(r#"{"sdk":{"version":"10.0.201","rollForward":"latestMinor"}}"#) + .unwrap(); + assert!(satisfies(&versions(&["10.1.100"]), &minor)); + assert!(!satisfies(&versions(&["11.0.100"]), &minor)); + } + + #[test] + fn disable_requires_an_exact_match() { + let requirement = + parse_sdk_requirement(r#"{"sdk":{"version":"10.0.201","rollForward":"disable"}}"#) + .unwrap(); + + assert!(satisfies(&versions(&["10.0.201"]), &requirement)); + assert!(!satisfies(&versions(&["10.0.202"]), &requirement)); + } + + #[test] + fn prerelease_sdks_only_count_when_allowed() { + let allowed = parse_sdk_requirement( + r#"{"sdk":{"version":"10.0.100","rollForward":"latestMajor","allowPrerelease":true}}"#, + ) + .unwrap(); + assert!(satisfies(&versions(&["10.0.100-rc.1.25451.107"]), &allowed)); + + let denied = parse_sdk_requirement( + r#"{"sdk":{"version":"10.0.100","rollForward":"latestMajor","allowPrerelease":false}}"#, + ) + .unwrap(); + assert!(!satisfies(&versions(&["10.0.100-rc.1.25451.107"]), &denied)); + } + + #[test] + fn unknown_roll_forward_values_stay_permissive() { + let requirement = parse_sdk_requirement( + r#"{"sdk":{"version":"8.0.100","rollForward":"someFutureMode"}}"#, + ) + .unwrap(); + + assert!(satisfies(&versions(&["10.0.201"]), &requirement)); + } +} diff --git a/toolchains/dotnet/src/infer_tasks.rs b/toolchains/dotnet/src/infer_tasks.rs new file mode 100644 index 00000000..40dc6e02 --- /dev/null +++ b/toolchains/dotnet/src/infer_tasks.rs @@ -0,0 +1,405 @@ +use crate::config::{INFERABLE_TASKS, InferTasksSetting}; +use crate::msbuild::MsbuildEvaluation; +use moon_common::Id; +use moon_config::{ + Input, Output, PartialTaskArgs, PartialTaskConfig, PartialTaskDependency, + PartialTaskDependencyConfig, PartialTaskOptionsConfig, TaskOptionCache, TaskOptionRunInCI, +}; +use moon_pdk_api::{AnyResult, anyhow}; +use moon_target::Target; +use std::collections::{BTreeMap, BTreeSet}; + +/// Everything task inference needs to know about one MSBuild project. +pub struct InferInputs<'a> { + pub evaluation: &'a MsbuildEvaluation, + + /// Project file name to pass explicitly in commands when the project + /// directory holds more than one MSBuild project file (bare `dotnet + /// build` would otherwise error on ambiguity). + pub explicit_project_file: Option<&'a str>, + + /// Host-real absolute path of the project directory (for making + /// evaluated output paths project-relative). Forward or back slashes. + pub project_dir: &'a str, + + /// Host-real absolute path of the workspace root. + pub workspace_dir: &'a str, + + /// Whether the `global.json` governing this project selects + /// Microsoft.Testing.Platform for `dotnet test`. A project can also opt + /// in on its own via `TestingPlatformDotnetTestSupport`, which is read + /// from the evaluation. + pub test_platform_runner: bool, +} + +/// Strip `base` (plus one separator) from the start of `value`, +/// case-insensitively — Windows paths are case-insensitive and MSBuild +/// output casing is not guaranteed to match what moon reports. Both inputs +/// must already use forward slashes. +fn strip_prefix_ci<'a>(value: &'a str, base: &str) -> Option<&'a str> { + if base.is_empty() { + return None; + } + + let mut value_iter = value.char_indices(); + let mut base_iter = base.chars(); + + loop { + let Some(base_ch) = base_iter.next() else { + // Base fully consumed: the next value char must be the separator. + return match value_iter.next() { + Some((index, '/')) => Some(&value[index + 1..]), + _ => None, + }; + }; + + let (_, value_ch) = value_iter.next()?; + + if value_ch != base_ch && !value_ch.to_lowercase().eq(base_ch.to_lowercase()) { + return None; + } + } +} + +/// Does a package identity mark its project as a test project? +/// +/// Matching is exact or by prefix, never a substring search: real test projects +/// commonly reference `Microsoft.AspNetCore.Mvc.Testing` and +/// `Microsoft.AspNetCore.TestHost`, and plenty of non-test libraries reference +/// helpers with "test" in the name. Those must not qualify on their own. +/// +/// The prefixes cover the families that replaced `Microsoft.NET.Test.Sdk` under +/// Microsoft.Testing.Platform, where that package is absent entirely — `xunit.v3` +/// ships as `xunit.v3`, `xunit.v3.core`, `xunit.v3.mtp-v2` and more, so the whole +/// family is matched rather than enumerated. +fn is_test_package(name: &str) -> bool { + const EXACT: &[&str] = &[ + "microsoft.net.test.sdk", + "mstest", + "mstest.testframework", + "nunit3testadapter", + "tunit", + ]; + + const PREFIXES: &[&str] = &["xunit.v3", "microsoft.testing.platform", "tunit."]; + + let lower = name.to_ascii_lowercase(); + + EXACT.contains(&lower.as_str()) || PREFIXES.iter().any(|prefix| lower.starts_with(prefix)) +} + +/// Turn an evaluated MSBuild output path into a moon task output: relative +/// paths pass through, absolute paths under the project dir become +/// project-relative, absolute paths under the workspace root become +/// workspace-relative (leading `/`). Anything else (redirected outside the +/// workspace) is `None` — the task must then disable caching rather than +/// cache the wrong directory. +pub fn resolve_output_path(raw: &str, project_dir: &str, workspace_dir: &str) -> Option { + if raw.is_empty() { + return None; + } + + let value = raw.replace('\\', "/"); + let value = value.trim_end_matches('/'); + + if value.is_empty() { + return None; + } + + let is_absolute = value.starts_with('/') || value.as_bytes().get(1) == Some(&b':'); + + if !is_absolute { + return Some(value.to_string()); + } + + let project_dir = project_dir.replace('\\', "/"); + + if let Some(relative) = strip_prefix_ci(value, project_dir.trim_end_matches('/')) { + return Some(relative.to_string()); + } + + let workspace_dir = workspace_dir.replace('\\', "/"); + + if let Some(relative) = strip_prefix_ci(value, workspace_dir.trim_end_matches('/')) { + return Some(format!("/{relative}")); + } + + None +} + +fn command(verb: &str, project_file: Option<&str>, extra_args: &[&str]) -> PartialTaskArgs { + let mut list = vec!["dotnet".to_string(), verb.to_string()]; + + list.extend(project_file.map(str::to_string)); + list.extend(extra_args.iter().map(|arg| arg.to_string())); + + PartialTaskArgs::List(list) +} + +/// Pin the evaluated `Configuration` on cacheable commands. `dotnet build` +/// defaults to Debug but `dotnet publish` defaults to Release (.NET 8+), so +/// without an explicit `-c` a `publish --no-build` would look for outputs a +/// `build` never produced. Passing the configuration the evaluation itself +/// saw keeps every command consistent with the evaluated output paths — +/// including repos that set `Configuration` in `Directory.Build.props`. +fn pin_configuration(command: &mut PartialTaskArgs, configuration: &str) { + if configuration.is_empty() { + return; + } + + if let PartialTaskArgs::List(list) = command { + list.push("-c".into()); + list.push(configuration.into()); + } +} + +fn parse_target(target: &str) -> AnyResult { + Ok(PartialTaskDependency::Target( + Target::parse(target).map_err(|error| anyhow!("{error}"))?, + )) +} + +/// Same, but tolerated when the target does not exist. Required for `~:` deps: +/// moon defaults `optional` to `false` for the `OwnSelf` scope, so a project +/// that infers `test` or `publish` without `build` — `inferTasks: ['test']`, or +/// a `build` id claimed by an inherited task file — would fail project-graph +/// construction outright with `UnknownDepTarget` rather than simply losing the +/// ordering edge. +fn parse_optional_target(target: &str) -> AnyResult { + Ok(PartialTaskDependency::Object(PartialTaskDependencyConfig { + target: Some(Target::parse(target).map_err(|error| anyhow!("{error}"))?), + optional: Some(true), + ..Default::default() + })) +} + +/// Inputs for cacheable tasks: everything in the project EXCEPT the +/// evaluated output and intermediate directories. moon's default `**/*` +/// would otherwise hash `obj/` (which MSBuild mutates on every build), so +/// task hashes would never stabilize and nothing would ever be a cache hit. +fn stable_inputs(inputs: &InferInputs) -> AnyResult> { + let mut list = vec![Input::parse("**/*").map_err(|error| anyhow!("{error}"))?]; + + for property in ["BaseOutputPath", "BaseIntermediateOutputPath"] { + if let Some(dir) = resolve_output_path( + inputs.evaluation.property(property), + inputs.project_dir, + inputs.workspace_dir, + ) { + list.push(Input::parse(format!("!{dir}/**")).map_err(|error| anyhow!("{error}"))?); + } + } + + Ok(list) +} + +/// Give a task its evaluated outputs, or disable caching when they could +/// not be determined (never cache the wrong directory). +fn apply_outputs(task: &mut PartialTaskConfig, outputs: Option) -> AnyResult<()> { + match outputs { + Some(path) => { + task.outputs = Some(vec![ + Output::parse(&path).map_err(|error| anyhow!("{error}"))?, + ]); + } + None => { + task.options.get_or_insert_default().cache = Some(TaskOptionCache::Enabled(false)); + } + } + + Ok(()) +} + +/// Task ids that inference would have contributed but had to yield to an +/// inherited task file, paired with the file that claimed each one. +/// +/// Yielding is silent otherwise, which turns "why does no project have a +/// build task?" into a dead end — worth one report per workspace. +pub fn reportable_conflicts<'a>( + reserved: &'a BTreeMap, + setting: &InferTasksSetting, +) -> Vec<(&'a str, &'a str)> { + INFERABLE_TASKS + .iter() + .filter(|task| setting.includes(task)) + .filter_map(|task| { + reserved + .get_key_value(*task) + .map(|(id, file)| (id.as_str(), file.as_str())) + }) + .collect() +} + +/// Infer `build` / `test` / `run` / `publish` tasks from one project's +/// MSBuild evaluation. +/// +/// - `build` — every project; `--no-dependencies` so moon's task graph +/// (`deps: ^:build`) orchestrates upstream builds and caches each project +/// independently (verified: MSBuild resolves `ProjectReference`s from the +/// upstream `bin` output without rebuilding them). +/// - `test` — projects with `IsTestProject=true` or a `Microsoft.NET.Test.Sdk` +/// reference; `--no-build` on top of a `build` dep. +/// - `run` — `Exe`/`WinExe` non-test projects; never cached, never in CI. +/// - `publish` — `Exe`/`WinExe` non-test single-TFM projects (multi-TFM +/// `dotnet publish` requires an explicit `-f`); `--no-build` on top of a +/// `build` dep. +/// +/// `restore` is deliberately NOT a task: moon models it as the +/// install-dependencies action (with `--locked-mode`), which runs before +/// tasks — hence `--no-restore` everywhere. +/// +/// `reserved_ids` (task ids from applicable inherited task files) are +/// skipped entirely: moon merges plugin tasks over inherited tasks with +/// args-append semantics, which produces garbage commands — yielding is the +/// only safe move. Project-level `moon.yml` tasks need no such handling; +/// moon itself guarantees they win over plugin tasks. +pub fn infer_tasks( + setting: &InferTasksSetting, + reserved_ids: &BTreeSet, + inputs: &InferInputs, +) -> AnyResult> { + let mut tasks = BTreeMap::new(); + let evaluation = inputs.evaluation; + + // Three independent signals, because no single one covers the ecosystem: + // + // - `IsTestProject` comes from Microsoft.NET.Test.Sdk's build props, so it + // is only set once that package is restored. + // - `IsTestingPlatformApplication` is set by test-oriented project SDKs + // (``) without needing a restore, and by + // Microsoft.Testing.Platform packages once restored. + // - The package references themselves are visible without any restore, + // which is the situation during a cold project-graph build. + // + // Verified against real repositories: an MSTest.Sdk project reports only + // `IsTestingPlatformApplication`, an xunit.v3 project on an unrestored tree + // reports only its package, and a BenchmarkDotNet project sets both + // properties to `false` and must stay excluded. + let is_test = evaluation + .property("IsTestProject") + .eq_ignore_ascii_case("true") + || evaluation + .property("IsTestingPlatformApplication") + .eq_ignore_ascii_case("true") + || evaluation + .package_references() + .keys() + .any(|name| is_test_package(name)); + + let output_type = evaluation.property("OutputType"); + let is_exe = !is_test + && (output_type.eq_ignore_ascii_case("Exe") || output_type.eq_ignore_ascii_case("WinExe")); + let is_single_tfm = evaluation.property("TargetFrameworks").is_empty(); + + let wants = |task: &str| setting.includes(task) && !reserved_ids.contains(task); + let file = inputs.explicit_project_file; + + let hash_inputs = stable_inputs(inputs)?; + let configuration = evaluation.property("Configuration"); + + if wants("build") { + let mut build_command = command("build", file, &["--no-restore", "--no-dependencies"]); + pin_configuration(&mut build_command, configuration); + + let mut task = PartialTaskConfig { + command: Some(build_command), + deps: Some(vec![parse_target("^:build")?]), + description: Some( + "Builds the project. Upstream projects build through moon task deps. (inferred)" + .into(), + ), + inputs: Some(hash_inputs.clone()), + ..Default::default() + }; + + apply_outputs( + &mut task, + resolve_output_path( + evaluation.property("BaseOutputPath"), + inputs.project_dir, + inputs.workspace_dir, + ), + )?; + + tasks.insert(Id::raw("build"), task); + } + + if is_test && wants("test") { + // Microsoft.Testing.Platform's `dotnet test` takes the project + // through `--project` and rejects a positional path; classic VSTest + // mode is the exact opposite and rejects `--project`. Both verified + // against SDK 10.0.201, so the flavour has to match the runner. + let uses_test_platform = inputs.test_platform_runner + || evaluation + .property("TestingPlatformDotnetTestSupport") + .eq_ignore_ascii_case("true"); + + let mut test_command = match file { + Some(file) if uses_test_platform => command( + "test", + None, + &["--project", file, "--no-build", "--no-restore"], + ), + _ => command("test", file, &["--no-build", "--no-restore"]), + }; + + pin_configuration(&mut test_command, configuration); + + tasks.insert( + Id::raw("test"), + PartialTaskConfig { + command: Some(test_command), + deps: Some(vec![parse_optional_target("~:build")?]), + description: Some("Runs tests against the built assemblies. (inferred)".into()), + inputs: Some(hash_inputs.clone()), + ..Default::default() + }, + ); + } + + if is_exe && wants("run") { + tasks.insert( + Id::raw("run"), + PartialTaskConfig { + command: Some(if let Some(file) = file { + command("run", None, &["--project", file]) + } else { + command("run", None, &[]) + }), + description: Some("Runs the application locally. (inferred)".into()), + options: Some(PartialTaskOptionsConfig { + cache: Some(TaskOptionCache::Enabled(false)), + run_in_ci: Some(TaskOptionRunInCI::Enabled(false)), + ..Default::default() + }), + ..Default::default() + }, + ); + } + + if is_exe && is_single_tfm && wants("publish") { + let mut publish_command = command("publish", file, &["--no-build", "--no-restore"]); + pin_configuration(&mut publish_command, configuration); + + let mut task = PartialTaskConfig { + command: Some(publish_command), + deps: Some(vec![parse_optional_target("~:build")?]), + description: Some("Publishes the built application. (inferred)".into()), + inputs: Some(hash_inputs), + ..Default::default() + }; + + apply_outputs( + &mut task, + resolve_output_path( + evaluation.property("PublishDir"), + inputs.project_dir, + inputs.workspace_dir, + ), + )?; + + tasks.insert(Id::raw("publish"), task); + } + + Ok(tasks) +} diff --git a/toolchains/dotnet/src/inherited_tasks.rs b/toolchains/dotnet/src/inherited_tasks.rs new file mode 100644 index 00000000..a7d2ba40 --- /dev/null +++ b/toolchains/dotnet/src/inherited_tasks.rs @@ -0,0 +1,173 @@ +//! Which task ids are already claimed by moon's inherited task files. +//! +//! Task inference must never contribute an id an inherited task file defines. +//! moon replaces a project-level task wholesale, but *merges* over an inherited +//! one with args appended — so an inferred `dotnet run` landing on an inherited +//! `echo inherited-run` produces `dotnet inherited-run run`. Yielding the id +//! entirely is the only safe option. + +use moon_pdk_api::VirtualPath; +use starbase_utils::{fs, yaml}; +use std::collections::BTreeMap; + +/// Partial shape of an inherited tasks file (`.moon/tasks.yml` or +/// `.moon/tasks/**/*.yml`) — just enough to know which task ids it defines +/// and whether it can apply to dotnet projects. +#[derive(Debug, Default, serde::Deserialize)] +#[serde(default, rename_all = "camelCase")] +struct InheritedTasksFile { + inherited_by: Option, + tasks: BTreeMap, +} + +#[derive(Debug, Default, serde::Deserialize)] +#[serde(default, rename_all = "camelCase")] +struct InheritedByScope { + toolchains: Option>, + languages: Option>, +} + +/// Can an inherited tasks file apply to dotnet projects? Only an explicit +/// `inheritedBy` scope naming other toolchains/languages rules it out; +/// everything else (unscoped, tag/stack/layer-scoped) is conservatively +/// assumed to apply — suppressing an inferred task is recoverable, while +/// moon's args-append merge of an inferred task over an inherited one +/// produces garbage commands. +fn applies_to_dotnet(scope: Option<&InheritedByScope>) -> bool { + let Some(scope) = scope else { + return true; + }; + + let mut scoped = false; + + if let Some(toolchains) = &scope.toolchains { + scoped = true; + + if toolchains + .iter() + .any(|id| id.eq_ignore_ascii_case("dotnet")) + { + return true; + } + } + + if let Some(languages) = &scope.languages { + scoped = true; + + if languages.iter().any(|lang| { + matches!( + lang.to_lowercase().as_str(), + "csharp" | "c#" | "fsharp" | "f#" | "vb" | "visualbasic" | "dotnet" + ) + }) { + return true; + } + } + + !scoped +} + +fn collect_yaml_files(dir: &VirtualPath, out: &mut Vec) { + if let Ok(entries) = fs::read_dir(dir.any_path()) { + for entry in entries { + let Ok(name) = entry.file_name().into_string() else { + continue; + }; + + let path = dir.join(&name); + + if entry.file_type().is_ok_and(|kind| kind.is_dir()) { + collect_yaml_files(&path, out); + } else if name.ends_with(".yml") || name.ends_with(".yaml") { + out.push(path); + } + } + } +} + +/// Task ids defined in inherited task files that can apply to dotnet +/// projects, mapped to the file that defines each one (for reporting). +/// Inference must never contribute one of these ids — see +/// `applies_to_dotnet` for why. +pub fn load_inherited_task_ids(workspace_root: &VirtualPath) -> BTreeMap { + let mut ids = BTreeMap::new(); + let mut files = vec![workspace_root.join(".moon").join("tasks.yml")]; + + collect_yaml_files(&workspace_root.join(".moon").join("tasks"), &mut files); + + for file in files { + if !file.exists() { + continue; + } + + // An unparseable file is moon's problem to report; there is nothing + // for inference to yield to. + if let Ok(parsed) = yaml::read_file::(file.any_path()) + && applies_to_dotnet(parsed.inherited_by.as_ref()) + { + let label = file.to_string(); + + for id in parsed.tasks.into_keys() { + ids.entry(id).or_insert_with(|| label.clone()); + } + } + } + + ids +} + +#[cfg(test)] +mod tests { + use super::*; + + fn scope(toolchains: Option<&[&str]>, languages: Option<&[&str]>) -> InheritedByScope { + InheritedByScope { + toolchains: toolchains.map(|list| list.iter().map(|id| id.to_string()).collect()), + languages: languages.map(|list| list.iter().map(|id| id.to_string()).collect()), + } + } + + #[test] + fn unscoped_files_always_apply() { + assert!(applies_to_dotnet(None)); + assert!(applies_to_dotnet(Some(&scope(None, None)))); + } + + #[test] + fn matching_toolchain_or_language_applies() { + assert!(applies_to_dotnet(Some(&scope(Some(&["dotnet"]), None)))); + assert!(applies_to_dotnet(Some(&scope(Some(&["DotNet"]), None)))); + assert!(applies_to_dotnet(Some(&scope(None, Some(&["csharp"]))))); + assert!(applies_to_dotnet(Some(&scope(None, Some(&["F#"]))))); + assert!(applies_to_dotnet(Some(&scope( + None, + Some(&["visualbasic"]) + )))); + } + + #[test] + fn a_scope_naming_only_other_toolchains_does_not_apply() { + assert!(!applies_to_dotnet(Some(&scope(Some(&["node"]), None)))); + assert!(!applies_to_dotnet(Some(&scope( + None, + Some(&["typescript"]) + )))); + assert!(!applies_to_dotnet(Some(&scope( + Some(&["rust"]), + Some(&["go"]) + )))); + } + + #[test] + fn a_match_in_either_dimension_is_enough() { + // Scoped to another toolchain but a .NET language, or vice versa. + assert!(applies_to_dotnet(Some(&scope( + Some(&["node"]), + Some(&["csharp"]) + )))); + assert!(applies_to_dotnet(Some(&scope( + Some(&["dotnet"]), + Some(&["typescript"]) + )))); + } +} diff --git a/toolchains/dotnet/src/lib.rs b/toolchains/dotnet/src/lib.rs new file mode 100644 index 00000000..d09da596 --- /dev/null +++ b/toolchains/dotnet/src/lib.rs @@ -0,0 +1,31 @@ +pub mod config; +pub mod discovery; +pub mod dotnet_install; +pub mod eval_cache; +pub mod global_json; +pub mod infer_tasks; +pub mod inherited_tasks; +pub mod msbuild; +pub mod nuget_lock; + +#[cfg(feature = "wasm")] +mod project_graph; +#[cfg(feature = "wasm")] +mod tier1; +#[cfg(feature = "wasm")] +mod tier2; +#[cfg(feature = "wasm")] +mod tier2_env; +#[cfg(feature = "wasm")] +mod tier3; + +#[cfg(feature = "wasm")] +pub use project_graph::*; +#[cfg(feature = "wasm")] +pub use tier1::*; +#[cfg(feature = "wasm")] +pub use tier2::*; +#[cfg(feature = "wasm")] +pub use tier2_env::*; +#[cfg(feature = "wasm")] +pub use tier3::*; diff --git a/toolchains/dotnet/src/msbuild.rs b/toolchains/dotnet/src/msbuild.rs new file mode 100644 index 00000000..eb76c5d6 --- /dev/null +++ b/toolchains/dotnet/src/msbuild.rs @@ -0,0 +1,585 @@ +use moon_pdk_api::AnyResult; +use serde::Deserialize; +use std::collections::BTreeMap; + +/// Result of an MSBuild evaluation via `dotnet msbuild -getProperty:... -getItem:...`. +#[derive(Clone, Debug, Default, Deserialize)] +pub struct MsbuildEvaluation { + #[serde(rename = "Properties", default)] + pub properties: BTreeMap, + + #[serde(rename = "Items", default)] + pub items: BTreeMap>, +} + +impl MsbuildEvaluation { + pub fn property(&self, name: &str) -> &str { + self.properties.get(name).map(String::as_str).unwrap_or("") + } + + /// `FullPath` of every ProjectReference item (host-real absolute paths). + pub fn project_reference_paths(&self) -> Vec { + self.items + .get("ProjectReference") + .map(|items| { + items + .iter() + .filter_map(|item| item.get("FullPath")) + .filter_map(|value| value.as_str()) + .map(|value| value.to_owned()) + .collect() + }) + .unwrap_or_default() + } + + /// PackageReference `Identity` -> `Version` (missing version becomes `*`). + pub fn package_references(&self) -> BTreeMap { + self.identity_version_items("PackageReference", "*") + } + + /// PackageVersion `Identity` -> `Version` (Central Package Management + /// declarations from `Directory.Packages.props`; empty without CPM). + pub fn package_versions(&self) -> BTreeMap { + self.identity_version_items("PackageVersion", "") + } + + fn identity_version_items( + &self, + item_type: &str, + missing_version: &str, + ) -> BTreeMap { + self.items + .get(item_type) + .map(|items| { + items + .iter() + .filter_map(|item| { + let identity = item.get("Identity")?.as_str()?; + let version = item + .get("Version") + .and_then(|value| value.as_str()) + .filter(|value| !value.is_empty()) + .unwrap_or(missing_version); + + Some((identity.to_owned(), version.to_owned())) + }) + .collect() + }) + .unwrap_or_default() + } +} + +/// The exact `-getProperty` list requested per evaluation. +/// `BaseOutputPath`/`BaseIntermediateOutputPath`/`PublishDir` feed inferred +/// task outputs and input exclusions (they follow redirected output +/// locations, e.g. .NET 8 `UseArtifactsOutput`). `AssemblyName`/`Version` +/// feed the project alias and manifest metadata. +/// `TestingPlatformDotnetTestSupport` opts a single project into +/// Microsoft.Testing.Platform, which changes the `dotnet test` command line. +/// `IsTestingPlatformApplication` identifies a test project that carries no +/// `Microsoft.NET.Test.Sdk` reference at all, which is the norm for the +/// test-oriented project SDKs (``). +pub const EVAL_PROPERTIES: &str = "TargetFramework,TargetFrameworks,OutputType,IsTestProject,IsTestingPlatformApplication,IsPackable,RestorePackagesWithLockFile,BaseOutputPath,BaseIntermediateOutputPath,PublishDir,Configuration,AssemblyName,Version,TestingPlatformDotnetTestSupport"; + +/// The exact `-getItem` list requested per evaluation. `PackageVersion` +/// items exist under Central Package Management (declared in +/// `Directory.Packages.props`) and are empty otherwise. +/// +/// Only `ProjectReference` and `PackageReference` come back from *batched* +/// evaluation — the injected target flattens those two into item metadata. +/// `PackageVersion` is therefore only ever populated on the per-project path, +/// which is where it is needed: `parse_manifest` evaluates a +/// `Directory.Packages.props` singly. +pub const EVAL_ITEMS: &str = "ProjectReference,PackageReference,PackageVersion"; + +/// Parse the stdout of an MSBuild `-get*` invocation. MSBuild may print stray +/// warnings before the JSON — start at the first `{`. +pub fn parse_msbuild_output(stdout: &str) -> AnyResult { + let json_start = stdout + .find('{') + .ok_or_else(|| moon_pdk_api::anyhow!("no JSON found in MSBuild output"))?; + + Ok(serde_json::from_str(&stdout[json_start..])?) +} + +/// Lexically normalize a host path for cross-referencing MSBuild output +/// against moon project paths: forward slashes, lowercased (paths on Windows +/// are case-insensitive; MSBuild output casing is not guaranteed to match +/// the on-disk casing moon reports). +pub fn normalize_path_key(path: &str) -> String { + path.replace('\\', "/").to_lowercase() +} + +/// Host environment applied to every MSBuild invocation, so graph evaluation +/// resolves the same SDK that tasks will run under. +#[derive(Clone, Debug, Default)] +pub struct EvalEnv { + /// `DOTNET_ROOT` to evaluate under, when one was resolved. + pub dotnet_root: Option, + + /// Absolute path to the `dotnet` muxer inside `dotnet_root`, when its + /// existence could be confirmed. + /// + /// This is what actually selects the SDK: the host resolves a bare + /// command name from its own `PATH` (warpgate `host.rs` — a command + /// containing a separator is treated as a path, anything else goes + /// through `find_command_on_path`), and the muxer locates SDKs relative + /// to its own location rather than from `DOTNET_ROOT`. Verified + /// empirically: setting only `DOTNET_ROOT`/`paths` left evaluation on the + /// `PATH` SDK. + pub dotnet_exe: Option, + + /// Directory to run MSBuild in. The dotnet host resolves `global.json` + /// from the **current directory** — not from the project path (verified + /// empirically) — so this is what decides which SDK evaluates the + /// projects. Leaving it unset would inherit moon's own working directory, + /// making evaluation depend on where the user happened to run moon from. + pub cwd: Option, + + /// Extra global properties for the evaluation, from the toolchain's + /// `msbuildProperties` setting, passed as `-p:NAME=VALUE`. Command-line + /// global properties propagate through the batched traversal's `` + /// task into every child project, so batch and per-project evaluation see + /// identical values. + pub msbuild_properties: BTreeMap, +} + +/// Render `msbuildProperties` as MSBuild `-p:` arguments, in deterministic +/// (BTreeMap) order. +pub fn msbuild_property_args(properties: &BTreeMap) -> Vec { + properties + .iter() + .map(|(name, value)| format!("-p:{name}={value}")) + .collect() +} + +/// Deepest directory that contains all of the given workspace-relative +/// project sources, as a workspace-relative path (empty when they share no +/// prefix, i.e. the workspace root itself). +/// +/// Used as the evaluation working directory: in a repo whose .NET projects +/// live under one subtree, this is the subtree root, so a `global.json` there +/// applies to evaluation exactly as it applies to the tasks that run inside +/// it. +pub fn common_source_prefix(sources: &[&str]) -> String { + let mut common: Option> = None; + + for source in sources { + let parts = source + .split(['/', '\\']) + .filter(|part| !part.is_empty() && *part != ".") + .collect::>(); + + common = Some(match common { + None => parts, + Some(existing) => existing + .into_iter() + .zip(parts) + // Sources come from moon config, which is case-sensitive + // about the paths it reports; compare them verbatim. + .take_while(|(left, right)| left == right) + .map(|(left, _)| left) + .collect(), + }); + + if common.as_ref().is_some_and(|parts| parts.is_empty()) { + break; + } + } + + common.unwrap_or_default().join("/") +} + +/// Escape a literal path for use inside an MSBuild `Include` attribute: +/// MSBuild's own special characters (property/item expansion, list +/// separators, globs) via `%XX` escapes, then XML attribute characters. +pub fn escape_msbuild_include(path: &str) -> String { + path.replace('%', "%25") + .replace('$', "%24") + .replace('@', "%40") + .replace(';', "%3B") + .replace('*', "%2A") + .replace('?', "%3F") + .replace('&', "&") + .replace('<', "<") + .replace('>', ">") + .replace('"', """) +} + +/// Item metadata name carrying the `|`-joined ProjectReference full paths in +/// batched evaluation output. +const BATCH_PROJECT_REFS: &str = "MoonProjectRefs"; + +/// Item metadata name carrying the `|`-joined `Identity@Version` +/// PackageReference entries in batched evaluation output. +const BATCH_PACKAGE_REFS: &str = "MoonPackageRefs"; + +/// The `.targets` file injected into every project during batched evaluation +/// (via the `CustomAfterMicrosoftCommon(CrossTargeting)Targets` hooks): a +/// target that returns the project's evaluation state as one item with +/// metadata. It runs as the entry target with no dependencies, so item state +/// when it executes is identical to evaluation state — the same answers as a +/// per-project `-getItem` query. +pub fn moon_eval_targets_xml() -> String { + let properties = EVAL_PROPERTIES + .split(',') + .map(|prop| format!(" <{prop}>$({prop})\n")) + .collect::(); + + format!( + r#" + + + <_MoonEvalResult Include="$(MSBuildProjectFullPath)"> +{properties} <{BATCH_PROJECT_REFS}>@(ProjectReference->'%(FullPath)', '|') + <{BATCH_PACKAGE_REFS}>@(PackageReference->'%(Identity)@%(Version)', '|') + + + + +"# + ) +} + +/// The traversal project for batched evaluation: fans out to every listed +/// project with `BuildInParallel` (in-process MSBuild worker nodes) and +/// collects the injected target's outputs. A raw `` with no `Sdk` +/// attribute imports nothing implicitly, so the workspace's own +/// `Directory.Build.props` cannot interfere with the traversal itself, while +/// the child projects still evaluate with their full normal import chains. +/// `ContinueOnError` keeps one broken project from aborting the batch — it +/// just goes missing from the output (and falls back to per-project +/// evaluation). +pub fn traversal_project_xml(project_paths: &[String]) -> String { + let includes = project_paths + .iter() + .map(|path| { + format!( + " \n", + escape_msbuild_include(path) + ) + }) + .collect::(); + + format!( + r#" + +{includes} + + + + + + +"# + ) +} + +/// Parse the `-getItem:MoonEval` JSON of a batched traversal invocation into +/// per-project evaluations. Each project is keyed (normalized) by every +/// identifying path on its item: the traversal `Include` we wrote +/// (`OriginalItemSpec`) and MSBuild's own expanded full path +/// (`MSBuildSourceProjectFile` / `Identity`) — these can differ lexically, +/// e.g. Windows 8.3 short names in temp directories. +pub fn parse_batch_output(stdout: &str) -> AnyResult> { + let raw = parse_msbuild_output(stdout)?; + let mut results = BTreeMap::new(); + + let Some(items) = raw.items.get("MoonEval") else { + return Ok(results); + }; + + for item in items { + let metadata = |name: &str| { + item.get(name) + .and_then(|value| value.as_str()) + .unwrap_or("") + }; + + let mut evaluation = MsbuildEvaluation::default(); + + for prop in EVAL_PROPERTIES.split(',') { + evaluation + .properties + .insert(prop.to_owned(), metadata(prop).to_owned()); + } + + let project_refs = metadata(BATCH_PROJECT_REFS) + .split('|') + .filter(|path| !path.is_empty()) + .map(|path| serde_json::json!({ "FullPath": path })) + .collect::>(); + + if !project_refs.is_empty() { + evaluation + .items + .insert("ProjectReference".to_owned(), project_refs); + } + + let package_refs = metadata(BATCH_PACKAGE_REFS) + .split('|') + .filter(|entry| !entry.is_empty()) + .map(|entry| { + // '@' cannot appear in NuGet package ids or versions; an + // empty version (e.g. Central Package Management) becomes + // `*` downstream. + let (identity, version) = entry.rsplit_once('@').unwrap_or((entry, "")); + serde_json::json!({ "Identity": identity, "Version": version }) + }) + .collect::>(); + + if !package_refs.is_empty() { + evaluation + .items + .insert("PackageReference".to_owned(), package_refs); + } + + for key_field in ["OriginalItemSpec", "MSBuildSourceProjectFile", "Identity"] { + let key = metadata(key_field); + + if !key.is_empty() { + results.insert(normalize_path_key(key), evaluation.clone()); + } + } + } + + Ok(results) +} + +/// Did an invocation fail because the dotnet host could not resolve an SDK +/// (rather than because a project is broken)? +/// +/// Matches on the help URL the host prints, which — unlike the surrounding +/// message text — is not localized. The English phrasing is accepted as a +/// fallback for hosts that omit the link. +pub fn is_sdk_resolution_failure(output: &str) -> bool { + let lower = output.to_lowercase(); + + lower.contains("aka.ms/dotnet/sdk-not-found") + || (lower.contains("global.json") && lower.contains("sdk") && lower.contains("not found")) +} + +/// Given the output of a failed batch invocation, find which of the input +/// projects MSBuild reported diagnostics for. +/// +/// MSBuild writes the file in two shapes, and both have to be recognized: +/// +/// ```text +/// (6,3): error MSB4025: The project file could not be loaded. ... +/// : error : Could not resolve SDK "Totally.Bogus.Sdk". ... +/// ``` +/// +/// The second form — no line/column, emitted for SDK resolution among others — +/// was missed, so a batch killed by an unresolvable SDK reference identified no +/// offender, the retry below never fired, and the whole batch was discarded. +/// +/// Only the trailing `/` suffix is matched, not the full path: +/// MSBuild prints expanded long paths, which can differ lexically from the ones +/// we passed (e.g. Windows 8.3 short names like `RUNNER~1` in a temp-dir +/// prefix). Both anchors keep the match at a token boundary so `App.csproj` +/// cannot match `MyApp.csproj`. +/// +/// Deliberately not filtered to lines containing `": error "`: MSBuild localizes +/// diagnostic text, so that would break on a non-English host. The cost is that +/// a path mentioned in a *warning* is treated as failed too — which merely +/// over-excludes, and an over-excluded project falls back to per-project +/// evaluation and stays correct. The same is true of a suffix shared by two +/// projects. +pub fn detect_failed_projects(output: &str, project_paths: &[String]) -> Vec { + let haystack = normalize_path_key(output); + + project_paths + .iter() + .filter(|path| { + let normalized = normalize_path_key(path); + + // From the second-to-last separator: "//" — the + // leading slash anchors the match to a component boundary. + let suffix = normalized + .rmatch_indices('/') + .nth(1) + .map(|(index, _)| &normalized[index..]) + .unwrap_or(&normalized); + + haystack.contains(&format!("{suffix}(")) || haystack.contains(&format!("{suffix} :")) + }) + .cloned() + .collect() +} + +/// Apply the resolved SDK environment (and any configured evaluation +/// properties) to an MSBuild invocation. +#[cfg(feature = "wasm")] +fn with_eval_env( + mut input: moon_pdk_api::ExecCommandInput, + env: &EvalEnv, +) -> moon_pdk_api::ExecCommandInput { + input + .args + .extend(msbuild_property_args(&env.msbuild_properties)); + + if let Some(root) = &env.dotnet_root { + input.env.insert("DOTNET_ROOT".into(), root.clone()); + input + .paths + .push(moon_pdk_api::VirtualPath::Real(root.into())); + } + + // Only an explicit executable path redirects which SDK evaluates; see + // `EvalEnv::dotnet_exe`. + if let Some(exe) = &env.dotnet_exe { + input.command = exe.clone(); + } + + if let Some(cwd) = &env.cwd { + input.cwd = Some(cwd.clone()); + } + + input +} + +/// Evaluate many projects with a single MSBuild invocation, paying the +/// dotnet/MSBuild startup cost (which dominates per-project evaluation) +/// once instead of once per project, and evaluating in parallel. The +/// generated traversal files live under `.moon/cache/` in the workspace. +#[cfg(feature = "wasm")] +pub fn evaluate_projects_batch( + workspace_root: &moon_pdk_api::VirtualPath, + project_real_paths: &[std::path::PathBuf], + eval_env: &EvalEnv, +) -> AnyResult> { + use moon_pdk::exec; + use moon_pdk_api::{ExecCommandInput, anyhow}; + use starbase_utils::fs; + + // Known constraint: both scratch files use fixed names here, so two moon + // processes building a graph in the same checkout at the same time can have + // one truncating `traversal.proj` while the other's MSBuild reads it. A + // per-invocation subdirectory would fix it, but wasm has no pid, clock or + // randomness to name one with, and `MoonContext` offers only `working_dir` + // and `workspace_root` — a name derived from the project set would still + // collide for the identical batch. The failure is a malformed traversal + // project, which surfaces as a batch failure and falls back to per-project + // evaluation, so it degrades rather than corrupting results. + let dir = workspace_root + .join(".moon") + .join("cache") + .join("dotnet-toolchain"); + + fs::create_dir_all(&dir)?; + fs::write_file(dir.join("moon-eval.targets"), moon_eval_targets_xml())?; + + let traversal = dir.join("traversal.proj"); + + let traversal_arg = traversal + .real_path() + .ok_or_else(|| anyhow!("no host-real path for {traversal:?}"))? + .to_string_lossy() + .to_string(); + + let run = |batch_paths: &[String]| { + fs::write_file(&traversal, traversal_project_xml(batch_paths))?; + + exec(with_eval_env( + ExecCommandInput::pipe( + "dotnet", + [ + "msbuild", + traversal_arg.as_str(), + "-nologo", + // Parallel in-process worker nodes, but never leave them + // alive after the invocation (node reuse lingers ~15 min, + // which is hostile to CI containers). + "-maxCpuCount", + "-nodeReuse:false", + "-t:MoonCollect", + "-getItem:MoonEval", + ], + ), + eval_env, + )) + }; + + let paths = project_real_paths + .iter() + .map(|path| path.to_string_lossy().to_string()) + .collect::>(); + + let output = run(&paths)?; + + if output.exit_code == 0 { + return parse_batch_output(&output.stdout); + } + + // MSBuild returns NO target outputs at all when any project fails to + // load (ContinueOnError does not rescue load errors). Identify the + // offenders from the error lines and retry once without them — their + // absence from the result triggers the caller's per-project fallback, + // which surfaces the real error. + let combined = format!("{}{}", output.stdout, output.stderr); + let failed = detect_failed_projects(&combined, &paths); + + if !failed.is_empty() && failed.len() < paths.len() { + let remaining = paths + .iter() + .filter(|path| !failed.contains(path)) + .cloned() + .collect::>(); + + let retry = run(&remaining)?; + + if retry.exit_code == 0 { + return parse_batch_output(&retry.stdout); + } + } + + Err(anyhow!( + "Batched MSBuild evaluation failed (exit code {}): {}{}", + output.exit_code, + output.stdout, + output.stderr, + )) +} + +/// Run a real MSBuild evaluation for a project file (host-real path). +#[cfg(feature = "wasm")] +pub fn evaluate_project( + csproj_real_path: &std::path::Path, + eval_env: &EvalEnv, +) -> AnyResult { + use moon_pdk::exec; + use moon_pdk_api::{ExecCommandInput, anyhow}; + + let path_arg = csproj_real_path.to_string_lossy().to_string(); + + let output = exec(with_eval_env( + ExecCommandInput::pipe( + "dotnet", + [ + "msbuild", + path_arg.as_str(), + "-nologo", + &format!("-getProperty:{EVAL_PROPERTIES}"), + &format!("-getItem:{EVAL_ITEMS}"), + ], + ), + eval_env, + ))?; + + if output.exit_code != 0 { + return Err(anyhow!( + "MSBuild evaluation failed for {} (exit code {}): {}{}", + csproj_real_path.display(), + output.exit_code, + output.stdout, + output.stderr, + )); + } + + parse_msbuild_output(&output.stdout) +} diff --git a/toolchains/dotnet/src/nuget_lock.rs b/toolchains/dotnet/src/nuget_lock.rs new file mode 100644 index 00000000..a47a4329 --- /dev/null +++ b/toolchains/dotnet/src/nuget_lock.rs @@ -0,0 +1,104 @@ +use serde::Deserialize; +use std::collections::BTreeMap; + +/// A NuGet `packages.lock.json` file. +/// +/// Shape: `{ "version": 1, "dependencies": { "": { "": +/// { "type", "requested", "resolved", "contentHash", ... } } } }` +#[derive(Debug, Default, Deserialize)] +pub struct NugetLockFile { + #[serde(default)] + pub version: u32, + + #[serde(default)] + pub dependencies: BTreeMap>, +} + +#[derive(Debug, Default, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct NugetLockEntry { + /// "Direct", "Transitive", "Project", or "CentralTransitive". + #[serde(default, rename = "type")] + pub dep_type: String, + + #[serde(default)] + pub requested: Option, + + #[serde(default)] + pub resolved: Option, + + #[serde(default)] + pub content_hash: Option, +} + +pub fn parse_lock_file(content: &str) -> Result { + serde_json::from_str(content) +} + +#[cfg(test)] +mod tests { + use super::*; + + const SAMPLE: &str = r#"{ + "version": 1, + "dependencies": { + "net8.0": { + "Newtonsoft.Json": { + "type": "Direct", + "requested": "[13.0.3, )", + "resolved": "13.0.3", + "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" + }, + "App": { + "type": "Project", + "dependencies": { + "Lib": "[1.0.0, )" + } + } + }, + "net9.0": { + "Newtonsoft.Json": { + "type": "Direct", + "requested": "[13.0.3, )", + "resolved": "13.0.3", + "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" + } + } + } +}"#; + + #[test] + fn parses_lock_file() { + let lock = parse_lock_file(SAMPLE).unwrap(); + + assert_eq!(lock.version, 1); + assert_eq!(lock.dependencies.len(), 2); + + let net8 = &lock.dependencies["net8.0"]; + let newtonsoft = &net8["Newtonsoft.Json"]; + + assert_eq!(newtonsoft.dep_type, "Direct"); + assert_eq!(newtonsoft.requested.as_deref(), Some("[13.0.3, )")); + assert_eq!(newtonsoft.resolved.as_deref(), Some("13.0.3")); + assert!( + newtonsoft + .content_hash + .as_deref() + .unwrap() + .starts_with("HrC5") + ); + + // Project-type entries carry no resolved version/hash. + let project = &net8["App"]; + assert_eq!(project.dep_type, "Project"); + assert!(project.resolved.is_none()); + } + + #[test] + fn tolerates_empty_file_shape() { + let lock = parse_lock_file("{}").unwrap(); + + assert_eq!(lock.version, 0); + assert!(lock.dependencies.is_empty()); + } +} diff --git a/toolchains/dotnet/src/project_graph.rs b/toolchains/dotnet/src/project_graph.rs new file mode 100644 index 00000000..1f5ccaa1 --- /dev/null +++ b/toolchains/dotnet/src/project_graph.rs @@ -0,0 +1,553 @@ +//! Contributing .NET structure to moon's project graph. +//! +//! `extend_project_graph` runs in three passes: index every project's MSBuild +//! files, evaluate them all in one batched MSBuild invocation, then map each +//! project's `ProjectReference` items onto moon project ids and infer its tasks. + +use crate::config::DotnetToolchainConfig; +use crate::discovery::{find_lock_files, find_project_files}; +use crate::eval_cache::write_eval_cache; +use crate::infer_tasks::{InferInputs, infer_tasks, reportable_conflicts}; +use crate::inherited_tasks::load_inherited_task_ids; +use crate::msbuild::{ + EvalEnv, MsbuildEvaluation, common_source_prefix, evaluate_project, evaluate_projects_batch, + is_sdk_resolution_failure, normalize_path_key, +}; +use crate::tier2_env::{ + build_eval_env, find_sdk_requirement, sdk_install_configured, uses_test_platform_runner, +}; +use extism_pdk::*; +use moon_config::DependencyScope; +use moon_pdk::{ + HostLogInput, HostLogTarget, command_exists, get_host_environment, host_log, + parse_toolchain_config, plugin_err, +}; +use moon_pdk_api::*; +use std::collections::{BTreeMap, BTreeSet}; + +#[host_fn] +extern "ExtismHost" { + fn host_log(input: Json); +} + +/// Everything the first pass discovers: which MSBuild files each moon project +/// owns, plus two indexes for resolving a `ProjectReference` path back to a moon +/// project id. +struct ProjectIndexes { + files: BTreeMap>, + + /// Normalized host-real project-file path -> owning project. + by_real_path: BTreeMap, + + /// Normalized workspace-relative `//` suffix -> owning + /// project, or `None` when two projects share the suffix (ambiguous). + /// + /// This exists because the exact real paths can differ lexically from what + /// MSBuild prints — Windows 8.3 short names such as `RUNNER~1` in a temp-dir + /// prefix expand to their long form in MSBuild output. + by_suffix: BTreeMap>, +} + +/// Pass 1: locate every project's MSBuild files and index them. +fn build_project_indexes(input: &ExtendProjectGraphInput) -> ProjectIndexes { + let mut indexes = ProjectIndexes { + files: BTreeMap::new(), + by_real_path: BTreeMap::new(), + by_suffix: BTreeMap::new(), + }; + + for (id, source) in &input.project_sources { + let project_root = input.context.workspace_root.join(source); + let files = find_project_files(&project_root); + + if files.is_empty() { + // Not a .NET project; none of our business. + continue; + } + + for file in &files { + if let Some(real) = file.real_path() { + indexes + .by_real_path + .insert(normalize_path_key(&real.to_string_lossy()), id.to_owned()); + } + + if let Some(name) = file.file_name().and_then(|name| name.to_str()) { + let source = source.trim_matches('/'); + let suffix = if source.is_empty() || source == "." { + normalize_path_key(&format!("/{name}")) + } else { + normalize_path_key(&format!("/{source}/{name}")) + }; + + indexes + .by_suffix + .entry(suffix) + .and_modify(|existing| *existing = None) + .or_insert_with(|| Some(id.to_owned())); + } + } + + indexes.files.insert(id.to_owned(), files); + } + + indexes +} + +/// Resolve a `ProjectReference` path to the moon project that owns it: exact +/// real-path match first, then the **longest** matching workspace-relative +/// suffix. +/// +/// Longest, not first. One key can end with several indexed suffixes: with +/// sources `lib` and `src/lib` both holding an `App.csproj`, a reference to +/// `/ws/src/lib/App.csproj` ends with both `/lib/app.csproj` and +/// `/src/lib/app.csproj`. Taking the first match in `BTreeMap` order returned +/// the lexicographically smaller `/lib/...`, i.e. a dependency edge pointing at +/// the wrong project — the worst failure mode here, since the graph is then +/// silently wrong rather than merely incomplete. +/// +/// There is no tie to break: two suffixes of equal length that are both +/// suffixes of the same key are the same string, and the index holds each key +/// once. Genuinely ambiguous suffixes are already recorded as `None` when the +/// index is built. +fn resolve_reference<'index>( + indexes: &'index ProjectIndexes, + reference: &str, +) -> Option<&'index Id> { + let key = normalize_path_key(reference); + + indexes.by_real_path.get(&key).or_else(|| { + indexes + .by_suffix + .iter() + .filter(|(suffix, id)| id.is_some() && key.ends_with(suffix.as_str())) + .max_by_key(|(suffix, _)| suffix.len()) + .and_then(|(_, id)| id.as_ref()) + }) +} + +/// The working directory to evaluate from: the deepest directory containing +/// every .NET project, so a `global.json` in that subtree governs evaluation +/// exactly as it governs the tasks that run inside it. Without an explicit +/// working directory the dotnet host would resolve `global.json` from wherever +/// moon happened to be invoked, so the same workspace could evaluate under +/// different SDKs run to run. +fn batch_eval_env( + config: &DotnetToolchainConfig, + input: &ExtendProjectGraphInput, + indexes: &ProjectIndexes, +) -> AnyResult { + let sources = input + .project_sources + .iter() + .filter(|(id, _)| indexes.files.contains_key(*id)) + .map(|(_, source)| source.as_str()) + .collect::>(); + + let eval_prefix = common_source_prefix(&sources); + let eval_dir = if eval_prefix.is_empty() { + input.context.workspace_root.clone() + } else { + input.context.workspace_root.join(&eval_prefix) + }; + + build_eval_env(config, eval_dir, &input.context.workspace_root) +} + +/// Pass 2: evaluate every project in a single batched MSBuild invocation (one +/// process, parallel in-process evaluation). The dotnet/MSBuild startup cost +/// dominates per-project evaluation, so this is the difference between minutes +/// and seconds on large workspaces. +/// +/// A recoverable batch failure yields an empty map: each project then falls back +/// to its own evaluation, keeping the batch purely an optimization. +/// +/// An unresolvable SDK is different — it dooms every project, so falling back +/// would only repeat the host's cryptic output once per project and still leave +/// the graph empty. It is reported once, naming the pin and the ways out, and +/// whether that report is fatal depends on whether anything is going to fix it: +/// +/// - No `version:` configured: nothing will install the missing SDK, so this is a +/// terminal misconfiguration and the graph build fails with the guidance. +/// - `version:` configured: tier 3 installs that SDK later in the same run — the +/// project graph is built before the action pipeline starts, so failing here +/// would deadlock the very bootstrap the setting exists for. Warns instead and +/// returns `None`, the same way a missing `dotnet` does. +/// +/// `None` means "contribute nothing at all", and is distinct from `Some(empty)`: +/// an empty batch sends every project through per-project evaluation, which is +/// right for a recoverable failure and wrong when no SDK exists — there it would +/// reproduce the host's output once per project, the exact noise this reports +/// once instead. +fn run_batch_evaluation( + input: &ExtendProjectGraphInput, + indexes: &ProjectIndexes, + eval_env: &EvalEnv, +) -> FnResult>> { + let all_project_paths = indexes + .files + .values() + .flatten() + .filter_map(|file| file.real_path()) + .collect::>(); + + match evaluate_projects_batch(&input.context.workspace_root, &all_project_paths, eval_env) { + Ok(results) => Ok(Some(results)), + Err(error) => { + let message = error.to_string(); + + if is_sdk_resolution_failure(&message) { + let pin = find_sdk_requirement( + eval_env + .cwd + .as_ref() + .unwrap_or(&input.context.workspace_root), + &input.context.workspace_root, + ); + + let requirement = match &pin { + Some((file, requirement)) => format!( + "The .NET SDK pinned by {} ({}) is not available", + file, requirement.version + ), + None => "No usable .NET SDK was found".to_owned(), + }; + + if sdk_install_configured(&input.context.workspace_root) { + host_log!( + warn, + "{requirement} yet, so .NET project graph evaluation is being skipped — no dependency edges or inferred tasks will be contributed on this run. moon installs the SDK configured by version later in this run; re-run afterwards to pick them up." + ); + + return Ok(None); + } + + return Err(plugin_err!( + "{requirement}, so MSBuild evaluation cannot run.\n\nInstall that SDK, set version under dotnet in .moon/toolchains.yml to have moon install it, or point dotnetRoot at an SDK that satisfies the pin.\n\n{message}" + )); + } + + host_log!( + warn, + "Batched MSBuild evaluation failed; falling back to per-project evaluation: {}", + error + ); + + Ok(Some(BTreeMap::new())) + } + } +} + +/// Task ids inference must not contribute, because an inherited task file +/// already defines them. moon merges plugin tasks over inherited ones with +/// args-append semantics, which produces a garbage command. Project-level +/// `moon.yml` needs no such handling: moon guarantees local tasks win. +fn reserved_task_ids( + config: &DotnetToolchainConfig, + workspace_root: &VirtualPath, +) -> AnyResult> { + let reserved = load_inherited_task_ids(workspace_root); + + // Report once per workspace, not once per project: without this, "no + // project has a build task" has no visible cause. + for (task_id, file) in reportable_conflicts(&reserved, &config.infer_tasks) { + host_log!( + warn, + "Not inferring the {} task: {} already defines it, and moon merges inherited and plugin tasks by appending args — which would produce a broken command. Rename or remove that task to let inference contribute, or list only the tasks you want in inferTasks.", + task_id, + file + ); + } + + Ok(reserved.into_keys().collect()) +} + +/// Shared, read-only state for mapping one project. +struct GraphContext<'a> { + config: &'a DotnetToolchainConfig, + indexes: &'a ProjectIndexes, + eval_env: &'a EvalEnv, + reserved_task_ids: &'a BTreeSet, + workspace_root: &'a VirtualPath, + + /// Host-real workspace root, for making evaluated output paths relative. + workspace_dir: &'a str, + + infer_tasks_enabled: bool, +} + +/// What one moon project contributed. +struct ProjectEvaluation { + output: ExtendProjectOutput, + packages: BTreeMap, + + /// Every one of the project's MSBuild files evaluated successfully. The + /// package set above may only be cached when this holds — a partial set is + /// indistinguishable from a complete one once written, and would then be + /// served under a digest that stays valid. + complete: bool, + + /// Project files to report to moon as graph inputs. + input_files: Vec, +} + +/// Pass 3, for one project: map its `ProjectReference` items onto moon project +/// ids, take its alias from the evaluated `AssemblyName`, and infer its tasks. +fn extend_one_project( + ctx: &GraphContext<'_>, + id: &Id, + files: &[VirtualPath], + batch: &mut BTreeMap, + test_platform_runner: bool, +) -> AnyResult { + let mut result = ProjectEvaluation { + output: ExtendProjectOutput::default(), + packages: BTreeMap::new(), + complete: true, + input_files: vec![], + }; + + let mut seen_deps: BTreeSet = BTreeSet::new(); + + for file in files { + let Some(real_path) = file.real_path() else { + result.complete = false; + continue; + }; + + let batch_key = normalize_path_key(&real_path.to_string_lossy()); + + let evaluation = if let Some(evaluation) = batch.remove(&batch_key) { + evaluation + } else { + // Fall back with the project's own directory as the working + // directory — the same `global.json` its tasks will resolve. + let single_env = EvalEnv { + cwd: file.parent().or_else(|| ctx.eval_env.cwd.clone()), + ..ctx.eval_env.clone() + }; + + match evaluate_project(&real_path, &single_env) { + Ok(evaluation) => evaluation, + Err(error) => { + // One broken project must not take down graph construction + // for the whole workspace. + host_log!( + warn, + "MSBuild evaluation failed for project {} ({}): {}", + id, + real_path.display(), + error + ); + + result.complete = false; + continue; + } + } + }; + + result.packages.extend(evaluation.package_references()); + + // Project alias from the evaluated AssemblyName, so tasks can reference + // the project by its .NET name (e.g. `moon run MyCompany.App:build`). + // moon silently skips aliases that collide with project ids or + // already-claimed aliases, and an alias equal to its own id is a no-op — + // no need to filter beyond emptiness here. + if result.output.alias.is_none() { + let assembly_name = evaluation.property("AssemblyName"); + + if !assembly_name.is_empty() { + result.output.alias = Some(assembly_name.to_owned()); + } + } + + if ctx.config.infer_dependencies { + for reference in evaluation.project_reference_paths() { + let Some(dep_id) = resolve_reference(ctx.indexes, &reference) else { + host_log!( + debug, + "Project {} references {} which is outside the moon workspace; skipping", + id, + reference + ); + continue; + }; + + if dep_id != id && seen_deps.insert(dep_id.to_owned()) { + let file_name = std::path::Path::new(&reference) + .file_name() + .map(|name| name.to_string_lossy().to_string()) + .unwrap_or(reference.clone()); + + result.output.dependencies.push(ProjectDependency { + id: dep_id.to_owned(), + scope: DependencyScope::Production, + via: Some(format!("project-reference {file_name}")), + }); + } + } + } + + if ctx.infer_tasks_enabled { + let project_dir = real_path + .parent() + .map(|dir| dir.to_string_lossy().to_string()) + .unwrap_or_default(); + + // Bare `dotnet build` errors on ambiguity when the directory holds + // several project files — pass the file explicitly. + let explicit_project_file = if files.len() > 1 { + file.file_name().and_then(|name| name.to_str()) + } else { + None + }; + + let inferred = infer_tasks( + &ctx.config.infer_tasks, + ctx.reserved_task_ids, + &InferInputs { + evaluation: &evaluation, + explicit_project_file, + project_dir: &project_dir, + workspace_dir: ctx.workspace_dir, + test_platform_runner, + }, + ); + + match inferred { + Ok(tasks) => { + for (task_id, task) in tasks { + result.output.tasks.entry(task_id).or_insert(task); + } + } + Err(error) => { + host_log!( + warn, + "Task inference failed for project {}: {}", + id, + error + ); + } + } + } + + if let Some(virtual_file) = file.virtual_path() { + result.input_files.push(virtual_file); + } + } + + Ok(result) +} + +#[plugin_fn] +pub fn extend_project_graph( + Json(mut input): Json, +) -> FnResult> { + // Taken rather than moved out, so `input` stays whole for the helpers below. + let config = parse_toolchain_config::(std::mem::take( + &mut input.toolchain_config, + ))?; + let mut output = ExtendProjectGraphOutput::default(); + + let infer_tasks_enabled = config.infer_tasks.any_enabled(); + + if !config.infer_dependencies && !infer_tasks_enabled { + return Ok(Json(output)); + } + + let indexes = build_project_indexes(&input); + + if indexes.files.is_empty() { + return Ok(Json(output)); + } + + // Degrade rather than fail, like `parse_manifest` and `hash_task_contents` + // below. The graph is built before the action pipeline runs, so a `version:` + // configured for tier 3 to install has not been installed yet on a fresh + // machine — erroring here would fail the whole-workspace graph, for every + // toolchain, before moon ever gets to install the SDK it was told to + // install. + if !command_exists(&get_host_environment()?, "dotnet") { + host_log!( + warn, + "No dotnet executable found on PATH, skipping .NET project graph evaluation — no dependency edges or inferred tasks will be contributed. Install a .NET 8+ SDK, or set version in .moon/toolchains.yml to have moon install one." + ); + + return Ok(Json(output)); + } + + let eval_env = batch_eval_env(&config, &input, &indexes)?; + + // `None` means no SDK is available to evaluate with. Returning here rather + // than continuing with an empty batch is what keeps a single unresolvable pin + // from being reported once per project by the per-project fallback. + let Some(mut batch) = run_batch_evaluation(&input, &indexes, &eval_env)? else { + return Ok(Json(output)); + }; + + let workspace_dir = input + .context + .workspace_root + .real_path() + .map(|path| path.to_string_lossy().to_string()) + .unwrap_or_default(); + + let reserved = if infer_tasks_enabled { + reserved_task_ids(&config, &input.context.workspace_root)? + } else { + BTreeSet::new() + }; + + let ctx = GraphContext { + config: &config, + indexes: &indexes, + eval_env: &eval_env, + reserved_task_ids: &reserved, + workspace_root: &input.context.workspace_root, + workspace_dir: &workspace_dir, + infer_tasks_enabled, + }; + + for (id, files) in &indexes.files { + let project_root = input + .project_sources + .get(id) + .map(|source| input.context.workspace_root.join(source)); + + // Which `dotnet test` flavour this project's tasks will run under. + let test_platform_runner = infer_tasks_enabled + && project_root + .as_ref() + .is_some_and(|root| uses_test_platform_runner(root, ctx.workspace_root)); + + let result = extend_one_project(&ctx, id, files, &mut batch, test_platform_runner)?; + + output.input_files.extend(result.input_files); + + // Hand the evaluated package set to task hashing. Projects with a lock + // file take the lock-file branch there and never need it. + if result.complete + && let Some(project_root) = project_root + && find_lock_files(&project_root).is_empty() + { + write_eval_cache( + &input.context.workspace_root, + id.as_str(), + &project_root, + &ctx.config.msbuild_properties, + result.packages, + ); + } + + if !result.output.dependencies.is_empty() + || !result.output.tasks.is_empty() + || result.output.alias.is_some() + { + output + .extended_projects + .insert(id.to_owned(), result.output); + } + } + + Ok(Json(output)) +} diff --git a/toolchains/dotnet/src/tier1.rs b/toolchains/dotnet/src/tier1.rs new file mode 100644 index 00000000..3af3908d --- /dev/null +++ b/toolchains/dotnet/src/tier1.rs @@ -0,0 +1,119 @@ +use crate::config::DotnetToolchainConfig; +use extism_pdk::*; +use moon_config::LanguageType; +use moon_pdk_api::*; +use schematic::SchemaBuilder; +use starbase_utils::fs; +use toolchain_common::enable_tracing; + +#[plugin_fn] +pub fn register_toolchain( + Json(_): Json, +) -> FnResult> { + enable_tracing(); + + Ok(Json(RegisterToolchainOutput { + name: ".NET".into(), + description: Some( + "Provides .NET SDK project-graph extraction, dependency install (dotnet restore), and Docker support for SDK-style C#/F#/VB projects.".into(), + ), + plugin_version: env!("CARGO_PKG_VERSION").into(), + language: Some(LanguageType::CSharp), + exe_names: vec!["dotnet".into()], + config_file_globs: vec![ + "*.{csproj,fsproj,vbproj}".into(), + "*.{sln,slnx}".into(), + "global.json".into(), + "Directory.Build.props".into(), + "Directory.Build.targets".into(), + "Directory.Build.rsp".into(), + "Directory.Packages.props".into(), + // NuGet accepts any casing; cover the ones seen in the wild so + // case-sensitive filesystems still match. + "{nuget,NuGet}.{config,Config}".into(), + // `packages..lock.json` via NuGetLockFilePath; the + // default name lives in lock_file_names (exact match only there). + "packages.*.lock.json".into(), + ], + // Project files (*.csproj) have variable names, which moon's + // literal-name manifest matching cannot express; their detection is + // covered by config_file_globs instead. Directory.Packages.props is + // the one fixed-name .NET manifest: registering it makes CPM version + // bumps re-trigger dependency installs via parse_manifest. + manifest_file_names: vec!["Directory.Packages.props".into()], + lock_file_names: vec!["packages.lock.json".into()], + // NuGet uses a global package cache, not an in-repo vendor dir. + vendor_dir_name: None, + })) +} + +#[plugin_fn] +pub fn define_toolchain_config() -> FnResult> { + Ok(Json(DefineToolchainConfigOutput { + schema: SchemaBuilder::build_root::(), + })) +} + +#[plugin_fn] +pub fn initialize_toolchain( + Json(_): Json, +) -> FnResult> { + // There is nothing to prompt for: every setting has a working default, and + // the SDK version is read from `global.json` rather than configured here. + Ok(Json(InitializeToolchainOutput::default())) +} + +#[plugin_fn] +pub fn define_docker_metadata( + Json(_): Json, +) -> FnResult> { + Ok(Json(DefineDockerMetadataOutput { + // Intentionally not derived from the configured `version`: doing so + // would mean reading toolchain config here to pick an image tag, which + // is a product decision rather than a default. `version` is honoured + // where it matters — tier 3 installs exactly that SDK. + default_image: Some("mcr.microsoft.com/dotnet/sdk:latest".into()), + scaffold_globs: vec![ + "**/*.{csproj,fsproj,vbproj}".into(), + "**/*.{sln,slnx}".into(), + "**/*.props".into(), + "**/*.targets".into(), + "**/Directory.Build.rsp".into(), + "**/{nuget,NuGet}.{config,Config}".into(), + "**/packages.lock.json".into(), + "**/packages.*.lock.json".into(), + "global.json".into(), + // bin/obj contain generated *.props (obj/*.nuget.g.props) and + // must never end up in the restore layer. + "!**/bin/**".into(), + "!**/obj/**".into(), + ], + })) +} + +#[plugin_fn] +pub fn prune_docker(Json(input): Json) -> FnResult> { + let mut output = PruneDockerOutput::default(); + + let mut roots = vec![input.root.clone()]; + + for project in &input.projects { + roots.push(input.context.get_project_root(project)); + } + + for root in roots { + for dir_name in ["bin", "obj"] { + let dir = root.join(dir_name); + + if dir.exists() { + fs::remove_dir_all(&dir)?; + + if let Some(file) = dir.virtual_path() { + output.changed_files.push(file); + } + } + } + } + + Ok(Json(output)) +} diff --git a/toolchains/dotnet/src/tier2.rs b/toolchains/dotnet/src/tier2.rs new file mode 100644 index 00000000..dfc50e07 --- /dev/null +++ b/toolchains/dotnet/src/tier2.rs @@ -0,0 +1,369 @@ +use crate::config::DotnetToolchainConfig; +use crate::discovery::{ + LOCKFILE_SEARCH_DEPTH, contains_lockfile, find_config_files, find_lock_files, + find_project_files, has_solution_file, walk_up, +}; +use crate::eval_cache::{read_eval_cache, write_eval_cache}; +use crate::msbuild::evaluate_project; +use crate::nuget_lock::parse_lock_file; +use crate::tier2_env::build_eval_env; +use extism_pdk::*; +use moon_config::{UnresolvedVersionSpec, VersionSpec}; +use moon_pdk::{ + HostLogInput, HostLogTarget, command_exists, get_host_environment, host_log, + is_project_toolchain_enabled, parse_toolchain_config, +}; +use moon_pdk_api::*; +use starbase_utils::fs; +use std::collections::BTreeMap; + +#[host_fn] +extern "ExtismHost" { + fn host_log(input: Json); +} + +#[plugin_fn] +pub fn locate_dependencies_root( + Json(input): Json, +) -> FnResult> { + let mut output = LocateDependenciesRootOutput::default(); + let workspace_root = &input.context.workspace_root; + + // Nearest solution file wins. + for dir in walk_up(&input.starting_dir, workspace_root) { + if has_solution_file(&dir) { + output.root = dir.virtual_path(); + break; + } + } + + // Fall back to the nearest lockfile, then the nearest project file. + for probe in [find_lock_files, find_project_files] { + if output.root.is_some() { + break; + } + + for dir in walk_up(&input.starting_dir, workspace_root) { + if !probe(&dir).is_empty() { + output.root = dir.virtual_path(); + break; + } + } + } + + // Single dependencies root for v1; no member globs. + output.members = None; + + Ok(Json(output)) +} + +#[plugin_fn] +pub fn install_dependencies( + Json(input): Json, +) -> FnResult> { + let config = parse_toolchain_config::(input.toolchain_config)?; + let mut output = InstallDependenciesOutput::default(); + + let mut args: Vec = vec!["restore".into()]; + + // The mere presence of a lock file opts a project into lock-file restore; + // --locked-mode additionally fails restore (NU1004) when declared + // dependencies drifted from the lock file. + if contains_lockfile(&input.root, LOCKFILE_SEARCH_DEPTH) { + args.push("--locked-mode".into()); + } + + args.extend(config.restore_args.iter().cloned()); + + output.install_command = Some( + ExecCommandInput::new("dotnet", args) + .cwd(input.root.clone()) + .into(), + ); + // NuGet has no dedupe concept. + output.dedupe_command = None; + + Ok(Json(output)) +} + +#[plugin_fn] +pub fn parse_lock(Json(input): Json) -> FnResult> { + let mut output = ParseLockOutput::default(); + let lock = parse_lock_file(&fs::read_file(&input.path)?)?; + + // Dedupe identical entries across target frameworks. + for entries in lock.dependencies.into_values() { + for (name, entry) in entries { + // Project-type entries are in-repo ProjectReferences, not packages. + if entry.dep_type.eq_ignore_ascii_case("Project") { + continue; + } + + let versions = output.dependencies.entry(name).or_default(); + + let version = entry + .resolved + .as_deref() + .and_then(|value| VersionSpec::parse(value).ok()); + + let already_present = versions.iter().any(|existing: &LockDependency| { + existing.version == version && existing.hash == entry.content_hash + }); + + if !already_present { + versions.push(LockDependency { + hash: entry.content_hash, + meta: None, + // NuGet ranges like "[13.0.3, )" may not parse; omit then. + req: entry + .requested + .as_deref() + .and_then(|value| UnresolvedVersionSpec::parse(value).ok()), + version, + }); + } + } + } + + Ok(Json(output)) +} + +#[plugin_fn] +pub fn parse_manifest( + Json(input): Json, +) -> FnResult> { + let mut output = ParseManifestOutput::default(); + + let Some(real_path) = input.path.real_path() else { + return Ok(Json(output)); + }; + + let env = get_host_environment()?; + + // Degrade silently like hash_task_contents: a missing dotnet must not + // fail moon's install fingerprinting. + if !command_exists(&env, "dotnet") { + return Ok(Json(output)); + } + + let manifest_dir = input + .path + .parent() + .unwrap_or_else(|| input.context.workspace_root.clone()); + + // `parse_manifest` carries no toolchain config, so an explicit + // `dotnetRoot` cannot be honored here (nor `msbuildProperties`); the env + // var and the guarded `~/.dotnet` fallback still apply. + let eval_env = build_eval_env( + &DotnetToolchainConfig::default(), + manifest_dir, + &input.context.workspace_root, + )?; + + let evaluation = match evaluate_project(&real_path, &eval_env) { + Ok(evaluation) => evaluation, + Err(error) => { + host_log!( + warn, + "MSBuild evaluation failed while parsing manifest {}: {}", + real_path.display(), + error + ); + + return Ok(Json(output)); + } + }; + + // NuGet range syntax ("[13.0.3]", "(1.0,2.0)") is not a moon version + // spec; keep the raw string as a reference so the dependency is still + // listed (it just won't contribute a version to fingerprints). + let to_dependency = |version: String| match UnresolvedVersionSpec::parse(&version) { + Ok(spec) => ManifestDependency::new(spec), + Err(_) => ManifestDependency::Config(ManifestDependencyConfig { + reference: Some(version), + ..Default::default() + }), + }; + + let is_packages_props = input + .path + .file_name() + .and_then(|name| name.to_str()) + .is_some_and(|name| name.eq_ignore_ascii_case("Directory.Packages.props")); + + if is_packages_props { + // Central Package Management: PackageVersion items declare the + // workspace-level versions that versionless PackageReferences + // inherit. This is the only manifest name moon can actually track + // for .NET — project files have variable names, which moon's + // literal-name manifest matching cannot express. + for (name, version) in evaluation.package_versions() { + output.dependencies.insert(name, to_dependency(version)); + } + } else { + for (name, version) in evaluation.package_references() { + let dep = if version == "*" { + // Versionless under CPM: inherited from the workspace + // manifest (Directory.Packages.props). + ManifestDependency::inherited() + } else { + to_dependency(version) + }; + + output.dependencies.insert(name, dep); + } + + output.publishable = evaluation + .property("IsPackable") + .eq_ignore_ascii_case("true"); + } + + Ok(Json(output)) +} + +#[plugin_fn] +pub fn hash_task_contents( + Json(input): Json, +) -> FnResult> { + let mut output = HashTaskContentsOutput::default(); + + if !is_project_toolchain_enabled(&input.project) { + return Ok(Json(output)); + } + + let project_root = input.context.get_project_root(&input.project); + + // Config files (Directory.Build.props/targets/rsp, Directory.Packages.props, + // nuget.config, global.json) from the project dir up to the workspace root + // are always hashed: conditions/imports can make any of them affect the + // resolved package set, and props/targets/rsp change build behavior even + // when the package set is fully pinned by a lock file. Effects of custom + // ``s outside these conventions are only captured via the + // evaluated package set below, not content-hashed. + let mut configs: BTreeMap = BTreeMap::new(); + let workspace_root = &input.context.workspace_root; + + for dir in walk_up(&project_root, workspace_root) { + for file in find_config_files(&dir) { + let key = file + .virtual_path() + .map(|path| path.to_string_lossy().to_string()) + .unwrap_or_else(|| file.to_string()); + + configs.insert(key, fs::read_file(&file)?); + } + } + + // Lock file(s) present: their content already pins the entire resolved + // package set (incl. contentHashes) — include them raw and skip the + // costly MSBuild evaluation. + let lock_files = find_lock_files(&project_root); + + if !lock_files.is_empty() { + let mut lockfiles: BTreeMap = BTreeMap::new(); + + for file in &lock_files { + let key = file + .virtual_path() + .map(|path| path.to_string_lossy().to_string()) + .unwrap_or_else(|| file.to_string()); + + lockfiles.insert(key, fs::read_file(file)?); + } + + output.contents.push(json::json!({ + "configs": configs, + "lockfiles": lockfiles, + })); + + return Ok(Json(output)); + } + + // No lock file: hash the *evaluated* PackageReference set instead. + // + // Three levels of reuse, because this function runs once per task and a + // cold MSBuild evaluation costs ~0.5s per project: + // 1. a plugin-instance var, for repeated tasks of the same project; + // 2. the on-disk cache the batched graph evaluation primed, which is + // what keeps a lock-file-less workspace from paying one evaluation + // per project here (the batch already evaluated them all at once); + // 3. evaluating this project alone. + let cache_key = format!("eval-packages:{}", input.project.id); + let config = parse_toolchain_config::(input.toolchain_config)?; + + let packages: BTreeMap = if let Some(cached) = var::get::(&cache_key)? { + serde_json::from_str(&cached)? + } else if let Some(cached) = read_eval_cache( + workspace_root, + input.project.id.as_str(), + &project_root, + &config.msbuild_properties, + ) { + var::set(&cache_key, serde_json::to_string(&cached)?)?; + + cached + } else { + let mut packages = BTreeMap::new(); + let mut evaluated_all = false; + let env = get_host_environment()?; + + if command_exists(&env, "dotnet") { + let eval_env = build_eval_env(&config, project_root.clone(), workspace_root)?; + + evaluated_all = true; + + for file in find_project_files(&project_root) { + let Some(real_path) = file.real_path() else { + evaluated_all = false; + continue; + }; + + match evaluate_project(&real_path, &eval_env) { + Ok(evaluation) => { + packages.extend(evaluation.package_references()); + } + Err(error) => { + host_log!( + warn, + "MSBuild evaluation failed while hashing {}: {}", + input.project.id, + error + ); + + evaluated_all = false; + } + } + } + } + + // Kept regardless: the var is scoped to this plugin instance, so it + // stops us re-evaluating once per task while an SDK is genuinely + // missing, and it disappears with the process. + var::set(&cache_key, serde_json::to_string(&packages)?)?; + + // The on-disk cache only ever holds a complete set. Writing a partial + // one would persist it under a digest that keeps validating, and since + // this set is the only hash signal for a workspace without lock files, + // package changes would stop invalidating task hashes — moon would + // serve stale builds, and installing the missing SDK later would not + // recover it. + if evaluated_all { + write_eval_cache( + workspace_root, + input.project.id.as_str(), + &project_root, + &config.msbuild_properties, + packages.clone(), + ); + } + + packages + }; + + output.contents.push(json::json!({ + "configs": configs, + "packages": packages, + })); + + Ok(Json(output)) +} diff --git a/toolchains/dotnet/src/tier2_env.rs b/toolchains/dotnet/src/tier2_env.rs new file mode 100644 index 00000000..a5e372a0 --- /dev/null +++ b/toolchains/dotnet/src/tier2_env.rs @@ -0,0 +1,311 @@ +//! Resolving which SDK to use, and the environment tasks run under. +//! +//! Task environments and MSBuild evaluation must agree on a `DOTNET_ROOT`, or +//! the project graph gets evaluated by one SDK while tasks run under another. +//! Everything that decides that lives here, alongside the two tier-2 functions +//! that materialize an environment. + +use crate::config::DotnetToolchainConfig; +use crate::discovery::{installed_sdk_versions, walk_up}; +use crate::eval_cache::content_digest; +use crate::global_json::{SdkRequirement, parse_sdk_requirement, satisfies, selects_test_platform}; +use crate::msbuild::EvalEnv; +use extism_pdk::*; +use moon_pdk::{ + HostLogInput, HostLogTarget, command_exists, get_host_env_var, get_host_environment, host_log, + into_virtual_path, parse_toolchain_config, +}; +use moon_pdk_api::*; +use starbase_utils::{fs, yaml}; + +#[host_fn] +extern "ExtismHost" { + fn host_log(input: Json); +} + +/// Has moon been told to install a .NET SDK itself, via `version:` under +/// `dotnet` in `.moon/toolchains.yml`? +/// +/// That is a moon-level toolchain setting, not one of ours, so it never reaches +/// the plugin through `toolchain_config` — `setup_toolchain` receives it as +/// `configured_version`, but the project graph is built before any of that runs. +/// Reading the file is the only way to know at graph-build time, and it decides +/// whether an unresolvable SDK is a terminal misconfiguration or simply an SDK +/// that has not been installed yet. +pub fn sdk_install_configured(workspace_root: &VirtualPath) -> bool { + let file = workspace_root.join(".moon").join("toolchains.yml"); + + if !file.exists() { + return false; + } + + // Untyped: `version` may be a string (`'8.0'`), a bare YAML float (`8.0`) or + // an alias (`lts`), and only its presence matters here. + yaml::read_file::(file.any_path()) + .ok() + .and_then(|root| { + root.get("dotnet") + .and_then(|section| section.get("version")) + .cloned() + }) + .is_some_and(|version| !version.is_null()) +} + +/// Nearest `global.json` SDK pin, searching from `start` up to (and +/// including) the workspace root — the same direction the dotnet host +/// searches from its working directory. Returns the file path (for messages) +/// and the parsed pin. +/// +/// The search stops at the first `global.json` that exists, whether or not it +/// declares an `sdk.version`, because that is the one file the dotnet host +/// resolves — it neither merges them nor keeps looking. Walking past a pinless +/// file would attribute an ancestor's pin to a directory it does not govern, and +/// name that non-governing file in the diagnostics. `uses_test_platform_runner` +/// below already implements this rule; the two must agree. +pub fn find_sdk_requirement( + start: &VirtualPath, + workspace_root: &VirtualPath, +) -> Option<(String, SdkRequirement)> { + for dir in walk_up(start, workspace_root) { + let file = dir.join("global.json"); + + if file.exists() { + return fs::read_file(&file) + .ok() + .and_then(|content| parse_sdk_requirement(&content)) + .map(|requirement| (file.to_string(), requirement)); + } + } + + None +} + +/// Does the `global.json` governing this directory select +/// Microsoft.Testing.Platform for `dotnet test`? The nearest file wins, +/// whether or not it names a runner — the dotnet host resolves exactly one +/// `global.json`, it does not merge them. +pub fn uses_test_platform_runner(start: &VirtualPath, workspace_root: &VirtualPath) -> bool { + for dir in walk_up(start, workspace_root) { + let file = dir.join("global.json"); + + if file.exists() { + return fs::read_file(&file) + .map(|content| selects_test_platform(&content)) + .unwrap_or(false); + } + } + + false +} + +/// Where to look for a `global.json` SDK pin when validating the `~/.dotnet` +/// fallback: from `start` up to (and including) `workspace_root`. +pub struct SdkPinScope<'a> { + pub start: &'a VirtualPath, + pub workspace_root: &'a VirtualPath, +} + +/// Resolve the DOTNET_ROOT for task environments *and* MSBuild evaluation — +/// both must agree, or the graph gets evaluated by one SDK while tasks run +/// under another. +/// +/// Order: explicit config > existing host env var > `~/.dotnet` when it holds +/// a real SDK layout (where the proto dotnet plugin installs). +/// +/// The `~/.dotnet` fallback is guarded: a leftover install there (a stale +/// proto experiment, say) would otherwise be injected over a perfectly good +/// system SDK, making every task fail against a `global.json` pin it cannot +/// satisfy. When a `dotnet` exists on PATH and the fallback cannot serve the +/// workspace's pin, the fallback is skipped so PATH wins. Explicit +/// configuration is never second-guessed. +fn resolve_dotnet_root( + config: &DotnetToolchainConfig, + scope: Option>, +) -> AnyResult> { + if let Some(root) = &config.dotnet_root { + return Ok(Some(root.clone())); + } + + if let Some(existing) = get_host_env_var("DOTNET_ROOT")? + && !existing.is_empty() + { + return Ok(Some(existing)); + } + + let env = get_host_environment()?; + let candidate = env.home_dir.join(".dotnet"); + + // `~/.dotnet` doubles as the dotnet CLI's user-level cache directory, so + // mere existence is not enough — require the `dotnet` host executable, + // which a real SDK install provides. + let exe = if env.os.is_windows() { + "dotnet.exe" + } else { + "dotnet" + }; + + if !candidate.join(exe).exists() { + return Ok(None); + } + + if let Some(scope) = scope + && command_exists(&env, "dotnet") + && let Some((file, requirement)) = find_sdk_requirement(scope.start, scope.workspace_root) + { + let installed = installed_sdk_versions(&candidate); + + if !satisfies(&installed, &requirement) { + host_log!( + warn, + "Ignoring the ~/.dotnet fallback for DOTNET_ROOT: it has no SDK satisfying {} from {} (found: {}). Using the dotnet on PATH instead — set dotnetRoot to override.", + requirement.version, + file, + if installed.is_empty() { + "none".to_owned() + } else { + installed.join(", ") + } + ); + + return Ok(None); + } + } + + if let Some(real) = candidate.real_path() { + let root = real.to_string_lossy().to_string(); + + host_log!( + debug, + "Using the ~/.dotnet fallback as DOTNET_ROOT: {}", + root + ); + + return Ok(Some(root)); + } + + Ok(None) +} + +/// Build the MSBuild evaluation environment: the same DOTNET_ROOT tasks get, +/// plus an explicit working directory (`global.json` resolves from there). +pub fn build_eval_env( + config: &DotnetToolchainConfig, + cwd: VirtualPath, + workspace_root: &VirtualPath, +) -> AnyResult { + let dotnet_root = resolve_dotnet_root( + config, + Some(SdkPinScope { + start: &cwd, + workspace_root, + }), + )?; + + // Point at the muxer inside the root, but only when its existence can be + // confirmed — a host path must be virtualized before wasm can stat it, + // and roots outside the plugin's readable paths cannot be checked at all. + // Guessing would turn a working evaluation into "command not found", so + // unverifiable roots keep using the `dotnet` on PATH, as before. + let dotnet_exe = dotnet_root.as_ref().and_then(|root| { + let env = get_host_environment().ok()?; + let exe = if env.os.is_windows() { + "dotnet.exe" + } else { + "dotnet" + }; + let real = std::path::PathBuf::from(root).join(exe); + + into_virtual_path(&real) + .ok()? + .exists() + // The host converts a command containing a separator back from + // its virtual form, so pass the real path. + .then(|| real.to_string_lossy().to_string()) + }); + + Ok(EvalEnv { + dotnet_root, + dotnet_exe, + cwd: Some(cwd), + msbuild_properties: config.msbuild_properties.clone(), + }) +} + +#[plugin_fn] +pub fn extend_task_command( + Json(input): Json, +) -> FnResult> { + let config = parse_toolchain_config::(input.toolchain_config)?; + let mut output = ExtendTaskCommandOutput::default(); + + // Tasks run in their project directory, so that is where the dotnet host + // resolves `global.json` from — validate the fallback against that pin. + let project_root = input.context.get_project_root(&input.project); + let scope = SdkPinScope { + start: &project_root, + workspace_root: &input.context.workspace_root, + }; + + // Deliberately only DOTNET_ROOT and PATH. Injecting vendor environment + // variables nobody asked for is surprising, and the + // `DOTNET_CLI_TELEMETRY_OPTOUT` that used to be set here was set *inside* + // this branch — so it never applied in the common case of a system SDK on + // PATH with no DOTNET_ROOT. It also does not suppress the "Welcome to .NET" + // first-run banner, which is what `DOTNET_NOLOGO` controls. Both belong in a + // task's own `env`, where they are visible. + if let Some(root) = resolve_dotnet_root(&config, Some(scope))? { + output.env.insert("DOTNET_ROOT".into(), root.clone()); + output.paths.push(root.into()); + } + + Ok(Json(output)) +} + +#[plugin_fn] +pub fn setup_environment( + Json(input): Json, +) -> FnResult> { + let mut output = SetupEnvironmentOutput::default(); + + // Restore local dotnet tools once per dependencies root when a tool + // manifest exists. Local tools (.config/dotnet-tools.json) are distinct + // from global tools, which remain out of scope. + // + // Search from the dependencies root up to the workspace root, the same + // way the dotnet CLI resolves a tool manifest: it conventionally lives at + // the repository root, which is not necessarily a dependencies root (any + // project directory holding a lock file becomes one). + let mut tool_manifest = None; + + for dir in walk_up(&input.root, &input.context.workspace_root) { + let candidate = dir.join(".config").join("dotnet-tools.json"); + + if candidate.exists() { + tool_manifest = Some(candidate); + break; + } + } + + if let Some(tool_manifest) = tool_manifest { + let mut command = ExecCommand::new( + ExecCommandInput::new("dotnet", ["tool", "restore"]).cwd(input.root.clone()), + ); + + command.label = Some("dotnet tool restore".into()); + + // The cache key carries a digest of the manifest content, because + // moon fingerprints this action on the *declaration* we return here + // and skips it wholesale when unchanged — a stable key would mean a + // manifest edit never re-runs the restore. `inputs` then prevents + // re-execution when the action runs again for unrelated reasons. + command.cache = Some(format!( + "dotnet-tool-restore-{}", + content_digest(&fs::read_file(&tool_manifest)?) + )); + command.inputs.push(CacheInput::FileHash(tool_manifest)); + + output.commands.push(command); + } + + Ok(Json(output)) +} diff --git a/toolchains/dotnet/src/tier3.rs b/toolchains/dotnet/src/tier3.rs new file mode 100644 index 00000000..a87af2bd --- /dev/null +++ b/toolchains/dotnet/src/tier3.rs @@ -0,0 +1,239 @@ +use crate::config::DotnetToolchainConfig; +use crate::discovery::{SKIP_DIRS, installed_sdk_versions}; +use crate::dotnet_install::{ + exact_version, install_script_file_name, install_script_url, install_version_args, +}; +use crate::global_json::{parse_sdk_requirement, satisfies}; +use extism_pdk::*; +use moon_pdk::{ + HostLogInput, HostLogTarget, exec, fetch_text, get_host_environment, host_log, + into_virtual_path, parse_toolchain_config, plugin_err, +}; +use moon_pdk_api::*; +use starbase_utils::fs; + +#[host_fn] +extern "ExtismHost" { + fn host_log(input: Json); +} + +/// Collect `global.json` files in the workspace (depth-limited). Unlike task +/// environments, setup has no project to walk up from — and the pin often +/// lives in a subtree (`src/backend/global.json`) rather than at the root. +fn collect_global_json_files(dir: &VirtualPath, depth: u8, out: &mut Vec) { + let Ok(entries) = fs::read_dir(dir.any_path()) else { + return; + }; + + let mut subdirs = vec![]; + + for entry in entries { + let Ok(name) = entry.file_name().into_string() else { + continue; + }; + + if entry.file_type().is_ok_and(|kind| kind.is_dir()) { + if depth > 0 + && !SKIP_DIRS + .iter() + .any(|skip| skip.eq_ignore_ascii_case(&name)) + { + subdirs.push(name); + } + } else if name.eq_ignore_ascii_case("global.json") { + out.push(dir.join(&name)); + } + } + + for name in subdirs { + collect_global_json_files(&dir.join(name), depth - 1, out); + } +} + +/// Warn when the SDKs now present in the install root cannot serve a +/// `global.json` pin in the workspace. Installing 8.0 while a subtree pins +/// 10.x is a silent misconfiguration otherwise: setup succeeds and every task +/// in that subtree fails later with the host's own error. +/// +/// A warning rather than an error: the pinned subtree may deliberately rely +/// on a system-wide SDK instead of the one moon manages. +fn warn_on_unsatisfied_pins( + workspace_root: &VirtualPath, + install_root: &VirtualPath, +) -> AnyResult<()> { + let mut files = vec![]; + collect_global_json_files(workspace_root, 4, &mut files); + + if files.is_empty() { + return Ok(()); + } + + let installed = installed_sdk_versions(install_root); + + for file in files { + let Ok(content) = fs::read_file(&file) else { + continue; + }; + + let Some(requirement) = parse_sdk_requirement(&content) else { + continue; + }; + + if !satisfies(&installed, &requirement) { + host_log!( + warn, + "{} pins .NET SDK {}, which the installed SDKs do not satisfy ({}). Tasks under that directory will fail until the pinned SDK is installed — set version to a matching value.", + file, + requirement.version, + if installed.is_empty() { + "none installed".to_owned() + } else { + installed.join(", ") + } + ); + } + } + + Ok(()) +} + +#[plugin_fn] +pub fn setup_toolchain( + Json(input): Json, +) -> FnResult> { + let mut output = SetupToolchainOutput::default(); + + // Without a `version:` setting moon skips the setup action entirely + // ("use globals on PATH"); stay a no-op if called anyway. + let Some(spec) = &input.configured_version else { + return Ok(Json(output)); + }; + + let config = parse_toolchain_config::(input.toolchain_config)?; + let env = get_host_environment()?; + let windows = env.os.is_windows(); + + // Install root: explicit `dotnetRoot` config wins, else `~/.dotnet` — + // the same order resolve_dotnet_root uses when injecting DOTNET_ROOT + // into task environments, so installed SDKs are picked up without any + // further configuration. SDK versions install side-by-side. + let install_root: std::path::PathBuf = match &config.dotnet_root { + Some(root) => root.into(), + None => { + let Some(home) = env.home_dir.real_path() else { + return Err(plugin_err!( + "Unable to resolve the host home directory for the default `~/.dotnet` install root." + )); + }; + + home.join(".dotnet") + } + }; + + let version_args = match install_version_args(spec, windows) { + Ok(args) => args, + Err(message) => return Err(plugin_err!("{}", message)), + }; + + // Fully-qualified versions can skip the network entirely when that SDK + // is already laid out. Channels/aliases resolve server-side, so the + // install script decides for those (it skips re-installs itself). + if let Some(version) = exact_version(spec) + && into_virtual_path(install_root.join("sdk").join(&version))?.exists() + { + warn_on_unsatisfied_pins( + &input.context.workspace_root, + &into_virtual_path(&install_root)?, + )?; + + return Ok(Json(output)); + } + + // Stage the official install script under moon's cache dir, fetched once. + // moon does not fingerprint-cache this action — `setup_toolchain` uses + // `create_hash_and_return_lock`, which unlike the `_if_changed` variant has + // no "manifest exists, skip" short-circuit — so this function runs on every + // moon invocation. Re-downloading each time made every command depend on + // reaching dot.net, which breaks offline and air-gapped workspaces + // outright. Delete the file to force a re-fetch. + let script_file = input + .context + .workspace_root + .join(".moon/cache/dotnet-toolchain") + .join(install_script_file_name(windows)); + + if !script_file.exists() { + if let Some(parent) = script_file.parent() { + fs::create_dir_all(parent)?; + } + + fs::write_file(&script_file, fetch_text(install_script_url(windows))?)?; + } + + let Some(script_path) = script_file.real_path() else { + return Err(plugin_err!( + "Unable to resolve a host path for the staged install script." + )); + }; + + // `--no-path`: task environments get DOTNET_ROOT/PATH injected by + // extend_task_command; the user's shell profile is left alone. + let mut args: Vec = if windows { + vec![ + "-NoProfile".into(), + "-ExecutionPolicy".into(), + "Bypass".into(), + "-File".into(), + script_path.to_string_lossy().to_string(), + "-InstallDir".into(), + install_root.to_string_lossy().to_string(), + "-NoPath".into(), + ] + } else { + vec![ + script_path.to_string_lossy().to_string(), + "--install-dir".into(), + install_root.to_string_lossy().to_string(), + "--no-path".into(), + ] + }; + + args.extend(version_args); + + let command = if windows { "powershell.exe" } else { "bash" }; + + // Known limitation: for a channel or alias (`version: '8.0'`, `'lts'`) the + // script still runs on every invocation, because only the server can say + // which patch a channel currently resolves to. It exits early once that SDK + // is present, but it does need the network to find out. Pin a + // fully-qualified version to take the exact-version path above and skip + // this entirely. + let mut operation = Operation::new("install-sdk")?; + let result = exec(ExecCommandInput::pipe(command, args))?; + + if result.exit_code != 0 { + operation.finish(OperationStatus::Failed); + output.operations.push(operation); + + return Err(plugin_err!( + "dotnet-install failed with exit code {}:\n{}\n{}", + result.exit_code, + result.stdout, + result.stderr, + )); + } + + operation.finish(OperationStatus::Passed); + output.operations.push(operation); + + warn_on_unsatisfied_pins( + &input.context.workspace_root, + &into_virtual_path(&install_root)?, + )?; + + // Informational only: for WASM-only toolchains the host currently + // derives the action status itself and merges just operations/files. + output.installed = true; + + Ok(Json(output)) +} diff --git a/toolchains/dotnet/tests/__fixtures__/cpm/Directory.Packages.props b/toolchains/dotnet/tests/__fixtures__/cpm/Directory.Packages.props new file mode 100644 index 00000000..ed592163 --- /dev/null +++ b/toolchains/dotnet/tests/__fixtures__/cpm/Directory.Packages.props @@ -0,0 +1,8 @@ + + + true + + + + + diff --git a/toolchains/dotnet/tests/__fixtures__/cpm/proj/Class1.cs b/toolchains/dotnet/tests/__fixtures__/cpm/proj/Class1.cs new file mode 100644 index 00000000..4e32e62e --- /dev/null +++ b/toolchains/dotnet/tests/__fixtures__/cpm/proj/Class1.cs @@ -0,0 +1,3 @@ +namespace Cpm; + +public class Class1; diff --git a/toolchains/dotnet/tests/__fixtures__/cpm/proj/Cpm.csproj b/toolchains/dotnet/tests/__fixtures__/cpm/proj/Cpm.csproj new file mode 100644 index 00000000..95ba4f5a --- /dev/null +++ b/toolchains/dotnet/tests/__fixtures__/cpm/proj/Cpm.csproj @@ -0,0 +1,8 @@ + + + net8.0 + + + + + diff --git a/toolchains/dotnet/tests/__fixtures__/locate-no-sln/proj/Proj.csproj b/toolchains/dotnet/tests/__fixtures__/locate-no-sln/proj/Proj.csproj new file mode 100644 index 00000000..ec2cce14 --- /dev/null +++ b/toolchains/dotnet/tests/__fixtures__/locate-no-sln/proj/Proj.csproj @@ -0,0 +1,5 @@ + + + net8.0 + + diff --git a/toolchains/dotnet/tests/__fixtures__/locate/Root.sln b/toolchains/dotnet/tests/__fixtures__/locate/Root.sln new file mode 100644 index 00000000..0e909292 --- /dev/null +++ b/toolchains/dotnet/tests/__fixtures__/locate/Root.sln @@ -0,0 +1,2 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 diff --git a/toolchains/dotnet/tests/__fixtures__/locate/nested/proj/Proj.csproj b/toolchains/dotnet/tests/__fixtures__/locate/nested/proj/Proj.csproj new file mode 100644 index 00000000..ec2cce14 --- /dev/null +++ b/toolchains/dotnet/tests/__fixtures__/locate/nested/proj/Proj.csproj @@ -0,0 +1,5 @@ + + + net8.0 + + diff --git a/toolchains/dotnet/tests/__fixtures__/locked/proj/Class1.cs b/toolchains/dotnet/tests/__fixtures__/locked/proj/Class1.cs new file mode 100644 index 00000000..c0bd9ca5 --- /dev/null +++ b/toolchains/dotnet/tests/__fixtures__/locked/proj/Class1.cs @@ -0,0 +1,6 @@ +namespace Locked; + +public static class Class1 +{ + public static string Json() => Newtonsoft.Json.JsonConvert.SerializeObject(new { ok = true }); +} diff --git a/toolchains/dotnet/tests/__fixtures__/locked/proj/Locked.csproj b/toolchains/dotnet/tests/__fixtures__/locked/proj/Locked.csproj new file mode 100644 index 00000000..933d733e --- /dev/null +++ b/toolchains/dotnet/tests/__fixtures__/locked/proj/Locked.csproj @@ -0,0 +1,9 @@ + + + net8.0 + enable + + + + + diff --git a/toolchains/dotnet/tests/__fixtures__/locked/proj/packages.lock.json b/toolchains/dotnet/tests/__fixtures__/locked/proj/packages.lock.json new file mode 100644 index 00000000..edc62aba --- /dev/null +++ b/toolchains/dotnet/tests/__fixtures__/locked/proj/packages.lock.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "dependencies": { + "net8.0": { + "Newtonsoft.Json": { + "type": "Direct", + "requested": "[13.0.3, )", + "resolved": "13.0.3", + "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" + } + } + } +} \ No newline at end of file diff --git a/toolchains/dotnet/tests/__fixtures__/matrix/Directory.Build.props b/toolchains/dotnet/tests/__fixtures__/matrix/Directory.Build.props new file mode 100644 index 00000000..5c14cf23 --- /dev/null +++ b/toolchains/dotnet/tests/__fixtures__/matrix/Directory.Build.props @@ -0,0 +1,8 @@ + + + 12 + + + + + diff --git a/toolchains/dotnet/tests/__fixtures__/matrix/cond/Class1.cs b/toolchains/dotnet/tests/__fixtures__/matrix/cond/Class1.cs new file mode 100644 index 00000000..b3547b3e --- /dev/null +++ b/toolchains/dotnet/tests/__fixtures__/matrix/cond/Class1.cs @@ -0,0 +1,3 @@ +namespace Cond; + +public class Class1; diff --git a/toolchains/dotnet/tests/__fixtures__/matrix/cond/Cond.csproj b/toolchains/dotnet/tests/__fixtures__/matrix/cond/Cond.csproj new file mode 100644 index 00000000..66fccc33 --- /dev/null +++ b/toolchains/dotnet/tests/__fixtures__/matrix/cond/Cond.csproj @@ -0,0 +1,11 @@ + + + net8.0 + 1 + + + + + + + diff --git a/toolchains/dotnet/tests/__fixtures__/matrix/multi/Class1.cs b/toolchains/dotnet/tests/__fixtures__/matrix/multi/Class1.cs new file mode 100644 index 00000000..dc5020eb --- /dev/null +++ b/toolchains/dotnet/tests/__fixtures__/matrix/multi/Class1.cs @@ -0,0 +1,5 @@ +namespace Multi; + +public class Class1 +{ +} diff --git a/toolchains/dotnet/tests/__fixtures__/matrix/multi/Multi.csproj b/toolchains/dotnet/tests/__fixtures__/matrix/multi/Multi.csproj new file mode 100644 index 00000000..61914e6f --- /dev/null +++ b/toolchains/dotnet/tests/__fixtures__/matrix/multi/Multi.csproj @@ -0,0 +1,10 @@ + + + net8.0;netstandard2.0 + + + + + + diff --git a/toolchains/dotnet/tests/__fixtures__/matrix/nested/Directory.Build.props b/toolchains/dotnet/tests/__fixtures__/matrix/nested/Directory.Build.props new file mode 100644 index 00000000..2bf8a608 --- /dev/null +++ b/toolchains/dotnet/tests/__fixtures__/matrix/nested/Directory.Build.props @@ -0,0 +1,7 @@ + + + + + + + diff --git a/toolchains/dotnet/tests/__fixtures__/matrix/nested/deep/Class1.cs b/toolchains/dotnet/tests/__fixtures__/matrix/nested/deep/Class1.cs new file mode 100644 index 00000000..ea7802bc --- /dev/null +++ b/toolchains/dotnet/tests/__fixtures__/matrix/nested/deep/Class1.cs @@ -0,0 +1,3 @@ +namespace Deep; + +public class Class1; diff --git a/toolchains/dotnet/tests/__fixtures__/matrix/nested/deep/Deep.csproj b/toolchains/dotnet/tests/__fixtures__/matrix/nested/deep/Deep.csproj new file mode 100644 index 00000000..81fb97b0 --- /dev/null +++ b/toolchains/dotnet/tests/__fixtures__/matrix/nested/deep/Deep.csproj @@ -0,0 +1,6 @@ + + + MyCompany.Deep + net8.0 + + diff --git a/toolchains/dotnet/tests/__fixtures__/mixed-lang/app/App.csproj b/toolchains/dotnet/tests/__fixtures__/mixed-lang/app/App.csproj new file mode 100644 index 00000000..c563353f --- /dev/null +++ b/toolchains/dotnet/tests/__fixtures__/mixed-lang/app/App.csproj @@ -0,0 +1,10 @@ + + + Exe + net8.0 + enable + + + + + diff --git a/toolchains/dotnet/tests/__fixtures__/mixed-lang/app/Program.cs b/toolchains/dotnet/tests/__fixtures__/mixed-lang/app/Program.cs new file mode 100644 index 00000000..63d1d2cf --- /dev/null +++ b/toolchains/dotnet/tests/__fixtures__/mixed-lang/app/Program.cs @@ -0,0 +1 @@ +Console.WriteLine(Lib.Say.hello()); diff --git a/toolchains/dotnet/tests/__fixtures__/mixed-lang/core/Class1.vb b/toolchains/dotnet/tests/__fixtures__/mixed-lang/core/Class1.vb new file mode 100644 index 00000000..f0cb2e9c --- /dev/null +++ b/toolchains/dotnet/tests/__fixtures__/mixed-lang/core/Class1.vb @@ -0,0 +1,2 @@ +Public Class Class1 +End Class diff --git a/toolchains/dotnet/tests/__fixtures__/mixed-lang/core/Core.vbproj b/toolchains/dotnet/tests/__fixtures__/mixed-lang/core/Core.vbproj new file mode 100644 index 00000000..e489be62 --- /dev/null +++ b/toolchains/dotnet/tests/__fixtures__/mixed-lang/core/Core.vbproj @@ -0,0 +1,6 @@ + + + Core + net8.0 + + diff --git a/toolchains/dotnet/tests/__fixtures__/mixed-lang/lib/Lib.fsproj b/toolchains/dotnet/tests/__fixtures__/mixed-lang/lib/Lib.fsproj new file mode 100644 index 00000000..87a10718 --- /dev/null +++ b/toolchains/dotnet/tests/__fixtures__/mixed-lang/lib/Lib.fsproj @@ -0,0 +1,11 @@ + + + net8.0 + + + + + + + + diff --git a/toolchains/dotnet/tests/__fixtures__/mixed-lang/lib/Library.fs b/toolchains/dotnet/tests/__fixtures__/mixed-lang/lib/Library.fs new file mode 100644 index 00000000..6b6340f4 --- /dev/null +++ b/toolchains/dotnet/tests/__fixtures__/mixed-lang/lib/Library.fs @@ -0,0 +1,4 @@ +namespace Lib + +module Say = + let hello () = "hello" diff --git a/toolchains/dotnet/tests/__fixtures__/mtp/global.json b/toolchains/dotnet/tests/__fixtures__/mtp/global.json new file mode 100644 index 00000000..3140116d --- /dev/null +++ b/toolchains/dotnet/tests/__fixtures__/mtp/global.json @@ -0,0 +1,5 @@ +{ + "test": { + "runner": "Microsoft.Testing.Platform" + } +} diff --git a/toolchains/dotnet/tests/__fixtures__/mtp/suite/Helpers.csproj b/toolchains/dotnet/tests/__fixtures__/mtp/suite/Helpers.csproj new file mode 100644 index 00000000..ec2cce14 --- /dev/null +++ b/toolchains/dotnet/tests/__fixtures__/mtp/suite/Helpers.csproj @@ -0,0 +1,5 @@ + + + net8.0 + + diff --git a/toolchains/dotnet/tests/__fixtures__/mtp/suite/Suite.Tests.csproj b/toolchains/dotnet/tests/__fixtures__/mtp/suite/Suite.Tests.csproj new file mode 100644 index 00000000..bcef8366 --- /dev/null +++ b/toolchains/dotnet/tests/__fixtures__/mtp/suite/Suite.Tests.csproj @@ -0,0 +1,11 @@ + + + net8.0 + + Exe + true + + + + + diff --git a/toolchains/dotnet/tests/__fixtures__/mtp/suite/UnitTest1.cs b/toolchains/dotnet/tests/__fixtures__/mtp/suite/UnitTest1.cs new file mode 100644 index 00000000..c9418ae6 --- /dev/null +++ b/toolchains/dotnet/tests/__fixtures__/mtp/suite/UnitTest1.cs @@ -0,0 +1,6 @@ +public class UnitTest1 +{ + public void Passes() + { + } +} diff --git a/toolchains/dotnet/tests/__fixtures__/projects/app-tests/App.Tests.csproj b/toolchains/dotnet/tests/__fixtures__/projects/app-tests/App.Tests.csproj new file mode 100644 index 00000000..4e363e65 --- /dev/null +++ b/toolchains/dotnet/tests/__fixtures__/projects/app-tests/App.Tests.csproj @@ -0,0 +1,13 @@ + + + net8.0 + enable + false + + + + + + + + diff --git a/toolchains/dotnet/tests/__fixtures__/projects/app-tests/UnitTest1.cs b/toolchains/dotnet/tests/__fixtures__/projects/app-tests/UnitTest1.cs new file mode 100644 index 00000000..21c1b23a --- /dev/null +++ b/toolchains/dotnet/tests/__fixtures__/projects/app-tests/UnitTest1.cs @@ -0,0 +1,9 @@ +using Xunit; + +namespace App.Tests; + +public class UnitTest1 +{ + [Fact] + public void Passes() => Assert.True(true); +} diff --git a/toolchains/dotnet/tests/__fixtures__/projects/app/App.csproj b/toolchains/dotnet/tests/__fixtures__/projects/app/App.csproj new file mode 100644 index 00000000..d1d8dc7a --- /dev/null +++ b/toolchains/dotnet/tests/__fixtures__/projects/app/App.csproj @@ -0,0 +1,11 @@ + + + Exe + net8.0 + enable + enable + + + + + diff --git a/toolchains/dotnet/tests/__fixtures__/projects/app/Program.cs b/toolchains/dotnet/tests/__fixtures__/projects/app/Program.cs new file mode 100644 index 00000000..1d8b652f --- /dev/null +++ b/toolchains/dotnet/tests/__fixtures__/projects/app/Program.cs @@ -0,0 +1 @@ +Console.WriteLine(Lib.Class1.Hello()); diff --git a/toolchains/dotnet/tests/__fixtures__/projects/core/Class1.cs b/toolchains/dotnet/tests/__fixtures__/projects/core/Class1.cs new file mode 100644 index 00000000..4cbf7427 --- /dev/null +++ b/toolchains/dotnet/tests/__fixtures__/projects/core/Class1.cs @@ -0,0 +1,6 @@ +namespace Core; + +public static class Class1 +{ + public static string Value() => "hello"; +} diff --git a/toolchains/dotnet/tests/__fixtures__/projects/core/Core.csproj b/toolchains/dotnet/tests/__fixtures__/projects/core/Core.csproj new file mode 100644 index 00000000..ad1b47cd --- /dev/null +++ b/toolchains/dotnet/tests/__fixtures__/projects/core/Core.csproj @@ -0,0 +1,6 @@ + + + net8.0 + enable + + diff --git a/toolchains/dotnet/tests/__fixtures__/projects/lib/Class1.cs b/toolchains/dotnet/tests/__fixtures__/projects/lib/Class1.cs new file mode 100644 index 00000000..006dbb05 --- /dev/null +++ b/toolchains/dotnet/tests/__fixtures__/projects/lib/Class1.cs @@ -0,0 +1,6 @@ +namespace Lib; + +public static class Class1 +{ + public static string Hello() => Core.Class1.Value(); +} diff --git a/toolchains/dotnet/tests/__fixtures__/projects/lib/Lib.csproj b/toolchains/dotnet/tests/__fixtures__/projects/lib/Lib.csproj new file mode 100644 index 00000000..6cc721a2 --- /dev/null +++ b/toolchains/dotnet/tests/__fixtures__/projects/lib/Lib.csproj @@ -0,0 +1,9 @@ + + + net8.0 + enable + + + + + diff --git a/toolchains/dotnet/tests/__fixtures__/unevaluatable/proj/Broken.csproj b/toolchains/dotnet/tests/__fixtures__/unevaluatable/proj/Broken.csproj new file mode 100644 index 00000000..f8bab3ef --- /dev/null +++ b/toolchains/dotnet/tests/__fixtures__/unevaluatable/proj/Broken.csproj @@ -0,0 +1,9 @@ + + + net8.0 + + + + diff --git a/toolchains/dotnet/tests/infer_tasks_test.rs b/toolchains/dotnet/tests/infer_tasks_test.rs new file mode 100644 index 00000000..1e0e6650 --- /dev/null +++ b/toolchains/dotnet/tests/infer_tasks_test.rs @@ -0,0 +1,521 @@ +use dotnet_toolchain::config::InferTasksSetting; +use dotnet_toolchain::infer_tasks::*; +use dotnet_toolchain::msbuild::MsbuildEvaluation; +use moon_common::Id; +use moon_config::{ + Input, Output, PartialTaskArgs, PartialTaskConfig, PartialTaskDependency, + PartialTaskDependencyConfig, TaskOptionCache, TaskOptionRunInCI, +}; +use moon_target::Target; +use std::collections::{BTreeMap, BTreeSet}; + +mod infer_tasks { + use super::*; + + fn evaluation(properties: &[(&str, &str)]) -> MsbuildEvaluation { + let mut evaluation = MsbuildEvaluation::default(); + + for (name, value) in properties { + evaluation + .properties + .insert(name.to_string(), value.to_string()); + } + + evaluation + } + + fn infer( + evaluation: &MsbuildEvaluation, + setting: &InferTasksSetting, + reserved: &[&str], + ) -> BTreeMap { + infer_tasks( + setting, + &reserved.iter().map(|id| id.to_string()).collect(), + &InferInputs { + evaluation, + explicit_project_file: None, + project_dir: "C:\\work\\repo\\app", + workspace_dir: "C:\\work\\repo", + test_platform_runner: false, + }, + ) + .unwrap() + } + + fn test_project_evaluation() -> MsbuildEvaluation { + let mut eval = evaluation(&[("OutputType", "Exe"), ("TargetFramework", "net10.0")]); + eval.items.insert( + "PackageReference".into(), + vec![serde_json::json!({ "Identity": "Microsoft.NET.Test.Sdk" })], + ); + eval + } + + /// An `Exe` with the given package references and no test-related property, + /// mirroring what an unrestored tree reports. + fn exe_with_packages(packages: &[&str]) -> MsbuildEvaluation { + let mut eval = evaluation(&[("OutputType", "Exe"), ("TargetFramework", "net10.0")]); + eval.items.insert( + "PackageReference".into(), + packages + .iter() + .map(|name| serde_json::json!({ "Identity": name })) + .collect(), + ); + eval + } + + fn command_line(task: &PartialTaskConfig) -> String { + match task.command.as_ref().unwrap() { + PartialTaskArgs::List(list) => list.join(" "), + PartialTaskArgs::String(value) => value.clone(), + other => panic!("unexpected command shape: {other:?}"), + } + } + + #[test] + fn classlib_gets_build_only() { + let eval = evaluation(&[ + ("OutputType", "Library"), + ("BaseOutputPath", "bin\\"), + ("BaseIntermediateOutputPath", "obj\\"), + ]); + let tasks = infer(&eval, &InferTasksSetting::default(), &[]); + + assert_eq!( + tasks.keys().map(|id| id.as_str()).collect::>(), + vec!["build"] + ); + + let build = &tasks[&Id::raw("build")]; + assert_eq!( + command_line(build), + "dotnet build --no-restore --no-dependencies" + ); + assert_eq!( + build.outputs.as_ref().unwrap(), + &vec![Output::parse("bin").unwrap()] + ); + assert!(build.options.is_none(), "outputs known => cache untouched"); + assert!(build.deps.is_some()); + // Inputs exclude the evaluated output/intermediate dirs so hashes + // stabilize (obj is mutated by every build). + assert_eq!( + build.inputs.as_ref().unwrap(), + &vec![ + Input::parse("**/*").unwrap(), + Input::parse("!bin/**").unwrap(), + Input::parse("!obj/**").unwrap(), + ] + ); + } + + #[test] + fn exe_gets_build_run_publish() { + let eval = evaluation(&[ + ("OutputType", "Exe"), + ("BaseOutputPath", "bin\\"), + ("TargetFramework", "net8.0"), + ("PublishDir", "bin\\Debug\\net8.0\\publish\\"), + ]); + let tasks = infer(&eval, &InferTasksSetting::default(), &[]); + + assert_eq!( + tasks.keys().map(|id| id.as_str()).collect::>(), + vec!["build", "publish", "run"] + ); + + let run = &tasks[&Id::raw("run")]; + assert_eq!(command_line(run), "dotnet run"); + let run_options = run.options.as_ref().unwrap(); + assert_eq!(run_options.cache, Some(TaskOptionCache::Enabled(false))); + assert_eq!( + run_options.run_in_ci, + Some(TaskOptionRunInCI::Enabled(false)) + ); + + let publish = &tasks[&Id::raw("publish")]; + assert_eq!( + command_line(publish), + "dotnet publish --no-build --no-restore" + ); + assert_eq!( + publish.outputs.as_ref().unwrap(), + &vec![Output::parse("bin/Debug/net8.0/publish").unwrap()] + ); + } + + #[test] + fn test_project_gets_build_test_never_run() { + // Modern test SDKs can flip OutputType to Exe — test wins over run. + let mut eval = evaluation(&[("OutputType", "Exe"), ("TargetFramework", "net8.0")]); + eval.items.insert( + "PackageReference".into(), + vec![serde_json::json!({ "Identity": "Microsoft.NET.Test.Sdk", "Version": "17.10.0" })], + ); + + let tasks = infer(&eval, &InferTasksSetting::default(), &[]); + + assert!(tasks.contains_key(&Id::raw("build"))); + assert!(tasks.contains_key(&Id::raw("test"))); + assert!(!tasks.contains_key(&Id::raw("run"))); + assert!(!tasks.contains_key(&Id::raw("publish"))); + + let test = &tasks[&Id::raw("test")]; + assert_eq!(command_line(test), "dotnet test --no-build --no-restore"); + } + + #[test] + fn pins_evaluated_configuration_on_cacheable_commands() { + // `dotnet publish` defaults to Release (.NET 8+) while `dotnet build` + // defaults to Debug — the explicit `-c` keeps `--no-build` coherent. + let mut eval = evaluation(&[ + ("OutputType", "Exe"), + ("TargetFramework", "net8.0"), + ("Configuration", "Debug"), + ("PublishDir", "bin\\Debug\\net8.0\\publish\\"), + ]); + eval.items.insert( + "PackageReference".into(), + vec![serde_json::json!({ "Identity": "Microsoft.NET.Test.Sdk" })], + ); + + let tasks = infer(&eval, &InferTasksSetting::default(), &[]); + + assert_eq!( + command_line(&tasks[&Id::raw("build")]), + "dotnet build --no-restore --no-dependencies -c Debug" + ); + assert_eq!( + command_line(&tasks[&Id::raw("test")]), + "dotnet test --no-build --no-restore -c Debug" + ); + } + + #[test] + fn multi_tfm_exe_skips_publish() { + let eval = evaluation(&[("OutputType", "Exe"), ("TargetFrameworks", "net8.0;net9.0")]); + let tasks = infer(&eval, &InferTasksSetting::default(), &[]); + + assert!(tasks.contains_key(&Id::raw("run"))); + assert!(!tasks.contains_key(&Id::raw("publish"))); + } + + #[test] + fn unknown_outputs_disable_caching_instead_of_guessing() { + // BaseOutputPath redirected outside the workspace entirely. + let eval = evaluation(&[ + ("OutputType", "Library"), + ("BaseOutputPath", "D:\\global-outputs\\app\\"), + ]); + let tasks = infer(&eval, &InferTasksSetting::default(), &[]); + + let build = &tasks[&Id::raw("build")]; + assert!(build.outputs.is_none()); + assert_eq!( + build.options.as_ref().unwrap().cache, + Some(TaskOptionCache::Enabled(false)) + ); + } + + #[test] + fn granular_selection_and_reserved_ids_are_respected() { + let eval = evaluation(&[ + ("OutputType", "Exe"), + ("BaseOutputPath", "bin\\"), + ("TargetFramework", "net8.0"), + ("PublishDir", "bin\\Debug\\net8.0\\publish\\"), + ]); + + let only = InferTasksSetting::Only(vec!["run".into(), "publish".into()]); + let tasks = infer(&eval, &only, &[]); + assert_eq!( + tasks.keys().map(|id| id.as_str()).collect::>(), + vec!["publish", "run"], + "granular selection" + ); + + let tasks = infer(&eval, &InferTasksSetting::default(), &["run", "build"]); + assert_eq!( + tasks.keys().map(|id| id.as_str()).collect::>(), + vec!["publish"], + "reserved (inherited) ids skipped" + ); + + let tasks = infer(&eval, &InferTasksSetting::Enabled(false), &[]); + assert!(tasks.is_empty()); + } + + #[test] + fn detects_test_projects_that_have_no_microsoft_net_test_sdk() { + // Microsoft.Testing.Platform test projects replace Microsoft.NET.Test.Sdk + // outright. Shapes taken from real repositories: dotnet/eShop uses + // ``, which sets the property and references no + // test package; OrchardCMS/OrchardCore uses `xunit.v3.mtp-v2` with + // neither property set on an unrestored tree. + let by_property = evaluation(&[ + ("OutputType", "Exe"), + ("TargetFramework", "net10.0"), + ("IsTestingPlatformApplication", "true"), + ]); + + for (label, eval) in [ + ("MSTest.Sdk property", by_property), + ("xunit.v3 package", exe_with_packages(&["xunit.v3.mtp-v2"])), + ( + "platform package", + exe_with_packages(&["Microsoft.Testing.Platform.MSBuild"]), + ), + ] { + let tasks = infer(&eval, &InferTasksSetting::default(), &[]); + let ids = tasks.keys().map(|id| id.as_str()).collect::>(); + + assert!( + ids.contains(&"test"), + "{label}: expected a test task, got {ids:?}" + ); + // A test project is not an application, so it gets neither of these. + assert!(!ids.contains(&"run"), "{label}: {ids:?}"); + assert!(!ids.contains(&"publish"), "{label}: {ids:?}"); + } + } + + #[test] + fn does_not_mistake_test_helper_packages_for_a_test_project() { + // All three appear in real test-adjacent projects. Matching "test" as a + // substring would wrongly flag every one of them, and a BenchmarkDotNet + // project explicitly sets the properties to `false`. + let eval = exe_with_packages(&[ + "Microsoft.AspNetCore.Mvc.Testing", + "Microsoft.AspNetCore.TestHost", + "BenchmarkDotNet", + ]); + + let tasks = infer(&eval, &InferTasksSetting::default(), &[]); + let ids = tasks.keys().map(|id| id.as_str()).collect::>(); + + assert!(!ids.contains(&"test"), "{ids:?}"); + assert!( + ids.contains(&"run"), + "an executable must still get run: {ids:?}" + ); + + let benchmarks = evaluation(&[ + ("OutputType", "Exe"), + ("TargetFramework", "net10.0"), + ("IsTestProject", "false"), + ("IsTestingPlatformApplication", "false"), + ]); + let tasks = infer(&benchmarks, &InferTasksSetting::default(), &[]); + + assert!(!tasks.keys().any(|id| id.as_str() == "test")); + } + + #[test] + fn the_self_build_dep_is_optional() { + // moon defaults `~:` deps to mandatory, so selecting only `test` or + // only `publish` — no `build` task to depend on — would fail + // project-graph construction with `UnknownDepTarget` if these were + // plain targets. + // A project is either a test project or an executable — `is_exe` + // excludes `is_test` — so each dep needs its own evaluation. + let cases = [ + ("test", evaluation(&[("IsTestProject", "true")])), + ( + "publish", + evaluation(&[ + ("OutputType", "Exe"), + ("TargetFramework", "net8.0"), + ("PublishDir", "bin\\Debug\\net8.0\\publish\\"), + ]), + ), + ]; + + for (id, eval) in cases { + let tasks = infer(&eval, &InferTasksSetting::Only(vec![id.into()]), &[]); + + assert_eq!( + tasks[&Id::raw(id)].deps.as_deref(), + Some( + &[PartialTaskDependency::Object(PartialTaskDependencyConfig { + target: Some(Target::parse("~:build").unwrap()), + optional: Some(true), + ..Default::default() + })][..] + ), + "`{id}` must depend on an optional `~:build`" + ); + } + } + + #[test] + fn multiple_project_files_get_explicit_targets() { + let eval = evaluation(&[ + ("OutputType", "Exe"), + ("BaseOutputPath", "bin\\"), + ("TargetFramework", "net8.0"), + ]); + + let tasks = infer_tasks( + &InferTasksSetting::default(), + &BTreeSet::new(), + &InferInputs { + evaluation: &eval, + explicit_project_file: Some("App.csproj"), + project_dir: "/repo/app", + workspace_dir: "/repo", + test_platform_runner: false, + }, + ) + .unwrap(); + + assert_eq!( + command_line(&tasks[&Id::raw("build")]), + "dotnet build App.csproj --no-restore --no-dependencies" + ); + assert_eq!( + command_line(&tasks[&Id::raw("run")]), + "dotnet run --project App.csproj" + ); + } + + #[test] + fn test_platform_takes_the_project_through_a_flag() { + let eval = test_project_evaluation(); + + let infer_with = |runner: bool, file: Option<&str>| { + let tasks = infer_tasks( + &InferTasksSetting::default(), + &BTreeSet::new(), + &InferInputs { + evaluation: &eval, + explicit_project_file: file, + project_dir: "/repo/app-tests", + workspace_dir: "/repo", + test_platform_runner: runner, + }, + ) + .unwrap(); + + command_line(&tasks[&Id::raw("test")]) + }; + + // MTP rejects a positional project path... + assert_eq!( + infer_with(true, Some("App.Tests.csproj")), + "dotnet test --project App.Tests.csproj --no-build --no-restore" + ); + // ...while classic VSTest mode rejects `--project`. + assert_eq!( + infer_with(false, Some("App.Tests.csproj")), + "dotnet test App.Tests.csproj --no-build --no-restore" + ); + // With one project file in the directory neither flavour applies: + // the command runs in the project directory with no path at all. + assert_eq!( + infer_with(true, None), + "dotnet test --no-build --no-restore" + ); + assert_eq!( + infer_with(false, None), + "dotnet test --no-build --no-restore" + ); + } + + #[test] + fn project_level_test_platform_opt_in_is_honored() { + // A project can select MTP on its own, without a global.json. + let mut eval = test_project_evaluation(); + eval.properties + .insert("TestingPlatformDotnetTestSupport".into(), "true".into()); + + let tasks = infer_tasks( + &InferTasksSetting::default(), + &BTreeSet::new(), + &InferInputs { + evaluation: &eval, + explicit_project_file: Some("App.Tests.csproj"), + project_dir: "/repo/app-tests", + workspace_dir: "/repo", + test_platform_runner: false, + }, + ) + .unwrap(); + + assert_eq!( + command_line(&tasks[&Id::raw("test")]), + "dotnet test --project App.Tests.csproj --no-build --no-restore" + ); + } + + #[test] + fn reports_only_conflicts_that_actually_suppress_inference() { + let reserved: BTreeMap = [ + ("build", "/workspace/.moon/tasks/dotnet.yml"), + ("publish", "/workspace/.moon/tasks.yml"), + // Not inferable, so its presence is unremarkable. + ("lint", "/workspace/.moon/tasks/all.yml"), + ] + .iter() + .map(|(id, file)| (id.to_string(), file.to_string())) + .collect(); + + assert_eq!( + reportable_conflicts(&reserved, &InferTasksSetting::default()), + vec![ + ("build", "/workspace/.moon/tasks/dotnet.yml"), + ("publish", "/workspace/.moon/tasks.yml"), + ] + ); + + // A task the user did not ask us to infer is not a conflict. + assert_eq!( + reportable_conflicts(&reserved, &InferTasksSetting::Only(vec!["publish".into()])), + vec![("publish", "/workspace/.moon/tasks.yml")] + ); + + assert!(reportable_conflicts(&reserved, &InferTasksSetting::Enabled(false)).is_empty()); + assert!(reportable_conflicts(&BTreeMap::new(), &InferTasksSetting::default()).is_empty()); + } + + #[test] + fn resolves_output_paths_in_every_form() { + // Relative stays relative. + assert_eq!( + resolve_output_path("bin\\", "C:\\repo\\app", "C:\\repo"), + Some("bin".into()) + ); + // Absolute under the project dir, case-insensitive. + assert_eq!( + resolve_output_path("C:\\Repo\\App\\bin\\Debug\\", "c:\\repo\\app", "c:\\repo"), + Some("bin/Debug".into()) + ); + // Absolute under the workspace (artifacts layout) => workspace-relative. + assert_eq!( + resolve_output_path( + "C:\\repo\\artifacts\\bin\\app\\", + "C:\\repo\\app", + "C:\\repo" + ), + Some("/artifacts/bin/app".into()) + ); + // Unix forms. + assert_eq!( + resolve_output_path("/repo/app/bin", "/repo/app", "/repo"), + Some("bin".into()) + ); + // Outside the workspace => not resolvable. + assert_eq!( + resolve_output_path("D:\\elsewhere\\bin", "C:\\repo\\app", "C:\\repo"), + None + ); + // Empty => not resolvable. + assert_eq!(resolve_output_path("", "C:\\repo\\app", "C:\\repo"), None); + // Prefix must respect component boundaries. + assert_eq!( + resolve_output_path("/repo/app-other/bin", "/repo/app", "/repo"), + Some("/app-other/bin".into()) + ); + } +} diff --git a/toolchains/dotnet/tests/msbuild_batch_test.rs b/toolchains/dotnet/tests/msbuild_batch_test.rs new file mode 100644 index 00000000..460ade2b --- /dev/null +++ b/toolchains/dotnet/tests/msbuild_batch_test.rs @@ -0,0 +1,154 @@ +//! Requires a .NET SDK (8+) on `PATH`: these tests spawn a real +//! `dotnet msbuild` outside the wasm plugin, which is the point of the file — +//! the plugin's per-project fallback would silently mask a broken batch in the +//! sandbox tests. `-getItem` JSON output is what needs SDK 8+. + +use dotnet_toolchain::msbuild::{ + MsbuildEvaluation, detect_failed_projects, moon_eval_targets_xml, normalize_path_key, + parse_batch_output, traversal_project_xml, +}; +use starbase_sandbox::create_empty_sandbox; +use std::path::{Path, PathBuf}; +use std::process::{Command, Output}; + +mod msbuild { + use super::*; + + fn fixtures() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/__fixtures__/projects") + } + + /// Stage the generated traversal project alongside the injected targets file + /// and evaluate it, exactly as `evaluate_projects_batch` does. + fn run_batch(scratch: &Path, projects: &[String]) -> Output { + std::fs::write(scratch.join("moon-eval.targets"), moon_eval_targets_xml()).unwrap(); + std::fs::write( + scratch.join("traversal.proj"), + traversal_project_xml(projects), + ) + .unwrap(); + + Command::new("dotnet") + .args([ + "msbuild", + scratch.join("traversal.proj").to_str().unwrap(), + "-nologo", + "-maxCpuCount", + "-nodeReuse:false", + "-t:MoonCollect", + "-getItem:MoonEval", + ]) + .output() + .expect("failed to spawn `dotnet msbuild`") + } + + /// End-to-end validation of the batched evaluation mechanism against a real + /// MSBuild. + #[test] + fn batched_traversal_evaluates_fixture_projects() { + let sandbox = create_empty_sandbox(); + + let projects = [ + ["app", "App.csproj"], + ["lib", "Lib.csproj"], + ["core", "Core.csproj"], + ["app-tests", "App.Tests.csproj"], + ] + .iter() + .map(|[dir, file]| { + fixtures() + .join(dir) + .join(file) + .to_string_lossy() + .to_string() + }) + .collect::>(); + + let output = run_batch(sandbox.path(), &projects); + let stdout = String::from_utf8_lossy(&output.stdout); + + assert!( + output.status.success(), + "batch invocation failed ({:?}):\n{stdout}\n{}", + output.status, + String::from_utf8_lossy(&output.stderr), + ); + + let results = parse_batch_output(&stdout).unwrap(); + + let get = |index: usize| -> &MsbuildEvaluation { + let key = normalize_path_key(&projects[index]); + + results + .get(&key) + .unwrap_or_else(|| panic!("missing {key} in batch output: {:?}", results.keys())) + }; + + // app -> lib, and its Exe OutputType survives the round-trip. + let app = get(0); + let app_refs = app.project_reference_paths(); + assert_eq!(app_refs.len(), 1); + assert!(normalize_path_key(&app_refs[0]).ends_with("lib/lib.csproj")); + assert_eq!(app.property("OutputType"), "Exe"); + + // lib -> core. + let lib_refs = get(1).project_reference_paths(); + assert_eq!(lib_refs.len(), 1); + assert!(normalize_path_key(&lib_refs[0]).ends_with("core/core.csproj")); + + // core has no references at all. + let core = get(2); + assert!(core.project_reference_paths().is_empty()); + assert!(core.package_references().is_empty()); + + // app-tests -> app, with its evaluated package set intact. + let tests = get(3); + let tests_refs = tests.project_reference_paths(); + assert!(normalize_path_key(&tests_refs[0]).ends_with("app/app.csproj")); + + let packages = tests.package_references(); + assert_eq!(packages.get("Microsoft.NET.Test.Sdk").unwrap(), "17.10.0"); + assert_eq!(packages.get("xunit").unwrap(), "2.8.0"); + } + + /// Documents the MSBuild behavior the retry logic in + /// `evaluate_projects_batch` exists for: one unloadable project makes the + /// whole batch return exit != 0 with ZERO target outputs (`ContinueOnError` + /// does not rescue load errors) — and validates that + /// `detect_failed_projects` identifies exactly the offender from the real + /// error output, so the retry can exclude it. + #[test] + fn broken_project_aborts_batch_and_is_detectable() { + let sandbox = create_empty_sandbox(); + sandbox.create_file( + "broken/Broken.csproj", + " the workspace root. + assert_eq!( + common_source_prefix(&["src/backend/App", "tools/Generator"]), + "" + ); + + // A single project yields its own directory; separators normalize. + assert_eq!(common_source_prefix(&["apps\\api"]), "apps/api"); + assert_eq!(common_source_prefix(&["."]), ""); + assert_eq!(common_source_prefix(&[]), ""); + // No accidental partial-component matches. + assert_eq!(common_source_prefix(&["src/app", "src/app-other"]), "src"); + } + + #[test] + fn escapes_msbuild_includes() { + assert_eq!( + escape_msbuild_include("C:\\repo\\A & B\\$(odd)@*?;\"100%\".csproj"), + "C:\\repo\\A & B\\%24(odd)%40%2A%3F%3B<x>"100%25".csproj" + ); + } + + #[test] + fn targets_xml_covers_all_eval_properties() { + let xml = moon_eval_targets_xml(); + + for prop in EVAL_PROPERTIES.split(',') { + assert!( + xml.contains(&format!("<{prop}>$({prop})")), + "{prop}" + ); + } + + assert!(xml.contains("MoonProjectRefs")); + assert!(xml.contains("MoonPackageRefs")); + } + + #[test] + fn traversal_xml_lists_projects_and_injects_both_hooks() { + let xml = traversal_project_xml(&[ + "C:\\repo\\a\\a.csproj".to_string(), + "/home/x/b & c/b.csproj".to_string(), + ]); + + assert!(xml.contains("Include=\"C:\\repo\\a\\a.csproj\"")); + assert!(xml.contains("Include=\"/home/x/b & c/b.csproj\"")); + // Both hooks: plain SDK projects import CustomAfterMicrosoftCommonTargets, + // multi-TFM outer builds import the CrossTargeting variant instead. + assert!(xml.contains( + "CustomAfterMicrosoftCommonTargets=$(MSBuildThisFileDirectory)moon-eval.targets" + )); + assert!(xml.contains("CustomAfterMicrosoftCommonCrossTargetingTargets=$(MSBuildThisFileDirectory)moon-eval.targets")); + assert!(xml.contains("BuildInParallel=\"true\"")); + assert!(xml.contains("ContinueOnError=\"WarnAndContinue\"")); + } + + #[test] + fn recognizes_sdk_resolution_failures() { + // Real host output (abridged) from a workspace pinning an SDK that + // is not installed. + let output = "\ +5.0.100 [C:\\Program Files\\dotnet\\sdk] + A compatible .NET SDK was not found. + +Requested SDK version: 10.0.301 +global.json file: C:\\repo\\src\\backend\\global.json + +Learn about SDK resolution: +https://aka.ms/dotnet/sdk-not-found"; + + assert!(is_sdk_resolution_failure(output)); + // The URL alone is enough, so localized message text still matches. + assert!(is_sdk_resolution_failure( + "irgendein Fehler\nhttps://aka.ms/dotnet/sdk-not-found" + )); + + // A broken project is a different failure class and must not be + // reported as a missing SDK. + assert!(!is_sdk_resolution_failure( + "C:\\repo\\app\\App.csproj(1,41): error MSB4025: The project file could not be loaded." + )); + assert!(!is_sdk_resolution_failure( + "error MSB1009: Project file does not exist." + )); + } + + #[test] + fn detects_failed_projects_across_short_and_long_path_forms() { + // Real shape from GitHub's windows-latest runners: we pass a path + // with an 8.3 short-name prefix (from %TEMP%), MSBuild's error line + // prints the expanded long form. + let output = "C:\\Users\\runneradmin\\AppData\\Local\\Temp\\scratch\\broken\\Broken.csproj(1,41): error MSB4025: The project file could not be loaded."; + + let paths = vec![ + "C:\\Users\\RUNNER~1\\AppData\\Local\\Temp\\scratch\\broken/Broken.csproj".to_string(), + "C:\\Users\\RUNNER~1\\AppData\\Local\\Temp\\scratch\\ok\\Ok.csproj".to_string(), + ]; + + assert_eq!( + detect_failed_projects(output, &paths), + vec![paths[0].clone()] + ); + assert!(detect_failed_projects("no errors here", &paths).is_empty()); + } + + #[test] + fn detects_failed_projects_without_a_line_and_column() { + // Verbatim shape from SDK 10.0.201 for an unresolvable SDK reference: + // no line/column, and no error code either. Matching only the + // `(line,col):` form left the batch with no offender to exclude, + // so the retry never happened. + let output = "C:\\ws\\bad\\BadSdk.csproj : error : Could not resolve SDK \"Totally.Bogus.Sdk\". Exactly one of the probing messages below indicates why we could not resolve the SDK."; + + let paths = vec![ + "C:\\ws\\bad\\BadSdk.csproj".to_string(), + "C:\\ws\\ok\\Ok.csproj".to_string(), + ]; + + assert_eq!( + detect_failed_projects(output, &paths), + vec![paths[0].clone()] + ); + } + + #[test] + fn normalizes_path_keys() { + assert_eq!( + normalize_path_key("C:\\Abs\\Path\\LibA\\LibA.csproj"), + "c:/abs/path/liba/liba.csproj" + ); + assert_eq!( + normalize_path_key("/home/x/App.csproj"), + "/home/x/app.csproj" + ); + } + + #[test] + fn renders_msbuild_properties_as_p_args_in_deterministic_order() { + use std::collections::BTreeMap; + + assert!(msbuild_property_args(&BTreeMap::new()).is_empty()); + + let args = msbuild_property_args(&BTreeMap::from([ + ("SkipApiClientGen".to_owned(), "true".to_owned()), + ("Configuration".to_owned(), "Release".to_owned()), + ])); + + // BTreeMap iteration is sorted, so the rendered order is stable across + // runs — evaluation commands must not differ between otherwise + // identical invocations. + assert_eq!( + args, + vec!["-p:Configuration=Release", "-p:SkipApiClientGen=true"] + ); + } +} diff --git a/toolchains/dotnet/tests/tier1_test.rs b/toolchains/dotnet/tests/tier1_test.rs new file mode 100644 index 00000000..1357704f --- /dev/null +++ b/toolchains/dotnet/tests/tier1_test.rs @@ -0,0 +1,147 @@ +use moon_pdk_api::*; +use moon_pdk_test_utils::create_empty_moon_sandbox; +use serde_json::json; +use std::path::PathBuf; + +mod dotnet_toolchain_tier1 { + use super::*; + + mod register_toolchain { + use super::*; + + #[tokio::test(flavor = "multi_thread")] + async fn registers_metadata() { + let sandbox = create_empty_moon_sandbox(); + let plugin = sandbox.create_toolchain("dotnet").await; + + let output = plugin + .register_toolchain(RegisterToolchainInput { + id: Id::raw("dotnet"), + }) + .await; + + assert_eq!(output.name, ".NET"); + assert_eq!(output.exe_names, vec!["dotnet".to_string()]); + assert_eq!( + output.lock_file_names, + vec!["packages.lock.json".to_string()] + ); + assert_eq!( + output.manifest_file_names, + vec!["Directory.Packages.props".to_string()] + ); + assert!(output.vendor_dir_name.is_none()); + assert!( + output + .config_file_globs + .contains(&"*.{csproj,fsproj,vbproj}".to_string()) + ); + assert!( + output + .config_file_globs + .contains(&"Directory.Build.targets".to_string()) + ); + assert!( + output + .config_file_globs + .contains(&"{nuget,NuGet}.{config,Config}".to_string()) + ); + assert!( + output + .config_file_globs + .contains(&"packages.*.lock.json".to_string()) + ); + } + } + + mod define_docker_metadata { + use super::*; + + #[tokio::test(flavor = "multi_thread")] + async fn docker_metadata_defaults() { + let sandbox = create_empty_moon_sandbox(); + let plugin = sandbox.create_toolchain("dotnet").await; + + let output = plugin + .define_docker_metadata(DefineDockerMetadataInput { + toolchain_config: json!({}), + ..Default::default() + }) + .await; + + assert_eq!( + output.default_image.unwrap(), + "mcr.microsoft.com/dotnet/sdk:latest" + ); + assert!( + output + .scaffold_globs + .contains(&"**/*.{csproj,fsproj,vbproj}".to_string()) + ); + assert!( + output + .scaffold_globs + .contains(&"**/packages.lock.json".to_string()) + ); + assert!(output.scaffold_globs.contains(&"**/*.targets".to_string())); + assert!( + output + .scaffold_globs + .contains(&"**/packages.*.lock.json".to_string()) + ); + } + } + + mod prune_docker { + use super::*; + + #[tokio::test(flavor = "multi_thread")] + async fn removes_bin_and_obj_dirs() { + let sandbox = create_empty_moon_sandbox(); + sandbox.create_file("app/bin/Debug/x.dll", ""); + sandbox.create_file("app/obj/project.assets.json", ""); + sandbox.create_file("app/keep.cs", ""); + + let plugin = sandbox.create_toolchain("dotnet").await; + + let output = plugin + .prune_docker(PruneDockerInput { + projects: vec![moon_pdk_api::ProjectFragment { + id: Id::raw("app"), + source: "app".into(), + ..Default::default() + }], + root: VirtualPath::Real(sandbox.path().into()), + ..Default::default() + }) + .await; + + assert!(!sandbox.path().join("app/bin").exists()); + assert!(!sandbox.path().join("app/obj").exists()); + assert!(sandbox.path().join("app/keep.cs").exists()); + + assert_eq!( + output.changed_files, + vec![ + PathBuf::from("/workspace/app/bin"), + PathBuf::from("/workspace/app/obj"), + ] + ); + } + + #[tokio::test(flavor = "multi_thread")] + async fn does_nothing_without_bin_obj() { + let sandbox = create_empty_moon_sandbox(); + let plugin = sandbox.create_toolchain("dotnet").await; + + let output = plugin + .prune_docker(PruneDockerInput { + root: VirtualPath::Real(sandbox.path().into()), + ..Default::default() + }) + .await; + + assert!(output.changed_files.is_empty()); + } + } +} diff --git a/toolchains/dotnet/tests/tier2_test.rs b/toolchains/dotnet/tests/tier2_test.rs new file mode 100644 index 00000000..b74a6776 --- /dev/null +++ b/toolchains/dotnet/tests/tier2_test.rs @@ -0,0 +1,1459 @@ +//! Most of this file requires a .NET SDK (8+) on `PATH`. +//! +//! `exec_command` is not mocked in the plugin sandbox — warpgate's host function +//! spawns a real process — so `extend_project_graph`, `parse_manifest` and +//! `hash_task_contents` all shell out to `dotnet msbuild` and evaluate the +//! fixtures for real. `-getProperty`/`-getItem` JSON output is what needs SDK 8+. +//! Without one, those tests fail rather than skip. +//! +//! `locate_dependencies_root`, `install_dependencies`, `parse_lock` and +//! `extend_task_command` are pure and need no SDK. + +use moon_config::DependencyScope; +use moon_pdk_api::*; +use moon_pdk_test_utils::{create_empty_moon_sandbox, create_moon_sandbox}; +use serde_json::json; +use std::path::PathBuf; + +mod dotnet_toolchain_tier2 { + use super::*; + + mod locate_dependencies_root { + use super::*; + + #[tokio::test(flavor = "multi_thread")] + async fn finds_solution_root_from_nested_dir() { + let sandbox = create_moon_sandbox("locate"); + let plugin = sandbox.create_toolchain("dotnet").await; + + let output = plugin + .locate_dependencies_root(LocateDependenciesRootInput { + starting_dir: VirtualPath::Real(sandbox.path().join("nested/proj")), + ..Default::default() + }) + .await; + + assert_eq!(output.root.unwrap(), PathBuf::from("/workspace")); + assert!(output.members.is_none()); + } + + #[tokio::test(flavor = "multi_thread")] + async fn falls_back_to_project_file_dir_without_solution() { + let sandbox = create_moon_sandbox("locate-no-sln"); + let plugin = sandbox.create_toolchain("dotnet").await; + + let output = plugin + .locate_dependencies_root(LocateDependenciesRootInput { + starting_dir: VirtualPath::Real(sandbox.path().join("proj")), + ..Default::default() + }) + .await; + + assert_eq!(output.root.unwrap(), PathBuf::from("/workspace/proj")); + } + + #[tokio::test(flavor = "multi_thread")] + async fn finds_slnx_root_from_nested_dir() { + let sandbox = create_empty_moon_sandbox(); + // .slnx is a marker only — content is never parsed. + sandbox.create_file("App.slnx", "\n\n"); + sandbox.create_file( + "nested/proj/Proj.csproj", + "", + ); + + let plugin = sandbox.create_toolchain("dotnet").await; + + let output = plugin + .locate_dependencies_root(LocateDependenciesRootInput { + starting_dir: VirtualPath::Real(sandbox.path().join("nested/proj")), + ..Default::default() + }) + .await; + + assert_eq!(output.root.unwrap(), PathBuf::from("/workspace")); + assert!(output.members.is_none()); + } + + #[tokio::test(flavor = "multi_thread")] + async fn falls_back_to_alternate_lock_file_name() { + let sandbox = create_empty_moon_sandbox(); + sandbox.create_file( + "proj/packages.Proj.lock.json", + r#"{"version": 1, "dependencies": {}}"#, + ); + + let plugin = sandbox.create_toolchain("dotnet").await; + + let output = plugin + .locate_dependencies_root(LocateDependenciesRootInput { + starting_dir: VirtualPath::Real(sandbox.path().join("proj")), + ..Default::default() + }) + .await; + + assert_eq!(output.root.unwrap(), PathBuf::from("/workspace/proj")); + } + + #[tokio::test(flavor = "multi_thread")] + async fn returns_none_when_nothing_found() { + let sandbox = create_empty_moon_sandbox(); + sandbox.create_file("empty/dir/marker.txt", ""); + + let plugin = sandbox.create_toolchain("dotnet").await; + + let output = plugin + .locate_dependencies_root(LocateDependenciesRootInput { + starting_dir: VirtualPath::Real(sandbox.path().join("empty/dir")), + ..Default::default() + }) + .await; + + assert!(output.root.is_none()); + } + } + + mod extend_project_graph { + use super::*; + + fn projects_input() -> ExtendProjectGraphInput { + let mut input = ExtendProjectGraphInput::default(); + input.project_sources.insert(Id::raw("app"), "app".into()); + input.project_sources.insert(Id::raw("lib"), "lib".into()); + input.project_sources.insert(Id::raw("core"), "core".into()); + input + .project_sources + .insert(Id::raw("app-tests"), "app-tests".into()); + input + } + + #[tokio::test(flavor = "multi_thread")] + async fn maps_project_references_to_moon_deps() { + let sandbox = create_moon_sandbox("projects"); + let plugin = sandbox.create_toolchain("dotnet").await; + + let mut input = projects_input(); + input.toolchain_config = json!({ "inferDependencies": true, "inferTasks": false }); + + let output = plugin.extend_project_graph(input).await; + + let app = &output.extended_projects[&Id::raw("app")]; + assert_eq!(app.dependencies.len(), 1); + assert_eq!(app.dependencies[0].id, Id::raw("lib")); + assert_eq!(app.dependencies[0].scope, DependencyScope::Production); + + let lib = &output.extended_projects[&Id::raw("lib")]; + assert_eq!(lib.dependencies[0].id, Id::raw("core")); + + // core has no references, but still contributes its + // AssemblyName-derived alias. + let core = &output.extended_projects[&Id::raw("core")]; + assert!(core.dependencies.is_empty()); + assert_eq!(core.alias.as_deref(), Some("Core")); + + let tests = &output.extended_projects[&Id::raw("app-tests")]; + assert_eq!(tests.dependencies[0].id, Id::raw("app")); + + // One csproj per project, virtual-path form. + assert_eq!(output.input_files.len(), 4); + assert!( + output + .input_files + .contains(&PathBuf::from("/workspace/app/App.csproj")) + ); + } + + #[tokio::test(flavor = "multi_thread")] + async fn respects_infer_dependencies_off() { + let sandbox = create_moon_sandbox("projects"); + let plugin = sandbox.create_toolchain("dotnet").await; + + let mut input = projects_input(); + input.toolchain_config = json!({ "inferDependencies": false, "inferTasks": false }); + + let output = plugin.extend_project_graph(input).await; + + assert!(output.extended_projects.is_empty()); + } + + fn task_ids(project: &ExtendProjectOutput) -> Vec<&str> { + project.tasks.keys().map(|id| id.as_str()).collect() + } + + /// Inferred `test` command for the `mtp` fixture's `suite` project. + async fn mtp_test_command( + sandbox: &moon_pdk_test_utils::MoonWasmSandbox, + ) -> Option { + let plugin = sandbox.create_toolchain("dotnet").await; + + let mut input = ExtendProjectGraphInput::default(); + input + .project_sources + .insert(Id::raw("suite"), "suite".into()); + input.toolchain_config = json!({ "inferDependencies": false }); + + let output = plugin.extend_project_graph(input).await; + + output.extended_projects[&Id::raw("suite")].tasks[&Id::raw("test")] + .command + .clone() + } + + #[tokio::test(flavor = "multi_thread")] + async fn infers_tasks_by_default() { + let sandbox = create_moon_sandbox("projects"); + let plugin = sandbox.create_toolchain("dotnet").await; + + let mut input = projects_input(); + input.toolchain_config = json!({}); + + let output = plugin.extend_project_graph(input).await; + + // app is an Exe -> build + publish + run. + let app = &output.extended_projects[&Id::raw("app")]; + assert_eq!(task_ids(app), vec!["build", "publish", "run"]); + + let build = &app.tasks[&Id::raw("build")]; + assert_eq!( + build.command, + Some(moon_config::PartialTaskArgs::List(vec![ + "dotnet".into(), + "build".into(), + "--no-restore".into(), + "--no-dependencies".into(), + "-c".into(), + "Debug".into(), + ])) + ); + // Outputs came from the real evaluated BaseOutputPath. + assert_eq!( + build.outputs, + Some(vec![moon_config::Output::parse("bin").unwrap()]) + ); + assert!(build.deps.is_some(), "build depends on ^:build"); + + // run is never cached and never runs in CI. + let run_options = app.tasks[&Id::raw("run")].options.as_ref().unwrap(); + assert_eq!( + run_options.cache, + Some(moon_config::TaskOptionCache::Enabled(false)) + ); + + // app-tests references Microsoft.NET.Test.Sdk -> build + test. + let tests = &output.extended_projects[&Id::raw("app-tests")]; + assert_eq!(task_ids(tests), vec!["build", "test"]); + + // Plain classlibs still get a build task. + let core = &output.extended_projects[&Id::raw("core")]; + assert_eq!(task_ids(core), vec!["build"]); + } + + #[tokio::test(flavor = "multi_thread")] + async fn passes_the_project_through_a_flag_for_the_testing_platform() { + // The `mtp` fixture's `global.json` selects Microsoft.Testing.Platform, + // and the project directory holds two project files so the command + // has to name one — the case where the two runners' command lines + // are incompatible. + let sandbox = create_moon_sandbox("mtp"); + + assert_eq!( + mtp_test_command(&sandbox).await, + Some(moon_config::PartialTaskArgs::List(vec![ + "dotnet".into(), + "test".into(), + "--project".into(), + "Suite.Tests.csproj".into(), + "--no-build".into(), + "--no-restore".into(), + "-c".into(), + "Debug".into(), + ])) + ); + } + + #[tokio::test(flavor = "multi_thread")] + async fn passes_the_project_positionally_for_vstest() { + // Same fixture with the runner deselected: classic VSTest mode + // rejects `--project` and requires the positional form. + let sandbox = create_moon_sandbox("mtp"); + sandbox.create_file("global.json", "{}"); + + assert_eq!( + mtp_test_command(&sandbox).await, + Some(moon_config::PartialTaskArgs::List(vec![ + "dotnet".into(), + "test".into(), + "Suite.Tests.csproj".into(), + "--no-build".into(), + "--no-restore".into(), + "-c".into(), + "Debug".into(), + ])) + ); + } + + #[tokio::test(flavor = "multi_thread")] + async fn infers_only_listed_tasks() { + let sandbox = create_moon_sandbox("projects"); + let plugin = sandbox.create_toolchain("dotnet").await; + + let mut input = projects_input(); + input.toolchain_config = json!({ "inferDependencies": false, "inferTasks": ["test"] }); + + let output = plugin.extend_project_graph(input).await; + + // Only app-tests qualifies for a test task; nothing else + // contributes anything. + let tests = &output.extended_projects[&Id::raw("app-tests")]; + assert_eq!(task_ids(tests), vec!["test"]); + + // The others still appear, but only to contribute their + // AssemblyName alias — no tasks, and no deps with inference off. + for id in ["app", "core", "lib"] { + let project = &output.extended_projects[&Id::raw(id)]; + + assert!(project.tasks.is_empty(), "{id} tasks"); + assert!(project.dependencies.is_empty(), "{id} deps"); + assert!(project.alias.is_some(), "{id} alias"); + } + } + + #[tokio::test(flavor = "multi_thread")] + async fn inference_yields_to_inherited_task_files() { + let sandbox = create_moon_sandbox("projects"); + + // Applies to dotnet projects: suppresses inferred `build`. + sandbox.create_file( + ".moon/tasks/dotnet.yml", + "inheritedBy:\n toolchains: ['dotnet']\ntasks:\n build:\n command: 'dotnet build'\n", + ); + // Unscoped: assumed to apply -> suppresses inferred `publish`. + sandbox.create_file( + ".moon/tasks.yml", + "tasks:\n publish:\n command: 'echo deploy'\n", + ); + // Explicitly scoped to another toolchain: must NOT suppress `run`. + sandbox.create_file( + ".moon/tasks/node.yml", + "inheritedBy:\n toolchains: ['javascript']\ntasks:\n run:\n command: 'node server.js'\n", + ); + + let plugin = sandbox.create_toolchain("dotnet").await; + + let mut input = projects_input(); + input.toolchain_config = json!({ "inferDependencies": false }); + + let output = plugin.extend_project_graph(input).await; + + let app = &output.extended_projects[&Id::raw("app")]; + assert_eq!(task_ids(app), vec!["run"]); + + let tests = &output.extended_projects[&Id::raw("app-tests")]; + assert_eq!(task_ids(tests), vec!["test"]); + } + + #[tokio::test(flavor = "multi_thread")] + async fn infers_dependencies_across_languages() { + let sandbox = create_moon_sandbox("mixed-lang"); + let plugin = sandbox.create_toolchain("dotnet").await; + + let mut input = ExtendProjectGraphInput::default(); + input.project_sources.insert(Id::raw("app"), "app".into()); + input.project_sources.insert(Id::raw("lib"), "lib".into()); + input.project_sources.insert(Id::raw("core"), "core".into()); + input.toolchain_config = json!({ "inferDependencies": true }); + + let output = plugin.extend_project_graph(input).await; + + // C# -> F# -> VB project references all resolve; MSBuild + // evaluation is language-agnostic. + let app = &output.extended_projects[&Id::raw("app")]; + assert_eq!(app.dependencies[0].id, Id::raw("lib")); + + let lib = &output.extended_projects[&Id::raw("lib")]; + assert_eq!(lib.dependencies[0].id, Id::raw("core")); + + assert!( + output + .input_files + .contains(&PathBuf::from("/workspace/lib/Lib.fsproj")) + ); + assert!( + output + .input_files + .contains(&PathBuf::from("/workspace/core/Core.vbproj")) + ); + } + + #[tokio::test(flavor = "multi_thread")] + async fn emits_assembly_name_as_alias() { + let sandbox = create_moon_sandbox("matrix"); + let plugin = sandbox.create_toolchain("dotnet").await; + + let mut input = ExtendProjectGraphInput::default(); + input + .project_sources + .insert(Id::raw("deep"), "nested/deep".into()); + input.toolchain_config = json!({ "inferDependencies": true }); + + let output = plugin.extend_project_graph(input).await; + + // Explicit beats the file-name default. + let deep = &output.extended_projects[&Id::raw("deep")]; + assert_eq!(deep.alias.as_deref(), Some("MyCompany.Deep")); + } + + #[tokio::test(flavor = "multi_thread")] + async fn condition_gated_project_references_resolve() { + let sandbox = create_moon_sandbox("matrix"); + let plugin = sandbox.create_toolchain("dotnet").await; + + let mut input = ExtendProjectGraphInput::default(); + input + .project_sources + .insert(Id::raw("deep"), "nested/deep".into()); + input.project_sources.insert(Id::raw("cond"), "cond".into()); + input.toolchain_config = json!({ "inferDependencies": true }); + + let output = plugin.extend_project_graph(input).await; + + // The ProjectReference is gated on '$(EnableDeepRef)' == '1', + // set in the project itself — real evaluation resolves it. + let cond = &output.extended_projects[&Id::raw("cond")]; + assert_eq!(cond.dependencies[0].id, Id::raw("deep")); + } + + /// The counterpart to the test above: same fixture, same gated + /// reference, but evaluated under a `msbuildProperties` value that + /// makes the condition false. Asserts the setting actually reaches + /// MSBuild and changes the graph, rather than only that it renders + /// into `-p:` arguments. + #[tokio::test(flavor = "multi_thread")] + async fn msbuild_properties_are_applied_to_the_evaluation() { + let sandbox = create_moon_sandbox("matrix"); + let plugin = sandbox.create_toolchain("dotnet").await; + + let mut input = ExtendProjectGraphInput::default(); + input + .project_sources + .insert(Id::raw("deep"), "nested/deep".into()); + input.project_sources.insert(Id::raw("cond"), "cond".into()); + input.toolchain_config = json!({ + "inferDependencies": true, + // Cond.csproj declares 1. A + // command-line global property cannot be overridden by the + // project, so this wins and the gated reference drops out. + "msbuildProperties": { "EnableDeepRef": "0" }, + }); + + let output = plugin.extend_project_graph(input).await; + + let cond = &output.extended_projects[&Id::raw("cond")]; + assert!( + cond.dependencies.is_empty(), + "expected the gated reference to be excluded, got {:?}", + cond.dependencies + .iter() + .map(|dep| dep.id.as_str()) + .collect::>() + ); + } + + #[tokio::test(flavor = "multi_thread")] + async fn unsatisfiable_global_json_pin_fails_with_guidance() { + let sandbox = create_moon_sandbox("projects"); + // No such SDK exists, so the dotnet host refuses to run MSBuild + // at all — every project would fail identically. + sandbox.create_file( + "global.json", + r#"{"sdk":{"version":"99.0.100","rollForward":"disable"}}"#, + ); + + let plugin = sandbox.create_toolchain("dotnet").await; + + let mut input = projects_input(); + input.toolchain_config = json!({ "inferDependencies": true }); + input.context = plugin.create_context(); + + // The wrapper unwraps, so call through the plugin to inspect the + // error itself. + let error = plugin + .plugin + .call_func_with::<_, _, ExtendProjectGraphOutput>("extend_project_graph", input) + .await + .expect_err("an unsatisfiable SDK pin must fail the graph build") + .to_string(); + + // Names the pin, where it came from, and the ways out — instead + // of one cryptic host dump per project and an empty graph. + assert!(error.contains("99.0.100"), "{error}"); + assert!(error.contains("global.json"), "{error}"); + assert!(error.contains("version"), "{error}"); + assert!(error.contains("dotnetRoot"), "{error}"); + } + + #[tokio::test(flavor = "multi_thread")] + async fn unsatisfiable_pin_degrades_when_moon_installs_the_sdk() { + let sandbox = create_moon_sandbox("projects"); + sandbox.create_file( + "global.json", + r#"{"sdk":{"version":"99.0.100","rollForward":"disable"}}"#, + ); + + // Same unsatisfiable pin, but moon has been told to install an SDK. + // The project graph is built before the action pipeline runs, so + // failing here would deadlock the bootstrap this setting exists for. + sandbox.create_file(".moon/toolchains.yml", "dotnet:\n version: '8.0'\n"); + + let plugin = sandbox.create_toolchain("dotnet").await; + + let mut input = projects_input(); + input.toolchain_config = json!({ "inferDependencies": true }); + input.context = plugin.create_context(); + + let output = plugin + .plugin + .call_func_with::<_, _, ExtendProjectGraphOutput>("extend_project_graph", input) + .await + .expect("a pending SDK install must not fail the graph build"); + + assert!(output.extended_projects.is_empty()); + } + + #[tokio::test(flavor = "multi_thread")] + async fn broken_project_does_not_abort_graph() { + let sandbox = create_moon_sandbox("projects"); + sandbox.create_file( + "core/Core.csproj", + " same key; different content -> different key. + let repeat = plugin + .setup_environment(SetupEnvironmentInput { + root: VirtualPath::Real(sandbox.path().into()), + toolchain_config: json!({}), + ..Default::default() + }) + .await; + + assert_eq!(repeat.commands[0].cache.as_deref(), Some(cache_key)); + + sandbox.create_file( + ".config/dotnet-tools.json", + r#"{"version": 1, "isRoot": true, "tools": {"dotnetsay": {"version": "2.1.7", "commands": ["dotnetsay"]}}}"#, + ); + + let edited = plugin + .setup_environment(SetupEnvironmentInput { + root: VirtualPath::Real(sandbox.path().into()), + toolchain_config: json!({}), + ..Default::default() + }) + .await; + + assert_ne!(edited.commands[0].cache.as_deref(), Some(cache_key)); + } + + #[tokio::test(flavor = "multi_thread")] + async fn finds_tool_manifest_above_the_dependencies_root() { + let sandbox = create_moon_sandbox("projects"); + // Tool manifests conventionally live at the repository root, but + // any project directory with a lock file is its own dependencies + // root — so the lookup walks upward like the dotnet CLI does. + sandbox.create_file( + ".config/dotnet-tools.json", + r#"{"version": 1, "isRoot": true, "tools": {}}"#, + ); + + let plugin = sandbox.create_toolchain("dotnet").await; + + let output = plugin + .setup_environment(SetupEnvironmentInput { + root: VirtualPath::Real(sandbox.path().join("app")), + toolchain_config: json!({}), + ..Default::default() + }) + .await; + + assert_eq!(output.commands.len(), 1); + assert_eq!( + output.commands[0].command.args, + vec!["tool".to_string(), "restore".to_string()] + ); + } + } + + mod hash_task_contents { + use super::*; + + fn fragment(id: &str, source: &str) -> moon_pdk_api::ProjectFragment { + moon_pdk_api::ProjectFragment { + id: Id::raw(id), + source: source.into(), + toolchains: vec![Id::raw("dotnet")], + ..Default::default() + } + } + + #[tokio::test(flavor = "multi_thread")] + async fn lockfile_branch_includes_raw_lock_text() { + let sandbox = create_moon_sandbox("locked"); + let plugin = sandbox.create_toolchain("dotnet").await; + + let output = plugin + .hash_task_contents(HashTaskContentsInput { + project: fragment("proj", "proj"), + toolchain_config: json!({}), + ..Default::default() + }) + .await; + + assert_eq!(output.contents.len(), 1); + let lockfiles = output.contents[0]["lockfiles"].as_object().unwrap(); + let lock_text = lockfiles["/workspace/proj/packages.lock.json"] + .as_str() + .unwrap(); + assert!(lock_text.contains("Newtonsoft.Json")); + assert!(lock_text.contains("contentHash")); + } + + #[tokio::test(flavor = "multi_thread")] + async fn lockfile_branch_still_hashes_config_files() { + let sandbox = create_moon_sandbox("locked"); + // Even with the package set pinned by the lock file, props/targets + // change build behavior and must contribute to the hash. + sandbox.create_file( + "Directory.Build.props", + "12", + ); + + let plugin = sandbox.create_toolchain("dotnet").await; + + let output = plugin + .hash_task_contents(HashTaskContentsInput { + project: fragment("proj", "proj"), + toolchain_config: json!({}), + ..Default::default() + }) + .await; + + let contents = &output.contents[0]; + assert!(contents["lockfiles"].is_object()); + let configs = contents["configs"].as_object().unwrap(); + assert!( + configs["/workspace/Directory.Build.props"] + .as_str() + .unwrap() + .contains("LangVersion") + ); + } + + #[tokio::test(flavor = "multi_thread")] + async fn alternate_lock_file_name_takes_lock_branch() { + let sandbox = create_moon_sandbox("projects"); + // `packages..lock.json` via NuGetLockFilePath. + sandbox.create_file( + "app/packages.App.lock.json", + r#"{"version": 1, "dependencies": {}}"#, + ); + + let plugin = sandbox.create_toolchain("dotnet").await; + + let output = plugin + .hash_task_contents(HashTaskContentsInput { + project: fragment("app", "app"), + toolchain_config: json!({}), + ..Default::default() + }) + .await; + + let contents = &output.contents[0]; + let lockfiles = contents["lockfiles"].as_object().unwrap(); + assert!(lockfiles.contains_key("/workspace/app/packages.App.lock.json")); + // Lock branch: no MSBuild evaluation happens. + assert!(contents.get("packages").is_none()); + } + + #[tokio::test(flavor = "multi_thread")] + async fn hashes_all_config_file_kinds() { + let sandbox = create_moon_sandbox("projects"); + // Valid-but-harmless contents: MSBuild auto-imports + // Directory.Build.targets and auto-applies Directory.Build.rsp, + // so garbage would break evaluation of the fixture projects. + sandbox.create_file("core/Directory.Build.targets", ""); + sandbox.create_file("Directory.Build.rsp", ""); + sandbox.create_file("NuGet.Config", ""); + sandbox.create_file("global.json", "{}"); + + let plugin = sandbox.create_toolchain("dotnet").await; + + let output = plugin + .hash_task_contents(HashTaskContentsInput { + project: fragment("core", "core"), + toolchain_config: json!({}), + ..Default::default() + }) + .await; + + let configs = output.contents[0]["configs"].as_object().unwrap(); + assert!(configs.contains_key("/workspace/core/Directory.Build.targets")); + assert!(configs.contains_key("/workspace/Directory.Build.rsp")); + // Actual (non-lowercase) file name is preserved in the key. + assert!(configs.contains_key("/workspace/NuGet.Config")); + assert!(configs.contains_key("/workspace/global.json")); + } + + #[tokio::test(flavor = "multi_thread")] + async fn evaluated_packages_branch_without_lockfile() { + let sandbox = create_moon_sandbox("projects"); + sandbox.create_file( + "Directory.Build.props", + "latest", + ); + + let plugin = sandbox.create_toolchain("dotnet").await; + + let output = plugin + .hash_task_contents(HashTaskContentsInput { + project: fragment("app-tests", "app-tests"), + toolchain_config: json!({}), + ..Default::default() + }) + .await; + + assert_eq!(output.contents.len(), 1); + let contents = &output.contents[0]; + + assert_eq!(contents["packages"]["xunit"].as_str().unwrap(), "2.8.0"); + assert_eq!( + contents["packages"]["Microsoft.NET.Test.Sdk"] + .as_str() + .unwrap(), + "17.10.0" + ); + + let configs = contents["configs"].as_object().unwrap(); + assert_eq!(configs.len(), 1); + assert!( + configs + .values() + .next() + .unwrap() + .as_str() + .unwrap() + .contains("LangVersion") + ); + } + + #[tokio::test(flavor = "multi_thread")] + async fn props_inheritance_chain_hashes_every_level() { + let sandbox = create_moon_sandbox("matrix"); + let plugin = sandbox.create_toolchain("dotnet").await; + + let output = plugin + .hash_task_contents(HashTaskContentsInput { + project: fragment("deep", "nested/deep"), + toolchain_config: json!({}), + ..Default::default() + }) + .await; + + let contents = &output.contents[0]; + let configs = contents["configs"].as_object().unwrap(); + + // Every props file from the project dir up to the workspace root + // is content-hashed, not just the nearest one. + assert!(configs.contains_key("/workspace/nested/Directory.Build.props")); + assert!(configs.contains_key("/workspace/Directory.Build.props")); + + // The nested props chains to the root props via + // GetPathOfFileAbove, so packages from both levels evaluate in. + let packages = contents["packages"].as_object().unwrap(); + assert_eq!(packages["NestedPkg"].as_str().unwrap(), "2.0.0"); + assert_eq!(packages["RootPkg"].as_str().unwrap(), "1.0.0"); + } + + #[tokio::test(flavor = "multi_thread")] + async fn multi_targeted_project_hashes_the_outer_build() { + let sandbox = create_moon_sandbox("matrix"); + let plugin = sandbox.create_toolchain("dotnet").await; + + let output = plugin + .hash_task_contents(HashTaskContentsInput { + project: fragment("multi", "multi"), + toolchain_config: json!({}), + ..Default::default() + }) + .await; + + let packages = output.contents[0]["packages"].as_object().unwrap(); + + // Documented scope cut: evaluation is the outer (cross-targeting) + // build, where TargetFramework is empty — so per-TFM conditional + // packages are invisible. The root props package still resolves, + // proving the project itself evaluated. + assert!(packages.contains_key("RootPkg")); + assert!(!packages.contains_key("Net8OnlyPkg")); + } + + #[tokio::test(flavor = "multi_thread")] + async fn condition_gated_packages_resolve_by_evaluation() { + let sandbox = create_moon_sandbox("matrix"); + let plugin = sandbox.create_toolchain("dotnet").await; + + let output = plugin + .hash_task_contents(HashTaskContentsInput { + project: fragment("cond", "cond"), + toolchain_config: json!({}), + ..Default::default() + }) + .await; + + let packages = output.contents[0]["packages"].as_object().unwrap(); + + // Conditions are resolved by MSBuild, so a true condition + // contributes and a false one does not. + assert_eq!(packages["ExtraPkg"].as_str().unwrap(), "3.0.0"); + assert!(!packages.contains_key("NeverPkg")); + } + + /// `msbuildProperties` can change the evaluated *package* set, not just + /// the dependency graph — which is why the properties belong in the + /// eval-cache digest. This is that claim tested against a real + /// evaluation rather than only at the digest level. + #[tokio::test(flavor = "multi_thread")] + async fn msbuild_properties_change_the_evaluated_package_set() { + let sandbox = create_moon_sandbox("matrix"); + let plugin = sandbox.create_toolchain("dotnet").await; + + let output = plugin + .hash_task_contents(HashTaskContentsInput { + project: fragment("cond", "cond"), + toolchain_config: json!({ + "msbuildProperties": { "EnableDeepRef": "0" }, + }), + ..Default::default() + }) + .await; + + let packages = output.contents[0]["packages"].as_object().unwrap(); + + // ExtraPkg is gated on the same property as the ProjectReference. + assert!(!packages.contains_key("ExtraPkg")); + + // Unconditional packages are unaffected, so this is the condition + // being re-evaluated rather than the set collapsing. + assert_eq!(packages["RootPkg"].as_str().unwrap(), "1.0.0"); + } + + #[tokio::test(flavor = "multi_thread")] + async fn central_package_management_hashes_via_props() { + let sandbox = create_moon_sandbox("cpm"); + let plugin = sandbox.create_toolchain("dotnet").await; + + let output = plugin + .hash_task_contents(HashTaskContentsInput { + project: fragment("proj", "proj"), + toolchain_config: json!({}), + ..Default::default() + }) + .await; + + let contents = &output.contents[0]; + + // CPM applies versions during restore, not evaluation, so the + // versionless PackageReference surfaces as "*" — the pinned + // version reaches the hash through the Directory.Packages.props + // content below, which is what keeps caching correct. + assert_eq!( + contents["packages"]["Newtonsoft.Json"].as_str().unwrap(), + "*" + ); + + let configs = contents["configs"].as_object().unwrap(); + assert!( + configs["/workspace/Directory.Packages.props"] + .as_str() + .unwrap() + .contains("13.0.3") + ); + } + + #[tokio::test(flavor = "multi_thread")] + async fn reuses_the_package_set_from_the_batched_graph_evaluation() { + let sandbox = create_moon_sandbox("projects"); + let plugin = sandbox.create_toolchain("dotnet").await; + + // Build the graph first: that is where the single batched + // evaluation happens, and it primes the on-disk package sets. + let mut graph_input = ExtendProjectGraphInput::default(); + graph_input + .project_sources + .insert(Id::raw("app-tests"), "app-tests".into()); + graph_input.toolchain_config = json!({ "inferTasks": false }); + + plugin.extend_project_graph(graph_input).await; + + let cache_file = sandbox + .path() + .join(".moon/cache/dotnet-toolchain/eval/app-tests.json"); + + assert!( + cache_file.exists(), + "the graph build must persist the evaluated package set" + ); + + let mut entry: serde_json::Value = + serde_json::from_str(&std::fs::read_to_string(&cache_file).unwrap()).unwrap(); + assert_eq!(entry["packages"]["xunit"].as_str().unwrap(), "2.8.0"); + + // Swap in a package MSBuild could never report, keeping the + // digest: if hashing returns it, the entry was reused instead of + // re-evaluating. + let digest = entry["digest"].as_str().unwrap().to_string(); + entry["packages"] = json!({ "SentinelOnlyInCache": "1.2.3" }); + std::fs::write(&cache_file, entry.to_string()).unwrap(); + + let output = plugin + .hash_task_contents(HashTaskContentsInput { + project: fragment("app-tests", "app-tests"), + toolchain_config: json!({}), + ..Default::default() + }) + .await; + + assert_eq!( + output.contents[0]["packages"]["SentinelOnlyInCache"] + .as_str() + .unwrap(), + "1.2.3", + "task hashing must reuse the primed package set" + ); + + // Editing the project file must invalidate the entry rather than + // serving that stale set. A fresh plugin instance avoids the + // in-instance memo from the call above. + let csproj = sandbox.path().join("app-tests/App.Tests.csproj"); + let edited = std::fs::read_to_string(&csproj) + .unwrap() + .replace("2.8.0", "2.9.0"); + std::fs::write(&csproj, edited).unwrap(); + + let plugin = sandbox.create_toolchain("dotnet").await; + + let output = plugin + .hash_task_contents(HashTaskContentsInput { + project: fragment("app-tests", "app-tests"), + toolchain_config: json!({}), + ..Default::default() + }) + .await; + + let packages = &output.contents[0]["packages"]; + assert!( + packages.get("SentinelOnlyInCache").is_none(), + "a project-file edit must invalidate the cached package set" + ); + assert_eq!(packages["xunit"].as_str().unwrap(), "2.9.0"); + + // ...and the refreshed entry replaces the stale one on disk. + let refreshed: serde_json::Value = + serde_json::from_str(&std::fs::read_to_string(&cache_file).unwrap()).unwrap(); + assert_ne!(refreshed["digest"].as_str().unwrap(), digest); + assert_eq!(refreshed["packages"]["xunit"].as_str().unwrap(), "2.9.0"); + } + + #[tokio::test(flavor = "multi_thread")] + async fn does_not_cache_a_package_set_it_could_not_fully_evaluate() { + let sandbox = create_moon_sandbox("unevaluatable"); + let plugin = sandbox.create_toolchain("dotnet").await; + + let mut graph_input = ExtendProjectGraphInput::default(); + graph_input + .project_sources + .insert(Id::raw("proj"), "proj".into()); + graph_input.toolchain_config = json!({ "inferTasks": false }); + + plugin.extend_project_graph(graph_input).await; + + // An unloadable project yields no package set. Persisting the empty + // one would validate forever under its digest, and since that set + // is the only hash signal without a lock file, package changes + // would stop invalidating task hashes entirely. + assert!( + !sandbox + .path() + .join(".moon/cache/dotnet-toolchain/eval/proj.json") + .exists(), + "an incomplete package set must not reach the on-disk cache" + ); + } + + #[tokio::test(flavor = "multi_thread")] + async fn skips_projects_without_dotnet_toolchain() { + let sandbox = create_moon_sandbox("projects"); + let plugin = sandbox.create_toolchain("dotnet").await; + + let mut project = fragment("app", "app"); + project.toolchains = vec![]; + + let output = plugin + .hash_task_contents(HashTaskContentsInput { + project, + toolchain_config: json!({}), + ..Default::default() + }) + .await; + + assert!(output.contents.is_empty()); + } + } + + // `get_env_var` in the plugin reads the *real* host process environment, so + // an ambient `DOTNET_ROOT` — `actions/setup-dotnet` exports one on every CI + // runner — takes precedence and returns before `resolve_dotnet_root` ever + // consults the home-dir fallback or `global.json`. Assertions here must + // therefore hold in both environments; `assert_ne!` against the sandbox's + // own `.home/.dotnet` does, because that path is never the ambient value. + // Removing the variable instead would need `unsafe { env::remove_var }`, + // which is unsound in these multi-threaded tests. + mod extend_task_command { + use super::*; + + #[tokio::test(flavor = "multi_thread")] + async fn injects_explicit_dotnet_root() { + let sandbox = create_empty_moon_sandbox(); + let plugin = sandbox.create_toolchain("dotnet").await; + + let output = plugin + .extend_task_command(ExtendTaskCommandInput { + toolchain_config: json!({ "dotnetRoot": "/custom/dotnet" }), + ..Default::default() + }) + .await; + + assert_eq!(output.env.get("DOTNET_ROOT").unwrap(), "/custom/dotnet"); + assert_eq!( + output.paths, + vec![std::path::PathBuf::from("/custom/dotnet")] + ); + + // DOTNET_ROOT is the only variable injected; vendor environment + // variables belong in a task's own `env`. + assert_eq!(output.env.len(), 1); + } + + #[tokio::test(flavor = "multi_thread")] + async fn falls_back_to_home_dotnet_when_sdk_layout_present() { + let sandbox = create_empty_moon_sandbox(); + + // A real SDK layout has the dotnet host executable at the root. + let exe = if cfg!(windows) { + "dotnet.exe" + } else { + "dotnet" + }; + sandbox.create_file(format!(".home/.dotnet/{exe}").as_str(), ""); + + let plugin = sandbox.create_toolchain("dotnet").await; + + let output = plugin + .extend_task_command(ExtendTaskCommandInput { + toolchain_config: json!({}), + ..Default::default() + }) + .await; + + let root = output.env.get("DOTNET_ROOT").expect("DOTNET_ROOT not set"); + + // Positive assertion, so it can only check the fallback value when + // no ambient DOTNET_ROOT pre-empts it — see the note on this module. + match std::env::var("DOTNET_ROOT") { + Ok(ambient) if !ambient.is_empty() => assert_eq!(root, &ambient), + _ => assert!(root.contains(".dotnet")), + } + + assert_eq!(output.paths.len(), 1); + } + + #[tokio::test(flavor = "multi_thread")] + async fn skips_home_fallback_that_cannot_satisfy_global_json() { + let sandbox = create_moon_sandbox("projects"); + + // A leftover ~/.dotnet holding only SDK 8 — the exact shape that + // made every task fail against a 10.x pin in a real repo. + let exe = if cfg!(windows) { + "dotnet.exe" + } else { + "dotnet" + }; + sandbox.create_file(format!(".home/.dotnet/{exe}").as_str(), ""); + sandbox.create_file(".home/.dotnet/sdk/8.0.423/marker", ""); + sandbox.create_file( + "global.json", + r#"{"sdk":{"version":"10.0.301","rollForward":"latestMajor"}}"#, + ); + + let plugin = sandbox.create_toolchain("dotnet").await; + + let output = plugin + .extend_task_command(ExtendTaskCommandInput { + project: moon_pdk_api::ProjectFragment { + id: Id::raw("app"), + source: "app".into(), + toolchains: vec![Id::raw("dotnet")], + ..Default::default() + }, + toolchain_config: json!({}), + ..Default::default() + }) + .await; + + // Holds whether or not an ambient DOTNET_ROOT is set: with one, it + // wins and is never the sandbox home; without one, the guard leaves + // DOTNET_ROOT unset. Either way the unsatisfying ~/.dotnet must not + // be what we inject. The satisfaction rules themselves are + // unit-tested in `global_json`. + assert_ne!( + output.env.get("DOTNET_ROOT").map(String::as_str), + sandbox.path().join(".home/.dotnet").to_str(), + "an SDK-8-only ~/.dotnet must not be injected for a 10.x pin" + ); + } + + #[tokio::test(flavor = "multi_thread")] + async fn uses_home_fallback_that_satisfies_global_json() { + let sandbox = create_moon_sandbox("projects"); + + let exe = if cfg!(windows) { + "dotnet.exe" + } else { + "dotnet" + }; + sandbox.create_file(format!(".home/.dotnet/{exe}").as_str(), ""); + sandbox.create_file(".home/.dotnet/sdk/10.0.301/marker", ""); + sandbox.create_file( + "global.json", + r#"{"sdk":{"version":"10.0.301","rollForward":"latestMajor"}}"#, + ); + + let plugin = sandbox.create_toolchain("dotnet").await; + + let output = plugin + .extend_task_command(ExtendTaskCommandInput { + project: moon_pdk_api::ProjectFragment { + id: Id::raw("app"), + source: "app".into(), + toolchains: vec![Id::raw("dotnet")], + ..Default::default() + }, + toolchain_config: json!({}), + ..Default::default() + }) + .await; + + let root = output.env.get("DOTNET_ROOT").expect("DOTNET_ROOT not set"); + + match std::env::var("DOTNET_ROOT") { + Ok(ambient) if !ambient.is_empty() => assert_eq!(root, &ambient), + _ => assert!(root.contains(".dotnet")), + } + } + + #[tokio::test(flavor = "multi_thread")] + async fn no_injection_without_any_dotnet_root() { + let sandbox = create_empty_moon_sandbox(); + + // `~/.dotnet` existing as a mere cache dir (no dotnet executable) + // must NOT be treated as a DOTNET_ROOT. + sandbox.create_file(".home/.dotnet/sdk/8.0.404/marker", ""); + + let plugin = sandbox.create_toolchain("dotnet").await; + + let output = plugin + .extend_task_command(ExtendTaskCommandInput { + toolchain_config: json!({}), + ..Default::default() + }) + .await; + + // Unconditional: an ambient DOTNET_ROOT is never the sandbox home, + // so this asserts the cache dir was rejected in both environments. + // The previous `if env::var(..).is_err()` guard meant this test + // asserted nothing at all on CI. + assert_ne!( + output.env.get("DOTNET_ROOT").map(String::as_str), + sandbox.path().join(".home/.dotnet").to_str(), + "a ~/.dotnet with no dotnet executable is a cache dir, not a DOTNET_ROOT" + ); + } + } +} diff --git a/toolchains/dotnet/tests/tier3_test.rs b/toolchains/dotnet/tests/tier3_test.rs new file mode 100644 index 00000000..082a02ed --- /dev/null +++ b/toolchains/dotnet/tests/tier3_test.rs @@ -0,0 +1,98 @@ +use moon_config::UnresolvedVersionSpec; +use moon_pdk_api::*; +use moon_pdk_test_utils::create_empty_moon_sandbox; +use serde_json::json; + +mod dotnet_toolchain_tier3 { + use super::*; + + mod setup_toolchain { + use super::*; + + #[tokio::test(flavor = "multi_thread")] + async fn no_configured_version_is_a_noop() { + let sandbox = create_empty_moon_sandbox(); + let plugin = sandbox.create_toolchain("dotnet").await; + + let output = plugin + .setup_toolchain(SetupToolchainInput { + configured_version: None, + toolchain_config: json!({}), + ..Default::default() + }) + .await; + + assert!(!output.installed); + assert!(output.operations.is_empty()); + assert!(output.changed_files.is_empty()); + } + + #[tokio::test(flavor = "multi_thread")] + async fn exact_version_already_installed_skips_without_network() { + let sandbox = create_empty_moon_sandbox(); + // A pre-existing SDK layout at the default `~/.dotnet` root. + // Anything past the short-circuit would hit the network and + // fail the test, so completing cleanly proves the skip. + sandbox.create_file(".home/.dotnet/sdk/8.0.404/marker", ""); + + let plugin = sandbox.create_toolchain("dotnet").await; + + let output = plugin + .setup_toolchain(SetupToolchainInput { + configured_version: Some(UnresolvedVersionSpec::parse("8.0.404").unwrap()), + toolchain_config: json!({}), + ..Default::default() + }) + .await; + + assert!(!output.installed); + assert!(output.operations.is_empty()); + } + + #[tokio::test(flavor = "multi_thread")] + #[should_panic(expected = "Unsupported .NET version")] + async fn unsupported_version_spec_errors() { + let sandbox = create_empty_moon_sandbox(); + let plugin = sandbox.create_toolchain("dotnet").await; + + plugin + .setup_toolchain(SetupToolchainInput { + configured_version: Some(UnresolvedVersionSpec::parse("canary").unwrap()), + toolchain_config: json!({}), + ..Default::default() + }) + .await; + } + + // Downloads and runs the real dotnet-install script, fetching a full + // ~200 MB SDK, so it stays out of the default run. On demand: + // cargo nextest run -p dotnet_toolchain --no-default-features \ + // --run-ignored=only full_sdk_install + #[tokio::test(flavor = "multi_thread")] + #[ignore = "downloads a full .NET SDK from the network"] + async fn full_sdk_install() { + let sandbox = create_empty_moon_sandbox(); + let plugin = sandbox.create_toolchain("dotnet").await; + + let root = sandbox.path().join(".home/.dotnet"); + + let output = plugin + .setup_toolchain(SetupToolchainInput { + configured_version: Some(UnresolvedVersionSpec::parse("8.0").unwrap()), + toolchain_config: json!({}), + ..Default::default() + }) + .await; + + assert_eq!(output.operations.len(), 1); + + let exe = if cfg!(windows) { + "dotnet.exe" + } else { + "dotnet" + }; + assert!(root.join(exe).exists()); + assert!(root.join("sdk").exists()); + } + } +} diff --git a/toolchains/go/CHANGELOG.md b/toolchains/go/CHANGELOG.md index eb0ec3e4..1e0543be 100644 --- a/toolchains/go/CHANGELOG.md +++ b/toolchains/go/CHANGELOG.md @@ -1,5 +1,47 @@ # Changelog +## 1.5.0 + +#### 🚀 Updates + +- Reworked relationship inference to match package import paths instead of module paths. Each project now resolves a canonical import path (nearest `go.mod` module path plus the project's relative directory), and `go list -deps` results are matched against those by longest prefix. This makes relationships resolvable in repositories that share a single `go.mod` across all projects. +- Sibling modules required by version without a `go.work` no longer create project relationships, since those builds consume the published module rather than the local source. When the `go` binary is unavailable, projects with their own `go.mod` under a workspace `go.work` fall back to resolving relationships from their direct requires. +- `replace` directives keep their meaning in the new model: a require replaced by a local directory always links to the project at that location (it consumes local source even without a `go.work`), while a require replaced by another module never links. +- Imports within a project's own import path are treated as ownership rather than dependencies. `go list -deps ./...` enumerates packages belonging to projects nested inside the scanned project, which previously inferred an edge from the parent to every nested child — forming a cycle whenever a child declared `dependsOn` on its parent. + +## 1.4.7 + +#### 🐞 Fixes + +- Fixed project relationships not linking when a `go.mod` is located in a major + version folder (`v2`+) but its `module` directive omits the version suffix. + The suffix is now inferred from the folder name, for both the project's alias + and dependency matching. +- `replace` directives are now honored when linking project relationships. A + replacement pointing to a local directory links to the project at that + location (regardless of module names), while a replacement pointing to + another module no longer creates a relationship. +- Project relationships now reference the dependency's project identifier + instead of its module path alias, aligning with other toolchains. + +## 1.4.6 + +#### 🐞 Fixes + +- Fixed configured `bins` not being reinstalled when their binaries were + uninstalled or deleted outside of moon. Missing binaries are now detected in + the globals directory, and only missing binaries are installed. Binaries + pinned to a version, branch, or commit are always installed, as their + installed version cannot be verified. +- The `force` option for `bins` entries is now respected, and will always + install the binary. + +## 1.4.5 + +#### 🚀 Updates + +- Updated to support moon v2.5 release. + ## 1.4.4 #### 🐞 Fixes diff --git a/toolchains/go/Cargo.toml b/toolchains/go/Cargo.toml index 9a148fb1..ba52fe00 100644 --- a/toolchains/go/Cargo.toml +++ b/toolchains/go/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "go_toolchain" -version = "1.4.4" +version = "1.5.0" edition = "2024" description = "Go toolchain WASM plugin for moon." authors = ["Miles Johnson"] diff --git a/toolchains/go/src/lib.rs b/toolchains/go/src/lib.rs index 0f2d4748..6a349766 100644 --- a/toolchains/go/src/lib.rs +++ b/toolchains/go/src/lib.rs @@ -3,6 +3,8 @@ pub mod go_mod; pub mod go_sum; pub mod go_work; +#[cfg(feature = "wasm")] +mod package_graph; #[cfg(feature = "wasm")] mod tier1; #[cfg(feature = "wasm")] diff --git a/toolchains/go/src/package_graph.rs b/toolchains/go/src/package_graph.rs new file mode 100644 index 00000000..5c64ddbe --- /dev/null +++ b/toolchains/go/src/package_graph.rs @@ -0,0 +1,483 @@ +use crate::config::GoToolchainConfig; +use crate::go_mod::{GoMod, ModuleReplacement, Replacement, parse_go_mod}; +use moon_config::DependencyScope; +use moon_pdk::exec; +use moon_pdk_api::*; +use starbase_utils::fs; +use std::collections::{BTreeMap, BTreeSet}; + +// The `go list` half of the package graph: every package a directory's +// packages depend on, as canonical import paths. +fn execute_go_list(dir: &VirtualPath, packages: &[String], test: bool) -> AnyResult> { + let mut args = vec![ + "list", + "-deps", + "-f", + "{{if .Module}}{{.ImportPath}}{{end}}", + ]; + + if test { + args.push("-test"); + } + + // Scan all packages recursively by default so that dependencies imported + // only from subdirectories (internal/, pkg/, ...) are also inferred. + if packages.is_empty() { + args.push("./..."); + } else { + for package in packages { + args.push(package.as_str()); + } + } + + let result = exec(ExecCommandInput::pipe("go", args).cwd(dir.to_owned()))?; + + if result.exit_code != 0 { + return Ok(vec![]); + } + + Ok(result + .stdout + .lines() + .filter_map(|line| { + // Test binary pseudo-packages render as `pkg [pkg.test]`; + // only the real package path participates in matching. + let import_path = line.trim().split(' ').next().unwrap_or_default(); + + (!import_path.is_empty()).then(|| import_path.to_owned()) + }) + .collect()) +} + +// Whether `import_path` is the package at `prefix` itself, or nested beneath +// it. +fn import_within(import_path: &str, prefix: &str) -> bool { + import_path == prefix + || import_path + .strip_prefix(prefix) + .is_some_and(|rest| rest.starts_with('/')) +} + +// Workspace-relative dirs that may own a directory's `go.mod`: the dir +// itself, then each ancestor up to the workspace root (""). +fn module_dir_candidates(source: &str) -> Vec<&str> { + let mut candidates = vec![]; + let mut dir = source; + + while !dir.is_empty() { + candidates.push(dir); + dir = dir.rfind('/').map_or("", |index| &dir[..index]); + } + + candidates.push(""); + candidates +} + +// Resolves workspace directories to the import paths their packages live +// under, caching each `go.mod` it parses along the way. A `None` cache entry +// memoizes "no usable `go.mod` here", so ancestors shared between +// directories only hit the disk once. +struct ModuleResolver { + workspace_root: VirtualPath, + go_mods: BTreeMap>, +} + +impl ModuleResolver { + fn new(workspace_root: VirtualPath) -> Self { + Self { + workspace_root, + go_mods: BTreeMap::default(), + } + } + + // The canonical import path of a workspace-relative dir: the module path + // of the nearest `go.mod` at or above it (bounded by the workspace + // root), joined with the dir's path relative to that module root. + // Returns the owning dir alongside so callers can key back into the + // cache. + fn import_path(&mut self, source: &str) -> AnyResult> { + for dir in module_dir_candidates(source) { + if !self.go_mods.contains_key(dir) { + let go_mod_path = self.go_mod_path(dir); + + let manifest = if go_mod_path.exists() { + Some(parse_go_mod(fs::read_file(&go_mod_path)?)?) + .filter(|manifest| !manifest.module.is_empty()) + .map(|mut manifest| { + // A module in a major version folder (v2+) is + // imported with the version suffix, even when the + // `module` directive omits it + // https://go.dev/ref/mod#major-version-suffixes + if let Some(version) = dir + .rsplit('/') + .next() + .filter(|segment| is_version_segment(segment)) + && !is_version_segment( + manifest.module.rsplit('/').next().unwrap_or_default(), + ) + { + manifest.module = format!("{}/{version}", manifest.module); + } + + manifest + }) + } else { + None + }; + + self.go_mods.insert(dir.to_owned(), manifest); + } + + if let Some(manifest) = self.go_mods.get(dir).and_then(|entry| entry.as_ref()) { + let relative = source[dir.len()..].trim_start_matches('/'); + + let import_path = if relative.is_empty() { + manifest.module.clone() + } else { + format!("{}/{relative}", manifest.module) + }; + + return Ok(Some((dir.to_owned(), import_path))); + } + } + + Ok(None) + } + + // The parsed manifest owned by a dir previously resolved through + // `import_path`, with any major version suffix already applied to its + // module path. + fn manifest(&self, dir: &str) -> Option<&GoMod> { + self.go_mods.get(dir).and_then(|entry| entry.as_ref()) + } + + fn go_mod_path(&self, dir: &str) -> VirtualPath { + if dir.is_empty() { + self.workspace_root.join("go.mod") + } else { + self.workspace_root.join(dir).join("go.mod") + } + } +} + +pub struct GoProject { + pub id: Id, + /// Module path as declared when the project owns its `go.mod` + pub alias: Option, + root: VirtualPath, + import_path: Option, + /// Direct module requires from the project's own `go.mod` + requires: Vec, +} + +struct GoRequire { + module_path: String, + target: GoRequireTarget, +} + +// What a `require` actually resolves to once `replace` directives are +// applied, which determines whether it can link to a local project. +enum GoRequireTarget { + /// Required by version; only links when the environment wires the + /// module to local source (a `go.work` workspace) + Module, + /// Replaced by a local directory (workspace-relative), so it always + /// consumes local source + LocalSource(String), + /// Replaced by another module, or a path outside the workspace, so it + /// can never be a local project + External, +} + +// All the state relationship inference works from: the resolved projects, +// the import-path prefix map they resolve against, and the module resolver +// used to build both. +pub struct GoPackageGraph { + workspace_root: VirtualPath, + config: GoToolchainConfig, + go_exists: bool, + resolver: ModuleResolver, + projects: Vec, + /// Project lookup by workspace-relative source dir, for `replace` + /// directives that point at local directories + source_to_id: BTreeMap, + /// Import path each project resolves to, matched by prefix + package_prefixes: Vec<(String, Id)>, + /// The `go.mod` files backing the resolved import paths + input_files: Vec, +} + +impl GoPackageGraph { + pub fn new(workspace_root: VirtualPath, config: GoToolchainConfig, go_exists: bool) -> Self { + Self { + resolver: ModuleResolver::new(workspace_root.clone()), + workspace_root, + config, + go_exists, + projects: vec![], + source_to_id: BTreeMap::default(), + package_prefixes: vec![], + input_files: vec![], + } + } + + // First pass: resolve every project to its import path and manifest, and + // build the prefix map that imports are resolved against. + pub fn load_projects(&mut self, sources: BTreeMap) -> AnyResult<()> { + for (id, source) in sources { + let root = self.workspace_root.join(&source); + let source = if source == "." { "" } else { source.as_str() }; + + self.source_to_id.insert(source.to_owned(), id.clone()); + + let mut project = GoProject { + id, + root, + alias: None, + import_path: None, + requires: vec![], + }; + + if let Some((mod_dir, import_path)) = self.resolver.import_path(source)? { + let go_mod_path = self.resolver.go_mod_path(&mod_dir); + + if !self.input_files.contains(&go_mod_path) { + self.input_files.push(go_mod_path); + } + + // An ancestor's requires describe the whole module, so they + // only participate for the project that owns the `go.mod` + if mod_dir == source + && let Some(manifest) = self.resolver.manifest(&mod_dir) + { + if manifest.module != project.id.as_str() { + project.alias = Some(manifest.module.clone()); + } + + project.requires = manifest + .require + .iter() + .filter(|dep| !dep.indirect) + .map(|dep| { + let module_path = dep.module.module_path.clone(); + + // A `replace` directive changes what the require + // resolves to, so it takes precedence over + // matching import paths + let target = match manifest + .replace + .iter() + .find(|replacement| replacement.module_path == module_path) + { + Some(ModuleReplacement { + replacement: Replacement::FilePath(path), + .. + }) => resolve_source_path(source, path).map_or( + GoRequireTarget::External, + GoRequireTarget::LocalSource, + ), + Some(_) => GoRequireTarget::External, + None => GoRequireTarget::Module, + }; + + GoRequire { + module_path, + target, + } + }) + .collect(); + } + + project.import_path = Some(import_path); + } + + self.projects.push(project); + } + + self.package_prefixes = self + .projects + .iter() + .filter_map(|project| { + project + .import_path + .clone() + .map(|path| (path, project.id.clone())) + }) + .collect(); + + Ok(()) + } + + pub fn projects(&self) -> &[GoProject] { + &self.projects + } + + pub fn into_input_files(self) -> Vec { + self.input_files + } + + // Resolves an import path to the project whose import path prefixes it. + // The longest match wins, so the module root can't shadow projects + // nested beneath it. + fn resolve_import(&self, import_path: &str) -> Option<&Id> { + self.package_prefixes + .iter() + .filter(|(prefix, _)| import_within(import_path, prefix)) + .max_by_key(|(prefix, _)| prefix.len()) + .map(|(_, id)| id) + } + + // Second pass: infer one project's dependencies, picking the mechanism + // the environment supports. + pub fn project_dependencies(&self, project: &GoProject) -> AnyResult> { + let mut dependencies = vec![]; + + // A project without an import path may still sit under a `go.work`, + // where `go list` resolves imports in workspace mode + if self.go_exists + && (project.import_path.is_some() || project.root.join("go.work").exists()) + { + dependencies = self.dependencies_from_go_list(project)?; + } + + // Monorepos using go.work can be resolved purely off the modfile: the + // workspace wires required sibling modules to their local source, so + // a project's own requires reflect real local relationships. That + // only matters when `go` isn't around to resolve them properly, while + // requires replaced by a local directory always consume local source. + let include_unreplaced = !self.go_exists + && self.workspace_root.join("go.work").exists() + && project.root.join("go.mod").exists(); + + for dependency in self.dependencies_from_modfile(project, include_unreplaced) { + if !dependencies.iter().any(|dep| dep.id == dependency.id) { + dependencies.push(dependency); + } + } + + Ok(dependencies) + } + + fn dependencies_from_modfile( + &self, + project: &GoProject, + include_unreplaced: bool, + ) -> Vec { + let mut dependencies = vec![]; + let mut seen = BTreeSet::new(); + + for require in &project.requires { + let dep_id = match &require.target { + GoRequireTarget::LocalSource(path) => self.source_to_id.get(path), + GoRequireTarget::External => None, + GoRequireTarget::Module => include_unreplaced + .then(|| self.resolve_import(&require.module_path)) + .flatten(), + }; + + if let Some(dep_id) = dep_id + && dep_id != &project.id + && seen.insert(dep_id) + { + dependencies.push(ProjectDependency { + id: dep_id.to_owned(), + scope: DependencyScope::Production, + via: Some(format!("module {}", require.module_path)), + }); + } + } + + dependencies + } + + fn dependencies_from_go_list(&self, project: &GoProject) -> AnyResult> { + let mut dependencies = vec![]; + let mut seen = BTreeSet::new(); + + for (enabled, test, scope) in [ + ( + self.config.infer_relationships, + false, + DependencyScope::Production, + ), + ( + self.config.infer_relationships_from_tests, + true, + DependencyScope::Development, + ), + ] { + if !enabled { + continue; + } + + let imports = execute_go_list( + &project.root, + &self.config.infer_relationships_packages, + test, + )?; + + for import_path in imports { + // `./...` also enumerates packages belonging to projects + // nested inside this one; anything within the project's own + // import path is ownership, not an import + if let Some(own) = project.import_path.as_deref() + && import_within(&import_path, own) + { + continue; + } + + if let Some(dep_id) = self.resolve_import(&import_path) + && dep_id != &project.id + && seen.insert(dep_id) + { + dependencies.push(ProjectDependency { + id: dep_id.to_owned(), + scope, + via: Some(format!("package {import_path}")), + }); + } + } + } + + Ok(dependencies) + } +} + +// Lexically resolve a relative path (`./`, `../`) against a workspace +// relative source, returning `None` when it escapes the workspace +fn resolve_source_path(base: &str, path: &str) -> Option { + if path.starts_with('/') { + return None; + } + + let mut segments = base + .split('/') + .filter(|segment| !segment.is_empty() && *segment != ".") + .collect::>(); + + for segment in path.split('/') { + match segment { + "" | "." => {} + ".." => { + segments.pop()?; + } + _ => segments.push(segment), + } + } + + Some(segments.join("/")) +} + +// A major version suffix segment: v2, v3, etc, but never v0 or v1 +// https://go.dev/ref/mod#major-version-suffixes +pub fn is_version_segment(segment: &str) -> bool { + match segment.strip_prefix('v') { + Some(digits) => { + !digits.is_empty() + && !digits.starts_with('0') + && digits != "1" + && digits.chars().all(|c| c.is_ascii_digit()) + } + None => false, + } +} diff --git a/toolchains/go/src/tier1.rs b/toolchains/go/src/tier1.rs index d5eea26c..84cecac5 100644 --- a/toolchains/go/src/tier1.rs +++ b/toolchains/go/src/tier1.rs @@ -86,9 +86,7 @@ pub fn prune_docker(Json(input): Json) -> FnResult bool { - dir.join("go.mod").exists() - || dir.join("go.sum").exists() - || dir.join("go.work").exists() - || dir.join("main.go").exists() -} - -fn execute_go_list( - dir: &VirtualPath, - packages: &[String], - test: bool, -) -> AnyResult> { - let mut args = vec![ - "list", - "-deps", - "-f", - "{{if .Module}}{{.Module.Path}}{{end}}", - ]; - - if test { - args.push("-test"); - } - - // Scan all packages recursively by default so that dependencies imported - // only from subdirectories (internal/, pkg/, ...) are also inferred. - if packages.is_empty() { - args.push("./..."); - } else { - for package in packages { - args.push(package.as_str()); - } - } - - let result = exec(ExecCommandInput::pipe("go", args).cwd(dir.to_owned()))?; - - if result.exit_code != 0 { - return Ok(vec![]); - } - - Ok(result - .stdout - .lines() - .flat_map(|line| { - let line = line.trim(); - - if line.is_empty() { - None - } else { - Some(ModuleDependency { - module: Module { - module_path: line.into(), - // This is a hack for our use case! - version: if test { - "internal-test".into() - } else { - "".into() - }, - }, - indirect: false, - }) - } - }) - .collect()) -} - #[plugin_fn] pub fn extend_project_graph( Json(input): Json, ) -> FnResult> { - let mut output = ExtendProjectGraphOutput::default(); let config = parse_toolchain_config_schema::(input.toolchain_config)?; let env = get_host_environment()?; - let go_exists = command_exists(&env, "go"); - // First pass, gather all packages and their manifests - let mut packages = BTreeMap::default(); + let mut graph = GoPackageGraph::new( + input.context.workspace_root, + config, + command_exists(env, "go"), + ); - for (id, source) in input.project_sources { - let project_root = input.context.workspace_root.join(source); - let go_mod_path = project_root.join("go.mod"); - - let mut manifest = if go_mod_path.exists() { - if let Some(file) = go_mod_path.virtual_path() { - output.input_files.push(file); - } + // First pass through, we figure out what projects we have and what their root import path is + graph.load_projects(input.project_sources)?; - parse_go_mod(fs::read_file(&go_mod_path)?)? - } else { - GoMod { - // This name isn't correct, but we need something! - module: id.to_string(), - ..Default::default() - } - }; - - if go_exists && is_go_project(&project_root) { - if config.infer_relationships { - manifest.require.extend(execute_go_list( - &project_root, - &config.infer_relationships_packages, - false, - )?); - } - - if config.infer_relationships_from_tests { - manifest.require.extend(execute_go_list( - &project_root, - &config.infer_relationships_packages, - true, - )?); - } - } - - packages.insert(manifest.module.clone(), (id, manifest)); - } + let mut output = ExtendProjectGraphOutput::default(); - // Second pass, extract packages and their relationships - for (id, manifest) in packages.values() { - let mut project_output = ExtendProjectOutput { - alias: if manifest.module.is_empty() || manifest.module == id.as_str() { - None - } else { - Some(manifest.module.clone()) - }, + // On the second pass, we work through all the projects and resolve their dependencies + for project in graph.projects() { + let project_output = ExtendProjectOutput { + alias: project.alias.clone(), + dependencies: graph.project_dependencies(project)?, ..Default::default() }; - for dep in &manifest.require { - let dep_module = &dep.module.module_path; - - if !dep.indirect - && packages - .get(dep_module) - .is_some_and(|(dep_id, _)| dep_id != id) - { - project_output.dependencies.push(ProjectDependency { - id: Id::raw(dep_module.clone()), - scope: if dep.module.version == "internal-test" { - DependencyScope::Development - } else { - DependencyScope::Production - }, - via: Some(format!("module {}", dep_module)), - }); - } - } - - if project_output.alias.is_some() - || !project_output.dependencies.is_empty() - || !project_output.tasks.is_empty() - { + if project_output.alias.is_some() || !project_output.dependencies.is_empty() { output .extended_projects - .insert(id.to_owned(), project_output); + .insert(project.id.to_owned(), project_output); } } + output.input_files = graph.into_input_files(); + Ok(Json(output)) } @@ -179,7 +58,7 @@ fn gather_shared_paths( paths: &mut Vec, ) -> AnyResult<()> { if let Some(globals_dir) = globals_dir - && globals_dir.real_path().is_some() + && globals_dir.to_real_path()?.is_some() { // Avoid the host env overhead if we already // have a valid globals directory! @@ -194,7 +73,11 @@ fn gather_shared_paths( } else if let Some(value) = get_host_env_var("GOPATH")? { Some(PathBuf::from(value).join("bin")) } else { - env.home_dir.join("go").join("bin").real_path() + env.home_dir + .join("go") + .join("bin") + .to_real_path()? + .map(|path| path.to_path_buf()) }; if let Some(dir) = maybe_dir { @@ -217,7 +100,7 @@ pub fn extend_task_command( let env = get_host_environment()?; // Always include Go specific paths for all commands - gather_shared_paths(&env, input.globals_dir.as_ref(), &mut output.paths)?; + gather_shared_paths(env, input.globals_dir.as_ref(), &mut output.paths)?; Ok(Json(output)) } @@ -230,7 +113,7 @@ pub fn extend_task_script( let env = get_host_environment()?; // Always include Go specific paths for all commands - gather_shared_paths(&env, input.globals_dir.as_ref(), &mut output.paths)?; + gather_shared_paths(env, input.globals_dir.as_ref(), &mut output.paths)?; Ok(Json(output)) } @@ -252,21 +135,17 @@ pub fn locate_dependencies_root( output.members = Some(go_work.modules); } - output.root = root.virtual_path(); + output.root = Some(root); } // Then `go.sum` second - if output.root.is_none() - && let Some(root) = locate_root(&input.starting_dir, "go.sum") - { - output.root = root.virtual_path(); + if output.root.is_none() { + output.root = locate_root(&input.starting_dir, "go.sum"); } // Otherwise assume `go.mod` - if output.root.is_none() - && let Some(root) = locate_root(&input.starting_dir, "go.mod") - { - output.root = root.virtual_path(); + if output.root.is_none() { + output.root = locate_root(&input.starting_dir, "go.mod"); } Ok(Json(output)) @@ -361,6 +240,29 @@ pub fn parse_manifest( Ok(Json(output)) } +fn is_bin_installed( + env: &HostEnvironment, + globals_dir: Option<&VirtualPath>, + module: &str, + version: &str, +) -> bool { + // Without a registry to inspect (like Cargo's `.crates.toml`), we can't + // verify which version an installed binary is, so entries pinned to a + // version, branch, or commit are always included. Their command only + // executes when the environment fingerprint changes, like a version bump + if version != "latest" { + return false; + } + + let Some(globals_dir) = globals_dir else { + return false; + }; + + globals_dir + .join(env.os.get_exe_name(get_bin_name(module))) + .exists() +} + #[plugin_fn] pub fn setup_environment( Json(input): Json, @@ -376,18 +278,23 @@ pub fn setup_environment( let mut bins_by_version = BTreeMap::default(); for bin in &config.bins { - let name = match bin { - BinEntry::String(inner) => inner, + let (name, force) = match bin { + BinEntry::String(inner) => (inner.as_str(), false), BinEntry::Object(cfg) => { if cfg.local && env.ci { continue; - } else { - cfg.bin.as_str() } + + (cfg.bin.as_str(), cfg.force) } }; let (module, version) = name.split_once('@').unwrap_or((name, "latest")); + + if !force && is_bin_installed(env, input.globals_dir.as_ref(), module, version) { + continue; + } + let base_module = get_base_module(module); bins_by_version @@ -402,7 +309,8 @@ pub fn setup_environment( output.commands.push( ExecCommand::new(ExecCommandInput::new("go", args).cwd(input.root.to_owned())) - .cache(format!("go-bins-{version}")), + .cache(CacheStrategy::Memory) + .label(format!("go-bins-{version}")), ); } } @@ -443,3 +351,16 @@ fn get_base_module(module: &str) -> String { base } + +// The executable is named after the last segment of the module path, +// excluding a major version suffix: `github.com/foo/bar/v2` -> `bar` +fn get_bin_name(module: &str) -> &str { + let mut segments = module.rsplit('/'); + let last = segments.next().unwrap_or(module); + + if is_version_segment(last) { + return segments.next().unwrap_or(last); + } + + last +} diff --git a/toolchains/go/tests/__fixtures__/projects-single-module/apps/a/main.go b/toolchains/go/tests/__fixtures__/projects-single-module/apps/a/main.go new file mode 100644 index 00000000..5a3780f5 --- /dev/null +++ b/toolchains/go/tests/__fixtures__/projects-single-module/apps/a/main.go @@ -0,0 +1,7 @@ +package main + +import "example.com/org/libs/b" + +func main() { + b.B() +} diff --git a/toolchains/go/tests/__fixtures__/projects-single-module/apps/a/tool/tool.go b/toolchains/go/tests/__fixtures__/projects-single-module/apps/a/tool/tool.go new file mode 100644 index 00000000..6757a538 --- /dev/null +++ b/toolchains/go/tests/__fixtures__/projects-single-module/apps/a/tool/tool.go @@ -0,0 +1,3 @@ +package tool + +func Tool() {} diff --git a/toolchains/go/tests/__fixtures__/projects-single-module/go.mod b/toolchains/go/tests/__fixtures__/projects-single-module/go.mod new file mode 100644 index 00000000..9ced3f55 --- /dev/null +++ b/toolchains/go/tests/__fixtures__/projects-single-module/go.mod @@ -0,0 +1,3 @@ +module example.com/org + +go 1.24 diff --git a/toolchains/go/tests/__fixtures__/projects-single-module/libs/b/lib.go b/toolchains/go/tests/__fixtures__/projects-single-module/libs/b/lib.go new file mode 100644 index 00000000..9d2a7e60 --- /dev/null +++ b/toolchains/go/tests/__fixtures__/projects-single-module/libs/b/lib.go @@ -0,0 +1,3 @@ +package b + +func B() {} diff --git a/toolchains/go/tests/__fixtures__/projects-versioned/arbitrary/go.mod b/toolchains/go/tests/__fixtures__/projects-versioned/arbitrary/go.mod new file mode 100644 index 00000000..ce3f30a4 --- /dev/null +++ b/toolchains/go/tests/__fixtures__/projects-versioned/arbitrary/go.mod @@ -0,0 +1 @@ +module example.com/org/whatever diff --git a/toolchains/go/tests/__fixtures__/projects-versioned/consumer/go.mod b/toolchains/go/tests/__fixtures__/projects-versioned/consumer/go.mod new file mode 100644 index 00000000..75e6f7e1 --- /dev/null +++ b/toolchains/go/tests/__fixtures__/projects-versioned/consumer/go.mod @@ -0,0 +1,7 @@ +module example.com/org/consumer + +require ( + example.com/org/mod v1.0.0 + example.com/org/mod/v2 v2.0.0 + example.com/org/suffixed/v3 v3.0.0 +) diff --git a/toolchains/go/tests/__fixtures__/projects-versioned/mod/go.mod b/toolchains/go/tests/__fixtures__/projects-versioned/mod/go.mod new file mode 100644 index 00000000..705246cd --- /dev/null +++ b/toolchains/go/tests/__fixtures__/projects-versioned/mod/go.mod @@ -0,0 +1 @@ +module example.com/org/mod diff --git a/toolchains/go/tests/__fixtures__/projects-versioned/mod/v2/go.mod b/toolchains/go/tests/__fixtures__/projects-versioned/mod/v2/go.mod new file mode 100644 index 00000000..705246cd --- /dev/null +++ b/toolchains/go/tests/__fixtures__/projects-versioned/mod/v2/go.mod @@ -0,0 +1 @@ +module example.com/org/mod diff --git a/toolchains/go/tests/__fixtures__/projects-versioned/replacer/go.mod b/toolchains/go/tests/__fixtures__/projects-versioned/replacer/go.mod new file mode 100644 index 00000000..67e3ced0 --- /dev/null +++ b/toolchains/go/tests/__fixtures__/projects-versioned/replacer/go.mod @@ -0,0 +1,13 @@ +module example.com/org/replacer + +require ( + example.com/org/renamed v1.0.0 + example.com/org/mod v1.0.0 + example.com/org/outside v1.0.0 +) + +replace example.com/org/renamed => ../arbitrary + +replace example.com/org/mod => example.com/external/mod v1.0.0 + +replace example.com/org/outside => ../../outside diff --git a/toolchains/go/tests/__fixtures__/projects-versioned/suffixed/v3/go.mod b/toolchains/go/tests/__fixtures__/projects-versioned/suffixed/v3/go.mod new file mode 100644 index 00000000..63b6c07b --- /dev/null +++ b/toolchains/go/tests/__fixtures__/projects-versioned/suffixed/v3/go.mod @@ -0,0 +1 @@ +module example.com/org/suffixed/v3 diff --git a/toolchains/go/tests/__fixtures__/projects-workspace-versioned/consumer/go.mod b/toolchains/go/tests/__fixtures__/projects-workspace-versioned/consumer/go.mod new file mode 100644 index 00000000..625851a0 --- /dev/null +++ b/toolchains/go/tests/__fixtures__/projects-workspace-versioned/consumer/go.mod @@ -0,0 +1 @@ +module example.com/org/consumer diff --git a/toolchains/go/tests/__fixtures__/projects-workspace-versioned/consumer/lib.go b/toolchains/go/tests/__fixtures__/projects-workspace-versioned/consumer/lib.go new file mode 100644 index 00000000..5d4b3ee5 --- /dev/null +++ b/toolchains/go/tests/__fixtures__/projects-workspace-versioned/consumer/lib.go @@ -0,0 +1,11 @@ +package consumer + +import ( + modv1 "example.com/org/mod" + modv2 "example.com/org/mod/v2" +) + +func Consume() { + modv1.Old() + modv2.New() +} diff --git a/toolchains/go/tests/__fixtures__/projects-workspace-versioned/go.work b/toolchains/go/tests/__fixtures__/projects-workspace-versioned/go.work new file mode 100644 index 00000000..6d065e4e --- /dev/null +++ b/toolchains/go/tests/__fixtures__/projects-workspace-versioned/go.work @@ -0,0 +1,7 @@ +go 1.24 + +use ( + ./consumer + ./mod + ./mod/v2 +) diff --git a/toolchains/go/tests/__fixtures__/projects-workspace-versioned/mod/go.mod b/toolchains/go/tests/__fixtures__/projects-workspace-versioned/mod/go.mod new file mode 100644 index 00000000..705246cd --- /dev/null +++ b/toolchains/go/tests/__fixtures__/projects-workspace-versioned/mod/go.mod @@ -0,0 +1 @@ +module example.com/org/mod diff --git a/toolchains/go/tests/__fixtures__/projects-workspace-versioned/mod/lib.go b/toolchains/go/tests/__fixtures__/projects-workspace-versioned/mod/lib.go new file mode 100644 index 00000000..4c011136 --- /dev/null +++ b/toolchains/go/tests/__fixtures__/projects-workspace-versioned/mod/lib.go @@ -0,0 +1,3 @@ +package mod + +func Old() {} diff --git a/toolchains/go/tests/__fixtures__/projects-workspace-versioned/mod/v2/go.mod b/toolchains/go/tests/__fixtures__/projects-workspace-versioned/mod/v2/go.mod new file mode 100644 index 00000000..9d858339 --- /dev/null +++ b/toolchains/go/tests/__fixtures__/projects-workspace-versioned/mod/v2/go.mod @@ -0,0 +1 @@ +module example.com/org/mod/v2 diff --git a/toolchains/go/tests/__fixtures__/projects-workspace-versioned/mod/v2/lib.go b/toolchains/go/tests/__fixtures__/projects-workspace-versioned/mod/v2/lib.go new file mode 100644 index 00000000..683a5aaf --- /dev/null +++ b/toolchains/go/tests/__fixtures__/projects-workspace-versioned/mod/v2/lib.go @@ -0,0 +1,3 @@ +package mod + +func New() {} diff --git a/toolchains/go/tests/tier1_test.rs b/toolchains/go/tests/tier1_test.rs index a0e2132d..76ac5341 100644 --- a/toolchains/go/tests/tier1_test.rs +++ b/toolchains/go/tests/tier1_test.rs @@ -2,7 +2,6 @@ use moon_config::DockerPruneConfig; use moon_pdk_api::*; use moon_pdk_test_utils::create_empty_moon_sandbox; use serde_json::json; -use std::path::PathBuf; mod go_toolchain_tier1 { use super::*; @@ -51,7 +50,7 @@ mod go_toolchain_tier1 { delete_vendor_directories: true, ..Default::default() }, - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), ..Default::default() }) .await; @@ -72,7 +71,7 @@ mod go_toolchain_tier1 { delete_vendor_directories: false, ..Default::default() }, - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), ..Default::default() }) .await; @@ -95,14 +94,17 @@ mod go_toolchain_tier1 { delete_vendor_directories: true, ..Default::default() }, - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), ..Default::default() }) .await; assert!(!sandbox.path().join("vendor").exists()); - assert_eq!(output.changed_files, [PathBuf::from("/workspace/vendor")]); + assert_eq!( + output.changed_files, + [VirtualPath::new("/workspace/vendor")] + ); } #[tokio::test(flavor = "multi_thread")] @@ -118,7 +120,7 @@ mod go_toolchain_tier1 { delete_vendor_directories: true, ..Default::default() }, - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "vendorDir": "nested/.vendor" }), @@ -130,7 +132,7 @@ mod go_toolchain_tier1 { assert_eq!( output.changed_files, - [PathBuf::from("/workspace/nested/.vendor")] + [VirtualPath::new("/workspace/nested/.vendor")] ); } } diff --git a/toolchains/go/tests/tier2_test.rs b/toolchains/go/tests/tier2_test.rs index 6826f047..6b0569be 100644 --- a/toolchains/go/tests/tier2_test.rs +++ b/toolchains/go/tests/tier2_test.rs @@ -5,7 +5,6 @@ use moon_pdk_test_utils::{create_empty_moon_sandbox, create_moon_sandbox}; use serde_json::json; use std::collections::BTreeMap; use std::env; -use std::path::PathBuf; mod go_toolchain_tier2 { use super::*; @@ -43,21 +42,12 @@ mod go_toolchain_tier2 { } ), ( + // `c` requires the sibling modules in its `go.mod` + // but has no source importing them; a require without + // a real import is not a relationship. Id::raw("c"), ExtendProjectOutput { alias: Some("example.com/org/c".into()), - dependencies: vec![ - ProjectDependency { - id: Id::raw("example.com/org/a"), - scope: DependencyScope::Production, - via: Some("module example.com/org/a".into()), - }, - ProjectDependency { - id: Id::raw("example.com/org/b"), - scope: DependencyScope::Production, - via: Some("module example.com/org/b".into()), - } - ], ..Default::default() } ), @@ -67,9 +57,9 @@ mod go_toolchain_tier2 { assert_eq!( output.input_files, [ - PathBuf::from("/workspace/a/go.mod"), - PathBuf::from("/workspace/b/go.mod"), - PathBuf::from("/workspace/c/go.mod"), + VirtualPath::new("/workspace/a/go.mod"), + VirtualPath::new("/workspace/b/go.mod"), + VirtualPath::new("/workspace/c/go.mod"), ] ); } @@ -95,7 +85,10 @@ mod go_toolchain_tier2 { ),]) ); - assert_eq!(output.input_files, [PathBuf::from("/workspace/a/go.mod")]); + assert_eq!( + output.input_files, + [VirtualPath::new("/workspace/a/go.mod")] + ); } #[tokio::test(flavor = "multi_thread")] @@ -114,6 +107,116 @@ mod go_toolchain_tier2 { assert!(output.input_files.is_empty()); } + #[tokio::test(flavor = "multi_thread")] + async fn appends_major_version_folder_to_module() { + let sandbox = create_moon_sandbox("projects-versioned"); + let plugin = sandbox.create_toolchain("go").await; + + let mut input = ExtendProjectGraphInput::default(); + input + .project_sources + .insert(Id::raw("consumer"), "consumer".into()); + input.project_sources.insert(Id::raw("mod"), "mod".into()); + input + .project_sources + .insert(Id::raw("mod-v2"), "mod/v2".into()); + input + .project_sources + .insert(Id::raw("suffixed"), "suffixed/v3".into()); + + let output = plugin.extend_project_graph(input).await; + + assert_eq!( + output.extended_projects, + BTreeMap::from_iter([ + ( + // `consumer` requires the versioned modules in its + // `go.mod` but has no source importing them, so no + // relationships are created; the version-suffixed + // aliases below still resolve. + Id::raw("consumer"), + ExtendProjectOutput { + alias: Some("example.com/org/consumer".into()), + ..Default::default() + } + ), + ( + Id::raw("mod"), + ExtendProjectOutput { + alias: Some("example.com/org/mod".into()), + ..Default::default() + } + ), + ( + Id::raw("mod-v2"), + ExtendProjectOutput { + alias: Some("example.com/org/mod/v2".into()), + ..Default::default() + } + ), + ( + Id::raw("suffixed"), + ExtendProjectOutput { + alias: Some("example.com/org/suffixed/v3".into()), + ..Default::default() + } + ), + ]) + ); + } + + #[tokio::test(flavor = "multi_thread")] + async fn resolves_deps_through_replace_directives() { + let sandbox = create_moon_sandbox("projects-versioned"); + let plugin = sandbox.create_toolchain("go").await; + + let mut input = ExtendProjectGraphInput::default(); + input + .project_sources + .insert(Id::raw("arbitrary"), "arbitrary".into()); + input.project_sources.insert(Id::raw("mod"), "mod".into()); + input + .project_sources + .insert(Id::raw("replacer"), "replacer".into()); + + let output = plugin.extend_project_graph(input).await; + + assert_eq!( + output.extended_projects, + BTreeMap::from_iter([ + ( + Id::raw("arbitrary"), + ExtendProjectOutput { + alias: Some("example.com/org/whatever".into()), + ..Default::default() + } + ), + ( + Id::raw("mod"), + ExtendProjectOutput { + alias: Some("example.com/org/mod".into()), + ..Default::default() + } + ), + ( + Id::raw("replacer"), + ExtendProjectOutput { + alias: Some("example.com/org/replacer".into()), + // The `mod` require is replaced with an external + // module, and `outside` escapes the workspace, so + // neither creates a relationship + dependencies: vec![ProjectDependency { + id: Id::raw("arbitrary"), + scope: DependencyScope::Production, + via: Some("module example.com/org/renamed".into()), + }], + ..Default::default() + } + ), + ]) + ); + } + mod go_list { use super::*; @@ -155,14 +258,14 @@ mod go_toolchain_tier2 { alias: Some("example.com/org/c".into()), dependencies: vec![ ProjectDependency { - id: Id::raw("example.com/org/a"), + id: Id::raw("a"), scope: DependencyScope::Production, - via: Some("module example.com/org/a".into()), + via: Some("package example.com/org/a".into()), }, ProjectDependency { - id: Id::raw("example.com/org/b"), + id: Id::raw("b"), scope: DependencyScope::Production, - via: Some("module example.com/org/b".into()), + via: Some("package example.com/org/b".into()), } ], ..Default::default() @@ -174,13 +277,134 @@ mod go_toolchain_tier2 { assert_eq!( output.input_files, [ - PathBuf::from("/workspace/a/go.mod"), - PathBuf::from("/workspace/b/go.mod"), - PathBuf::from("/workspace/c/go.mod"), + VirtualPath::new("/workspace/a/go.mod"), + VirtualPath::new("/workspace/b/go.mod"), + VirtualPath::new("/workspace/c/go.mod"), ] ); } + #[tokio::test(flavor = "multi_thread")] + async fn distinguishes_major_versioned_modules() { + let sandbox = create_moon_sandbox("projects-workspace-versioned"); + let plugin = sandbox.create_toolchain("go").await; + + let mut input = ExtendProjectGraphInput::default(); + input + .project_sources + .insert(Id::raw("consumer"), "consumer".into()); + input.project_sources.insert(Id::raw("mod"), "mod".into()); + input + .project_sources + .insert(Id::raw("mod-v2"), "mod/v2".into()); + input.toolchain_config = json!({ + "inferRelationships": true + }); + + let output = plugin.extend_project_graph(input).await; + + // `example.com/org/mod` prefixes `example.com/org/mod/v2`, so + // the v2 import must resolve to the v2 project rather than + // collapsing into the v1 module. + assert_eq!( + output.extended_projects.get(&Id::raw("consumer")), + Some(&ExtendProjectOutput { + alias: Some("example.com/org/consumer".into()), + dependencies: vec![ + ProjectDependency { + id: Id::raw("mod"), + scope: DependencyScope::Production, + via: Some("package example.com/org/mod".into()), + }, + ProjectDependency { + id: Id::raw("mod-v2"), + scope: DependencyScope::Production, + via: Some("package example.com/org/mod/v2".into()), + }, + ], + ..Default::default() + }) + ); + + assert_eq!( + output.extended_projects.get(&Id::raw("mod-v2")), + Some(&ExtendProjectOutput { + alias: Some("example.com/org/mod/v2".into()), + ..Default::default() + }) + ); + } + + #[tokio::test(flavor = "multi_thread")] + async fn infers_relations_within_a_single_module() { + let sandbox = create_moon_sandbox("projects-single-module"); + let plugin = sandbox.create_toolchain("go").await; + + let mut input = ExtendProjectGraphInput::default(); + input.project_sources.insert(Id::raw("a"), "apps/a".into()); + input.project_sources.insert(Id::raw("b"), "libs/b".into()); + input.toolchain_config = json!({ + "inferRelationships": true + }); + + let output = plugin.extend_project_graph(input).await; + + // Both projects derive their import path from the root go.mod, + // so only "a" has anything to output. + assert_eq!( + output.extended_projects, + BTreeMap::from_iter([( + Id::raw("a"), + ExtendProjectOutput { + dependencies: vec![ProjectDependency { + id: Id::raw("b"), + scope: DependencyScope::Production, + via: Some("package example.com/org/libs/b".into()), + }], + ..Default::default() + } + )]) + ); + + assert_eq!(output.input_files, [VirtualPath::new("/workspace/go.mod")]); + } + + #[tokio::test(flavor = "multi_thread")] + async fn doesnt_infer_edges_to_nested_projects_it_never_imports() { + let sandbox = create_moon_sandbox("projects-single-module"); + let plugin = sandbox.create_toolchain("go").await; + + let mut input = ExtendProjectGraphInput::default(); + input.project_sources.insert(Id::raw("a"), "apps/a".into()); + input.project_sources.insert(Id::raw("b"), "libs/b".into()); + input + .project_sources + .insert(Id::raw("tool"), "apps/a/tool".into()); + input.toolchain_config = json!({ + "inferRelationships": true + }); + + let output = plugin.extend_project_graph(input).await; + + // `go list -deps ./...` run from `a` enumerates the nested + // `tool` project's package as a root even though nothing + // imports it; ownership must not become a dependency edge. + assert_eq!( + output.extended_projects, + BTreeMap::from_iter([( + Id::raw("a"), + ExtendProjectOutput { + dependencies: vec![ProjectDependency { + id: Id::raw("b"), + scope: DependencyScope::Production, + via: Some("package example.com/org/libs/b".into()), + }], + ..Default::default() + } + )]) + ); + } + #[tokio::test(flavor = "multi_thread")] async fn doesnt_infer_relations_if_config_disabled() { let sandbox = create_moon_sandbox("projects-workspace"); @@ -226,9 +450,9 @@ mod go_toolchain_tier2 { assert_eq!( output.input_files, [ - PathBuf::from("/workspace/a/go.mod"), - PathBuf::from("/workspace/b/go.mod"), - PathBuf::from("/workspace/c/go.mod"), + VirtualPath::new("/workspace/a/go.mod"), + VirtualPath::new("/workspace/b/go.mod"), + VirtualPath::new("/workspace/c/go.mod"), ] ); } @@ -254,9 +478,9 @@ mod go_toolchain_tier2 { Some(&ExtendProjectOutput { alias: Some("example.com/org/d".into()), dependencies: vec![ProjectDependency { - id: Id::raw("example.com/org/a"), + id: Id::raw("a"), scope: DependencyScope::Production, - via: Some("module example.com/org/a".into()), + via: Some("package example.com/org/a".into()), }], ..Default::default() }) @@ -277,18 +501,17 @@ mod go_toolchain_tier2 { let output = plugin.extend_project_graph(input).await; - // `e` imports `example.com/org/a` only through the `a/pkg` - // subpackage, so the dependency is only inferred when - // `go list -deps` emits the owning module path via - // `-f {{if .Module}}{{.Module.Path}}{{end}}`. + // `e` never imports `example.com/org/a` itself, only the + // `a/pkg` subpackage, so the dependency relies on prefix + // matching rather than an exact import path match. assert_eq!( output.extended_projects.get(&Id::raw("e")), Some(&ExtendProjectOutput { alias: Some("example.com/org/e".into()), dependencies: vec![ProjectDependency { - id: Id::raw("example.com/org/a"), + id: Id::raw("a"), scope: DependencyScope::Production, - via: Some("module example.com/org/a".into()), + via: Some("package example.com/org/a/pkg".into()), }], ..Default::default() }) @@ -392,7 +615,7 @@ mod go_toolchain_tier2 { let output = plugin .locate_dependencies_root(LocateDependenciesRootInput { - starting_dir: VirtualPath::Real(sandbox.path().into()), + starting_dir: VirtualPath::new(sandbox.path()), ..Default::default() }) .await; @@ -408,13 +631,13 @@ mod go_toolchain_tier2 { let output = plugin .locate_dependencies_root(LocateDependenciesRootInput { - starting_dir: VirtualPath::Real(sandbox.path().join("package/nested")), + starting_dir: VirtualPath::new(sandbox.path().join("package/nested")), ..Default::default() }) .await; assert!(output.members.is_none()); - assert_eq!(output.root.unwrap(), PathBuf::from("/workspace/package")); + assert_eq!(output.root.unwrap(), VirtualPath::new("/workspace/package")); } #[tokio::test(flavor = "multi_thread")] @@ -424,7 +647,7 @@ mod go_toolchain_tier2 { let output = plugin .locate_dependencies_root(LocateDependenciesRootInput { - starting_dir: VirtualPath::Real(sandbox.path().join("package-with-sum/nested")), + starting_dir: VirtualPath::new(sandbox.path().join("package-with-sum/nested")), ..Default::default() }) .await; @@ -432,7 +655,7 @@ mod go_toolchain_tier2 { assert!(output.members.is_none()); assert_eq!( output.root.unwrap(), - PathBuf::from("/workspace/package-with-sum") + VirtualPath::new("/workspace/package-with-sum") ); } @@ -443,7 +666,7 @@ mod go_toolchain_tier2 { let output = plugin .locate_dependencies_root(LocateDependenciesRootInput { - starting_dir: VirtualPath::Real( + starting_dir: VirtualPath::new( sandbox.path().join("workspace/modules/a/nested"), ), ..Default::default() @@ -451,7 +674,10 @@ mod go_toolchain_tier2 { .await; assert_eq!(output.members.unwrap(), ["modules/a"]); - assert_eq!(output.root.unwrap(), PathBuf::from("/workspace/workspace")); + assert_eq!( + output.root.unwrap(), + VirtualPath::new("/workspace/workspace") + ); } } @@ -465,7 +691,7 @@ mod go_toolchain_tier2 { let output = plugin .install_dependencies(InstallDependenciesInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({}), ..Default::default() }) @@ -484,7 +710,7 @@ mod go_toolchain_tier2 { let output = plugin .install_dependencies(InstallDependenciesInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "workspaces": true }), @@ -511,7 +737,7 @@ mod go_toolchain_tier2 { let output = plugin .install_dependencies(InstallDependenciesInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "workspaces": false }), @@ -534,7 +760,7 @@ mod go_toolchain_tier2 { let output = plugin .install_dependencies(InstallDependenciesInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "tidyOnChange": true, "workspaces": true @@ -562,7 +788,7 @@ mod go_toolchain_tier2 { let output = plugin .install_dependencies(InstallDependenciesInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({}), ..Default::default() }) @@ -588,7 +814,7 @@ mod go_toolchain_tier2 { let output = plugin .install_dependencies(InstallDependenciesInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "tidyOnChange": true }), @@ -622,7 +848,7 @@ mod go_toolchain_tier2 { let output = plugin .install_dependencies(InstallDependenciesInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "tidyOnChange": false }), @@ -649,7 +875,7 @@ mod go_toolchain_tier2 { let output = plugin .install_dependencies(InstallDependenciesInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "tidyOnChange": true }), @@ -678,7 +904,7 @@ mod go_toolchain_tier2 { let output = plugin .parse_lock(ParseLockInput { - path: VirtualPath::Real(sandbox.path().join("basic.sum")), + path: VirtualPath::new(sandbox.path().join("basic.sum")), ..Default::default() }) .await; @@ -721,7 +947,7 @@ mod go_toolchain_tier2 { let output = plugin .parse_lock(ParseLockInput { - path: VirtualPath::Real(sandbox.path().join("basic.work.sum")), + path: VirtualPath::new(sandbox.path().join("basic.work.sum")), ..Default::default() }) .await; @@ -768,7 +994,7 @@ mod go_toolchain_tier2 { let output = plugin .parse_manifest(ParseManifestInput { - path: VirtualPath::Real(sandbox.path().join("c/go.mod")), + path: VirtualPath::new(sandbox.path().join("c/go.mod")), ..Default::default() }) .await; @@ -805,7 +1031,7 @@ mod go_toolchain_tier2 { let output = plugin .setup_environment(SetupEnvironmentInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "bins": [] }), @@ -823,7 +1049,7 @@ mod go_toolchain_tier2 { let output = plugin .setup_environment(SetupEnvironmentInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "bins": [ "golang.org/x/tools/gopls", @@ -846,12 +1072,14 @@ mod go_toolchain_tier2 { ) .cwd(plugin.plugin.to_virtual_path(sandbox.path())) ) - .cache("go-bins-github.com/revel/cmd@latest"), + .cache(CacheStrategy::Memory) + .label("go-bins-github.com/revel/cmd@latest"), ExecCommand::new( ExecCommandInput::new("go", ["install", "-v", "golang.org/x/tools/gopls"],) .cwd(plugin.plugin.to_virtual_path(sandbox.path())) ) - .cache("go-bins-golang.org/x/tools@latest") + .cache(CacheStrategy::Memory) + .label("go-bins-golang.org/x/tools@latest") ] ); } @@ -863,7 +1091,7 @@ mod go_toolchain_tier2 { let output = plugin .setup_environment(SetupEnvironmentInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "bins": [ "golang.org/x/tools/gopls@1", @@ -886,7 +1114,8 @@ mod go_toolchain_tier2 { ) .cwd(plugin.plugin.to_virtual_path(sandbox.path())) ) - .cache("go-bins-github.com/revel/cmd@2"), + .cache(CacheStrategy::Memory) + .label("go-bins-github.com/revel/cmd@2"), ExecCommand::new( ExecCommandInput::new( "go", @@ -894,7 +1123,8 @@ mod go_toolchain_tier2 { ) .cwd(plugin.plugin.to_virtual_path(sandbox.path())) ) - .cache("go-bins-golang.org/x/tools@1"), + .cache(CacheStrategy::Memory) + .label("go-bins-golang.org/x/tools@1"), ] ); } @@ -913,7 +1143,7 @@ mod go_toolchain_tier2 { let output = plugin .setup_environment(SetupEnvironmentInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "bins": [ { @@ -928,5 +1158,149 @@ mod go_toolchain_tier2 { assert!(output.commands.is_empty()); } + + // https://github.com/moonrepo/plugins/issues/137 + + #[tokio::test(flavor = "multi_thread")] + async fn only_installs_missing_bins() { + let sandbox = create_empty_moon_sandbox(); + sandbox.create_file(".go/bin/gopls", ""); + sandbox.create_file(".go/bin/gopls.exe", ""); + + let plugin = sandbox.create_toolchain("go").await; + + let output = plugin + .setup_environment(SetupEnvironmentInput { + root: VirtualPath::new(sandbox.path()), + globals_dir: Some(VirtualPath::new(sandbox.path().join(".go/bin"))), + toolchain_config: json!({ + "bins": ["golang.org/x/tools/gopls", "github.com/revel/cmd/revel"] + }), + ..Default::default() + }) + .await; + + assert_eq!( + output.commands, + [ExecCommand::new( + ExecCommandInput::new("go", ["install", "-v", "github.com/revel/cmd/revel"],) + .cwd(plugin.plugin.to_virtual_path(sandbox.path())) + ) + .cache(CacheStrategy::Memory) + .label("go-bins-github.com/revel/cmd@latest")] + ); + } + + #[tokio::test(flavor = "multi_thread")] + async fn adds_no_commands_if_all_bins_installed() { + let sandbox = create_empty_moon_sandbox(); + sandbox.create_file(".go/bin/gopls", ""); + sandbox.create_file(".go/bin/gopls.exe", ""); + + let plugin = sandbox.create_toolchain("go").await; + + let output = plugin + .setup_environment(SetupEnvironmentInput { + root: VirtualPath::new(sandbox.path()), + globals_dir: Some(VirtualPath::new(sandbox.path().join(".go/bin"))), + toolchain_config: json!({ + "bins": ["golang.org/x/tools/gopls"] + }), + ..Default::default() + }) + .await; + + assert!(output.commands.is_empty()); + } + + #[tokio::test(flavor = "multi_thread")] + async fn checks_exe_name_excluding_major_version_suffix() { + let sandbox = create_empty_moon_sandbox(); + sandbox.create_file(".go/bin/revel", ""); + sandbox.create_file(".go/bin/revel.exe", ""); + + let plugin = sandbox.create_toolchain("go").await; + + let output = plugin + .setup_environment(SetupEnvironmentInput { + root: VirtualPath::new(sandbox.path()), + globals_dir: Some(VirtualPath::new(sandbox.path().join(".go/bin"))), + toolchain_config: json!({ + "bins": ["github.com/revel/cmd/revel/v2"] + }), + ..Default::default() + }) + .await; + + assert!(output.commands.is_empty()); + } + + #[tokio::test(flavor = "multi_thread")] + async fn always_installs_versioned_bins() { + let sandbox = create_empty_moon_sandbox(); + sandbox.create_file(".go/bin/gopls", ""); + sandbox.create_file(".go/bin/gopls.exe", ""); + + let plugin = sandbox.create_toolchain("go").await; + + let output = plugin + .setup_environment(SetupEnvironmentInput { + root: VirtualPath::new(sandbox.path()), + globals_dir: Some(VirtualPath::new(sandbox.path().join(".go/bin"))), + toolchain_config: json!({ + "bins": ["golang.org/x/tools/gopls@v0.16.0"] + }), + ..Default::default() + }) + .await; + + assert_eq!( + output.commands, + [ExecCommand::new( + ExecCommandInput::new( + "go", + ["install", "-v", "golang.org/x/tools/gopls@v0.16.0"], + ) + .cwd(plugin.plugin.to_virtual_path(sandbox.path())) + ) + .cache(CacheStrategy::Memory) + .label("go-bins-golang.org/x/tools@v0.16.0")] + ); + } + + #[tokio::test(flavor = "multi_thread")] + async fn always_installs_forced_bins() { + let sandbox = create_empty_moon_sandbox(); + sandbox.create_file(".go/bin/gopls", ""); + sandbox.create_file(".go/bin/gopls.exe", ""); + + let plugin = sandbox.create_toolchain("go").await; + + let output = plugin + .setup_environment(SetupEnvironmentInput { + root: VirtualPath::new(sandbox.path()), + globals_dir: Some(VirtualPath::new(sandbox.path().join(".go/bin"))), + toolchain_config: json!({ + "bins": [ + { + "bin": "golang.org/x/tools/gopls", + "force": true + } + ] + }), + ..Default::default() + }) + .await; + + assert_eq!( + output.commands, + [ExecCommand::new( + ExecCommandInput::new("go", ["install", "-v", "golang.org/x/tools/gopls"],) + .cwd(plugin.plugin.to_virtual_path(sandbox.path())) + ) + .cache(CacheStrategy::Memory) + .label("go-bins-golang.org/x/tools@latest")] + ); + } } } diff --git a/toolchains/javascript/CHANGELOG.md b/toolchains/javascript/CHANGELOG.md index 784f1897..a14e398e 100644 --- a/toolchains/javascript/CHANGELOG.md +++ b/toolchains/javascript/CHANGELOG.md @@ -1,5 +1,27 @@ # Changelog +## 1.3.0 + +#### 🚀 Updates + +- Added unstable support for [Nub](https://nubjs.com/) as a package manager: + - Natively uses `nub.lock` (pnpm lockfile format), but will locate dependency + roots using other package manager lockfiles that nub can operate on. + - Reads workspace members and catalogs from `pnpm-workspace.yaml` when + present, otherwise from `package.json`. + - Does not require the Node.js toolchain, as nub is a standalone binary. + +#### 🐞 Fixes + +- Fixed `bun.lock` parsing failing on Git/GitHub dependencies that include both + package metadata (`dependencies`, `bin`, etc) and an integrity hash. + +## 1.2.2 + +#### 🚀 Updates + +- Updated to support moon v2.5 release. + ## 1.2.1 #### 🐞 Fixes diff --git a/toolchains/javascript/Cargo.toml b/toolchains/javascript/Cargo.toml index fa8a3528..018dc641 100644 --- a/toolchains/javascript/Cargo.toml +++ b/toolchains/javascript/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "javascript_toolchain" -version = "1.2.1" +version = "1.3.0" edition = "2024" description = "JavaScript toolchain WASM plugin for moon." authors = ["Miles Johnson"] diff --git a/toolchains/javascript/src/config.rs b/toolchains/javascript/src/config.rs index 8da9ce96..d389f6bb 100644 --- a/toolchains/javascript/src/config.rs +++ b/toolchains/javascript/src/config.rs @@ -1,5 +1,5 @@ use moon_common::Id; -use moon_pdk_api::{UnresolvedVersionSpec, Version, VersionReq, config_struct}; +use moon_pdk_api::{MatchesVersion, Requirement, UnresolvedVersionSpec, Version, config_struct}; use nodejs_package_json::VersionProtocol; use rustc_hash::FxHashMap; use schematic::{Config, ConfigEnum, derive_enum}; @@ -12,6 +12,7 @@ derive_enum!( Deno, #[default] Npm, + Nub, Pnpm, Yarn, } @@ -20,14 +21,22 @@ derive_enum!( impl JavaScriptPackageManager { pub fn get_runtime_toolchain(&self) -> Id { match self { - JavaScriptPackageManager::Bun => Id::raw("bun"), - JavaScriptPackageManager::Deno => Id::raw("deno"), + Self::Bun => Id::raw("bun"), + Self::Deno => Id::raw("deno"), + Self::Nub => Id::raw("nub"), _ => Id::raw("node"), } } + /// Installs dependencies for the Node.js ecosystem. pub fn is_for_node(&self) -> bool { - matches!(self, Self::Npm | Self::Pnpm | Self::Yarn) + matches!(self, Self::Npm | Self::Nub | Self::Pnpm | Self::Yarn) + } + + /// Runs as a standalone binary and does not require the + /// Node.js toolchain to operate. + pub fn is_standalone(&self) -> bool { + matches!(self, Self::Bun | Self::Deno | Self::Nub) } } @@ -146,23 +155,21 @@ impl SharedPackageManagerConfig { return false; }; - let req = VersionReq::parse(req).unwrap(); + let req = Requirement::parse(req).unwrap(); match spec { UnresolvedVersionSpec::Canary => true, - UnresolvedVersionSpec::Req(value) => { - let value = value.comparators.first().unwrap(); + UnresolvedVersionSpec::Requirement(value) => { let mut version = Version::new( - value.major, + value.major.unwrap_or(0), value.minor.unwrap_or(0), value.patch.unwrap_or(0), ); - version.pre = value.pre.clone(); + version.prerelease = value.prerelease.clone(); req.matches(&version) } - UnresolvedVersionSpec::Calendar(version) => req.matches(version), - UnresolvedVersionSpec::Semantic(version) => req.matches(version), + UnresolvedVersionSpec::Version(version) => req.matches(version), _ => false, } } diff --git a/toolchains/javascript/src/infer_tasks.rs b/toolchains/javascript/src/infer_tasks.rs index 605c5ce2..c706a237 100644 --- a/toolchains/javascript/src/infer_tasks.rs +++ b/toolchains/javascript/src/infer_tasks.rs @@ -139,10 +139,7 @@ impl<'a> TasksInferrer<'a> { } // toolchains - if matches!( - package_manager, - JavaScriptPackageManager::Bun | JavaScriptPackageManager::Deno - ) { + if package_manager.is_standalone() { config.toolchains = Some(OneOrMany::Many(vec![ Id::raw("javascript"), package_manager.get_runtime_toolchain(), diff --git a/toolchains/javascript/src/lockfiles/bun.rs b/toolchains/javascript/src/lockfiles/bun.rs index c8c7a37c..8e359c13 100644 --- a/toolchains/javascript/src/lockfiles/bun.rs +++ b/toolchains/javascript/src/lockfiles/bun.rs @@ -17,25 +17,46 @@ pub struct BunLockPackageJson { pub optional_dependencies: BTreeMap, } +// Entry shapes are defined by bun's lockfile serializer: +// https://github.com/oven-sh/bun/blob/main/src/install/lockfile/bun.lock.rs +// npm -> [ "name@version", registry, INFO, integrity ] +// git -> [ "name@git+repo", INFO, bun tag, integrity? ] +// github -> [ "name@github:user/repo", INFO, bun tag, integrity? ] +// tarball -> [ "name@url", INFO, integrity? ] +// symlink -> [ "name@link:path", INFO ] +// folder -> [ "name@file:path", INFO ] +// root -> [ "name@root:", INFO ] +// workspace -> [ "name@workspace:path" ] #[derive(Debug, Deserialize)] #[serde(untagged)] pub enum BunLockPackage { + // npm Dependency1( String, // identifier - String, // ??? + String, // registry JsonValue, // object String, // sha ), + // git/github with integrity Dependency2( String, // identifier JsonValue, // object + String, // bun tag String, // sha ), + // git/github without integrity, tarball Dependency3( String, // identifier JsonValue, // object + String, // bun tag or sha + ), + + // symlink, folder, root + Dependency4( + String, // identifier + JsonValue, // object ), // Must be last! @@ -79,21 +100,28 @@ pub fn parse_bun_lock(path: &VirtualPath, output: &mut ParseLockOutput) -> AnyRe continue; } - BunLockPackage::Dependency1(id, _unknown, _data, integrity) => { + BunLockPackage::Dependency1(id, _registry, _data, integrity) => { + let Some((name, version)) = parse_name_and_version(id, "") else { + continue; + }; + + (name, version, Some(integrity)) + } + BunLockPackage::Dependency2(id, _data, _tag, integrity) => { let Some((name, version)) = parse_name_and_version(id, "") else { continue; }; (name, version, Some(integrity)) } - BunLockPackage::Dependency2(id, _data, integrity) => { + BunLockPackage::Dependency3(id, _data, integrity) => { let Some((name, version)) = parse_name_and_version(id, "") else { continue; }; (name, version, Some(integrity)) } - BunLockPackage::Dependency3(id, _data) => { + BunLockPackage::Dependency4(id, _data) => { let Some((name, version)) = parse_name_and_version(id, "") else { continue; }; @@ -121,3 +149,58 @@ pub fn parse_bun_lockb(path: &VirtualPath, output: &mut ParseLockOutput) -> AnyR parse_yarn_lock_content(content.stdout.trim(), output) } + +#[cfg(test)] +mod tests { + use super::*; + + fn parse(content: &str) -> BunLockPackage { + json::parse(content).unwrap() + } + + #[test] + fn parses_npm_entry() { + assert!(matches!( + parse(r#"["csstype@3.1.3", "", {}, "sha512-M1uQ=="]"#), + BunLockPackage::Dependency1(..) + )); + } + + // https://github.com/moonrepo/plugins/issues/174 + #[test] + fn parses_github_entry_with_integrity() { + assert!(matches!( + parse( + r#"["@portkey-ai/gateway@github:Portkey-AI/gateway#ca77129", { "dependencies": { "async-retry": "^1.3.3" }, "bin": "build/start-server.js" }, "Portkey-AI-gateway-ca77129", "sha512-71lq=="]"# + ), + BunLockPackage::Dependency2(..) + )); + } + + // https://github.com/moonrepo/moon/issues/2049 + #[test] + fn parses_github_entry_without_integrity() { + assert!(matches!( + parse( + r#"["uWebSockets.js@github:uNetworking/uWebSockets.js#6609a88", {}, "uNetworking-uWebSockets.js-6609a88"]"# + ), + BunLockPackage::Dependency3(..) + )); + } + + #[test] + fn parses_symlink_entry() { + assert!(matches!( + parse(r#"["local-lib@link:packages/local-lib", {}]"#), + BunLockPackage::Dependency4(..) + )); + } + + #[test] + fn parses_workspace_entry() { + assert!(matches!( + parse(r#"["a@workspace:packages/a"]"#), + BunLockPackage::Workspace(..) + )); + } +} diff --git a/toolchains/javascript/src/tier1.rs b/toolchains/javascript/src/tier1.rs index efa09db5..59a9ca4c 100644 --- a/toolchains/javascript/src/tier1.rs +++ b/toolchains/javascript/src/tier1.rs @@ -42,6 +42,8 @@ pub fn register_toolchain( "deno.jsonc".into(), // npm ".npmrc".into(), + // nub + "nub.jsonc".into(), // pnpm "pnpm-workspace.yaml".into(), ".pnpmfile.*".into(), @@ -60,6 +62,8 @@ pub fn register_toolchain( // npm "package-lock.json".into(), "npm-shrinkwrap.json".into(), + // nub + "nub.lock".into(), // pnpm "pnpm-lock.yaml".into(), // yarn @@ -78,9 +82,14 @@ pub fn register_toolchain( // npm "npm".into(), "npx".into(), + // nub + "nub".into(), + "nubx".into(), // pnpm "pnpm".into(), "pnpx".into(), + "pn".into(), + "pnx".into(), // yarn "yarn".into(), "yarnpkg".into() @@ -114,6 +123,7 @@ pub fn initialize_toolchain( JsonValue::String("bun".into()), JsonValue::String("deno".into()), JsonValue::String("npm".into()), + JsonValue::String("nub".into()), JsonValue::String("pnpm".into()), JsonValue::String("yarn".into()), ], @@ -157,6 +167,8 @@ fn detect_package_manager(root: &VirtualPath) -> AnyResult) -> FnResult) -> FnResult { paths_are_equal(dep_root, project_root.join(rel_path)) } - VersionProtocol::Requirement(req) if req == &VersionReq::STAR => { + VersionProtocol::Requirement(req) if req.comparators.is_empty() => { packages.contains_key(dep_name) } VersionProtocol::Workspace(_) => true, @@ -130,9 +130,7 @@ pub fn extend_project_graph( .extended_projects .insert(id.to_owned(), project_output); - if let Some(file) = manifest.path.virtual_path() { - output.input_files.push(file); - } + output.input_files.push(manifest.path.clone()); } Ok(Json(output)) @@ -147,8 +145,12 @@ fn gather_shared_paths( let mut current_dir = context.get_project_root(project); while current_dir != context.workspace_root { - if let Some(bin_dir) = current_dir.join("node_modules").join(".bin").real_path() { - paths.push(bin_dir); + if let Some(bin_dir) = current_dir + .join("node_modules") + .join(".bin") + .to_real_path()? + { + paths.push(bin_dir.to_path_buf()); } match current_dir.parent() { @@ -165,9 +167,9 @@ fn gather_shared_paths( .workspace_root .join("node_modules") .join(".bin") - .real_path() + .to_real_path()? { - paths.push(bin_dir); + paths.push(bin_dir.to_path_buf()); } Ok(()) @@ -201,10 +203,13 @@ pub fn define_requirements( ) -> FnResult> { let config = parse_toolchain_config_schema::(input.toolchain_config)?; - let mut output = DefineRequirementsOutput::default(); + let mut output = DefineRequirementsOutput { + for_setup_toolchain: true, + ..Default::default() + }; if let Some(package_manager) = config.package_manager { - if package_manager.is_for_node() { + if !package_manager.is_standalone() { output.requires.push("node".into()); } @@ -214,6 +219,18 @@ pub fn define_requirements( Ok(Json(output)) } +// Nub natively uses `nub.lock`, but respects the lockfiles of other +// package managers (for both resolution and layout), so treat them +// all as valid dependency roots for migration +const NUB_LOCK_NAMES: &[&str] = &[ + "nub.lock", + "package-lock.json", + "npm-shrinkwrap.json", + "pnpm-lock.yaml", + "yarn.lock", + "bun.lock", +]; + fn get_var_key(prefix: &str, root: &VirtualPath) -> String { format!("{prefix}:{}", root.to_string().trim_end_matches('/')) } @@ -251,7 +268,9 @@ fn extract_workspace_members_and_catalogs( catalogs = deno.extract_catalogs(); members = deno.workspace.map(|ws| ws.get_members().to_vec()); } - JavaScriptPackageManager::Pnpm => { + // Nub reads `pnpm-workspace.yaml` when operating on a pnpm + // workspace, otherwise `package.json` (handled below) + JavaScriptPackageManager::Nub | JavaScriptPackageManager::Pnpm => { let workspace_file = root.join("pnpm-workspace.yaml"); if workspace_file.exists() { @@ -320,7 +339,9 @@ pub fn locate_dependencies_root( let workspace_manifest_names = match package_manager { JavaScriptPackageManager::Deno => vec!["deno.json", "deno.jsonc", "package.json"], - JavaScriptPackageManager::Pnpm => vec!["pnpm-workspace.yaml", "package.json"], + JavaScriptPackageManager::Nub | JavaScriptPackageManager::Pnpm => { + vec!["pnpm-workspace.yaml", "package.json"] + } _ => vec!["package.json"], }; @@ -343,14 +364,15 @@ pub fn locate_dependencies_root( } } JavaScriptPackageManager::Npm => vec!["package-lock.json", "npm-shrinkwrap.json"], + JavaScriptPackageManager::Nub => NUB_LOCK_NAMES.to_vec(), JavaScriptPackageManager::Pnpm => vec!["pnpm-lock.yaml"], JavaScriptPackageManager::Yarn => vec!["yarn.lock"], }; // First attempt: find lock files if let Some(root) = locate_root_many(&input.starting_dir, &lock_names) { - output.root = root.virtual_path(); output.members = extract_workspace_members_and_catalogs(package_manager, &root)?; + output.root = Some(root); } // Second attempt: find workspace-compatible manifest files @@ -359,7 +381,7 @@ pub fn locate_dependencies_root( let mut found = false; if let Some(members) = extract_workspace_members_and_catalogs(package_manager, root)? { - output.root = root.virtual_path(); + output.root = Some(root.to_owned()); output.members = Some(members); found = true; } @@ -374,7 +396,7 @@ pub fn locate_dependencies_root( { extract_workspace_members_and_catalogs(package_manager, &root)?; - output.root = root.virtual_path(); + output.root = Some(root); } Ok(Json(output)) @@ -467,6 +489,39 @@ pub fn install_dependencies( cmd } + JavaScriptPackageManager::Nub => { + // `nub ci` installs strictly from the lockfile, but has no + // `--prod` flag, so fall back to `nub install` for production + // installs, which is frozen by default in CI anyways + let use_ci = env.ci + && !input.production + && NUB_LOCK_NAMES + .iter() + .any(|name| input.root.join(name).exists()); + + let mut cmd = if use_ci { + ExecCommandInput::new("nub", ["ci"]) + } else { + ExecCommandInput::new("nub", ["install"]) + }; + + if input.production { + cmd.args.push("--prod".into()); + } + + for package_name in input.packages { + cmd.args.push(if input.production { + "--filter-prod".into() + } else { + "--filter".into() + }); + + // https://nubjs.com/docs/install#nub-install + cmd.args.push(format!("{package_name}...")); + } + + cmd + } JavaScriptPackageManager::Pnpm => { let mut cmd = ExecCommandInput::new("pnpm", ["install"]); @@ -528,6 +583,13 @@ pub fn install_dependencies( .into(), ); } + JavaScriptPackageManager::Nub => { + output.dedupe_command = Some( + ExecCommandInput::new("nub", ["dedupe"]) + .cwd(input.root) + .into(), + ); + } JavaScriptPackageManager::Pnpm => { output.dedupe_command = Some(if package_manager_config.version_satisfies("<7.26.0") { @@ -588,7 +650,10 @@ pub fn parse_lock(Json(input): Json) -> FnResult { parse_package_lock_json(&input.path, &mut output)? } - Some("pnpm-lock.yaml") => parse_pnpm_lock_yaml(&input.path, &mut output)?, + // `nub.lock` uses the pnpm lockfile format + Some("nub.lock") | Some("pnpm-lock.yaml") => { + parse_pnpm_lock_yaml(&input.path, &mut output)? + } Some("yarn.lock") => parse_yarn_lock(&input.path, &mut output)?, _ => {} }; @@ -704,7 +769,7 @@ pub fn parse_manifest( } if let Some(version) = &manifest.version { - output.version = Some(version.to_owned()); + output.version = Some(Version::parse(version.to_string())?); } output.publishable = manifest.version.is_some() @@ -738,7 +803,7 @@ pub fn setup_environment( if package_path.exists() && let Some(version) = package_manager_config.version - && matches!(version, UnresolvedVersionSpec::Semantic(_)) + && matches!(version, UnresolvedVersionSpec::Version(_)) { let (op, file) = Operation::track("sync-package-manager", || { let mut package = PackageJson::load(package_path)?; @@ -748,7 +813,7 @@ pub fn setup_environment( output.operations.push(op); - if let Some(file) = file.and_then(|file| file.virtual_path()) { + if let Some(file) = file { output.changed_files.push(file); } } diff --git a/toolchains/javascript/tests/__fixtures__/lockfiles/bun/bun.lock b/toolchains/javascript/tests/__fixtures__/lockfiles/bun/bun.lock index f77a9680..2eecfb41 100644 --- a/toolchains/javascript/tests/__fixtures__/lockfiles/bun/bun.lock +++ b/toolchains/javascript/tests/__fixtures__/lockfiles/bun/bun.lock @@ -2,6 +2,10 @@ "lockfileVersion": 1, "workspaces": { "": { + "dependencies": { + "@portkey-ai/gateway": "git+https://github.com/Portkey-AI/gateway.git#v1.15.2", + "uWebSockets.js": "github:uNetworking/uWebSockets.js#6609a88", + }, "devDependencies": { "typescript": "^5.9.0", }, @@ -31,6 +35,8 @@ }, }, "packages": { + "@portkey-ai/gateway": ["@portkey-ai/gateway@github:Portkey-AI/gateway#ca77129", { "dependencies": { "react": "^19.1.0" }, "bin": "build/start-server.js" }, "Portkey-AI-gateway-ca77129", "sha512-71lqRjKGGxMSs4l6DdrzXHkGvitFdCsWxLbxYVfbYDTfhqOL0dJbmqjyrKObjSTbQqCXTh2XZQeI2Un0FN7pig=="], + "a": ["a@workspace:a"], "b": ["b@workspace:b"], @@ -48,5 +54,7 @@ "solid-js": ["solid-js@1.9.9", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.3.0", "seroval-plugins": "~1.3.0" } }, "sha512-A0ZBPJQldAeGCTW0YRYJmt7RCeh5rbFfPZ2aOttgYnctHE7HgKeHCBB/PVc2P7eOfmNXqMFFFoYYdm3S4dcbkA=="], "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="], + + "uWebSockets.js": ["uWebSockets.js@github:uNetworking/uWebSockets.js#6609a88", {}, "uNetworking-uWebSockets.js-6609a88"], } } diff --git a/toolchains/javascript/tests/__fixtures__/lockfiles/nub/nub.lock b/toolchains/javascript/tests/__fixtures__/lockfiles/nub/nub.lock new file mode 100644 index 00000000..52fd1b9e --- /dev/null +++ b/toolchains/javascript/tests/__fixtures__/lockfiles/nub/nub.lock @@ -0,0 +1,84 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + typescript: + specifier: ^5.9.0 + version: 5.9.2 + + a: + dependencies: + react: + specifier: ^19.1.0 + version: 19.1.1 + + b: + dependencies: + react: + specifier: ^19.1.0 + version: 19.1.1 + solid-js: + specifier: ~1.9.9 + version: 1.9.9 + + c: + dependencies: + a: + specifier: file:../a + version: link:../a + solid-js: + specifier: ~1.9.9 + version: 1.9.9 + +packages: + + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + + react@19.1.1: + resolution: {integrity: sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==} + engines: {node: '>=0.10.0'} + + seroval-plugins@1.3.2: + resolution: {integrity: sha512-0QvCV2lM3aj/U3YozDiVwx9zpH0q8A60CTWIv4Jszj/givcudPb48B+rkU5D51NJ0pTpweGMttHjboPa9/zoIQ==} + engines: {node: '>=10'} + peerDependencies: + seroval: ^1.0 + + seroval@1.3.2: + resolution: {integrity: sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==} + engines: {node: '>=10'} + + solid-js@1.9.9: + resolution: {integrity: sha512-A0ZBPJQldAeGCTW0YRYJmt7RCeh5rbFfPZ2aOttgYnctHE7HgKeHCBB/PVc2P7eOfmNXqMFFFoYYdm3S4dcbkA==} + + typescript@5.9.2: + resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==} + engines: {node: '>=14.17'} + hasBin: true + +snapshots: + + csstype@3.1.3: {} + + react@19.1.1: {} + + seroval-plugins@1.3.2(seroval@1.3.2): + dependencies: + seroval: 1.3.2 + + seroval@1.3.2: {} + + solid-js@1.9.9: + dependencies: + csstype: 3.1.3 + seroval: 1.3.2 + seroval-plugins: 1.3.2(seroval@1.3.2) + + typescript@5.9.2: {} diff --git a/toolchains/javascript/tests/package_json_test.rs b/toolchains/javascript/tests/package_json_test.rs index 7d132c7f..8a498653 100644 --- a/toolchains/javascript/tests/package_json_test.rs +++ b/toolchains/javascript/tests/package_json_test.rs @@ -17,7 +17,7 @@ mod package_json { sandbox.create_file("package.json", json); let config_path = sandbox.path().join("package.json"); - let mut file = PackageJson::load(VirtualPath::Real(config_path.clone())).unwrap(); + let mut file = PackageJson::load(VirtualPath::new(config_path.clone())).unwrap(); // Trigger dirty file.dirty.push("unknown".into()); @@ -32,7 +32,7 @@ mod package_json { #[test] fn adds() { let mut tsc = PackageJson { - path: VirtualPath::Real(PathBuf::from("/base/tsconfig.json")), + path: VirtualPath::new(PathBuf::from("/base/tsconfig.json")), ..Default::default() }; @@ -52,7 +52,7 @@ mod package_json { #[test] fn overwrites() { let mut tsc = PackageJson { - path: VirtualPath::Real(PathBuf::from("/base/tsconfig.json")), + path: VirtualPath::new(PathBuf::from("/base/tsconfig.json")), data: PackageJsonInner { dependencies: Some(BTreeMap::from_iter([ ("example".into(), VersionProtocol::from_str("*").unwrap()), @@ -89,7 +89,7 @@ mod package_json { #[test] fn doesnt_overwrite() { let mut tsc = PackageJson { - path: VirtualPath::Real(PathBuf::from("/base/tsconfig.json")), + path: VirtualPath::new(PathBuf::from("/base/tsconfig.json")), data: PackageJsonInner { dependencies: Some(BTreeMap::from_iter([( "example".into(), @@ -125,7 +125,7 @@ mod package_json { #[test] fn adds() { let mut tsc = PackageJson { - path: VirtualPath::Real(PathBuf::from("/base/tsconfig.json")), + path: VirtualPath::new(PathBuf::from("/base/tsconfig.json")), ..Default::default() }; @@ -145,7 +145,7 @@ mod package_json { #[test] fn overwrites() { let mut tsc = PackageJson { - path: VirtualPath::Real(PathBuf::from("/base/tsconfig.json")), + path: VirtualPath::new(PathBuf::from("/base/tsconfig.json")), data: PackageJsonInner { dev_dependencies: Some(BTreeMap::from_iter([ ("example".into(), VersionProtocol::from_str("*").unwrap()), @@ -182,7 +182,7 @@ mod package_json { #[test] fn doesnt_overwrite() { let mut tsc = PackageJson { - path: VirtualPath::Real(PathBuf::from("/base/tsconfig.json")), + path: VirtualPath::new(PathBuf::from("/base/tsconfig.json")), data: PackageJsonInner { dev_dependencies: Some(BTreeMap::from_iter([( "example".into(), @@ -218,7 +218,7 @@ mod package_json { #[test] fn adds() { let mut tsc = PackageJson { - path: VirtualPath::Real(PathBuf::from("/base/tsconfig.json")), + path: VirtualPath::new(PathBuf::from("/base/tsconfig.json")), ..Default::default() }; @@ -238,7 +238,7 @@ mod package_json { #[test] fn overwrites() { let mut tsc = PackageJson { - path: VirtualPath::Real(PathBuf::from("/base/tsconfig.json")), + path: VirtualPath::new(PathBuf::from("/base/tsconfig.json")), data: PackageJsonInner { peer_dependencies: Some(BTreeMap::from_iter([ ("example".into(), VersionProtocol::from_str("*").unwrap()), @@ -275,7 +275,7 @@ mod package_json { #[test] fn doesnt_overwrite() { let mut tsc = PackageJson { - path: VirtualPath::Real(PathBuf::from("/base/tsconfig.json")), + path: VirtualPath::new(PathBuf::from("/base/tsconfig.json")), data: PackageJsonInner { peer_dependencies: Some(BTreeMap::from_iter([( "example".into(), @@ -311,7 +311,7 @@ mod package_json { #[test] fn sets() { let mut tsc = PackageJson { - path: VirtualPath::Real(PathBuf::from("/base/tsconfig.json")), + path: VirtualPath::new(PathBuf::from("/base/tsconfig.json")), ..Default::default() }; @@ -325,7 +325,7 @@ mod package_json { #[test] fn doesnt_set_if_empty() { let mut tsc = PackageJson { - path: VirtualPath::Real(PathBuf::from("/base/tsconfig.json")), + path: VirtualPath::new(PathBuf::from("/base/tsconfig.json")), ..Default::default() }; @@ -339,7 +339,7 @@ mod package_json { #[test] fn unsets_if_empty() { let mut tsc = PackageJson { - path: VirtualPath::Real(PathBuf::from("/base/tsconfig.json")), + path: VirtualPath::new(PathBuf::from("/base/tsconfig.json")), ..Default::default() }; @@ -353,7 +353,7 @@ mod package_json { #[test] fn doesnt_set_if_same_value() { let mut tsc = PackageJson { - path: VirtualPath::Real(PathBuf::from("/base/tsconfig.json")), + path: VirtualPath::new(PathBuf::from("/base/tsconfig.json")), ..Default::default() }; diff --git a/toolchains/javascript/tests/tier1_sync_test.rs b/toolchains/javascript/tests/tier1_sync_test.rs index 7e0ff905..00c8f9d7 100644 --- a/toolchains/javascript/tests/tier1_sync_test.rs +++ b/toolchains/javascript/tests/tier1_sync_test.rs @@ -4,7 +4,6 @@ use moon_pdk_api::*; use moon_pdk_test_utils::create_moon_sandbox; use serde_json::json; use starbase_sandbox::assert_snapshot; -use std::path::PathBuf; mod javascript_toolchain_tier1 { use super::*; @@ -189,7 +188,7 @@ mod javascript_toolchain_tier1 { assert!(!output.operations.is_empty()); assert_eq!( output.changed_files, - vec![PathBuf::from("/workspace/base/package.json")] + vec![VirtualPath::new("/workspace/base/package.json")] ); assert_snapshot!( format!("format_{format}"), diff --git a/toolchains/javascript/tests/tier1_test.rs b/toolchains/javascript/tests/tier1_test.rs index 814e812a..5117ab9c 100644 --- a/toolchains/javascript/tests/tier1_test.rs +++ b/toolchains/javascript/tests/tier1_test.rs @@ -2,7 +2,6 @@ use moon_config::DockerPruneConfig; use moon_pdk_api::*; use moon_pdk_test_utils::create_empty_moon_sandbox; use starbase_utils::json::JsonValue; -use std::path::PathBuf; mod javascript_toolchain_tier1 { use super::*; @@ -112,6 +111,40 @@ mod javascript_toolchain_tier1 { ); } + #[tokio::test(flavor = "multi_thread")] + async fn detects_nub_via_lockfile() { + let sandbox = create_empty_moon_sandbox(); + sandbox.create_file("nub.lock", ""); + + let plugin = sandbox.create_toolchain("javascript").await; + + let output = plugin + .initialize_toolchain(InitializeToolchainInput::default()) + .await; + + assert_eq!( + output.default_settings.get("packageManager").unwrap(), + &JsonValue::String("nub".into()) + ); + } + + #[tokio::test(flavor = "multi_thread")] + async fn detects_nub_via_package_json() { + let sandbox = create_empty_moon_sandbox(); + sandbox.create_file("package.json", r#"{ "packageManager": "nub@0.7.0" }"#); + + let plugin = sandbox.create_toolchain("javascript").await; + + let output = plugin + .initialize_toolchain(InitializeToolchainInput::default()) + .await; + + assert_eq!( + output.default_settings.get("packageManager").unwrap(), + &JsonValue::String("nub".into()) + ); + } + #[tokio::test(flavor = "multi_thread")] async fn detects_pnpm_via_lockfile() { let sandbox = create_empty_moon_sandbox(); @@ -198,7 +231,7 @@ mod javascript_toolchain_tier1 { delete_vendor_directories: true, ..Default::default() }, - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), ..Default::default() }) .await; @@ -219,7 +252,7 @@ mod javascript_toolchain_tier1 { delete_vendor_directories: false, ..Default::default() }, - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), ..Default::default() }) .await; @@ -240,7 +273,7 @@ mod javascript_toolchain_tier1 { delete_vendor_directories: true, ..Default::default() }, - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), ..Default::default() }) .await; @@ -249,7 +282,7 @@ mod javascript_toolchain_tier1 { assert_eq!( output.changed_files, - [PathBuf::from("/workspace/node_modules")] + [VirtualPath::new("/workspace/node_modules")] ); } } diff --git a/toolchains/javascript/tests/tier2_env_test.rs b/toolchains/javascript/tests/tier2_env_test.rs index 00fd85aa..4032a490 100644 --- a/toolchains/javascript/tests/tier2_env_test.rs +++ b/toolchains/javascript/tests/tier2_env_test.rs @@ -2,7 +2,6 @@ use moon_pdk_api::*; use moon_pdk_test_utils::create_moon_sandbox; use serde_json::json; use std::fs; -use std::path::PathBuf; mod javascript_toolchain_tier2 { use super::*; @@ -22,7 +21,7 @@ mod javascript_toolchain_tier2 { let output = plugin .setup_environment(SetupEnvironmentInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "syncPackageManagerField": false, "packageManager": "npm" @@ -52,7 +51,7 @@ mod javascript_toolchain_tier2 { let output = plugin .setup_environment(SetupEnvironmentInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "syncPackageManagerField": true, "packageManager": null @@ -82,7 +81,7 @@ mod javascript_toolchain_tier2 { let output = plugin .setup_environment(SetupEnvironmentInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "syncPackageManagerField": true, "packageManager": "npm" @@ -112,7 +111,7 @@ mod javascript_toolchain_tier2 { let output = plugin .setup_environment(SetupEnvironmentInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "syncPackageManagerField": true, "packageManager": "npm" @@ -142,7 +141,7 @@ mod javascript_toolchain_tier2 { let output = plugin .setup_environment(SetupEnvironmentInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "syncPackageManagerField": true, "packageManager": "bun" @@ -172,7 +171,7 @@ mod javascript_toolchain_tier2 { let output = plugin .setup_environment(SetupEnvironmentInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "syncPackageManagerField": true, "packageManager": "deno" @@ -190,6 +189,38 @@ mod javascript_toolchain_tier2 { ); } + #[tokio::test(flavor = "multi_thread")] + async fn sets_field_for_nub() { + let mut sandbox = create_moon_sandbox("files"); + + sandbox + .host_funcs + .mock_load_toolchain_config(|_, _| json!({ "version": "1.2.3" })); + + let plugin = sandbox.create_toolchain("javascript").await; + + let output = plugin + .setup_environment(SetupEnvironmentInput { + root: VirtualPath::new(sandbox.path()), + toolchain_config: json!({ + "syncPackageManagerField": true, + "packageManager": "nub" + }), + ..Default::default() + }) + .await; + + assert_eq!( + output.changed_files, + [VirtualPath::new("/workspace/package.json")] + ); + assert!( + fs::read_to_string(sandbox.path().join("package.json")) + .unwrap() + .contains(r#""packageManager": "nub@1.2.3""#) + ); + } + #[tokio::test(flavor = "multi_thread")] async fn sets_field() { let mut sandbox = create_moon_sandbox("files"); @@ -202,7 +233,7 @@ mod javascript_toolchain_tier2 { let output = plugin .setup_environment(SetupEnvironmentInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "syncPackageManagerField": true, "packageManager": "npm" @@ -213,7 +244,7 @@ mod javascript_toolchain_tier2 { assert_eq!( output.changed_files, - [PathBuf::from("/workspace/package.json")] + [VirtualPath::new("/workspace/package.json")] ); assert!( fs::read_to_string(sandbox.path().join("package.json")) diff --git a/toolchains/javascript/tests/tier2_test.rs b/toolchains/javascript/tests/tier2_test.rs index e1a08cf7..d857b7b0 100644 --- a/toolchains/javascript/tests/tier2_test.rs +++ b/toolchains/javascript/tests/tier2_test.rs @@ -9,7 +9,6 @@ use moon_target::Target; use serde_json::json; use starbase_utils::fs; use std::collections::BTreeMap; -use std::path::PathBuf; mod javascript_toolchain_tier2 { use super::*; @@ -68,14 +67,14 @@ mod javascript_toolchain_tier2 { ]) ); - output.input_files.sort(); + output.input_files.sort_by(|a, d| a.cmp(d)); assert_eq!( output.input_files, [ - PathBuf::from("/workspace/a/package.json"), - PathBuf::from("/workspace/b/package.json"), - PathBuf::from("/workspace/c/package.json"), + VirtualPath::new("/workspace/a/package.json"), + VirtualPath::new("/workspace/b/package.json"), + VirtualPath::new("/workspace/c/package.json"), ] ); } @@ -103,7 +102,7 @@ mod javascript_toolchain_tier2 { assert_eq!( output.input_files, - [PathBuf::from("/workspace/a/package.json")] + [VirtualPath::new("/workspace/a/package.json")] ); } @@ -457,7 +456,7 @@ mod javascript_toolchain_tier2 { let output = plugin .locate_dependencies_root(LocateDependenciesRootInput { - starting_dir: VirtualPath::Real(sandbox.path().into()), + starting_dir: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "packageManager": "npm" }), @@ -476,7 +475,7 @@ mod javascript_toolchain_tier2 { let output = plugin .locate_dependencies_root(LocateDependenciesRootInput { - starting_dir: VirtualPath::Real(sandbox.path().join("package")), + starting_dir: VirtualPath::new(sandbox.path().join("package")), toolchain_config: json!({ "packageManager": null }), @@ -495,7 +494,7 @@ mod javascript_toolchain_tier2 { let output = plugin .locate_dependencies_root(LocateDependenciesRootInput { - starting_dir: VirtualPath::Real(sandbox.path().join("package/nested")), + starting_dir: VirtualPath::new(sandbox.path().join("package/nested")), toolchain_config: json!({ "packageManager": "npm" }), @@ -504,7 +503,7 @@ mod javascript_toolchain_tier2 { .await; assert!(output.members.is_none()); - assert_eq!(output.root.unwrap(), PathBuf::from("/workspace/package")); + assert_eq!(output.root.unwrap(), VirtualPath::new("/workspace/package")); } #[tokio::test(flavor = "multi_thread")] @@ -516,7 +515,7 @@ mod javascript_toolchain_tier2 { let output = plugin .locate_dependencies_root(LocateDependenciesRootInput { - starting_dir: VirtualPath::Real(sandbox.path().join("package/nested")), + starting_dir: VirtualPath::new(sandbox.path().join("package/nested")), toolchain_config: json!({ "packageManager": "bun" }), @@ -525,7 +524,7 @@ mod javascript_toolchain_tier2 { .await; assert!(output.members.is_none()); - assert_eq!(output.root.unwrap(), PathBuf::from("/workspace/package")); + assert_eq!(output.root.unwrap(), VirtualPath::new("/workspace/package")); } #[tokio::test(flavor = "multi_thread")] @@ -537,7 +536,7 @@ mod javascript_toolchain_tier2 { let output = plugin .locate_dependencies_root(LocateDependenciesRootInput { - starting_dir: VirtualPath::Real(sandbox.path().join("package/nested")), + starting_dir: VirtualPath::new(sandbox.path().join("package/nested")), toolchain_config: json!({ "packageManager": "deno" }), @@ -546,7 +545,7 @@ mod javascript_toolchain_tier2 { .await; assert!(output.members.is_none()); - assert_eq!(output.root.unwrap(), PathBuf::from("/workspace/package")); + assert_eq!(output.root.unwrap(), VirtualPath::new("/workspace/package")); } #[tokio::test(flavor = "multi_thread")] @@ -564,7 +563,7 @@ mod javascript_toolchain_tier2 { let output = plugin .locate_dependencies_root(LocateDependenciesRootInput { - starting_dir: VirtualPath::Real(sandbox.path().join("package/nested/app")), + starting_dir: VirtualPath::new(sandbox.path().join("package/nested/app")), toolchain_config: json!({ "packageManager": "deno" }), @@ -573,7 +572,7 @@ mod javascript_toolchain_tier2 { .await; assert!(output.members.is_none()); - assert_eq!(output.root.unwrap(), PathBuf::from("/workspace/package")); + assert_eq!(output.root.unwrap(), VirtualPath::new("/workspace/package")); } #[tokio::test(flavor = "multi_thread")] @@ -585,7 +584,7 @@ mod javascript_toolchain_tier2 { let output = plugin .locate_dependencies_root(LocateDependenciesRootInput { - starting_dir: VirtualPath::Real(sandbox.path().join("package/nested")), + starting_dir: VirtualPath::new(sandbox.path().join("package/nested")), toolchain_config: json!({ "packageManager": "npm" }), @@ -594,7 +593,49 @@ mod javascript_toolchain_tier2 { .await; assert!(output.members.is_none()); - assert_eq!(output.root.unwrap(), PathBuf::from("/workspace/package")); + assert_eq!(output.root.unwrap(), VirtualPath::new("/workspace/package")); + } + + #[tokio::test(flavor = "multi_thread")] + async fn finds_package_with_nub_lock() { + let sandbox = create_moon_sandbox("locate"); + sandbox.create_file("package/nub.lock", ""); + + let plugin = sandbox.create_toolchain("javascript").await; + + let output = plugin + .locate_dependencies_root(LocateDependenciesRootInput { + starting_dir: VirtualPath::new(sandbox.path().join("package/nested")), + toolchain_config: json!({ + "packageManager": "nub" + }), + ..Default::default() + }) + .await; + + assert!(output.members.is_none()); + assert_eq!(output.root.unwrap(), VirtualPath::new("/workspace/package")); + } + + #[tokio::test(flavor = "multi_thread")] + async fn finds_package_with_other_pm_lock_when_using_nub() { + let sandbox = create_moon_sandbox("locate"); + sandbox.create_file("package/pnpm-lock.yaml", ""); + + let plugin = sandbox.create_toolchain("javascript").await; + + let output = plugin + .locate_dependencies_root(LocateDependenciesRootInput { + starting_dir: VirtualPath::new(sandbox.path().join("package/nested")), + toolchain_config: json!({ + "packageManager": "nub" + }), + ..Default::default() + }) + .await; + + assert!(output.members.is_none()); + assert_eq!(output.root.unwrap(), VirtualPath::new("/workspace/package")); } #[tokio::test(flavor = "multi_thread")] @@ -606,7 +647,7 @@ mod javascript_toolchain_tier2 { let output = plugin .locate_dependencies_root(LocateDependenciesRootInput { - starting_dir: VirtualPath::Real(sandbox.path().join("package/nested")), + starting_dir: VirtualPath::new(sandbox.path().join("package/nested")), toolchain_config: json!({ "packageManager": "pnpm" }), @@ -615,7 +656,7 @@ mod javascript_toolchain_tier2 { .await; assert!(output.members.is_none()); - assert_eq!(output.root.unwrap(), PathBuf::from("/workspace/package")); + assert_eq!(output.root.unwrap(), VirtualPath::new("/workspace/package")); } #[tokio::test(flavor = "multi_thread")] @@ -627,7 +668,7 @@ mod javascript_toolchain_tier2 { let output = plugin .locate_dependencies_root(LocateDependenciesRootInput { - starting_dir: VirtualPath::Real(sandbox.path().join("package/nested")), + starting_dir: VirtualPath::new(sandbox.path().join("package/nested")), toolchain_config: json!({ "packageManager": "yarn" }), @@ -636,7 +677,7 @@ mod javascript_toolchain_tier2 { .await; assert!(output.members.is_none()); - assert_eq!(output.root.unwrap(), PathBuf::from("/workspace/package")); + assert_eq!(output.root.unwrap(), VirtualPath::new("/workspace/package")); } #[tokio::test(flavor = "multi_thread")] @@ -646,7 +687,7 @@ mod javascript_toolchain_tier2 { let output = plugin .locate_dependencies_root(LocateDependenciesRootInput { - starting_dir: VirtualPath::Real( + starting_dir: VirtualPath::new( sandbox.path().join("workspace/packages/a/nested"), ), toolchain_config: json!({ @@ -657,7 +698,10 @@ mod javascript_toolchain_tier2 { .await; assert_eq!(output.members.unwrap(), ["packages/*"]); - assert_eq!(output.root.unwrap(), PathBuf::from("/workspace/workspace")); + assert_eq!( + output.root.unwrap(), + VirtualPath::new("/workspace/workspace") + ); } #[tokio::test(flavor = "multi_thread")] @@ -669,7 +713,7 @@ mod javascript_toolchain_tier2 { let output = plugin .locate_dependencies_root(LocateDependenciesRootInput { - starting_dir: VirtualPath::Real( + starting_dir: VirtualPath::new( sandbox.path().join("workspace/packages/a/nested"), ), toolchain_config: json!({ @@ -680,7 +724,10 @@ mod javascript_toolchain_tier2 { .await; assert_eq!(output.members.unwrap(), ["packages/*"]); - assert_eq!(output.root.unwrap(), PathBuf::from("/workspace/workspace")); + assert_eq!( + output.root.unwrap(), + VirtualPath::new("/workspace/workspace") + ); } #[tokio::test(flavor = "multi_thread")] @@ -692,7 +739,7 @@ mod javascript_toolchain_tier2 { let output = plugin .locate_dependencies_root(LocateDependenciesRootInput { - starting_dir: VirtualPath::Real( + starting_dir: VirtualPath::new( sandbox.path().join("workspace/packages/a/nested"), ), toolchain_config: json!({ @@ -703,7 +750,10 @@ mod javascript_toolchain_tier2 { .await; assert_eq!(output.members.unwrap(), ["packages/*"]); - assert_eq!(output.root.unwrap(), PathBuf::from("/workspace/workspace")); + assert_eq!( + output.root.unwrap(), + VirtualPath::new("/workspace/workspace") + ); } #[tokio::test(flavor = "multi_thread")] @@ -723,7 +773,7 @@ mod javascript_toolchain_tier2 { let output = plugin .locate_dependencies_root(LocateDependenciesRootInput { - starting_dir: VirtualPath::Real( + starting_dir: VirtualPath::new( sandbox.path().join("workspace/packages/a/nested"), ), toolchain_config: json!({ @@ -734,7 +784,10 @@ mod javascript_toolchain_tier2 { .await; assert_eq!(output.members.unwrap(), ["packages/*"]); - assert_eq!(output.root.unwrap(), PathBuf::from("/workspace/workspace")); + assert_eq!( + output.root.unwrap(), + VirtualPath::new("/workspace/workspace") + ); } #[tokio::test(flavor = "multi_thread")] @@ -746,7 +799,7 @@ mod javascript_toolchain_tier2 { let output = plugin .locate_dependencies_root(LocateDependenciesRootInput { - starting_dir: VirtualPath::Real( + starting_dir: VirtualPath::new( sandbox.path().join("workspace/packages/a/nested"), ), toolchain_config: json!({ @@ -757,7 +810,10 @@ mod javascript_toolchain_tier2 { .await; assert_eq!(output.members.unwrap(), ["packages/*"]); - assert_eq!(output.root.unwrap(), PathBuf::from("/workspace/workspace")); + assert_eq!( + output.root.unwrap(), + VirtualPath::new("/workspace/workspace") + ); } #[tokio::test(flavor = "multi_thread")] @@ -769,7 +825,7 @@ mod javascript_toolchain_tier2 { let output = plugin .locate_dependencies_root(LocateDependenciesRootInput { - starting_dir: VirtualPath::Real( + starting_dir: VirtualPath::new( sandbox.path().join("workspace/packages/a/nested"), ), toolchain_config: json!({ @@ -780,7 +836,62 @@ mod javascript_toolchain_tier2 { .await; assert_eq!(output.members.unwrap(), ["packages/*"]); - assert_eq!(output.root.unwrap(), PathBuf::from("/workspace/workspace")); + assert_eq!( + output.root.unwrap(), + VirtualPath::new("/workspace/workspace") + ); + } + + #[tokio::test(flavor = "multi_thread")] + async fn finds_workspace_with_nub_lock() { + let sandbox = create_moon_sandbox("locate"); + sandbox.create_file("workspace/nub.lock", ""); + + let plugin = sandbox.create_toolchain("javascript").await; + + let output = plugin + .locate_dependencies_root(LocateDependenciesRootInput { + starting_dir: VirtualPath::new( + sandbox.path().join("workspace/packages/a/nested"), + ), + toolchain_config: json!({ + "packageManager": "nub" + }), + ..Default::default() + }) + .await; + + assert_eq!(output.members.unwrap(), ["packages/*"]); + assert_eq!( + output.root.unwrap(), + VirtualPath::new("/workspace/workspace") + ); + } + + #[tokio::test(flavor = "multi_thread")] + async fn finds_workspace_with_pnpm_workspace_when_using_nub() { + let sandbox = create_moon_sandbox("locate"); + sandbox.create_file("workspace/pnpm-workspace.yaml", "packages: ['apps/*']"); + + let plugin = sandbox.create_toolchain("javascript").await; + + let output = plugin + .locate_dependencies_root(LocateDependenciesRootInput { + starting_dir: VirtualPath::new( + sandbox.path().join("workspace/packages/a/nested"), + ), + toolchain_config: json!({ + "packageManager": "nub" + }), + ..Default::default() + }) + .await; + + assert_eq!(output.members.unwrap(), ["apps/*"]); + assert_eq!( + output.root.unwrap(), + VirtualPath::new("/workspace/workspace") + ); } #[tokio::test(flavor = "multi_thread")] @@ -792,7 +903,7 @@ mod javascript_toolchain_tier2 { let output = plugin .locate_dependencies_root(LocateDependenciesRootInput { - starting_dir: VirtualPath::Real( + starting_dir: VirtualPath::new( sandbox.path().join("workspace/packages/a/nested"), ), toolchain_config: json!({ @@ -803,7 +914,10 @@ mod javascript_toolchain_tier2 { .await; assert_eq!(output.members.unwrap(), ["apps/*"]); - assert_eq!(output.root.unwrap(), PathBuf::from("/workspace/workspace")); + assert_eq!( + output.root.unwrap(), + VirtualPath::new("/workspace/workspace") + ); } #[tokio::test(flavor = "multi_thread")] @@ -815,7 +929,7 @@ mod javascript_toolchain_tier2 { let output = plugin .locate_dependencies_root(LocateDependenciesRootInput { - starting_dir: VirtualPath::Real( + starting_dir: VirtualPath::new( sandbox.path().join("workspace/packages/a/nested"), ), toolchain_config: json!({ @@ -826,7 +940,10 @@ mod javascript_toolchain_tier2 { .await; assert_eq!(output.members.unwrap(), ["packages/*"]); - assert_eq!(output.root.unwrap(), PathBuf::from("/workspace/workspace")); + assert_eq!( + output.root.unwrap(), + VirtualPath::new("/workspace/workspace") + ); } #[tokio::test(flavor = "multi_thread")] @@ -838,7 +955,7 @@ mod javascript_toolchain_tier2 { let output = plugin .locate_dependencies_root(LocateDependenciesRootInput { - starting_dir: VirtualPath::Real( + starting_dir: VirtualPath::new( sandbox.path().join("workspace/packages/a/nested"), ), toolchain_config: json!({ @@ -849,7 +966,10 @@ mod javascript_toolchain_tier2 { .await; assert_eq!(output.members.unwrap(), ["packages/*"]); - assert_eq!(output.root.unwrap(), PathBuf::from("/workspace/workspace")); + assert_eq!( + output.root.unwrap(), + VirtualPath::new("/workspace/workspace") + ); } } @@ -863,7 +983,7 @@ mod javascript_toolchain_tier2 { let output = plugin .install_dependencies(InstallDependenciesInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "packageManager": null }), @@ -885,7 +1005,7 @@ mod javascript_toolchain_tier2 { let output = plugin .install_dependencies(InstallDependenciesInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "packageManager": "bun", "dedupeOnLockfileChange": true @@ -911,7 +1031,7 @@ mod javascript_toolchain_tier2 { let output = plugin .install_dependencies(InstallDependenciesInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "packageManager": "bun", "dedupeOnLockfileChange": true @@ -954,7 +1074,7 @@ mod javascript_toolchain_tier2 { let output = plugin .install_dependencies(InstallDependenciesInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "packageManager": "bun", "dedupeOnLockfileChange": true @@ -984,7 +1104,7 @@ mod javascript_toolchain_tier2 { let output = plugin .install_dependencies(InstallDependenciesInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "packageManager": "deno", "dedupeOnLockfileChange": true @@ -1015,7 +1135,7 @@ mod javascript_toolchain_tier2 { let output = plugin .install_dependencies(InstallDependenciesInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "packageManager": "deno", "dedupeOnLockfileChange": true @@ -1049,7 +1169,7 @@ mod javascript_toolchain_tier2 { let output = plugin .install_dependencies(InstallDependenciesInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "packageManager": "deno", }), @@ -1087,7 +1207,7 @@ mod javascript_toolchain_tier2 { // No lockfile — stays as `deno install` let output = plugin .install_dependencies(InstallDependenciesInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "packageManager": "deno", }), @@ -1107,7 +1227,7 @@ mod javascript_toolchain_tier2 { let output = plugin .install_dependencies(InstallDependenciesInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "packageManager": "deno", }), @@ -1145,7 +1265,7 @@ mod javascript_toolchain_tier2 { let output = plugin .install_dependencies(InstallDependenciesInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "packageManager": "deno", }), @@ -1174,7 +1294,7 @@ mod javascript_toolchain_tier2 { let output = plugin .install_dependencies(InstallDependenciesInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "packageManager": "deno", "dedupeOnLockfileChange": true @@ -1204,7 +1324,7 @@ mod javascript_toolchain_tier2 { let output = plugin .install_dependencies(InstallDependenciesInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "packageManager": "npm", "dedupeOnLockfileChange": true @@ -1236,7 +1356,7 @@ mod javascript_toolchain_tier2 { let output = plugin .install_dependencies(InstallDependenciesInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "packageManager": "npm", "dedupeOnLockfileChange": true @@ -1285,7 +1405,7 @@ mod javascript_toolchain_tier2 { let output = plugin .install_dependencies(InstallDependenciesInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "packageManager": "npm", "dedupeOnLockfileChange": true @@ -1325,7 +1445,7 @@ mod javascript_toolchain_tier2 { // Doesn't work without the lockfile let output = plugin .install_dependencies(InstallDependenciesInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "packageManager": "npm", }), @@ -1346,7 +1466,7 @@ mod javascript_toolchain_tier2 { let output = plugin .install_dependencies(InstallDependenciesInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "packageManager": "npm", }), @@ -1364,6 +1484,216 @@ mod javascript_toolchain_tier2 { } } + mod nub { + use super::*; + + #[tokio::test(flavor = "multi_thread")] + async fn default_commands() { + let sandbox = create_empty_moon_sandbox(); + let plugin = sandbox.create_toolchain("javascript").await; + + let output = plugin + .install_dependencies(InstallDependenciesInput { + root: VirtualPath::new(sandbox.path()), + toolchain_config: json!({ + "packageManager": "nub", + "dedupeOnLockfileChange": true + }), + ..Default::default() + }) + .await; + + assert_eq!( + output.install_command.unwrap(), + ExecCommand::new( + ExecCommandInput::new("nub", ["install"]) + .cwd(plugin.plugin.to_virtual_path(sandbox.path())) + ) + ); + + assert_eq!( + output.dedupe_command.unwrap(), + ExecCommand::new( + ExecCommandInput::new("nub", ["dedupe"]) + .cwd(plugin.plugin.to_virtual_path(sandbox.path())) + ) + ); + } + + #[tokio::test(flavor = "multi_thread")] + async fn focused_commands() { + let sandbox = create_empty_moon_sandbox(); + let plugin = sandbox.create_toolchain("javascript").await; + + let output = plugin + .install_dependencies(InstallDependenciesInput { + root: VirtualPath::new(sandbox.path()), + toolchain_config: json!({ + "packageManager": "nub", + "dedupeOnLockfileChange": true + }), + packages: vec!["foo".into(), "@scope/bar".into()], + production: true, + ..Default::default() + }) + .await; + + assert_eq!( + output.install_command.unwrap(), + ExecCommand::new( + ExecCommandInput::new( + "nub", + [ + "install", + "--prod", + "--filter-prod", + "foo...", + "--filter-prod", + "@scope/bar..." + ] + ) + .cwd(plugin.plugin.to_virtual_path(sandbox.path())) + ) + ); + } + + #[tokio::test(flavor = "multi_thread")] + async fn inherits_args_from_nub_toolchain() { + let mut sandbox = create_empty_moon_sandbox(); + + sandbox + .host_funcs + .mock_load_toolchain_config(|_, _| json!({ "installArgs": ["-a", "b", "--c"]})); + + let plugin = sandbox.create_toolchain("javascript").await; + + let output = plugin + .install_dependencies(InstallDependenciesInput { + root: VirtualPath::new(sandbox.path()), + toolchain_config: json!({ + "packageManager": "nub", + "dedupeOnLockfileChange": true + }), + ..Default::default() + }) + .await; + + assert_eq!( + output.install_command.unwrap(), + ExecCommand::new( + ExecCommandInput::new("nub", ["install", "-a", "b", "--c"]) + .cwd(plugin.plugin.to_virtual_path(sandbox.path())) + ) + ); + } + + #[tokio::test(flavor = "multi_thread")] + async fn switches_to_ci_in_ci() { + let sandbox = create_empty_moon_sandbox(); + let plugin = sandbox + .create_toolchain_with_config("javascript", |cfg| { + cfg.host_environment(HostEnvironment { + ci: true, + ..Default::default() + }); + }) + .await; + + // Doesn't work without the lockfile + let output = plugin + .install_dependencies(InstallDependenciesInput { + root: VirtualPath::new(sandbox.path()), + toolchain_config: json!({ + "packageManager": "nub", + }), + ..Default::default() + }) + .await; + + assert_eq!( + output.install_command.unwrap(), + ExecCommand::new( + ExecCommandInput::new("nub", ["install"]) + .cwd(plugin.plugin.to_virtual_path(sandbox.path())) + ) + ); + + // Now works! + sandbox.create_file("nub.lock", ""); + + let output = plugin + .install_dependencies(InstallDependenciesInput { + root: VirtualPath::new(sandbox.path()), + toolchain_config: json!({ + "packageManager": "nub", + }), + ..Default::default() + }) + .await; + + assert_eq!( + output.install_command.unwrap(), + ExecCommand::new( + ExecCommandInput::new("nub", ["ci"]) + .cwd(plugin.plugin.to_virtual_path(sandbox.path())) + ) + ); + + // But not for production, since `nub ci` has no `--prod` + let output = plugin + .install_dependencies(InstallDependenciesInput { + root: VirtualPath::new(sandbox.path()), + toolchain_config: json!({ + "packageManager": "nub", + }), + production: true, + ..Default::default() + }) + .await; + + assert_eq!( + output.install_command.unwrap(), + ExecCommand::new( + ExecCommandInput::new("nub", ["install", "--prod"]) + .cwd(plugin.plugin.to_virtual_path(sandbox.path())) + ) + ); + } + + #[tokio::test(flavor = "multi_thread")] + async fn switches_to_ci_with_other_pm_lock() { + let sandbox = create_empty_moon_sandbox(); + sandbox.create_file("pnpm-lock.yaml", ""); + + let plugin = sandbox + .create_toolchain_with_config("javascript", |cfg| { + cfg.host_environment(HostEnvironment { + ci: true, + ..Default::default() + }); + }) + .await; + + let output = plugin + .install_dependencies(InstallDependenciesInput { + root: VirtualPath::new(sandbox.path()), + toolchain_config: json!({ + "packageManager": "nub", + }), + ..Default::default() + }) + .await; + + assert_eq!( + output.install_command.unwrap(), + ExecCommand::new( + ExecCommandInput::new("nub", ["ci"]) + .cwd(plugin.plugin.to_virtual_path(sandbox.path())) + ) + ); + } + } + mod pnpm { use super::*; @@ -1374,7 +1704,7 @@ mod javascript_toolchain_tier2 { let output = plugin .install_dependencies(InstallDependenciesInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "packageManager": "pnpm", "dedupeOnLockfileChange": true @@ -1399,7 +1729,7 @@ mod javascript_toolchain_tier2 { let output = plugin .install_dependencies(InstallDependenciesInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "packageManager": "pnpm", "dedupeOnLockfileChange": true @@ -1441,7 +1771,7 @@ mod javascript_toolchain_tier2 { let output = plugin .install_dependencies(InstallDependenciesInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "packageManager": "pnpm", "dedupeOnLockfileChange": true @@ -1471,7 +1801,7 @@ mod javascript_toolchain_tier2 { let output = plugin .install_dependencies(InstallDependenciesInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "packageManager": "pnpm", "dedupeOnLockfileChange": true @@ -1510,7 +1840,7 @@ mod javascript_toolchain_tier2 { let output = plugin .install_dependencies(InstallDependenciesInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "packageManager": "pnpm", "dedupeOnLockfileChange": true, @@ -1539,7 +1869,7 @@ mod javascript_toolchain_tier2 { let output = plugin .install_dependencies(InstallDependenciesInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "packageManager": "yarn", "dedupeOnLockfileChange": true @@ -1564,7 +1894,7 @@ mod javascript_toolchain_tier2 { let output = plugin .install_dependencies(InstallDependenciesInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "packageManager": "yarn", "dedupeOnLockfileChange": true @@ -1596,7 +1926,7 @@ mod javascript_toolchain_tier2 { let output = plugin .install_dependencies(InstallDependenciesInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "packageManager": "yarn", "dedupeOnLockfileChange": true @@ -1631,7 +1961,7 @@ mod javascript_toolchain_tier2 { let output = plugin .install_dependencies(InstallDependenciesInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "packageManager": "yarn", "dedupeOnLockfileChange": true @@ -1661,7 +1991,7 @@ mod javascript_toolchain_tier2 { let output = plugin .install_dependencies(InstallDependenciesInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "packageManager": "pnpm", "dedupeOnLockfileChange": true @@ -1700,7 +2030,7 @@ mod javascript_toolchain_tier2 { let output = plugin .install_dependencies(InstallDependenciesInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "packageManager": "yarn", "dedupeOnLockfileChange": true, @@ -1730,7 +2060,7 @@ mod javascript_toolchain_tier2 { let output = plugin .parse_manifest(ParseManifestInput { - path: VirtualPath::Real(sandbox.path().join("package.json")), + path: VirtualPath::new(sandbox.path().join("package.json")), ..Default::default() }) .await; @@ -1771,7 +2101,7 @@ mod javascript_toolchain_tier2 { let output = plugin .parse_manifest(ParseManifestInput { - path: VirtualPath::Real(sandbox.path().join("package/package.json")), + path: VirtualPath::new(sandbox.path().join("package/package.json")), ..Default::default() }) .await; @@ -1833,7 +2163,7 @@ mod javascript_toolchain_tier2 { // This must be ran to extract the catalogs plugin .locate_dependencies_root(LocateDependenciesRootInput { - starting_dir: VirtualPath::Real(sandbox.path().join("package")), + starting_dir: VirtualPath::new(sandbox.path().join("package")), toolchain_config: json!({ "packageManager": "npm" }), @@ -1843,8 +2173,8 @@ mod javascript_toolchain_tier2 { let output = plugin .parse_manifest(ParseManifestInput { - path: VirtualPath::Real(sandbox.path().join("package/package.json")), - root: VirtualPath::Real(sandbox.path().into()), + path: VirtualPath::new(sandbox.path().join("package/package.json")), + root: VirtualPath::new(sandbox.path()), ..Default::default() }) .await; @@ -1874,7 +2204,7 @@ mod javascript_toolchain_tier2 { // This must be ran to extract the catalogs plugin .locate_dependencies_root(LocateDependenciesRootInput { - starting_dir: VirtualPath::Real(sandbox.path().join("package")), + starting_dir: VirtualPath::new(sandbox.path().join("package")), toolchain_config: json!({ "packageManager": "pnpm" }), @@ -1884,8 +2214,8 @@ mod javascript_toolchain_tier2 { let output = plugin .parse_manifest(ParseManifestInput { - path: VirtualPath::Real(sandbox.path().join("package/package.json")), - root: VirtualPath::Real(sandbox.path().into()), + path: VirtualPath::new(sandbox.path().join("package/package.json")), + root: VirtualPath::new(sandbox.path()), ..Default::default() }) .await; @@ -1915,7 +2245,7 @@ mod javascript_toolchain_tier2 { // This must be ran to extract the catalogs plugin .locate_dependencies_root(LocateDependenciesRootInput { - starting_dir: VirtualPath::Real(sandbox.path().join("package")), + starting_dir: VirtualPath::new(sandbox.path().join("package")), toolchain_config: json!({ "packageManager": "yarn" }), @@ -1925,8 +2255,8 @@ mod javascript_toolchain_tier2 { let output = plugin .parse_manifest(ParseManifestInput { - path: VirtualPath::Real(sandbox.path().join("package/package.json")), - root: VirtualPath::Real(sandbox.path().into()), + path: VirtualPath::new(sandbox.path().join("package/package.json")), + root: VirtualPath::new(sandbox.path()), ..Default::default() }) .await; @@ -1956,7 +2286,7 @@ mod javascript_toolchain_tier2 { // This must be ran to extract the catalogs plugin .locate_dependencies_root(LocateDependenciesRootInput { - starting_dir: VirtualPath::Real(sandbox.path().join("package")), + starting_dir: VirtualPath::new(sandbox.path().join("package")), toolchain_config: json!({ "packageManager": "deno" }), @@ -1966,8 +2296,8 @@ mod javascript_toolchain_tier2 { let output = plugin .parse_manifest(ParseManifestInput { - path: VirtualPath::Real(sandbox.path().join("package/package.json")), - root: VirtualPath::Real(sandbox.path().into()), + path: VirtualPath::new(sandbox.path().join("package/package.json")), + root: VirtualPath::new(sandbox.path()), ..Default::default() }) .await; @@ -2108,12 +2438,33 @@ mod javascript_toolchain_tier2 { let output = plugin .parse_lock(ParseLockInput { - path: VirtualPath::Real(sandbox.path().join("bun.lock")), + path: VirtualPath::new(sandbox.path().join("bun.lock")), ..Default::default() }) .await; - assert_eq!(output.dependencies, expected_dependencies()); + // GitHub dependencies only exist in the bun lockfile fixture: + // with package metadata + integrity, and without either + let mut expected = expected_dependencies(); + expected.insert( + "@portkey-ai/gateway".into(), + vec![LockDependency { + hash: Some( + "sha512-71lqRjKGGxMSs4l6DdrzXHkGvitFdCsWxLbxYVfbYDTfhqOL0dJbmqjyrKObjSTbQqCXTh2XZQeI2Un0FN7pig==" + .into() + ), + ..Default::default() + }], + ); + expected.insert( + "uWebSockets.js".into(), + vec![LockDependency { + hash: Some("uNetworking-uWebSockets.js-6609a88".into()), + ..Default::default() + }], + ); + + assert_eq!(output.dependencies, expected); } // #[tokio::test(flavor = "multi_thread")] @@ -2123,7 +2474,7 @@ mod javascript_toolchain_tier2 { // let output = plugin // .parse_lock(ParseLockInput { - // path: VirtualPath::Real(sandbox.path().join("bun.lockb")), + // path: VirtualPath::new(sandbox.path().join("bun.lockb")), // ..Default::default() // }) // .await; @@ -2152,7 +2503,7 @@ mod javascript_toolchain_tier2 { let output = plugin .parse_lock(ParseLockInput { - path: VirtualPath::Real(sandbox.path().join("deno.lock")), + path: VirtualPath::new(sandbox.path().join("deno.lock")), ..Default::default() }) .await; @@ -2215,7 +2566,7 @@ mod javascript_toolchain_tier2 { let output = plugin .parse_lock(ParseLockInput { - path: VirtualPath::Real(sandbox.path().join("package-lock.json")), + path: VirtualPath::new(sandbox.path().join("package-lock.json")), ..Default::default() }) .await; @@ -2223,6 +2574,22 @@ mod javascript_toolchain_tier2 { assert_eq!(output.dependencies, expected_dependencies()); } + // `nub.lock` uses the pnpm lockfile format + #[tokio::test(flavor = "multi_thread")] + async fn parses_nub() { + let sandbox = create_lockfile_sandbox("nub"); + let plugin = sandbox.create_toolchain("javascript").await; + + let output = plugin + .parse_lock(ParseLockInput { + path: VirtualPath::new(sandbox.path().join("nub.lock")), + ..Default::default() + }) + .await; + + assert_eq!(output.dependencies, expected_base_dependencies()); + } + #[tokio::test(flavor = "multi_thread")] async fn parses_pnpm() { let sandbox = create_lockfile_sandbox("pnpm"); @@ -2230,7 +2597,7 @@ mod javascript_toolchain_tier2 { let output = plugin .parse_lock(ParseLockInput { - path: VirtualPath::Real(sandbox.path().join("pnpm-lock.yaml")), + path: VirtualPath::new(sandbox.path().join("pnpm-lock.yaml")), ..Default::default() }) .await; @@ -2248,7 +2615,7 @@ mod javascript_toolchain_tier2 { let output = plugin .parse_lock(ParseLockInput { - path: VirtualPath::Real(sandbox.path().join("pnpm-lock.yaml")), + path: VirtualPath::new(sandbox.path().join("pnpm-lock.yaml")), ..Default::default() }) .await; @@ -2286,7 +2653,7 @@ mod javascript_toolchain_tier2 { let output = plugin .parse_lock(ParseLockInput { - path: VirtualPath::Real(sandbox.path().join("yarn.lock")), + path: VirtualPath::new(sandbox.path().join("yarn.lock")), ..Default::default() }) .await; @@ -2372,7 +2739,7 @@ mod javascript_toolchain_tier2 { let output = plugin .parse_lock(ParseLockInput { - path: VirtualPath::Real(sandbox.path().join("yarn.lock")), + path: VirtualPath::new(sandbox.path().join("yarn.lock")), ..Default::default() }) .await; diff --git a/toolchains/node-depman/CHANGELOG.md b/toolchains/node-depman/CHANGELOG.md index 27c64525..4b0b4bed 100644 --- a/toolchains/node-depman/CHANGELOG.md +++ b/toolchains/node-depman/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## 1.1.0 + +#### 🚀 Updates + +- Added unstable support for [Nub](https://nubjs.com/). + +## 1.0.4 + +#### 🚀 Updates + +- Updated to support moon v2.5 release. + ## 1.0.3 #### 🚀 Updates diff --git a/toolchains/node-depman/Cargo.toml b/toolchains/node-depman/Cargo.toml index 1fd70a2e..769cf0da 100644 --- a/toolchains/node-depman/Cargo.toml +++ b/toolchains/node-depman/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "node_depman_toolchain" -version = "1.0.3" +version = "1.1.0" edition = "2024" -description = "Node.js dependency managers (npm, pnpm, yarn) toolchain WASM plugin for moon." +description = "Node.js dependency managers (npm, pnpm, yarn, nub) toolchain WASM plugin for moon." authors = ["Miles Johnson"] license = "MIT" repository = "https://github.com/moonrepo/plugins" diff --git a/toolchains/node-depman/src/config.rs b/toolchains/node-depman/src/config.rs index a44e4459..f2d0d401 100644 --- a/toolchains/node-depman/src/config.rs +++ b/toolchains/node-depman/src/config.rs @@ -16,6 +16,20 @@ config_struct!( } ); +config_struct!( + /// Configures and enables the Nub toolchain. + /// Docs: https://moonrepo.dev/docs/config/toolchain#nub + #[derive(Config)] + pub struct NubToolchainConfig { + /// List of arguments to append to `nub install` commands. + /// These arguments are inherited by the JavaScript toolchain. + pub install_args: Vec, + + /// Configured version to download and install. + pub version: Option, + } +); + config_struct!( /// Configures and enables the pnpm toolchain. /// Docs: https://moonrepo.dev/docs/config/toolchain#pnpm diff --git a/toolchains/node-depman/src/tier1.rs b/toolchains/node-depman/src/tier1.rs index 99b4db67..3ff2a6f0 100644 --- a/toolchains/node-depman/src/tier1.rs +++ b/toolchains/node-depman/src/tier1.rs @@ -21,16 +21,24 @@ pub fn register_toolchain( lock_file_names: vec!["package-lock.json".into(), "npm-shrinkwrap.json".into()], ..Default::default() }, - PackageManager::Pnpm | PackageManager::Pnpm11 => RegisterToolchainOutput { - config_file_globs: vec![ - ".npmrc".into(), - "pnpm-workspace.yaml".into(), - ".pnpmfile.*".into(), - ], - exe_names: vec!["pnpm".into(), "pnpx".into()], - lock_file_names: vec!["pnpm-lock.yaml".into()], + PackageManager::Nub => RegisterToolchainOutput { + config_file_globs: vec![".npmrc".into(), "nub.jsonc".into()], + exe_names: vec!["nub".into(), "nubx".into()], + lock_file_names: vec!["nub.lock".into()], ..Default::default() }, + PackageManager::Pnpm | PackageManager::Pnpm11 | PackageManager::Pnpm12 => { + RegisterToolchainOutput { + config_file_globs: vec![ + ".npmrc".into(), + "pnpm-workspace.yaml".into(), + ".pnpmfile.*".into(), + ], + exe_names: vec!["pnpm".into(), "pnpx".into()], + lock_file_names: vec!["pnpm-lock.yaml".into()], + ..Default::default() + } + } PackageManager::Yarn1 | PackageManager::Yarn2to5 | PackageManager::Yarn6 => { RegisterToolchainOutput { config_file_globs: vec![".npmrc".into(), ".yarnrc.*".into()], @@ -70,7 +78,8 @@ pub fn define_toolchain_config() -> FnResult> Ok(Json(DefineToolchainConfigOutput { schema: match manager { PackageManager::Npm => SchemaBuilder::build_root::(), - PackageManager::Pnpm | PackageManager::Pnpm11 => { + PackageManager::Nub => SchemaBuilder::build_root::(), + PackageManager::Pnpm | PackageManager::Pnpm11 | PackageManager::Pnpm12 => { SchemaBuilder::build_root::() } PackageManager::Yarn1 | PackageManager::Yarn2to5 | PackageManager::Yarn6 => { diff --git a/toolchains/node-depman/src/tier2.rs b/toolchains/node-depman/src/tier2.rs index cd9babfc..0bad4297 100644 --- a/toolchains/node-depman/src/tier2.rs +++ b/toolchains/node-depman/src/tier2.rs @@ -5,14 +5,21 @@ use extism_pdk::*; use moon_pdk::parse_toolchain_config_schema; use moon_pdk_api::*; use node_depman_tool::PackageManager; -use proto_pdk_api::{UnresolvedVersionSpec, VersionSpec}; #[plugin_fn] pub fn define_requirements( Json(_): Json, ) -> FnResult> { Ok(Json(DefineRequirementsOutput { - requires: vec!["node".into()], + // Nub is a standalone binary that can manage Node.js itself, + // while the other package managers are Node.js scripts + requires: if PackageManager::detect()?.is_nub() { + vec![] + } else { + vec!["node".into()] + }, + for_setup_environment: false, + for_setup_toolchain: true, })) } @@ -27,19 +34,18 @@ pub fn setup_environment( if manager.is_yarn() { let config = parse_toolchain_config_schema::(input.toolchain_config)?; - // TODO fix once moon is on proto 0.59 - if let Some(incompat_version) = &config.version { - let compat_version = UnresolvedVersionSpec::parse(incompat_version.to_string())?; - let compat_spec = match &compat_version { + if let Some(compat_version) = &config.version { + let compat_spec = match compat_version { + UnresolvedVersionSpec::Range(_) => None, UnresolvedVersionSpec::Requirement(req) => { - VersionSpec::parse(format!("{}.0.0", req.major.unwrap_or_default()))? + VersionSpec::parse(format!("{}.0.0", req.major.unwrap_or_default())).ok() } - _ => compat_version.to_resolved_spec(), + other => Some(other.to_resolved_spec()), }; - let new_manager = PackageManager::detect_from_version(&compat_spec)?; - - if new_manager == PackageManager::Yarn2to5 { + if let Some(compat_spec) = compat_spec + && PackageManager::detect_from_version(&compat_spec)? == PackageManager::Yarn2to5 + { for plugin in config.plugins { output.commands.push(ExecCommand::new( ExecCommandInput::new("yarn", ["plugin", "import", &plugin]) diff --git a/toolchains/node-depman/tests/tier1_test.rs b/toolchains/node-depman/tests/tier1_test.rs index adfa0d49..4aace750 100644 --- a/toolchains/node-depman/tests/tier1_test.rs +++ b/toolchains/node-depman/tests/tier1_test.rs @@ -27,6 +27,23 @@ mod node_depman_toolchain_tier1 { assert_eq!(output.vendor_dir_name.unwrap(), "node_modules"); } + #[tokio::test(flavor = "multi_thread")] + async fn handles_nub() { + let sandbox = create_empty_moon_sandbox(); + let plugin = sandbox.create_toolchain("nub").await; + + let output = plugin + .register_toolchain(RegisterToolchainInput { id: Id::raw("nub") }) + .await; + + assert_eq!(output.name, "nub"); + assert_eq!(output.config_file_globs, [".npmrc", "nub.jsonc"]); + assert_eq!(output.manifest_file_names, ["package.json"]); + assert_eq!(output.lock_file_names, ["nub.lock"]); + assert_eq!(output.exe_names, ["nub", "nubx"]); + assert_eq!(output.vendor_dir_name.unwrap(), "node_modules"); + } + #[tokio::test(flavor = "multi_thread")] async fn handles_pnpm() { let sandbox = create_empty_moon_sandbox(); diff --git a/toolchains/node-depman/tests/tier2_test.rs b/toolchains/node-depman/tests/tier2_test.rs index 5a9d0f16..622aa93a 100644 --- a/toolchains/node-depman/tests/tier2_test.rs +++ b/toolchains/node-depman/tests/tier2_test.rs @@ -15,7 +15,23 @@ mod node_depman_toolchain_tier2 { let output = plugin .setup_environment(SetupEnvironmentInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), + toolchain_config: json!({}), + ..Default::default() + }) + .await; + + assert!(output.commands.is_empty()); + } + + #[tokio::test(flavor = "multi_thread")] + async fn does_nothing_for_nub() { + let sandbox = create_empty_moon_sandbox(); + let plugin = sandbox.create_toolchain("nub").await; + + let output = plugin + .setup_environment(SetupEnvironmentInput { + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({}), ..Default::default() }) @@ -31,7 +47,7 @@ mod node_depman_toolchain_tier2 { let output = plugin .setup_environment(SetupEnvironmentInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({}), ..Default::default() }) @@ -47,7 +63,7 @@ mod node_depman_toolchain_tier2 { let output = plugin .setup_environment(SetupEnvironmentInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "plugins": [], "version": "^2" @@ -60,7 +76,7 @@ mod node_depman_toolchain_tier2 { let output = plugin .setup_environment(SetupEnvironmentInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "version": "^2" }), @@ -78,7 +94,7 @@ mod node_depman_toolchain_tier2 { let output = plugin .setup_environment(SetupEnvironmentInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "plugins": ["example"], "version": "^1" @@ -97,7 +113,7 @@ mod node_depman_toolchain_tier2 { let output = plugin .setup_environment(SetupEnvironmentInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "plugins": ["foo", "bar"], "version": "^2" diff --git a/toolchains/node/CHANGELOG.md b/toolchains/node/CHANGELOG.md index cd737571..8a18736a 100644 --- a/toolchains/node/CHANGELOG.md +++ b/toolchains/node/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## 1.0.4 + +#### 🚀 Updates + +- Deprecated the `syncVersionManagerConfig` setting (it never worked correctly). + +## 1.0.3 + +#### 🚀 Updates + +- Updated to support moon v2.5 release. + ## 1.0.2 #### 🚀 Updates diff --git a/toolchains/node/Cargo.toml b/toolchains/node/Cargo.toml index bc3aaf5a..f9d6f77b 100644 --- a/toolchains/node/Cargo.toml +++ b/toolchains/node/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "node_toolchain" -version = "1.0.2" +version = "1.0.4" edition = "2024" description = "Node.js toolchain WASM plugin for moon." authors = ["Miles Johnson"] diff --git a/toolchains/node/src/config.rs b/toolchains/node/src/config.rs index cdd0689d..44b7b116 100644 --- a/toolchains/node/src/config.rs +++ b/toolchains/node/src/config.rs @@ -36,6 +36,7 @@ config_struct!( pub profile_execution: Option, /// When `version` is defined, syncs the version to the chosen config. + #[deprecated] pub sync_version_manager_config: Option, /// Configured version to download and install. diff --git a/toolchains/node/src/tier2.rs b/toolchains/node/src/tier2.rs index f69ff3a2..e30f5b64 100644 --- a/toolchains/node/src/tier2.rs +++ b/toolchains/node/src/tier2.rs @@ -2,38 +2,8 @@ use crate::config::*; use extism_pdk::*; -use moon_pdk::parse_toolchain_config; +use moon_pdk::{VirtualPathExt, parse_toolchain_config}; use moon_pdk_api::*; -use starbase_utils::fs; - -#[plugin_fn] -pub fn setup_environment( - Json(input): Json, -) -> FnResult> { - let config = parse_toolchain_config::(input.toolchain_config)?; - let mut output = SetupEnvironmentOutput::default(); - - // Sync version manager - if let Some(version_manager) = config.sync_version_manager_config - && let Some(version) = config.version - { - let (op, file) = Operation::track("sync-version-manager", || { - let rc_path = input.root.join(match version_manager { - NodeVersionManager::Nodenv => ".node-version", - NodeVersionManager::Nvm => ".nvmrc", - }); - - fs::write_file(&rc_path, version.to_partial_string())?; - - Ok(rc_path) - })?; - - output.operations.push(op); - output.changed_files.extend(file.virtual_path()); - } - - Ok(Json(output)) -} #[plugin_fn] pub fn extend_task_command( @@ -47,7 +17,7 @@ pub fn extend_task_command( let project_root = input.context.get_project_root(&input.project); if let Some(profile) = &config.profile_execution - && let Some(prof_dir) = project_root.join(".moon").real_path_string() + && let Some(prof_dir) = project_root.join(".moon").to_real_path()? { match profile { NodeProfileType::Cpu => { @@ -56,7 +26,7 @@ pub fn extend_task_command( "--cpu-prof-name".into(), "snapshot.cpuprofile".into(), "--cpu-prof-dir".into(), - prof_dir, + prof_dir.to_string(), ]); } NodeProfileType::Heap => { @@ -65,7 +35,7 @@ pub fn extend_task_command( "--heap-prof-name".into(), "snapshot.heapprofile".into(), "--heap-prof-dir".into(), - prof_dir, + prof_dir.to_string(), ]); } }; diff --git a/toolchains/node/tests/tier2_test.rs b/toolchains/node/tests/tier2_test.rs index 53984fd5..63d7bfa7 100644 --- a/toolchains/node/tests/tier2_test.rs +++ b/toolchains/node/tests/tier2_test.rs @@ -1,124 +1,21 @@ -use moon_common::path::standardize_separators; use moon_pdk_api::*; use moon_pdk_test_utils::create_empty_moon_sandbox; use serde_json::json; -use std::fs; -use std::path::PathBuf; mod node_toolchain_tier2 { use super::*; - mod setup_environment { + mod extend_task_command { use super::*; - mod sync_toolchain_config { - use super::*; - - #[tokio::test(flavor = "multi_thread")] - async fn does_nothing_if_no_version() { - let sandbox = create_empty_moon_sandbox(); - let plugin = sandbox.create_toolchain("node").await; - - let output = plugin - .setup_environment(SetupEnvironmentInput { - root: VirtualPath::Real(sandbox.path().into()), - toolchain_config: json!({ - "syncVersionManagerConfig": "nvm", - "version": null - }), - ..Default::default() - }) - .await; - - assert!(output.operations.is_empty()); - assert!(output.changed_files.is_empty()); - } - - #[tokio::test(flavor = "multi_thread")] - async fn does_nothing_if_disabled() { - let sandbox = create_empty_moon_sandbox(); - let plugin = sandbox.create_toolchain("node").await; - - let output = plugin - .setup_environment(SetupEnvironmentInput { - root: VirtualPath::Real(sandbox.path().into()), - toolchain_config: json!({ - "syncVersionManagerConfig": null, - "version": "20.1" - }), - ..Default::default() - }) - .await; - - assert!(output.operations.is_empty()); - assert!(output.changed_files.is_empty()); - } - - #[tokio::test(flavor = "multi_thread")] - async fn writes_nvm_version() { - let sandbox = create_empty_moon_sandbox(); - let plugin = sandbox.create_toolchain("node").await; - - let output = plugin - .setup_environment(SetupEnvironmentInput { - root: VirtualPath::Real(sandbox.path().into()), - toolchain_config: json!({ - "syncVersionManagerConfig": "nvm", - "version": "20.1" - }), - ..Default::default() - }) - .await; - - assert!( - output - .operations - .iter() - .any(|op| op.id == "sync-version-manager") - ); - assert_eq!(output.changed_files, [PathBuf::from("/workspace/.nvmrc")]); - assert_eq!( - fs::read_to_string(sandbox.path().join(".nvmrc")).unwrap(), - "20.1" - ); - } - - #[tokio::test(flavor = "multi_thread")] - async fn writes_nodenv_version() { - let sandbox = create_empty_moon_sandbox(); - let plugin = sandbox.create_toolchain("node").await; - - let output = plugin - .setup_environment(SetupEnvironmentInput { - root: VirtualPath::Real(sandbox.path().into()), - toolchain_config: json!({ - "syncVersionManagerConfig": "nodenv", - "version": "20.1" - }), - ..Default::default() - }) - .await; - - assert!( - output - .operations - .iter() - .any(|op| op.id == "sync-version-manager") - ); - assert_eq!( - output.changed_files, - [PathBuf::from("/workspace/.node-version")] - ); - assert_eq!( - fs::read_to_string(sandbox.path().join(".node-version")).unwrap(), - "20.1" - ); - } + // The guest converts virtual paths to real paths by joining the + // stripped virtual suffix onto the native host prefix with `/`, so on + // Windows the result is a mixed-separator string like + // `C:\...\sandbox/project/.moon`. Mirror that exactly, since args are + // compared as strings, not paths. + fn expected_prof_dir(root: &std::path::Path) -> String { + format!("{}/project/.moon", root.display()) } - } - - mod extend_task_command { - use super::*; #[tokio::test(flavor = "multi_thread")] async fn prepends_exec_args_when_node() { @@ -181,7 +78,7 @@ mod node_toolchain_tier2 { "--cpu-prof-name".into(), "snapshot.cpuprofile".into(), "--cpu-prof-dir".into(), - standardize_separators(sandbox.path().join("project/.moon").to_string_lossy()) + expected_prof_dir(sandbox.path()) ]) ); } @@ -208,7 +105,7 @@ mod node_toolchain_tier2 { "--heap-prof-name".into(), "snapshot.heapprofile".into(), "--heap-prof-dir".into(), - standardize_separators(sandbox.path().join("project/.moon").to_string_lossy()) + expected_prof_dir(sandbox.path()) ]) ); } @@ -256,7 +153,7 @@ mod node_toolchain_tier2 { "--heap-prof-name".into(), "snapshot.heapprofile".into(), "--heap-prof-dir".into(), - standardize_separators(sandbox.path().join("project/.moon").to_string_lossy()) + expected_prof_dir(sandbox.path()) ]) ); } diff --git a/toolchains/python-pip/CHANGELOG.md b/toolchains/python-pip/CHANGELOG.md index 2e5ddcdd..7866d3b2 100644 --- a/toolchains/python-pip/CHANGELOG.md +++ b/toolchains/python-pip/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## 0.1.4 + +#### 🚀 Updates + +- Ensures that Python is installed before setting up this toolchain. + +## 0.1.3 + +#### 🚀 Updates + +- Updated to support moon v2.5 release. + ## 0.1.2 #### 🚀 Updates diff --git a/toolchains/python-pip/Cargo.toml b/toolchains/python-pip/Cargo.toml index ef12695f..fde3f354 100644 --- a/toolchains/python-pip/Cargo.toml +++ b/toolchains/python-pip/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "python_pip_toolchain" -version = "0.1.2" +version = "0.1.4" edition = "2024" description = "Python pip toolchain WASM plugin for moon." authors = ["Miles Johnson"] diff --git a/toolchains/python-pip/src/tier2.rs b/toolchains/python-pip/src/tier2.rs index 283eb09c..326ccb64 100644 --- a/toolchains/python-pip/src/tier2.rs +++ b/toolchains/python-pip/src/tier2.rs @@ -9,5 +9,8 @@ pub fn define_requirements( ) -> FnResult> { Ok(Json(DefineRequirementsOutput { requires: vec!["unstable_python".into()], + for_setup_environment: false, + // Requires python to run pip + for_setup_toolchain: true, })) } diff --git a/toolchains/python-poetry/CHANGELOG.md b/toolchains/python-poetry/CHANGELOG.md index 1180103e..d8eed6fe 100644 --- a/toolchains/python-poetry/CHANGELOG.md +++ b/toolchains/python-poetry/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## 0.1.2 + +#### 🚀 Updates + +- Ensures that Python is installed before setting up this toolchain. + +## 0.1.1 + +#### 🚀 Updates + +- Updated to support moon v2.5 release. + ## 0.1.0 #### 🚀 Updates diff --git a/toolchains/python-poetry/Cargo.toml b/toolchains/python-poetry/Cargo.toml index 9fffdf1b..fc01546b 100644 --- a/toolchains/python-poetry/Cargo.toml +++ b/toolchains/python-poetry/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "python_poetry_toolchain" -version = "0.1.0" +version = "0.1.2" edition = "2024" description = "Python Poetry toolchain WASM plugin for moon." authors = ["Miles Johnson"] diff --git a/toolchains/python-poetry/src/tier2.rs b/toolchains/python-poetry/src/tier2.rs index 283eb09c..196b3690 100644 --- a/toolchains/python-poetry/src/tier2.rs +++ b/toolchains/python-poetry/src/tier2.rs @@ -9,5 +9,8 @@ pub fn define_requirements( ) -> FnResult> { Ok(Json(DefineRequirementsOutput { requires: vec!["unstable_python".into()], + for_setup_environment: false, + // Requires python to run poetry + for_setup_toolchain: true, })) } diff --git a/toolchains/python-uv/CHANGELOG.md b/toolchains/python-uv/CHANGELOG.md index df1a4ae5..77c825fa 100644 --- a/toolchains/python-uv/CHANGELOG.md +++ b/toolchains/python-uv/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## 0.1.5 + +#### 🚀 Updates + +- Ensures that Python is installed before setting up this toolchain. + +## 0.1.4 + +#### 🚀 Updates + +- Updated to support moon v2.5 release. + ## 0.1.3 #### 🐞 Fixes diff --git a/toolchains/python-uv/Cargo.toml b/toolchains/python-uv/Cargo.toml index 599669e3..c96e0550 100644 --- a/toolchains/python-uv/Cargo.toml +++ b/toolchains/python-uv/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "python_uv_toolchain" -version = "0.1.3" +version = "0.1.5" edition = "2024" description = "Python uv toolchain WASM plugin for moon." authors = ["Miles Johnson"] diff --git a/toolchains/python-uv/src/tier2.rs b/toolchains/python-uv/src/tier2.rs index 283eb09c..aece7c9b 100644 --- a/toolchains/python-uv/src/tier2.rs +++ b/toolchains/python-uv/src/tier2.rs @@ -9,5 +9,8 @@ pub fn define_requirements( ) -> FnResult> { Ok(Json(DefineRequirementsOutput { requires: vec!["unstable_python".into()], + for_setup_environment: false, + // Aligns with other package managers + for_setup_toolchain: true, })) } diff --git a/toolchains/python/CHANGELOG.md b/toolchains/python/CHANGELOG.md index eee15142..7206f760 100644 --- a/toolchains/python/CHANGELOG.md +++ b/toolchains/python/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## 0.3.0 + +#### 🚀 Updates + +- Ensures that package manager toolchains are installed before setting up the environment. +- Added support to the Docker pruning workflow where we remove `.venv` directories for non-focused projects (those that were explicitly scaffolded). + +## 0.2.1 + +#### 🚀 Updates + +- Updated to support moon v2.5 release. + ## 0.2.0 #### 🚀 Updates diff --git a/toolchains/python/Cargo.toml b/toolchains/python/Cargo.toml index 6578ee10..6b654b0e 100644 --- a/toolchains/python/Cargo.toml +++ b/toolchains/python/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "python_toolchain" -version = "0.2.0" +version = "0.3.0" edition = "2024" description = "Python toolchain WASM plugin for moon." authors = ["Miles Johnson"] diff --git a/toolchains/python/src/managers/pip.rs b/toolchains/python/src/managers/pip.rs index ed4d5843..f86e3832 100644 --- a/toolchains/python/src/managers/pip.rs +++ b/toolchains/python/src/managers/pip.rs @@ -78,10 +78,7 @@ pub fn parse_requirements_txt(path: &VirtualPath, output: &mut ParseLockOutput) } if let Some(version) = config.version { - if matches!( - version, - UnresolvedVersionSpec::Semantic(_) | UnresolvedVersionSpec::Calendar(_) - ) { + if matches!(version, UnresolvedVersionSpec::Version(_)) { dep.version = Some(version.to_resolved_spec()); } else { dep.req = Some(version); @@ -107,7 +104,7 @@ pub fn parse_pyproject_toml(path: &VirtualPath, output: &mut ParseManifestOutput }; if let Some(version) = &project.version { - output.version = Version::parse(&version.to_string()).ok(); + output.version = Version::parse(version.to_string()).ok(); } if let Some(dependencies) = &project.dependencies { diff --git a/toolchains/python/src/tier1.rs b/toolchains/python/src/tier1.rs index d268336a..69b55596 100644 --- a/toolchains/python/src/tier1.rs +++ b/toolchains/python/src/tier1.rs @@ -1,9 +1,10 @@ use crate::config::{PythonPackageManager, PythonToolchainConfig}; use extism_pdk::*; use moon_config::LanguageType; -use moon_pdk::parse_toolchain_config; +use moon_pdk::{parse_toolchain_config, parse_toolchain_config_schema}; use moon_pdk_api::*; use schematic::SchemaBuilder; +use starbase_utils::fs; use starbase_utils::json::JsonValue; use toolchain_common::enable_tracing; @@ -141,3 +142,24 @@ pub fn define_docker_metadata( scaffold_globs: vec![], })) } + +#[plugin_fn] +pub fn prune_docker(Json(input): Json) -> FnResult> { + let config = parse_toolchain_config_schema::(input.toolchain_config)?; + let mut output = PruneDockerOutput::default(); + + if input.docker_config.delete_vendor_directories { + for dep in input.project_dependencies { + let dep_root = input.context.get_project_root(&dep); + let venv_dir = dep_root.join(&config.venv_name); + + if venv_dir.exists() { + fs::remove_dir_all(&venv_dir)?; + + output.changed_files.push(venv_dir); + } + } + } + + Ok(Json(output)) +} diff --git a/toolchains/python/src/tier2.rs b/toolchains/python/src/tier2.rs index 1b07f8a3..bafffb5a 100644 --- a/toolchains/python/src/tier2.rs +++ b/toolchains/python/src/tier2.rs @@ -3,6 +3,7 @@ use crate::managers::*; use crate::pyproject_toml::{PyProjectToml, PyProjectTomlWithTools, normalize_distribution_name}; use extism_pdk::*; use moon_config::DependencyScope; +use moon_pdk::VirtualPathExt; use moon_pdk::{ load_project_toolchain_config, load_toolchain_config, locate_root, locate_root_many, locate_root_many_with_check, parse_toolchain_config_schema, @@ -12,6 +13,27 @@ use pep508_rs::Requirement; use std::collections::BTreeMap; use std::path::PathBuf; +#[plugin_fn] +pub fn define_requirements( + Json(input): Json, +) -> FnResult> { + let config = parse_toolchain_config_schema::(input.toolchain_config)?; + + Ok(Json(DefineRequirementsOutput { + requires: match config.package_manager { + Some(package_manager) => vec![format!("unstable_{package_manager}")], + None => vec![], + }, + // We must ensure package managers are fully installed + // before we setup the environment, otherwise commands fail + for_setup_environment: true, + // However, we don't want this when we setup this toolchain, + // because the package managers depend on us, and it would + // introduce a cycle + for_setup_toolchain: false, + })) +} + #[plugin_fn] pub fn extend_project_graph( Json(input): Json, @@ -88,9 +110,7 @@ pub fn extend_project_graph( .extended_projects .insert(id.to_owned(), project_output); - if let Some(file) = manifest.path.virtual_path() { - output.input_files.push(file); - } + output.input_files.push(manifest.path.clone()); } Ok(Json(output)) @@ -102,10 +122,10 @@ fn gather_shared_paths( paths: &mut Vec, ) -> AnyResult<()> { if let Some(venv_parent) = locate_root(current_dir, &config.venv_name) - && let Some(venv_root) = venv_parent.join(&config.venv_name).real_path() + && let Some(venv_root) = venv_parent.join(&config.venv_name).to_real_path()? { - paths.push(venv_root.join("Scripts")); - paths.push(venv_root.join("bin")); + paths.push(venv_root.join("Scripts").to_path_buf()); + paths.push(venv_root.join("bin").to_path_buf()); } Ok(()) @@ -152,9 +172,9 @@ pub fn locate_dependencies_root( // First attempt: find lock files if let Some(root) = locate_root_many(&input.starting_dir, &lock_names) { - output.root = root.virtual_path(); output.members = PyProjectTomlWithTools::load(root.join("pyproject.toml"))? .extract_members(package_manager)?; + output.root = Some(root); } // Second attempt: find workspace-compatible manifest files @@ -166,7 +186,7 @@ pub fn locate_dependencies_root( if manifest.tool.is_some() && let Some(members) = manifest.extract_members(package_manager)? { - output.root = root.virtual_path(); + output.root = Some(root.to_owned()); output.members = Some(members); found = true; } @@ -176,10 +196,8 @@ pub fn locate_dependencies_root( } // Last attempt: find a manifest file (project only) - if output.root.is_none() - && let Some(root) = locate_root_many(&input.starting_dir, &manifest_names) - { - output.root = root.virtual_path(); + if output.root.is_none() { + output.root = locate_root_many(&input.starting_dir, &manifest_names); } Ok(Json(output)) diff --git a/toolchains/python/tests/tier1_test.rs b/toolchains/python/tests/tier1_test.rs index a89d8588..a2d2b82a 100644 --- a/toolchains/python/tests/tier1_test.rs +++ b/toolchains/python/tests/tier1_test.rs @@ -1,3 +1,4 @@ +use moon_config::DockerPruneConfig; use moon_pdk_api::*; use moon_pdk_test_utils::create_empty_moon_sandbox; use serde_json::json; @@ -87,4 +88,136 @@ mod python_toolchain_tier1 { assert_eq!(output.default_image.unwrap(), "python:3.10"); } } + + mod prune_docker { + use super::*; + + fn create_project_fragment(id: &str) -> ProjectFragment { + ProjectFragment { + id: Id::raw(id), + source: id.into(), + ..Default::default() + } + } + + #[tokio::test(flavor = "multi_thread")] + async fn does_nothing_if_disabled() { + let sandbox = create_empty_moon_sandbox(); + sandbox.create_file("a/.venv/pyvenv.cfg", ""); + + let plugin = sandbox.create_toolchain("python").await; + + let output = plugin + .prune_docker(PruneDockerInput { + docker_config: DockerPruneConfig { + delete_vendor_directories: false, + ..Default::default() + }, + project_dependencies: vec![create_project_fragment("a")], + root: VirtualPath::new(sandbox.path()), + toolchain_config: json!({}), + ..Default::default() + }) + .await; + + assert!(sandbox.path().join("a/.venv").exists()); + + assert!(output.changed_files.is_empty()); + } + + #[tokio::test(flavor = "multi_thread")] + async fn does_nothing_if_no_venv_dirs() { + let sandbox = create_empty_moon_sandbox(); + let plugin = sandbox.create_toolchain("python").await; + + let output = plugin + .prune_docker(PruneDockerInput { + docker_config: DockerPruneConfig { + delete_vendor_directories: true, + ..Default::default() + }, + project_dependencies: vec![create_project_fragment("a")], + root: VirtualPath::new(sandbox.path()), + toolchain_config: json!({}), + ..Default::default() + }) + .await; + + assert!(output.changed_files.is_empty()); + } + + #[tokio::test(flavor = "multi_thread")] + async fn removes_venv_dirs_from_dependency_projects() { + let sandbox = create_empty_moon_sandbox(); + sandbox.create_file("a/.venv/pyvenv.cfg", ""); + sandbox.create_file("b/.venv/pyvenv.cfg", ""); + sandbox.create_file("c/.venv/pyvenv.cfg", ""); + + let plugin = sandbox.create_toolchain("python").await; + + let output = plugin + .prune_docker(PruneDockerInput { + docker_config: DockerPruneConfig { + delete_vendor_directories: true, + ..Default::default() + }, + project_dependencies: vec![ + create_project_fragment("a"), + create_project_fragment("b"), + ], + root: VirtualPath::new(sandbox.path()), + toolchain_config: json!({}), + ..Default::default() + }) + .await; + + assert!(!sandbox.path().join("a/.venv").exists()); + assert!(!sandbox.path().join("b/.venv").exists()); + + // Not a dependency, so remains + assert!(sandbox.path().join("c/.venv").exists()); + + assert_eq!( + output.changed_files, + [ + VirtualPath::new("/workspace/a/.venv"), + VirtualPath::new("/workspace/b/.venv") + ] + ); + } + + #[tokio::test(flavor = "multi_thread")] + async fn removes_custom_named_venv_dirs() { + let sandbox = create_empty_moon_sandbox(); + sandbox.create_file("a/venv/pyvenv.cfg", ""); + sandbox.create_file("a/.venv/pyvenv.cfg", ""); + + let plugin = sandbox.create_toolchain("python").await; + + let output = plugin + .prune_docker(PruneDockerInput { + docker_config: DockerPruneConfig { + delete_vendor_directories: true, + ..Default::default() + }, + project_dependencies: vec![create_project_fragment("a")], + root: VirtualPath::new(sandbox.path()), + toolchain_config: json!({ + "venvName": "venv" + }), + ..Default::default() + }) + .await; + + assert!(!sandbox.path().join("a/venv").exists()); + + // Not the configured name, so remains + assert!(sandbox.path().join("a/.venv").exists()); + + assert_eq!( + output.changed_files, + [VirtualPath::new("/workspace/a/venv")] + ); + } + } } diff --git a/toolchains/python/tests/tier2_test.rs b/toolchains/python/tests/tier2_test.rs index 0ab1aed8..fa2f1838 100644 --- a/toolchains/python/tests/tier2_test.rs +++ b/toolchains/python/tests/tier2_test.rs @@ -3,7 +3,6 @@ use moon_pdk_api::*; use moon_pdk_test_utils::{create_empty_moon_sandbox, create_moon_sandbox}; use serde_json::json; use std::collections::BTreeMap; -use std::path::PathBuf; mod python_toolchain_tier2 { use super::*; @@ -62,14 +61,14 @@ mod python_toolchain_tier2 { ]) ); - output.input_files.sort(); + output.input_files.sort_by(|a, d| a.cmp(d)); assert_eq!( output.input_files, [ - PathBuf::from("/workspace/a/pyproject.toml"), - PathBuf::from("/workspace/b/pyproject.toml"), - PathBuf::from("/workspace/c/pyproject.toml"), + VirtualPath::new("/workspace/a/pyproject.toml"), + VirtualPath::new("/workspace/b/pyproject.toml"), + VirtualPath::new("/workspace/c/pyproject.toml"), ] ); } @@ -142,7 +141,7 @@ dependencies = ["internal-lib"] assert_eq!( output.input_files, - [PathBuf::from("/workspace/a/pyproject.toml")] + [VirtualPath::new("/workspace/a/pyproject.toml")] ); } @@ -273,7 +272,7 @@ dependencies = ["internal-lib"] let output = plugin .locate_dependencies_root(LocateDependenciesRootInput { - starting_dir: VirtualPath::Real(sandbox.path().into()), + starting_dir: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "packageManager": "pip" }), @@ -292,7 +291,7 @@ dependencies = ["internal-lib"] let output = plugin .locate_dependencies_root(LocateDependenciesRootInput { - starting_dir: VirtualPath::Real(sandbox.path().join("package")), + starting_dir: VirtualPath::new(sandbox.path().join("package")), toolchain_config: json!({ "packageManager": null }), @@ -314,7 +313,7 @@ dependencies = ["internal-lib"] let output = plugin .locate_dependencies_root(LocateDependenciesRootInput { - starting_dir: VirtualPath::Real(sandbox.path().join("package/nested")), + starting_dir: VirtualPath::new(sandbox.path().join("package/nested")), toolchain_config: json!({ "packageManager": "pip" }), @@ -323,7 +322,7 @@ dependencies = ["internal-lib"] .await; assert!(output.members.is_none()); - assert_eq!(output.root.unwrap(), PathBuf::from("/workspace/package")); + assert_eq!(output.root.unwrap(), VirtualPath::new("/workspace/package")); } #[tokio::test(flavor = "multi_thread")] @@ -335,7 +334,7 @@ dependencies = ["internal-lib"] let output = plugin .locate_dependencies_root(LocateDependenciesRootInput { - starting_dir: VirtualPath::Real(sandbox.path().join("package/nested")), + starting_dir: VirtualPath::new(sandbox.path().join("package/nested")), toolchain_config: json!({ "packageManager": "pip" }), @@ -344,7 +343,7 @@ dependencies = ["internal-lib"] .await; assert!(output.members.is_none()); - assert_eq!(output.root.unwrap(), PathBuf::from("/workspace/package")); + assert_eq!(output.root.unwrap(), VirtualPath::new("/workspace/package")); } #[tokio::test(flavor = "multi_thread")] @@ -354,7 +353,7 @@ dependencies = ["internal-lib"] let output = plugin .locate_dependencies_root(LocateDependenciesRootInput { - starting_dir: VirtualPath::Real( + starting_dir: VirtualPath::new( sandbox.path().join("workspace/packages/a/nested"), ), toolchain_config: json!({ @@ -367,7 +366,7 @@ dependencies = ["internal-lib"] assert!(output.members.is_none()); assert_eq!( output.root.unwrap(), - PathBuf::from("/workspace/workspace/packages/a") + VirtualPath::new("/workspace/workspace/packages/a") ); } } @@ -384,7 +383,7 @@ dependencies = ["internal-lib"] let output = plugin .locate_dependencies_root(LocateDependenciesRootInput { - starting_dir: VirtualPath::Real(sandbox.path().join("package/nested")), + starting_dir: VirtualPath::new(sandbox.path().join("package/nested")), toolchain_config: json!({ "packageManager": "poetry" }), @@ -393,7 +392,7 @@ dependencies = ["internal-lib"] .await; assert!(output.members.is_none()); - assert_eq!(output.root.unwrap(), PathBuf::from("/workspace/package")); + assert_eq!(output.root.unwrap(), VirtualPath::new("/workspace/package")); } #[tokio::test(flavor = "multi_thread")] @@ -406,7 +405,7 @@ dependencies = ["internal-lib"] let output = plugin .locate_dependencies_root(LocateDependenciesRootInput { - starting_dir: VirtualPath::Real(sandbox.path().join("package/nested")), + starting_dir: VirtualPath::new(sandbox.path().join("package/nested")), toolchain_config: json!({ "packageManager": "poetry" }), @@ -415,7 +414,7 @@ dependencies = ["internal-lib"] .await; assert!(output.members.is_none()); - assert_eq!(output.root.unwrap(), PathBuf::from("/workspace/package")); + assert_eq!(output.root.unwrap(), VirtualPath::new("/workspace/package")); } } @@ -429,7 +428,7 @@ dependencies = ["internal-lib"] let output = plugin .locate_dependencies_root(LocateDependenciesRootInput { - starting_dir: VirtualPath::Real(sandbox.path().join("package/nested")), + starting_dir: VirtualPath::new(sandbox.path().join("package/nested")), toolchain_config: json!({ "packageManager": "uv" }), @@ -438,7 +437,7 @@ dependencies = ["internal-lib"] .await; assert!(output.members.is_none()); - assert_eq!(output.root.unwrap(), PathBuf::from("/workspace/package")); + assert_eq!(output.root.unwrap(), VirtualPath::new("/workspace/package")); } #[tokio::test(flavor = "multi_thread")] @@ -450,7 +449,7 @@ dependencies = ["internal-lib"] let output = plugin .locate_dependencies_root(LocateDependenciesRootInput { - starting_dir: VirtualPath::Real(sandbox.path().join("package/nested")), + starting_dir: VirtualPath::new(sandbox.path().join("package/nested")), toolchain_config: json!({ "packageManager": "uv" }), @@ -459,7 +458,7 @@ dependencies = ["internal-lib"] .await; assert!(output.members.is_none()); - assert_eq!(output.root.unwrap(), PathBuf::from("/workspace/package")); + assert_eq!(output.root.unwrap(), VirtualPath::new("/workspace/package")); } #[tokio::test(flavor = "multi_thread")] @@ -469,7 +468,7 @@ dependencies = ["internal-lib"] let output = plugin .locate_dependencies_root(LocateDependenciesRootInput { - starting_dir: VirtualPath::Real( + starting_dir: VirtualPath::new( sandbox.path().join("workspace/packages/a/nested"), ), toolchain_config: json!({ @@ -480,7 +479,10 @@ dependencies = ["internal-lib"] .await; assert_eq!(output.members.unwrap(), ["packages/*"]); - assert_eq!(output.root.unwrap(), PathBuf::from("/workspace/workspace")); + assert_eq!( + output.root.unwrap(), + VirtualPath::new("/workspace/workspace") + ); } #[tokio::test(flavor = "multi_thread")] @@ -492,7 +494,7 @@ dependencies = ["internal-lib"] let output = plugin .locate_dependencies_root(LocateDependenciesRootInput { - starting_dir: VirtualPath::Real( + starting_dir: VirtualPath::new( sandbox.path().join("workspace/packages/a/nested"), ), toolchain_config: json!({ @@ -503,7 +505,10 @@ dependencies = ["internal-lib"] .await; assert_eq!(output.members.unwrap(), ["packages/*"]); - assert_eq!(output.root.unwrap(), PathBuf::from("/workspace/workspace")); + assert_eq!( + output.root.unwrap(), + VirtualPath::new("/workspace/workspace") + ); } } } @@ -518,7 +523,7 @@ dependencies = ["internal-lib"] let output = plugin .install_dependencies(InstallDependenciesInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({}), ..Default::default() }) @@ -541,7 +546,7 @@ dependencies = ["internal-lib"] working_dir: plugin.plugin.to_virtual_path(sandbox.path()), ..Default::default() }, - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "packageManager": "pip" }), @@ -579,7 +584,7 @@ dependencies = ["internal-lib"] working_dir: plugin.plugin.to_virtual_path(sandbox.path()), ..Default::default() }, - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "packageManager": "pip" }), @@ -615,7 +620,7 @@ dependencies = ["internal-lib"] working_dir: plugin.plugin.to_virtual_path(sandbox.path()), ..Default::default() }, - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "packageManager": "pip" }), @@ -652,7 +657,7 @@ dependencies = ["internal-lib"] working_dir: plugin.plugin.to_virtual_path(sandbox.path()), ..Default::default() }, - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "packageManager": "pip" }), @@ -685,7 +690,7 @@ dependencies = ["internal-lib"] working_dir: plugin.plugin.to_virtual_path(sandbox.path()), ..Default::default() }, - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "packageManager": "poetry" }), @@ -718,7 +723,7 @@ dependencies = ["internal-lib"] working_dir: plugin.plugin.to_virtual_path(sandbox.path()), ..Default::default() }, - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "packageManager": "poetry" }), @@ -756,7 +761,7 @@ dependencies = ["internal-lib"] working_dir: plugin.plugin.to_virtual_path(sandbox.path()), ..Default::default() }, - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "packageManager": "poetry" }), @@ -790,7 +795,7 @@ dependencies = ["internal-lib"] working_dir: plugin.plugin.to_virtual_path(sandbox.path()), ..Default::default() }, - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "packageManager": "uv" }), @@ -824,7 +829,7 @@ dependencies = ["internal-lib"] working_dir: plugin.plugin.to_virtual_path(sandbox.path()), ..Default::default() }, - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "packageManager": "uv" }), @@ -859,7 +864,7 @@ dependencies = ["internal-lib"] working_dir: plugin.plugin.to_virtual_path(sandbox.path()), ..Default::default() }, - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "packageManager": "uv", "version": "1.2.3" @@ -906,7 +911,7 @@ dependencies = ["internal-lib"] working_dir: plugin.plugin.to_virtual_path(sandbox.path()), ..Default::default() }, - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "packageManager": "uv" }), @@ -940,7 +945,7 @@ dependencies = ["internal-lib"] working_dir: plugin.plugin.to_virtual_path(sandbox.path()), ..Default::default() }, - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "packageManager": "uv" }), @@ -968,9 +973,9 @@ dependencies = ["internal-lib"] ], stream: true, cwd: Some(plugin.plugin.to_virtual_path(sandbox.path())), - ..ExecCommandInput::default() + ..Default::default() }, - ..ExecCommand::default() + ..Default::default() }; assert_eq!(actual, expected); @@ -988,7 +993,7 @@ dependencies = ["internal-lib"] working_dir: plugin.plugin.to_virtual_path(sandbox.path()), ..Default::default() }, - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "packageManager": "uv-pip" }), @@ -1008,9 +1013,9 @@ dependencies = ["internal-lib"] args: vec!["pip".into(), "install".into()], cwd: Some(plugin.plugin.to_virtual_path(sandbox.path())), stream: true, - ..ExecCommandInput::default() + ..Default::default() }, - ..ExecCommand::default() + ..Default::default() }; assert_eq!(actual, expected); @@ -1031,7 +1036,7 @@ dependencies = ["internal-lib"] working_dir: plugin.plugin.to_virtual_path(sandbox.path()), ..Default::default() }, - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "packageManager": "uv-pip" }), project: Some(ProjectFragment { id: Id::raw("workspace"), @@ -1066,7 +1071,7 @@ dependencies = ["internal-lib"] working_dir: plugin.plugin.to_virtual_path(sandbox.path()), ..Default::default() }, - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "packageManager": "uv-pip" }), project: Some(ProjectFragment { id: Id::raw("workspace"), @@ -1107,7 +1112,7 @@ dependencies = ["internal-lib"] let output = plugin .parse_lock(ParseLockInput { - path: VirtualPath::Real(sandbox.path().join("pylock.toml")), + path: VirtualPath::new(sandbox.path().join("pylock.toml")), ..Default::default() }) .await; @@ -1182,7 +1187,7 @@ files = [] let output = plugin .parse_lock(ParseLockInput { - path: VirtualPath::Real(sandbox.path().join("poetry.lock")), + path: VirtualPath::new(sandbox.path().join("poetry.lock")), ..Default::default() }) .await; @@ -1219,7 +1224,7 @@ files = [] let output = plugin .parse_lock(ParseLockInput { - path: VirtualPath::Real(sandbox.path().join("requirements.txt")), + path: VirtualPath::new(sandbox.path().join("requirements.txt")), ..Default::default() }) .await; @@ -1240,6 +1245,7 @@ files = [] "requests".into(), vec![LockDependency { meta: Some("security".into()), + req: Some(UnresolvedVersionSpec::parse("==2.8.*, >=2.8.1").unwrap()), ..Default::default() }] ), @@ -1255,7 +1261,7 @@ files = [] let output = plugin .parse_lock(ParseLockInput { - path: VirtualPath::Real(sandbox.path().join("uv.lock")), + path: VirtualPath::new(sandbox.path().join("uv.lock")), ..Default::default() }) .await; @@ -1300,7 +1306,7 @@ files = [] let output = plugin .parse_manifest(ParseManifestInput { - path: VirtualPath::Real(sandbox.path().join("pyproject.toml")), + path: VirtualPath::new(sandbox.path().join("pyproject.toml")), ..Default::default() }) .await; @@ -1348,7 +1354,7 @@ files = [] let output = plugin .parse_manifest(ParseManifestInput { - path: VirtualPath::Real(sandbox.path().join("requirements.in")), + path: VirtualPath::new(sandbox.path().join("requirements.in")), ..Default::default() }) .await; @@ -1375,6 +1381,9 @@ files = [] "requests".into(), ManifestDependency::Config(ManifestDependencyConfig { features: vec!["security".into()], + version: Some( + UnresolvedVersionSpec::parse("==2.8.*, >=2.8.1").unwrap() + ), ..Default::default() }) ), @@ -1403,7 +1412,7 @@ files = [] let output = plugin .setup_environment(SetupEnvironmentInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "packageManager": null }), @@ -1421,7 +1430,7 @@ files = [] let output = plugin .setup_environment(SetupEnvironmentInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "packageManager": "uv", "venvName": ".virtual-env" @@ -1451,7 +1460,7 @@ files = [] let output = plugin .setup_environment(SetupEnvironmentInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "packageManager": "pip" }), @@ -1484,7 +1493,7 @@ files = [] let plugin = sandbox.create_toolchain("python").await; let output = plugin .setup_environment(SetupEnvironmentInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "packageManager": "pip" }), @@ -1513,7 +1522,7 @@ files = [] let output = plugin .setup_environment(SetupEnvironmentInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "packageManager": "poetry" }), @@ -1546,7 +1555,7 @@ files = [] let plugin = sandbox.create_toolchain("python").await; let output = plugin .setup_environment(SetupEnvironmentInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "packageManager": "poetry" }), @@ -1575,7 +1584,7 @@ files = [] let output = plugin .setup_environment(SetupEnvironmentInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "packageManager": "uv" }), @@ -1604,7 +1613,7 @@ files = [] let output = plugin .setup_environment(SetupEnvironmentInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "packageManager": "uv", "version": "1.2.3" @@ -1647,7 +1656,7 @@ files = [] let plugin = sandbox.create_toolchain("python").await; let output = plugin .setup_environment(SetupEnvironmentInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "packageManager": "uv" }), @@ -1676,7 +1685,7 @@ files = [] let output = plugin .setup_environment(SetupEnvironmentInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "packageManager": "uv", "version": "3.12.0" @@ -1714,7 +1723,7 @@ files = [] let plugin = sandbox.create_toolchain("python").await; let output = plugin .setup_environment(SetupEnvironmentInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "packageManager": "uv-pip" }), project: Some(ProjectFragment { id: Id::raw("workspace"), @@ -1739,7 +1748,7 @@ files = [] let plugin = sandbox.create_toolchain("python").await; let output = plugin .setup_environment(SetupEnvironmentInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "packageManager": "uv-pip", "version": "3.12.0" }), project: Some(ProjectFragment { id: Id::raw("workspace"), @@ -1776,7 +1785,7 @@ files = [] let output = plugin .setup_environment(SetupEnvironmentInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "packageManager": "uv" }), @@ -1801,7 +1810,7 @@ files = [] let output = plugin .setup_environment(SetupEnvironmentInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "packageManager": "pip" }), @@ -1826,7 +1835,7 @@ files = [] let output = plugin .setup_environment(SetupEnvironmentInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "packageManager": "uv-pip" }), @@ -1851,7 +1860,7 @@ files = [] let output = plugin .setup_environment(SetupEnvironmentInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "packageManager": "uv", "venvName": ".virtual-env" @@ -1880,7 +1889,7 @@ files = [] let plugin = sandbox.create_toolchain("python").await; let output = plugin .setup_environment(SetupEnvironmentInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "packageManager": "uv" }), @@ -1914,7 +1923,7 @@ files = [] let plugin = sandbox.create_toolchain("python").await; let output = plugin .setup_environment(SetupEnvironmentInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "packageManager": "pip" }), diff --git a/toolchains/ruby/CHANGELOG.md b/toolchains/ruby/CHANGELOG.md index e1871b91..db3a27fc 100644 --- a/toolchains/ruby/CHANGELOG.md +++ b/toolchains/ruby/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 0.1.1 + +#### 🚀 Updates + +- Updated to support moon v2.5 release. + ## 0.1.0 #### 🚀 Updates diff --git a/toolchains/ruby/Cargo.toml b/toolchains/ruby/Cargo.toml index 98621149..30f0fbdf 100644 --- a/toolchains/ruby/Cargo.toml +++ b/toolchains/ruby/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruby_toolchain" -version = "0.1.0" +version = "0.1.1" edition = "2024" description = "Ruby toolchain WASM plugin for moon." authors = ["Alex Launi"] diff --git a/toolchains/ruby/src/tier2.rs b/toolchains/ruby/src/tier2.rs index ca03655d..e361e111 100644 --- a/toolchains/ruby/src/tier2.rs +++ b/toolchains/ruby/src/tier2.rs @@ -2,7 +2,7 @@ use crate::config::RubyToolchainConfig; use crate::gemfile; use extism_pdk::*; use moon_config::{DependencyScope, VersionSpec}; -use moon_pdk::{locate_root_many, parse_toolchain_config_schema}; +use moon_pdk::{VirtualPathExt, locate_root_many, parse_toolchain_config_schema}; use moon_pdk_api::*; use rubund::parser::{SourceType, parse_lockfile}; use starbase_utils::fs; @@ -18,17 +18,14 @@ const DEFAULT_NON_PRODUCTION_GROUPS: [&str; 2] = ["development", "test"]; pub fn locate_dependencies_root( Json(input): Json, ) -> FnResult> { - let mut output = LocateDependenciesRootOutput::default(); - // Bundler has no workspace concept (unlike npm/uv/Cargo), so each project // is its own dependency root and there are no members to report. Walk // upward for the nearest Gemfile/Gemfile.lock. If none is found, `root` // stays `None` and moon skips install steps for this project. - if let Some(root) = locate_root_many(&input.starting_dir, &ROOT_FILES) { - output.root = root.virtual_path(); - } - - Ok(Json(output)) + Ok(Json(LocateDependenciesRootOutput { + root: locate_root_many(&input.starting_dir, &ROOT_FILES), + ..Default::default() + })) } #[plugin_fn] @@ -113,9 +110,9 @@ pub fn extend_command( // PATH so tasks can invoke e.g. `rspec` directly. `bundle exec ` works // regardless, via the `bundle` binary provided by the toolchain (tier 3). if let Some(root) = locate_root_many(&input.current_dir, &ROOT_FILES) - && let Some(real) = root.real_path() + && let Some(real) = root.to_real_path()? { - output.paths.push(real.join("bin")); + output.paths.push(real.join("bin").to_path_buf()); } Ok(Json(output)) @@ -266,9 +263,7 @@ pub fn extend_project_graph( output.extended_projects.insert(id.clone(), project_output); } - if let Some(file) = gemfile_path.virtual_path() { - output.input_files.push(file); - } + output.input_files.push(gemfile_path); } Ok(Json(output)) diff --git a/toolchains/ruby/tests/tier2_test.rs b/toolchains/ruby/tests/tier2_test.rs index d03cf870..320abca4 100644 --- a/toolchains/ruby/tests/tier2_test.rs +++ b/toolchains/ruby/tests/tier2_test.rs @@ -17,7 +17,7 @@ mod ruby_toolchain_tier2 { let output = plugin .locate_dependencies_root(LocateDependenciesRootInput { - starting_dir: VirtualPath::Real(sandbox.path().into()), + starting_dir: VirtualPath::new(sandbox.path()), toolchain_config: json!({}), ..Default::default() }) @@ -35,7 +35,7 @@ mod ruby_toolchain_tier2 { let output = plugin .locate_dependencies_root(LocateDependenciesRootInput { - starting_dir: VirtualPath::Real(sandbox.path().join("app")), + starting_dir: VirtualPath::new(sandbox.path().join("app")), toolchain_config: json!({}), ..Default::default() }) @@ -43,7 +43,7 @@ mod ruby_toolchain_tier2 { // Bundler has no workspaces, so no members are reported. assert!(output.members.is_none()); - assert_eq!(output.root.unwrap(), PathBuf::from("/workspace/app")); + assert_eq!(output.root.unwrap(), VirtualPath::new("/workspace/app")); } #[tokio::test(flavor = "multi_thread")] @@ -54,13 +54,13 @@ mod ruby_toolchain_tier2 { let output = plugin .locate_dependencies_root(LocateDependenciesRootInput { - starting_dir: VirtualPath::Real(sandbox.path().join("app/lib/nested")), + starting_dir: VirtualPath::new(sandbox.path().join("app/lib/nested")), toolchain_config: json!({}), ..Default::default() }) .await; - assert_eq!(output.root.unwrap(), PathBuf::from("/workspace/app")); + assert_eq!(output.root.unwrap(), VirtualPath::new("/workspace/app")); } } @@ -74,7 +74,7 @@ mod ruby_toolchain_tier2 { let output = plugin .setup_environment(SetupEnvironmentInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({}), ..Default::default() }) @@ -99,7 +99,7 @@ mod ruby_toolchain_tier2 { let output = plugin .setup_environment(SetupEnvironmentInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "bundlePath": "vendor/gems" }), ..Default::default() }) @@ -124,7 +124,7 @@ mod ruby_toolchain_tier2 { let output = plugin .setup_environment(SetupEnvironmentInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "frozen": true }), ..Default::default() }) @@ -159,7 +159,7 @@ mod ruby_toolchain_tier2 { let output = plugin .setup_environment(SetupEnvironmentInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({}), ..Default::default() }) @@ -179,7 +179,7 @@ mod ruby_toolchain_tier2 { let output = plugin .install_dependencies(InstallDependenciesInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({}), ..Default::default() }) @@ -202,7 +202,7 @@ mod ruby_toolchain_tier2 { let output = plugin .install_dependencies(InstallDependenciesInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({}), production: true, ..Default::default() @@ -225,7 +225,7 @@ mod ruby_toolchain_tier2 { let output = plugin .install_dependencies(InstallDependenciesInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "bundlerInstallArgs": ["--jobs", "4"] }), ..Default::default() }) @@ -313,7 +313,7 @@ BUNDLED WITH let output = plugin .parse_lock(ParseLockInput { - path: VirtualPath::Real(sandbox.path().join("Gemfile.lock")), + path: VirtualPath::new(sandbox.path().join("Gemfile.lock")), ..Default::default() }) .await; @@ -365,7 +365,7 @@ BUNDLED WITH let output = plugin .parse_lock(ParseLockInput { - path: VirtualPath::Real(sandbox.path().join("Gemfile.lock")), + path: VirtualPath::new(sandbox.path().join("Gemfile.lock")), ..Default::default() }) .await; @@ -415,7 +415,7 @@ end let output = plugin .parse_manifest(ParseManifestInput { - path: VirtualPath::Real(sandbox.path().join("Gemfile")), + path: VirtualPath::new(sandbox.path().join("Gemfile")), ..Default::default() }) .await; diff --git a/toolchains/rust/CHANGELOG.md b/toolchains/rust/CHANGELOG.md index 999c4b62..4b18574f 100644 --- a/toolchains/rust/CHANGELOG.md +++ b/toolchains/rust/CHANGELOG.md @@ -1,5 +1,25 @@ # Changelog +## 1.0.9 + +#### 🐞 Fixes + +- Fixed configured `bins` not being reinstalled when their binaries were + uninstalled or deleted outside of moon. Only missing binaries are now + installed. + +## 1.0.8 + +#### 🚀 Updates + +- Updated to support moon v2.5 release. + +## 1.0.7 + +#### 🐞 Fixes + +- Fixed an issue where Docker scaffolding would leave behind empty `lib.rs` or `main.rs` files. + ## 1.0.6 #### 🐞 Fixes diff --git a/toolchains/rust/Cargo.toml b/toolchains/rust/Cargo.toml index 95ecb701..87fee9b5 100644 --- a/toolchains/rust/Cargo.toml +++ b/toolchains/rust/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rust_toolchain" -version = "1.0.6" +version = "1.0.9" edition = "2024" description = "Rust toolchain WASM plugin for moon." authors = ["Miles Johnson"] diff --git a/toolchains/rust/src/tier1.rs b/toolchains/rust/src/tier1.rs index b5182e50..2e94ca2f 100644 --- a/toolchains/rust/src/tier1.rs +++ b/toolchains/rust/src/tier1.rs @@ -86,24 +86,28 @@ pub fn scaffold_docker( Json(input): Json, ) -> FnResult> { let mut output = ScaffoldDockerOutput::default(); + let lib_file = input.output_dir.join("src/lib.rs"); + let main_file = input.output_dir.join("src/main.rs"); // Cargo requires either `lib.rs` or `main.rs` during // the workspace/configs phase, which isn't copied till the // sources phase. Because scaffolding may attempt to run // Cargo commands, it will fail without these files! if input.phase == ScaffoldDockerPhase::Configs && input.project.is_some() { - let lib_file = input.output_dir.join("src/lib.rs"); - let main_file = input.output_dir.join("src/main.rs"); - fs::write_file(&lib_file, "")?; fs::write_file(&main_file, "")?; - if let Some(file) = lib_file.virtual_path() { - output.copied_files.push(file); - } + output.copied_files.push(lib_file.clone()); + output.copied_files.push(main_file.clone()); + } - if let Some(file) = main_file.virtual_path() { - output.copied_files.push(file); + // When we copy sources, we then need to remove these files + // if they are empty, as to not cause Cargo build issues + if input.phase == ScaffoldDockerPhase::Sources && input.project.is_some() { + for file in [lib_file, main_file] { + if file.exists() && fs::metadata(&file).is_ok_and(|meta| meta.len() == 0) { + let _ = fs::remove_file(file); + } } } @@ -173,15 +177,13 @@ pub fn prune_docker(Json(input): Json) -> FnResult, ) -> AnyResult<()> { if let Some(globals_dir) = globals_dir - && globals_dir.real_path().is_some() + && globals_dir.to_real_path()?.is_some() { // Avoid the host env overhead if we already // have a valid globals directory! @@ -97,7 +97,11 @@ fn gather_shared_paths( } else if let Some(value) = get_host_env_var("CARGO_HOME")? { Some(PathBuf::from(value).join("bin")) } else { - env.home_dir.join(".cargo").join("bin").real_path() + env.home_dir + .join(".cargo") + .join("bin") + .to_real_path()? + .map(|path| path.to_path_buf()) }; if let Some(dir) = maybe_dir { @@ -140,7 +144,7 @@ pub fn extend_task_command( } // Always include Cargo specific paths for all commands - gather_shared_paths(&env, input.globals_dir.as_ref(), &mut output.paths)?; + gather_shared_paths(env, input.globals_dir.as_ref(), &mut output.paths)?; Ok(Json(output)) } @@ -153,7 +157,7 @@ pub fn extend_task_script( let env = get_host_environment()?; // Always include Cargo specific paths for all commands - gather_shared_paths(&env, input.globals_dir.as_ref(), &mut output.paths)?; + gather_shared_paths(env, input.globals_dir.as_ref(), &mut output.paths)?; Ok(Json(output)) } @@ -166,8 +170,8 @@ pub fn locate_dependencies_root( // Attempt to find `Cargo.lock` first if let Some(root) = locate_root(&input.starting_dir, "Cargo.lock") { - output.root = root.virtual_path(); output.members = CargoToml::load(root.join("Cargo.toml"))?.extract_members(); + output.root = Some(root); } // Otherwise find a `Cargo.toml` workspace @@ -177,7 +181,7 @@ pub fn locate_dependencies_root( let mut found = false; if manifest.workspace.is_some() { - output.root = root.virtual_path(); + output.root = Some(root.to_owned()); output.members = manifest.extract_members(); found = true; } @@ -193,7 +197,7 @@ pub fn locate_dependencies_root( let mut found = false; if manifest.package.is_some() { - output.root = root.virtual_path(); + output.root = Some(root.to_owned()); found = true; } diff --git a/toolchains/rust/src/tier2_env.rs b/toolchains/rust/src/tier2_env.rs index 922a60be..47523b18 100644 --- a/toolchains/rust/src/tier2_env.rs +++ b/toolchains/rust/src/tier2_env.rs @@ -11,6 +11,20 @@ fn create_command(bin: &str, args: Vec<&str>, cwd: &VirtualPath) -> ExecCommand ExecCommand::new(ExecCommandInput::new(bin, args).cwd(cwd.to_owned())) } +fn is_bin_installed(env: &HostEnvironment, globals_dir: Option<&VirtualPath>, spec: &str) -> bool { + let Some(globals_dir) = globals_dir else { + return false; + }; + + // Entries may be suffixed with a version: `cargo-nextest@0.9.52` + let name = match spec.split_once('@') { + Some((prefix, _)) => prefix, + None => spec, + }; + + globals_dir.join(env.os.get_exe_name(name)).exists() +} + #[plugin_fn] pub fn setup_environment( Json(input): Json, @@ -25,9 +39,7 @@ pub fn setup_environment( })?; output.operations.push(op); - output - .changed_files - .extend(files.into_iter().filter_map(|file| file.virtual_path())); + output.changed_files.extend(files); } // Sync `rust-toolchain.toml` toolchain @@ -37,9 +49,7 @@ pub fn setup_environment( })?; output.operations.push(op); - output - .changed_files - .extend(files.into_iter().filter_map(|file| file.virtual_path())); + output.changed_files.extend(files); } // Install components @@ -47,9 +57,11 @@ pub fn setup_environment( let mut args = vec!["component", "add"]; args.extend(config.components.iter().map(|c| c.as_str())); - output - .commands - .push(create_command("rustup", args, &input.root).cache("rustup-component-add")); + output.commands.push( + create_command("rustup", args, &input.root) + .cache(CacheStrategy::Memory) + .label("rustup-component-add"), + ); } // Install targets @@ -57,72 +69,84 @@ pub fn setup_environment( let mut args = vec!["target", "add"]; args.extend(config.targets.iter().map(|c| c.as_str())); - output - .commands - .push(create_command("rustup", args, &input.root).cache("rustup-target-add")); + output.commands.push( + create_command("rustup", args, &input.root) + .cache(CacheStrategy::Memory) + .label("rustup-target-add"), + ); } // Install binaries if !config.bins.is_empty() { let env = get_host_environment()?; - // Only install if we can't find the binary - if input - .globals_dir - .is_none_or(|dir| !dir.join(env.os.get_exe_name("cargo-binstall")).exists()) - { - let binstall_package = if let Some(version) = &config.binstall_version { - format!("cargo-binstall@{version}") - } else { - "cargo-binstall".into() - }; - - output.commands.push( - create_command( - "cargo", - vec!["install", &binstall_package, "--force", "--locked"], - &input.root, - ) - .cache("cargo-binstall"), - ); - } - let mut force_bins = vec![]; let mut non_force_bins = vec![]; for bin in &config.bins { match bin { BinEntry::String(inner) => { - non_force_bins.push(inner.as_str()); + if !is_bin_installed(env, input.globals_dir.as_ref(), inner) { + non_force_bins.push(inner.as_str()); + } } BinEntry::Object(cfg) => { if cfg.local && env.ci { continue; } else if cfg.force { force_bins.push(cfg.bin.as_str()); - } else { + } else if !is_bin_installed(env, input.globals_dir.as_ref(), &cfg.bin) { non_force_bins.push(cfg.bin.as_str()); } } }; } - if !force_bins.is_empty() { - let mut args = vec!["binstall", "--no-confirm", "--log-level", "info", "--force"]; - args.extend(force_bins); - - output - .commands - .push(create_command("cargo", args, &input.root).cache("cargo-bins-forced")); - } - - if !non_force_bins.is_empty() { - let mut args = vec!["binstall", "--no-confirm", "--log-level", "info"]; - args.extend(non_force_bins); - - output - .commands - .push(create_command("cargo", args, &input.root).cache("cargo-bins")); + if !force_bins.is_empty() || !non_force_bins.is_empty() { + // Only install if we can't find the binary + if input + .globals_dir + .as_ref() + .is_none_or(|dir| !dir.join(env.os.get_exe_name("cargo-binstall")).exists()) + { + let binstall_package = if let Some(version) = &config.binstall_version { + format!("cargo-binstall@{version}") + } else { + "cargo-binstall".into() + }; + + output.commands.push( + create_command( + "cargo", + vec!["install", &binstall_package, "--force", "--locked"], + &input.root, + ) + .cache(CacheStrategy::Memory) + .label("cargo-binstall"), + ); + } + + if !force_bins.is_empty() { + let mut args = vec!["binstall", "--no-confirm", "--log-level", "info", "--force"]; + args.extend(force_bins); + + output.commands.push( + create_command("cargo", args, &input.root) + .cache(CacheStrategy::Memory) + .label("cargo-bins-forced"), + ); + } + + if !non_force_bins.is_empty() { + let mut args = vec!["binstall", "--no-confirm", "--log-level", "info"]; + args.extend(non_force_bins); + + output.commands.push( + create_command("cargo", args, &input.root) + .cache(CacheStrategy::Memory) + .label("cargo-bins"), + ); + } } } diff --git a/toolchains/rust/tests/cargo_toml_test.rs b/toolchains/rust/tests/cargo_toml_test.rs index 64dae856..33d72101 100644 --- a/toolchains/rust/tests/cargo_toml_test.rs +++ b/toolchains/rust/tests/cargo_toml_test.rs @@ -11,7 +11,7 @@ mod cargo_toml { #[test] fn adds_if_not_set() { let mut manifest = CargoToml { - path: VirtualPath::Real(PathBuf::from("/base/Cargo.toml")), + path: VirtualPath::new(PathBuf::from("/base/Cargo.toml")), data: CargoTomlInner::new_package(), ..Default::default() }; @@ -30,7 +30,7 @@ mod cargo_toml { #[test] fn doesnt_add_if_empty() { let mut manifest = CargoToml { - path: VirtualPath::Real(PathBuf::from("/base/Cargo.toml")), + path: VirtualPath::new(PathBuf::from("/base/Cargo.toml")), data: CargoTomlInner::new_package(), ..Default::default() }; @@ -46,7 +46,7 @@ mod cargo_toml { #[test] fn doesnt_add_if_same_value() { let mut manifest = CargoToml { - path: VirtualPath::Real(PathBuf::from("/base/Cargo.toml")), + path: VirtualPath::new(PathBuf::from("/base/Cargo.toml")), data: { let mut pkg = CargoTomlInner::new_package(); pkg.package @@ -75,7 +75,7 @@ mod cargo_toml { #[test] fn saves_field_if_set() { let mut manifest = CargoToml { - path: VirtualPath::Real(PathBuf::from("/base/Cargo.toml")), + path: VirtualPath::new(PathBuf::from("/base/Cargo.toml")), data: CargoTomlInner::new_package(), ..Default::default() }; @@ -97,7 +97,7 @@ mod cargo_toml { #[test] fn doesnt_save_field_if_not_set() { let manifest = CargoToml { - path: VirtualPath::Real(PathBuf::from("/base/Cargo.toml")), + path: VirtualPath::new(PathBuf::from("/base/Cargo.toml")), data: CargoTomlInner::new_package(), ..Default::default() }; @@ -129,7 +129,7 @@ mod cargo_toml { #[test] fn adds_if_not_set() { let mut manifest = CargoToml { - path: VirtualPath::Real(PathBuf::from("/base/Cargo.toml")), + path: VirtualPath::new(PathBuf::from("/base/Cargo.toml")), data: CargoTomlInner::new_workspace(), ..Default::default() }; @@ -145,7 +145,7 @@ mod cargo_toml { #[test] fn doesnt_add_if_empty() { let mut manifest = CargoToml { - path: VirtualPath::Real(PathBuf::from("/base/Cargo.toml")), + path: VirtualPath::new(PathBuf::from("/base/Cargo.toml")), data: CargoTomlInner::new_workspace(), ..Default::default() }; @@ -161,7 +161,7 @@ mod cargo_toml { #[test] fn doesnt_add_if_same_value() { let mut manifest = CargoToml { - path: VirtualPath::Real(PathBuf::from("/base/Cargo.toml")), + path: VirtualPath::new(PathBuf::from("/base/Cargo.toml")), data: { let mut ws = CargoTomlInner::new_workspace(); let pkg = ws @@ -188,7 +188,7 @@ mod cargo_toml { #[test] fn saves_field_if_set() { let mut manifest = CargoToml { - path: VirtualPath::Real(PathBuf::from("/base/Cargo.toml")), + path: VirtualPath::new(PathBuf::from("/base/Cargo.toml")), data: CargoTomlInner::new_workspace(), ..Default::default() }; @@ -210,7 +210,7 @@ mod cargo_toml { #[test] fn doesnt_save_field_if_not_set() { let manifest = CargoToml { - path: VirtualPath::Real(PathBuf::from("/base/Cargo.toml")), + path: VirtualPath::new(PathBuf::from("/base/Cargo.toml")), data: CargoTomlInner::new_workspace(), ..Default::default() }; diff --git a/toolchains/rust/tests/tier1_test.rs b/toolchains/rust/tests/tier1_test.rs index d3b3c9f5..2834268c 100644 --- a/toolchains/rust/tests/tier1_test.rs +++ b/toolchains/rust/tests/tier1_test.rs @@ -3,7 +3,6 @@ use moon_pdk_api::*; use moon_pdk_test_utils::{create_empty_moon_sandbox, create_moon_sandbox}; use serde_json::json; use std::fs; -use std::path::PathBuf; mod rust_toolchain_tier1 { use super::*; @@ -52,8 +51,8 @@ mod rust_toolchain_tier1 { let output = plugin .scaffold_docker(ScaffoldDockerInput { - input_dir: VirtualPath::Real(sandbox.path().join("in")), - output_dir: VirtualPath::Real(output_dir.clone()), + input_dir: VirtualPath::new(sandbox.path().join("in")), + output_dir: VirtualPath::new(output_dir.clone()), phase: ScaffoldDockerPhase::Configs, project: Some(ProjectFragment::default()), ..Default::default() @@ -66,8 +65,8 @@ mod rust_toolchain_tier1 { assert_eq!( output.copied_files, [ - PathBuf::from("/workspace/out/src/lib.rs"), - PathBuf::from("/workspace/out/src/main.rs") + VirtualPath::new("/workspace/out/src/lib.rs"), + VirtualPath::new("/workspace/out/src/main.rs") ] ); } @@ -83,8 +82,8 @@ mod rust_toolchain_tier1 { let output = plugin .scaffold_docker(ScaffoldDockerInput { - input_dir: VirtualPath::Real(sandbox.path().join("in")), - output_dir: VirtualPath::Real(output_dir.clone()), + input_dir: VirtualPath::new(sandbox.path().join("in")), + output_dir: VirtualPath::new(output_dir.clone()), phase: ScaffoldDockerPhase::Sources, ..Default::default() }) @@ -95,6 +94,30 @@ mod rust_toolchain_tier1 { assert!(output.copied_files.is_empty()); } + + #[tokio::test(flavor = "multi_thread")] + async fn removes_empty_files_in_sources_phase() { + let sandbox = create_empty_moon_sandbox(); + let plugin = sandbox.create_toolchain("rust").await; + let output_dir = sandbox.path().join("out"); + + fs::create_dir_all(output_dir.join("src")).unwrap(); + fs::write(output_dir.join("src/lib.rs"), "").unwrap(); + fs::write(output_dir.join("src/main.rs"), "fn main() {}").unwrap(); + + plugin + .scaffold_docker(ScaffoldDockerInput { + input_dir: VirtualPath::new(sandbox.path().join("in")), + output_dir: VirtualPath::new(output_dir.clone()), + phase: ScaffoldDockerPhase::Sources, + project: Some(ProjectFragment::default()), + ..Default::default() + }) + .await; + + assert!(!output_dir.join("src/lib.rs").exists()); + assert!(output_dir.join("src/main.rs").exists()); + } } mod prune_docker { @@ -111,7 +134,7 @@ mod rust_toolchain_tier1 { delete_vendor_directories: true, ..Default::default() }, - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), ..Default::default() }) .await; @@ -132,7 +155,7 @@ mod rust_toolchain_tier1 { delete_vendor_directories: false, ..Default::default() }, - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), ..Default::default() }) .await; @@ -153,7 +176,7 @@ mod rust_toolchain_tier1 { delete_vendor_directories: true, ..Default::default() }, - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), ..Default::default() }) .await; @@ -161,7 +184,10 @@ mod rust_toolchain_tier1 { assert!(!sandbox.path().join("target").exists()); assert!(!sandbox.path().join("target/other-file").exists()); - assert_eq!(output.changed_files, [PathBuf::from("/workspace/target")]); + assert_eq!( + output.changed_files, + [VirtualPath::new("/workspace/target")] + ); } #[tokio::test(flavor = "multi_thread")] @@ -175,7 +201,7 @@ mod rust_toolchain_tier1 { delete_vendor_directories: true, ..Default::default() }, - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), ..Default::default() }) .await; @@ -194,7 +220,10 @@ mod rust_toolchain_tier1 { .exists() ); - assert_eq!(output.changed_files, [PathBuf::from("/workspace/target")]); + assert_eq!( + output.changed_files, + [VirtualPath::new("/workspace/target")] + ); } } } diff --git a/toolchains/rust/tests/tier2_env_test.rs b/toolchains/rust/tests/tier2_env_test.rs index 32a047ef..d683eb98 100644 --- a/toolchains/rust/tests/tier2_env_test.rs +++ b/toolchains/rust/tests/tier2_env_test.rs @@ -2,7 +2,6 @@ use moon_pdk_api::*; use moon_pdk_test_utils::{create_empty_moon_sandbox, create_moon_sandbox}; use serde_json::json; use std::fs; -use std::path::PathBuf; mod rust_toolchain_tier2 { use super::*; @@ -17,7 +16,7 @@ mod rust_toolchain_tier2 { let output = plugin .setup_environment(SetupEnvironmentInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "addMsrvConstraint": true, "version": null @@ -42,7 +41,7 @@ mod rust_toolchain_tier2 { let output = plugin .setup_environment(SetupEnvironmentInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "addMsrvConstraint": false, "version": "1.69.0" @@ -67,7 +66,7 @@ mod rust_toolchain_tier2 { let output = plugin .setup_environment(SetupEnvironmentInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "addMsrvConstraint": true, "version": "1.69.0" @@ -84,7 +83,7 @@ mod rust_toolchain_tier2 { ); assert_eq!( output.changed_files, - [PathBuf::from("/workspace/Cargo.toml")] + [VirtualPath::new("/workspace/Cargo.toml")] ); assert!( fs::read_to_string(sandbox.path().join("Cargo.toml")) @@ -104,7 +103,7 @@ mod rust_toolchain_tier2 { let output = plugin .setup_environment(SetupEnvironmentInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "syncToolchainConfig": true, "version": null @@ -129,7 +128,7 @@ mod rust_toolchain_tier2 { let output = plugin .setup_environment(SetupEnvironmentInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "syncToolchainConfig": false, "version": "1.69.0" @@ -154,7 +153,7 @@ mod rust_toolchain_tier2 { let output = plugin .setup_environment(SetupEnvironmentInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "syncToolchainConfig": true, "version": "1.69.0" @@ -171,7 +170,7 @@ mod rust_toolchain_tier2 { ); assert_eq!( output.changed_files, - [PathBuf::from("/workspace/rust-toolchain.toml")] + [VirtualPath::new("/workspace/rust-toolchain.toml")] ); assert!( fs::read_to_string(sandbox.path().join("rust-toolchain.toml")) @@ -189,7 +188,7 @@ mod rust_toolchain_tier2 { let output = plugin .setup_environment(SetupEnvironmentInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "syncToolchainConfig": true, "version": null @@ -201,8 +200,8 @@ mod rust_toolchain_tier2 { assert_eq!( output.changed_files, [ - PathBuf::from("/workspace/rust-toolchain.toml"), - PathBuf::from("/workspace/rust-toolchain") + VirtualPath::new("/workspace/rust-toolchain.toml"), + VirtualPath::new("/workspace/rust-toolchain") ] ); assert!( @@ -222,7 +221,7 @@ mod rust_toolchain_tier2 { let output = plugin .setup_environment(SetupEnvironmentInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "syncToolchainConfig": true, "version": null @@ -234,8 +233,8 @@ mod rust_toolchain_tier2 { assert_eq!( output.changed_files, [ - PathBuf::from("/workspace/rust-toolchain.toml"), - PathBuf::from("/workspace/rust-toolchain") + VirtualPath::new("/workspace/rust-toolchain.toml"), + VirtualPath::new("/workspace/rust-toolchain") ] ); assert!( @@ -257,7 +256,7 @@ mod rust_toolchain_tier2 { let output = plugin .setup_environment(SetupEnvironmentInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "components": [] }), @@ -275,7 +274,7 @@ mod rust_toolchain_tier2 { let output = plugin .setup_environment(SetupEnvironmentInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "components": ["rustfmt", "clippy"] }), @@ -289,7 +288,8 @@ mod rust_toolchain_tier2 { ExecCommandInput::new("rustup", ["component", "add", "rustfmt", "clippy"]) .cwd(plugin.plugin.to_virtual_path(sandbox.path())) ) - .cache("rustup-component-add")] + .cache(CacheStrategy::Memory) + .label("rustup-component-add")] ); } } @@ -304,7 +304,7 @@ mod rust_toolchain_tier2 { let output = plugin .setup_environment(SetupEnvironmentInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "targets": [] }), @@ -322,7 +322,7 @@ mod rust_toolchain_tier2 { let output = plugin .setup_environment(SetupEnvironmentInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "targets": ["wasm32-wasi", "nightly"] }), @@ -336,7 +336,8 @@ mod rust_toolchain_tier2 { ExecCommandInput::new("rustup", ["target", "add", "wasm32-wasi", "nightly"],) .cwd(plugin.plugin.to_virtual_path(sandbox.path())) ) - .cache("rustup-target-add")] + .cache(CacheStrategy::Memory) + .label("rustup-target-add")] ); } } @@ -351,7 +352,7 @@ mod rust_toolchain_tier2 { let output = plugin .setup_environment(SetupEnvironmentInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "bins": [] }), @@ -369,7 +370,7 @@ mod rust_toolchain_tier2 { let output = plugin .setup_environment(SetupEnvironmentInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "bins": [ "cargo-nextest", @@ -392,7 +393,8 @@ mod rust_toolchain_tier2 { ) .cwd(plugin.plugin.to_virtual_path(sandbox.path())) ) - .cache("cargo-binstall"), + .cache(CacheStrategy::Memory) + .label("cargo-binstall"), ExecCommand::new( ExecCommandInput::new( "cargo", @@ -407,7 +409,8 @@ mod rust_toolchain_tier2 { ) .cwd(plugin.plugin.to_virtual_path(sandbox.path())) ) - .cache("cargo-bins") + .cache(CacheStrategy::Memory) + .label("cargo-bins") ] ); } @@ -419,7 +422,7 @@ mod rust_toolchain_tier2 { let output = plugin .setup_environment(SetupEnvironmentInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "bins": [ "cargo-nextest", @@ -443,7 +446,8 @@ mod rust_toolchain_tier2 { ) .cwd(plugin.plugin.to_virtual_path(sandbox.path())) ) - .cache("cargo-binstall"), + .cache(CacheStrategy::Memory) + .label("cargo-binstall"), ExecCommand::new( ExecCommandInput::new( "cargo", @@ -458,7 +462,8 @@ mod rust_toolchain_tier2 { ) .cwd(plugin.plugin.to_virtual_path(sandbox.path())) ) - .cache("cargo-bins-forced"), + .cache(CacheStrategy::Memory) + .label("cargo-bins-forced"), ExecCommand::new( ExecCommandInput::new( "cargo", @@ -472,7 +477,8 @@ mod rust_toolchain_tier2 { ) .cwd(plugin.plugin.to_virtual_path(sandbox.path())) ) - .cache("cargo-bins") + .cache(CacheStrategy::Memory) + .label("cargo-bins") ] ); } @@ -491,7 +497,7 @@ mod rust_toolchain_tier2 { let output = plugin .setup_environment(SetupEnvironmentInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "bins": [ { @@ -504,8 +510,8 @@ mod rust_toolchain_tier2 { }) .await; - // binstall command - assert_eq!(output.commands.len(), 1); + // no binstall command either, since there's nothing to install + assert!(output.commands.is_empty()); } #[tokio::test(flavor = "multi_thread")] @@ -515,7 +521,7 @@ mod rust_toolchain_tier2 { let output = plugin .setup_environment(SetupEnvironmentInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), toolchain_config: json!({ "binstallVersion": "1.2.3", "bins": ["cargo-nextest"] @@ -534,7 +540,8 @@ mod rust_toolchain_tier2 { ) .cwd(plugin.plugin.to_virtual_path(sandbox.path())) ) - .cache("cargo-binstall"), + .cache(CacheStrategy::Memory) + .label("cargo-binstall"), ExecCommand::new( ExecCommandInput::new( "cargo", @@ -548,7 +555,8 @@ mod rust_toolchain_tier2 { ) .cwd(plugin.plugin.to_virtual_path(sandbox.path())) ) - .cache("cargo-bins") + .cache(CacheStrategy::Memory) + .label("cargo-bins") ] ); } @@ -563,8 +571,93 @@ mod rust_toolchain_tier2 { let output = plugin .setup_environment(SetupEnvironmentInput { - root: VirtualPath::Real(sandbox.path().into()), - globals_dir: Some(VirtualPath::Real(sandbox.path().join(".cargo-bins"))), + root: VirtualPath::new(sandbox.path()), + globals_dir: Some(VirtualPath::new(sandbox.path().join(".cargo-bins"))), + toolchain_config: json!({ + "bins": ["cargo-nextest"] + }), + ..Default::default() + }) + .await; + + assert_eq!( + output.commands, + [ExecCommand::new( + ExecCommandInput::new( + "cargo", + [ + "binstall", + "--no-confirm", + "--log-level", + "info", + "cargo-nextest", + ], + ) + .cwd(plugin.plugin.to_virtual_path(sandbox.path())) + ) + .cache(CacheStrategy::Memory) + .label("cargo-bins")] + ); + } + + // https://github.com/moonrepo/plugins/issues/137 + + #[tokio::test(flavor = "multi_thread")] + async fn only_installs_missing_bins() { + let sandbox = create_empty_moon_sandbox(); + sandbox.create_file(".cargo/bin/cargo-nextest", ""); + sandbox.create_file(".cargo/bin/cargo-nextest.exe", ""); + + let plugin = sandbox.create_toolchain("rust").await; + + let output = plugin + .setup_environment(SetupEnvironmentInput { + root: VirtualPath::new(sandbox.path()), + globals_dir: Some(VirtualPath::new(sandbox.path().join(".cargo/bin"))), + toolchain_config: json!({ + "bins": ["cargo-nextest", "just"] + }), + ..Default::default() + }) + .await; + + assert_eq!( + output.commands, + [ + ExecCommand::new( + ExecCommandInput::new( + "cargo", + ["install", "cargo-binstall", "--force", "--locked"], + ) + .cwd(plugin.plugin.to_virtual_path(sandbox.path())) + ) + .cache(CacheStrategy::Memory) + .label("cargo-binstall"), + ExecCommand::new( + ExecCommandInput::new( + "cargo", + ["binstall", "--no-confirm", "--log-level", "info", "just"], + ) + .cwd(plugin.plugin.to_virtual_path(sandbox.path())) + ) + .cache(CacheStrategy::Memory) + .label("cargo-bins") + ] + ); + } + + #[tokio::test(flavor = "multi_thread")] + async fn adds_no_commands_if_all_bins_installed() { + let sandbox = create_empty_moon_sandbox(); + sandbox.create_file(".cargo/bin/cargo-nextest", ""); + sandbox.create_file(".cargo/bin/cargo-nextest.exe", ""); + + let plugin = sandbox.create_toolchain("rust").await; + + let output = plugin + .setup_environment(SetupEnvironmentInput { + root: VirtualPath::new(sandbox.path()), + globals_dir: Some(VirtualPath::new(sandbox.path().join(".cargo/bin"))), toolchain_config: json!({ "bins": ["cargo-nextest"] }), @@ -572,6 +665,57 @@ mod rust_toolchain_tier2 { }) .await; + assert!(output.commands.is_empty()); + } + + #[tokio::test(flavor = "multi_thread")] + async fn skips_installed_versioned_bins() { + let sandbox = create_empty_moon_sandbox(); + sandbox.create_file(".cargo/bin/cargo-nextest", ""); + sandbox.create_file(".cargo/bin/cargo-nextest.exe", ""); + + let plugin = sandbox.create_toolchain("rust").await; + + let output = plugin + .setup_environment(SetupEnvironmentInput { + root: VirtualPath::new(sandbox.path()), + globals_dir: Some(VirtualPath::new(sandbox.path().join(".cargo/bin"))), + toolchain_config: json!({ + "bins": ["cargo-nextest@0.9.52"] + }), + ..Default::default() + }) + .await; + + assert!(output.commands.is_empty()); + } + + #[tokio::test(flavor = "multi_thread")] + async fn always_installs_forced_bins() { + let sandbox = create_empty_moon_sandbox(); + sandbox.create_file(".cargo/bin/cargo-binstall", ""); + sandbox.create_file(".cargo/bin/cargo-binstall.exe", ""); + sandbox.create_file(".cargo/bin/cargo-nextest", ""); + sandbox.create_file(".cargo/bin/cargo-nextest.exe", ""); + + let plugin = sandbox.create_toolchain("rust").await; + + let output = plugin + .setup_environment(SetupEnvironmentInput { + root: VirtualPath::new(sandbox.path()), + globals_dir: Some(VirtualPath::new(sandbox.path().join(".cargo/bin"))), + toolchain_config: json!({ + "bins": [ + { + "bin": "cargo-nextest", + "force": true + } + ] + }), + ..Default::default() + }) + .await; + assert_eq!( output.commands, [ExecCommand::new( @@ -582,12 +726,14 @@ mod rust_toolchain_tier2 { "--no-confirm", "--log-level", "info", + "--force", "cargo-nextest", ], ) .cwd(plugin.plugin.to_virtual_path(sandbox.path())) ) - .cache("cargo-bins")] + .cache(CacheStrategy::Memory) + .label("cargo-bins-forced")] ); } } diff --git a/toolchains/rust/tests/tier2_test.rs b/toolchains/rust/tests/tier2_test.rs index 888126e6..6490731d 100644 --- a/toolchains/rust/tests/tier2_test.rs +++ b/toolchains/rust/tests/tier2_test.rs @@ -4,7 +4,6 @@ use moon_pdk_api::*; use moon_pdk_test_utils::{create_empty_moon_sandbox, create_moon_sandbox}; use std::collections::BTreeMap; use std::env; -use std::path::PathBuf; mod rust_toolchain_tier2 { use super::*; @@ -66,9 +65,9 @@ mod rust_toolchain_tier2 { assert_eq!( output.input_files, [ - PathBuf::from("/workspace/a/Cargo.toml"), - PathBuf::from("/workspace/b/Cargo.toml"), - PathBuf::from("/workspace/c/Cargo.toml"), + VirtualPath::new("/workspace/a/Cargo.toml"), + VirtualPath::new("/workspace/b/Cargo.toml"), + VirtualPath::new("/workspace/c/Cargo.toml"), ] ); } @@ -96,7 +95,7 @@ mod rust_toolchain_tier2 { assert_eq!( output.input_files, - [PathBuf::from("/workspace/a/Cargo.toml")] + [VirtualPath::new("/workspace/a/Cargo.toml")] ); } @@ -159,7 +158,7 @@ mod rust_toolchain_tier2 { .extend_task_command(ExtendTaskCommandInput { command: "nextest".into(), args: vec!["run".into()], - globals_dir: Some(VirtualPath::Real(sandbox.path().join(".home/.cargo/bin"))), + globals_dir: Some(VirtualPath::new(sandbox.path().join(".home/.cargo/bin"))), ..Default::default() }) .await; @@ -174,7 +173,7 @@ mod rust_toolchain_tier2 { .extend_task_command(ExtendTaskCommandInput { command: "cargo-nextest".into(), args: vec!["run".into()], - globals_dir: Some(VirtualPath::Real(sandbox.path().join(".home/.cargo/bin"))), + globals_dir: Some(VirtualPath::new(sandbox.path().join(".home/.cargo/bin"))), ..Default::default() }) .await; @@ -198,7 +197,7 @@ mod rust_toolchain_tier2 { .extend_task_command(ExtendTaskCommandInput { command: "nextest".into(), args: vec!["run".into()], - globals_dir: Some(VirtualPath::Real(sandbox.path().join(".home/.cargo/bin"))), + globals_dir: Some(VirtualPath::new(sandbox.path().join(".home/.cargo/bin"))), ..Default::default() }) .await; @@ -219,7 +218,7 @@ mod rust_toolchain_tier2 { .extend_task_command(ExtendTaskCommandInput { command: "cargo".into(), args: vec!["build".into()], - globals_dir: Some(VirtualPath::Real(sandbox.path().join(".home/.cargo/bin"))), + globals_dir: Some(VirtualPath::new(sandbox.path().join(".home/.cargo/bin"))), ..Default::default() }) .await; @@ -230,7 +229,7 @@ mod rust_toolchain_tier2 { let output = plugin .extend_task_command(ExtendTaskCommandInput { command: "rustc".into(), - globals_dir: Some(VirtualPath::Real(sandbox.path().join(".home/.cargo/bin"))), + globals_dir: Some(VirtualPath::new(sandbox.path().join(".home/.cargo/bin"))), ..Default::default() }) .await; @@ -279,7 +278,7 @@ mod rust_toolchain_tier2 { let output = plugin .locate_dependencies_root(LocateDependenciesRootInput { - starting_dir: VirtualPath::Real(sandbox.path().into()), + starting_dir: VirtualPath::new(sandbox.path()), ..Default::default() }) .await; @@ -295,13 +294,13 @@ mod rust_toolchain_tier2 { let output = plugin .locate_dependencies_root(LocateDependenciesRootInput { - starting_dir: VirtualPath::Real(sandbox.path().join("package/nested")), + starting_dir: VirtualPath::new(sandbox.path().join("package/nested")), ..Default::default() }) .await; assert!(output.members.is_none()); - assert_eq!(output.root.unwrap(), PathBuf::from("/workspace/package")); + assert_eq!(output.root.unwrap(), VirtualPath::new("/workspace/package")); } #[tokio::test(flavor = "multi_thread")] @@ -311,9 +310,7 @@ mod rust_toolchain_tier2 { let output = plugin .locate_dependencies_root(LocateDependenciesRootInput { - starting_dir: VirtualPath::Real( - sandbox.path().join("package-with-lock/nested"), - ), + starting_dir: VirtualPath::new(sandbox.path().join("package-with-lock/nested")), ..Default::default() }) .await; @@ -321,7 +318,7 @@ mod rust_toolchain_tier2 { assert!(output.members.is_none()); assert_eq!( output.root.unwrap(), - PathBuf::from("/workspace/package-with-lock") + VirtualPath::new("/workspace/package-with-lock") ); } @@ -332,7 +329,7 @@ mod rust_toolchain_tier2 { let output = plugin .locate_dependencies_root(LocateDependenciesRootInput { - starting_dir: VirtualPath::Real( + starting_dir: VirtualPath::new( sandbox.path().join("workspace/crates/a/nested"), ), ..Default::default() @@ -340,7 +337,10 @@ mod rust_toolchain_tier2 { .await; assert_eq!(output.members.unwrap(), ["crates/*"]); - assert_eq!(output.root.unwrap(), PathBuf::from("/workspace/workspace")); + assert_eq!( + output.root.unwrap(), + VirtualPath::new("/workspace/workspace") + ); } #[tokio::test(flavor = "multi_thread")] @@ -350,7 +350,7 @@ mod rust_toolchain_tier2 { let output = plugin .locate_dependencies_root(LocateDependenciesRootInput { - starting_dir: VirtualPath::Real( + starting_dir: VirtualPath::new( sandbox.path().join("workspace-with-lock/crates/a/nested"), ), ..Default::default() @@ -360,7 +360,7 @@ mod rust_toolchain_tier2 { assert_eq!(output.members.unwrap(), ["crates/*"]); assert_eq!( output.root.unwrap(), - PathBuf::from("/workspace/workspace-with-lock") + VirtualPath::new("/workspace/workspace-with-lock") ); } } @@ -375,7 +375,7 @@ mod rust_toolchain_tier2 { let output = plugin .install_dependencies(InstallDependenciesInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), ..Default::default() }) .await; @@ -391,7 +391,7 @@ mod rust_toolchain_tier2 { let output = plugin .install_dependencies(InstallDependenciesInput { - root: VirtualPath::Real(sandbox.path().into()), + root: VirtualPath::new(sandbox.path()), ..Default::default() }) .await; @@ -417,7 +417,7 @@ mod rust_toolchain_tier2 { let output = plugin .parse_lock(ParseLockInput { - path: VirtualPath::Real(sandbox.path().join("Cargo.lock")), + path: VirtualPath::new(sandbox.path().join("Cargo.lock")), ..Default::default() }) .await; @@ -468,7 +468,7 @@ mod rust_toolchain_tier2 { let output = plugin .parse_manifest(ParseManifestInput { - path: VirtualPath::Real(sandbox.path().join("Cargo.toml")), + path: VirtualPath::new(sandbox.path().join("Cargo.toml")), ..Default::default() }) .await; @@ -512,7 +512,7 @@ mod rust_toolchain_tier2 { let output = plugin .parse_manifest(ParseManifestInput { - path: VirtualPath::Real(sandbox.path().join("package/Cargo.toml")), + path: VirtualPath::new(sandbox.path().join("package/Cargo.toml")), ..Default::default() }) .await; diff --git a/toolchains/rust/tests/toolchain_toml_test.rs b/toolchains/rust/tests/toolchain_toml_test.rs index 4c97dca6..63c5864a 100644 --- a/toolchains/rust/tests/toolchain_toml_test.rs +++ b/toolchains/rust/tests/toolchain_toml_test.rs @@ -12,7 +12,7 @@ mod toolchain_toml { #[test] fn adds_if_not_set() { let mut tc = ToolchainToml { - path: VirtualPath::Real(PathBuf::from("/base/rust-toolchain.toml")), + path: VirtualPath::new(PathBuf::from("/base/rust-toolchain.toml")), ..Default::default() }; @@ -27,7 +27,7 @@ mod toolchain_toml { #[test] fn doesnt_add_if_empty() { let mut tc = ToolchainToml { - path: VirtualPath::Real(PathBuf::from("/base/rust-toolchain.toml")), + path: VirtualPath::new(PathBuf::from("/base/rust-toolchain.toml")), ..Default::default() }; @@ -42,7 +42,7 @@ mod toolchain_toml { #[test] fn doesnt_add_if_same_value() { let mut tc = ToolchainToml { - path: VirtualPath::Real(PathBuf::from("/base/rust-toolchain.toml")), + path: VirtualPath::new(PathBuf::from("/base/rust-toolchain.toml")), data: BaseToolchainToml { toolchain: ToolchainSection { channel: Some("stable".into()), @@ -62,7 +62,7 @@ mod toolchain_toml { #[test] fn saves_field_if_set() { let mut tc = ToolchainToml { - path: VirtualPath::Real(PathBuf::from("/base/rust-toolchain.toml")), + path: VirtualPath::new(PathBuf::from("/base/rust-toolchain.toml")), ..Default::default() }; @@ -81,7 +81,7 @@ mod toolchain_toml { #[test] fn doesnt_save_field_if_not_set() { let tc = ToolchainToml { - path: VirtualPath::Real(PathBuf::from("/base/rust-toolchain.toml")), + path: VirtualPath::new(PathBuf::from("/base/rust-toolchain.toml")), ..Default::default() }; diff --git a/toolchains/system/CHANGELOG.md b/toolchains/system/CHANGELOG.md index 0ccd8b25..b539ea3e 100644 --- a/toolchains/system/CHANGELOG.md +++ b/toolchains/system/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 1.0.3 + +#### 🚀 Updates + +- Updated to support moon v2.5 release. + ## 1.0.2 #### 🚀 Updates diff --git a/toolchains/system/Cargo.toml b/toolchains/system/Cargo.toml index 01093e76..b5fc352b 100644 --- a/toolchains/system/Cargo.toml +++ b/toolchains/system/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "system_toolchain" -version = "1.0.2" +version = "1.0.3" edition = "2024" description = "System toolchain WASM plugin for moon, for running tasks against system-installed binaries." authors = ["Miles Johnson"] diff --git a/toolchains/typescript/CHANGELOG.md b/toolchains/typescript/CHANGELOG.md index 92f31fee..f4897e31 100644 --- a/toolchains/typescript/CHANGELOG.md +++ b/toolchains/typescript/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 1.1.4 + +#### 🚀 Updates + +- Updated to support moon v2.5 release. + ## 1.1.3 #### 🐞 Fixes diff --git a/toolchains/typescript/Cargo.toml b/toolchains/typescript/Cargo.toml index 9c565ad3..ad332d1b 100644 --- a/toolchains/typescript/Cargo.toml +++ b/toolchains/typescript/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "typescript_toolchain" -version = "1.1.3" +version = "1.1.4" edition = "2024" description = "TypeScript toolchain WASM plugin for moon." authors = ["Miles Johnson"] diff --git a/toolchains/typescript/src/context.rs b/toolchains/typescript/src/context.rs index b58dcb32..06eaced0 100644 --- a/toolchains/typescript/src/context.rs +++ b/toolchains/typescript/src/context.rs @@ -1,8 +1,7 @@ use crate::config::TypeScriptToolchainConfig; -use moon_pdk::VirtualPath; +use moon_pdk::{AnyResult, VirtualPath, VirtualPathExt}; use moon_pdk_api::MoonContext; use moon_project::ProjectFragment; -use std::path::PathBuf; use typescript_tsconfig_json::CompilerPath; #[derive(Debug)] @@ -13,26 +12,11 @@ pub struct TypeScriptContext { pub workspace_root: VirtualPath, } -fn create_virtual_path(base: &VirtualPath, path: PathBuf) -> VirtualPath { - match base { - VirtualPath::Real(_) => VirtualPath::Real(path), - VirtualPath::Virtual { - virtual_prefix, - real_prefix, - .. - } => VirtualPath::Virtual { - path, - virtual_prefix: virtual_prefix.to_owned(), - real_prefix: real_prefix.to_owned(), - }, - } -} - pub fn create_typescript_context( base: &MoonContext, config: &TypeScriptToolchainConfig, project: &ProjectFragment, -) -> TypeScriptContext { +) -> AnyResult { let root_config = CompilerPath::resolve( base.workspace_root .join(&config.root) @@ -52,10 +36,10 @@ pub fn create_typescript_context( .to_path_buf(), ); - TypeScriptContext { - root_config: create_virtual_path(&base.workspace_root, root_config), - root_options_config: create_virtual_path(&base.workspace_root, root_options_config), - project_config: create_virtual_path(&base.workspace_root, project_config), + Ok(TypeScriptContext { + root_config: VirtualPath::create(root_config)?, + root_options_config: VirtualPath::create(root_options_config)?, + project_config: VirtualPath::create(project_config)?, workspace_root: base.workspace_root.clone(), - } + }) } diff --git a/toolchains/typescript/src/tier1.rs b/toolchains/typescript/src/tier1.rs index 13da2551..d92c9a3d 100644 --- a/toolchains/typescript/src/tier1.rs +++ b/toolchains/typescript/src/tier1.rs @@ -91,7 +91,7 @@ pub fn sync_project(Json(input): Json) -> FnResult(input.toolchain_config)?; - let context = create_typescript_context(&input.context, &config, &input.project); + let context = create_typescript_context(&input.context, &config, &input.project)?; let (op, files) = Operation::track("sync-project-references", || { sync_project_references( @@ -103,9 +103,7 @@ pub fn sync_project(Json(input): Json) -> FnResult, ) -> FnResult> { let config = parse_toolchain_config::(input.toolchain_config)?; - let context = create_typescript_context(&input.context, &config, &input.project); + let context = create_typescript_context(&input.context, &config, &input.project)?; let mut output = HashTaskContentsOutput::default(); let mut data = json::json!({}); let mut has_data = false; diff --git a/toolchains/typescript/src/tsconfig_json.rs b/toolchains/typescript/src/tsconfig_json.rs index 2275345c..22ae8ca8 100644 --- a/toolchains/typescript/src/tsconfig_json.rs +++ b/toolchains/typescript/src/tsconfig_json.rs @@ -25,7 +25,7 @@ impl TsConfigJson { pub fn load_with_extends(path: VirtualPath) -> AnyResult { let mut config = BaseTsConfigJson::default(); - for next in BaseTsConfigJson::resolve_extends_chain(path.any_path())? { + for next in BaseTsConfigJson::resolve_extends_chain(&path)? { config.extend(next.config); } @@ -123,11 +123,8 @@ impl TsConfigJson { /// Convert an absolute virtual path to a relative virtual string, /// for use within tsconfig include, exclude, and other paths. pub fn to_relative_path(&self, path: impl AsRef) -> AnyResult { - let mut rel_path = to_relative_virtual_string( - path.as_ref().any_path(), - self.path.parent().unwrap().any_path(), - ) - .map_err(|error| anyhow!("{error}"))?; + let mut rel_path = to_relative_virtual_string(path.as_ref(), self.path.parent().unwrap()) + .map_err(|error| anyhow!("{error}"))?; // This is required for TS >= v6 because `baseUrl` was removed if rel_path != "." && !rel_path.starts_with(".") { diff --git a/toolchains/typescript/tests/tsconfig_json_test.rs b/toolchains/typescript/tests/tsconfig_json_test.rs index 0aad8a7e..ba42b88f 100644 --- a/toolchains/typescript/tests/tsconfig_json_test.rs +++ b/toolchains/typescript/tests/tsconfig_json_test.rs @@ -19,7 +19,7 @@ mod tsconfig_json { sandbox.create_file("tsconfig.json", json); let config_path = sandbox.path().join("tsconfig.json"); - let mut tsc = TsConfigJsonContainer::load(VirtualPath::Real(config_path.clone())).unwrap(); + let mut tsc = TsConfigJsonContainer::load(VirtualPath::new(config_path.clone())).unwrap(); // Trigger dirty tsc.dirty.push("unknown".into()); @@ -46,9 +46,9 @@ mod tsconfig_json { "es2020.symbol.wellknown", "es2021.weakref", ]), - ..CompilerOptions::default() + ..Default::default() }), - ..TsConfigJson::default() + ..Default::default() }; let expected = serde_json::json!({ @@ -113,9 +113,9 @@ mod tsconfig_json { module: Some(ModuleField::Es2015), module_resolution: Some(ModuleResolutionField::Classic), target: Some(TargetField::EsNext), - ..CompilerOptions::default() + ..Default::default() }), - ..TsConfigJson::default() + ..Default::default() }; let actual_typed: TsConfigJson = serde_json::from_value(actual).unwrap(); @@ -154,7 +154,7 @@ mod tsconfig_json { fn parse_basic_file() { let fixture = locate_fixture("configs"); let tsc = - TsConfigJsonContainer::load(VirtualPath::Real(fixture.join("tsconfig.default.json"))) + TsConfigJsonContainer::load(VirtualPath::new(fixture.join("tsconfig.default.json"))) .unwrap(); assert_eq!( @@ -174,7 +174,7 @@ mod tsconfig_json { #[test] fn adds_if_not_set() { let mut tsc = TsConfigJsonContainer { - path: VirtualPath::Real(PathBuf::from("/base/tsconfig.json")), + path: VirtualPath::new(PathBuf::from("/base/tsconfig.json")), ..Default::default() }; @@ -182,7 +182,7 @@ mod tsconfig_json { assert!( tsc.add_project_ref( - &VirtualPath::Real(PathBuf::from("/sibling")), + &VirtualPath::new(PathBuf::from("/sibling")), "tsconfig.json" ) .unwrap() @@ -205,15 +205,15 @@ mod tsconfig_json { path: "../sibling".into(), prepend: None, }]), - ..TsConfigJson::default() + ..Default::default() }, - path: VirtualPath::Real(PathBuf::from("/base/tsconfig.json")), + path: VirtualPath::new(PathBuf::from("/base/tsconfig.json")), ..Default::default() }; assert!( !tsc.add_project_ref( - &VirtualPath::Real(PathBuf::from("/sibling")), + &VirtualPath::new(PathBuf::from("/sibling")), "tsconfig.json" ) .unwrap() @@ -232,9 +232,9 @@ mod tsconfig_json { fn includes_custom_config_name() { let mut tsc = TsConfigJsonContainer { data: TsConfigJson { - ..TsConfigJson::default() + ..Default::default() }, - path: VirtualPath::Real(PathBuf::from("/base/tsconfig.json")), + path: VirtualPath::new(PathBuf::from("/base/tsconfig.json")), ..Default::default() }; @@ -242,7 +242,7 @@ mod tsconfig_json { assert!( tsc.add_project_ref( - &VirtualPath::Real(PathBuf::from("/sibling")), + &VirtualPath::new(PathBuf::from("/sibling")), "tsconfig.ref.json" ) .unwrap() @@ -262,9 +262,9 @@ mod tsconfig_json { fn forces_forward_slash() { let mut tsc = TsConfigJsonContainer { data: TsConfigJson { - ..TsConfigJson::default() + ..Default::default() }, - path: VirtualPath::Real(PathBuf::from("C:\\base\\dir\\tsconfig.json")), + path: VirtualPath::new(PathBuf::from("C:\\base\\dir\\tsconfig.json")), ..Default::default() }; @@ -272,7 +272,7 @@ mod tsconfig_json { assert!( tsc.add_project_ref( - &VirtualPath::Real(PathBuf::from("C:\\base\\sibling")), + &VirtualPath::new(PathBuf::from("C:\\base\\sibling")), "tsconfig.json" ) .unwrap() @@ -295,15 +295,15 @@ mod tsconfig_json { path: "../sister".into(), prepend: None, }]), - ..TsConfigJson::default() + ..Default::default() }, - path: VirtualPath::Real(PathBuf::from("/base/tsconfig.json")), + path: VirtualPath::new(PathBuf::from("/base/tsconfig.json")), ..Default::default() }; assert!( tsc.add_project_ref( - &VirtualPath::Real(PathBuf::from("/brother")), + &VirtualPath::new(PathBuf::from("/brother")), "tsconfig.json" ) .unwrap() @@ -336,15 +336,15 @@ mod tsconfig_json { path: "../stale".into(), prepend: None, }]), - ..TsConfigJson::default() + ..Default::default() }, - path: VirtualPath::Real(PathBuf::from("/base/tsconfig.json")), + path: VirtualPath::new(PathBuf::from("/base/tsconfig.json")), ..Default::default() }; assert!( tsc.sync_project_refs( - &[VirtualPath::Real(PathBuf::from("/sibling"))], + &[VirtualPath::new(PathBuf::from("/sibling"))], "tsconfig.json" ) .unwrap() @@ -367,9 +367,9 @@ mod tsconfig_json { path: "../stale".into(), prepend: None, }]), - ..TsConfigJson::default() + ..Default::default() }, - path: VirtualPath::Real(PathBuf::from("/base/tsconfig.json")), + path: VirtualPath::new(PathBuf::from("/base/tsconfig.json")), ..Default::default() }; @@ -422,9 +422,9 @@ mod tsconfig_json { data: TsConfigJson { compiler_options: Some(CompilerOptions { out_dir: Some("./old".into()), - ..CompilerOptions::default() + ..Default::default() }), - ..TsConfigJson::default() + ..Default::default() }, ..Default::default() }; diff --git a/tools/bun/CHANGELOG.md b/tools/bun/CHANGELOG.md index ef96662d..9c81453b 100644 --- a/tools/bun/CHANGELOG.md +++ b/tools/bun/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 0.16.10 + +#### 🚀 Updates + +- Updated to support proto v0.60 release. + ## 0.16.9 #### 🚀 Updates diff --git a/tools/bun/Cargo.toml b/tools/bun/Cargo.toml index 0a23a86c..a8754658 100644 --- a/tools/bun/Cargo.toml +++ b/tools/bun/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bun_tool" -version = "0.16.9" +version = "0.16.10" edition = "2024" description = "Bun WASM plugin for proto." authors = ["Miles Johnson"] diff --git a/tools/bun/src/proto.rs b/tools/bun/src/proto.rs index b40ea7df..db96746b 100644 --- a/tools/bun/src/proto.rs +++ b/tools/bun/src/proto.rs @@ -25,10 +25,10 @@ pub fn register_tool(Json(_): Json) -> FnResult [HostArch::X64, HostArch::Arm64], @@ -207,7 +207,7 @@ pub fn download_prebuilt( let mut avx2_suffix = ""; - if env.arch == HostArch::X64 && env.os.is_linux() && command_exists(&env, "grep") { + if env.arch == HostArch::X64 && env.os.is_linux() && command_exists(env, "grep") { let output = exec_captured("grep", ["avx2", "/proc/cpuinfo"])?; if output.exit_code != 0 { @@ -245,7 +245,7 @@ pub fn download_prebuilt( .replace("{file}", "SHASUMS256.txt"), ) }, - ..DownloadPrebuiltOutput::default() + ..Default::default() })) } @@ -265,7 +265,7 @@ pub fn locate_executables( // so execute `bun x` instead (notice the space). shim_before_args: Some(StringOrVec::String("x".into())), - ..ExecutableConfig::default() + ..Default::default() }; Ok(Json(LocateExecutablesOutput { @@ -277,6 +277,6 @@ pub fn locate_executables( ("bunx".into(), bunx), ]), globals_lookup_dirs: vec!["$HOME/.bun/bin".into()], - ..LocateExecutablesOutput::default() + ..Default::default() })) } diff --git a/tools/bun/tests/versions_test.rs b/tools/bun/tests/versions_test.rs index da48220b..e2b6800f 100644 --- a/tools/bun/tests/versions_test.rs +++ b/tools/bun/tests/versions_test.rs @@ -179,7 +179,7 @@ mod bun_tool { assert_eq!( plugin .pin_version(PinVersionInput { - dir: VirtualPath::Real(sandbox.path().into()), + dir: plugin.tool.to_virtual_path(sandbox.path()), version: UnresolvedVersionSpec::parse(">=1").unwrap(), ..Default::default() }) @@ -204,7 +204,7 @@ mod bun_tool { assert_eq!( plugin .pin_version(PinVersionInput { - dir: VirtualPath::Real(sandbox.path().into()), + dir: plugin.tool.to_virtual_path(sandbox.path()), version: UnresolvedVersionSpec::parse(">=1").unwrap(), ..Default::default() }) @@ -243,7 +243,7 @@ mod bun_tool { assert_eq!( plugin .pin_version(PinVersionInput { - dir: VirtualPath::Real(sandbox.path().into()), + dir: plugin.tool.to_virtual_path(sandbox.path()), version: UnresolvedVersionSpec::parse(">=1").unwrap(), ..Default::default() }) @@ -282,7 +282,7 @@ mod bun_tool { assert_eq!( plugin .pin_version(PinVersionInput { - dir: VirtualPath::Real(sandbox.path().into()), + dir: plugin.tool.to_virtual_path(sandbox.path()), version: UnresolvedVersionSpec::parse(">=1").unwrap(), ..Default::default() }) @@ -313,7 +313,7 @@ mod bun_tool { assert_eq!( plugin .unpin_version(UnpinVersionInput { - dir: VirtualPath::Real(sandbox.path().into()), + dir: plugin.tool.to_virtual_path(sandbox.path()), ..Default::default() }) .await, @@ -346,7 +346,7 @@ mod bun_tool { assert_eq!( plugin .unpin_version(UnpinVersionInput { - dir: VirtualPath::Real(sandbox.path().into()), + dir: plugin.tool.to_virtual_path(sandbox.path()), ..Default::default() }) .await, @@ -391,7 +391,7 @@ mod bun_tool { assert_eq!( plugin .unpin_version(UnpinVersionInput { - dir: VirtualPath::Real(sandbox.path().into()), + dir: plugin.tool.to_virtual_path(sandbox.path()), ..Default::default() }) .await, @@ -430,7 +430,7 @@ mod bun_tool { assert_eq!( plugin .unpin_version(UnpinVersionInput { - dir: VirtualPath::Real(sandbox.path().into()), + dir: plugin.tool.to_virtual_path(sandbox.path()), ..Default::default() }) .await, diff --git a/tools/deno/CHANGELOG.md b/tools/deno/CHANGELOG.md index 7e45cf72..c6edb97e 100644 --- a/tools/deno/CHANGELOG.md +++ b/tools/deno/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 0.15.11 + +#### 🚀 Updates + +- Updated to support proto v0.60 release. + ## 0.15.10 #### 🚀 Updates diff --git a/tools/deno/Cargo.toml b/tools/deno/Cargo.toml index 5f0ca526..1074aeb8 100644 --- a/tools/deno/Cargo.toml +++ b/tools/deno/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "deno_tool" -version = "0.15.10" +version = "0.15.11" edition = "2024" description = "Deno WASM plugin for proto." authors = ["Miles Johnson"] diff --git a/tools/deno/src/proto.rs b/tools/deno/src/proto.rs index cb0d35bf..e030d679 100644 --- a/tools/deno/src/proto.rs +++ b/tools/deno/src/proto.rs @@ -19,10 +19,10 @@ pub fn register_tool(Json(_): Json) -> FnResult [HostArch::X64, HostArch::Arm64], HostOS::MacOS => [HostArch::X64, HostArch::Arm64], @@ -155,7 +155,7 @@ pub fn download_prebuilt( check_supported_os_and_arch( NAME, - &env, + env, permutations! [ HostOS::Linux => [HostArch::X64, HostArch::Arm64], HostOS::MacOS => [HostArch::X64, HostArch::Arm64], @@ -215,7 +215,7 @@ pub fn download_prebuilt( }, download_url, download_name: Some(filename), - ..DownloadPrebuiltOutput::default() + ..Default::default() })) } @@ -235,6 +235,6 @@ pub fn locate_executables( "$DENO_HOME/bin".into(), "$HOME/.deno/bin".into(), ], - ..LocateExecutablesOutput::default() + ..Default::default() })) } diff --git a/tools/example/CHANGELOG.md b/tools/example/CHANGELOG.md index 3e5b1460..37317f28 100644 --- a/tools/example/CHANGELOG.md +++ b/tools/example/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 0.0.3 + +#### 🚀 Updates + +- Updated to support proto v0.60 release. + ## 0.0.2 #### 🚀 Updates diff --git a/tools/example/Cargo.toml b/tools/example/Cargo.toml index 61bfedbf..35ce675e 100644 --- a/tools/example/Cargo.toml +++ b/tools/example/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "example_tool" -version = "0.0.2" +version = "0.0.3" edition = "2024" description = "Example tool." authors = ["Miles Johnson"] diff --git a/tools/example/src/proto.rs b/tools/example/src/proto.rs index 15280a48..8e305f50 100644 --- a/tools/example/src/proto.rs +++ b/tools/example/src/proto.rs @@ -10,6 +10,6 @@ pub fn register_tool(Json(_): Json) -> FnResult) -> FnResult [ HostArch::X64, HostArch::Arm64, HostArch::X86, HostArch::Arm, HostArch::S390x @@ -118,7 +118,7 @@ pub fn build_instructions( "./all.bash" } .into(), - cwd: Some("src".into()), + cwd: Some(input.install_dir.join("src")), ..Default::default() } })), @@ -138,7 +138,7 @@ pub fn download_prebuilt( check_supported_os_and_arch( NAME, - &env, + env, permutations! [ HostOS::Linux => [ HostArch::X64, HostArch::Arm64, HostArch::X86, HostArch::Arm, HostArch::S390x @@ -193,7 +193,7 @@ pub fn download_prebuilt( .replace("{version}", &version) .replace("{file}", &filename), download_name: Some(filename), - ..DownloadPrebuiltOutput::default() + ..Default::default() })) } @@ -220,7 +220,7 @@ pub fn locate_executables( "$GOPATH/bin".into(), "$HOME/go/bin".into(), ], - ..LocateExecutablesOutput::default() + ..Default::default() })) } diff --git a/tools/internal-schema/CHANGELOG.md b/tools/internal-schema/CHANGELOG.md index dd44bacc..f8ceaf02 100644 --- a/tools/internal-schema/CHANGELOG.md +++ b/tools/internal-schema/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 0.18.2 + +#### 🚀 Updates + +- Updated to support proto v0.60 release. + ## 0.18.1 #### 🚀 Updates diff --git a/tools/internal-schema/Cargo.toml b/tools/internal-schema/Cargo.toml index 7b560bde..82f397e0 100644 --- a/tools/internal-schema/Cargo.toml +++ b/tools/internal-schema/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "schema_tool" -version = "0.18.1" +version = "0.18.2" edition = "2024" description = "Schema-based WASM plugin for proto, powering the TOML plugin pattern." authors = ["Miles Johnson"] diff --git a/tools/internal-schema/src/proto.rs b/tools/internal-schema/src/proto.rs index c51bd11e..9e6aab70 100644 --- a/tools/internal-schema/src/proto.rs +++ b/tools/internal-schema/src/proto.rs @@ -42,7 +42,7 @@ pub fn register_tool(Json(_): Json) -> FnResult) -> FnResult PluginType::Language, SchemaType::VersionManager => PluginType::VersionManager, }, - minimum_proto_version: Some(Version::new(0, 59, 0)), + minimum_proto_version: Some(Version::new(0, 60, 0)), default_version: schema.metadata.default_version, plugin_version: match schema.metadata.plugin_version { Some(version) => Some(version), @@ -83,7 +83,7 @@ pub fn register_tool(Json(_): Json) -> FnResult FnResult> { let env = get_host_environment()?; let schema = get_schema()?; - let platform = get_platform(&schema, &env)?; + let platform = get_platform(&schema, env)?; if !platform.archs.is_empty() { check_supported_os_and_arch( &schema.name, - &env, + env, HashMap::from_iter([(env.os, platform.archs.clone())]), )?; } @@ -307,7 +307,7 @@ pub fn download_prebuilt( let is_canary = version.is_canary(); let download_file = - interpolate_tokens(&platform.download_file, version, &schema, platform, &env); + interpolate_tokens(&platform.download_file, version, &schema, platform, env); let download_url = interpolate_tokens( if is_canary { @@ -322,7 +322,7 @@ pub fn download_prebuilt( version, &schema, platform, - &env, + env, ) .replace("{download_file}", &download_file); @@ -331,7 +331,7 @@ pub fn download_prebuilt( version, &schema, platform, - &env, + env, ); let checksum_url = if is_canary { @@ -345,14 +345,14 @@ pub fn download_prebuilt( }; let checksum_url = checksum_url.map(|url| { - interpolate_tokens(url, version, &schema, platform, &env) + interpolate_tokens(url, version, &schema, platform, env) .replace("{checksum_file}", &checksum_file) }); let archive_prefix = platform .archive_prefix .as_ref() - .map(|prefix| interpolate_tokens(prefix, version, &schema, platform, &env)); + .map(|prefix| interpolate_tokens(prefix, version, &schema, platform, env)); Ok(Json(DownloadPrebuiltOutput { archive_prefix, @@ -361,7 +361,7 @@ pub fn download_prebuilt( checksum_public_key: schema.install.checksum_public_key, download_url, download_name: Some(download_file), - ..DownloadPrebuiltOutput::default() + ..Default::default() })) } @@ -387,7 +387,7 @@ pub fn locate_executables( ) -> FnResult> { let env = get_host_environment()?; let schema = get_schema()?; - let platform = get_platform(&schema, &env)?; + let platform = get_platform(&schema, env)?; let id = get_plugin_id()?; // On Windows, automatically add the `.exe` extension to all executables. @@ -422,7 +422,7 @@ pub fn locate_executables( &input.context.version, &schema, platform, - &env, + env, ) .into(), ); @@ -505,6 +505,5 @@ pub fn locate_executables( }, globals_lookup_dirs: schema.packages.globals_lookup_dirs, globals_prefix: schema.packages.globals_prefix, - ..Default::default() })) } diff --git a/tools/java/CHANGELOG.md b/tools/java/CHANGELOG.md index 336bacef..bc4b9b39 100644 --- a/tools/java/CHANGELOG.md +++ b/tools/java/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 0.1.1 + +#### 🚀 Updates + +- Updated to support proto v0.60 release. + ## 0.1.0 #### 🚀 Updates diff --git a/tools/java/Cargo.toml b/tools/java/Cargo.toml index cbe3ef27..355bbce1 100644 --- a/tools/java/Cargo.toml +++ b/tools/java/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "java_tool" -version = "0.1.0" +version = "0.1.1" edition = "2024" description = "Java (JDK/JRE) WASM plugin for proto." authors = ["Miles Johnson"] diff --git a/tools/java/src/foojay.rs b/tools/java/src/foojay.rs index a9468e55..399cc68f 100644 --- a/tools/java/src/foojay.rs +++ b/tools/java/src/foojay.rs @@ -239,7 +239,7 @@ mod tests { HostEnvironment { os, libc, - ..HostEnvironment::default() + ..Default::default() } } @@ -249,7 +249,7 @@ mod tests { distribution: Some(Distribution::default()), lib_c_type, operating_system: "linux".into(), - ..FoojayPackage::default() + ..Default::default() } } diff --git a/tools/java/src/java.rs b/tools/java/src/java.rs index f9d9f130..58b37322 100644 --- a/tools/java/src/java.rs +++ b/tools/java/src/java.rs @@ -2,7 +2,7 @@ use crate::{ config::{Distribution, PackageType}, version::to_java_version, }; -use proto_pdk::{AnyResult, UnresolvedVersionSpec, VersionSpec}; +use proto_pdk_api::{AnyResult, UnresolvedVersionSpec, VersionSpec}; #[derive(Default)] pub struct JavaContext { diff --git a/tools/java/src/lib.rs b/tools/java/src/lib.rs index b96e1fa2..fc5666e3 100644 --- a/tools/java/src/lib.rs +++ b/tools/java/src/lib.rs @@ -1,5 +1,8 @@ -pub mod config; +#[cfg(feature = "wasm")] +mod config; +#[cfg(feature = "wasm")] mod foojay; +#[cfg(feature = "wasm")] mod java; #[cfg(feature = "wasm")] mod proto; diff --git a/tools/java/src/proto.rs b/tools/java/src/proto.rs index 68472758..c955f775 100644 --- a/tools/java/src/proto.rs +++ b/tools/java/src/proto.rs @@ -25,7 +25,7 @@ pub fn register_tool(Json(input): Json) -> FnResult) -> FnResult package, None => { return Err(plugin_err!( @@ -301,8 +301,8 @@ pub fn activate_environment( let mut output = ActivateEnvironmentOutput::default(); let home_dir = get_home_dir(&input.context.tool_dir); - if let Some(home) = home_dir.real_path_string() { - output.env.insert("JAVA_HOME".into(), home); + if let Some(home) = home_dir.to_real_path()? { + output.env.insert("JAVA_HOME".into(), home.to_string()); } Ok(Json(output)) diff --git a/tools/java/tests/download_test.rs b/tools/java/tests/download_test.rs index 86d9344e..c27c845b 100644 --- a/tools/java/tests/download_test.rs +++ b/tools/java/tests/download_test.rs @@ -439,8 +439,9 @@ mod java_tool { .await; let mut input = create_locate_input("temurin-21.0.11+10"); - input.context.tool_dir = - VirtualPath::Real(sandbox.path().join(".proto/tools/jdk/21.0.11")); + input.context.tool_dir = plugin + .tool + .to_virtual_path(sandbox.path().join(".proto/tools/jdk/21.0.11")); let output = plugin.locate_executables(input).await; diff --git a/tools/java/tests/metadata_test.rs b/tools/java/tests/metadata_test.rs index 32bacd2b..0ff07cd7 100644 --- a/tools/java/tests/metadata_test.rs +++ b/tools/java/tests/metadata_test.rs @@ -15,7 +15,6 @@ mod java_tool { .await; assert_eq!(metadata.name, "Java"); - assert_eq!(metadata.minimum_proto_version, Some(Version::new(0, 59, 0))); assert!(matches!(metadata.unstable, Switch::Toggle(true))); } diff --git a/tools/moon/CHANGELOG.md b/tools/moon/CHANGELOG.md index 11ed164c..a2b189f8 100644 --- a/tools/moon/CHANGELOG.md +++ b/tools/moon/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 0.4.3 + +#### 🚀 Updates + +- Updated to support proto v0.60 release. + ## 0.4.2 #### 🚀 Updates diff --git a/tools/moon/Cargo.toml b/tools/moon/Cargo.toml index 1cde8f7e..d254e33a 100644 --- a/tools/moon/Cargo.toml +++ b/tools/moon/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "moon_tool" -version = "0.4.2" +version = "0.4.3" edition = "2024" description = "moon WASM plugin for proto." authors = ["Miles Johnson"] diff --git a/tools/moon/src/proto.rs b/tools/moon/src/proto.rs index 30399ab7..58e6d19f 100644 --- a/tools/moon/src/proto.rs +++ b/tools/moon/src/proto.rs @@ -14,10 +14,10 @@ pub fn register_tool(Json(_): Json) -> FnResult [HostArch::X64, HostArch::Arm64], HostOS::MacOS => [HostArch::X64, HostArch::Arm64], @@ -87,7 +87,7 @@ pub fn download_prebuilt( check_supported_os_and_arch( "moon", - &env, + env, permutations! [ HostOS::Linux => [HostArch::X64, HostArch::Arm64], HostOS::MacOS => [HostArch::X64, HostArch::Arm64], diff --git a/tools/node-depman/CHANGELOG.md b/tools/node-depman/CHANGELOG.md index 8d02db95..78872774 100644 --- a/tools/node-depman/CHANGELOG.md +++ b/tools/node-depman/CHANGELOG.md @@ -1,11 +1,35 @@ # Changelog +## 0.20.1 + +#### 🐞 Fixes + +- Fixed an issue where the `nub` binary wasn't executable. + +## 0.20.0 + +#### 🚀 Updates + +- Added experimental support for Nub: https://nubjs.com/. +- Added experimental support for pnpm v12 (Rust based). +- Updated version detection to loop through all possible fields (`packageManager`, `engines`, etc) to find an applicable version, instead of failing on the first invalid version. + +#### 🐞 Fixes + +- Fixed an issue where auth headers were not included in load verion requests, which would cause failures for private registries. + +## 0.19.1 + +#### 🚀 Updates + +- Updated to support proto v0.60 release. + ## 0.19.0 #### 🚀 Updates - Updated to support proto v0.59 release. -- Added experimental support for yarn v6. +- Added experimental support for yarn v6 (Rust based). ## 0.18.1 diff --git a/tools/node-depman/Cargo.toml b/tools/node-depman/Cargo.toml index 6b2de4f9..4ab197a7 100644 --- a/tools/node-depman/Cargo.toml +++ b/tools/node-depman/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "node_depman_tool" -version = "0.19.0" +version = "0.20.1" edition = "2024" description = "Node.js dependency managers (npm, pnpm, yarn) WASM plugin for proto." authors = ["Miles Johnson"] diff --git a/tools/node-depman/src/package_manager.rs b/tools/node-depman/src/package_manager.rs index 84f3a274..f79530ff 100644 --- a/tools/node-depman/src/package_manager.rs +++ b/tools/node-depman/src/package_manager.rs @@ -5,7 +5,10 @@ use crate::yarn_compat::*; use npmrc_config_rs::{ Credentials, LoadOptions, NpmrcConfig, nerf_dart, registry::parse_registry_url, }; -use proto_pdk::{AnyResult, VersionSpec, VirtualPath, get_plugin_id}; +use proto_pdk::{ + AnyResult, HostArch, HostEnvironment, HostLibc, HostOS, PluginError, VersionSpec, VirtualPath, + get_plugin_id, +}; use rustc_hash::FxHashMap; use starbase_utils::{fs::find_upwards, yaml}; @@ -13,11 +16,18 @@ use starbase_utils::{fs::find_upwards, yaml}; pub enum PackageManager { Npm, + Nub, + Pnpm, + // Major changes / New shims Pnpm11, + // Rust based + Pnpm12, Yarn1, + // Major rewrite with different APIs / PNP Yarn2to5, + // Rust based Yarn6, } @@ -29,6 +39,8 @@ impl PackageManager { Self::Yarn1 } else if id.to_lowercase().contains("pnpm") { Self::Pnpm + } else if id.to_lowercase().contains("nub") { + Self::Nub } else { Self::Npm }) @@ -39,16 +51,20 @@ impl PackageManager { if manager == Self::Pnpm { manager = match version { - VersionSpec::Canary => Self::Pnpm11, + VersionSpec::Canary => Self::Pnpm12, VersionSpec::Alias(alias) => { if alias == "latest" { Self::Pnpm11 + } else if alias == "next" { + Self::Pnpm12 } else { Self::Pnpm } } VersionSpec::Version(version) => { - if version.major >= 11 { + if version.major >= 12 { + Self::Pnpm12 + } else if version.major >= 11 { Self::Pnpm11 } else { Self::Pnpm @@ -86,29 +102,138 @@ impl PackageManager { matches!(self, Self::Npm) } + pub fn is_nub(&self) -> bool { + matches!(self, Self::Nub) + } + pub fn is_pnpm(&self) -> bool { - matches!(self, Self::Pnpm | Self::Pnpm11) + matches!(self, Self::Pnpm | Self::Pnpm11 | Self::Pnpm12) } pub fn is_yarn(&self) -> bool { matches!(self, Self::Yarn1 | Self::Yarn2to5 | Self::Yarn6) } + pub fn is_rust_based(&self) -> bool { + matches!(self, Self::Nub | Self::Pnpm12 | Self::Yarn6) + } + pub fn get_bin_name(&self) -> String { match self { Self::Npm => "npm".into(), - Self::Pnpm | Self::Pnpm11 => "pnpm".into(), + Self::Nub => "nub".into(), + Self::Pnpm | Self::Pnpm11 | Self::Pnpm12 => "pnpm".into(), Self::Yarn1 | Self::Yarn2to5 | Self::Yarn6 => "yarn".into(), } } pub fn get_package_name(&self) -> String { match self { + Self::Nub => "@nubjs/nub".into(), Self::Yarn2to5 => "@yarnpkg/cli-dist".into(), _ => self.get_bin_name(), } } + pub fn get_package_name_for_download(&self, env: &HostEnvironment) -> AnyResult { + let arch = match env.arch { + HostArch::Arm64 => "arm64", + HostArch::X64 => "x64", + other => { + return Err(PluginError::UnsupportedArch { + tool: self.get_bin_name(), + arch: other.to_string(), + } + .into()); + } + }; + + let os = match env.os { + HostOS::MacOS => "darwin", + HostOS::Linux => "linux", + HostOS::Windows => "win32", + other => { + return Err(PluginError::UnsupportedOS { + tool: self.get_bin_name(), + os: other.to_string(), + } + .into()); + } + }; + + match self { + Self::Nub => { + let mut name = format!("@nubjs/nub-{os}-{arch}"); + + if env.libc == HostLibc::Musl { + name.push_str("-musl"); + } + + Ok(name) + } + Self::Pnpm12 => { + let mut name = format!("@pnpm/exe.{os}-{arch}"); + + if env.libc == HostLibc::Musl { + name.push_str("-musl"); + } + + Ok(name) + } + _ => Ok(self.get_package_name()), + } + } + + pub fn get_global_lookup_dirs(&self, env: &HostEnvironment) -> Vec { + let mut dirs: Vec = vec![ + "$PREFIX/bin".into(), + "$PREFIX/shims".into(), + "$PROTO_HOME/tools/node/$PROTO_NODE_VERSION/bin".into(), + ]; + + match self { + // https://docs.npmjs.com/cli/v9/configuring-npm/folders#prefix-configuration + // https://github.com/npm/cli/blob/latest/lib/npm.js + // https://github.com/npm/cli/blob/latest/workspaces/config/lib/index.js#L339 + Self::Npm => { + dirs.push("$TOOL_DIR/shims".into()); + } + // Nub's global layout is modeled on pnpm v11 and intentionally + // shares pnpm's directories. There is no NUB_HOME. + // https://github.com/nubjs/nub/blob/main/vendor/aube/crates/aube/src/commands/global.rs + Self::Nub | + // https://pnpm.io/npmrc#global-dir + // https://github.com/pnpm/pnpm/blob/main/config/config/src/index.ts#L350 + // https://github.com/pnpm/pnpm/blob/main/config/config/src/dirs.ts#L40 + Self::Pnpm | Self::Pnpm11 | Self::Pnpm12 => { + dirs.push("$PNPM_HOME".into()); + + if env.os.is_windows() { + dirs.push("$LOCALAPPDATA/pnpm".into()); + } else if env.os.is_mac() { + dirs.push("$HOME/Library/pnpm".into()); + } else { + dirs.push("$XDG_DATA_HOME/pnpm".into()); + dirs.push("$HOME/.local/share/pnpm".into()); + } + } + // https://github.com/yarnpkg/yarn/blob/master/src/cli/commands/global.js#L84 + Self::Yarn1 | Self::Yarn2to5 | Self::Yarn6 => { + if env.os.is_windows() { + dirs.push("$LOCALAPPDATA/Yarn/bin".into()); + } + + dirs.push("$HOME/.yarn/bin".into()); + } + } + + if env.os.is_windows() { + return dirs.into_iter().map(|dir| dir.replace("/", "\\")).collect(); + } + + dirs + } + pub fn get_http_headers( &self, registry_url: &str, @@ -118,7 +243,7 @@ impl PackageManager { let url = parse_registry_url(registry_url)?; let credentials = match self { - Self::Npm | Self::Pnpm | Self::Pnpm11 => { + Self::Npm | Self::Nub | Self::Pnpm | Self::Pnpm11 | Self::Pnpm12 => { let rc = NpmrcConfig::load_with_options(LoadOptions { cwd: Some(working_dir.into()), global_prefix: None, diff --git a/tools/node-depman/src/proto.rs b/tools/node-depman/src/proto.rs index dfcd3f66..edfd68e0 100644 --- a/tools/node-depman/src/proto.rs +++ b/tools/node-depman/src/proto.rs @@ -21,7 +21,6 @@ const CMD_SHIM_TEMPLATE: &str = include_str!("../templates/cmd-shim.cmd"); extern "ExtismHost" { fn exec_command(input: Json) -> Json; fn get_env_var(key: &str) -> String; - fn to_virtual_path(input: String) -> String; } #[plugin_fn] @@ -34,16 +33,21 @@ pub fn register_tool(Json(_): Json) -> FnResult(&input.content) { let manager_name = PackageManager::detect()?.get_bin_name(); + let mut candidates = vec![]; if let Some(constraint) = extract_dev_engine_package_manager_version(&package_json, &manager_name) { - version = Some(UnresolvedVersionSpec::parse(constraint)?); + candidates.push(constraint); } - if version.is_none() - && let Some(constraint) = extract_package_manager_version(&package_json, &manager_name) - { - version = Some(UnresolvedVersionSpec::parse(constraint)?); + if let Some(constraint) = extract_package_manager_version(&package_json, &manager_name) { + candidates.push(constraint); } - if version.is_none() - && let Some(constraint) = - extract_volta_version(&package_json, &input.path, &manager_name)? + if let Some(constraint) = extract_volta_version(&package_json, &input.path, &manager_name)? { - version = Some(UnresolvedVersionSpec::parse(constraint)?); + candidates.push(constraint); + } + + if let Some(constraint) = extract_engine_version(&package_json, &manager_name) { + candidates.push(constraint); + } + + let mut error = None; + + for candidates in candidates { + match UnresolvedVersionSpec::parse(candidates) { + Ok(spec) => { + version = Some(spec); + break; + } + Err(err) => { + if error.is_none() { + error = Some(err); + } + } + }; } if version.is_none() - && let Some(constraint) = extract_engine_version(&package_json, &manager_name) + && let Some(error) = error { - version = Some(UnresolvedVersionSpec::parse(constraint)?); + return Err(plugin_err!("{error}")); } } @@ -157,14 +178,22 @@ pub fn unpin_version(Json(input): Json) -> FnResult) -> FnResult> { +pub fn load_versions(Json(input): Json) -> FnResult> { let mut output = LoadVersionsOutput::default(); let manager = PackageManager::detect()?; let registry_url = get_tool_config::()?.registry_url; let package_name = manager.get_package_name(); - - let mut map_output = |res_text: String, is_yarn: bool| -> Result<(), Error> { - let res = parse_registry_response(res_text, is_yarn)?; + let headers = manager.get_http_headers(®istry_url, &input.context.working_dir)?; + + let mut fetch_versions = |url: String, is_yarn: bool| -> Result<(), Error> { + let res = parse_registry_response( + fetch(SendRequestInput { + url, + headers: headers.clone(), + })? + .text()?, + is_yarn, + )?; for item in res.versions.values() { output.versions.push(VersionSpec::parse(&item.version)?); @@ -193,13 +222,10 @@ pub fn load_versions(Json(_input): Json) -> FnResult) -> FnResult { + if input.initial == UnresolvedVersionSpec::Canary { + output.candidate = Some(UnresolvedVersionSpec::parse("^12.0.0-rc.0")?); + } else if let UnresolvedVersionSpec::Alias(alias) = input.initial + && (alias == "rust" || alias == "next") + { + output.candidate = Some(UnresolvedVersionSpec::parse("^12.0.0-rc.0")?); + } + } + PackageManager::Yarn1 | PackageManager::Yarn2to5 | PackageManager::Yarn6 => { if input.initial == UnresolvedVersionSpec::Canary { output.candidate = Some(UnresolvedVersionSpec::parse("^6.0.0-rc.0")?); @@ -326,11 +359,11 @@ pub fn download_prebuilt( })); } + let env = get_host_environment()?; + // Yarn v6 is Rust based and is NOT installed from the npm registry! // https://v6.yarnpkg.com/getting-started if manager == PackageManager::Yarn6 { - let env = get_host_environment()?; - let arch = match env.arch { HostArch::Arm64 => "aarch64", HostArch::X64 => "x86_64", @@ -379,7 +412,7 @@ pub fn download_prebuilt( } // Everything else is provided by the npm registry - let mut package_name = manager.get_package_name(); + let mut package_name = manager.get_package_name_for_download(env)?; // Version 2.4.3 was published to the wrong package. It should // have been published to `@yarnpkg/cli-dist` but was published @@ -422,86 +455,73 @@ pub fn locate_executables( Json(input): Json, ) -> FnResult> { let env = get_host_environment()?; + let config = get_tool_config::()?; let manager = PackageManager::detect_from_version(&input.context.version)?; + let rust_based = manager.is_rust_based(); let mut secondary = FxHashMap::::default(); let primary; - if !input.install_dir.join("shims").exists() { - create_internal_shims(&env, &input.install_dir, &manager)?; + if !rust_based && !input.install_dir.join("shims").exists() { + create_internal_shims(env, &input.install_dir, &manager)?; } - // These are the directories that contain the executable binaries, - // NOT where the packages/node modules are stored. Some package managers - // have separate folders for the 2 processes, and then create symlinks. - let mut globals_lookup_dirs = vec![ - "$PREFIX/bin".into(), - "$PREFIX/shims".into(), - "$PROTO_HOME/tools/node/$PROTO_NODE_VERSION/bin".into(), - ]; - match &manager { PackageManager::Npm => { primary = ExecutableConfig::new_primary("shims/npm"); - - // npx secondary.insert("npx".into(), ExecutableConfig::new("shims/npx")); - - // node-gyp secondary.insert( "node-gyp".into(), ExecutableConfig::with_parent("node_modules/node-gyp/bin/node-gyp.js", "node"), ); + } + PackageManager::Nub => { + let exe_name = env.os.get_exe_name("bin/nub"); - // https://docs.npmjs.com/cli/v9/configuring-npm/folders#prefix-configuration - // https://github.com/npm/cli/blob/latest/lib/npm.js - // https://github.com/npm/cli/blob/latest/workspaces/config/lib/index.js#L339 - globals_lookup_dirs.push("$TOOL_DIR/shims".into()); + // Nub binaries are not executable by default within the npm package, + // so we need to make them executable on our end! + // https://github.com/nubjs/nub/blob/main/npm/nub/postinstall.js#L22 + primary = ExecutableConfig::new_primary(&exe_name).update_perms(true); + secondary.insert( + "nubx".into(), + ExecutableConfig::new(exe_name).update_perms(true), + ); } - PackageManager::Pnpm | PackageManager::Pnpm11 => { + PackageManager::Pnpm => { primary = ExecutableConfig::new_primary("shims/pnpm"); - - // pnpx secondary.insert("pnpx".into(), ExecutableConfig::new("shims/pnpx")); + } + PackageManager::Pnpm11 => { + primary = ExecutableConfig::new_primary("shims/pnpm"); + secondary.insert("pnpx".into(), ExecutableConfig::new("shims/pnpx")); + secondary.insert("pn".into(), ExecutableConfig::new("shims/pn")); + secondary.insert("pnx".into(), ExecutableConfig::new("shims/pnx")); + } + PackageManager::Pnpm12 => { + let exe_name = env.os.get_exe_name("pnpm"); - if manager == PackageManager::Pnpm11 { - secondary.insert("pn".into(), ExecutableConfig::new("shims/pn")); - secondary.insert("pnx".into(), ExecutableConfig::new("shims/pnx")); - } + primary = ExecutableConfig::new_primary(&exe_name); + secondary.insert("pn".into(), ExecutableConfig::new(&exe_name)); - // https://pnpm.io/npmrc#global-dir - // https://github.com/pnpm/pnpm/blob/main/config/config/src/index.ts#L350 - // https://github.com/pnpm/pnpm/blob/main/config/config/src/dirs.ts#L40 - globals_lookup_dirs.push("$PNPM_HOME".into()); + let pnpx_config = ExecutableConfig::new(exe_name) + .no_bin(true) + .shim_before_args(StringOrVec::String("dlx".into())); - if env.os.is_windows() { - globals_lookup_dirs.push("$LOCALAPPDATA\\pnpm".into()); - } else if env.os.is_mac() { - globals_lookup_dirs.push("$HOME/Library/pnpm".into()); - } else { - globals_lookup_dirs.push("$HOME/.local/share/pnpm".into()); - } + secondary.insert("pnpx".into(), pnpx_config.clone()); + secondary.insert("pnx".into(), pnpx_config); } - PackageManager::Yarn1 | PackageManager::Yarn2to5 | PackageManager::Yarn6 => { - if manager == PackageManager::Yarn6 { - primary = ExecutableConfig::new_primary(env.os.get_exe_name("yarn-bin")); - } else { - primary = ExecutableConfig::new_primary("shims/yarn"); - - // yarnpkg - secondary.insert("yarnpkg".into(), ExecutableConfig::new("shims/yarn")); - } - - // https://github.com/yarnpkg/yarn/blob/master/src/cli/commands/global.js#L84 - if env.os.is_windows() { - globals_lookup_dirs.push("$LOCALAPPDATA\\Yarn\\bin".into()); - globals_lookup_dirs.push("$HOME\\.yarn\\bin".into()); - } else { - globals_lookup_dirs.push("$HOME/.yarn/bin".into()); - } + PackageManager::Yarn1 | PackageManager::Yarn2to5 => { + primary = ExecutableConfig::new_primary("shims/yarn"); + secondary.insert("yarnpkg".into(), ExecutableConfig::new("shims/yarn")); + } + PackageManager::Yarn6 => { + primary = ExecutableConfig::new_primary(env.os.get_exe_name("yarn-bin")); } }; - let config = get_tool_config::()?; + // These are the directories that contain the executable binaries, + // NOT where the packages/node modules are stored. Some package managers + // have separate folders for the 2 processes, and then create symlinks. + let mut globals_lookup_dirs = manager.get_global_lookup_dirs(env); // If only shared dir, clear everything else if config.shared_globals_dir { @@ -516,6 +536,11 @@ pub fn locate_executables( // Update the permissions of each executable since they are custom shims exes.iter_mut().for_each(|(name, config)| { + if rust_based { + return; + } + + // Only applies to shims, not real binaries! config.no_bin = true; if name != "node-gyp" { @@ -535,7 +560,7 @@ pub fn locate_executables( Ok(Json(LocateExecutablesOutput { exes, globals_lookup_dirs, - ..LocateExecutablesOutput::default() + ..Default::default() })) } @@ -548,17 +573,12 @@ pub fn activate_environment( if config.shared_globals_dir && let Some(globals_dir) = &input.globals_dir + && let Some(globals_dir) = globals_dir.to_real_path()? { // Includes trailing /bin folder - let globals_bin_dir = globals_dir.real_path_string().unwrap(); + let globals_bin_dir = globals_dir.to_string(); // Parent directory, doesn't include /bin folder - let globals_root_dir = globals_dir - .real_path() - .unwrap() - .parent() - .unwrap() - .to_string_lossy() - .to_string(); + let globals_root_dir = globals_dir.parent().unwrap().to_string(); let env = get_host_environment()?; let manager = PackageManager::detect_from_version(&input.context.version)?; @@ -577,9 +597,22 @@ pub fn activate_environment( ); } + // Nub mirrors pnpm's global-dir/global-bin-dir split, but only + // the npm-compat env family is always honored (pnpm_config_* + // requires pnpm to be the incumbent package manager, and PREFIX + // is never read for install locations). + PackageManager::Nub => { + output + .env + .insert("npm_config_global_dir".into(), globals_root_dir); + output + .env + .insert("npm_config_global_bin_dir".into(), globals_bin_dir); + } + // Pnpm has explicit support for the bin and root dirs, // which makes this super simple to handle. - PackageManager::Pnpm | PackageManager::Pnpm11 => { + PackageManager::Pnpm | PackageManager::Pnpm11 | PackageManager::Pnpm12 => { output .env .insert("pnpm_config_global_dir".into(), globals_root_dir); @@ -642,8 +675,8 @@ fn create_internal_shims( PackageManager::Yarn1 | PackageManager::Yarn2to5 => { create_internal_shim(env, tool_dir, "yarn", "yarn.js")?; } - // Yarn v6+ is a native binary and requires no shims - PackageManager::Yarn6 => {} + // nub, yarn v6+, and pnpm v12+ are a native binary and require no shims + PackageManager::Nub | PackageManager::Pnpm12 | PackageManager::Yarn6 => {} }; Ok(()) diff --git a/tools/node-depman/tests/activate_test.rs b/tools/node-depman/tests/activate_test.rs index 4aeee723..053dc1ed 100644 --- a/tools/node-depman/tests/activate_test.rs +++ b/tools/node-depman/tests/activate_test.rs @@ -1,7 +1,6 @@ use proto_pdk_api::ActivateEnvironmentInput; use proto_pdk_test_utils::*; use std::collections::HashMap; -use std::path::PathBuf; #[derive(Debug, Default, serde::Deserialize, serde::Serialize)] #[serde(default, deny_unknown_fields, rename_all = "kebab-case")] @@ -16,11 +15,7 @@ mod node_depman_tool { use super::*; fn create_globals_dir() -> VirtualPath { - VirtualPath::Virtual { - path: PathBuf::from("/proto/tools/node/globals/bin"), - virtual_prefix: PathBuf::from("/proto"), - real_prefix: PathBuf::from("/.proto"), - } + VirtualPath::new("/proto/tools/node/globals/bin") } mod npm { @@ -73,7 +68,7 @@ mod node_depman_tool { let result = plugin .activate_environment(ActivateEnvironmentInput { globals_dir: Some(create_globals_dir()), - ..ActivateEnvironmentInput::default() + ..Default::default() }) .await; @@ -81,16 +76,98 @@ mod node_depman_tool { result.env, HashMap::from_iter([( "PREFIX".into(), - if cfg!(windows) { - "/.proto/tools/node/globals/bin".into() - } else { - "/.proto/tools/node/globals".into() - } + sandbox + .path() + .join(if cfg!(windows) { + ".proto/tools/node/globals/bin" + } else { + ".proto/tools/node/globals" + }) + .to_string_lossy() + .to_string() )]) ); } } + mod nub { + use super::*; + + #[tokio::test(flavor = "multi_thread")] + + async fn does_nothing_if_not_configured() { + let sandbox = create_empty_proto_sandbox(); + let plugin = sandbox.create_plugin("nub-test").await; + + let result = plugin + .activate_environment(ActivateEnvironmentInput::default()) + .await; + + assert!(result.env.is_empty()); + } + + #[tokio::test(flavor = "multi_thread")] + + async fn does_nothing_if_disabled() { + let sandbox = create_empty_proto_sandbox(); + let plugin = sandbox + .create_plugin_with_config("nub-test", |config| { + config.tool_config(NodeDepmanPluginConfig { + shared_globals_dir: false, + }); + }) + .await; + + let result = plugin + .activate_environment(ActivateEnvironmentInput::default()) + .await; + + assert!(result.env.is_empty()); + } + + #[tokio::test(flavor = "multi_thread")] + + async fn adds_env_var() { + let sandbox = create_empty_proto_sandbox(); + let plugin = sandbox + .create_plugin_with_config("nub-test", |config| { + config.tool_config(NodeDepmanPluginConfig { + shared_globals_dir: true, + }); + }) + .await; + + let result = plugin + .activate_environment(ActivateEnvironmentInput { + globals_dir: Some(create_globals_dir()), + ..Default::default() + }) + .await; + + assert_eq!( + result.env, + HashMap::from_iter([ + ( + "npm_config_global_dir".into(), + sandbox + .path() + .join(".proto/tools/node/globals") + .to_string_lossy() + .to_string() + ), + ( + "npm_config_global_bin_dir".into(), + sandbox + .path() + .join(".proto/tools/node/globals/bin") + .to_string_lossy() + .to_string() + ) + ]) + ); + } + } + mod pnpm { use super::*; @@ -141,7 +218,7 @@ mod node_depman_tool { let result = plugin .activate_environment(ActivateEnvironmentInput { globals_dir: Some(create_globals_dir()), - ..ActivateEnvironmentInput::default() + ..Default::default() }) .await; @@ -150,11 +227,64 @@ mod node_depman_tool { HashMap::from_iter([ ( "pnpm_config_global_dir".into(), - "/.proto/tools/node/globals".into() + sandbox + .path() + .join(".proto/tools/node/globals") + .to_string_lossy() + .to_string() ), ( "pnpm_config_global_bin_dir".into(), - "/.proto/tools/node/globals/bin".into() + sandbox + .path() + .join(".proto/tools/node/globals/bin") + .to_string_lossy() + .to_string() + ) + ]) + ); + } + + #[tokio::test(flavor = "multi_thread")] + + async fn adds_env_var_for_v12() { + let sandbox = create_empty_proto_sandbox(); + let plugin = sandbox + .create_plugin_with_config("pnpm-test", |config| { + config.tool_config(NodeDepmanPluginConfig { + shared_globals_dir: true, + }); + }) + .await; + + let result = plugin + .activate_environment(ActivateEnvironmentInput { + context: PluginContext { + version: VersionSpec::parse("12.0.0").unwrap(), + ..Default::default() + }, + globals_dir: Some(create_globals_dir()), + }) + .await; + + assert_eq!( + result.env, + HashMap::from_iter([ + ( + "pnpm_config_global_dir".into(), + sandbox + .path() + .join(".proto/tools/node/globals") + .to_string_lossy() + .to_string() + ), + ( + "pnpm_config_global_bin_dir".into(), + sandbox + .path() + .join(".proto/tools/node/globals/bin") + .to_string_lossy() + .to_string() ) ]) ); @@ -211,13 +341,20 @@ mod node_depman_tool { let result = plugin .activate_environment(ActivateEnvironmentInput { globals_dir: Some(create_globals_dir()), - ..ActivateEnvironmentInput::default() + ..Default::default() }) .await; assert_eq!( result.env, - HashMap::from_iter([("PREFIX".into(), "/.proto/tools/node/globals".into())]) + HashMap::from_iter([( + "PREFIX".into(), + sandbox + .path() + .join(".proto/tools/node/globals") + .to_string_lossy() + .to_string() + )]) ); } } diff --git a/tools/node-depman/tests/download_test.rs b/tools/node-depman/tests/download_test.rs index b9c3c8ee..5b36c76c 100644 --- a/tools/node-depman/tests/download_test.rs +++ b/tools/node-depman/tests/download_test.rs @@ -58,7 +58,7 @@ mod node_depman_tool { version: VersionSpec::parse("9.0.0").unwrap(), ..Default::default() }, - install_dir: VirtualPath::Real(sandbox.path().into()), + install_dir: plugin.tool.to_virtual_path(sandbox.path()), }) .await .exes @@ -116,7 +116,7 @@ mod node_depman_tool { .download_prebuilt(DownloadPrebuiltInput { context: PluginContext { version: VersionSpec::parse("9.0.0").unwrap(), - working_dir: VirtualPath::Real(sandbox.path().into()), + working_dir: plugin.tool.to_virtual_path(sandbox.path()), ..Default::default() }, ..Default::default() @@ -154,7 +154,7 @@ mod node_depman_tool { .download_prebuilt(DownloadPrebuiltInput { context: PluginContext { version: VersionSpec::parse("9.0.0").unwrap(), - working_dir: VirtualPath::Real(sandbox.path().into()), + working_dir: plugin.tool.to_virtual_path(sandbox.path()), ..Default::default() }, ..Default::default() @@ -173,17 +173,480 @@ mod node_depman_tool { } } + mod nub { + use super::*; + + // Nub is Rust based and downloaded from the npm registry + // as an os/arch specific package: @nubjs/nub-{os}-{arch} + + generate_download_install_tests!("nub-test", "0.7.4"); + + #[tokio::test(flavor = "multi_thread")] + async fn supports_prebuilt_macos_arm64() { + let sandbox = create_empty_proto_sandbox(); + let plugin = sandbox + .create_plugin_with_config("nub-test", |config| { + config.host(HostOS::MacOS, HostArch::Arm64); + }) + .await; + + assert_eq!( + plugin + .download_prebuilt(DownloadPrebuiltInput { + context: PluginContext { + version: VersionSpec::parse("0.7.4").unwrap(), + ..Default::default() + }, + ..Default::default() + }) + .await, + DownloadPrebuiltOutput { + archive_prefix: Some("package".into()), + download_url: "https://registry.npmjs.org/@nubjs/nub-darwin-arm64/-/nub-darwin-arm64-0.7.4.tgz".into(), + ..Default::default() + } + ); + } + + #[tokio::test(flavor = "multi_thread")] + async fn supports_prebuilt_linux_x64_gnu() { + let sandbox = create_empty_proto_sandbox(); + let plugin = sandbox + .create_plugin_with_config("nub-test", |config| { + config.host_with(|host| { + host.os = HostOS::Linux; + host.arch = HostArch::X64; + host.libc = HostLibc::Gnu; + }); + }) + .await; + + // Gnu is supported and has no libc suffix + assert_eq!( + plugin + .download_prebuilt(DownloadPrebuiltInput { + context: PluginContext { + version: VersionSpec::parse("0.7.4").unwrap(), + ..Default::default() + }, + ..Default::default() + }) + .await, + DownloadPrebuiltOutput { + archive_prefix: Some("package".into()), + download_url: "https://registry.npmjs.org/@nubjs/nub-linux-x64/-/nub-linux-x64-0.7.4.tgz".into(), + ..Default::default() + } + ); + } + + #[tokio::test(flavor = "multi_thread")] + async fn supports_prebuilt_linux_arm64_musl() { + let sandbox = create_empty_proto_sandbox(); + let plugin = sandbox + .create_plugin_with_config("nub-test", |config| { + config.host_with(|host| { + host.os = HostOS::Linux; + host.arch = HostArch::Arm64; + host.libc = HostLibc::Musl; + }); + }) + .await; + + assert_eq!( + plugin + .download_prebuilt(DownloadPrebuiltInput { + context: PluginContext { + version: VersionSpec::parse("0.7.4").unwrap(), + ..Default::default() + }, + ..Default::default() + }) + .await, + DownloadPrebuiltOutput { + archive_prefix: Some("package".into()), + download_url: "https://registry.npmjs.org/@nubjs/nub-linux-arm64-musl/-/nub-linux-arm64-musl-0.7.4.tgz".into(), + ..Default::default() + } + ); + } + + #[tokio::test(flavor = "multi_thread")] + async fn supports_prebuilt_windows_x64() { + let sandbox = create_empty_proto_sandbox(); + let plugin = sandbox + .create_plugin_with_config("nub-test", |config| { + config.host(HostOS::Windows, HostArch::X64); + }) + .await; + + assert_eq!( + plugin + .download_prebuilt(DownloadPrebuiltInput { + context: PluginContext { + version: VersionSpec::parse("0.7.4").unwrap(), + ..Default::default() + }, + ..Default::default() + }) + .await, + DownloadPrebuiltOutput { + archive_prefix: Some("package".into()), + download_url: "https://registry.npmjs.org/@nubjs/nub-win32-x64/-/nub-win32-x64-0.7.4.tgz".into(), + ..Default::default() + } + ); + } + + #[tokio::test(flavor = "multi_thread")] + async fn downloads_from_custom_registry() { + let sandbox = create_empty_proto_sandbox(); + let plugin = sandbox + .create_plugin_with_config("nub-test", |config| { + config + .host(HostOS::MacOS, HostArch::X64) + .tool_config(NodeDepmanPluginConfig { + registry_url: "https://some-internal-url.example".into(), + }); + }) + .await; + + assert_eq!( + plugin + .download_prebuilt(DownloadPrebuiltInput { + context: PluginContext { + version: VersionSpec::parse("0.7.4").unwrap(), + ..Default::default() + }, + ..Default::default() + }) + .await, + DownloadPrebuiltOutput { + archive_prefix: Some("package".into()), + download_url: "https://some-internal-url.example/@nubjs/nub-darwin-x64/-/nub-darwin-x64-0.7.4.tgz".into(), + ..Default::default() + } + ); + } + + #[tokio::test(flavor = "multi_thread")] + async fn extracts_auth_token_header() { + let sandbox = create_empty_proto_sandbox(); + sandbox.create_file(".npmrc", "//registry.npmjs.org/:_authToken = abc123"); + + let plugin = sandbox + .create_plugin_with_config("nub-test", |config| { + config.host(HostOS::Linux, HostArch::Arm64); + }) + .await; + + assert_eq!( + plugin + .download_prebuilt(DownloadPrebuiltInput { + context: PluginContext { + version: VersionSpec::parse("0.7.4").unwrap(), + working_dir: plugin.tool.to_virtual_path(sandbox.path()), + ..Default::default() + }, + ..Default::default() + }) + .await, + DownloadPrebuiltOutput { + archive_prefix: Some("package".into()), + download_url: "https://registry.npmjs.org/@nubjs/nub-linux-arm64/-/nub-linux-arm64-0.7.4.tgz".into(), + http_headers: FxHashMap::from_iter([( + "Authorization".into(), + "Bearer abc123".into() + )]), + ..Default::default() + } + ); + } + + #[tokio::test(flavor = "multi_thread")] + #[should_panic(expected = "unsupported architecture x86.")] + async fn doesnt_support_other_archs() { + let sandbox = create_empty_proto_sandbox(); + let plugin = sandbox + .create_plugin_with_config("nub-test", |config| { + config.host(HostOS::Linux, HostArch::X86); + }) + .await; + + plugin + .download_prebuilt(DownloadPrebuiltInput { + context: PluginContext { + version: VersionSpec::parse("0.7.4").unwrap(), + ..Default::default() + }, + ..Default::default() + }) + .await; + } + + #[tokio::test(flavor = "multi_thread")] + #[should_panic(expected = "unsupported OS freebsd.")] + async fn doesnt_support_other_os() { + let sandbox = create_empty_proto_sandbox(); + let plugin = sandbox + .create_plugin_with_config("nub-test", |config| { + config.host(HostOS::FreeBSD, HostArch::Arm64); + }) + .await; + + plugin + .download_prebuilt(DownloadPrebuiltInput { + context: PluginContext { + version: VersionSpec::parse("0.7.4").unwrap(), + ..Default::default() + }, + ..Default::default() + }) + .await; + } + + #[tokio::test(flavor = "multi_thread")] + #[should_panic(expected = "nub does not support canary/nightly versions.")] + async fn doesnt_support_canary() { + let sandbox = create_empty_proto_sandbox(); + let plugin = sandbox + .create_plugin_with_config("nub-test", |config| { + config.host(HostOS::MacOS, HostArch::Arm64); + }) + .await; + + plugin + .download_prebuilt(DownloadPrebuiltInput { + context: PluginContext { + version: VersionSpec::parse("canary").unwrap(), + ..Default::default() + }, + ..Default::default() + }) + .await; + } + + #[tokio::test(flavor = "multi_thread")] + async fn locates_native_bin() { + let sandbox = create_empty_proto_sandbox(); + let plugin = sandbox + .create_plugin_with_config("nub-test", |config| { + config.host(HostOS::MacOS, HostArch::Arm64); + }) + .await; + + let exes = plugin + .locate_executables(LocateExecutablesInput { + context: PluginContext { + version: VersionSpec::parse("0.7.4").unwrap(), + ..Default::default() + }, + install_dir: plugin.tool.to_virtual_path(sandbox.path()), + }) + .await + .exes; + + assert_eq!(exes.get("nub").unwrap().exe_path, Some("bin/nub".into())); + + // nubx runs through the primary binary + assert_eq!(exes.get("nubx").unwrap().exe_path, Some("bin/nub".into())); + + // No internal shims are created for the native binary + assert!(!sandbox.path().join("shims/nub").exists()); + } + + #[tokio::test(flavor = "multi_thread")] + async fn locates_native_bin_windows() { + let sandbox = create_empty_proto_sandbox(); + let plugin = sandbox + .create_plugin_with_config("nub-test", |config| { + config.host(HostOS::Windows, HostArch::X64); + }) + .await; + + let exes = plugin + .locate_executables(LocateExecutablesInput { + context: PluginContext { + version: VersionSpec::parse("0.7.4").unwrap(), + ..Default::default() + }, + install_dir: plugin.tool.to_virtual_path(sandbox.path()), + }) + .await + .exes; + + // The .exe extension must not be rewritten to .cmd + assert_eq!( + exes.get("nub").unwrap().exe_path, + Some("bin/nub.exe".into()) + ); + } + } + mod pnpm { use super::*; - generate_download_install_tests!("pnpm-test", "8.0.0"); + generate_download_install_tests!("pnpm-test", "8.0.0"); + + #[tokio::test(flavor = "multi_thread")] + async fn supports_prebuilt() { + let sandbox = create_empty_proto_sandbox(); + let plugin = sandbox + .create_plugin_with_config("pnpm-test", |config| { + config.host(HostOS::Windows, HostArch::X64); + }) + .await; + + assert_eq!( + plugin + .download_prebuilt(DownloadPrebuiltInput { + context: PluginContext { + version: VersionSpec::parse("8.0.0").unwrap(), + ..Default::default() + }, + ..Default::default() + }) + .await, + DownloadPrebuiltOutput { + archive_prefix: Some("package".into()), + download_url: "https://registry.npmjs.org/pnpm/-/pnpm-8.0.0.tgz".into(), + ..Default::default() + } + ); + } + + #[tokio::test(flavor = "multi_thread")] + async fn supports_prebuilt_v11() { + let sandbox = create_empty_proto_sandbox(); + let plugin = sandbox + .create_plugin_with_config("pnpm-test", |config| { + config.host(HostOS::MacOS, HostArch::Arm64); + }) + .await; + + // v11 is still a platform agnostic tarball, not a native binary + assert_eq!( + plugin + .download_prebuilt(DownloadPrebuiltInput { + context: PluginContext { + version: VersionSpec::parse("11.0.0").unwrap(), + ..Default::default() + }, + ..Default::default() + }) + .await, + DownloadPrebuiltOutput { + archive_prefix: Some("package".into()), + download_url: "https://registry.npmjs.org/pnpm/-/pnpm-11.0.0.tgz".into(), + ..Default::default() + } + ); + } + + #[tokio::test(flavor = "multi_thread")] + async fn locates_default_bin() { + let sandbox = create_empty_proto_sandbox(); + let plugin = sandbox + .create_plugin_with_config("pnpm-test", |config| { + config.host(HostOS::Windows, HostArch::X64); + }) + .await; + + assert_eq!( + plugin + .locate_executables(LocateExecutablesInput { + context: PluginContext { + version: VersionSpec::parse("8.0.0").unwrap(), + ..Default::default() + }, + install_dir: plugin.tool.to_virtual_path(sandbox.path()), + }) + .await + .exes + .get("pnpm") + .unwrap() + .exe_path, + Some("shims/pnpm.cmd".into()) + ); + } + + #[tokio::test(flavor = "multi_thread")] + async fn downloads_from_custom_registry() { + let sandbox = create_empty_proto_sandbox(); + let plugin = sandbox + .create_plugin_with_config("pnpm-test", |config| { + config + .host(HostOS::MacOS, HostArch::X64) + .tool_config(NodeDepmanPluginConfig { + registry_url: "https://some-internal-url.example".into(), + }); + }) + .await; + + assert_eq!( + plugin + .download_prebuilt(DownloadPrebuiltInput { + context: PluginContext { + version: VersionSpec::parse("8.0.0").unwrap(), + ..Default::default() + }, + ..Default::default() + }) + .await, + DownloadPrebuiltOutput { + archive_prefix: Some("package".into()), + download_url: "https://some-internal-url.example/pnpm/-/pnpm-8.0.0.tgz".into(), + ..Default::default() + } + ); + } + + #[tokio::test(flavor = "multi_thread")] + async fn extracts_auth_token_header() { + let sandbox = create_empty_proto_sandbox(); + sandbox.create_file(".npmrc", "//registry.npmjs.org/:_authToken = abc123"); + + let plugin = sandbox + .create_plugin_with_config("pnpm-test", |config| { + config.host(HostOS::Linux, HostArch::Arm64); + }) + .await; + + assert_eq!( + plugin + .download_prebuilt(DownloadPrebuiltInput { + context: PluginContext { + version: VersionSpec::parse("9.0.0").unwrap(), + working_dir: plugin.tool.to_virtual_path(sandbox.path()), + ..Default::default() + }, + ..Default::default() + }) + .await, + DownloadPrebuiltOutput { + archive_prefix: Some("package".into()), + download_url: "https://registry.npmjs.org/pnpm/-/pnpm-9.0.0.tgz".into(), + http_headers: FxHashMap::from_iter([( + "Authorization".into(), + "Bearer abc123".into() + )]), + ..Default::default() + } + ); + } #[tokio::test(flavor = "multi_thread")] - async fn supports_prebuilt() { + async fn extracts_auth_token_header_for_custom_registry() { let sandbox = create_empty_proto_sandbox(); + sandbox.create_file(".npmrc", "//registry.yarnpkg.com/:_authToken = abc123"); + let plugin = sandbox .create_plugin_with_config("pnpm-test", |config| { - config.host(HostOS::Windows, HostArch::X64); + config.host(HostOS::Linux, HostArch::Arm64).tool_config( + NodeDepmanPluginConfig { + registry_url: "https://registry.yarnpkg.com".into(), + }, + ); }) .await; @@ -191,7 +654,8 @@ mod node_depman_tool { plugin .download_prebuilt(DownloadPrebuiltInput { context: PluginContext { - version: VersionSpec::parse("8.0.0").unwrap(), + version: VersionSpec::parse("9.0.0").unwrap(), + working_dir: plugin.tool.to_virtual_path(sandbox.path()), ..Default::default() }, ..Default::default() @@ -199,57 +663,69 @@ mod node_depman_tool { .await, DownloadPrebuiltOutput { archive_prefix: Some("package".into()), - download_url: "https://registry.npmjs.org/pnpm/-/pnpm-8.0.0.tgz".into(), + download_url: "https://registry.yarnpkg.com/pnpm/-/pnpm-9.0.0.tgz".into(), + http_headers: FxHashMap::from_iter([( + "Authorization".into(), + "Bearer abc123".into() + )]), ..Default::default() } ); } + } + + mod pnpm12 { + use super::*; + + // Pnpm >= 12 is Rust based and downloaded from the npm registry + // as an os/arch specific package: @pnpm/exe.{os}-{arch} #[tokio::test(flavor = "multi_thread")] - async fn locates_default_bin() { + async fn supports_prebuilt_macos_arm64() { let sandbox = create_empty_proto_sandbox(); let plugin = sandbox .create_plugin_with_config("pnpm-test", |config| { - config.host(HostOS::Windows, HostArch::X64); + config.host(HostOS::MacOS, HostArch::Arm64); }) .await; assert_eq!( plugin - .locate_executables(LocateExecutablesInput { + .download_prebuilt(DownloadPrebuiltInput { context: PluginContext { - version: VersionSpec::parse("8.0.0").unwrap(), + version: VersionSpec::parse("12.0.0").unwrap(), ..Default::default() }, - install_dir: VirtualPath::Real(sandbox.path().into()), + ..Default::default() }) - .await - .exes - .get("pnpm") - .unwrap() - .exe_path, - Some("shims/pnpm.cmd".into()) + .await, + DownloadPrebuiltOutput { + archive_prefix: Some("package".into()), + download_url: "https://registry.npmjs.org/@pnpm/exe.darwin-arm64/-/exe.darwin-arm64-12.0.0.tgz".into(), + ..Default::default() + } ); } #[tokio::test(flavor = "multi_thread")] - async fn downloads_from_custom_registry() { + async fn supports_prebuilt_linux_x64_gnu() { let sandbox = create_empty_proto_sandbox(); let plugin = sandbox .create_plugin_with_config("pnpm-test", |config| { - config - .host(HostOS::MacOS, HostArch::X64) - .tool_config(NodeDepmanPluginConfig { - registry_url: "https://some-internal-url.example".into(), - }); + config.host_with(|host| { + host.os = HostOS::Linux; + host.arch = HostArch::X64; + host.libc = HostLibc::Gnu; + }); }) .await; + // Unlike yarn, gnu is supported and has no libc suffix assert_eq!( plugin .download_prebuilt(DownloadPrebuiltInput { context: PluginContext { - version: VersionSpec::parse("8.0.0").unwrap(), + version: VersionSpec::parse("12.0.0").unwrap(), ..Default::default() }, ..Default::default() @@ -257,29 +733,58 @@ mod node_depman_tool { .await, DownloadPrebuiltOutput { archive_prefix: Some("package".into()), - download_url: "https://some-internal-url.example/pnpm/-/pnpm-8.0.0.tgz".into(), + download_url: "https://registry.npmjs.org/@pnpm/exe.linux-x64/-/exe.linux-x64-12.0.0.tgz".into(), ..Default::default() } ); } #[tokio::test(flavor = "multi_thread")] - async fn extracts_auth_token_header() { + async fn supports_prebuilt_linux_arm64_musl() { let sandbox = create_empty_proto_sandbox(); - sandbox.create_file(".npmrc", "//registry.npmjs.org/:_authToken = abc123"); + let plugin = sandbox + .create_plugin_with_config("pnpm-test", |config| { + config.host_with(|host| { + host.os = HostOS::Linux; + host.arch = HostArch::Arm64; + host.libc = HostLibc::Musl; + }); + }) + .await; + + assert_eq!( + plugin + .download_prebuilt(DownloadPrebuiltInput { + context: PluginContext { + version: VersionSpec::parse("12.0.0").unwrap(), + ..Default::default() + }, + ..Default::default() + }) + .await, + DownloadPrebuiltOutput { + archive_prefix: Some("package".into()), + download_url: "https://registry.npmjs.org/@pnpm/exe.linux-arm64-musl/-/exe.linux-arm64-musl-12.0.0.tgz".into(), + ..Default::default() + } + ); + } + #[tokio::test(flavor = "multi_thread")] + async fn supports_prebuilt_windows_x64() { + let sandbox = create_empty_proto_sandbox(); let plugin = sandbox .create_plugin_with_config("pnpm-test", |config| { - config.host(HostOS::Linux, HostArch::Arm64); + config.host(HostOS::Windows, HostArch::X64); }) .await; + // Unlike yarn, windows is supported assert_eq!( plugin .download_prebuilt(DownloadPrebuiltInput { context: PluginContext { - version: VersionSpec::parse("9.0.0").unwrap(), - working_dir: VirtualPath::Real(sandbox.path().into()), + version: VersionSpec::parse("12.0.0").unwrap(), ..Default::default() }, ..Default::default() @@ -287,28 +792,20 @@ mod node_depman_tool { .await, DownloadPrebuiltOutput { archive_prefix: Some("package".into()), - download_url: "https://registry.npmjs.org/pnpm/-/pnpm-9.0.0.tgz".into(), - http_headers: FxHashMap::from_iter([( - "Authorization".into(), - "Bearer abc123".into() - )]), + download_url: "https://registry.npmjs.org/@pnpm/exe.win32-x64/-/exe.win32-x64-12.0.0.tgz".into(), ..Default::default() } ); } #[tokio::test(flavor = "multi_thread")] - async fn extracts_auth_token_header_for_custom_registry() { + async fn extracts_auth_token_header() { let sandbox = create_empty_proto_sandbox(); - sandbox.create_file(".npmrc", "//registry.yarnpkg.com/:_authToken = abc123"); + sandbox.create_file(".npmrc", "//registry.npmjs.org/:_authToken = abc123"); let plugin = sandbox .create_plugin_with_config("pnpm-test", |config| { - config.host(HostOS::Linux, HostArch::Arm64).tool_config( - NodeDepmanPluginConfig { - registry_url: "https://registry.yarnpkg.com".into(), - }, - ); + config.host(HostOS::Linux, HostArch::Arm64); }) .await; @@ -316,8 +813,8 @@ mod node_depman_tool { plugin .download_prebuilt(DownloadPrebuiltInput { context: PluginContext { - version: VersionSpec::parse("9.0.0").unwrap(), - working_dir: VirtualPath::Real(sandbox.path().into()), + version: VersionSpec::parse("12.0.0").unwrap(), + working_dir: plugin.tool.to_virtual_path(sandbox.path()), ..Default::default() }, ..Default::default() @@ -325,7 +822,7 @@ mod node_depman_tool { .await, DownloadPrebuiltOutput { archive_prefix: Some("package".into()), - download_url: "https://registry.yarnpkg.com/pnpm/-/pnpm-9.0.0.tgz".into(), + download_url: "https://registry.npmjs.org/@pnpm/exe.linux-arm64/-/exe.linux-arm64-12.0.0.tgz".into(), http_headers: FxHashMap::from_iter([( "Authorization".into(), "Bearer abc123".into() @@ -334,6 +831,163 @@ mod node_depman_tool { } ); } + + #[tokio::test(flavor = "multi_thread")] + #[should_panic(expected = "unsupported architecture x86.")] + async fn doesnt_support_other_archs() { + let sandbox = create_empty_proto_sandbox(); + let plugin = sandbox + .create_plugin_with_config("pnpm-test", |config| { + config.host(HostOS::Linux, HostArch::X86); + }) + .await; + + plugin + .download_prebuilt(DownloadPrebuiltInput { + context: PluginContext { + version: VersionSpec::parse("12.0.0").unwrap(), + ..Default::default() + }, + ..Default::default() + }) + .await; + } + + #[tokio::test(flavor = "multi_thread")] + #[should_panic(expected = "unsupported OS freebsd.")] + async fn doesnt_support_other_os() { + let sandbox = create_empty_proto_sandbox(); + let plugin = sandbox + .create_plugin_with_config("pnpm-test", |config| { + config.host(HostOS::FreeBSD, HostArch::Arm64); + }) + .await; + + plugin + .download_prebuilt(DownloadPrebuiltInput { + context: PluginContext { + version: VersionSpec::parse("12.0.0").unwrap(), + ..Default::default() + }, + ..Default::default() + }) + .await; + } + + #[tokio::test(flavor = "multi_thread")] + #[should_panic(expected = "pnpm does not support canary/nightly versions.")] + async fn doesnt_support_canary() { + let sandbox = create_empty_proto_sandbox(); + let plugin = sandbox + .create_plugin_with_config("pnpm-test", |config| { + config.host(HostOS::MacOS, HostArch::Arm64); + }) + .await; + + plugin + .download_prebuilt(DownloadPrebuiltInput { + context: PluginContext { + version: VersionSpec::parse("canary").unwrap(), + ..Default::default() + }, + ..Default::default() + }) + .await; + } + + #[tokio::test(flavor = "multi_thread")] + async fn locates_native_bin() { + let sandbox = create_empty_proto_sandbox(); + let plugin = sandbox + .create_plugin_with_config("pnpm-test", |config| { + config.host(HostOS::MacOS, HostArch::Arm64); + }) + .await; + + let exes = plugin + .locate_executables(LocateExecutablesInput { + context: PluginContext { + version: VersionSpec::parse("12.0.0").unwrap(), + ..Default::default() + }, + install_dir: plugin.tool.to_virtual_path(sandbox.path()), + }) + .await + .exes; + + assert_eq!(exes.get("pnpm").unwrap().exe_path, Some("pnpm".into())); + assert_eq!(exes.get("pn").unwrap().exe_path, Some("pnpm".into())); + + // pnpx and pnx run through the primary binary as `pnpm dlx` + for alias in ["pnpx", "pnx"] { + let exe = exes.get(alias).unwrap(); + + assert_eq!(exe.exe_path, Some("pnpm".into())); + assert!(exe.no_bin); + assert_eq!( + exe.shim_before_args, + Some(StringOrVec::String("dlx".into())) + ); + } + + // No internal shims are created for the native binary + assert!(!sandbox.path().join("shims/pnpm").exists()); + } + + #[tokio::test(flavor = "multi_thread")] + async fn locates_native_bin_windows() { + let sandbox = create_empty_proto_sandbox(); + let plugin = sandbox + .create_plugin_with_config("pnpm-test", |config| { + config.host(HostOS::Windows, HostArch::X64); + }) + .await; + + let exes = plugin + .locate_executables(LocateExecutablesInput { + context: PluginContext { + version: VersionSpec::parse("12.0.0").unwrap(), + ..Default::default() + }, + install_dir: plugin.tool.to_virtual_path(sandbox.path()), + }) + .await + .exes; + + // The .exe extension must not be rewritten to .cmd + assert_eq!( + exes.get("pnpm").unwrap().exe_path, + Some("pnpm.exe".into()) + ); + } + + #[tokio::test(flavor = "multi_thread")] + async fn locates_native_bin_for_next_alias() { + let sandbox = create_empty_proto_sandbox(); + let plugin = sandbox + .create_plugin_with_config("pnpm-test", |config| { + config.host(HostOS::MacOS, HostArch::Arm64); + }) + .await; + + // The "next" alias maps to v12 + assert_eq!( + plugin + .locate_executables(LocateExecutablesInput { + context: PluginContext { + version: VersionSpec::parse("next").unwrap(), + ..Default::default() + }, + install_dir: plugin.tool.to_virtual_path(sandbox.path()), + }) + .await + .exes + .get("pnpm") + .unwrap() + .exe_path, + Some("pnpm".into()) + ); + } } mod yarn1 { @@ -384,7 +1038,7 @@ mod node_depman_tool { version: VersionSpec::parse("1.22.0").unwrap(), ..Default::default() }, - install_dir: VirtualPath::Real(sandbox.path().into()), + install_dir: plugin.tool.to_virtual_path(sandbox.path()), }) .await .exes @@ -483,7 +1137,7 @@ mod node_depman_tool { version: VersionSpec::parse("3.6.1").unwrap(), ..Default::default() }, - install_dir: VirtualPath::Real(sandbox.path().into()), + install_dir: plugin.tool.to_virtual_path(sandbox.path()), }) .await .exes @@ -510,7 +1164,7 @@ mod node_depman_tool { .download_prebuilt(DownloadPrebuiltInput { context: PluginContext { version: VersionSpec::parse("4.5.0").unwrap(), - working_dir: VirtualPath::Real(sandbox.path().into()), + working_dir: plugin.tool.to_virtual_path(sandbox.path()), ..Default::default() }, ..Default::default() @@ -558,7 +1212,7 @@ npmRegistries: .download_prebuilt(DownloadPrebuiltInput { context: PluginContext { version: VersionSpec::parse("4.5.0").unwrap(), - working_dir: VirtualPath::Real(sandbox.path().into()), + working_dir: plugin.tool.to_virtual_path(sandbox.path()), ..Default::default() }, ..Default::default() @@ -818,15 +1472,12 @@ npmRegistries: version: VersionSpec::parse("6.0.0-rc.19").unwrap(), ..Default::default() }, - install_dir: VirtualPath::Real(sandbox.path().into()), + install_dir: plugin.tool.to_virtual_path(sandbox.path()), }) .await .exes; - assert_eq!( - exes.get("yarn").unwrap().exe_path, - Some("yarn-bin".into()) - ); + assert_eq!(exes.get("yarn").unwrap().exe_path, Some("yarn-bin".into())); // The yarnpkg alias is not supported in v6 assert!(!exes.contains_key("yarnpkg")); @@ -850,7 +1501,7 @@ npmRegistries: version: VersionSpec::parse("6.0.0-rc.19").unwrap(), ..Default::default() }, - install_dir: VirtualPath::Real(sandbox.path().into()), + install_dir: plugin.tool.to_virtual_path(sandbox.path()), }) .await .exes; diff --git a/tools/node-depman/tests/metadata_test.rs b/tools/node-depman/tests/metadata_test.rs index 4c0bcffc..5b85a83c 100644 --- a/tools/node-depman/tests/metadata_test.rs +++ b/tools/node-depman/tests/metadata_test.rs @@ -20,10 +20,27 @@ mod node_depman_tool { assert_eq!(metadata.name, "npm"); assert!(metadata.lock_options.ignore_os_arch); assert_eq!(metadata.type_of, PluginType::DependencyManager); - assert_eq!( - metadata.plugin_version.unwrap().to_string(), - env!("CARGO_PKG_VERSION") - ); + } + } + + mod nub { + use super::*; + + #[tokio::test(flavor = "multi_thread")] + async fn registers_metadata() { + let sandbox = create_empty_proto_sandbox(); + let plugin = sandbox.create_plugin("nub-test").await; + + let metadata = plugin.register_tool(create_metadata("nub-test")).await; + + assert_eq!(metadata.name, "nub"); + + // Binaries are os/arch specific, so records must be scoped + assert!(!metadata.lock_options.ignore_os_arch); + assert_eq!(metadata.type_of, PluginType::DependencyManager); + + // Unlike the other package managers, nub does not require Node.js + assert!(metadata.requires.is_empty()); } } @@ -38,12 +55,10 @@ mod node_depman_tool { let metadata = plugin.register_tool(create_metadata("pnpm-test")).await; assert_eq!(metadata.name, "pnpm"); - assert!(metadata.lock_options.ignore_os_arch); + + // v12+ binaries are os/arch specific, so records must be scoped + assert!(!metadata.lock_options.ignore_os_arch); assert_eq!(metadata.type_of, PluginType::DependencyManager); - assert_eq!( - metadata.plugin_version.unwrap().to_string(), - env!("CARGO_PKG_VERSION") - ); } } @@ -62,10 +77,6 @@ mod node_depman_tool { // v6+ binaries are os/arch specific, so records must be scoped assert!(!metadata.lock_options.ignore_os_arch); assert_eq!(metadata.type_of, PluginType::DependencyManager); - assert_eq!( - metadata.plugin_version.unwrap().to_string(), - env!("CARGO_PKG_VERSION") - ); } } } diff --git a/tools/node-depman/tests/shims_test.rs b/tools/node-depman/tests/shims_test.rs index b32e208d..dd5c7a14 100644 --- a/tools/node-depman/tests/shims_test.rs +++ b/tools/node-depman/tests/shims_test.rs @@ -8,6 +8,12 @@ mod node_depman_tool { generate_shims_test!("npm-test", ["npm", "npx", "node-gyp"]); } + mod nub { + use super::*; + + generate_shims_test!("nub-test", ["nub", "nubx"]); + } + mod pnpm { use super::*; diff --git a/tools/node-depman/tests/snapshots/shims_test__node_depman_tool__nub__creates_shims.snap b/tools/node-depman/tests/snapshots/shims_test__node_depman_tool__nub__creates_shims.snap new file mode 100644 index 00000000..21d263dc --- /dev/null +++ b/tools/node-depman/tests/snapshots/shims_test__node_depman_tool__nub__creates_shims.snap @@ -0,0 +1,14 @@ +--- +source: tools/node-depman/tests/shims_test.rs +expression: "std :: fs ::\nread_to_string(sandbox.path().join(\".proto/shims/registry.json\")).unwrap()" +--- +{ + "nub": { + "alt_exe": true, + "context": "nub-test" + }, + "nubx": { + "alt_exe": true, + "context": "nub-test" + } +} diff --git a/tools/node-depman/tests/versions_test.rs b/tools/node-depman/tests/versions_test.rs index 8035a43e..265ba6cd 100644 --- a/tools/node-depman/tests/versions_test.rs +++ b/tools/node-depman/tests/versions_test.rs @@ -19,7 +19,9 @@ mod node_depman_tool { .parse_version_file(ParseVersionFileInput { content: r#"{ "volta": { "extends": "./a.json" } }"#.into(), file: "package.json".into(), - path: VirtualPath::Real(sandbox.path().join("package.json")), + path: plugin + .tool + .to_virtual_path(sandbox.path().join("package.json")), ..Default::default() }) .await, @@ -29,6 +31,62 @@ mod node_depman_tool { ); } + #[tokio::test(flavor = "multi_thread")] + async fn parses_first_field_when_multiple_are_valid() { + let sandbox = create_empty_proto_sandbox(); + let plugin = sandbox.create_plugin("npm-test").await; + + assert_eq!( + plugin + .parse_version_file(ParseVersionFileInput { + content: r#"{ "devEngines": { "packageManager": { "name": "npm", "version": "1.2.3" } }, "packageManager": "npm@4.5.6", "volta": { "npm": "7.8.9" }, "engines": { "npm": "^10" } }"# + .into(), + file: "package.json".into(), + ..Default::default() + }) + .await, + ParseVersionFileOutput { + version: Some(UnresolvedVersionSpec::parse("1.2.3").unwrap()), + } + ); + } + + #[tokio::test(flavor = "multi_thread")] + async fn skips_invalid_field_and_parses_next_valid_field() { + let sandbox = create_empty_proto_sandbox(); + let plugin = sandbox.create_plugin("npm-test").await; + + assert_eq!( + plugin + .parse_version_file(ParseVersionFileInput { + content: r#"{ "packageManager": "npm@https://registry.npmjs.org/npm/-/npm-9.0.0.tgz", "volta": { "npm": "7.8.9" }, "engines": { "npm": "^10" } }"# + .into(), + file: "package.json".into(), + ..Default::default() + }) + .await, + ParseVersionFileOutput { + version: Some(UnresolvedVersionSpec::parse("7.8.9").unwrap()), + } + ); + } + + #[tokio::test(flavor = "multi_thread")] + #[should_panic(expected = "Failed to parse a version requirement.")] + async fn errors_if_no_field_is_valid() { + let sandbox = create_empty_proto_sandbox(); + let plugin = sandbox.create_plugin("npm-test").await; + + plugin + .parse_version_file(ParseVersionFileInput { + content: r#"{ "packageManager": "npm@https://registry.npmjs.org/npm/-/npm-9.0.0.tgz" }"# + .into(), + file: "package.json".into(), + ..Default::default() + }) + .await; + } + mod npm { use super::*; @@ -168,6 +226,128 @@ mod node_depman_tool { } } + mod nub { + use super::*; + + generate_resolve_versions_tests!("nub-test", { + "0.2" => "0.2.10", + "0.4" => "0.4.13", + "0.5.0" => "0.5.0", + }); + + #[tokio::test(flavor = "multi_thread")] + async fn doesnt_parse_package_manager_if_diff_name() { + let sandbox = create_empty_proto_sandbox(); + let plugin = sandbox.create_plugin("nub-test").await; + + assert_eq!( + plugin + .parse_version_file(ParseVersionFileInput { + content: r#"{ "packageManager": "npm@1.2.3" }"#.into(), + file: "package.json".into(), + ..Default::default() + }) + .await, + ParseVersionFileOutput { version: None } + ); + } + + #[tokio::test(flavor = "multi_thread")] + async fn parses_package_manager() { + let sandbox = create_empty_proto_sandbox(); + let plugin = sandbox.create_plugin("nub-test").await; + + assert_eq!( + plugin + .parse_version_file(ParseVersionFileInput { + content: r#"{ "packageManager": "nub@1.2.3" }"#.into(), + file: "package.json".into(), + ..Default::default() + }) + .await, + ParseVersionFileOutput { + version: Some(UnresolvedVersionSpec::parse("1.2.3").unwrap()), + } + ); + } + + #[tokio::test(flavor = "multi_thread")] + async fn parses_package_manager_latest() { + let sandbox = create_empty_proto_sandbox(); + let plugin = sandbox.create_plugin("nub-test").await; + + assert_eq!( + plugin + .parse_version_file(ParseVersionFileInput { + content: r#"{ "packageManager": "nub" }"#.into(), + file: "package.json".into(), + ..Default::default() + }) + .await, + ParseVersionFileOutput { + version: Some(UnresolvedVersionSpec::parse("latest").unwrap()), + } + ); + } + + #[tokio::test(flavor = "multi_thread")] + async fn parses_engines() { + let sandbox = create_empty_proto_sandbox(); + let plugin = sandbox.create_plugin("nub-test").await; + + assert_eq!( + plugin + .parse_version_file(ParseVersionFileInput { + content: r#"{ "engines": { "nub": "1.2.3" } }"#.into(), + file: "package.json".into(), + ..Default::default() + }) + .await, + ParseVersionFileOutput { + version: Some(UnresolvedVersionSpec::parse("1.2.3").unwrap()), + } + ); + } + + #[tokio::test(flavor = "multi_thread")] + async fn parses_dev_engines() { + let sandbox = create_empty_proto_sandbox(); + let plugin = sandbox.create_plugin("nub-test").await; + + assert_eq!( + plugin + .parse_version_file(ParseVersionFileInput { + content: r#"{ "devEngines": { "packageManager": { "name": "nub", "version": "1.2.3" } } }"#.into(), + file: "package.json".into(), + ..Default::default() + }) + .await, + ParseVersionFileOutput { + version: Some(UnresolvedVersionSpec::parse("1.2.3").unwrap()), + } + ); + } + + #[tokio::test(flavor = "multi_thread")] + async fn parses_volta() { + let sandbox = create_empty_proto_sandbox(); + let plugin = sandbox.create_plugin("nub-test").await; + + assert_eq!( + plugin + .parse_version_file(ParseVersionFileInput { + content: r#"{ "volta": { "nub": "1.2.3" } }"#.into(), + file: "package.json".into(), + ..Default::default() + }) + .await, + ParseVersionFileOutput { + version: Some(UnresolvedVersionSpec::parse("1.2.3").unwrap()), + } + ); + } + } + mod pnpm { use super::*; @@ -448,7 +628,7 @@ mod node_depman_tool { assert_eq!( plugin .pin_version(PinVersionInput { - dir: VirtualPath::Real(sandbox.path().into()), + dir: plugin.tool.to_virtual_path(sandbox.path()), version: UnresolvedVersionSpec::parse(">=10").unwrap(), ..Default::default() }) @@ -473,7 +653,7 @@ mod node_depman_tool { assert_eq!( plugin .pin_version(PinVersionInput { - dir: VirtualPath::Real(sandbox.path().into()), + dir: plugin.tool.to_virtual_path(sandbox.path()), version: UnresolvedVersionSpec::parse(">=10").unwrap(), ..Default::default() }) @@ -512,7 +692,7 @@ mod node_depman_tool { assert_eq!( plugin .pin_version(PinVersionInput { - dir: VirtualPath::Real(sandbox.path().into()), + dir: plugin.tool.to_virtual_path(sandbox.path()), version: UnresolvedVersionSpec::parse(">=10").unwrap(), ..Default::default() }) @@ -551,7 +731,7 @@ mod node_depman_tool { assert_eq!( plugin .pin_version(PinVersionInput { - dir: VirtualPath::Real(sandbox.path().into()), + dir: plugin.tool.to_virtual_path(sandbox.path()), version: UnresolvedVersionSpec::parse(">=10").unwrap(), ..Default::default() }) @@ -582,7 +762,7 @@ mod node_depman_tool { assert_eq!( plugin .unpin_version(UnpinVersionInput { - dir: VirtualPath::Real(sandbox.path().into()), + dir: plugin.tool.to_virtual_path(sandbox.path()), ..Default::default() }) .await, @@ -615,7 +795,7 @@ mod node_depman_tool { assert_eq!( plugin .unpin_version(UnpinVersionInput { - dir: VirtualPath::Real(sandbox.path().into()), + dir: plugin.tool.to_virtual_path(sandbox.path()), ..Default::default() }) .await, @@ -660,7 +840,7 @@ mod node_depman_tool { assert_eq!( plugin .unpin_version(UnpinVersionInput { - dir: VirtualPath::Real(sandbox.path().into()), + dir: plugin.tool.to_virtual_path(sandbox.path()), ..Default::default() }) .await, @@ -699,7 +879,7 @@ mod node_depman_tool { assert_eq!( plugin .unpin_version(UnpinVersionInput { - dir: VirtualPath::Real(sandbox.path().into()), + dir: plugin.tool.to_virtual_path(sandbox.path()), ..Default::default() }) .await, diff --git a/tools/node/CHANGELOG.md b/tools/node/CHANGELOG.md index 8803db2f..cc8015d9 100644 --- a/tools/node/CHANGELOG.md +++ b/tools/node/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## 0.17.12 + +#### 🚀 Updates + +- Updated version detection to loop through all possible fields (`packageManager`, `engines`, etc) to find an applicable version, instead of failing on the first invalid version. + +## 0.17.11 + +#### 🚀 Updates + +- Updated to support proto v0.60 release. + ## 0.17.10 #### 🚀 Updates diff --git a/tools/node/Cargo.toml b/tools/node/Cargo.toml index c5b53b85..ba09a343 100644 --- a/tools/node/Cargo.toml +++ b/tools/node/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "node_tool" -version = "0.17.10" +version = "0.17.12" edition = "2024" description = "Node.js WASM plugin for proto." authors = ["Miles Johnson"] diff --git a/tools/node/src/proto.rs b/tools/node/src/proto.rs index 5b31294c..1c231f24 100644 --- a/tools/node/src/proto.rs +++ b/tools/node/src/proto.rs @@ -24,9 +24,9 @@ pub fn register_tool(Json(_): Json) -> FnResult(&input.content) { + let mut candidates = vec![]; + if let Some(constraint) = extract_dev_engine_runtime_version(&package_json, "node") { - version = Some(UnresolvedVersionSpec::parse(constraint)?); + candidates.push(constraint); } - if version.is_none() - && let Some(constraint) = extract_volta_version(&package_json, &input.path, "node")? - { - version = Some(UnresolvedVersionSpec::parse(constraint)?); + if let Some(constraint) = extract_volta_version(&package_json, &input.path, "node")? { + candidates.push(constraint); + } + + if let Some(constraint) = extract_engine_version(&package_json, "node") { + candidates.push(constraint); + } + + let mut error = None; + + for candidates in candidates { + match UnresolvedVersionSpec::parse(candidates) { + Ok(spec) => { + version = Some(spec); + break; + } + Err(err) => { + if error.is_none() { + error = Some(err); + } + } + }; } if version.is_none() - && let Some(constraint) = extract_engine_version(&package_json, "node") + && let Some(error) = error { - version = Some(UnresolvedVersionSpec::parse(constraint)?); + return Err(plugin_err!("{error}")); } } } else if let Some(constraint) = extract_version_from_text(&input.content) { @@ -199,7 +219,7 @@ pub fn build_instructions( check_supported_os_and_arch( NAME, - &env, + env, permutations! [ HostOS::Linux => [HostArch::X64, HostArch::Arm64, HostArch::Arm, HostArch::Powerpc64, HostArch::S390x], HostOS::MacOS => [HostArch::X64, HostArch::Arm64], @@ -273,7 +293,7 @@ pub fn download_prebuilt( check_supported_os_and_arch( NAME, - &env, + env, permutations! [ HostOS::Linux => [HostArch::X64, HostArch::Arm64, HostArch::Arm, HostArch::Powerpc64, HostArch::S390x], HostOS::MacOS => [HostArch::X64, HostArch::Arm64], @@ -363,7 +383,7 @@ pub fn download_prebuilt( host.replace("{version}", &version.to_string()) .replace("{file}", "SHASUMS256.txt"), ), - ..DownloadPrebuiltOutput::default() + ..Default::default() })) } @@ -388,7 +408,7 @@ pub fn locate_executables( "bin".into() }], globals_lookup_dirs: vec!["$PROTO_HOME/tools/node/globals/bin".into()], - ..LocateExecutablesOutput::default() + ..Default::default() })) } diff --git a/tools/node/tests/versions_test.rs b/tools/node/tests/versions_test.rs index 6f235d98..967b61ad 100644 --- a/tools/node/tests/versions_test.rs +++ b/tools/node/tests/versions_test.rs @@ -113,6 +113,61 @@ mod node_tool { ); } + #[tokio::test(flavor = "multi_thread")] + async fn parses_first_field_when_multiple_are_valid() { + let sandbox = create_empty_proto_sandbox(); + let plugin = sandbox.create_plugin("node-test").await; + + assert_eq!( + plugin + .parse_version_file(ParseVersionFileInput { + content: r#"{ "devEngines": { "runtime": { "name": "node", "version": "^20" } }, "volta": { "node": "18.1.0" }, "engines": { "node": ">=16" } }"# + .into(), + file: "package.json".into(), + ..Default::default() + }) + .await, + ParseVersionFileOutput { + version: Some(UnresolvedVersionSpec::parse("^20").unwrap()), + } + ); + } + + #[tokio::test(flavor = "multi_thread")] + async fn skips_invalid_field_and_parses_next_valid_field() { + let sandbox = create_empty_proto_sandbox(); + let plugin = sandbox.create_plugin("node-test").await; + + assert_eq!( + plugin + .parse_version_file(ParseVersionFileInput { + content: r#"{ "volta": { "node": "invalid version" }, "engines": { "node": ">=18" } }"# + .into(), + file: "package.json".into(), + ..Default::default() + }) + .await, + ParseVersionFileOutput { + version: Some(UnresolvedVersionSpec::parse(">=18").unwrap()), + } + ); + } + + #[tokio::test(flavor = "multi_thread")] + #[should_panic(expected = "Failed to parse a version requirement.")] + async fn errors_if_no_field_is_valid() { + let sandbox = create_empty_proto_sandbox(); + let plugin = sandbox.create_plugin("node-test").await; + + plugin + .parse_version_file(ParseVersionFileInput { + content: r#"{ "volta": { "node": "invalid version" } }"#.into(), + file: "package.json".into(), + ..Default::default() + }) + .await; + } + #[tokio::test(flavor = "multi_thread")] async fn parses_nvmrc() { let sandbox = create_empty_proto_sandbox(); @@ -200,7 +255,7 @@ mod node_tool { assert_eq!( plugin .pin_version(PinVersionInput { - dir: VirtualPath::Real(sandbox.path().into()), + dir: plugin.tool.to_virtual_path(sandbox.path()), version: UnresolvedVersionSpec::parse(">=20").unwrap(), ..Default::default() }) @@ -225,7 +280,7 @@ mod node_tool { assert_eq!( plugin .pin_version(PinVersionInput { - dir: VirtualPath::Real(sandbox.path().into()), + dir: plugin.tool.to_virtual_path(sandbox.path()), version: UnresolvedVersionSpec::parse(">=20").unwrap(), ..Default::default() }) @@ -264,7 +319,7 @@ mod node_tool { assert_eq!( plugin .pin_version(PinVersionInput { - dir: VirtualPath::Real(sandbox.path().into()), + dir: plugin.tool.to_virtual_path(sandbox.path()), version: UnresolvedVersionSpec::parse(">=20").unwrap(), ..Default::default() }) @@ -303,7 +358,7 @@ mod node_tool { assert_eq!( plugin .pin_version(PinVersionInput { - dir: VirtualPath::Real(sandbox.path().into()), + dir: plugin.tool.to_virtual_path(sandbox.path()), version: UnresolvedVersionSpec::parse(">=20").unwrap(), ..Default::default() }) @@ -334,7 +389,7 @@ mod node_tool { assert_eq!( plugin .unpin_version(UnpinVersionInput { - dir: VirtualPath::Real(sandbox.path().into()), + dir: plugin.tool.to_virtual_path(sandbox.path()), ..Default::default() }) .await, @@ -367,7 +422,7 @@ mod node_tool { assert_eq!( plugin .unpin_version(UnpinVersionInput { - dir: VirtualPath::Real(sandbox.path().into()), + dir: plugin.tool.to_virtual_path(sandbox.path()), ..Default::default() }) .await, @@ -412,7 +467,7 @@ mod node_tool { assert_eq!( plugin .unpin_version(UnpinVersionInput { - dir: VirtualPath::Real(sandbox.path().into()), + dir: plugin.tool.to_virtual_path(sandbox.path()), ..Default::default() }) .await, @@ -451,7 +506,7 @@ mod node_tool { assert_eq!( plugin .unpin_version(UnpinVersionInput { - dir: VirtualPath::Real(sandbox.path().into()), + dir: plugin.tool.to_virtual_path(sandbox.path()), ..Default::default() }) .await, diff --git a/tools/proto/CHANGELOG.md b/tools/proto/CHANGELOG.md index d7518d9f..1cd082e7 100644 --- a/tools/proto/CHANGELOG.md +++ b/tools/proto/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 0.5.8 + +#### 🚀 Updates + +- Updated to support proto v0.60 release. + ## 0.5.7 #### 🚀 Updates diff --git a/tools/proto/Cargo.toml b/tools/proto/Cargo.toml index d2968743..8b37c4e9 100644 --- a/tools/proto/Cargo.toml +++ b/tools/proto/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "proto_tool" -version = "0.5.7" +version = "0.5.8" edition = "2024" description = "Internal-only WASM plugin for managing proto itself." authors = ["Miles Johnson"] diff --git a/tools/proto/src/proto.rs b/tools/proto/src/proto.rs index e1525ca0..e146af3a 100644 --- a/tools/proto/src/proto.rs +++ b/tools/proto/src/proto.rs @@ -15,10 +15,10 @@ pub fn register_tool(Json(_): Json) -> FnResult [HostArch::X64, HostArch::Arm64], HostOS::MacOS => [HostArch::X64, HostArch::Arm64], @@ -103,7 +103,7 @@ pub fn download_prebuilt( check_supported_os_and_arch( "proto", - &env, + env, permutations! [ HostOS::Linux => [HostArch::X64, HostArch::Arm64], HostOS::MacOS => [HostArch::X64, HostArch::Arm64], @@ -139,7 +139,7 @@ pub fn download_prebuilt( checksum_name: Some(checksum_file), download_url: format!("{base_url}/{download_file}"), download_name: Some(download_file), - ..DownloadPrebuiltOutput::default() + ..Default::default() })) } @@ -159,6 +159,6 @@ pub fn locate_executables( Ok(Json(LocateExecutablesOutput { exes: HashMap::from_iter([("proto".into(), primary), ("proto-shim".into(), secondary)]), - ..LocateExecutablesOutput::default() + ..Default::default() })) } diff --git a/tools/python-poetry/CHANGELOG.md b/tools/python-poetry/CHANGELOG.md index 23fcc16d..77e7b90d 100644 --- a/tools/python-poetry/CHANGELOG.md +++ b/tools/python-poetry/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 0.1.9 + +#### 🚀 Updates + +- Updated to support proto v0.60 release. + ## 0.1.8 #### 🚀 Updates diff --git a/tools/python-poetry/Cargo.toml b/tools/python-poetry/Cargo.toml index b7297695..bcf9818b 100644 --- a/tools/python-poetry/Cargo.toml +++ b/tools/python-poetry/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "python_poetry_tool" -version = "0.1.8" +version = "0.1.9" edition = "2024" description = "Python Poetry WASM plugin for proto." authors = ["Miles Johnson"] diff --git a/tools/python-poetry/src/proto.rs b/tools/python-poetry/src/proto.rs index ca33c614..3c3d4e50 100644 --- a/tools/python-poetry/src/proto.rs +++ b/tools/python-poetry/src/proto.rs @@ -21,11 +21,11 @@ pub fn register_tool(Json(_): Json) -> FnResult) -> FnResult [HostArch::X64, HostArch::Arm64], HostOS::MacOS => [HostArch::X64, HostArch::Arm64], @@ -119,7 +119,7 @@ pub fn download_prebuilt( checksum_name: Some(checksum_file), download_url: format!("{base_url}/{download_file}"), download_name: Some(download_file), - ..DownloadPrebuiltOutput::default() + ..Default::default() })) } @@ -147,6 +147,6 @@ pub fn locate_executables( "$XDG_DATA_HOME/../bin".into(), "$HOME/.local/bin".into(), ], - ..LocateExecutablesOutput::default() + ..Default::default() })) } diff --git a/tools/python-uv/tests/download_test.rs b/tools/python-uv/tests/download_test.rs index 29aa449b..6d88a37c 100644 --- a/tools/python-uv/tests/download_test.rs +++ b/tools/python-uv/tests/download_test.rs @@ -31,7 +31,7 @@ mod python_uv_tool { checksum_url: Some("https://github.com/astral-sh/uv/releases/download/1.41.0/uv-aarch64-unknown-linux-gnu.tar.gz.sha256".into()), download_name: Some("uv-aarch64-unknown-linux-gnu.tar.gz".into()), download_url: "https://github.com/astral-sh/uv/releases/download/1.41.0/uv-aarch64-unknown-linux-gnu.tar.gz".into(), - ..DownloadPrebuiltOutput::default() + ..Default::default() } ); } @@ -62,7 +62,7 @@ mod python_uv_tool { checksum_url: Some("https://github.com/astral-sh/uv/releases/download/1.2.0/uv-x86_64-unknown-linux-gnu.tar.gz.sha256".into()), download_name: Some("uv-x86_64-unknown-linux-gnu.tar.gz".into()), download_url: "https://github.com/astral-sh/uv/releases/download/1.2.0/uv-x86_64-unknown-linux-gnu.tar.gz".into(), - ..DownloadPrebuiltOutput::default() + ..Default::default() } ); } @@ -92,7 +92,7 @@ mod python_uv_tool { checksum_url: Some("https://github.com/astral-sh/uv/releases/download/1.2.0/uv-aarch64-apple-darwin.tar.gz.sha256".into()), download_name: Some("uv-aarch64-apple-darwin.tar.gz".into()), download_url: "https://github.com/astral-sh/uv/releases/download/1.2.0/uv-aarch64-apple-darwin.tar.gz".into(), - ..DownloadPrebuiltOutput::default() + ..Default::default() } ); } @@ -123,7 +123,7 @@ mod python_uv_tool { checksum_url: Some("https://github.com/astral-sh/uv/releases/download/1.2.0/uv-x86_64-apple-darwin.tar.gz.sha256".into()), download_name: Some("uv-x86_64-apple-darwin.tar.gz".into()), download_url: "https://github.com/astral-sh/uv/releases/download/1.2.0/uv-x86_64-apple-darwin.tar.gz".into(), - ..DownloadPrebuiltOutput::default() + ..Default::default() } ); } @@ -180,7 +180,7 @@ mod python_uv_tool { checksum_url: Some("https://github.com/astral-sh/uv/releases/download/1.2.0/uv-x86_64-pc-windows-msvc.zip.sha256".into()), download_name: Some("uv-x86_64-pc-windows-msvc.zip".into()), download_url: "https://github.com/astral-sh/uv/releases/download/1.2.0/uv-x86_64-pc-windows-msvc.zip".into(), - ..DownloadPrebuiltOutput::default() + ..Default::default() } ); } diff --git a/tools/python/CHANGELOG.md b/tools/python/CHANGELOG.md index c8c8fed5..379e2d42 100644 --- a/tools/python/CHANGELOG.md +++ b/tools/python/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 0.14.9 + +#### 🚀 Updates + +- Updated to support proto v0.60 release. + ## 0.14.8 #### 🚀 Updates diff --git a/tools/python/Cargo.toml b/tools/python/Cargo.toml index da153f6f..be430698 100644 --- a/tools/python/Cargo.toml +++ b/tools/python/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "python_tool" -version = "0.14.8" +version = "0.14.9" edition = "2024" description = "Python WASM plugin for proto." authors = ["Miles Johnson"] diff --git a/tools/python/releases-v2.json b/tools/python/releases-v2.json index c8fd2a86..2302000c 100644 --- a/tools/python/releases-v2.json +++ b/tools/python/releases-v2.json @@ -600,68 +600,135 @@ }, "3.10.20": { "aarch64-apple-darwin": { - "file": "cpython-3.10.20+20260610-aarch64-apple-darwin-install_only.tar.gz", - "release": "20260610", + "file": "cpython-3.10.20+20260807-aarch64-apple-darwin-install_only.tar.gz", + "release": "20260807", "sha": 1 }, "aarch64-unknown-linux-gnu": { - "file": "cpython-3.10.20+20260610-aarch64-unknown-linux-gnu-install_only.tar.gz", - "release": "20260610", + "file": "cpython-3.10.20+20260807-aarch64-unknown-linux-gnu-install_only.tar.gz", + "release": "20260807", "sha": 1 }, "aarch64-unknown-linux-musl": { - "file": "cpython-3.10.20+20260610-aarch64-unknown-linux-musl-install_only.tar.gz", - "release": "20260610", + "file": "cpython-3.10.20+20260807-aarch64-unknown-linux-musl-install_only.tar.gz", + "release": "20260807", "sha": 1 }, "armv7-unknown-linux-gnueabi": { - "file": "cpython-3.10.20+20260610-armv7-unknown-linux-gnueabi-install_only.tar.gz", - "release": "20260610", + "file": "cpython-3.10.20+20260807-armv7-unknown-linux-gnueabi-install_only.tar.gz", + "release": "20260807", "sha": 1 }, "armv7-unknown-linux-gnueabihf": { - "file": "cpython-3.10.20+20260610-armv7-unknown-linux-gnueabihf-install_only.tar.gz", - "release": "20260610", + "file": "cpython-3.10.20+20260807-armv7-unknown-linux-gnueabihf-install_only.tar.gz", + "release": "20260807", "sha": 1 }, "i686-pc-windows-msvc": { - "file": "cpython-3.10.20+20260610-i686-pc-windows-msvc-install_only.tar.gz", - "release": "20260610", + "file": "cpython-3.10.20+20260807-i686-pc-windows-msvc-install_only.tar.gz", + "release": "20260807", "sha": 1 }, "powerpc64le-unknown-linux-gnu": { - "file": "cpython-3.10.20+20260610-ppc64le-unknown-linux-gnu-install_only.tar.gz", - "release": "20260610", + "file": "cpython-3.10.20+20260807-ppc64le-unknown-linux-gnu-install_only.tar.gz", + "release": "20260807", "sha": 1 }, "riscv64gc-unknown-linux-gnu": { - "file": "cpython-3.10.20+20260610-riscv64-unknown-linux-gnu-install_only.tar.gz", - "release": "20260610", + "file": "cpython-3.10.20+20260807-riscv64-unknown-linux-gnu-install_only.tar.gz", + "release": "20260807", "sha": 1 }, "s390x-unknown-linux-gnu": { - "file": "cpython-3.10.20+20260610-s390x-unknown-linux-gnu-install_only.tar.gz", - "release": "20260610", + "file": "cpython-3.10.20+20260807-s390x-unknown-linux-gnu-install_only.tar.gz", + "release": "20260807", "sha": 1 }, "x86_64-apple-darwin": { - "file": "cpython-3.10.20+20260610-x86_64-apple-darwin-install_only.tar.gz", - "release": "20260610", + "file": "cpython-3.10.20+20260807-x86_64-apple-darwin-install_only.tar.gz", + "release": "20260807", "sha": 1 }, "x86_64-pc-windows-msvc": { - "file": "cpython-3.10.20+20260610-x86_64-pc-windows-msvc-install_only.tar.gz", - "release": "20260610", + "file": "cpython-3.10.20+20260807-x86_64-pc-windows-msvc-install_only.tar.gz", + "release": "20260807", "sha": 1 }, "x86_64-unknown-linux-gnu": { - "file": "cpython-3.10.20+20260610-x86_64-unknown-linux-gnu-install_only.tar.gz", - "release": "20260610", + "file": "cpython-3.10.20+20260807-x86_64-unknown-linux-gnu-install_only.tar.gz", + "release": "20260807", "sha": 1 }, "x86_64-unknown-linux-musl": { - "file": "cpython-3.10.20+20260610-x86_64-unknown-linux-musl-install_only.tar.gz", - "release": "20260610", + "file": "cpython-3.10.20+20260807-x86_64-unknown-linux-musl-install_only.tar.gz", + "release": "20260807", + "sha": 1 + } + }, + "3.10.21": { + "aarch64-apple-darwin": { + "file": "cpython-3.10.21+20260814-aarch64-apple-darwin-install_only.tar.gz", + "release": "20260814", + "sha": 1 + }, + "aarch64-unknown-linux-gnu": { + "file": "cpython-3.10.21+20260814-aarch64-unknown-linux-gnu-install_only.tar.gz", + "release": "20260814", + "sha": 1 + }, + "aarch64-unknown-linux-musl": { + "file": "cpython-3.10.21+20260814-aarch64-unknown-linux-musl-install_only.tar.gz", + "release": "20260814", + "sha": 1 + }, + "armv7-unknown-linux-gnueabi": { + "file": "cpython-3.10.21+20260814-armv7-unknown-linux-gnueabi-install_only.tar.gz", + "release": "20260814", + "sha": 1 + }, + "armv7-unknown-linux-gnueabihf": { + "file": "cpython-3.10.21+20260814-armv7-unknown-linux-gnueabihf-install_only.tar.gz", + "release": "20260814", + "sha": 1 + }, + "i686-pc-windows-msvc": { + "file": "cpython-3.10.21+20260814-i686-pc-windows-msvc-install_only.tar.gz", + "release": "20260814", + "sha": 1 + }, + "powerpc64le-unknown-linux-gnu": { + "file": "cpython-3.10.21+20260814-ppc64le-unknown-linux-gnu-install_only.tar.gz", + "release": "20260814", + "sha": 1 + }, + "riscv64gc-unknown-linux-gnu": { + "file": "cpython-3.10.21+20260814-riscv64-unknown-linux-gnu-install_only.tar.gz", + "release": "20260814", + "sha": 1 + }, + "s390x-unknown-linux-gnu": { + "file": "cpython-3.10.21+20260814-s390x-unknown-linux-gnu-install_only.tar.gz", + "release": "20260814", + "sha": 1 + }, + "x86_64-apple-darwin": { + "file": "cpython-3.10.21+20260814-x86_64-apple-darwin-install_only.tar.gz", + "release": "20260814", + "sha": 1 + }, + "x86_64-pc-windows-msvc": { + "file": "cpython-3.10.21+20260814-x86_64-pc-windows-msvc-install_only.tar.gz", + "release": "20260814", + "sha": 1 + }, + "x86_64-unknown-linux-gnu": { + "file": "cpython-3.10.21+20260814-x86_64-unknown-linux-gnu-install_only.tar.gz", + "release": "20260814", + "sha": 1 + }, + "x86_64-unknown-linux-musl": { + "file": "cpython-3.10.21+20260814-x86_64-unknown-linux-musl-install_only.tar.gz", + "release": "20260814", "sha": 1 } }, @@ -1328,73 +1395,145 @@ }, "3.11.15": { "aarch64-apple-darwin": { - "file": "cpython-3.11.15+20260610-aarch64-apple-darwin-install_only.tar.gz", - "release": "20260610", + "file": "cpython-3.11.15+20260807-aarch64-apple-darwin-install_only.tar.gz", + "release": "20260807", "sha": 1 }, "aarch64-pc-windows-msvc": { - "file": "cpython-3.11.15+20260610-aarch64-pc-windows-msvc-install_only.tar.gz", - "release": "20260610", + "file": "cpython-3.11.15+20260807-aarch64-pc-windows-msvc-install_only.tar.gz", + "release": "20260807", "sha": 1 }, "aarch64-unknown-linux-gnu": { - "file": "cpython-3.11.15+20260610-aarch64-unknown-linux-gnu-install_only.tar.gz", - "release": "20260610", + "file": "cpython-3.11.15+20260807-aarch64-unknown-linux-gnu-install_only.tar.gz", + "release": "20260807", "sha": 1 }, "aarch64-unknown-linux-musl": { - "file": "cpython-3.11.15+20260610-aarch64-unknown-linux-musl-install_only.tar.gz", - "release": "20260610", + "file": "cpython-3.11.15+20260807-aarch64-unknown-linux-musl-install_only.tar.gz", + "release": "20260807", "sha": 1 }, "armv7-unknown-linux-gnueabi": { - "file": "cpython-3.11.15+20260610-armv7-unknown-linux-gnueabi-install_only.tar.gz", - "release": "20260610", + "file": "cpython-3.11.15+20260807-armv7-unknown-linux-gnueabi-install_only.tar.gz", + "release": "20260807", "sha": 1 }, "armv7-unknown-linux-gnueabihf": { - "file": "cpython-3.11.15+20260610-armv7-unknown-linux-gnueabihf-install_only.tar.gz", - "release": "20260610", + "file": "cpython-3.11.15+20260807-armv7-unknown-linux-gnueabihf-install_only.tar.gz", + "release": "20260807", "sha": 1 }, "i686-pc-windows-msvc": { - "file": "cpython-3.11.15+20260610-i686-pc-windows-msvc-install_only.tar.gz", - "release": "20260610", + "file": "cpython-3.11.15+20260807-i686-pc-windows-msvc-install_only.tar.gz", + "release": "20260807", "sha": 1 }, "powerpc64le-unknown-linux-gnu": { - "file": "cpython-3.11.15+20260610-ppc64le-unknown-linux-gnu-install_only.tar.gz", - "release": "20260610", + "file": "cpython-3.11.15+20260807-ppc64le-unknown-linux-gnu-install_only.tar.gz", + "release": "20260807", "sha": 1 }, "riscv64gc-unknown-linux-gnu": { - "file": "cpython-3.11.15+20260610-riscv64-unknown-linux-gnu-install_only.tar.gz", - "release": "20260610", + "file": "cpython-3.11.15+20260807-riscv64-unknown-linux-gnu-install_only.tar.gz", + "release": "20260807", "sha": 1 }, "s390x-unknown-linux-gnu": { - "file": "cpython-3.11.15+20260610-s390x-unknown-linux-gnu-install_only.tar.gz", - "release": "20260610", + "file": "cpython-3.11.15+20260807-s390x-unknown-linux-gnu-install_only.tar.gz", + "release": "20260807", "sha": 1 }, "x86_64-apple-darwin": { - "file": "cpython-3.11.15+20260610-x86_64-apple-darwin-install_only.tar.gz", - "release": "20260610", + "file": "cpython-3.11.15+20260807-x86_64-apple-darwin-install_only.tar.gz", + "release": "20260807", "sha": 1 }, "x86_64-pc-windows-msvc": { - "file": "cpython-3.11.15+20260610-x86_64-pc-windows-msvc-install_only.tar.gz", - "release": "20260610", + "file": "cpython-3.11.15+20260807-x86_64-pc-windows-msvc-install_only.tar.gz", + "release": "20260807", "sha": 1 }, "x86_64-unknown-linux-gnu": { - "file": "cpython-3.11.15+20260610-x86_64-unknown-linux-gnu-install_only.tar.gz", - "release": "20260610", + "file": "cpython-3.11.15+20260807-x86_64-unknown-linux-gnu-install_only.tar.gz", + "release": "20260807", "sha": 1 }, "x86_64-unknown-linux-musl": { - "file": "cpython-3.11.15+20260610-x86_64-unknown-linux-musl-install_only.tar.gz", - "release": "20260610", + "file": "cpython-3.11.15+20260807-x86_64-unknown-linux-musl-install_only.tar.gz", + "release": "20260807", + "sha": 1 + } + }, + "3.11.16": { + "aarch64-apple-darwin": { + "file": "cpython-3.11.16+20260814-aarch64-apple-darwin-install_only.tar.gz", + "release": "20260814", + "sha": 1 + }, + "aarch64-pc-windows-msvc": { + "file": "cpython-3.11.16+20260814-aarch64-pc-windows-msvc-install_only.tar.gz", + "release": "20260814", + "sha": 1 + }, + "aarch64-unknown-linux-gnu": { + "file": "cpython-3.11.16+20260814-aarch64-unknown-linux-gnu-install_only.tar.gz", + "release": "20260814", + "sha": 1 + }, + "aarch64-unknown-linux-musl": { + "file": "cpython-3.11.16+20260814-aarch64-unknown-linux-musl-install_only.tar.gz", + "release": "20260814", + "sha": 1 + }, + "armv7-unknown-linux-gnueabi": { + "file": "cpython-3.11.16+20260814-armv7-unknown-linux-gnueabi-install_only.tar.gz", + "release": "20260814", + "sha": 1 + }, + "armv7-unknown-linux-gnueabihf": { + "file": "cpython-3.11.16+20260814-armv7-unknown-linux-gnueabihf-install_only.tar.gz", + "release": "20260814", + "sha": 1 + }, + "i686-pc-windows-msvc": { + "file": "cpython-3.11.16+20260814-i686-pc-windows-msvc-install_only.tar.gz", + "release": "20260814", + "sha": 1 + }, + "powerpc64le-unknown-linux-gnu": { + "file": "cpython-3.11.16+20260814-ppc64le-unknown-linux-gnu-install_only.tar.gz", + "release": "20260814", + "sha": 1 + }, + "riscv64gc-unknown-linux-gnu": { + "file": "cpython-3.11.16+20260814-riscv64-unknown-linux-gnu-install_only.tar.gz", + "release": "20260814", + "sha": 1 + }, + "s390x-unknown-linux-gnu": { + "file": "cpython-3.11.16+20260814-s390x-unknown-linux-gnu-install_only.tar.gz", + "release": "20260814", + "sha": 1 + }, + "x86_64-apple-darwin": { + "file": "cpython-3.11.16+20260814-x86_64-apple-darwin-install_only.tar.gz", + "release": "20260814", + "sha": 1 + }, + "x86_64-pc-windows-msvc": { + "file": "cpython-3.11.16+20260814-x86_64-pc-windows-msvc-install_only.tar.gz", + "release": "20260814", + "sha": 1 + }, + "x86_64-unknown-linux-gnu": { + "file": "cpython-3.11.16+20260814-x86_64-unknown-linux-gnu-install_only.tar.gz", + "release": "20260814", + "sha": 1 + }, + "x86_64-unknown-linux-musl": { + "file": "cpython-3.11.16+20260814-x86_64-unknown-linux-musl-install_only.tar.gz", + "release": "20260814", "sha": 1 } }, @@ -2049,73 +2188,145 @@ }, "3.12.13": { "aarch64-apple-darwin": { - "file": "cpython-3.12.13+20260610-aarch64-apple-darwin-install_only.tar.gz", - "release": "20260610", + "file": "cpython-3.12.13+20260807-aarch64-apple-darwin-install_only.tar.gz", + "release": "20260807", "sha": 1 }, "aarch64-pc-windows-msvc": { - "file": "cpython-3.12.13+20260610-aarch64-pc-windows-msvc-install_only.tar.gz", - "release": "20260610", + "file": "cpython-3.12.13+20260807-aarch64-pc-windows-msvc-install_only.tar.gz", + "release": "20260807", "sha": 1 }, "aarch64-unknown-linux-gnu": { - "file": "cpython-3.12.13+20260610-aarch64-unknown-linux-gnu-install_only.tar.gz", - "release": "20260610", + "file": "cpython-3.12.13+20260807-aarch64-unknown-linux-gnu-install_only.tar.gz", + "release": "20260807", "sha": 1 }, "aarch64-unknown-linux-musl": { - "file": "cpython-3.12.13+20260610-aarch64-unknown-linux-musl-install_only.tar.gz", - "release": "20260610", + "file": "cpython-3.12.13+20260807-aarch64-unknown-linux-musl-install_only.tar.gz", + "release": "20260807", "sha": 1 }, "armv7-unknown-linux-gnueabi": { - "file": "cpython-3.12.13+20260610-armv7-unknown-linux-gnueabi-install_only.tar.gz", - "release": "20260610", + "file": "cpython-3.12.13+20260807-armv7-unknown-linux-gnueabi-install_only.tar.gz", + "release": "20260807", "sha": 1 }, "armv7-unknown-linux-gnueabihf": { - "file": "cpython-3.12.13+20260610-armv7-unknown-linux-gnueabihf-install_only.tar.gz", - "release": "20260610", + "file": "cpython-3.12.13+20260807-armv7-unknown-linux-gnueabihf-install_only.tar.gz", + "release": "20260807", "sha": 1 }, "i686-pc-windows-msvc": { - "file": "cpython-3.12.13+20260610-i686-pc-windows-msvc-install_only.tar.gz", - "release": "20260610", + "file": "cpython-3.12.13+20260807-i686-pc-windows-msvc-install_only.tar.gz", + "release": "20260807", "sha": 1 }, "powerpc64le-unknown-linux-gnu": { - "file": "cpython-3.12.13+20260610-ppc64le-unknown-linux-gnu-install_only.tar.gz", - "release": "20260610", + "file": "cpython-3.12.13+20260807-ppc64le-unknown-linux-gnu-install_only.tar.gz", + "release": "20260807", "sha": 1 }, "riscv64gc-unknown-linux-gnu": { - "file": "cpython-3.12.13+20260610-riscv64-unknown-linux-gnu-install_only.tar.gz", - "release": "20260610", + "file": "cpython-3.12.13+20260807-riscv64-unknown-linux-gnu-install_only.tar.gz", + "release": "20260807", "sha": 1 }, "s390x-unknown-linux-gnu": { - "file": "cpython-3.12.13+20260610-s390x-unknown-linux-gnu-install_only.tar.gz", - "release": "20260610", + "file": "cpython-3.12.13+20260807-s390x-unknown-linux-gnu-install_only.tar.gz", + "release": "20260807", "sha": 1 }, "x86_64-apple-darwin": { - "file": "cpython-3.12.13+20260610-x86_64-apple-darwin-install_only.tar.gz", - "release": "20260610", + "file": "cpython-3.12.13+20260807-x86_64-apple-darwin-install_only.tar.gz", + "release": "20260807", "sha": 1 }, "x86_64-pc-windows-msvc": { - "file": "cpython-3.12.13+20260610-x86_64-pc-windows-msvc-install_only.tar.gz", - "release": "20260610", + "file": "cpython-3.12.13+20260807-x86_64-pc-windows-msvc-install_only.tar.gz", + "release": "20260807", "sha": 1 }, "x86_64-unknown-linux-gnu": { - "file": "cpython-3.12.13+20260610-x86_64-unknown-linux-gnu-install_only.tar.gz", - "release": "20260610", + "file": "cpython-3.12.13+20260807-x86_64-unknown-linux-gnu-install_only.tar.gz", + "release": "20260807", "sha": 1 }, "x86_64-unknown-linux-musl": { - "file": "cpython-3.12.13+20260610-x86_64-unknown-linux-musl-install_only.tar.gz", - "release": "20260610", + "file": "cpython-3.12.13+20260807-x86_64-unknown-linux-musl-install_only.tar.gz", + "release": "20260807", + "sha": 1 + } + }, + "3.12.14": { + "aarch64-apple-darwin": { + "file": "cpython-3.12.14+20260814-aarch64-apple-darwin-install_only.tar.gz", + "release": "20260814", + "sha": 1 + }, + "aarch64-pc-windows-msvc": { + "file": "cpython-3.12.14+20260814-aarch64-pc-windows-msvc-install_only.tar.gz", + "release": "20260814", + "sha": 1 + }, + "aarch64-unknown-linux-gnu": { + "file": "cpython-3.12.14+20260814-aarch64-unknown-linux-gnu-install_only.tar.gz", + "release": "20260814", + "sha": 1 + }, + "aarch64-unknown-linux-musl": { + "file": "cpython-3.12.14+20260814-aarch64-unknown-linux-musl-install_only.tar.gz", + "release": "20260814", + "sha": 1 + }, + "armv7-unknown-linux-gnueabi": { + "file": "cpython-3.12.14+20260814-armv7-unknown-linux-gnueabi-install_only.tar.gz", + "release": "20260814", + "sha": 1 + }, + "armv7-unknown-linux-gnueabihf": { + "file": "cpython-3.12.14+20260814-armv7-unknown-linux-gnueabihf-install_only.tar.gz", + "release": "20260814", + "sha": 1 + }, + "i686-pc-windows-msvc": { + "file": "cpython-3.12.14+20260814-i686-pc-windows-msvc-install_only.tar.gz", + "release": "20260814", + "sha": 1 + }, + "powerpc64le-unknown-linux-gnu": { + "file": "cpython-3.12.14+20260814-ppc64le-unknown-linux-gnu-install_only.tar.gz", + "release": "20260814", + "sha": 1 + }, + "riscv64gc-unknown-linux-gnu": { + "file": "cpython-3.12.14+20260814-riscv64-unknown-linux-gnu-install_only.tar.gz", + "release": "20260814", + "sha": 1 + }, + "s390x-unknown-linux-gnu": { + "file": "cpython-3.12.14+20260814-s390x-unknown-linux-gnu-install_only.tar.gz", + "release": "20260814", + "sha": 1 + }, + "x86_64-apple-darwin": { + "file": "cpython-3.12.14+20260814-x86_64-apple-darwin-install_only.tar.gz", + "release": "20260814", + "sha": 1 + }, + "x86_64-pc-windows-msvc": { + "file": "cpython-3.12.14+20260814-x86_64-pc-windows-msvc-install_only.tar.gz", + "release": "20260814", + "sha": 1 + }, + "x86_64-unknown-linux-gnu": { + "file": "cpython-3.12.14+20260814-x86_64-unknown-linux-gnu-install_only.tar.gz", + "release": "20260814", + "sha": 1 + }, + "x86_64-unknown-linux-musl": { + "file": "cpython-3.12.14+20260814-x86_64-unknown-linux-musl-install_only.tar.gz", + "release": "20260814", "sha": 1 } }, @@ -3098,73 +3309,145 @@ }, "3.13.14": { "aarch64-apple-darwin": { - "file": "cpython-3.13.14+20260610-aarch64-apple-darwin-install_only.tar.gz", - "release": "20260610", + "file": "cpython-3.13.14+20260805-aarch64-apple-darwin-install_only.tar.gz", + "release": "20260805", "sha": 1 }, "aarch64-pc-windows-msvc": { - "file": "cpython-3.13.14+20260610-aarch64-pc-windows-msvc-install_only.tar.gz", - "release": "20260610", + "file": "cpython-3.13.14+20260805-aarch64-pc-windows-msvc-install_only.tar.gz", + "release": "20260805", "sha": 1 }, "aarch64-unknown-linux-gnu": { - "file": "cpython-3.13.14+20260610-aarch64-unknown-linux-gnu-install_only.tar.gz", - "release": "20260610", + "file": "cpython-3.13.14+20260805-aarch64-unknown-linux-gnu-install_only.tar.gz", + "release": "20260805", "sha": 1 }, "aarch64-unknown-linux-musl": { - "file": "cpython-3.13.14+20260610-aarch64-unknown-linux-musl-install_only.tar.gz", - "release": "20260610", + "file": "cpython-3.13.14+20260805-aarch64-unknown-linux-musl-install_only.tar.gz", + "release": "20260805", "sha": 1 }, "armv7-unknown-linux-gnueabi": { - "file": "cpython-3.13.14+20260610-armv7-unknown-linux-gnueabi-install_only.tar.gz", - "release": "20260610", + "file": "cpython-3.13.14+20260805-armv7-unknown-linux-gnueabi-install_only.tar.gz", + "release": "20260805", "sha": 1 }, "armv7-unknown-linux-gnueabihf": { - "file": "cpython-3.13.14+20260610-armv7-unknown-linux-gnueabihf-install_only.tar.gz", - "release": "20260610", + "file": "cpython-3.13.14+20260805-armv7-unknown-linux-gnueabihf-install_only.tar.gz", + "release": "20260805", "sha": 1 }, "i686-pc-windows-msvc": { - "file": "cpython-3.13.14+20260610-i686-pc-windows-msvc-install_only.tar.gz", - "release": "20260610", + "file": "cpython-3.13.14+20260805-i686-pc-windows-msvc-install_only.tar.gz", + "release": "20260805", "sha": 1 }, "powerpc64le-unknown-linux-gnu": { - "file": "cpython-3.13.14+20260610-ppc64le-unknown-linux-gnu-install_only.tar.gz", - "release": "20260610", + "file": "cpython-3.13.14+20260805-ppc64le-unknown-linux-gnu-install_only.tar.gz", + "release": "20260805", "sha": 1 }, "riscv64gc-unknown-linux-gnu": { - "file": "cpython-3.13.14+20260610-riscv64-unknown-linux-gnu-install_only.tar.gz", - "release": "20260610", + "file": "cpython-3.13.14+20260805-riscv64-unknown-linux-gnu-install_only.tar.gz", + "release": "20260805", "sha": 1 }, "s390x-unknown-linux-gnu": { - "file": "cpython-3.13.14+20260610-s390x-unknown-linux-gnu-install_only.tar.gz", - "release": "20260610", + "file": "cpython-3.13.14+20260805-s390x-unknown-linux-gnu-install_only.tar.gz", + "release": "20260805", "sha": 1 }, "x86_64-apple-darwin": { - "file": "cpython-3.13.14+20260610-x86_64-apple-darwin-install_only.tar.gz", - "release": "20260610", + "file": "cpython-3.13.14+20260805-x86_64-apple-darwin-install_only.tar.gz", + "release": "20260805", "sha": 1 }, "x86_64-pc-windows-msvc": { - "file": "cpython-3.13.14+20260610-x86_64-pc-windows-msvc-install_only.tar.gz", - "release": "20260610", + "file": "cpython-3.13.14+20260805-x86_64-pc-windows-msvc-install_only.tar.gz", + "release": "20260805", "sha": 1 }, "x86_64-unknown-linux-gnu": { - "file": "cpython-3.13.14+20260610-x86_64-unknown-linux-gnu-install_only.tar.gz", - "release": "20260610", + "file": "cpython-3.13.14+20260805-x86_64-unknown-linux-gnu-install_only.tar.gz", + "release": "20260805", "sha": 1 }, "x86_64-unknown-linux-musl": { - "file": "cpython-3.13.14+20260610-x86_64-unknown-linux-musl-install_only.tar.gz", - "release": "20260610", + "file": "cpython-3.13.14+20260805-x86_64-unknown-linux-musl-install_only.tar.gz", + "release": "20260805", + "sha": 1 + } + }, + "3.13.15": { + "aarch64-apple-darwin": { + "file": "cpython-3.13.15+20260814-aarch64-apple-darwin-install_only.tar.gz", + "release": "20260814", + "sha": 1 + }, + "aarch64-pc-windows-msvc": { + "file": "cpython-3.13.15+20260814-aarch64-pc-windows-msvc-install_only.tar.gz", + "release": "20260814", + "sha": 1 + }, + "aarch64-unknown-linux-gnu": { + "file": "cpython-3.13.15+20260814-aarch64-unknown-linux-gnu-install_only.tar.gz", + "release": "20260814", + "sha": 1 + }, + "aarch64-unknown-linux-musl": { + "file": "cpython-3.13.15+20260814-aarch64-unknown-linux-musl-install_only.tar.gz", + "release": "20260814", + "sha": 1 + }, + "armv7-unknown-linux-gnueabi": { + "file": "cpython-3.13.15+20260814-armv7-unknown-linux-gnueabi-install_only.tar.gz", + "release": "20260814", + "sha": 1 + }, + "armv7-unknown-linux-gnueabihf": { + "file": "cpython-3.13.15+20260814-armv7-unknown-linux-gnueabihf-install_only.tar.gz", + "release": "20260814", + "sha": 1 + }, + "i686-pc-windows-msvc": { + "file": "cpython-3.13.15+20260814-i686-pc-windows-msvc-install_only.tar.gz", + "release": "20260814", + "sha": 1 + }, + "powerpc64le-unknown-linux-gnu": { + "file": "cpython-3.13.15+20260814-ppc64le-unknown-linux-gnu-install_only.tar.gz", + "release": "20260814", + "sha": 1 + }, + "riscv64gc-unknown-linux-gnu": { + "file": "cpython-3.13.15+20260814-riscv64-unknown-linux-gnu-install_only.tar.gz", + "release": "20260814", + "sha": 1 + }, + "s390x-unknown-linux-gnu": { + "file": "cpython-3.13.15+20260814-s390x-unknown-linux-gnu-install_only.tar.gz", + "release": "20260814", + "sha": 1 + }, + "x86_64-apple-darwin": { + "file": "cpython-3.13.15+20260814-x86_64-apple-darwin-install_only.tar.gz", + "release": "20260814", + "sha": 1 + }, + "x86_64-pc-windows-msvc": { + "file": "cpython-3.13.15+20260814-x86_64-pc-windows-msvc-install_only.tar.gz", + "release": "20260814", + "sha": 1 + }, + "x86_64-unknown-linux-gnu": { + "file": "cpython-3.13.15+20260814-x86_64-unknown-linux-gnu-install_only.tar.gz", + "release": "20260814", + "sha": 1 + }, + "x86_64-unknown-linux-musl": { + "file": "cpython-3.13.15+20260814-x86_64-unknown-linux-musl-install_only.tar.gz", + "release": "20260814", "sha": 1 } }, @@ -4954,73 +5237,145 @@ }, "3.14.6": { "aarch64-apple-darwin": { - "file": "cpython-3.14.6+20260610-aarch64-apple-darwin-install_only.tar.gz", - "release": "20260610", + "file": "cpython-3.14.6+20260804-aarch64-apple-darwin-install_only.tar.gz", + "release": "20260804", "sha": 1 }, "aarch64-pc-windows-msvc": { - "file": "cpython-3.14.6+20260610-aarch64-pc-windows-msvc-install_only.tar.gz", - "release": "20260610", + "file": "cpython-3.14.6+20260804-aarch64-pc-windows-msvc-install_only.tar.gz", + "release": "20260804", "sha": 1 }, "aarch64-unknown-linux-gnu": { - "file": "cpython-3.14.6+20260610-aarch64-unknown-linux-gnu-install_only.tar.gz", - "release": "20260610", + "file": "cpython-3.14.6+20260804-aarch64-unknown-linux-gnu-install_only.tar.gz", + "release": "20260804", "sha": 1 }, "aarch64-unknown-linux-musl": { - "file": "cpython-3.14.6+20260610-aarch64-unknown-linux-musl-install_only.tar.gz", - "release": "20260610", + "file": "cpython-3.14.6+20260804-aarch64-unknown-linux-musl-install_only.tar.gz", + "release": "20260804", "sha": 1 }, "armv7-unknown-linux-gnueabi": { - "file": "cpython-3.14.6+20260610-armv7-unknown-linux-gnueabi-install_only.tar.gz", - "release": "20260610", + "file": "cpython-3.14.6+20260804-armv7-unknown-linux-gnueabi-install_only.tar.gz", + "release": "20260804", "sha": 1 }, "armv7-unknown-linux-gnueabihf": { - "file": "cpython-3.14.6+20260610-armv7-unknown-linux-gnueabihf-install_only.tar.gz", - "release": "20260610", + "file": "cpython-3.14.6+20260804-armv7-unknown-linux-gnueabihf-install_only.tar.gz", + "release": "20260804", "sha": 1 }, "i686-pc-windows-msvc": { - "file": "cpython-3.14.6+20260610-i686-pc-windows-msvc-install_only.tar.gz", - "release": "20260610", + "file": "cpython-3.14.6+20260804-i686-pc-windows-msvc-install_only.tar.gz", + "release": "20260804", "sha": 1 }, "powerpc64le-unknown-linux-gnu": { - "file": "cpython-3.14.6+20260610-ppc64le-unknown-linux-gnu-install_only.tar.gz", - "release": "20260610", + "file": "cpython-3.14.6+20260804-ppc64le-unknown-linux-gnu-install_only.tar.gz", + "release": "20260804", "sha": 1 }, "riscv64gc-unknown-linux-gnu": { - "file": "cpython-3.14.6+20260610-riscv64-unknown-linux-gnu-install_only.tar.gz", - "release": "20260610", + "file": "cpython-3.14.6+20260804-riscv64-unknown-linux-gnu-install_only.tar.gz", + "release": "20260804", "sha": 1 }, "s390x-unknown-linux-gnu": { - "file": "cpython-3.14.6+20260610-s390x-unknown-linux-gnu-install_only.tar.gz", - "release": "20260610", + "file": "cpython-3.14.6+20260804-s390x-unknown-linux-gnu-install_only.tar.gz", + "release": "20260804", "sha": 1 }, "x86_64-apple-darwin": { - "file": "cpython-3.14.6+20260610-x86_64-apple-darwin-install_only.tar.gz", - "release": "20260610", + "file": "cpython-3.14.6+20260804-x86_64-apple-darwin-install_only.tar.gz", + "release": "20260804", "sha": 1 }, "x86_64-pc-windows-msvc": { - "file": "cpython-3.14.6+20260610-x86_64-pc-windows-msvc-install_only.tar.gz", - "release": "20260610", + "file": "cpython-3.14.6+20260804-x86_64-pc-windows-msvc-install_only.tar.gz", + "release": "20260804", "sha": 1 }, "x86_64-unknown-linux-gnu": { - "file": "cpython-3.14.6+20260610-x86_64-unknown-linux-gnu-install_only.tar.gz", - "release": "20260610", + "file": "cpython-3.14.6+20260804-x86_64-unknown-linux-gnu-install_only.tar.gz", + "release": "20260804", "sha": 1 }, "x86_64-unknown-linux-musl": { - "file": "cpython-3.14.6+20260610-x86_64-unknown-linux-musl-install_only.tar.gz", - "release": "20260610", + "file": "cpython-3.14.6+20260804-x86_64-unknown-linux-musl-install_only.tar.gz", + "release": "20260804", + "sha": 1 + } + }, + "3.14.7": { + "aarch64-apple-darwin": { + "file": "cpython-3.14.7+20260814-aarch64-apple-darwin-install_only.tar.gz", + "release": "20260814", + "sha": 1 + }, + "aarch64-pc-windows-msvc": { + "file": "cpython-3.14.7+20260814-aarch64-pc-windows-msvc-install_only.tar.gz", + "release": "20260814", + "sha": 1 + }, + "aarch64-unknown-linux-gnu": { + "file": "cpython-3.14.7+20260814-aarch64-unknown-linux-gnu-install_only.tar.gz", + "release": "20260814", + "sha": 1 + }, + "aarch64-unknown-linux-musl": { + "file": "cpython-3.14.7+20260814-aarch64-unknown-linux-musl-install_only.tar.gz", + "release": "20260814", + "sha": 1 + }, + "armv7-unknown-linux-gnueabi": { + "file": "cpython-3.14.7+20260814-armv7-unknown-linux-gnueabi-install_only.tar.gz", + "release": "20260814", + "sha": 1 + }, + "armv7-unknown-linux-gnueabihf": { + "file": "cpython-3.14.7+20260814-armv7-unknown-linux-gnueabihf-install_only.tar.gz", + "release": "20260814", + "sha": 1 + }, + "i686-pc-windows-msvc": { + "file": "cpython-3.14.7+20260814-i686-pc-windows-msvc-install_only.tar.gz", + "release": "20260814", + "sha": 1 + }, + "powerpc64le-unknown-linux-gnu": { + "file": "cpython-3.14.7+20260814-ppc64le-unknown-linux-gnu-install_only.tar.gz", + "release": "20260814", + "sha": 1 + }, + "riscv64gc-unknown-linux-gnu": { + "file": "cpython-3.14.7+20260814-riscv64-unknown-linux-gnu-install_only.tar.gz", + "release": "20260814", + "sha": 1 + }, + "s390x-unknown-linux-gnu": { + "file": "cpython-3.14.7+20260814-s390x-unknown-linux-gnu-install_only.tar.gz", + "release": "20260814", + "sha": 1 + }, + "x86_64-apple-darwin": { + "file": "cpython-3.14.7+20260814-x86_64-apple-darwin-install_only.tar.gz", + "release": "20260814", + "sha": 1 + }, + "x86_64-pc-windows-msvc": { + "file": "cpython-3.14.7+20260814-x86_64-pc-windows-msvc-install_only.tar.gz", + "release": "20260814", + "sha": 1 + }, + "x86_64-unknown-linux-gnu": { + "file": "cpython-3.14.7+20260814-x86_64-unknown-linux-gnu-install_only.tar.gz", + "release": "20260814", + "sha": 1 + }, + "x86_64-unknown-linux-musl": { + "file": "cpython-3.14.7+20260814-x86_64-unknown-linux-musl-install_only.tar.gz", + "release": "20260814", "sha": 1 } }, @@ -5744,6 +6099,222 @@ "sha": 1 } }, + "3.15.0-b.3": { + "aarch64-apple-darwin": { + "file": "cpython-3.15.0b3+20260623-aarch64-apple-darwin-install_only.tar.gz", + "release": "20260623", + "sha": 1 + }, + "aarch64-pc-windows-msvc": { + "file": "cpython-3.15.0b3+20260623-aarch64-pc-windows-msvc-install_only.tar.gz", + "release": "20260623", + "sha": 1 + }, + "aarch64-unknown-linux-gnu": { + "file": "cpython-3.15.0b3+20260623-aarch64-unknown-linux-gnu-install_only.tar.gz", + "release": "20260623", + "sha": 1 + }, + "aarch64-unknown-linux-musl": { + "file": "cpython-3.15.0b3+20260623-aarch64-unknown-linux-musl-install_only.tar.gz", + "release": "20260623", + "sha": 1 + }, + "armv7-unknown-linux-gnueabi": { + "file": "cpython-3.15.0b3+20260623-armv7-unknown-linux-gnueabi-install_only.tar.gz", + "release": "20260623", + "sha": 1 + }, + "armv7-unknown-linux-gnueabihf": { + "file": "cpython-3.15.0b3+20260623-armv7-unknown-linux-gnueabihf-install_only.tar.gz", + "release": "20260623", + "sha": 1 + }, + "i686-pc-windows-msvc": { + "file": "cpython-3.15.0b3+20260623-i686-pc-windows-msvc-install_only.tar.gz", + "release": "20260623", + "sha": 1 + }, + "powerpc64le-unknown-linux-gnu": { + "file": "cpython-3.15.0b3+20260623-ppc64le-unknown-linux-gnu-install_only.tar.gz", + "release": "20260623", + "sha": 1 + }, + "riscv64gc-unknown-linux-gnu": { + "file": "cpython-3.15.0b3+20260623-riscv64-unknown-linux-gnu-install_only.tar.gz", + "release": "20260623", + "sha": 1 + }, + "s390x-unknown-linux-gnu": { + "file": "cpython-3.15.0b3+20260623-s390x-unknown-linux-gnu-install_only.tar.gz", + "release": "20260623", + "sha": 1 + }, + "x86_64-apple-darwin": { + "file": "cpython-3.15.0b3+20260623-x86_64-apple-darwin-install_only.tar.gz", + "release": "20260623", + "sha": 1 + }, + "x86_64-pc-windows-msvc": { + "file": "cpython-3.15.0b3+20260623-x86_64-pc-windows-msvc-install_only.tar.gz", + "release": "20260623", + "sha": 1 + }, + "x86_64-unknown-linux-gnu": { + "file": "cpython-3.15.0b3+20260623-x86_64-unknown-linux-gnu-install_only.tar.gz", + "release": "20260623", + "sha": 1 + }, + "x86_64-unknown-linux-musl": { + "file": "cpython-3.15.0b3+20260623-x86_64-unknown-linux-musl-install_only.tar.gz", + "release": "20260623", + "sha": 1 + } + }, + "3.15.0-b.4": { + "aarch64-apple-darwin": { + "file": "cpython-3.15.0b4+20260728-aarch64-apple-darwin-install_only.tar.gz", + "release": "20260728", + "sha": 1 + }, + "aarch64-pc-windows-msvc": { + "file": "cpython-3.15.0b4+20260728-aarch64-pc-windows-msvc-install_only.tar.gz", + "release": "20260728", + "sha": 1 + }, + "aarch64-unknown-linux-gnu": { + "file": "cpython-3.15.0b4+20260728-aarch64-unknown-linux-gnu-install_only.tar.gz", + "release": "20260728", + "sha": 1 + }, + "aarch64-unknown-linux-musl": { + "file": "cpython-3.15.0b4+20260728-aarch64-unknown-linux-musl-install_only.tar.gz", + "release": "20260728", + "sha": 1 + }, + "armv7-unknown-linux-gnueabi": { + "file": "cpython-3.15.0b4+20260728-armv7-unknown-linux-gnueabi-install_only.tar.gz", + "release": "20260728", + "sha": 1 + }, + "armv7-unknown-linux-gnueabihf": { + "file": "cpython-3.15.0b4+20260728-armv7-unknown-linux-gnueabihf-install_only.tar.gz", + "release": "20260728", + "sha": 1 + }, + "i686-pc-windows-msvc": { + "file": "cpython-3.15.0b4+20260728-i686-pc-windows-msvc-install_only.tar.gz", + "release": "20260728", + "sha": 1 + }, + "powerpc64le-unknown-linux-gnu": { + "file": "cpython-3.15.0b4+20260728-ppc64le-unknown-linux-gnu-install_only.tar.gz", + "release": "20260728", + "sha": 1 + }, + "riscv64gc-unknown-linux-gnu": { + "file": "cpython-3.15.0b4+20260728-riscv64-unknown-linux-gnu-install_only.tar.gz", + "release": "20260728", + "sha": 1 + }, + "s390x-unknown-linux-gnu": { + "file": "cpython-3.15.0b4+20260728-s390x-unknown-linux-gnu-install_only.tar.gz", + "release": "20260728", + "sha": 1 + }, + "x86_64-apple-darwin": { + "file": "cpython-3.15.0b4+20260728-x86_64-apple-darwin-install_only.tar.gz", + "release": "20260728", + "sha": 1 + }, + "x86_64-pc-windows-msvc": { + "file": "cpython-3.15.0b4+20260728-x86_64-pc-windows-msvc-install_only.tar.gz", + "release": "20260728", + "sha": 1 + }, + "x86_64-unknown-linux-gnu": { + "file": "cpython-3.15.0b4+20260728-x86_64-unknown-linux-gnu-install_only.tar.gz", + "release": "20260728", + "sha": 1 + }, + "x86_64-unknown-linux-musl": { + "file": "cpython-3.15.0b4+20260728-x86_64-unknown-linux-musl-install_only.tar.gz", + "release": "20260728", + "sha": 1 + } + }, + "3.15.0-rc.1": { + "aarch64-apple-darwin": { + "file": "cpython-3.15.0rc1+20260814-aarch64-apple-darwin-install_only.tar.gz", + "release": "20260814", + "sha": 1 + }, + "aarch64-pc-windows-msvc": { + "file": "cpython-3.15.0rc1+20260814-aarch64-pc-windows-msvc-install_only.tar.gz", + "release": "20260814", + "sha": 1 + }, + "aarch64-unknown-linux-gnu": { + "file": "cpython-3.15.0rc1+20260814-aarch64-unknown-linux-gnu-install_only.tar.gz", + "release": "20260814", + "sha": 1 + }, + "aarch64-unknown-linux-musl": { + "file": "cpython-3.15.0rc1+20260814-aarch64-unknown-linux-musl-install_only.tar.gz", + "release": "20260814", + "sha": 1 + }, + "armv7-unknown-linux-gnueabi": { + "file": "cpython-3.15.0rc1+20260814-armv7-unknown-linux-gnueabi-install_only.tar.gz", + "release": "20260814", + "sha": 1 + }, + "armv7-unknown-linux-gnueabihf": { + "file": "cpython-3.15.0rc1+20260814-armv7-unknown-linux-gnueabihf-install_only.tar.gz", + "release": "20260814", + "sha": 1 + }, + "i686-pc-windows-msvc": { + "file": "cpython-3.15.0rc1+20260814-i686-pc-windows-msvc-install_only.tar.gz", + "release": "20260814", + "sha": 1 + }, + "powerpc64le-unknown-linux-gnu": { + "file": "cpython-3.15.0rc1+20260814-ppc64le-unknown-linux-gnu-install_only.tar.gz", + "release": "20260814", + "sha": 1 + }, + "riscv64gc-unknown-linux-gnu": { + "file": "cpython-3.15.0rc1+20260814-riscv64-unknown-linux-gnu-install_only.tar.gz", + "release": "20260814", + "sha": 1 + }, + "s390x-unknown-linux-gnu": { + "file": "cpython-3.15.0rc1+20260814-s390x-unknown-linux-gnu-install_only.tar.gz", + "release": "20260814", + "sha": 1 + }, + "x86_64-apple-darwin": { + "file": "cpython-3.15.0rc1+20260814-x86_64-apple-darwin-install_only.tar.gz", + "release": "20260814", + "sha": 1 + }, + "x86_64-pc-windows-msvc": { + "file": "cpython-3.15.0rc1+20260814-x86_64-pc-windows-msvc-install_only.tar.gz", + "release": "20260814", + "sha": 1 + }, + "x86_64-unknown-linux-gnu": { + "file": "cpython-3.15.0rc1+20260814-x86_64-unknown-linux-gnu-install_only.tar.gz", + "release": "20260814", + "sha": 1 + }, + "x86_64-unknown-linux-musl": { + "file": "cpython-3.15.0rc1+20260814-x86_64-unknown-linux-musl-install_only.tar.gz", + "release": "20260814", + "sha": 1 + } + }, "3.7.6": { "x86_64-pc-windows-msvc": { "file": "cpython-3.7.6-windows-x86-shared-pgo-20200217T0110.tar.zst", diff --git a/tools/python/releases.json b/tools/python/releases.json index 31a15da6..993e5b5c 100644 --- a/tools/python/releases.json +++ b/tools/python/releases.json @@ -483,56 +483,110 @@ }, "3.10.20": { "aarch64-apple-darwin": { - "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/SHA256SUMS", - "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.10.20+20260610-aarch64-apple-darwin-pgo+lto-full.tar.zst" + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/cpython-3.10.20+20260807-aarch64-apple-darwin-pgo+lto-full.tar.zst" }, "aarch64-unknown-linux-gnu": { - "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/SHA256SUMS", - "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.10.20+20260610-aarch64-unknown-linux-gnu-pgo+lto-full.tar.zst" + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/cpython-3.10.20+20260807-aarch64-unknown-linux-gnu-pgo+lto-full.tar.zst" }, "aarch64-unknown-linux-musl": { - "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/SHA256SUMS", - "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.10.20+20260610-aarch64-unknown-linux-musl-lto-full.tar.zst" + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/cpython-3.10.20+20260807-aarch64-unknown-linux-musl-lto-full.tar.zst" }, "armv7-unknown-linux-gnueabi": { - "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/SHA256SUMS", - "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.10.20+20260610-armv7-unknown-linux-gnueabi-lto-full.tar.zst" + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/cpython-3.10.20+20260807-armv7-unknown-linux-gnueabi-lto-full.tar.zst" }, "armv7-unknown-linux-gnueabihf": { - "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/SHA256SUMS", - "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.10.20+20260610-armv7-unknown-linux-gnueabihf-lto-full.tar.zst" + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/cpython-3.10.20+20260807-armv7-unknown-linux-gnueabihf-lto-full.tar.zst" }, "i686-pc-windows-msvc": { - "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/SHA256SUMS", - "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.10.20+20260610-i686-pc-windows-msvc-pgo-full.tar.zst" + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/cpython-3.10.20+20260807-i686-pc-windows-msvc-pgo-full.tar.zst" }, "powerpc64le-unknown-linux-gnu": { - "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/SHA256SUMS", - "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.10.20+20260610-ppc64le-unknown-linux-gnu-lto-full.tar.zst" + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/cpython-3.10.20+20260807-ppc64le-unknown-linux-gnu-lto-full.tar.zst" }, "riscv64gc-unknown-linux-gnu": { - "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/SHA256SUMS", - "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.10.20+20260610-riscv64-unknown-linux-gnu-lto-full.tar.zst" + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/cpython-3.10.20+20260807-riscv64-unknown-linux-gnu-lto-full.tar.zst" }, "s390x-unknown-linux-gnu": { - "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/SHA256SUMS", - "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.10.20+20260610-s390x-unknown-linux-gnu-lto-full.tar.zst" + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/cpython-3.10.20+20260807-s390x-unknown-linux-gnu-lto-full.tar.zst" }, "x86_64-apple-darwin": { - "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/SHA256SUMS", - "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.10.20+20260610-x86_64-apple-darwin-pgo+lto-full.tar.zst" + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/cpython-3.10.20+20260807-x86_64-apple-darwin-pgo+lto-full.tar.zst" }, "x86_64-pc-windows-msvc": { - "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/SHA256SUMS", - "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.10.20+20260610-x86_64-pc-windows-msvc-pgo-full.tar.zst" + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/cpython-3.10.20+20260807-x86_64-pc-windows-msvc-pgo-full.tar.zst" }, "x86_64-unknown-linux-gnu": { - "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/SHA256SUMS", - "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.10.20+20260610-x86_64-unknown-linux-gnu-pgo+lto-full.tar.zst" + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/cpython-3.10.20+20260807-x86_64-unknown-linux-gnu-pgo+lto-full.tar.zst" }, "x86_64-unknown-linux-musl": { - "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/SHA256SUMS", - "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.10.20+20260610-x86_64-unknown-linux-musl-lto-full.tar.zst" + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/cpython-3.10.20+20260807-x86_64-unknown-linux-musl-lto-full.tar.zst" + } + }, + "3.10.21": { + "aarch64-apple-darwin": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.10.21+20260814-aarch64-apple-darwin-pgo+lto-full.tar.zst" + }, + "aarch64-unknown-linux-gnu": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.10.21+20260814-aarch64-unknown-linux-gnu-pgo+lto-full.tar.zst" + }, + "aarch64-unknown-linux-musl": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.10.21+20260814-aarch64-unknown-linux-musl-lto-full.tar.zst" + }, + "armv7-unknown-linux-gnueabi": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.10.21+20260814-armv7-unknown-linux-gnueabi-lto-full.tar.zst" + }, + "armv7-unknown-linux-gnueabihf": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.10.21+20260814-armv7-unknown-linux-gnueabihf-lto-full.tar.zst" + }, + "i686-pc-windows-msvc": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.10.21+20260814-i686-pc-windows-msvc-pgo-full.tar.zst" + }, + "powerpc64le-unknown-linux-gnu": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.10.21+20260814-ppc64le-unknown-linux-gnu-lto-full.tar.zst" + }, + "riscv64gc-unknown-linux-gnu": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.10.21+20260814-riscv64-unknown-linux-gnu-lto-full.tar.zst" + }, + "s390x-unknown-linux-gnu": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.10.21+20260814-s390x-unknown-linux-gnu-lto-full.tar.zst" + }, + "x86_64-apple-darwin": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.10.21+20260814-x86_64-apple-darwin-pgo+lto-full.tar.zst" + }, + "x86_64-pc-windows-msvc": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.10.21+20260814-x86_64-pc-windows-msvc-pgo-full.tar.zst" + }, + "x86_64-unknown-linux-gnu": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.10.21+20260814-x86_64-unknown-linux-gnu-pgo+lto-full.tar.zst" + }, + "x86_64-unknown-linux-musl": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.10.21+20260814-x86_64-unknown-linux-musl-lto-full.tar.zst" } }, "3.10.3": { @@ -1071,60 +1125,118 @@ }, "3.11.15": { "aarch64-apple-darwin": { - "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/SHA256SUMS", - "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.11.15+20260610-aarch64-apple-darwin-pgo+lto-full.tar.zst" + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/cpython-3.11.15+20260807-aarch64-apple-darwin-pgo+lto-full.tar.zst" }, "aarch64-pc-windows-msvc": { - "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/SHA256SUMS", - "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.11.15+20260610-aarch64-pc-windows-msvc-pgo-full.tar.zst" + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/cpython-3.11.15+20260807-aarch64-pc-windows-msvc-pgo-full.tar.zst" }, "aarch64-unknown-linux-gnu": { - "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/SHA256SUMS", - "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.11.15+20260610-aarch64-unknown-linux-gnu-pgo+lto-full.tar.zst" + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/cpython-3.11.15+20260807-aarch64-unknown-linux-gnu-pgo+lto-full.tar.zst" }, "aarch64-unknown-linux-musl": { - "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/SHA256SUMS", - "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.11.15+20260610-aarch64-unknown-linux-musl-lto-full.tar.zst" + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/cpython-3.11.15+20260807-aarch64-unknown-linux-musl-lto-full.tar.zst" }, "armv7-unknown-linux-gnueabi": { - "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/SHA256SUMS", - "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.11.15+20260610-armv7-unknown-linux-gnueabi-lto-full.tar.zst" + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/cpython-3.11.15+20260807-armv7-unknown-linux-gnueabi-lto-full.tar.zst" }, "armv7-unknown-linux-gnueabihf": { - "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/SHA256SUMS", - "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.11.15+20260610-armv7-unknown-linux-gnueabihf-lto-full.tar.zst" + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/cpython-3.11.15+20260807-armv7-unknown-linux-gnueabihf-lto-full.tar.zst" }, "i686-pc-windows-msvc": { - "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/SHA256SUMS", - "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.11.15+20260610-i686-pc-windows-msvc-pgo-full.tar.zst" + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/cpython-3.11.15+20260807-i686-pc-windows-msvc-pgo-full.tar.zst" }, "powerpc64le-unknown-linux-gnu": { - "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/SHA256SUMS", - "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.11.15+20260610-ppc64le-unknown-linux-gnu-lto-full.tar.zst" + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/cpython-3.11.15+20260807-ppc64le-unknown-linux-gnu-lto-full.tar.zst" }, "riscv64gc-unknown-linux-gnu": { - "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/SHA256SUMS", - "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.11.15+20260610-riscv64-unknown-linux-gnu-lto-full.tar.zst" + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/cpython-3.11.15+20260807-riscv64-unknown-linux-gnu-lto-full.tar.zst" }, "s390x-unknown-linux-gnu": { - "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/SHA256SUMS", - "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.11.15+20260610-s390x-unknown-linux-gnu-lto-full.tar.zst" + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/cpython-3.11.15+20260807-s390x-unknown-linux-gnu-lto-full.tar.zst" }, "x86_64-apple-darwin": { - "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/SHA256SUMS", - "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.11.15+20260610-x86_64-apple-darwin-pgo+lto-full.tar.zst" + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/cpython-3.11.15+20260807-x86_64-apple-darwin-pgo+lto-full.tar.zst" }, "x86_64-pc-windows-msvc": { - "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/SHA256SUMS", - "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.11.15+20260610-x86_64-pc-windows-msvc-pgo-full.tar.zst" + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/cpython-3.11.15+20260807-x86_64-pc-windows-msvc-pgo-full.tar.zst" }, "x86_64-unknown-linux-gnu": { - "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/SHA256SUMS", - "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.11.15+20260610-x86_64-unknown-linux-gnu-pgo+lto-full.tar.zst" + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/cpython-3.11.15+20260807-x86_64-unknown-linux-gnu-pgo+lto-full.tar.zst" }, "x86_64-unknown-linux-musl": { - "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/SHA256SUMS", - "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.11.15+20260610-x86_64-unknown-linux-musl-lto-full.tar.zst" + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/cpython-3.11.15+20260807-x86_64-unknown-linux-musl-lto-full.tar.zst" + } + }, + "3.11.16": { + "aarch64-apple-darwin": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.11.16+20260814-aarch64-apple-darwin-pgo+lto-full.tar.zst" + }, + "aarch64-pc-windows-msvc": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.11.16+20260814-aarch64-pc-windows-msvc-pgo-full.tar.zst" + }, + "aarch64-unknown-linux-gnu": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.11.16+20260814-aarch64-unknown-linux-gnu-pgo+lto-full.tar.zst" + }, + "aarch64-unknown-linux-musl": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.11.16+20260814-aarch64-unknown-linux-musl-lto-full.tar.zst" + }, + "armv7-unknown-linux-gnueabi": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.11.16+20260814-armv7-unknown-linux-gnueabi-lto-full.tar.zst" + }, + "armv7-unknown-linux-gnueabihf": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.11.16+20260814-armv7-unknown-linux-gnueabihf-lto-full.tar.zst" + }, + "i686-pc-windows-msvc": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.11.16+20260814-i686-pc-windows-msvc-pgo-full.tar.zst" + }, + "powerpc64le-unknown-linux-gnu": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.11.16+20260814-ppc64le-unknown-linux-gnu-lto-full.tar.zst" + }, + "riscv64gc-unknown-linux-gnu": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.11.16+20260814-riscv64-unknown-linux-gnu-lto-full.tar.zst" + }, + "s390x-unknown-linux-gnu": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.11.16+20260814-s390x-unknown-linux-gnu-lto-full.tar.zst" + }, + "x86_64-apple-darwin": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.11.16+20260814-x86_64-apple-darwin-pgo+lto-full.tar.zst" + }, + "x86_64-pc-windows-msvc": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.11.16+20260814-x86_64-pc-windows-msvc-pgo-full.tar.zst" + }, + "x86_64-unknown-linux-gnu": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.11.16+20260814-x86_64-unknown-linux-gnu-pgo+lto-full.tar.zst" + }, + "x86_64-unknown-linux-musl": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.11.16+20260814-x86_64-unknown-linux-musl-lto-full.tar.zst" } }, "3.11.3": { @@ -1653,60 +1765,118 @@ }, "3.12.13": { "aarch64-apple-darwin": { - "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/SHA256SUMS", - "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.12.13+20260610-aarch64-apple-darwin-pgo+lto-full.tar.zst" + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/cpython-3.12.13+20260807-aarch64-apple-darwin-pgo+lto-full.tar.zst" }, "aarch64-pc-windows-msvc": { - "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/SHA256SUMS", - "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.12.13+20260610-aarch64-pc-windows-msvc-pgo-full.tar.zst" + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/cpython-3.12.13+20260807-aarch64-pc-windows-msvc-pgo-full.tar.zst" }, "aarch64-unknown-linux-gnu": { - "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/SHA256SUMS", - "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.12.13+20260610-aarch64-unknown-linux-gnu-pgo+lto-full.tar.zst" + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/cpython-3.12.13+20260807-aarch64-unknown-linux-gnu-pgo+lto-full.tar.zst" }, "aarch64-unknown-linux-musl": { - "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/SHA256SUMS", - "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.12.13+20260610-aarch64-unknown-linux-musl-lto-full.tar.zst" + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/cpython-3.12.13+20260807-aarch64-unknown-linux-musl-lto-full.tar.zst" }, "armv7-unknown-linux-gnueabi": { - "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/SHA256SUMS", - "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.12.13+20260610-armv7-unknown-linux-gnueabi-lto-full.tar.zst" + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/cpython-3.12.13+20260807-armv7-unknown-linux-gnueabi-lto-full.tar.zst" }, "armv7-unknown-linux-gnueabihf": { - "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/SHA256SUMS", - "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.12.13+20260610-armv7-unknown-linux-gnueabihf-lto-full.tar.zst" + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/cpython-3.12.13+20260807-armv7-unknown-linux-gnueabihf-lto-full.tar.zst" }, "i686-pc-windows-msvc": { - "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/SHA256SUMS", - "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.12.13+20260610-i686-pc-windows-msvc-pgo-full.tar.zst" + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/cpython-3.12.13+20260807-i686-pc-windows-msvc-pgo-full.tar.zst" }, "powerpc64le-unknown-linux-gnu": { - "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/SHA256SUMS", - "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.12.13+20260610-ppc64le-unknown-linux-gnu-lto-full.tar.zst" + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/cpython-3.12.13+20260807-ppc64le-unknown-linux-gnu-lto-full.tar.zst" }, "riscv64gc-unknown-linux-gnu": { - "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/SHA256SUMS", - "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.12.13+20260610-riscv64-unknown-linux-gnu-lto-full.tar.zst" + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/cpython-3.12.13+20260807-riscv64-unknown-linux-gnu-lto-full.tar.zst" }, "s390x-unknown-linux-gnu": { - "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/SHA256SUMS", - "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.12.13+20260610-s390x-unknown-linux-gnu-lto-full.tar.zst" + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/cpython-3.12.13+20260807-s390x-unknown-linux-gnu-lto-full.tar.zst" }, "x86_64-apple-darwin": { - "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/SHA256SUMS", - "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.12.13+20260610-x86_64-apple-darwin-pgo+lto-full.tar.zst" + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/cpython-3.12.13+20260807-x86_64-apple-darwin-pgo+lto-full.tar.zst" }, "x86_64-pc-windows-msvc": { - "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/SHA256SUMS", - "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.12.13+20260610-x86_64-pc-windows-msvc-pgo-full.tar.zst" + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/cpython-3.12.13+20260807-x86_64-pc-windows-msvc-pgo-full.tar.zst" }, "x86_64-unknown-linux-gnu": { - "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/SHA256SUMS", - "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.12.13+20260610-x86_64-unknown-linux-gnu-pgo+lto-full.tar.zst" + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/cpython-3.12.13+20260807-x86_64-unknown-linux-gnu-pgo+lto-full.tar.zst" }, "x86_64-unknown-linux-musl": { - "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/SHA256SUMS", - "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.12.13+20260610-x86_64-unknown-linux-musl-lto-full.tar.zst" + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/cpython-3.12.13+20260807-x86_64-unknown-linux-musl-lto-full.tar.zst" + } + }, + "3.12.14": { + "aarch64-apple-darwin": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.12.14+20260814-aarch64-apple-darwin-pgo+lto-full.tar.zst" + }, + "aarch64-pc-windows-msvc": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.12.14+20260814-aarch64-pc-windows-msvc-pgo-full.tar.zst" + }, + "aarch64-unknown-linux-gnu": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.12.14+20260814-aarch64-unknown-linux-gnu-pgo+lto-full.tar.zst" + }, + "aarch64-unknown-linux-musl": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.12.14+20260814-aarch64-unknown-linux-musl-lto-full.tar.zst" + }, + "armv7-unknown-linux-gnueabi": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.12.14+20260814-armv7-unknown-linux-gnueabi-lto-full.tar.zst" + }, + "armv7-unknown-linux-gnueabihf": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.12.14+20260814-armv7-unknown-linux-gnueabihf-lto-full.tar.zst" + }, + "i686-pc-windows-msvc": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.12.14+20260814-i686-pc-windows-msvc-pgo-full.tar.zst" + }, + "powerpc64le-unknown-linux-gnu": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.12.14+20260814-ppc64le-unknown-linux-gnu-lto-full.tar.zst" + }, + "riscv64gc-unknown-linux-gnu": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.12.14+20260814-riscv64-unknown-linux-gnu-lto-full.tar.zst" + }, + "s390x-unknown-linux-gnu": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.12.14+20260814-s390x-unknown-linux-gnu-lto-full.tar.zst" + }, + "x86_64-apple-darwin": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.12.14+20260814-x86_64-apple-darwin-pgo+lto-full.tar.zst" + }, + "x86_64-pc-windows-msvc": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.12.14+20260814-x86_64-pc-windows-msvc-pgo-full.tar.zst" + }, + "x86_64-unknown-linux-gnu": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.12.14+20260814-x86_64-unknown-linux-gnu-pgo+lto-full.tar.zst" + }, + "x86_64-unknown-linux-musl": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.12.14+20260814-x86_64-unknown-linux-musl-lto-full.tar.zst" } }, "3.12.2": { @@ -2499,60 +2669,118 @@ }, "3.13.14": { "aarch64-apple-darwin": { - "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/SHA256SUMS", - "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.13.14+20260610-aarch64-apple-darwin-pgo+lto-full.tar.zst" + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260805/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260805/cpython-3.13.14+20260805-aarch64-apple-darwin-pgo+lto-full.tar.zst" }, "aarch64-pc-windows-msvc": { - "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/SHA256SUMS", - "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.13.14+20260610-aarch64-pc-windows-msvc-pgo-full.tar.zst" + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260805/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260805/cpython-3.13.14+20260805-aarch64-pc-windows-msvc-pgo-full.tar.zst" }, "aarch64-unknown-linux-gnu": { - "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/SHA256SUMS", - "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.13.14+20260610-aarch64-unknown-linux-gnu-pgo+lto-full.tar.zst" + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260805/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260805/cpython-3.13.14+20260805-aarch64-unknown-linux-gnu-pgo+lto-full.tar.zst" }, "aarch64-unknown-linux-musl": { - "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/SHA256SUMS", - "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.13.14+20260610-aarch64-unknown-linux-musl-lto-full.tar.zst" + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260805/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260805/cpython-3.13.14+20260805-aarch64-unknown-linux-musl-lto-full.tar.zst" }, "armv7-unknown-linux-gnueabi": { - "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/SHA256SUMS", - "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.13.14+20260610-armv7-unknown-linux-gnueabi-lto-full.tar.zst" + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260805/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260805/cpython-3.13.14+20260805-armv7-unknown-linux-gnueabi-lto-full.tar.zst" }, "armv7-unknown-linux-gnueabihf": { - "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/SHA256SUMS", - "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.13.14+20260610-armv7-unknown-linux-gnueabihf-lto-full.tar.zst" + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260805/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260805/cpython-3.13.14+20260805-armv7-unknown-linux-gnueabihf-lto-full.tar.zst" }, "i686-pc-windows-msvc": { - "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/SHA256SUMS", - "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.13.14+20260610-i686-pc-windows-msvc-pgo-full.tar.zst" + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260805/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260805/cpython-3.13.14+20260805-i686-pc-windows-msvc-pgo-full.tar.zst" }, "powerpc64le-unknown-linux-gnu": { - "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/SHA256SUMS", - "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.13.14+20260610-ppc64le-unknown-linux-gnu-lto-full.tar.zst" + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260805/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260805/cpython-3.13.14+20260805-ppc64le-unknown-linux-gnu-lto-full.tar.zst" }, "riscv64gc-unknown-linux-gnu": { - "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/SHA256SUMS", - "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.13.14+20260610-riscv64-unknown-linux-gnu-lto-full.tar.zst" + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260805/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260805/cpython-3.13.14+20260805-riscv64-unknown-linux-gnu-lto-full.tar.zst" }, "s390x-unknown-linux-gnu": { - "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/SHA256SUMS", - "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.13.14+20260610-s390x-unknown-linux-gnu-lto-full.tar.zst" + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260805/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260805/cpython-3.13.14+20260805-s390x-unknown-linux-gnu-lto-full.tar.zst" }, "x86_64-apple-darwin": { - "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/SHA256SUMS", - "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.13.14+20260610-x86_64-apple-darwin-pgo+lto-full.tar.zst" + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260805/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260805/cpython-3.13.14+20260805-x86_64-apple-darwin-pgo+lto-full.tar.zst" }, "x86_64-pc-windows-msvc": { - "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/SHA256SUMS", - "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.13.14+20260610-x86_64-pc-windows-msvc-pgo-full.tar.zst" + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260805/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260805/cpython-3.13.14+20260805-x86_64-pc-windows-msvc-pgo-full.tar.zst" }, "x86_64-unknown-linux-gnu": { - "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/SHA256SUMS", - "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.13.14+20260610-x86_64-unknown-linux-gnu-pgo+lto-full.tar.zst" + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260805/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260805/cpython-3.13.14+20260805-x86_64-unknown-linux-gnu-pgo+lto-full.tar.zst" }, "x86_64-unknown-linux-musl": { - "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/SHA256SUMS", - "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.13.14+20260610-x86_64-unknown-linux-musl-lto-full.tar.zst" + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260805/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260805/cpython-3.13.14+20260805-x86_64-unknown-linux-musl-lto-full.tar.zst" + } + }, + "3.13.15": { + "aarch64-apple-darwin": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.13.15+20260814-aarch64-apple-darwin-pgo+lto-full.tar.zst" + }, + "aarch64-pc-windows-msvc": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.13.15+20260814-aarch64-pc-windows-msvc-pgo-full.tar.zst" + }, + "aarch64-unknown-linux-gnu": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.13.15+20260814-aarch64-unknown-linux-gnu-pgo+lto-full.tar.zst" + }, + "aarch64-unknown-linux-musl": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.13.15+20260814-aarch64-unknown-linux-musl-lto-full.tar.zst" + }, + "armv7-unknown-linux-gnueabi": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.13.15+20260814-armv7-unknown-linux-gnueabi-lto-full.tar.zst" + }, + "armv7-unknown-linux-gnueabihf": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.13.15+20260814-armv7-unknown-linux-gnueabihf-lto-full.tar.zst" + }, + "i686-pc-windows-msvc": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.13.15+20260814-i686-pc-windows-msvc-pgo-full.tar.zst" + }, + "powerpc64le-unknown-linux-gnu": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.13.15+20260814-ppc64le-unknown-linux-gnu-lto-full.tar.zst" + }, + "riscv64gc-unknown-linux-gnu": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.13.15+20260814-riscv64-unknown-linux-gnu-lto-full.tar.zst" + }, + "s390x-unknown-linux-gnu": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.13.15+20260814-s390x-unknown-linux-gnu-lto-full.tar.zst" + }, + "x86_64-apple-darwin": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.13.15+20260814-x86_64-apple-darwin-pgo+lto-full.tar.zst" + }, + "x86_64-pc-windows-msvc": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.13.15+20260814-x86_64-pc-windows-msvc-pgo-full.tar.zst" + }, + "x86_64-unknown-linux-gnu": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.13.15+20260814-x86_64-unknown-linux-gnu-pgo+lto-full.tar.zst" + }, + "x86_64-unknown-linux-musl": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.13.15+20260814-x86_64-unknown-linux-musl-lto-full.tar.zst" } }, "3.13.2": { @@ -3995,60 +4223,118 @@ }, "3.14.6": { "aarch64-apple-darwin": { - "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/SHA256SUMS", - "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.14.6+20260610-aarch64-apple-darwin-pgo+lto-full.tar.zst" + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260804/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260804/cpython-3.14.6+20260804-aarch64-apple-darwin-pgo+lto-full.tar.zst" }, "aarch64-pc-windows-msvc": { - "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/SHA256SUMS", - "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.14.6+20260610-aarch64-pc-windows-msvc-pgo-full.tar.zst" + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260804/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260804/cpython-3.14.6+20260804-aarch64-pc-windows-msvc-pgo-full.tar.zst" }, "aarch64-unknown-linux-gnu": { - "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/SHA256SUMS", - "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.14.6+20260610-aarch64-unknown-linux-gnu-pgo+lto-full.tar.zst" + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260804/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260804/cpython-3.14.6+20260804-aarch64-unknown-linux-gnu-pgo+lto-full.tar.zst" }, "aarch64-unknown-linux-musl": { - "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/SHA256SUMS", - "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.14.6+20260610-aarch64-unknown-linux-musl-lto-full.tar.zst" + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260804/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260804/cpython-3.14.6+20260804-aarch64-unknown-linux-musl-lto-full.tar.zst" }, "armv7-unknown-linux-gnueabi": { - "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/SHA256SUMS", - "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.14.6+20260610-armv7-unknown-linux-gnueabi-lto-full.tar.zst" + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260804/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260804/cpython-3.14.6+20260804-armv7-unknown-linux-gnueabi-lto-full.tar.zst" }, "armv7-unknown-linux-gnueabihf": { - "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/SHA256SUMS", - "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.14.6+20260610-armv7-unknown-linux-gnueabihf-lto-full.tar.zst" + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260804/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260804/cpython-3.14.6+20260804-armv7-unknown-linux-gnueabihf-lto-full.tar.zst" }, "i686-pc-windows-msvc": { - "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/SHA256SUMS", - "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.14.6+20260610-i686-pc-windows-msvc-pgo-full.tar.zst" + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260804/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260804/cpython-3.14.6+20260804-i686-pc-windows-msvc-pgo-full.tar.zst" }, "powerpc64le-unknown-linux-gnu": { - "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/SHA256SUMS", - "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.14.6+20260610-ppc64le-unknown-linux-gnu-lto-full.tar.zst" + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260804/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260804/cpython-3.14.6+20260804-ppc64le-unknown-linux-gnu-lto-full.tar.zst" }, "riscv64gc-unknown-linux-gnu": { - "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/SHA256SUMS", - "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.14.6+20260610-riscv64-unknown-linux-gnu-lto-full.tar.zst" + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260804/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260804/cpython-3.14.6+20260804-riscv64-unknown-linux-gnu-lto-full.tar.zst" }, "s390x-unknown-linux-gnu": { - "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/SHA256SUMS", - "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.14.6+20260610-s390x-unknown-linux-gnu-lto-full.tar.zst" + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260804/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260804/cpython-3.14.6+20260804-s390x-unknown-linux-gnu-lto-full.tar.zst" }, "x86_64-apple-darwin": { - "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/SHA256SUMS", - "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.14.6+20260610-x86_64-apple-darwin-pgo+lto-full.tar.zst" + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260804/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260804/cpython-3.14.6+20260804-x86_64-apple-darwin-pgo+lto-full.tar.zst" }, "x86_64-pc-windows-msvc": { - "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/SHA256SUMS", - "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.14.6+20260610-x86_64-pc-windows-msvc-pgo-full.tar.zst" + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260804/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260804/cpython-3.14.6+20260804-x86_64-pc-windows-msvc-pgo-full.tar.zst" }, "x86_64-unknown-linux-gnu": { - "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/SHA256SUMS", - "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.14.6+20260610-x86_64-unknown-linux-gnu-pgo+lto-full.tar.zst" + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260804/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260804/cpython-3.14.6+20260804-x86_64-unknown-linux-gnu-pgo+lto-full.tar.zst" }, "x86_64-unknown-linux-musl": { - "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/SHA256SUMS", - "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.14.6+20260610-x86_64-unknown-linux-musl-lto-full.tar.zst" + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260804/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260804/cpython-3.14.6+20260804-x86_64-unknown-linux-musl-lto-full.tar.zst" + } + }, + "3.14.7": { + "aarch64-apple-darwin": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.14.7+20260814-aarch64-apple-darwin-pgo+lto-full.tar.zst" + }, + "aarch64-pc-windows-msvc": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.14.7+20260814-aarch64-pc-windows-msvc-pgo-full.tar.zst" + }, + "aarch64-unknown-linux-gnu": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.14.7+20260814-aarch64-unknown-linux-gnu-pgo+lto-full.tar.zst" + }, + "aarch64-unknown-linux-musl": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.14.7+20260814-aarch64-unknown-linux-musl-lto-full.tar.zst" + }, + "armv7-unknown-linux-gnueabi": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.14.7+20260814-armv7-unknown-linux-gnueabi-lto-full.tar.zst" + }, + "armv7-unknown-linux-gnueabihf": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.14.7+20260814-armv7-unknown-linux-gnueabihf-lto-full.tar.zst" + }, + "i686-pc-windows-msvc": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.14.7+20260814-i686-pc-windows-msvc-pgo-full.tar.zst" + }, + "powerpc64le-unknown-linux-gnu": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.14.7+20260814-ppc64le-unknown-linux-gnu-lto-full.tar.zst" + }, + "riscv64gc-unknown-linux-gnu": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.14.7+20260814-riscv64-unknown-linux-gnu-lto-full.tar.zst" + }, + "s390x-unknown-linux-gnu": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.14.7+20260814-s390x-unknown-linux-gnu-lto-full.tar.zst" + }, + "x86_64-apple-darwin": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.14.7+20260814-x86_64-apple-darwin-pgo+lto-full.tar.zst" + }, + "x86_64-pc-windows-msvc": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.14.7+20260814-x86_64-pc-windows-msvc-pgo-full.tar.zst" + }, + "x86_64-unknown-linux-gnu": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.14.7+20260814-x86_64-unknown-linux-gnu-pgo+lto-full.tar.zst" + }, + "x86_64-unknown-linux-musl": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.14.7+20260814-x86_64-unknown-linux-musl-lto-full.tar.zst" } }, "3.15.0-a.1": { @@ -4631,6 +4917,180 @@ "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260610/cpython-3.15.0b2+20260610-x86_64-unknown-linux-musl-lto-full.tar.zst" } }, + "3.15.0-b.3": { + "aarch64-apple-darwin": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260623/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260623/cpython-3.15.0b3+20260623-aarch64-apple-darwin-pgo+lto-full.tar.zst" + }, + "aarch64-pc-windows-msvc": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260623/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260623/cpython-3.15.0b3+20260623-aarch64-pc-windows-msvc-pgo-full.tar.zst" + }, + "aarch64-unknown-linux-gnu": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260623/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260623/cpython-3.15.0b3+20260623-aarch64-unknown-linux-gnu-pgo+lto-full.tar.zst" + }, + "aarch64-unknown-linux-musl": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260623/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260623/cpython-3.15.0b3+20260623-aarch64-unknown-linux-musl-lto-full.tar.zst" + }, + "armv7-unknown-linux-gnueabi": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260623/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260623/cpython-3.15.0b3+20260623-armv7-unknown-linux-gnueabi-lto-full.tar.zst" + }, + "armv7-unknown-linux-gnueabihf": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260623/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260623/cpython-3.15.0b3+20260623-armv7-unknown-linux-gnueabihf-lto-full.tar.zst" + }, + "i686-pc-windows-msvc": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260623/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260623/cpython-3.15.0b3+20260623-i686-pc-windows-msvc-pgo-full.tar.zst" + }, + "powerpc64le-unknown-linux-gnu": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260623/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260623/cpython-3.15.0b3+20260623-ppc64le-unknown-linux-gnu-lto-full.tar.zst" + }, + "riscv64gc-unknown-linux-gnu": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260623/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260623/cpython-3.15.0b3+20260623-riscv64-unknown-linux-gnu-lto-full.tar.zst" + }, + "s390x-unknown-linux-gnu": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260623/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260623/cpython-3.15.0b3+20260623-s390x-unknown-linux-gnu-lto-full.tar.zst" + }, + "x86_64-apple-darwin": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260623/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260623/cpython-3.15.0b3+20260623-x86_64-apple-darwin-pgo+lto-full.tar.zst" + }, + "x86_64-pc-windows-msvc": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260623/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260623/cpython-3.15.0b3+20260623-x86_64-pc-windows-msvc-pgo-full.tar.zst" + }, + "x86_64-unknown-linux-gnu": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260623/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260623/cpython-3.15.0b3+20260623-x86_64-unknown-linux-gnu-pgo+lto-full.tar.zst" + }, + "x86_64-unknown-linux-musl": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260623/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260623/cpython-3.15.0b3+20260623-x86_64-unknown-linux-musl-lto-full.tar.zst" + } + }, + "3.15.0-b.4": { + "aarch64-apple-darwin": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260728/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260728/cpython-3.15.0b4+20260728-aarch64-apple-darwin-pgo+lto-full.tar.zst" + }, + "aarch64-pc-windows-msvc": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260728/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260728/cpython-3.15.0b4+20260728-aarch64-pc-windows-msvc-pgo-full.tar.zst" + }, + "aarch64-unknown-linux-gnu": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260728/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260728/cpython-3.15.0b4+20260728-aarch64-unknown-linux-gnu-pgo+lto-full.tar.zst" + }, + "aarch64-unknown-linux-musl": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260728/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260728/cpython-3.15.0b4+20260728-aarch64-unknown-linux-musl-lto-full.tar.zst" + }, + "armv7-unknown-linux-gnueabi": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260728/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260728/cpython-3.15.0b4+20260728-armv7-unknown-linux-gnueabi-lto-full.tar.zst" + }, + "armv7-unknown-linux-gnueabihf": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260728/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260728/cpython-3.15.0b4+20260728-armv7-unknown-linux-gnueabihf-lto-full.tar.zst" + }, + "i686-pc-windows-msvc": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260728/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260728/cpython-3.15.0b4+20260728-i686-pc-windows-msvc-pgo-full.tar.zst" + }, + "powerpc64le-unknown-linux-gnu": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260728/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260728/cpython-3.15.0b4+20260728-ppc64le-unknown-linux-gnu-lto-full.tar.zst" + }, + "riscv64gc-unknown-linux-gnu": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260728/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260728/cpython-3.15.0b4+20260728-riscv64-unknown-linux-gnu-lto-full.tar.zst" + }, + "s390x-unknown-linux-gnu": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260728/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260728/cpython-3.15.0b4+20260728-s390x-unknown-linux-gnu-lto-full.tar.zst" + }, + "x86_64-apple-darwin": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260728/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260728/cpython-3.15.0b4+20260728-x86_64-apple-darwin-pgo+lto-full.tar.zst" + }, + "x86_64-pc-windows-msvc": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260728/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260728/cpython-3.15.0b4+20260728-x86_64-pc-windows-msvc-pgo-full.tar.zst" + }, + "x86_64-unknown-linux-gnu": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260728/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260728/cpython-3.15.0b4+20260728-x86_64-unknown-linux-gnu-pgo+lto-full.tar.zst" + }, + "x86_64-unknown-linux-musl": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260728/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260728/cpython-3.15.0b4+20260728-x86_64-unknown-linux-musl-lto-full.tar.zst" + } + }, + "3.15.0-rc.1": { + "aarch64-apple-darwin": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.15.0rc1+20260814-aarch64-apple-darwin-pgo+lto-full.tar.zst" + }, + "aarch64-pc-windows-msvc": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.15.0rc1+20260814-aarch64-pc-windows-msvc-pgo-full.tar.zst" + }, + "aarch64-unknown-linux-gnu": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.15.0rc1+20260814-aarch64-unknown-linux-gnu-pgo+lto-full.tar.zst" + }, + "aarch64-unknown-linux-musl": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.15.0rc1+20260814-aarch64-unknown-linux-musl-lto-full.tar.zst" + }, + "armv7-unknown-linux-gnueabi": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.15.0rc1+20260814-armv7-unknown-linux-gnueabi-lto-full.tar.zst" + }, + "armv7-unknown-linux-gnueabihf": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.15.0rc1+20260814-armv7-unknown-linux-gnueabihf-lto-full.tar.zst" + }, + "i686-pc-windows-msvc": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.15.0rc1+20260814-i686-pc-windows-msvc-pgo-full.tar.zst" + }, + "powerpc64le-unknown-linux-gnu": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.15.0rc1+20260814-ppc64le-unknown-linux-gnu-lto-full.tar.zst" + }, + "riscv64gc-unknown-linux-gnu": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.15.0rc1+20260814-riscv64-unknown-linux-gnu-lto-full.tar.zst" + }, + "s390x-unknown-linux-gnu": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.15.0rc1+20260814-s390x-unknown-linux-gnu-lto-full.tar.zst" + }, + "x86_64-apple-darwin": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.15.0rc1+20260814-x86_64-apple-darwin-pgo+lto-full.tar.zst" + }, + "x86_64-pc-windows-msvc": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.15.0rc1+20260814-x86_64-pc-windows-msvc-pgo-full.tar.zst" + }, + "x86_64-unknown-linux-gnu": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.15.0rc1+20260814-x86_64-unknown-linux-gnu-pgo+lto-full.tar.zst" + }, + "x86_64-unknown-linux-musl": { + "checksum": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/SHA256SUMS", + "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20260814/cpython-3.15.0rc1+20260814-x86_64-unknown-linux-musl-lto-full.tar.zst" + } + }, "3.7.6": { "x86_64-pc-windows-msvc": { "download": "https://github.com/astral-sh/python-build-standalone/releases/download/20200216/cpython-3.7.6-windows-x86-shared-pgo-20200217T0110.tar.zst" diff --git a/tools/python/src/proto.rs b/tools/python/src/proto.rs index 90c5bf80..b90f5031 100644 --- a/tools/python/src/proto.rs +++ b/tools/python/src/proto.rs @@ -21,9 +21,9 @@ pub fn register_tool(Json(_): Json) -> FnResult) -> FnResult, +) -> FnResult> { + let env = get_host_environment()?; + let version = &input.context.version; + + let Some(asset) = load_prebuilt_asset(env, version)? else { + return Err(plugin_err!( + "No pre-built available for {version} on {}-{}! Try building from source with --build.", + env.os, + env.arch, + )); + }; + + Ok(Json(create_download_output(asset, version))) +} + #[plugin_fn] pub fn locate_executables( Json(_): Json, @@ -155,6 +184,6 @@ pub fn locate_executables( ]), exes_dirs: vec!["bin".into()], globals_lookup_dirs: vec![], - ..LocateExecutablesOutput::default() + ..Default::default() })) } diff --git a/tools/ruby/src/releases.rs b/tools/ruby/src/releases.rs new file mode 100644 index 00000000..eb328eaa --- /dev/null +++ b/tools/ruby/src/releases.rs @@ -0,0 +1,148 @@ +use proto_pdk::*; +use std::collections::BTreeMap; + +pub type PrebuiltReleases = BTreeMap>; + +#[derive(Debug, PartialEq)] +pub struct PrebuiltAsset { + pub filename: String, + pub url: String, +} + +pub fn load_prebuilt_asset( + env: &HostEnvironment, + version: &VersionSpec, +) -> AnyResult> { + let Some(platform) = get_prebuilt_platform(env) else { + return Ok(None); + }; + + let releases: PrebuiltReleases = fetch_json( + "https://raw.githubusercontent.com/moonrepo/plugins/master/tools/ruby/releases.json", + )?; + + Ok(select_prebuilt_asset(&releases, platform, version)) +} + +pub fn select_prebuilt_asset( + releases: &PrebuiltReleases, + platform: &str, + version: &VersionSpec, +) -> Option { + let filename = releases.get(&version.to_string())?.get(platform)?; + + Some(PrebuiltAsset { + filename: filename.to_owned(), + url: format!("https://github.com/jdx/ruby/releases/download/{version}/{filename}"), + }) +} + +pub fn create_download_output( + asset: PrebuiltAsset, + version: &VersionSpec, +) -> DownloadPrebuiltOutput { + DownloadPrebuiltOutput { + archive_prefix: Some(format!("ruby-{version}")), + download_name: Some(asset.filename), + download_url: asset.url, + ..Default::default() + } +} + +pub fn get_prebuilt_platform(env: &HostEnvironment) -> Option<&'static str> { + match (env.os, env.arch) { + (HostOS::Linux, HostArch::X64) => Some("x86_64_linux"), + (HostOS::Linux, HostArch::Arm64) => Some("arm64_linux"), + (HostOS::MacOS, HostArch::Arm64) => Some("macos"), + _ => None, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn maps_jdx_supported_platforms() { + for (os, arch, expected) in [ + (HostOS::Linux, HostArch::X64, Some("x86_64_linux")), + (HostOS::Linux, HostArch::Arm64, Some("arm64_linux")), + (HostOS::MacOS, HostArch::Arm64, Some("macos")), + (HostOS::MacOS, HostArch::X64, None), + (HostOS::Windows, HostArch::X64, None), + ] { + assert_eq!( + get_prebuilt_platform(&HostEnvironment { + os, + arch, + ..Default::default() + }), + expected + ); + } + } + + #[test] + fn selects_matching_release_asset() { + let asset = select_prebuilt_asset( + &BTreeMap::from_iter([( + "3.4.9".into(), + BTreeMap::from_iter([( + "arm64_linux".into(), + "ruby-3.4.9.arm64_linux.tar.gz".into(), + )]), + )]), + "arm64_linux", + &VersionSpec::parse("3.4.9").unwrap(), + ); + + assert_eq!( + asset, + Some(PrebuiltAsset { + filename: "ruby-3.4.9.arm64_linux.tar.gz".into(), + url: "https://github.com/jdx/ruby/releases/download/3.4.9/ruby-3.4.9.arm64_linux.tar.gz".into(), + }) + ); + } + + #[test] + fn skips_release_without_matching_asset() { + let asset = select_prebuilt_asset( + &BTreeMap::from_iter([("3.4.9".into(), BTreeMap::new())]), + "macos", + &VersionSpec::parse("3.4.9").unwrap(), + ); + + assert_eq!(asset, None); + } + + #[test] + fn skips_missing_release() { + let asset = select_prebuilt_asset( + &BTreeMap::new(), + "macos", + &VersionSpec::parse("3.1.0").unwrap(), + ); + + assert_eq!(asset, None); + } + + #[test] + fn creates_download_output() { + assert_eq!( + create_download_output( + PrebuiltAsset { + filename: "ruby-3.4.9.macos.tar.gz".into(), + url: "https://example.com/ruby-3.4.9.macos.tar.gz".into(), + }, + &VersionSpec::parse("3.4.9").unwrap(), + ), + DownloadPrebuiltOutput { + archive_prefix: Some("ruby-3.4.9".into()), + download_name: Some("ruby-3.4.9.macos.tar.gz".into()), + download_url: "https://example.com/ruby-3.4.9.macos.tar.gz".into(), + ..Default::default() + } + ); + } +} diff --git a/tools/rust/CHANGELOG.md b/tools/rust/CHANGELOG.md index c3065077..86e7032b 100644 --- a/tools/rust/CHANGELOG.md +++ b/tools/rust/CHANGELOG.md @@ -1,5 +1,23 @@ # Changelog +## 0.13.12 + +#### 🚀 Updates + +- Updated to use helpers from the `lang_rust_common` crate. + +## 0.13.11 + +#### 🐞 Fixes + +- Fixed Cargo/Rustup home directory detection triggering an error. + +## 0.13.10 + +#### 🚀 Updates + +- Updated to support proto v0.60 release. + ## 0.13.9 #### 🚀 Updates diff --git a/tools/rust/Cargo.toml b/tools/rust/Cargo.toml index a82aca56..6bc1f190 100644 --- a/tools/rust/Cargo.toml +++ b/tools/rust/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rust_tool" -version = "0.13.9" +version = "0.13.12" edition = "2024" description = "Rust WASM plugin for proto." authors = ["Miles Johnson"] @@ -18,6 +18,7 @@ pre-release-replacements = [ crate-type = ["cdylib", "lib"] [dependencies] +lang_rust_common = { path = "../../crates/lang-rust-common" } tool_common = { path = "../../crates/tool-common" } extism-pdk = { workspace = true } proto_pdk = { workspace = true } @@ -32,4 +33,4 @@ tokio = { workspace = true } [features] default = ["wasm"] -wasm = [] +wasm = ["lang_rust_common/wasm"] diff --git a/tools/rust/src/lib.rs b/tools/rust/src/lib.rs index 0f98c15a..ac438333 100644 --- a/tools/rust/src/lib.rs +++ b/tools/rust/src/lib.rs @@ -1,6 +1,4 @@ #[cfg(feature = "wasm")] -mod helpers; -#[cfg(feature = "wasm")] mod proto; mod toolchain_toml; diff --git a/tools/rust/src/proto.rs b/tools/rust/src/proto.rs index e6fabaed..05e75d58 100644 --- a/tools/rust/src/proto.rs +++ b/tools/rust/src/proto.rs @@ -1,6 +1,6 @@ -use crate::helpers::*; use crate::toolchain_toml::ToolchainToml; use extism_pdk::*; +use lang_rust_common::*; use proto_pdk::*; use starbase_utils::fs; use std::collections::HashMap; @@ -29,17 +29,17 @@ pub fn register_tool(Json(_): Json) -> FnResultrustup"); let is_windows = env.os.is_windows(); @@ -134,17 +134,21 @@ pub fn native_install( } exec(ExecCommandInput { - command: script_path.real_path_string().unwrap(), + command: script_path + .to_real_path()? + .expect("Invalid script path!") + .to_string(), args: vec!["--default-toolchain".into(), "none".into(), "-y".into()], set_executable: true, stream: true, - ..ExecCommandInput::default() + ..Default::default() })?; // Update PATH explicitly, since we can't "reload the shell" // on the host side. This is good enough since it's deterministic. add_host_paths([ - get_cargo_home(&env)?.join("bin").to_string(), + get_cargo_home(env)?.join("bin").to_string(), + "$CARGO_HOME/bin".to_string(), "$HOME/.cargo/bin".to_string(), ])?; } @@ -152,7 +156,7 @@ pub fn native_install( let version = &input.context.version; let channel = get_channel_from_version(version); - let triple = format!("{}-{}", channel, get_target_triple(&env, NAME)?); + let triple = format!("{}-{}", channel, get_target_triple(env, NAME)?); debug!("Installing target {} with rustup", triple); @@ -186,7 +190,7 @@ pub fn native_install( // Always mark as installed so that binaries can be located! Ok(Json(NativeInstallOutput { installed: true, - ..NativeInstallOutput::default() + ..Default::default() })) } @@ -196,13 +200,13 @@ pub fn native_uninstall( ) -> FnResult> { let env = get_host_environment()?; let channel = get_channel_from_version(&input.context.version); - let triple = format!("{}-{}", channel, get_target_triple(&env, NAME)?); + let triple = format!("{}-{}", channel, get_target_triple(env, NAME)?); exec_streamed("rustup", ["toolchain", "uninstall", &triple])?; Ok(Json(NativeUninstallOutput { uninstalled: true, - ..NativeUninstallOutput::default() + ..Default::default() })) } @@ -228,15 +232,14 @@ pub fn locate_executables( "$HOME/.cargo/bin".into(), ], globals_prefix: Some("cargo-".into()), - ..LocateExecutablesOutput::default() })) } #[plugin_fn] pub fn sync_manifest(Json(_): Json) -> FnResult> { let env = get_host_environment()?; - let triple = get_target_triple(&env, NAME)?; - let toolchain_dir = get_toolchain_dir(&env)?; + let triple = get_target_triple(env, NAME)?; + let toolchain_dir = get_toolchain_dir(env)?; let mut output = SyncManifestOutput::default(); let mut versions = vec![];