From e12570fba3fd51d7fb4b50022063c149cc934e6e Mon Sep 17 00:00:00 2001 From: Kinflou Date: Sat, 31 Oct 2026 21:46:07 +0800 Subject: [PATCH] generate: run external generators, and the built-ins through the generator protocol Re-pins core 98d64fa, generation 2d922ce (the codegen comline-rust 8bbce6d and comline-typescript be8bc79 pin; identical to e8b8a73's), and those generators. - `generator = ""` on a [[generate.target]]: a .wasm runs under wasmtime 49.0.3 (feature gen-wasm, default on) with no preopens, env, network or args past argv[0], 1 GiB memory; anything else runs natively with a cleared env. - Describe first: protocol, IR format, language and version are checked. - Wall-time limit (`timeout`, default 60 s), 256 MiB stdout and generated-bytes caps, stderr shown prefixed and capped at 64 KiB, and every generated path checked before anything is written. - Built-ins answer an encoded Request through protocol::respond in process. - Tests: a fixture crate with the real Rust generator and a hostile one, built natively and for wasm32-wasip1. --- .github/workflows/ci.yml | 8 + Cargo.lock | 1122 +++++++++++++++++- Cargo.toml | 20 +- README.md | 6 + docs/cli.md | 33 + src/commands/generate.rs | 183 ++- src/external.rs | 660 +++++++++++ src/gen_config.rs | 43 +- src/lang_targets.rs | 2 +- src/main.rs | 1 + tests/cli/external.rs | 288 +++++ tests/cli/main.rs | 1 + tests/fixtures/generators/Cargo.lock | 380 ++++++ tests/fixtures/generators/Cargo.toml | 16 + tests/fixtures/generators/src/bin/hostile.rs | 111 ++ tests/fixtures/generators/src/bin/real.rs | 10 + 16 files changed, 2799 insertions(+), 85 deletions(-) create mode 100644 src/external.rs create mode 100644 tests/cli/external.rs create mode 100644 tests/fixtures/generators/Cargo.lock create mode 100644 tests/fixtures/generators/Cargo.toml create mode 100644 tests/fixtures/generators/src/bin/hostile.rs create mode 100644 tests/fixtures/generators/src/bin/real.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0ea931b..ca68719 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,6 +17,10 @@ jobs: - uses: actions/checkout@v4 - name: Install Rust uses: dtolnay/rust-toolchain@stable + with: + # The external-generator tests build their fixture generators for + # wasm32-wasip1 (tests/cli/external.rs). + targets: wasm32-wasip1 - name: Build run: cargo build --verbose - name: Run tests @@ -35,3 +39,7 @@ jobs: run: cargo fmt --check - name: clippy run: cargo clippy --all-targets -- -D warnings + - name: clippy (without gen-wasm) + run: cargo clippy --all-targets --no-default-features --features gen-rust,gen-typescript -- -D warnings + - name: rustfmt (fixture generators) + run: cargo fmt --check --manifest-path tests/fixtures/generators/Cargo.toml diff --git a/Cargo.lock b/Cargo.lock index 9e5713c..3725738 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,7 +8,16 @@ version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" dependencies = [ - "gimli", + "gimli 0.32.3", +] + +[[package]] +name = "addr2line" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59317f77929f0e679d39364702289274de2f0f0b22cbf50b2b8cff2169a0b27a" +dependencies = [ + "gimli 0.33.0", ] [[package]] @@ -44,6 +53,12 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "ambient-authority" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9d4ee0d472d1cd2e28c97dfa124b3d8d992e10eb0a035f33f5d12e3a177ba3b" + [[package]] name = "anstream" version = "0.6.21" @@ -96,9 +111,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" [[package]] name = "ar_archive_writer" @@ -109,6 +124,12 @@ dependencies = [ "object 0.32.2", ] +[[package]] +name = "arbitrary" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bc62ac97cc33321f50863d514c3bc38a453947a8f9e781137e47c7401020aed" + [[package]] name = "ariadne" version = "0.3.0" @@ -147,6 +168,17 @@ dependencies = [ "wait-timeout", ] +[[package]] +name = "async-trait" +version = "0.1.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + [[package]] name = "autocfg" version = "1.5.0" @@ -159,7 +191,7 @@ version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" dependencies = [ - "addr2line", + "addr2line 0.25.1", "cfg-if", "libc", "miniz_oxide", @@ -200,7 +232,7 @@ dependencies = [ "cc", "cfg-if", "constant_time_eq", - "cpufeatures", + "cpufeatures 0.2.17", ] [[package]] @@ -228,6 +260,15 @@ name = "bumpalo" version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" +dependencies = [ + "allocator-api2", +] + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "c2rust-bitfields" @@ -249,6 +290,24 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "cap-primitives" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b5f74729fd2f44701d1a8eb47e906cdb3ccd9ec0f02baad85a744b791940b18" +dependencies = [ + "ambient-authority", + "fs-set-times", + "io-extras", + "io-lifetimes 3.0.1", + "ipnet", + "maybe-owned", + "rustix", + "rustix-linux-procfs", + "windows-sys 0.60.2", + "winx", +] + [[package]] name = "cc" version = "1.2.52" @@ -265,6 +324,17 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "chacha20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "rand_core", +] + [[package]] name = "chumsky" version = "0.9.3" @@ -361,6 +431,15 @@ dependencies = [ "roff", ] +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.17", +] + [[package]] name = "colorchoice" version = "1.0.4" @@ -391,25 +470,29 @@ dependencies = [ "serde_json", "tempfile", "thiserror 1.0.69", + "tokio", "toml", "tracing", "tracing-subscriber", + "wasmtime", + "wasmtime-wasi", ] [[package]] name = "comline-codegen" version = "0.1.0" -source = "git+https://github.com/ComlineProject/generation?rev=6e96a143393bb7f493dbfd2d8629e465d55d0d26#6e96a143393bb7f493dbfd2d8629e465d55d0d26" +source = "git+https://github.com/ComlineProject/generation?rev=2d922cebb347ea1939d90c713ccf77a7ec82db65#2d922cebb347ea1939d90c713ccf77a7ec82db65" dependencies = [ "comline-core", "eyre", + "serde", "serde_json", ] [[package]] name = "comline-codegen-rust" version = "0.1.0" -source = "git+https://github.com/ComlineProject/comline-rust?rev=eb9aabbc4f3a7fd9ce496592c183a644aa8ae631#eb9aabbc4f3a7fd9ce496592c183a644aa8ae631" +source = "git+https://github.com/ComlineProject/comline-rust?rev=8bbce6dbe7f872550f4300025e5c139ff5ec304c#8bbce6dbe7f872550f4300025e5c139ff5ec304c" dependencies = [ "comline-codegen", "comline-core", @@ -419,7 +502,7 @@ dependencies = [ [[package]] name = "comline-codegen-typescript" version = "0.1.0" -source = "git+https://github.com/ComlineProject/comline-typescript?rev=76b58cdde145cd1e79ccb9187df1ee524b2ee492#76b58cdde145cd1e79ccb9187df1ee524b2ee492" +source = "git+https://github.com/ComlineProject/comline-typescript?rev=be8bc79d99f3196cebfded93f08c26e0de1c72fc#be8bc79d99f3196cebfded93f08c26e0de1c72fc" dependencies = [ "comline-codegen", "comline-core", @@ -429,7 +512,7 @@ dependencies = [ [[package]] name = "comline-core" version = "0.1.0" -source = "git+https://github.com/ComlineProject/core?rev=f6e289aaa137258e9e96c378c32d057c249a5b5d#f6e289aaa137258e9e96c378c32d057c249a5b5d" +source = "git+https://github.com/ComlineProject/core?rev=98d64faedfc48ab4d2c93722e2b099debedd503c#98d64faedfc48ab4d2c93722e2b099debedd503c" dependencies = [ "ariadne", "blake3", @@ -448,6 +531,7 @@ dependencies = [ "semver", "serde", "serde_derive", + "serde_json", "snafu", "toml_edit 0.21.1", "tracing", @@ -456,7 +540,7 @@ dependencies = [ [[package]] name = "comline-core-stdlib" version = "0.1.0" -source = "git+https://github.com/ComlineProject/core?rev=f6e289aaa137258e9e96c378c32d057c249a5b5d#f6e289aaa137258e9e96c378c32d057c249a5b5d" +source = "git+https://github.com/ComlineProject/core?rev=98d64faedfc48ab4d2c93722e2b099debedd503c#98d64faedfc48ab4d2c93722e2b099debedd503c" [[package]] name = "concolor" @@ -497,6 +581,21 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" +[[package]] +name = "core_detect" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48" + +[[package]] +name = "cpp_demangle" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0667304c32ea56cb4cd6d2d7c0cfe9a2f8041229db8c033af7f8d69492429def" +dependencies = [ + "cfg-if", +] + [[package]] name = "cpufeatures" version = "0.2.17" @@ -506,6 +605,170 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + +[[package]] +name = "cranelift-assembler-x64" +version = "0.136.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "249a61efca6a54425e027f5704a6f68a0232b4df527e05a3ef947d89dbdcb078" +dependencies = [ + "cranelift-assembler-x64-meta", +] + +[[package]] +name = "cranelift-assembler-x64-meta" +version = "0.136.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5336a46e05db2d515440b1ce8a9363e7bc04b608c0f66cc98dddc5492a66fbf" +dependencies = [ + "cranelift-srcgen", +] + +[[package]] +name = "cranelift-bforest" +version = "0.136.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca7162cd4d4fc5f9f2f88d1459d939882203d93a8c5a19ef2c405b2e82ef13f" +dependencies = [ + "cranelift-entity", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-bitset" +version = "0.136.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec2ba2d4f3d959ec4023a10266c3dcfb28381d1938d507a9a5e1abb78bbcc88" +dependencies = [ + "serde", + "serde_derive", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-codegen" +version = "0.136.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51a64cb821d5fed56bdd6053acd36fa928cd82b70bf6b19b8b14b8b27d047b9a" +dependencies = [ + "bumpalo", + "cranelift-assembler-x64", + "cranelift-bforest", + "cranelift-bitset", + "cranelift-codegen-meta", + "cranelift-codegen-shared", + "cranelift-control", + "cranelift-entity", + "cranelift-isle", + "gimli 0.33.0", + "hashbrown 0.17.1", + "libm", + "log", + "postcard", + "pulley-interpreter", + "regalloc2", + "rustc-hash", + "serde", + "serde_derive", + "sha2", + "smallvec", + "target-lexicon", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-codegen-meta" +version = "0.136.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142be1e267a7497ba3c2b5a829eaf5387b92b9bcdba6e0a6020f1ffccae7f2b8" +dependencies = [ + "cranelift-assembler-x64-meta", + "cranelift-codegen-shared", + "cranelift-srcgen", + "heck 0.5.0", + "pulley-interpreter", +] + +[[package]] +name = "cranelift-codegen-shared" +version = "0.136.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1260aa93b0bd48e8e55c1c7fd9ff6a260f1a1bc46e1f0151a5055b124e5b15d" + +[[package]] +name = "cranelift-control" +version = "0.136.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50c179632a09850f81f1a1e9116cb09330c734a86689f3ee4c3f4ec81543acbd" +dependencies = [ + "arbitrary", +] + +[[package]] +name = "cranelift-entity" +version = "0.136.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a03e8b44c4877cf2fb21e5788d514af6fd291a066ed9ae37adf114a25fecb4aa" +dependencies = [ + "cranelift-bitset", + "serde", + "serde_derive", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-frontend" +version = "0.136.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "366ff7cb014bd08752210eba55a650969f8db9b52670d69d2ea0b23e1e9e3edf" +dependencies = [ + "cranelift-codegen", + "hashbrown 0.17.1", + "log", + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cranelift-isle" +version = "0.136.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ea6526ebf9064fff8ce3b1e26afad7027f9e6bb0e45d2b9200e4cbab8343345" + +[[package]] +name = "cranelift-native" +version = "0.136.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b44d323f3e5108a8a5bc61f3acd227285ece066e19afe9aece211d66b34a64ea" +dependencies = [ + "cranelift-codegen", + "libc", + "target-lexicon", +] + +[[package]] +name = "cranelift-srcgen" +version = "0.136.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f016b9994e39cad68107f33fa5b8dcee8fb7e38332bde3f97690e5e8abe1b10" + +[[package]] +name = "crc32fast" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ba106e844045ddc02c20768ac4b0b7cb45f2cc2ecbd833ac9f5dacb183c2055" +dependencies = [ + "cfg-if", +] + [[package]] name = "crossbeam-channel" version = "0.5.16" @@ -576,12 +839,44 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" +[[package]] +name = "either" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be" + +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + [[package]] name = "encode_unicode" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" +[[package]] +name = "encoding_rs" +version = "0.8.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89253175e704ab869b04f9db224d38630e0211a53841a79181deae0889e169f5" +dependencies = [ + "cfg-if", + "core_detect", + "multiversion_no_op", + "rustversion", + "scopeguard", + "simdutf8", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -639,6 +934,18 @@ dependencies = [ "num-traits", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -648,6 +955,17 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs-set-times" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94e7099f6313ecacbe1256e8ff9d617b75d1bcb16a6fddef94866d225a01a14a" +dependencies = [ + "io-lifetimes 2.0.4", + "rustix", + "windows-sys 0.59.0", +] + [[package]] name = "fsevent-sys" version = "4.1.0" @@ -657,12 +975,48 @@ dependencies = [ "libc", ] +[[package]] +name = "futures" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", + "futures-sink", +] + [[package]] name = "futures-core" version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + [[package]] name = "futures-task" version = "0.3.34" @@ -675,8 +1029,12 @@ version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" dependencies = [ + "futures-channel", "futures-core", + "futures-io", + "futures-sink", "futures-task", + "memchr", "pin-project-lite", "slab", ] @@ -699,16 +1057,40 @@ checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 5.3.0", "wasip2", ] +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "rand_core", +] + [[package]] name = "gimli" version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" +[[package]] +name = "gimli" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf7f043f89559805f8c7cacc432749b2fa0d0a0a9ee46ce47164ed5ba7f126c" +dependencies = [ + "fnv", + "hashbrown 0.16.1", + "indexmap", + "stable_deref_trait", +] + [[package]] name = "glob" version = "0.3.3" @@ -756,6 +1138,17 @@ version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash", + "serde", + "serde_core", +] + [[package]] name = "heck" version = "0.4.1" @@ -855,6 +1248,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "idna" version = "1.1.0" @@ -884,12 +1283,14 @@ checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" [[package]] name = "indexmap" -version = "2.13.0" +version = "2.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" dependencies = [ "equivalent", - "hashbrown 0.16.1", + "hashbrown 0.17.1", + "serde", + "serde_core", ] [[package]] @@ -934,6 +1335,34 @@ dependencies = [ "libc", ] +[[package]] +name = "io-extras" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20fd6de4ccfcc187e38bc21cfa543cb5a302cb86a8b114eb7f0bf0dc9f8ac00f" +dependencies = [ + "io-lifetimes 3.0.1", + "windows-sys 0.60.2", +] + +[[package]] +name = "io-lifetimes" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06432fb54d3be7964ecd3649233cddf80db2832f47fec34c01f65b3d9d774983" + +[[package]] +name = "io-lifetimes" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0fb0570afe1fed943c5c3d4102d5358592d8625fda6a0007fdbe65a92fba96" + +[[package]] +name = "ipnet" +version = "2.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" + [[package]] name = "is-terminal" version = "0.4.17" @@ -958,11 +1387,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] -name = "itoa" -version = "1.0.17" +name = "itertools" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" - +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + [[package]] name = "js-sys" version = "0.3.104" @@ -1000,17 +1438,35 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "leb128" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c83bff1d572d6b9aeef67ddfc8448e4a3737909cb28e81f97c791b9018703e52" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libc" -version = "0.2.180" +version = "0.2.191" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a" + +[[package]] +name = "libm" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" @@ -1033,6 +1489,12 @@ dependencies = [ "twox-hash", ] +[[package]] +name = "mach2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae608c151f68243f2b000364e1f7b186d9c29845f7d2d85bd31b9ad77ad552b" + [[package]] name = "matchers" version = "0.2.0" @@ -1042,12 +1504,27 @@ dependencies = [ "regex-automata", ] +[[package]] +name = "maybe-owned" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4facc753ae494aeb6e3c22f839b158aebd4f9270f55cd3c79906c45476c47ab4" + [[package]] name = "memchr" version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +[[package]] +name = "memfd" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57804b2c9b69967f1536a56f86297e367a33b19e98852ed624b84551cdbc0d90" +dependencies = [ + "rustix", +] + [[package]] name = "miette" version = "7.6.0" @@ -1099,6 +1576,23 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "mio" +version = "1.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1788edb87fdc09c7e26304471e2f5be8cdefb1b6930d6e3985fc02ff53bf86ee" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "multiversion_no_op" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743fb55ba31b18fb1ecef6bdc9aa2743314978ac084044301a7eee33fb99a20d" + [[package]] name = "normalize-line-endings" version = "0.3.0" @@ -1119,7 +1613,7 @@ dependencies = [ "kqueue", "libc", "log", - "mio", + "mio 0.8.11", "walkdir", "windows-sys 0.48.0", ] @@ -1177,6 +1671,18 @@ dependencies = [ "memchr", ] +[[package]] +name = "object" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd229a0361b9d0d4396176e02d65897f487eebeab7caa6d443855ee152ca0b9c" +dependencies = [ + "crc32fast", + "hashbrown 0.17.1", + "indexmap", + "memchr", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -1256,6 +1762,18 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "serde", +] + [[package]] name = "potential_utf" version = "0.1.4" @@ -1314,6 +1832,29 @@ dependencies = [ "cc", ] +[[package]] +name = "pulley-interpreter" +version = "49.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "def59f28ccaf2cb4f8dfaac83197fd7d56a4470a70e4f2589454c731370101c5" +dependencies = [ + "cranelift-bitset", + "log", + "pulley-macros", + "wasmtime-internal-core", +] + +[[package]] +name = "pulley-macros" +version = "49.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a1834d3b3ef994f266a0c9ed29a8214f2c2faaa9b1c44babfa57566bb05af83" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "quote" version = "1.0.43" @@ -1329,6 +1870,44 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "regalloc2" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "757712e8e61590d6d4f5d563483755538b5aa13467837a3b41cd9832509a7f85" +dependencies = [ + "allocator-api2", + "bumpalo", + "hashbrown 0.17.1", + "log", + "rustc-hash", + "serde", + "smallvec", +] + [[package]] name = "regex" version = "1.12.2" @@ -1427,9 +2006,9 @@ checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" [[package]] name = "rustix" -version = "1.1.3" +version = "1.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +checksum = "e5e76742e5294153d4dde1207dbec5bdd81eef6fabdc0d2118babadabfd858aa" dependencies = [ "bitflags 2.13.1", "errno", @@ -1438,6 +2017,16 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "rustix-linux-procfs" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fc84bf7e9aa16c4f2c758f27412dc9841341e16aa682d9c7ac308fe3ee12056" +dependencies = [ + "once_cell", + "rustix", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -1453,6 +2042,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "semver" version = "1.0.27" @@ -1523,7 +2118,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest", ] @@ -1542,6 +2137,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "slab" version = "0.4.12" @@ -1559,6 +2160,9 @@ name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] [[package]] name = "snafu" @@ -1582,6 +2186,16 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -1656,6 +2270,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn-inline-mod" version = "0.6.0" @@ -1677,19 +2302,34 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "target-lexicon" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" + [[package]] name = "tempfile" -version = "3.24.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom", + "getrandom 0.3.4", "once_cell", "rustix", "windows-sys 0.60.2", ] +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + [[package]] name = "terminal_size" version = "0.4.3" @@ -1775,6 +2415,20 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tokio" +version = "1.54.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b52fd3255258d26fac7264ab5a76b53c13367136dc011d3d6ad4f8ed070396a3" +dependencies = [ + "bytes", + "libc", + "mio 1.2.4", + "pin-project-lite", + "socket2", + "windows-sys 0.61.2", +] + [[package]] name = "toml" version = "0.8.23" @@ -1972,9 +2626,9 @@ checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" [[package]] name = "unicode-ident" -version = "1.0.22" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" [[package]] name = "unicode-linebreak" @@ -2110,6 +2764,301 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.258.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f64765e43c000fae008a48db8b288e175e237fc9ce20a26586836878c1bee89f" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.258.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e10808eb27efde692f1b1706089fe19296728bf872291f5b4e0d50fba88f2f" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.258.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe9269dd817e0e7021bf534ccd9761bc2d64778b6708e41228b8439d75eaa0b1" +dependencies = [ + "bitflags 2.13.1", + "hashbrown 0.17.1", + "indexmap", + "semver", + "serde", +] + +[[package]] +name = "wasmprinter" +version = "0.258.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14a457ba8670a3db3d357c1aeac3ed4fe378a76054a221789f0df0ea507e292b" +dependencies = [ + "anyhow", + "termcolor", + "wasmparser", +] + +[[package]] +name = "wasmtime" +version = "49.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fbdd39f5c66f63ba6b5b31391b324b7ade7237d29c866cdb561330118ef436e" +dependencies = [ + "addr2line 0.26.1", + "async-trait", + "bitflags 2.13.1", + "bumpalo", + "cc", + "encoding_rs", + "futures", + "libc", + "log", + "mach2", + "memfd", + "object 0.40.0", + "once_cell", + "postcard", + "pulley-interpreter", + "rustix", + "semver", + "serde", + "serde_derive", + "smallvec", + "target-lexicon", + "wasmparser", + "wasmtime-environ", + "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-unwinder", + "wasmtime-internal-versioned-export-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmtime-environ" +version = "49.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e915c27826de4f7b4411b61fd6db32ca448657a5136b8c8e8874a15e90cecbf4" +dependencies = [ + "anyhow", + "cpp_demangle", + "cranelift-bforest", + "cranelift-bitset", + "cranelift-entity", + "gimli 0.33.0", + "hashbrown 0.17.1", + "indexmap", + "log", + "object 0.40.0", + "postcard", + "rustc-demangle", + "semver", + "serde", + "serde_derive", + "sha2", + "smallvec", + "target-lexicon", + "wasm-encoder", + "wasmparser", + "wasmprinter", + "wasmtime-internal-component-util", + "wasmtime-internal-core", +] + +[[package]] +name = "wasmtime-internal-component-macro" +version = "49.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d95f531a0b809fd9fd158be6133c97bd2fc628ac47f5812aff3e3b5624eb02c2" +dependencies = [ + "anyhow", + "proc-macro2", + "quote", + "syn 2.0.114", + "wasmtime-internal-component-util", + "wasmtime-internal-wit-bindgen", + "wit-parser", +] + +[[package]] +name = "wasmtime-internal-component-util" +version = "49.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e754a3ff5690c34eb4713bd7c65f60dd607ead4e39042cbf52ac8011de14455b" + +[[package]] +name = "wasmtime-internal-core" +version = "49.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba00c5db77adb0749ee059068e5a417bfac3d8969f5a3479d9bd1ca8c1ba248d" +dependencies = [ + "hashbrown 0.17.1", + "libm", + "serde", +] + +[[package]] +name = "wasmtime-internal-cranelift" +version = "49.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97ceeb4325712a253f463e50a69b5a547fbbecd52e45612360c543eaa25a03ff" +dependencies = [ + "cranelift-codegen", + "cranelift-control", + "cranelift-entity", + "cranelift-frontend", + "cranelift-native", + "gimli 0.33.0", + "itertools", + "log", + "object 0.40.0", + "pulley-interpreter", + "smallvec", + "target-lexicon", + "thiserror 2.0.17", + "wasmparser", + "wasmtime-environ", + "wasmtime-internal-core", + "wasmtime-internal-unwinder", + "wasmtime-internal-versioned-export-macros", +] + +[[package]] +name = "wasmtime-internal-fiber" +version = "49.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eb3f21bc32b5382ea95f83ce9a5f2f62b68f1b25a37d0802a41a65236eae5ae" +dependencies = [ + "cc", + "libc", + "rustix", + "wasmtime-environ", + "wasmtime-internal-versioned-export-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmtime-internal-jit-debug" +version = "49.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e786f9925126e0d2e466c5308e572e8b6226f4047596228a3d63d99528a78949" +dependencies = [ + "cc", + "wasmtime-internal-versioned-export-macros", +] + +[[package]] +name = "wasmtime-internal-jit-icache-coherence" +version = "49.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92cb1dd5da78f5b21d65e91063bdd8c2eb343c1f077a1dcbc29003d4a794abb0" +dependencies = [ + "libc", + "wasmtime-internal-core", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmtime-internal-unwinder" +version = "49.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0243d244f6dd07ccd114e6bb099e8dd1197b47de5c054576adc9483f80f65d8" +dependencies = [ + "cranelift-codegen", + "log", + "object 0.40.0", + "wasmtime-environ", +] + +[[package]] +name = "wasmtime-internal-versioned-export-macros" +version = "49.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20fa5616cd875ec78f5b92020fa47ced7c8bb923079185c8475751098b8c4a69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "wasmtime-internal-wit-bindgen" +version = "49.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cad5e8cef56790d4741b62f7b6c971af53b2ad4c2d1603f8ffe86872763147aa" +dependencies = [ + "anyhow", + "bitflags 2.13.1", + "heck 0.5.0", + "indexmap", + "wit-component", + "wit-parser", +] + +[[package]] +name = "wasmtime-wasi" +version = "49.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "284555904d42017e049fd57c05810b7975075c322481c045f99b07385fb7a5ba" +dependencies = [ + "async-trait", + "bitflags 2.13.1", + "bytes", + "cap-primitives", + "futures", + "rand", + "rustix", + "rustix-linux-procfs", + "thiserror 2.0.17", + "tokio", + "tracing", + "url", + "wasmtime", + "wasmtime-wasi-io", + "wiggle", + "windows-sys 0.61.2", + "winx", +] + +[[package]] +name = "wasmtime-wasi-io" +version = "49.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebaf573aae85636595bdb976b6c76e9277e1f81db067aff6764cb256e0c17dff" +dependencies = [ + "async-trait", + "bytes", + "futures", + "tracing", + "wasmtime", +] + +[[package]] +name = "wast" +version = "35.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ef140f1b49946586078353a453a1d28ba90adfc54dde75710bc1931de204d68" +dependencies = [ + "leb128", +] + [[package]] name = "web-time" version = "1.1.0" @@ -2120,13 +3069,53 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "wiggle" +version = "49.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad3dcdc2980b91d0e7db6975caee4f553c6e37fd8e1464a38b09baceed3fe568" +dependencies = [ + "bitflags 2.13.1", + "thiserror 2.0.17", + "tracing", + "wasmtime", + "wasmtime-environ", + "wiggle-macro", +] + +[[package]] +name = "wiggle-generate" +version = "49.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4765433ad668e225812b956e0f598011e5cdab4d1a57b2d295ab6863e36ff231" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.114", + "wasmtime-environ", + "witx", +] + +[[package]] +name = "wiggle-macro" +version = "49.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d94c6623099d11a3fee39986492e803dd0db6676c9d40ab9553a7edd29f317e5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", + "wiggle-generate", +] + [[package]] name = "winapi-util" version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.60.2", ] [[package]] @@ -2171,6 +3160,15 @@ dependencies = [ "windows-targets 0.53.5", ] +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-targets" version = "0.42.2" @@ -2432,12 +3430,72 @@ dependencies = [ "memchr", ] +[[package]] +name = "winx" +version = "0.36.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f3fd376f71958b862e7afb20cfe5a22830e1963462f3a17f49d82a6c1d1f42d" +dependencies = [ + "bitflags 2.13.1", + "windows-sys 0.59.0", +] + [[package]] name = "wit-bindgen" version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +[[package]] +name = "wit-component" +version = "0.258.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0fca88950af0c1f42bc77e17315e48798b8d68340171b6d204f62bc19a878a6" +dependencies = [ + "anyhow", + "bitflags 2.13.1", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.258.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a2127663dcf3ee16614fb0e3c33d5c6c66fec9dad317a8b0fdd3869487a533" +dependencies = [ + "anyhow", + "hashbrown 0.17.1", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-ident", + "wasmparser", +] + +[[package]] +name = "witx" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e366f27a5cabcddb2706a78296a40b8fcc451e1a6aba2fc1d94b4a01bdaaef4b" +dependencies = [ + "anyhow", + "log", + "thiserror 1.0.69", + "wast", +] + [[package]] name = "writeable" version = "0.6.2" diff --git a/Cargo.toml b/Cargo.toml index 0e925a8..7e6b0ea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,23 +32,33 @@ eyre = "0.6.8" # needs real I/O (git fetch, filesystem), so it's off by default for # `comline-core`'s wasm32 consumers (the playground editor, the language # server); the CLI is the one consumer that actually runs `comline build`. -comline-core = { git = "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/ComlineProject/core", rev = "f6e289aaa137258e9e96c378c32d057c249a5b5d", features = ["deps"] } +comline-core = { git = "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/ComlineProject/core", rev = "98d64faedfc48ab4d2c93722e2b099debedd503c", features = ["deps"] } # All three pinned to revs that agree on one `comline-codegen`, and bumped # together: comline-rust and comline-typescript's codegen crates consume # `comline-codegen`'s types, so the rev below must be byte-identical to the # one *they* pin - Cargo only unifies a git dependency across identical rev # strings, and two copies of `GenRequest` are two incompatible types. -comline-codegen = { git = "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/ComlineProject/generation", rev = "6e96a143393bb7f493dbfd2d8629e465d55d0d26" } -comline-codegen-rust = { git = "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/ComlineProject/comline-rust", rev = "eb9aabbc4f3a7fd9ce496592c183a644aa8ae631", optional = true } -comline-codegen-typescript = { git = "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/ComlineProject/comline-typescript", rev = "76b58cdde145cd1e79ccb9187df1ee524b2ee492", optional = true } +comline-codegen = { git = "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/ComlineProject/generation", rev = "2d922cebb347ea1939d90c713ccf77a7ec82db65" } +comline-codegen-rust = { git = "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/ComlineProject/comline-rust", rev = "8bbce6dbe7f872550f4300025e5c139ff5ec304c", optional = true } +comline-codegen-typescript = { git = "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/ComlineProject/comline-typescript", rev = "be8bc79d99f3196cebfded93f08c26e0de1c72fc", optional = true } +# Runs a `generator = ".wasm"` target (see src/external.rs): no +# preopened directories, no environment, no network. Exact pins, bumped +# together. +wasmtime = { version = "=49.0.3", optional = true, default-features = false, features = ["cranelift", "runtime", "std"] } +wasmtime-wasi = { version = "=49.0.3", optional = true, default-features = false, features = ["p1"] } +# Only for the `AsyncWrite` trait wasmtime-wasi's stdio streams implement. +tokio = { version = "1", default-features = false, optional = true } [features] # Which language generators are compiled in. Drop one to shed its whole # dependency tree from the build. A build with neither can compile but # `comline generate` will find no generator. -default = ["gen-rust", "gen-typescript"] +default = ["gen-rust", "gen-typescript", "gen-wasm"] gen-rust = ["dep:comline-codegen-rust"] gen-typescript = ["dep:comline-codegen-typescript"] +# Running `.wasm` generators under wasmtime. Without it, a `generator` path +# to a `.wasm` is an error naming the feature; native executables still run. +gen-wasm = ["dep:wasmtime", "dep:wasmtime-wasi", "dep:tokio"] [target.'cfg(unix)'.dependencies] libc = "0.2" diff --git a/README.md b/README.md index 566fa15..b4bfa2e 100644 --- a/README.md +++ b/README.md @@ -69,11 +69,17 @@ demand. ## Development ```bash +rustup target add wasm32-wasip1 # the external-generator tests build a .wasm generator cargo test cargo fmt --check cargo clippy --all-targets -- -D warnings ``` +Cargo features: `gen-rust` and `gen-typescript` compile in the built-in +generators, and `gen-wasm` runs external `.wasm` generators under wasmtime +(`generator = ""` on a `[[generate.target]]`, see [docs/cli.md](docs/cli.md)). +All three are on by default. + Man pages and completion scripts for all shells are generated into `OUT_DIR` (`target/.../build/comline-*/out/{man,completions}/`) on every build. diff --git a/docs/cli.md b/docs/cli.md index 315698c..bc708fe 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -264,6 +264,39 @@ flatten = true # crate lands at out/ directly, not out/rust/ — flattening a shared `out` across several `lib` targets would land two crates in the same directory. +**External generators.** A target can use a generator outside this binary: + +```toml +[[generate.target]] +generator = "tools/comline-gen-go.wasm" # or a native executable +language = "go" # optional if the generator offers one language +timeout = 120 # seconds per call, default 60 +``` + +`generator` is a path, relative to `comline.toml`'s directory. The generator +speaks the generator protocol: one CBOR request on stdin, one CBOR response on +stdout. `comline` asks it to describe itself first, and refuses one whose +protocol version or IR format it doesn't share, naming both sides, or one that +doesn't offer the target's language at its version. `language` picks among the +languages it offers and must still be one `config.idp` declares; `lang_version` +applies as for a built-in. + +- A **`.wasm`** runs under wasmtime (cargo feature `gen-wasm`, on by default), + sandboxed: no preopened directories, no environment variables, no network, + no arguments beyond `argv[0]`, and at most 1 GiB of memory. +- **Anything else** runs as a native process with an empty environment. It isn't + sandboxed: run only executables you trust. +- Either is stopped after `timeout` seconds, and refused if it writes more than + 256 MiB to stdout or generates more than 256 MiB of files. Its stderr is + shown prefixed with its file name, up to 64 KiB. +- Every path in its response must be relative and normalized: no `..` or `.` + components, no absolute paths, no drive or UNC prefixes (`:`, `\`), no empty + components, no NUL. A response with any bad path is refused whole, and + nothing from it is written. + +The built-in generators answer the same protocol in process, so their output +goes through the same checks. + **Which versions.** `package_versions = "latest"` (the default) generates the working tree — no `.comline/` read. `"all"` generates every committed version in the CAS chain; `["0.3.0", "0.4.0"]` (versions or commit hashes) generates just diff --git a/src/commands/generate.rs b/src/commands/generate.rs index 1e7bd03..6180d70 100644 --- a/src/commands/generate.rs +++ b/src/commands/generate.rs @@ -11,9 +11,12 @@ use std::collections::BTreeMap; use std::path::{Path, PathBuf}; +use std::time::Duration; +use comline_codegen::protocol::{Description, TargetLanguage}; +use comline_codegen::versions::LangVersion; use comline_codegen::{ - connections, features, GenRequest, Mode, PackageMeta, Registry, Resolved, Unresolved, + connections, features, GenerateRequest, Mode, PackageMeta, Registry, Resolved, Unresolved, }; use comline_core::package::build::{self, cas::ObjectStore}; use comline_core::package::config::ir::frozen::{self, FrozenUnit as ConfigUnit}; @@ -22,6 +25,7 @@ use comline_core::schema::ir::frozen::unit::{schema_namespace_as_path, FrozenUni use miette::{miette, IntoDiagnostic, Result, WrapErr}; use crate::commands::{core_err, ensure_project}; +use crate::external::{self, External}; use crate::gen_config::{self, ComlineToml, DeclaredLang, Overrides, VersionSpec}; use crate::lang_targets::{self, TargetError}; use crate::{history, ui, watch}; @@ -78,7 +82,7 @@ fn generate_once(work_dir: &Path, overrides: &Overrides, dry_run: bool, list: bo }, )); - let cfg = ComlineToml::load(work_dir)?; + let mut cfg = ComlineToml::load(work_dir)?; let spinner = ui::spinner("validating schemas"); let context = build::compile_package(work_dir); @@ -119,6 +123,20 @@ fn generate_once(work_dir: &Path, overrides: &Overrides, dry_run: bool, list: bo }) .collect(); + // External generators, described once each. A target that names one + // and no `language` takes the one language it offers. + let mut externals = Externals::default(); + for t in &mut cfg.generate.targets { + if let (Some(generator), true) = (&t.generator, t.language.is_empty()) { + let timeout = t + .timeout + .map(Duration::from_secs) + .unwrap_or(external::DEFAULT_TIMEOUT); + let (generator, description) = externals.get(&work_dir.join(generator), timeout)?; + t.language = external::only_language(&generator.name, description)?; + } + } + let targets = gen_config::resolve(&cfg, &declared, work_dir, overrides)?; let registry = generator_registry(); @@ -156,33 +174,63 @@ fn generate_once(work_dir: &Path, overrides: &Overrides, dry_run: bool, list: bo )); } - // Reached for every language the congregation declares now, not only - // for one named by a `[[generate.target]]`, so it says what to do - // about it: `only` is how a consumer whose binary can't produce a - // language says so on purpose. - let resolved: Resolved = - match lang_targets::resolve(®istry, &context, &t.language, &t.lang_version) { - Ok(resolved) => resolved, - Err(TargetError::Unresolved(Unresolved::UnknownLanguage { .. })) => { - return Err(miette!( - "no generator for `{}` (version `{}`)\n \ + let (source, ext) = match &t.generator { + Some(path) => { + let (generator, description) = externals.get(path, t.timeout)?; + let info = + external::select(&generator.name, description, &t.language, &t.lang_version)?; + if let Some((dependency, written, minimum)) = + lang_targets::dependency_minimum(&context, &t.language) + { + if LangVersion::parse(&t.lang_version).is_some_and(|v| v < minimum) { + return Err(miette!( + "target `{}`: `{}#{}` is older than dependency `{dependency}` allows: \ + it declares `{}#{written}`", + t.language, + t.language, + t.lang_version, + t.language, + )); + } + } + let ext = info.ext.clone(); + (Source::External(generator), ext) + } + None => { + // Reached for every language the congregation declares now, not only + // for one named by a `[[generate.target]]`, so it says what to do + // about it: `only` is how a consumer whose binary can't produce a + // language says so on purpose. + let resolved: Resolved = match lang_targets::resolve( + ®istry, + &context, + &t.language, + &t.lang_version, + ) { + Ok(resolved) => resolved, + Err(TargetError::Unresolved(Unresolved::UnknownLanguage { .. })) => { + return Err(miette!( + "no generator for `{}` (version `{}`)\n \ help: this build of comline has no {} generator. Install one that \ does, or narrow the set in {}:\n \ [generate]\n only = [...]", - t.language, - t.lang_version, - t.language, - gen_config::FILE_NAME, - )) + t.language, + t.lang_version, + t.language, + gen_config::FILE_NAME, + )) + } + Err(e) => return Err(miette!("target `{}`: {e}", t.language)), + }; + match lang_targets::unverified(&context, &t.language, &resolved) { + Ok(Some(warning)) => ui::warn(warning), + Ok(None) => {} + Err(error) => return Err(miette!("target `{}`: {error}", t.language)), } - Err(e) => return Err(miette!("target `{}`: {e}", t.language)), - }; - match lang_targets::unverified(&context, &t.language, &resolved) { - Ok(Some(warning)) => ui::warn(warning), - Ok(None) => {} - Err(error) => return Err(miette!("target `{}`: {error}", t.language)), - } - let (generator, ext) = (resolved.generator.generate, resolved.generator.ext); + let ext = resolved.generator.ext.to_string(); + (Source::Builtin(resolved), ext) + } + }; let versions = expand_versions(&t.versions, work_dir, &live_schemas, config_units)?; if mode == Mode::Lib && versions.len() > 1 { @@ -212,9 +260,9 @@ fn generate_once(work_dir: &Path, overrides: &Overrides, dry_run: bool, list: bo }); for gv in &versions { - let request = GenRequest { + let request = GenerateRequest { mode, - schemas: &gv.schemas, + schemas: gv.schemas.clone(), package: PackageMeta { name: package_name.clone(), // An unbuilt working tree has no version yet; `0.0.0` keeps @@ -225,22 +273,35 @@ fn generate_once(work_dir: &Path, overrides: &Overrides, dry_run: bool, list: bo gv.package_version.clone() }, }, - lang_version: t.lang_version.clone(), + target: TargetLanguage { + language: t.language.clone(), + version: t.lang_version.clone(), + }, wire_format: gv.wire_format.clone(), external_std: t.external_std, async_api: t.async_api, validate_on_decode: t.validate_on_decode, + // Filled from the encodings the package declares once + // `comline.toml` maps them (core#120, B3). + encodings: Default::default(), }; // Deliberately not `core_err`: a generator's errors are about // schema/package *structure* (e.g. a namespace that's both a // schema and a parent of deeper schemas), not source *text* - // they carry no span to render a snippet against, so there's // nothing for the multi-line branch there to do. - warn_ignored(&t.language, &request, &resolved); + if let Source::Builtin(resolved) = &source { + warn_ignored(&t.language, &request, resolved); + } warn_async_unused(&t.language, &request); warn_no_package_connection(&t.language, &request); - let files = - generator(&request).map_err(|e| miette!("`{}` generator: {e}", t.language))?; + // Both kinds answer the same protocol request, and either + // response is checked whole - every path, the total size - + // before anything below writes a file. + let files = match &source { + Source::Builtin(_) => external::builtin(®istry, &t.language, request)?, + Source::External(generator) => generator.generate(request)?, + }; // Every destination, whether or not it gets written: the tree // is drawn from these once the loop is done. A dry run differs @@ -257,9 +318,13 @@ fn generate_once(work_dir: &Path, overrides: &Overrides, dry_run: bool, list: bo // dependency it doesn't inline). Mode::Code => { for f in &files { - let namespace = f.path.with_extension("").to_string_lossy().into_owned(); + let namespace = f + .path_buf() + .with_extension("") + .to_string_lossy() + .into_owned(); let dest = - t.dest_for(&namespace, ext, &spec_version, &gv.package_version)?; + t.dest_for(&namespace, &ext, &spec_version, &gv.package_version)?; if !dry_run { write_file(&dest, &f.contents)?; } @@ -290,17 +355,13 @@ fn generate_once(work_dir: &Path, overrides: &Overrides, dry_run: bool, list: bo t.out.join(&t.language) }; for f in &files { - let dest = root.join(&f.path); + let dest = root.join(f.path_buf()); if !dry_run { write_file(&dest, &f.contents)?; } if list { let shown = dest.strip_prefix(work_dir).unwrap_or(&dest).display(); - ui::detail(format!( - " {} {} {shown}", - f.path.display(), - ui::arrow() - )); + ui::detail(format!(" {} {} {shown}", f.path, ui::arrow())); } destinations.push(dest); written += 1; @@ -322,7 +383,37 @@ fn generate_once(work_dir: &Path, overrides: &Overrides, dry_run: bool, list: bo Ok(()) } -fn write_file(dest: &Path, contents: &str) -> Result<()> { +/// Which generator a target runs. +enum Source<'e> { + Builtin(Resolved), + External(&'e External), +} + +/// External generators by path and timeout, each opened and described once +/// per run. +#[derive(Default)] +struct Externals { + described: Vec<((PathBuf, Duration), (External, Description))>, +} + +impl Externals { + fn get(&mut self, path: &Path, timeout: Duration) -> Result<(&External, &Description)> { + let key = (path.to_owned(), timeout); + let at = match self.described.iter().position(|(k, _)| *k == key) { + Some(at) => at, + None => { + let generator = External::open(path, timeout)?; + let description = generator.describe()?; + self.described.push((key, (generator, description))); + self.described.len() - 1 + } + }; + let (generator, description) = &self.described[at].1; + Ok((generator, description)) + } +} + +fn write_file(dest: &Path, contents: &[u8]) -> Result<()> { if let Some(parent) = dest.parent() { std::fs::create_dir_all(parent) .into_diagnostic() @@ -412,11 +503,11 @@ fn gen_version_from_commit( /// Every protocol is on one by default, so that's a package whose protocols /// are all lockstep: on the `datagram` framing, or on JSON-RPC with none of /// them client-provided. -fn warn_async_unused(language: &str, request: &GenRequest) { +fn warn_async_unused(language: &str, request: &GenerateRequest) { if !request.async_api { return; } - let uses = features::used(request.schemas, &features::Options::of(request)); + let uses = features::used(&request.schemas, &features::Options::of(request)); if uses.iter().any(|u| u.feature == features::Feature::Async) { return; } @@ -431,8 +522,8 @@ fn warn_async_unused(language: &str, request: &GenRequest) { /// The package's `serve` / `connect` need its protocols on one connection; /// when they disagree on the framing or handshake, the generators leave them /// out, which would otherwise look like they forgot. -fn warn_no_package_connection(language: &str, request: &GenRequest) { - if let Err(reason) = connections::package_connection(request.schemas) { +fn warn_no_package_connection(language: &str, request: &GenerateRequest) { + if let Err(reason) = connections::package_connection(&request.schemas) { ui::warn(format!( "`{language}`: no package `serve` / `connect` - {reason}; each protocol still has \ its own" @@ -444,8 +535,8 @@ fn warn_no_package_connection(language: &str, request: &GenRequest) { /// generates, but without the feature (a TypeScript target with /// `@validators` gets the types, not the checks). An unsupported feature /// needs nothing here: the generator refuses it, naming every use. -fn warn_ignored(language: &str, request: &GenRequest, resolved: &Resolved) { - let uses = features::used(request.schemas, &features::Options::of(request)); +fn warn_ignored(language: &str, request: &GenerateRequest, resolved: &Resolved) { + let uses = features::used(&request.schemas, &features::Options::of(request)); let mut ignored: BTreeMap> = BTreeMap::new(); for u in &uses { if resolved.support(u.feature).state == features::State::Ignored { diff --git a/src/external.rs b/src/external.rs new file mode 100644 index 0000000..2e2bdf0 --- /dev/null +++ b/src/external.rs @@ -0,0 +1,660 @@ +//! Generators outside this binary: `generator = ""` on a +//! `[[generate.target]]`. +//! +//! An external generator speaks the generator protocol +//! (`comline_codegen::protocol`): one CBOR [`Request`] on stdin, one CBOR +//! [`Response`] on stdout. The CLI asks it to describe itself first, refuses +//! a protocol or IR format it doesn't share, and then sends the request to +//! generate. The built-ins answer the same requests in process +//! ([`builtin`]), so the protocol is exercised on every run. +//! +//! A generator is untrusted code. A `.wasm` runs under wasmtime with no +//! preopened directories, no environment, no network and no arguments beyond +//! `argv[0]`, with its memory limited ([`WASM_MEMORY`]). A native executable +//! runs as its own process with a cleared environment - it can do anything +//! the user can, so only a `.wasm` is sandboxed. Both have a wall-time limit +//! ([`DEFAULT_TIMEOUT`], `timeout` on the target) and a stdout cap +//! ([`STDOUT_CAP`]); stderr is shown, prefixed with the generator's name, up +//! to [`STDERR_CAP`]. Every path in a response is checked ([`check_path`]) +//! before anything is written. + +use std::io::{Read, Write}; +use std::path::{Path, PathBuf}; +use std::process::{Command, Stdio}; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Mutex}; +use std::time::{Duration, Instant}; + +use comline_codegen::protocol::{ + self, Description, FileOut, GenerateRequest, LanguageInfo, Request, Response, IR_FORMAT, + PROTOCOL_VERSION, +}; +use comline_codegen::versions::LangVersion; +use comline_codegen::Registry; +use miette::{miette, Result}; + +/// How long one call to a generator may take, unless the target sets +/// `timeout` (in seconds). +pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(60); +/// The most a generator may write to stdout: one encoded response. +pub const STDOUT_CAP: usize = 256 * 1024 * 1024; +/// The most generated file contents one response may carry, all files +/// together. +pub const GENERATED_CAP: usize = 256 * 1024 * 1024; +/// The most of a generator's stderr that's shown; the rest is dropped. +pub const STDERR_CAP: usize = 64 * 1024; +/// The most linear memory a `.wasm` generator may grow to. +#[cfg(feature = "gen-wasm")] +pub const WASM_MEMORY: usize = 1024 * 1024 * 1024; + +/// A generator at a path, not yet run. +pub struct External { + /// What messages call it: the file name. + pub name: String, + path: PathBuf, + timeout: Duration, + runner: Runner, +} + +enum Runner { + Native, + #[cfg(feature = "gen-wasm")] + Wasm(wasm::Compiled), +} + +impl External { + /// The generator at `path` (already absolute). A `.wasm` is compiled + /// here, once, for every call after. + pub fn open(path: &Path, timeout: Duration) -> Result { + let name = path + .file_name() + .map(|n| n.to_string_lossy().into_owned()) + .unwrap_or_else(|| path.display().to_string()); + if !path.is_file() { + return Err(miette!( + "generator `{}`: no such file (relative paths are relative to comline.toml's \ + directory)", + path.display() + )); + } + let is_wasm = path + .extension() + .is_some_and(|e| e.eq_ignore_ascii_case("wasm")); + let runner = if is_wasm { + #[cfg(feature = "gen-wasm")] + { + Runner::Wasm( + wasm::Compiled::new(path).map_err(|e| miette!("generator `{name}`: {e}"))?, + ) + } + #[cfg(not(feature = "gen-wasm"))] + { + return Err(miette!( + "generator `{name}` is a `.wasm`, and this build of comline was built without \ + the `gen-wasm` feature that runs one" + )); + } + } else { + Runner::Native + }; + Ok(External { + name, + path: path.to_owned(), + timeout, + runner, + }) + } + + /// One request, one response. + fn call(&self, request: &Request) -> Result { + let input = protocol::encode(request).map_err(|e| miette!("{e}"))?; + let output = match &self.runner { + Runner::Native => native::run(&self.path, &self.name, &input, self.timeout), + #[cfg(feature = "gen-wasm")] + Runner::Wasm(compiled) => compiled.run(&self.name, &input, self.timeout), + } + .map_err(|why| miette!("generator `{}`: {why}", self.name))?; + protocol::decode(&output).map_err(|e| { + miette!( + "generator `{}` answered with something that isn't a protocol response: {e}", + self.name + ) + }) + } + + /// What the generator handles, checked against this CLI's protocol + /// version and IR format. + pub fn describe(&self) -> Result { + let description = match self.call(&Request::describe())? { + Response::Describe(d) => d, + Response::Error { message } => { + return Err(miette!( + "generator `{}` refused to describe itself: {message}", + self.name + )) + } + Response::Generate(_) => { + return Err(miette!( + "generator `{}` answered `Describe` with generated files", + self.name + )) + } + }; + let (min, max) = description.protocols; + if !(min..=max).contains(&PROTOCOL_VERSION) { + return Err(miette!( + "generator `{}` speaks generator protocol {}, and this comline speaks protocol \ + {PROTOCOL_VERSION}", + self.name, + if min == max { + min.to_string() + } else { + format!("{min} to {max}") + }, + )); + } + if !description.ir_formats.contains(&IR_FORMAT) { + return Err(miette!( + "generator `{}` reads IR format {}, and this comline writes IR format {IR_FORMAT}", + self.name, + list(description.ir_formats.iter()), + )); + } + Ok(description) + } + + /// Generates one target. The response is checked whole before it's + /// returned: nothing it carries has been written. + pub fn generate(&self, request: GenerateRequest) -> Result> { + let response = self.call(&Request::generate(request))?; + files(&format!("generator `{}`", self.name), response) + } +} + +/// `items` as `a, b, c`, or `none`. +fn list(items: impl Iterator) -> String { + let all: Vec = items.map(|i| i.to_string()).collect(); + if all.is_empty() { + "none".to_string() + } else { + all.join(", ") + } +} + +/// The one language a description offers, when the target names none. +pub fn only_language(name: &str, description: &Description) -> Result { + let mut ids: Vec<&str> = description + .languages + .iter() + .map(|l| l.id.as_str()) + .collect(); + ids.sort(); + ids.dedup(); + match ids.as_slice() { + [one] => Ok(one.to_string()), + [] => Err(miette!("generator `{name}` describes no languages")), + many => Err(miette!( + "generator `{name}` offers {}: set `language` on its `[[generate.target]]` to pick one", + list(many.iter().map(|l| format!("`{l}`"))) + )), + } +} + +/// The entry of `description` for `language#version`: the newest floor at +/// or below the version, as the registry picks among built-ins. +pub fn select<'d>( + name: &str, + description: &'d Description, + language: &str, + version: &str, +) -> Result<&'d LanguageInfo> { + let offered: Vec<&LanguageInfo> = description + .languages + .iter() + .filter(|l| l.id.eq_ignore_ascii_case(language)) + .collect(); + if offered.is_empty() { + return Err(miette!( + "generator `{name}` doesn't generate `{language}`; it offers {}", + list(description.languages.iter().map(|l| format!("`{}`", l.id))) + )); + } + let declared = LangVersion::parse(version).ok_or_else(|| { + miette!("`{language}#{version}`: `{version}` isn't a version - expected dotted numbers, like `1.70.0`") + })?; + offered + .iter() + .filter_map(|l| LangVersion::parse(&l.versions.floor).map(|floor| (floor, *l))) + .filter(|(floor, _)| *floor <= declared) + .max_by(|a, b| a.0.cmp(&b.0)) + .map(|(_, l)| l) + .ok_or_else(|| { + miette!( + "generator `{name}` handles `{language}` from {} on, not `{version}`", + list(offered.iter().map(|l| format!("`{}`", l.versions.floor))) + ) + }) +} + +/// A built-in's answer to `request`, through the generator protocol in +/// process: the request is encoded, answered by +/// [`protocol::respond`] as an external generator's `serve_stdio` would, and +/// checked like any response. +pub fn builtin( + registry: &Registry, + language: &str, + request: GenerateRequest, +) -> Result> { + let bytes = protocol::encode(&Request::generate(request)).map_err(|e| miette!("{e}"))?; + files( + &format!("`{language}` generator"), + protocol::respond(registry, &bytes), + ) +} + +/// The files in `response`, after every check: an error response, a wrong +/// answer, a bad path or too much output is refused whole. +fn files(who: &str, response: Response) -> Result> { + let generated = match response { + Response::Generate(g) => g, + Response::Error { message } => return Err(miette!("{who}: {message}")), + Response::Describe(_) => { + return Err(miette!("{who} answered `Generate` with a description")) + } + }; + for diagnostic in &generated.diagnostics { + crate::ui::warn(format!("{who}: {diagnostic}")); + } + let mut total = 0usize; + for file in &generated.files { + check_path(&file.path).map_err(|why| { + miette!( + "{who} returned the path `{}`, which {why}; nothing was written", + file.path.escape_debug() + ) + })?; + total = total.saturating_add(file.contents.len()); + } + if total > GENERATED_CAP { + return Err(miette!( + "{who} generated {total} bytes, over the {GENERATED_CAP}-byte limit; nothing was written" + )); + } + Ok(generated.files) +} + +/// Why `path` can't be a generated file's path, if it can't: it must be +/// relative, `/`-separated and normalized, so it stays under the target's +/// output directory on every platform. +pub fn check_path(path: &str) -> std::result::Result<(), &'static str> { + if path.is_empty() { + return Err("is empty"); + } + if path.contains('\0') { + return Err("contains a NUL"); + } + if path.contains('\\') { + return Err("contains a `\\` (a generated path is `/`-separated)"); + } + if path.starts_with('/') { + return Err("is absolute"); + } + for component in path.split('/') { + match component { + "" => return Err("has an empty component"), + "." | ".." => return Err("isn't normalized (it has a `.` or `..` component)"), + c if c.contains(':') => return Err("has a `:` (a drive or stream prefix on Windows)"), + _ => {} + } + } + Ok(()) +} + +/// A shared, capped byte buffer: what a generator wrote to one stream. +#[derive(Clone)] +struct Capped { + bytes: Arc>>, + cap: usize, + /// Set once something past `cap` was offered. + over: Arc, +} + +impl Capped { + fn new(cap: usize) -> Self { + Capped { + bytes: Arc::default(), + cap, + over: Arc::default(), + } + } + + /// Keeps what fits; `false` once anything didn't. + fn push(&self, data: &[u8]) -> bool { + let mut bytes = self.bytes.lock().unwrap_or_else(|e| e.into_inner()); + let room = self.cap.saturating_sub(bytes.len()); + bytes.extend_from_slice(&data[..data.len().min(room)]); + if data.len() > room { + self.over.store(true, Ordering::SeqCst); + return false; + } + true + } + + fn over(&self) -> bool { + self.over.load(Ordering::SeqCst) + } + + fn take(&self) -> Vec { + std::mem::take(&mut *self.bytes.lock().unwrap_or_else(|e| e.into_inner())) + } +} + +/// Shows a generator's stderr, each line prefixed with its name. +fn show_stderr(name: &str, stderr: &[u8], truncated: bool) { + let text = String::from_utf8_lossy(stderr); + let mut out = std::io::stderr().lock(); + for line in text.lines() { + let _ = writeln!(out, " [{name}] {line}"); + } + if truncated { + let _ = writeln!( + out, + " [{name}] ... (stderr past {STDERR_CAP} bytes not shown)" + ); + } +} + +fn too_much_output() -> String { + format!("its output passed the {STDOUT_CAP}-byte limit; nothing was written") +} + +fn timed_out(timeout: Duration) -> String { + format!( + "didn't finish within {}s and was stopped (raise `timeout` on the target if it needs \ + longer); nothing was written", + timeout.as_secs_f64() + ) +} + +mod native { + use super::*; + + /// Runs the executable at `path` with `input` on stdin, and returns its + /// stdout once it exits successfully. + /// + /// The environment is cleared. `PATH` isn't kept: a generator gets its + /// request on stdin and has no reason to find other programs. On + /// Windows, `SYSTEMROOT` is kept, without which a process can't load the + /// system libraries every program needs. + pub fn run( + path: &Path, + name: &str, + input: &[u8], + timeout: Duration, + ) -> std::result::Result, String> { + let mut command = Command::new(path); + command + .env_clear() + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + #[cfg(windows)] + if let Some(root) = std::env::var_os("SYSTEMROOT") { + command.env("SYSTEMROOT", root); + } + let mut child = command + .spawn() + .map_err(|e| format!("couldn't start it: {e}"))?; + + let mut stdin = child.stdin.take().expect("piped"); + let input = input.to_vec(); + // A generator that never reads stdin would otherwise block this + // write, and with it the timeout. + let writer = std::thread::spawn(move || { + let _ = stdin.write_all(&input); + }); + + let stdout = Capped::new(STDOUT_CAP); + let reader = drain(child.stdout.take().expect("piped"), stdout.clone()); + let stderr = Capped::new(STDERR_CAP); + let errors = drain(child.stderr.take().expect("piped"), stderr.clone()); + + let deadline = Instant::now() + timeout; + let status = loop { + match child.try_wait() { + Ok(Some(status)) => break Some(status), + Ok(None) if stdout.over() || Instant::now() >= deadline => { + let _ = child.kill(); + let _ = child.wait(); + break None; + } + Ok(None) => std::thread::sleep(Duration::from_millis(5)), + Err(e) => return Err(format!("couldn't wait for it: {e}")), + } + }; + let _ = writer.join(); + let _ = reader.join(); + let _ = errors.join(); + show_stderr(name, &stderr.take(), stderr.over()); + + if stdout.over() { + return Err(too_much_output()); + } + match status { + None => Err(timed_out(timeout)), + Some(status) if !status.success() => { + Err(format!("exited with {status}; nothing was written")) + } + Some(_) => Ok(stdout.take()), + } + } + + /// Reads `from` into `into` until it ends, or until `into` is full. + fn drain(mut from: impl Read + Send + 'static, into: Capped) -> std::thread::JoinHandle<()> { + std::thread::spawn(move || { + let mut chunk = [0u8; 64 * 1024]; + loop { + match from.read(&mut chunk) { + Ok(0) | Err(_) => return, + Ok(n) => { + // Past the cap, stderr keeps draining (and dropping) + // so the generator doesn't block on a full pipe; + // stdout stops, and the generator is killed. + if !into.push(&chunk[..n]) && into.cap == STDOUT_CAP { + return; + } + } + } + } + }) + } +} + +#[cfg(feature = "gen-wasm")] +mod wasm { + use super::*; + use std::pin::Pin; + use std::task::{Context, Poll}; + + use wasmtime::{Config, Engine, Linker, Module, Store, StoreLimits, StoreLimitsBuilder, Trap}; + use wasmtime_wasi::cli::{IsTerminal, StdoutStream}; + use wasmtime_wasi::p1::{self, WasiP1Ctx}; + use wasmtime_wasi::p2::pipe::MemoryInputPipe; + use wasmtime_wasi::{I32Exit, WasiCtxBuilder}; + + /// A compiled module, run once per call. + pub struct Compiled { + engine: Engine, + module: Module, + } + + struct State { + wasi: WasiP1Ctx, + limits: StoreLimits, + } + + impl Compiled { + pub fn new(path: &Path) -> std::result::Result { + let mut config = Config::new(); + // The wall-time limit: the epoch is bumped once the time is up, + // which traps the guest at its next loop header or call. + config.epoch_interruption(true); + let engine = + Engine::new(&config).map_err(|e| format!("couldn't set up wasmtime: {e:#}"))?; + let module = Module::from_file(&engine, path) + .map_err(|e| format!("isn't a WebAssembly module wasmtime can run: {e:#}"))?; + Ok(Compiled { engine, module }) + } + + /// Runs the module's `_start` with `input` on stdin, and returns its + /// stdout once it exits successfully. + pub fn run( + &self, + name: &str, + input: &[u8], + timeout: Duration, + ) -> std::result::Result, String> { + let stdout = Capped::new(STDOUT_CAP); + let stderr = Capped::new(STDERR_CAP); + // Nothing beyond stdio: no preopened directories, no + // environment, no arguments past `argv[0]`, no sockets. + let wasi = WasiCtxBuilder::new() + .stdin(MemoryInputPipe::new(input.to_vec())) + .stdout(Stream { + into: stdout.clone(), + fail_past_cap: true, + }) + .stderr(Stream { + into: stderr.clone(), + fail_past_cap: false, + }) + .arg(name) + .allow_tcp(false) + .allow_udp(false) + .allow_ip_name_lookup(false) + .build_p1(); + let limits = StoreLimitsBuilder::new() + .memory_size(WASM_MEMORY) + .instances(1) + .build(); + let mut store = Store::new(&self.engine, State { wasi, limits }); + store.limiter(|s| &mut s.limits); + store.set_epoch_deadline(1); + + let mut linker: Linker = Linker::new(&self.engine); + p1::add_to_linker_sync(&mut linker, |s| &mut s.wasi).map_err(|e| format!("{e:#}"))?; + let module = self.module.clone(); + + // The guest runs on its own thread, so a host call that blocks + // (a WASI sleep) can't hold the timeout up either. + let (done, finished) = std::sync::mpsc::channel(); + std::thread::spawn(move || { + let result = linker + .instantiate(&mut store, &module) + .and_then(|instance| instance.get_typed_func::<(), ()>(&mut store, "_start")) + .and_then(|start| start.call(&mut store, ())); + let _ = done.send(result); + }); + let result = match finished.recv_timeout(timeout) { + Ok(result) => result, + Err(_) => { + self.engine.increment_epoch(); + show_stderr(name, &stderr.take(), stderr.over()); + return Err(timed_out(timeout)); + } + }; + show_stderr(name, &stderr.take(), stderr.over()); + if stdout.over() { + return Err(too_much_output()); + } + match result { + Ok(()) => Ok(stdout.take()), + Err(e) => match e.downcast_ref::() { + Some(I32Exit(0)) => Ok(stdout.take()), + Some(I32Exit(code)) => { + Err(format!("exited with code {code}; nothing was written")) + } + None if e.downcast_ref::() == Some(&Trap::Interrupt) => { + Err(timed_out(timeout)) + } + None => Err(format!("failed: {e:#}; nothing was written")), + }, + } + } + } + + /// stdout or stderr, into a [`Capped`] buffer. Past the cap, stdout + /// fails the write (the generator can't finish a response that won't be + /// read), and stderr drops it. + struct Stream { + into: Capped, + fail_past_cap: bool, + } + + impl IsTerminal for Stream { + fn is_terminal(&self) -> bool { + false + } + } + + impl StdoutStream for Stream { + fn async_stream(&self) -> Box { + Box::new(Stream { + into: self.into.clone(), + fail_past_cap: self.fail_past_cap, + }) + } + } + + impl tokio::io::AsyncWrite for Stream { + fn poll_write( + self: Pin<&mut Self>, + _: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + if !self.into.push(buf) && self.fail_past_cap { + return Poll::Ready(Err(std::io::Error::other("output limit reached"))); + } + Poll::Ready(Ok(buf.len())) + } + + fn poll_flush(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + + fn poll_shutdown(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + } +} + +#[cfg(test)] +mod tests { + use super::check_path; + + #[test] + fn paths_must_be_relative_and_normalized() { + for good in ["a.rs", "a/b.rs", "src/lib.rs", "Cargo.toml", "a-b_c/d.e.ts"] { + assert_eq!(check_path(good), Ok(()), "{good}"); + } + for bad in [ + "", + "/etc/passwd", + "../x", + "a/../../x", + "a/./b", + "./a", + "a//b", + "a/", + "C:/x", + "C:x", + "\\\\server\\share\\x", + "a\\b", + "a\0b", + "..", + ".", + ] { + assert!(check_path(bad).is_err(), "{bad:?}"); + } + } +} diff --git a/src/gen_config.rs b/src/gen_config.rs index b8db029..655f8c1 100644 --- a/src/gen_config.rs +++ b/src/gen_config.rs @@ -130,8 +130,18 @@ pub struct Generate { #[serde(deny_unknown_fields)] pub struct Target { /// Must match a language the congregation declared under - /// `code_generation.languages`. + /// `code_generation.languages`. With `generator`, it also picks among the + /// languages that generator offers, and may be left out when it offers + /// one; [`crate::commands::generate`] fills it in before [`resolve`]. + #[serde(default)] pub language: String, + /// A generator outside this binary: a path to a `.wasm` (run sandboxed + /// under wasmtime) or to an executable, relative to `comline.toml`'s + /// directory. Unset, the built-in generator for `language` is used. + pub generator: Option, + /// The wall-time limit for each call to `generator`, in seconds. + /// Default 60. + pub timeout: Option, /// Language/toolchain version selecting the generator (e.g. `1.88.0`). /// Falls back to the version the congregation declared for this language, /// and may raise it but never lower it. @@ -213,6 +223,10 @@ pub struct Overrides<'a> { #[derive(Debug)] pub struct ResolvedTarget { pub language: String, + /// See [`Target::generator`], made absolute. + pub generator: Option, + /// See [`Target::timeout`]. + pub timeout: std::time::Duration, pub lang_version: String, pub out: PathBuf, pub layout: String, @@ -305,6 +319,13 @@ pub fn resolve( return Err(moved_default_framing(written)); } + if let Some(t) = cfg.generate.targets.iter().find(|t| t.language.is_empty()) { + return Err(match &t.generator { + Some(g) => miette!("`[[generate.target]]` with `generator = \"{g}\"` needs `language`"), + None => miette!("a `[[generate.target]]` sets no `language`"), + }); + } + let declares = |language: &str| { declared .iter() @@ -361,6 +382,8 @@ pub fn resolve( // Declared but unconfigured: the `[generate]` defaults. vec![Target { language: d.language.clone(), + generator: None, + timeout: None, lang_version: Some(d.lang_version.clone()), out: None, layout: None, @@ -528,8 +551,26 @@ pub fn resolve( .or_else(|| cfg.generate.package_versions.clone()) .unwrap_or_default(); + if t.timeout == Some(0) { + return Err(miette!( + "target `{}`: `timeout = 0` - it's in seconds, and must be at least 1", + t.language + )); + } + if t.timeout.is_some() && t.generator.is_none() { + return Err(miette!( + "target `{}`: `timeout` limits an external `generator`, and this target uses the \ + built-in one", + t.language + )); + } resolved.push(ResolvedTarget { language: t.language.clone(), + generator: t.generator.as_ref().map(|g| work_dir.join(g)), + timeout: t + .timeout + .map(std::time::Duration::from_secs) + .unwrap_or(crate::external::DEFAULT_TIMEOUT), lang_version, out: work_dir.join(out), layout, diff --git a/src/lang_targets.rs b/src/lang_targets.rs index f47e6ff..40682b1 100644 --- a/src/lang_targets.rs +++ b/src/lang_targets.rs @@ -57,7 +57,7 @@ pub fn declared(config: &[ConfigUnit]) -> Vec { /// The highest version any dependency, direct or not, declares for /// `language`: (the dependency's name, its declaration as written, parsed). /// An unparsable one is that dependency's own build's business. -fn dependency_minimum( +pub(crate) fn dependency_minimum( context: &ProjectContext, language: &str, ) -> Option<(String, String, LangVersion)> { diff --git a/src/main.rs b/src/main.rs index 8c440fa..462fb26 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,7 @@ mod changes; mod cli; mod commands; mod error; +mod external; mod gen_config; mod history; mod lang_targets; diff --git a/tests/cli/external.rs b/tests/cli/external.rs new file mode 100644 index 0000000..c7ca8c8 --- /dev/null +++ b/tests/cli/external.rs @@ -0,0 +1,288 @@ +//! `generator = ""`: external generators, native and `.wasm`. +//! +//! The generators come from `tests/fixtures/generators`, built once per run +//! for the host and for `wasm32-wasip1` (which must be installed: +//! `rustup target add wasm32-wasip1`). `real` is the official Rust generator +//! behind the generator protocol; `hostile` misbehaves in the way its file +//! name says. + +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command as StdCommand; +use std::sync::OnceLock; + +use predicates::prelude::*; + +use crate::util::*; + +/// The fixture generators' build directories: (native, wasm32-wasip1). +fn built() -> &'static (PathBuf, PathBuf) { + static BUILT: OnceLock<(PathBuf, PathBuf)> = OnceLock::new(); + BUILT.get_or_init(|| { + let manifest = + Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/generators/Cargo.toml"); + let target_dir = Path::new(env!("CARGO_TARGET_TMPDIR")).join("generators"); + let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".into()); + for target in [None, Some("wasm32-wasip1")] { + let mut build = StdCommand::new(&cargo); + build + .args(["build", "--release", "--manifest-path"]) + .arg(&manifest) + .arg("--target-dir") + .arg(&target_dir); + if let Some(target) = target { + build.args(["--target", target]); + } + let status = build.status().expect("cargo runs"); + assert!( + status.success(), + "building the fixture generators ({target:?}) failed" + ); + } + ( + target_dir.join("release"), + target_dir.join("wasm32-wasip1/release"), + ) + }) +} + +#[derive(Clone, Copy, Debug)] +enum Kind { + Native, + Wasm, +} + +/// Copies fixture binary `bin` into `project` as `name` (plus the kind's +/// extension), returning the file name to put in `comline.toml`. +fn install(project: &Path, kind: Kind, bin: &str, name: &str) -> String { + let (native, wasm) = built(); + let (from, file) = match kind { + Kind::Native => ( + native.join(format!("{bin}{}", std::env::consts::EXE_SUFFIX)), + format!("{name}{}", std::env::consts::EXE_SUFFIX), + ), + Kind::Wasm => (wasm.join(format!("{bin}.wasm")), format!("{name}.wasm")), + }; + fs::copy(&from, project.join(&file)).unwrap_or_else(|e| panic!("{}: {e}", from.display())); + file +} + +/// Every file under `dir`, relative, with its contents. +fn tree(dir: &Path) -> Vec<(PathBuf, Vec)> { + let mut out = Vec::new(); + let mut stack = vec![dir.to_path_buf()]; + while let Some(d) = stack.pop() { + for entry in fs::read_dir(&d).unwrap() { + let path = entry.unwrap().path(); + if path.is_dir() { + stack.push(path); + } else { + out.push(( + path.strip_prefix(dir).unwrap().to_owned(), + fs::read(&path).unwrap(), + )); + } + } + } + out.sort(); + out +} + +/// The built-in Rust generator's output for the fixture, in `mode`. +fn builtin_output(mode: &str) -> Vec<(PathBuf, Vec)> { + let temp = tempfile::tempdir().unwrap(); + let project = fixture_project(temp.path()); + comline_cmd() + .current_dir(&project) + .args(["generate", "--mode", mode]) + .assert() + .success(); + tree(&project.join("generated")) +} + +fn matches_builtin(kind: Kind) { + for mode in ["code", "lib"] { + let temp = tempfile::tempdir().unwrap(); + let project = fixture_project(temp.path()); + let file = install(&project, kind, "real", "comline-gen-rust"); + // No `language`: the generator offers only `rust`. + fs::write( + project.join("comline.toml"), + format!("[[generate.target]]\ngenerator = \"{file}\"\nmode = \"{mode}\"\n"), + ) + .unwrap(); + comline_cmd() + .current_dir(&project) + .arg("generate") + .assert() + .success(); + let external = tree(&project.join("generated")); + assert!(!external.is_empty()); + assert!( + external == builtin_output(mode), + "{kind:?} {mode}: the external output differs from the built-in's" + ); + } +} + +#[cfg(feature = "gen-wasm")] +#[test] +fn a_wasm_generator_writes_what_the_builtin_does() { + matches_builtin(Kind::Wasm); +} + +#[test] +fn a_native_generator_writes_what_the_builtin_does() { + matches_builtin(Kind::Native); +} + +/// Runs hostile generator `mode` as `kind`, expecting a failure whose +/// message contains `expected`, and nothing written. +fn refused(kind: Kind, mode: &str, extra: &str, expected: &str) -> String { + let temp = tempfile::tempdir().unwrap(); + let project = fixture_project(temp.path()); + let file = install(&project, kind, "hostile", mode); + fs::write( + project.join("comline.toml"), + format!("[[generate.target]]\nlanguage = \"rust\"\ngenerator = \"{file}\"\n{extra}"), + ) + .unwrap(); + let output = comline_cmd() + .current_dir(&project) + .arg("generate") + .assert() + .failure() + .get_output() + .clone(); + let stderr = String::from_utf8_lossy(&output.stderr).into_owned(); + assert!( + stderr.contains(expected), + "{kind:?} {mode}: expected `{expected}` in:\n{stderr}" + ); + assert!( + stderr.contains(&format!("generator `{file}`")), + "{kind:?} {mode}: the generator isn't named:\n{stderr}" + ); + assert!( + !project.join("generated").exists(), + "{kind:?} {mode}: something was written" + ); + assert!(!temp.path().join("escape.rs").exists() && !project.join("escape.rs").exists()); + stderr +} + +/// Runs `test` for a `.wasm` (when this build runs them) and a native +/// generator. +fn both(test: impl Fn(Kind)) { + if cfg!(feature = "gen-wasm") { + test(Kind::Wasm); + } + test(Kind::Native); +} + +#[test] +fn a_parent_path_is_refused() { + both(|kind| { + refused( + kind, + "dotdot", + "", + "returned the path `../escape.rs`, which isn't normalized", + ); + }); +} + +#[test] +fn an_absolute_path_is_refused() { + both(|kind| { + refused( + kind, + "absolute", + "", + "which is absolute; nothing was written", + ); + assert!(!Path::new("/tmp/comline-hostile-absolute.rs").exists()); + }); +} + +#[test] +fn oversized_output_is_refused() { + both(|kind| { + refused( + kind, + "oversized", + "", + "its output passed the 268435456-byte limit", + ); + }); +} + +#[test] +fn a_generator_that_never_finishes_is_stopped() { + both(|kind| { + refused( + kind, + "timeout", + "timeout = 2\n", + "didn't finish within 2s and was stopped", + ); + }); +} + +#[test] +fn a_protocol_mismatch_names_both_sides() { + both(|kind| { + refused( + kind, + "mismatch", + "", + "speaks generator protocol 7, and this comline speaks protocol 0", + ); + }); +} + +#[cfg(feature = "gen-wasm")] +#[test] +fn a_wasm_generator_cannot_read_the_filesystem() { + let stderr = refused(Kind::Wasm, "readfs", "", "couldn't read the filesystem"); + assert!(!stderr.contains("simple_project")); +} + +#[test] +fn stderr_is_shown_prefixed_and_capped() { + both(|kind| { + let stderr = refused(kind, "stderr", "", "refusing after the noise"); + let name = match kind { + Kind::Wasm => "stderr.wasm".to_string(), + Kind::Native => format!("stderr{}", std::env::consts::EXE_SUFFIX), + }; + assert!( + stderr.contains(&format!(" [{name}] hello from the generator")), + "{stderr}" + ); + assert!( + stderr.contains("(stderr past 65536 bytes not shown)"), + "{stderr}" + ); + }); +} + +#[test] +fn a_language_the_generator_does_not_offer_is_refused() { + let temp = tempfile::tempdir().unwrap(); + let project = fixture_project(temp.path()); + let file = install(&project, Kind::Native, "hostile", "future"); + fs::write( + project.join("comline.toml"), + format!("[[generate.target]]\ngenerator = \"{file}\"\n"), + ) + .unwrap(); + comline_cmd() + .current_dir(&project) + .arg("generate") + .assert() + .failure() + .stderr(predicate::str::contains(format!( + "generator `{file}` handles `rust` from `9.0.0` on, not `1.85.0`" + ))); +} diff --git a/tests/cli/main.rs b/tests/cli/main.rs index a599648..182eca9 100644 --- a/tests/cli/main.rs +++ b/tests/cli/main.rs @@ -13,6 +13,7 @@ mod check; mod clean; mod diff; mod end_to_end; +mod external; mod generate; mod global; mod new; diff --git a/tests/fixtures/generators/Cargo.lock b/tests/fixtures/generators/Cargo.lock new file mode 100644 index 0000000..c2da2e9 --- /dev/null +++ b/tests/fixtures/generators/Cargo.lock @@ -0,0 +1,380 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "blake3" +version = "1.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d9e454fc11f76977dc803893aff6304ed33d6a26efae8696573bea74baa27ae" +dependencies = [ + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures", +] + +[[package]] +name = "cc" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86e055fa6e3e40ad6187ba25b6855eeb635d5271f8e0f0e1e9a5826b8d75bea0" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "comline-codegen" +version = "0.1.0" +source = "git+https://github.com/ComlineProject/generation?rev=2d922cebb347ea1939d90c713ccf77a7ec82db65#2d922cebb347ea1939d90c713ccf77a7ec82db65" +dependencies = [ + "comline-core", + "eyre", + "serde", + "serde_json", +] + +[[package]] +name = "comline-codegen-rust" +version = "0.1.0" +source = "git+https://github.com/ComlineProject/comline-rust?rev=8bbce6dbe7f872550f4300025e5c139ff5ec304c#8bbce6dbe7f872550f4300025e5c139ff5ec304c" +dependencies = [ + "comline-codegen", + "comline-core", + "eyre", +] + +[[package]] +name = "comline-core" +version = "0.1.0" +source = "git+https://github.com/ComlineProject/core?rev=98d64faedfc48ab4d2c93722e2b099debedd503c#98d64faedfc48ab4d2c93722e2b099debedd503c" +dependencies = [ + "blake3", + "ciborium", + "comline-core-stdlib", + "eyre", + "lz4_flex", + "semver", + "serde", + "serde_derive", + "serde_json", + "tracing", +] + +[[package]] +name = "comline-core-stdlib" +version = "0.1.0" +source = "git+https://github.com/ComlineProject/core?rev=98d64faedfc48ab4d2c93722e2b099debedd503c#98d64faedfc48ab4d2c93722e2b099debedd503c" + +[[package]] +name = "comline-test-generators" +version = "0.0.0" +dependencies = [ + "comline-codegen", + "comline-codegen-rust", +] + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "eyre" +version = "0.6.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08309dbcc659c5549a24ddb9b27027640641b282ef5768267c7e675558986a3" +dependencies = [ + "autocfg", + "indenter", + "once_cell", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c66915cc5391097de7a2b0b972e8505aa33c0df401fd3a6811dbdb32881ba48" + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "indenter" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "libc" +version = "0.2.191" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a" + +[[package]] +name = "lz4_flex" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "373f5eceeeab7925e0c1098212f2fbc4d416adec9d35051a6ab251e824c1854a" +dependencies = [ + "twox-hash", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "serde_json" +version = "1.0.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "twox-hash" +version = "2.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86a801b3cea342a06d468c8710662aa29e5e05e4f5c0d62f00bbb7f2ad7941c2" + +[[package]] +name = "unicode-ident" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" + +[[package]] +name = "zerocopy" +version = "0.8.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5fe1f8f1b06191a00962174c61aa5005e0bb391a6d80d07e24d115c01a92ed8" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863ad3ac83293fb4d740aedbfdc9240dd8d1a50c1099acd76ce80ce7c7230c7f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/tests/fixtures/generators/Cargo.toml b/tests/fixtures/generators/Cargo.toml new file mode 100644 index 0000000..2bbc4d3 --- /dev/null +++ b/tests/fixtures/generators/Cargo.toml @@ -0,0 +1,16 @@ +# Generators the integration tests run as external generators +# (tests/cli/external.rs), built there natively and for wasm32-wasip1. Its own +# workspace: not part of the CLI's build. +[package] +name = "comline-test-generators" +version = "0.0.0" +edition = "2021" +publish = false + +[workspace] + +[dependencies] +# The same revs the CLI pins, so the real generator's output is comparable +# byte for byte with the CLI's built-in one. +comline-codegen = { git = "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/ComlineProject/generation", rev = "2d922cebb347ea1939d90c713ccf77a7ec82db65" } +comline-codegen-rust = { git = "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/ComlineProject/comline-rust", rev = "8bbce6dbe7f872550f4300025e5c139ff5ec304c" } diff --git a/tests/fixtures/generators/src/bin/hostile.rs b/tests/fixtures/generators/src/bin/hostile.rs new file mode 100644 index 0000000..8c39c80 --- /dev/null +++ b/tests/fixtures/generators/src/bin/hostile.rs @@ -0,0 +1,111 @@ +//! A generator that misbehaves, one way per name: the tests copy it to +//! `.wasm` or `` and the mode is `argv[0]`'s file stem, since a +//! generator gets no other arguments and no environment. + +use std::io::{Read, Write}; + +use comline_codegen::protocol::{ + decode, encode, Description, FileOut, Generated, LanguageInfo, Request, RequestBody, Response, + VersionsInfo, IR_FORMAT, PROTOCOL_VERSION, +}; + +fn main() { + let arg0 = std::env::args().next().unwrap_or_default(); + let mode = std::path::Path::new(&arg0) + .file_stem() + .map(|s| s.to_string_lossy().into_owned()) + .unwrap_or_default(); + let mut input = Vec::new(); + std::io::stdin().read_to_end(&mut input).unwrap(); + let request: Request = decode(&input).unwrap(); + let response = match request.body { + RequestBody::Describe => describe(&mode), + RequestBody::Generate(_) => generate(&mode), + }; + let mut out = std::io::stdout().lock(); + out.write_all(&encode(&response).unwrap()).unwrap(); + out.flush().unwrap(); +} + +fn describe(mode: &str) -> Response { + let protocol = if mode == "mismatch" { + 7 + } else { + PROTOCOL_VERSION + }; + Response::Describe(Description { + languages: vec![LanguageInfo { + id: "rust".into(), + ext: "rs".into(), + versions: VersionsInfo { + floor: if mode == "future" { "9.0.0" } else { "1.0.0" }.into(), + changes: vec![], + verified_up_to: "1.0.0".into(), + forward_compatible: true, + }, + support: vec![], + }], + protocols: (protocol, protocol), + ir_formats: vec![IR_FORMAT], + }) +} + +fn files(paths: &[&str]) -> Response { + Response::Generate(Generated { + files: paths + .iter() + .map(|p| FileOut { + path: p.to_string(), + contents: b"x".to_vec(), + }) + .collect(), + diagnostics: vec![], + }) +} + +fn generate(mode: &str) -> Response { + match mode { + // A good file first: nothing at all may be written. + "dotdot" => files(&["api.rs", "../escape.rs"]), + "absolute" => files(&["api.rs", "/tmp/comline-hostile-absolute.rs"]), + "oversized" => { + let chunk = vec![0u8; 1024 * 1024]; + let mut out = std::io::stdout().lock(); + loop { + out.write_all(&chunk).unwrap(); + } + } + "timeout" => + { + #[allow(clippy::empty_loop)] + loop {} + } + "readfs" => match std::fs::read_to_string("comline.toml") + .or_else(|_| std::fs::read_to_string("/etc/hostname")) + { + Ok(text) => Response::Generate(Generated { + files: vec![FileOut { + path: "leaked.rs".into(), + contents: text.into_bytes(), + }], + diagnostics: vec![], + }), + Err(e) => Response::Error { + message: format!("couldn't read the filesystem: {e}"), + }, + }, + "stderr" => { + eprintln!("hello from the generator"); + let line = "x".repeat(1023); + for _ in 0..100 { + eprintln!("{line}"); + } + Response::Error { + message: "refusing after the noise".into(), + } + } + other => Response::Error { + message: format!("unknown mode `{other}`"), + }, + } +} diff --git a/tests/fixtures/generators/src/bin/real.rs b/tests/fixtures/generators/src/bin/real.rs new file mode 100644 index 0000000..ee66fcd --- /dev/null +++ b/tests/fixtures/generators/src/bin/real.rs @@ -0,0 +1,10 @@ +//! The official Rust generator as an external one: what comline-rust's +//! `comline-gen-rust` binary is. + +use std::process::ExitCode; + +fn main() -> ExitCode { + let mut registry = comline_codegen::Registry::new(); + comline_codegen_rust::register(&mut registry); + comline_codegen::protocol::serve_stdio(®istry) +}