From dcc4cbd9eff42d639e10e30249784caa04742fa4 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 20 Mar 2026 07:30:21 +0000 Subject: [PATCH 1/3] test: add conda-forge activation script compatibility tests Add a bash script (scripts/test_conda_activation.sh) that downloads all activation/deactivation .sh scripts from conda-forge feedstocks (go, rust, flang, ctng-compiler, clang-compiler, clang-win) and runs them through the shell binary to test compatibility. Add Rust integration tests (conda_activation_tests.rs) that test specific patterns used in these activation scripts. Results: - 10 passing: simple exports, function definitions, conditionals, path manipulation, backup/restore env vars - 3 ignored (documents unsupported features): - `function name() {}` syntax (bash keyword-style) - `[[ ]]` double-bracket conditionals - `${!var}` indirect variable expansion https://claude.ai/code/session_0178vMPxzmTVt9Kso1zkYjeN --- Cargo.lock | 1025 +++++++++++++++++++- crates/tests/Cargo.toml | 2 + crates/tests/src/conda_activation_tests.rs | 336 +++++++ crates/tests/src/lib.rs | 1 + scripts/test_conda_activation.sh | 238 +++++ 5 files changed, 1577 insertions(+), 25 deletions(-) create mode 100644 crates/tests/src/conda_activation_tests.rs create mode 100755 scripts/test_conda_activation.sh diff --git a/Cargo.lock b/Cargo.lock index 99df8bf..f1917dc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -112,6 +112,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca6c635b3aa665c649ad1415f1573c85957dfa47690ec27aebe7ec17efe3c643" +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.4.0" @@ -130,7 +136,7 @@ dependencies = [ "miniz_oxide", "object", "rustc-demangle", - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -142,6 +148,12 @@ dependencies = [ "backtrace", ] +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bitflags" version = "2.8.0" @@ -201,7 +213,7 @@ dependencies = [ "js-sys", "num-traits", "wasm-bindgen", - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -281,6 +293,16 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -378,6 +400,17 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "dtparse" version = "2.0.1" @@ -402,6 +435,15 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "endian-type" version = "0.1.2" @@ -414,6 +456,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe" +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + [[package]] name = "errno" version = "0.3.10" @@ -465,6 +513,36 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + [[package]] name = "futures" version = "0.3.31" @@ -584,7 +662,7 @@ dependencies = [ "cfg-if", "libc", "wasi 0.13.3+wasi-0.2.2", - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -599,6 +677,31 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" +[[package]] +name = "h2" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + [[package]] name = "heck" version = "0.5.0" @@ -625,6 +728,118 @@ dependencies = [ "windows", ] +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + [[package]] name = "iana-time-zone" version = "0.1.61" @@ -648,6 +863,124 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + [[package]] name = "is_ci" version = "1.2.0" @@ -714,6 +1047,12 @@ version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + [[package]] name = "lock_api" version = "0.4.12" @@ -777,6 +1116,12 @@ dependencies = [ "syn", ] +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -803,6 +1148,23 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "nibble_vec" version = "0.1.0" @@ -882,6 +1244,50 @@ version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +[[package]] +name = "openssl" +version = "0.10.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.112" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "option-ext" version = "0.2.0" @@ -933,7 +1339,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -976,6 +1382,12 @@ dependencies = [ "once_cell", ] +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + [[package]] name = "pest" version = "2.7.15" @@ -1086,6 +1498,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + [[package]] name = "platform-info" version = "2.0.5" @@ -1096,6 +1514,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + [[package]] name = "pretty_assertions" version = "1.4.1" @@ -1199,21 +1626,80 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] -name = "rust_decimal" -version = "1.36.0" +name = "reqwest" +version = "0.12.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b082d80e3e3cc52b2ed634388d436fe1f4de6af5786cc2de9ba9737527bdf555" +checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb" dependencies = [ - "arrayvec", - "num-traits", + "base64", + "bytes", + "encoding_rs", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-native-tls", + "tower", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-registry", ] [[package]] -name = "rustc-demangle" -version = "0.1.24" +name = "ring" +version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" - +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.15", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rust_decimal" +version = "1.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b082d80e3e3cc52b2ed634388d436fe1f4de6af5786cc2de9ba9737527bdf555" +dependencies = [ + "arrayvec", + "num-traits", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + [[package]] name = "rustix" version = "0.38.44" @@ -1227,6 +1713,48 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "rustls" +version = "0.23.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.19" @@ -1273,12 +1801,44 @@ version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "serde" version = "1.0.217" @@ -1311,6 +1871,18 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "sha2" version = "0.10.8" @@ -1394,12 +1966,24 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + [[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "supports-color" version = "3.0.2" @@ -1432,6 +2016,47 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tempfile" version = "3.16.0" @@ -1465,6 +2090,8 @@ dependencies = [ "futures", "miette", "pretty_assertions", + "regex", + "reqwest", "shell", "tempfile", "tokio", @@ -1520,6 +2147,16 @@ dependencies = [ "syn", ] +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "tokio" version = "1.43.0" @@ -1549,6 +2186,26 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.13" @@ -1562,6 +2219,58 @@ dependencies = [ "tokio", ] +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[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 = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "typenum" version = "1.17.0" @@ -1604,6 +2313,30 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" version = "0.2.2" @@ -1719,12 +2452,27 @@ dependencies = [ "ansi-width", ] +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -1766,6 +2514,19 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.100" @@ -1798,6 +2559,16 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "which" version = "7.0.2" @@ -1857,7 +2628,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" dependencies = [ "windows-core", - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -1866,7 +2637,48 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-registry" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" +dependencies = [ + "windows-result", + "windows-strings", + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" +dependencies = [ + "windows-link 0.1.3", ] [[package]] @@ -1875,7 +2687,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -1884,7 +2696,16 @@ version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", ] [[package]] @@ -1893,14 +2714,31 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] [[package]] @@ -1909,48 +2747,96 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "winsafe" version = "0.0.19" @@ -1966,6 +2852,12 @@ dependencies = [ "bitflags", ] +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + [[package]] name = "xattr" version = "1.4.0" @@ -1982,3 +2874,86 @@ name = "yansi" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/crates/tests/Cargo.toml b/crates/tests/Cargo.toml index d8f0e26..6196136 100644 --- a/crates/tests/Cargo.toml +++ b/crates/tests/Cargo.toml @@ -13,4 +13,6 @@ miette = "7.5.0" [dev-dependencies] pretty_assertions = "1.4.1" +regex = "1" +reqwest = { version = "0.12", features = ["blocking"] } tempfile = "3.16.0" diff --git a/crates/tests/src/conda_activation_tests.rs b/crates/tests/src/conda_activation_tests.rs new file mode 100644 index 0000000..c03b3f4 --- /dev/null +++ b/crates/tests/src/conda_activation_tests.rs @@ -0,0 +1,336 @@ +/// Tests for conda-forge activation scripts. +/// +/// These tests verify that our shell can parse and execute real activation +/// scripts from conda-forge feedstocks. The scripts are stored locally +/// after being downloaded by `scripts/test_conda_activation.sh`. +/// +/// The scripts are preprocessed to replace conda-build template variables +/// (@VAR@) with plausible dummy values before execution. +use crate::test_builder::TestBuilder; + +/// Replace conda-build template variables (@VAR@) with plausible dummy values. +fn preprocess_conda_template(script: &str) -> String { + let replacements = [ + ("@CHOST@", "x86_64-conda-linux-gnu"), + ("@CONDA_TOOLCHAIN_HOST@", "x86_64-conda-linux-gnu"), + ("@CONDA_TOOLCHAIN_BUILD@", "x86_64-conda-linux-gnu"), + ("@MACH@", "x86_64"), + ("@VENDOR@", "conda"), + ("@OS@", "linux"), + ("@cross_target_platform@", "linux-64"), + ("@native_compiler_subdir@", "linux-64"), + ("@target_platform@", "linux-64"), + ("@build_platform@", "linux-64"), + ("@c_compiler@", "gcc"), + ("@cxx_compiler@", "g++"), + ("@fortran_compiler@", "gfortran"), + ("@c_compiler_version@", "12"), + ("@cxx_compiler_version@", "12"), + ("@fortran_compiler_version@", "12"), + ("@CC@", "gcc"), + ("@CXX@", "g++"), + ("@FC@", "gfortran"), + ("@IS_WIN@", "False"), + ("@CMAKE_SYSTEM_NAME@", "Linux"), + ("@CONDA_BUILD_CROSS_COMPILATION@", "0"), + ( + "@CONDA_BUILD_SYSROOT@", + "/opt/conda/x86_64-conda-linux-gnu/sysroot", + ), + ("@rust_arch@", "x86_64-unknown-linux-gnu"), + ("@rust_arch_env@", "X86_64_UNKNOWN_LINUX_GNU"), + ("@rust_arch_env_build@", "X86_64_UNKNOWN_LINUX_GNU"), + ("@rust_default_cc@", "x86_64-conda-linux-gnu-cc"), + ("@rust_default_cc_build@", "x86_64-conda-linux-gnu-cc"), + ("@CONDA_RUST_HOST_LOWER@", "x86_64_unknown_linux_gnu"), + ("@CONDA_RUST_TARGET_LOWER@", "x86_64_unknown_linux_gnu"), + ("@GOARCH@", "amd64"), + ("@GOOS@", "linux"), + ("@CGO_ENABLED@", "1"), + ("@MACOSX_SDK_VERSION@", "10.15"), + ]; + + let mut result = script.to_string(); + for (pattern, replacement) in &replacements { + result = result.replace(pattern, replacement); + } + + // Replace any remaining @VAR@ patterns with a dummy value + let re = regex::Regex::new(r"@[A-Za-z_][A-Za-z_0-9]*@").unwrap(); + result = re.replace_all(&result, "TEMPLATE_VALUE").to_string(); + + result +} + +async fn run_conda_script(script: &str) { + let preprocessed = preprocess_conda_template(script); + TestBuilder::new() + .command(&preprocessed) + .check_stdout(false) + .env_var("CONDA_PREFIX", "/tmp/test_conda_prefix") + .env_var("PREFIX", "/tmp/test_conda_prefix") + .env_var("BUILD_PREFIX", "/tmp/test_conda_build_prefix") + .env_var("CONDA_BUILD", "") + .run() + .await; +} + +// === Simple activation scripts (export/unset only) === + +#[tokio::test] +async fn conda_go_activate() { + // go-activation-feedstock: recipe/activate.sh + // Simple exports of env vars + run_conda_script( + r#"export CGO_ENABLED=${CGO_ENABLED} +export GOOS=${GOOS} +export GOARCH=${GOARCH} +export CONDA_GO_COMPILER=1 +export GOFLAGS="-modcacherw -buildmode=pie -trimpath" +"#, + ) + .await; +} + +#[tokio::test] +async fn conda_go_deactivate() { + // go-activation-feedstock: recipe/deactivate.sh + run_conda_script("unset CONDA_GO_COMPILER\n").await; +} + +#[tokio::test] +async fn conda_flang_activate() { + // flang-activation-feedstock: recipe/activate.sh + run_conda_script( + r#"export CONDA_BACKUP_FC=$FC +export FC="x86_64-conda-linux-gnu-flang" +"#, + ) + .await; +} + +#[tokio::test] +async fn conda_flang_deactivate() { + // flang-activation-feedstock: recipe/deactivate.sh + run_conda_script("export FC=$CONDA_BACKUP_FC\n").await; +} + +// === Rust activation (uses [[ ]] and mkdir) === + +#[tokio::test] +#[ignore = "shell does not yet support [[ ]] double-bracket conditionals"] +async fn conda_rust_activate() { + // rust-activation-feedstock: recipe/activate.sh (preprocessed) + // This tests [[ ]] conditionals, ${VAR:-default}, and complex variable exports + run_conda_script( + r#"#!/usr/bin/env bash + +export CARGO_HOME=${CARGO_HOME:-${CONDA_PREFIX}/.cargo} +export CARGO_CONFIG=${CARGO_CONFIG:-${CARGO_HOME}/config} +export RUSTUP_HOME=${RUSTUP_HOME:-${CARGO_HOME}/rustup} + +[[ -d ${CARGO_HOME} ]] || mkdir -p ${CARGO_HOME} + +export CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER=${CC_FOR_BUILD:-${CONDA_PREFIX}/bin/x86_64-conda-linux-gnu-cc} +export CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER=${CC:-${CONDA_PREFIX}/bin/x86_64-conda-linux-gnu-cc} +export CARGO_BUILD_TARGET=x86_64-unknown-linux-gnu +export CONDA_RUST_HOST=X86_64_UNKNOWN_LINUX_GNU +export CONDA_RUST_TARGET=X86_64_UNKNOWN_LINUX_GNU +export PKG_CONFIG_PATH_X86_64_UNKNOWN_LINUX_GNU=${CONDA_PREFIX}/lib/pkgconfig +export PKG_CONFIG_PATH_X86_64_UNKNOWN_LINUX_GNU=${PREFIX:-${CONDA_PREFIX}}/lib/pkgconfig + +export CC_x86_64_unknown_linux_gnu="${CC_FOR_BUILD:-${CONDA_PREFIX}/bin/x86_64-conda-linux-gnu-cc}" +export CFLAGS_x86_64_unknown_linux_gnu="-isystem ${CONDA_PREFIX}/include" +export CFLAGS_x86_64_unknown_linux_gnu="${CFLAGS}" +export CPPFLAGS_x86_64_unknown_linux_gnu="-isystem ${CONDA_PREFIX}/include" +export CPPFLAGS_x86_64_unknown_linux_gnu="${CPPFLAGS}" +export CXXFLAGS_x86_64_unknown_linux_gnu="-isystem ${CONDA_PREFIX}/include" +export CXXFLAGS_x86_64_unknown_linux_gnu="${CXXFLAGS}" + +if [[ "linux-64" == linux* ]]; then + export CARGO_BUILD_RUSTFLAGS="-C link-arg=-Wl,-rpath-link,${PREFIX:-${CONDA_PREFIX}}/lib -C link-arg=-Wl,-rpath,${PREFIX:-${CONDA_PREFIX}}/lib" +elif [[ "linux-64" == win* ]]; then + export CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER=${CONDA_PREFIX}/bin/lld-link + export AR_x86_64_unknown_linux_gnu="${AR}" + export AR_x86_64_unknown_linux_gnu=$CONDA_PREFIX/bin/llvm-lib + export CC_x86_64_unknown_linux_gnu=$CONDA_PREFIX/bin/clang-cl + export CXX_x86_64_unknown_linux_gnu=$CONDA_PREFIX/bin/clang-cl + export LDFLAGS="$LDFLAGS -manifest:no" + export CMAKE_GENERATOR=Ninja +elif [[ "linux-64" == osx* ]]; then + export CARGO_BUILD_RUSTFLAGS="-C link-arg=-Wl,-rpath,${PREFIX:-${CONDA_PREFIX}}/lib" + if [[ "${CONDA_BUILD:-}" != "" ]]; then + export CARGO_BUILD_RUSTFLAGS="$CARGO_BUILD_RUSTFLAGS -C link-arg=-Wl,-headerpad_max_install_names -C link-arg=-Wl,-dead_strip_dylibs" + fi +fi + +export PATH=${CARGO_HOME}/bin:${PATH} + +if [[ "linux-64" == osx* || "linux-64" == linux* ]]; then + if [[ "X86_64_UNKNOWN_LINUX_GNU" != "X86_64_UNKNOWN_LINUX_GNU" ]]; then + export CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS="-C link-arg=-Wl,-rpath,${BUILD_PREFIX:-${CONDA_PREFIX}}/lib" + fi +fi +"#, + ) + .await; +} + +// === Compiler activation scripts with functions and complex shell features === + +#[tokio::test] +async fn conda_ctng_gcc_activate_function_definition() { + // ctng-compiler-activation-feedstock uses function definitions and + // ${BASH_SOURCE[0]} array access. Test that the function keyword + parens works. + run_conda_script( + r#"_get_sourced_filename() { + if [ -n "${BASH_SOURCE+x}" ]; then + basename "${BASH_SOURCE}" + else + echo "UNKNOWN" + fi +} +echo "$(_get_sourced_filename)" +"#, + ) + .await; +} + +#[tokio::test] +#[ignore = "shell does not yet support `function name() {}` syntax (bash keyword-style function definition)"] +async fn conda_clang_activate_function_keyword() { + // clang-compiler-activation-feedstock uses `function name() {` syntax + run_conda_script( + r#"function _get_sourced_filename() { + if [ -n "${BASH_SOURCE+x}" ]; then + basename "${BASH_SOURCE}" + else + echo "UNKNOWN" + fi +} +echo "$(_get_sourced_filename)" +"#, + ) + .await; +} + +#[tokio::test] +#[ignore = "shell does not yet support ${!var} indirect variable expansion"] +async fn conda_tc_activation_pattern() { + // The _tc_activation function pattern from ctng/clang compiler activation scripts. + // Tests local variables, for loops, if/elif/else, export, and unset. + run_conda_script( + r#"_tc_activation() { + local new_val + for val in "$@"; do + local var_name="${val%%,*}" + new_val="${val#*,}" + if [ -n "${!var_name+x}" ]; then + export "CONDA_BACKUP_${var_name}=${!var_name}" + fi + export "${var_name}=${new_val}" + done +} + +_tc_activation \ + "CC,gcc" \ + "CXX,g++" \ + "CFLAGS,-O2" + +echo $CC +echo $CXX +echo $CFLAGS +"#, + ) + .await; +} + +#[tokio::test] +async fn conda_tc_deactivation_pattern() { + // The _tc_deactivation function pattern - restores backed-up env vars + run_conda_script( + r#"_tc_deactivation() { + for val in "$@"; do + local var_name="${val}" + local backup_var="CONDA_BACKUP_${var_name}" + if [ -n "${!backup_var+x}" ]; then + export "${var_name}=${!backup_var}" + unset "${backup_var}" + else + unset "${var_name}" + fi + done +} + +export CC=gcc +export CONDA_BACKUP_CC=old_gcc +_tc_deactivation CC +echo ${CC} +"#, + ) + .await; +} + +#[tokio::test] +async fn conda_win_activation_env_setup() { + // clang-win-activation-feedstock: activate-msvc-headers-libs.sh pattern + // Tests dirname, readlink, and path manipulation + run_conda_script( + r#"export CONDA_PREFIX="/tmp/test_conda_prefix" + +if [ -z "${VSINSTALLDIR+x}" ]; then + export VSINSTALLDIR="${CONDA_PREFIX}/vs_default" +fi + +export INCLUDE="${CONDA_PREFIX}/include" +export LIB="${CONDA_PREFIX}/lib" +"#, + ) + .await; +} + +#[tokio::test] +async fn conda_path_manipulation() { + // Common pattern in activation scripts: prepending to PATH + run_conda_script( + r#"export CONDA_PREFIX="/tmp/test_conda_prefix" +export PATH="${CONDA_PREFIX}/bin:${PATH}" +echo "ok" +"#, + ) + .await; +} + +#[tokio::test] +async fn conda_conditional_platform_check() { + // Pattern from multiple activation scripts: platform-dependent configuration + run_conda_script( + r#"target_platform="linux-64" +if [ "$target_platform" = "linux-64" ]; then + export CONDA_BUILD_SYSROOT="/opt/conda/x86_64-conda-linux-gnu/sysroot" +elif [ "$target_platform" = "osx-64" ]; then + export CONDA_BUILD_SYSROOT="/opt/conda/sysroot" +fi +echo $CONDA_BUILD_SYSROOT +"#, + ) + .await; +} + +#[tokio::test] +async fn conda_backup_and_restore_pattern() { + // Common pattern: backup current value, set new value, then restore + run_conda_script( + r#"export ORIGINAL_CC="old_compiler" +export CONDA_BACKUP_CC="$ORIGINAL_CC" +export CC="new_compiler" +echo "CC=$CC" +echo "backup=$CONDA_BACKUP_CC" +# Restore +export CC="$CONDA_BACKUP_CC" +unset CONDA_BACKUP_CC +echo "restored CC=$CC" +"#, + ) + .await; +} diff --git a/crates/tests/src/lib.rs b/crates/tests/src/lib.rs index 8d24e47..22e42b1 100644 --- a/crates/tests/src/lib.rs +++ b/crates/tests/src/lib.rs @@ -1,6 +1,7 @@ // Copyright 2018-2024 the Deno authors. MIT license. #![cfg(test)] +mod conda_activation_tests; mod test_builder; mod test_runner; diff --git a/scripts/test_conda_activation.sh b/scripts/test_conda_activation.sh new file mode 100755 index 0000000..efcc51d --- /dev/null +++ b/scripts/test_conda_activation.sh @@ -0,0 +1,238 @@ +#!/usr/bin/env bash +# Test conda-forge activation scripts against prefix-dev/shell +# +# This script: +# 1. Downloads activation/deactivation .sh scripts from conda-forge feedstocks on GitHub +# 2. Preprocesses conda-build template variables (@VAR@) with dummy values +# 3. Runs each script through the shell binary +# 4. Reports which scripts pass/fail (parsing + execution) + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +SHELL_BIN="${ROOT_DIR}/target/release/shell" +DOWNLOAD_DIR="${ROOT_DIR}/target/conda_activation_scripts" +RESULTS_DIR="${ROOT_DIR}/target/conda_activation_results" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +CYAN='\033[0;36m' +NC='\033[0m' # No Color + +# Counters +TOTAL=0 +PASSED=0 +FAILED=0 +SKIPPED=0 + +# Check shell binary exists +if [ ! -f "$SHELL_BIN" ]; then + echo -e "${RED}Error: Shell binary not found at $SHELL_BIN${NC}" + echo "Build it first with: cargo build --release" + exit 1 +fi + +mkdir -p "$DOWNLOAD_DIR" "$RESULTS_DIR" + +# Define all conda-forge activation script URLs +# Format: "repo_name:branch:path_in_repo" +SCRIPTS=( + # go-activation + "go-activation-feedstock:main:recipe/activate.sh" + "go-activation-feedstock:main:recipe/deactivate.sh" + + # rust-activation + "rust-activation-feedstock:main:recipe/activate.sh" + + # flang-activation + "flang-activation-feedstock:main:recipe/activate.sh" + "flang-activation-feedstock:main:recipe/deactivate.sh" + + # ctng-compiler-activation (gcc, g++, gfortran) + "ctng-compiler-activation-feedstock:main:recipe/activate-gcc.sh" + "ctng-compiler-activation-feedstock:main:recipe/deactivate-gcc.sh" + "ctng-compiler-activation-feedstock:main:recipe/activate-g++.sh" + "ctng-compiler-activation-feedstock:main:recipe/deactivate-g++.sh" + "ctng-compiler-activation-feedstock:main:recipe/activate-gfortran.sh" + "ctng-compiler-activation-feedstock:main:recipe/deactivate-gfortran.sh" + + # clang-compiler-activation + "clang-compiler-activation-feedstock:main:recipe/activate-clang.sh" + "clang-compiler-activation-feedstock:main:recipe/deactivate-clang.sh" + "clang-compiler-activation-feedstock:main:recipe/activate-clang++.sh" + "clang-compiler-activation-feedstock:main:recipe/deactivate-clang++.sh" + + # clang-win-activation (sh files only) + "clang-win-activation-feedstock:main:recipe/activate-clang_win-64.sh" + "clang-win-activation-feedstock:main:recipe/activate-clangxx_win-64.sh" + "clang-win-activation-feedstock:main:recipe/activate-msvc-headers-libs.sh" + "clang-win-activation-feedstock:main:recipe/activate-winsdk.sh" + "clang-win-activation-feedstock:main:recipe/deactivate-clang_win-64.sh" + "clang-win-activation-feedstock:main:recipe/deactivate-clangxx_win-64.sh" + "clang-win-activation-feedstock:main:recipe/deactivate-msvc-headers-libs.sh" + "clang-win-activation-feedstock:main:recipe/deactivate-winsdk.sh" +) + +echo -e "${CYAN}=== Conda-Forge Activation Script Test Runner ===${NC}" +echo -e "${CYAN}Shell binary: ${SHELL_BIN}${NC}" +echo "" + +# Step 1: Download all scripts +echo -e "${CYAN}--- Downloading activation scripts ---${NC}" +for entry in "${SCRIPTS[@]}"; do + IFS=':' read -r repo branch path <<< "$entry" + filename="${repo}__$(basename "$path")" + dest="${DOWNLOAD_DIR}/${filename}" + + if [ -f "$dest" ]; then + echo " [cached] $filename" + else + url="https://raw.githubusercontent.com/conda-forge/${repo}/${branch}/${path}" + if curl -sS -f -o "$dest" "$url" 2>/dev/null; then + echo " [downloaded] $filename" + else + echo -e " ${YELLOW}[not found] $filename (URL: $url)${NC}" + rm -f "$dest" + fi + fi +done +echo "" + +# Step 2: Preprocess template variables (@VAR@) with dummy values +# Conda-build replaces these at package build time; we substitute with plausible defaults +preprocess_script() { + local input="$1" + local output="$2" + + sed \ + -e 's/@CHOST@/x86_64-conda-linux-gnu/g' \ + -e 's/@CONDA_TOOLCHAIN_HOST@/x86_64-conda-linux-gnu/g' \ + -e 's/@CONDA_TOOLCHAIN_BUILD@/x86_64-conda-linux-gnu/g' \ + -e 's/@MACH@/x86_64/g' \ + -e 's/@VENDOR@/conda/g' \ + -e 's/@OS@/linux/g' \ + -e 's/@cross_target_platform@/linux-64/g' \ + -e 's/@native_compiler_subdir@/linux-64/g' \ + -e 's/@target_platform@/linux-64/g' \ + -e 's/@build_platform@/linux-64/g' \ + -e 's/@c_compiler@/gcc/g' \ + -e 's/@cxx_compiler@/g++/g' \ + -e 's/@fortran_compiler@/gfortran/g' \ + -e 's/@c_compiler_version@/12/g' \ + -e 's/@cxx_compiler_version@/12/g' \ + -e 's/@fortran_compiler_version@/12/g' \ + -e 's/@CC@/gcc/g' \ + -e 's/@CXX@/g++/g' \ + -e 's/@FC@/gfortran/g' \ + -e 's/@IS_WIN@/False/g' \ + -e 's/@CMAKE_SYSTEM_NAME@/Linux/g' \ + -e 's/@CONDA_BUILD_CROSS_COMPILATION@/0/g' \ + -e 's|@CONDA_BUILD_SYSROOT@|/opt/conda/x86_64-conda-linux-gnu/sysroot|g' \ + -e 's/@rust_arch@/x86_64-unknown-linux-gnu/g' \ + -e 's/@rust_arch_env@/X86_64_UNKNOWN_LINUX_GNU/g' \ + -e 's/@rust_arch_env_build@/X86_64_UNKNOWN_LINUX_GNU/g' \ + -e 's/@rust_default_cc@/x86_64-conda-linux-gnu-cc/g' \ + -e 's/@rust_default_cc_build@/x86_64-conda-linux-gnu-cc/g' \ + -e 's/@CONDA_RUST_HOST_LOWER@/x86_64_unknown_linux_gnu/g' \ + -e 's/@CONDA_RUST_TARGET_LOWER@/x86_64_unknown_linux_gnu/g' \ + -e 's/@GOARCH@/amd64/g' \ + -e 's/@GOOS@/linux/g' \ + -e 's/@CGO_ENABLED@/1/g' \ + -e 's/@MACOSX_SDK_VERSION@/10.15/g' \ + -e 's/@[A-Za-z_][A-Za-z_0-9]*@/TEMPLATE_VALUE/g' \ + "$input" > "$output" +} + +# Step 3: Run each script through the shell +echo -e "${CYAN}--- Running activation scripts through shell ---${NC}" +echo "" + +for script_file in "$DOWNLOAD_DIR"/*.sh; do + [ -f "$script_file" ] || continue + + filename="$(basename "$script_file")" + preprocessed="${RESULTS_DIR}/${filename}.preprocessed" + result_file="${RESULTS_DIR}/${filename}.result" + + TOTAL=$((TOTAL + 1)) + + # Preprocess template variables + preprocess_script "$script_file" "$preprocessed" + + # Set up a minimal environment that activation scripts expect + export CONDA_PREFIX="/tmp/test_conda_prefix" + export PREFIX="/tmp/test_conda_prefix" + export BUILD_PREFIX="/tmp/test_conda_build_prefix" + export CONDA_BUILD="" + export PATH="/usr/local/bin:/usr/bin:/bin" + + # Run through the shell binary with a timeout + # We add 'true' at the end so that the script succeeds even if last command is benign failure + # We wrap in a subshell command string using source-like behavior + script_content=$(cat "$preprocessed") + + # Run the preprocessed script through the shell + set +e + output=$("$SHELL_BIN" -c "$script_content" 2>"${result_file}.stderr" ) + exit_code=$? + set -e + + stderr_content="" + if [ -f "${result_file}.stderr" ]; then + stderr_content=$(cat "${result_file}.stderr") + fi + + # Store results + echo "exit_code=$exit_code" > "$result_file" + echo "stdout=$output" >> "$result_file" + echo "stderr=$stderr_content" >> "$result_file" + + # Report + if [ $exit_code -eq 0 ]; then + echo -e " ${GREEN}[PASS]${NC} $filename" + PASSED=$((PASSED + 1)) + else + echo -e " ${RED}[FAIL]${NC} $filename (exit code: $exit_code)" + if [ -n "$stderr_content" ]; then + # Show first 3 lines of stderr + echo "$stderr_content" | head -3 | while IFS= read -r line; do + echo -e " ${RED}$line${NC}" + done + fi + FAILED=$((FAILED + 1)) + fi +done + +echo "" +echo -e "${CYAN}=== Results ===${NC}" +echo -e " Total: $TOTAL" +echo -e " ${GREEN}Passed: $PASSED${NC}" +echo -e " ${RED}Failed: $FAILED${NC}" +echo -e " ${YELLOW}Skipped: $SKIPPED${NC}" +echo "" + +if [ $FAILED -gt 0 ]; then + echo -e "${YELLOW}--- Failed script details ---${NC}" + for result_file in "$RESULTS_DIR"/*.sh.result; do + [ -f "$result_file" ] || continue + exit_code=$(grep "^exit_code=" "$result_file" | cut -d= -f2) + if [ "$exit_code" != "0" ]; then + filename="$(basename "$result_file" .result)" + echo "" + echo -e " ${RED}$filename${NC}:" + echo " Preprocessed script: ${RESULTS_DIR}/${filename}.preprocessed" + if [ -f "${result_file}.stderr" ]; then + echo " Stderr:" + cat "${result_file}.stderr" | head -10 | sed 's/^/ /' + fi + fi + done + echo "" + exit 1 +else + echo -e "${GREEN}All activation scripts passed!${NC}" + exit 0 +fi From 33879281562a524e264d7ba2f4dc614a9d0f1585 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 20 Mar 2026 08:27:58 +0000 Subject: [PATCH 2/3] feat: add bash features needed for conda-forge activation scripts Implement multiple bash features required to run conda-forge activation scripts: - `function name() {}` syntax (bash keyword-style function definition) - `[[ ]]` as standalone command (not just in if/while/until) - `||` and `&&` inside `[[ ]]` conditional expressions - `${!var}` indirect variable expansion - `${var%%pattern}` / `${var%pattern}` suffix removal (longest/shortest) - `${var##pattern}` / `${var#pattern}` prefix removal (longest/shortest) - `${var+word}` / `${var-word}` check-set/check-unset modifiers - `if compound_list; then` (POSIX-compliant if condition as command list) These features enable the shell to parse and execute activation scripts from go-activation, rust-activation, flang-activation, and the simpler compiler activation feedstocks. All 68 tests pass with 0 ignored. https://claude.ai/code/session_0178vMPxzmTVt9Kso1zkYjeN --- crates/deno_task_shell/src/grammar.pest | 59 ++- crates/deno_task_shell/src/parser.rs | 245 +++++++++++- crates/deno_task_shell/src/shell/command.rs | 1 + crates/deno_task_shell/src/shell/execute.rs | 406 ++++++++++++++++---- crates/tests/src/conda_activation_tests.rs | 92 ++++- 5 files changed, 706 insertions(+), 97 deletions(-) diff --git a/crates/deno_task_shell/src/grammar.pest b/crates/deno_task_shell/src/grammar.pest index 347f8da..a37f606 100644 --- a/crates/deno_task_shell/src/grammar.pest +++ b/crates/deno_task_shell/src/grammar.pest @@ -84,8 +84,11 @@ PARAMETER_ESCAPE_CHAR = ${ "\\" ~ "$" | "$" ~ !"(" ~ !"{" ~ !VARIABLE ~ !SPECIAL UNQUOTED_CHAR = ${ ("\\" ~ " ") | !("]]" | "[[" | "(" | ")" | "<" | ">" | "|" | "&" | ";" | "\"" | "'" | "$") ~ ANY } QUOTED_CHAR = ${ !"\"" ~ ANY } +INDIRECT_VARIABLE = ${ "!" ~ VARIABLE } + VARIABLE_EXPANSION = ${ "$" ~ ( + "{" ~ INDIRECT_VARIABLE ~ VARIABLE_MODIFIER? ~ "}" | "{" ~ VARIABLE ~ VARIABLE_MODIFIER? ~ "}" | SPECIAL_PARAM | VARIABLE @@ -120,13 +123,38 @@ VARIABLE_MODIFIER = _{ VAR_DEFAULT_VALUE | VAR_ASSIGN_DEFAULT | VAR_ALTERNATE_VALUE | - VAR_SUBSTRING + VAR_SUBSTRING | + VAR_LONGEST_SUFFIX_REMOVE | + VAR_SHORTEST_SUFFIX_REMOVE | + VAR_LONGEST_PREFIX_REMOVE | + VAR_SHORTEST_PREFIX_REMOVE | + VAR_CHECK_UNSET | + VAR_CHECK_SET } VAR_DEFAULT_VALUE = !{ ":-" ~ PARAMETER_PENDING_WORD? } VAR_ASSIGN_DEFAULT = !{ ":=" ~ PARAMETER_PENDING_WORD } VAR_ALTERNATE_VALUE = !{ ":+" ~ PARAMETER_PENDING_WORD } -VAR_SUBSTRING = !{ ":" ~ PARAMETER_PENDING_WORD ~ (":" ~ PARAMETER_PENDING_WORD)? } +VAR_SUBSTRING = !{ ":" ~ PARAMETER_PENDING_WORD ~ (":" ~ PARAMETER_PENDING_WORD)? } +VAR_LONGEST_SUFFIX_REMOVE = !{ "%%" ~ PATTERN_PENDING_WORD } +VAR_SHORTEST_SUFFIX_REMOVE = !{ "%" ~ PATTERN_PENDING_WORD } +VAR_LONGEST_PREFIX_REMOVE = !{ "##" ~ PATTERN_PENDING_WORD } +VAR_SHORTEST_PREFIX_REMOVE = !{ "#" ~ PATTERN_PENDING_WORD } +VAR_CHECK_UNSET = !{ "-" ~ PARAMETER_PENDING_WORD? } +VAR_CHECK_SET = !{ "+" ~ PARAMETER_PENDING_WORD? } + +// Like PARAMETER_PENDING_WORD but allows ":" in patterns +PATTERN_PENDING_WORD = ${ + ( !"}" ~ ( + EXIT_STATUS | + PARAMETER_ESCAPE_CHAR | + "$" ~ ARITHMETIC_EXPRESSION | + SUB_COMMAND | + VARIABLE_EXPANSION | + QUOTED_WORD | + QUOTED_CHAR + ))+ + } TILDE_PREFIX = ${ "~" ~ (!(OPERATOR | WHITESPACE | NEWLINE | "/") ~ ( @@ -196,9 +224,11 @@ In = _{ "in" } Stdout = ${ "|" ~ !"|" ~ !"&"} StdoutStderr = { "|&" } +Function = _{ "function" } + RESERVED_WORD = _{ (If | Then | Else | Elif | Fi | Done | Do | - Case | Esac | While | Until | For | + Case | Esac | While | Until | For | Function | Lbrace | Rbrace | Bang | In | StdoutStderr | Stdout) ~ &(WHITESPACE | NEWLINE | EOI) } @@ -224,7 +254,8 @@ compound_command = { case_clause | if_clause | while_clause | - until_clause + until_clause | + conditional_expression } ARITHMETIC_EXPRESSION = !{ "((" ~ arithmetic_sequence ~ "))" } @@ -350,19 +381,26 @@ pattern = !{ } if_clause = !{ - If ~ conditional_expression ~ + If ~ if_condition ~ linebreak ~ Then ~ linebreak ~ complete_command ~ linebreak ~ else_part? ~ linebreak ~ Fi } else_part = !{ - Elif ~ conditional_expression ~ linebreak ~ Then ~ complete_command ~ linebreak ~ else_part? | + Elif ~ if_condition ~ linebreak ~ Then ~ complete_command ~ linebreak ~ else_part? | Else ~ linebreak ~ complete_command } +// The condition of an if/elif can be either a conditional expression or +// a compound list (e.g., `if [ ... ] && [ ... ]; then` or `if command; then`). +if_condition = !{ compound_list } + +condition_inner = !{ unary_conditional_expression | binary_conditional_expression | UNQUOTED_PENDING_WORD } +condition_chain_op = { "||" | "&&" } + conditional_expression = !{ - ("[[" ~ (unary_conditional_expression | binary_conditional_expression | UNQUOTED_PENDING_WORD) ~ "]]" ~ ";"?) | - ("[" ~ (unary_conditional_expression | binary_conditional_expression | UNQUOTED_PENDING_WORD) ~ "]" ~ ";"?) | + ("[[" ~ condition_inner ~ (condition_chain_op ~ condition_inner)* ~ "]]" ~ ";"?) | + ("[" ~ (unary_conditional_expression | binary_conditional_expression | UNQUOTED_PENDING_WORD) ~ "]" ~ ";"?) | ("test" ~ (unary_conditional_expression | binary_conditional_expression | UNQUOTED_PENDING_WORD) ~ ";"?) } @@ -401,10 +439,13 @@ binary_posix_conditional_op = !{ "-eq" | "-ne" | "-lt" | "-le" | "-gt" | "-ge" } +// Negation operator for [ ! expr ] +condition_negation = { "!" } + while_clause = !{ While ~ conditional_expression ~ do_group } until_clause = !{ Until ~ conditional_expression ~ do_group } -function_definition = !{ fname ~ "(" ~ ")" ~ linebreak ~ function_body } +function_definition = !{ (Function ~ fname ~ ("(" ~ ")")? | fname ~ "(" ~ ")") ~ linebreak ~ function_body } function_body = !{ compound_command ~ redirect_list? } fname = @{ RESERVED_WORD | NAME | ASSIGNMENT_WORD | UNQUOTED_PENDING_WORD } diff --git a/crates/deno_task_shell/src/parser.rs b/crates/deno_task_shell/src/parser.rs index c50bf18..240a576 100644 --- a/crates/deno_task_shell/src/parser.rs +++ b/crates/deno_task_shell/src/parser.rs @@ -172,6 +172,8 @@ pub enum CommandInner { ArithmeticExpression(Arithmetic), #[error("Invalid function definition")] Function(FunctionDefinition), + #[error("Invalid condition expression")] + ConditionExpression(Condition), } impl From for Sequence { @@ -226,7 +228,7 @@ impl From for Sequence { #[derive(Debug, PartialEq, Eq, Clone, Error)] #[error("Invalid if clause")] pub struct IfClause { - pub condition: Condition, + pub condition: SequentialList, pub then_body: SequentialList, pub else_part: Option, } @@ -300,6 +302,8 @@ pub enum ConditionInner { op: Option, right: Word, }, + LogicalOr(Box, Box), + LogicalAnd(Box, Box), } #[cfg_attr(feature = "serialization", derive(serde::Serialize))] @@ -427,6 +431,18 @@ pub enum VariableModifier { DefaultValue(Word), AssignDefault(Word), AlternateValue(Word), + /// ${var%%pattern} - Remove longest matching suffix + LongestSuffixRemove(Word), + /// ${var%pattern} - Remove shortest matching suffix + ShortestSuffixRemove(Word), + /// ${var##pattern} - Remove longest matching prefix + LongestPrefixRemove(Word), + /// ${var#pattern} - Remove shortest matching prefix + ShortestPrefixRemove(Word), + /// ${var+word} - If var is set (even if empty), substitute word + CheckSet(Word), + /// ${var-word} - If var is unset, substitute word + CheckUnset(Word), } #[cfg_attr(feature = "serialization", derive(serde::Serialize))] @@ -452,7 +468,9 @@ pub enum WordPart { #[error("Invalid text")] Text(String), #[error("Invalid variable")] - Variable(String, Option>), + /// Variable(name, modifier, indirect) + /// When indirect is true, the variable name is first resolved to get the actual variable name. + Variable(String, Option>, bool), #[error("Invalid command")] Command(SequentialList), #[error("Invalid quoted string")] @@ -1139,6 +1157,13 @@ fn parse_compound_command(pair: Pair) -> Result { redirect: None, }) } + Rule::conditional_expression => { + let condition = parse_conditional_expression(inner)?; + Ok(Command { + inner: CommandInner::ConditionExpression(condition), + redirect: None, + }) + } _ => Err(miette!( "Unexpected rule in compound_command: {:?}", inner.as_rule() @@ -1218,12 +1243,20 @@ fn parse_function_definition(pair: Pair) -> Result { }) } +fn parse_if_condition(pair: Pair) -> Result { + let mut items = Vec::new(); + for inner in pair.into_inner() { + parse_compound_list(inner, &mut items)?; + } + Ok(SequentialList { items }) +} + fn parse_if_clause(pair: Pair) -> Result { let mut inner = pair.into_inner(); let condition = inner .next() .ok_or_else(|| miette!("Expected condition after If"))?; - let condition = parse_conditional_expression(condition)?; + let condition = parse_if_condition(condition)?; let then_body_pair = inner .next() @@ -1298,7 +1331,7 @@ fn parse_else_part(pair: Pair) -> Result { let condition = inner .next() .ok_or_else(|| miette!("Expected condition after Elif"))?; - let condition = parse_conditional_expression(condition)?; + let condition = parse_if_condition(condition)?; let then_body_pair = inner .next() @@ -1330,11 +1363,11 @@ fn parse_else_part(pair: Pair) -> Result { } } -fn parse_conditional_expression(pair: Pair) -> Result { +fn parse_condition_inner(pair: Pair) -> Result { let inner = pair .into_inner() .next() - .ok_or_else(|| miette!("Expected conditional expression content"))?; + .ok_or_else(|| miette!("Expected condition_inner content"))?; match inner.as_rule() { Rule::unary_conditional_expression => { @@ -1344,12 +1377,76 @@ fn parse_conditional_expression(pair: Pair) -> Result { parse_binary_conditional_expression(inner) } _ => Err(miette!( - "Unexpected rule in conditional expression: {:?}", + "Unexpected rule in condition_inner: {:?}", inner.as_rule() )), } } +fn parse_conditional_expression(pair: Pair) -> Result { + let mut inner = pair.into_inner(); + let first = inner + .next() + .ok_or_else(|| miette!("Expected conditional expression content"))?; + + let mut result = match first.as_rule() { + Rule::condition_inner => parse_condition_inner(first)?, + Rule::unary_conditional_expression => { + parse_unary_conditional_expression(first)? + } + Rule::binary_conditional_expression => { + parse_binary_conditional_expression(first)? + } + _ => { + return Err(miette!( + "Unexpected rule in conditional expression: {:?}", + first.as_rule() + )) + } + }; + + // Handle chained || and && operators + while let Some(op_pair) = inner.next() { + if op_pair.as_rule() != Rule::condition_chain_op { + continue; + } + let op_str = op_pair.as_str(); + let next = inner + .next() + .ok_or_else(|| miette!("Expected condition after logical operator"))?; + let right = match next.as_rule() { + Rule::condition_inner => parse_condition_inner(next)?, + Rule::unary_conditional_expression => { + parse_unary_conditional_expression(next)? + } + Rule::binary_conditional_expression => { + parse_binary_conditional_expression(next)? + } + _ => { + return Err(miette!( + "Unexpected rule after condition chain op: {:?}", + next.as_rule() + )) + } + }; + result = Condition { + condition_inner: if op_str == "||" { + ConditionInner::LogicalOr( + Box::new(result), + Box::new(right), + ) + } else { + ConditionInner::LogicalAnd( + Box::new(result), + Box::new(right), + ) + }, + }; + } + + Ok(result) +} + fn parse_unary_conditional_expression(pair: Pair) -> Result { let mut inner = pair.into_inner(); let operator = inner.next().ok_or_else(|| miette!("Expected operator"))?; @@ -1586,6 +1683,7 @@ fn parse_word(pair: Pair) -> Result { Rule::VARIABLE => parts.push(WordPart::Variable( part.as_str().to_string(), None, + false, )), Rule::UNQUOTED_CHAR => { if let Some(WordPart::Text(ref mut text)) = @@ -1611,6 +1709,11 @@ fn parse_word(pair: Pair) -> Result { parse_arithmetic_expression(part)?; parts.push(WordPart::Arithmetic(arithmetic_expression)); } + Rule::VARIABLE_EXPANSION => { + let variable_expansion = + parse_variable_expansion(part)?; + parts.push(variable_expansion); + } _ => { return Err(miette!( "Unexpected rule in FILE_NAME_PENDING_WORD: {:?}", @@ -1685,6 +1788,79 @@ fn parse_word(pair: Pair) -> Result { } } } + Rule::PATTERN_PENDING_WORD => { + // Same as PARAMETER_PENDING_WORD but allows ":" + for part in pair.into_inner() { + match part.as_rule() { + Rule::PARAMETER_ESCAPE_CHAR => { + let mut chars = part.as_str().chars(); + let mut escaped_char = String::new(); + if let Some(c) = chars.next() { + match c { + '\\' => { + let next_char = + chars.next().unwrap_or('\0'); + escaped_char.push(next_char); + } + _ => { + escaped_char.push(c); + break; + } + } + } + if let Some(WordPart::Text(ref mut text)) = + parts.last_mut() + { + text.push_str(&escaped_char); + } else { + parts.push(WordPart::Text(escaped_char)); + } + } + Rule::VARIABLE_EXPANSION => { + let variable_expansion = + parse_variable_expansion(part)?; + parts.push(variable_expansion); + } + Rule::QUOTED_WORD => { + let quoted = parse_quoted_word(part)?; + parts.push(quoted); + } + Rule::ARITHMETIC_EXPRESSION => { + let arithmetic_expression = + parse_arithmetic_expression(part)?; + parts.push(WordPart::Arithmetic( + arithmetic_expression, + )); + } + Rule::SUB_COMMAND => { + let command = parse_complete_command( + part.into_inner().next().unwrap(), + )?; + parts.push(WordPart::Command(command)); + } + Rule::QUOTED_CHAR => { + if let Some(WordPart::Text(ref mut s)) = + parts.last_mut() + { + s.push_str(part.as_str()); + } else { + parts.push(WordPart::Text( + part.as_str().to_string(), + )); + } + } + Rule::EXIT_STATUS => { + parts.push(WordPart::ExitStatus); + } + _ => { + return Err(miette!( + "Unexpected rule in PATTERN_PENDING_WORD: {:?}", + part.as_rule() + )); + } + } + } + } // This is the bare _name_ of a variable, not a variable expansion Rule::VARIABLE => parts.push(WordPart::Text(pair.as_str().to_string())), _ => { @@ -1969,7 +2145,17 @@ fn parse_variable_expansion(part: Pair) -> Result { let variable = inner .next() .ok_or_else(|| miette!("Expected variable name"))?; - let variable_name = variable.as_str().to_string(); + let indirect = variable.as_rule() == Rule::INDIRECT_VARIABLE; + let variable_name = if indirect { + // INDIRECT_VARIABLE is "!" ~ VARIABLE, extract just the variable name + variable + .into_inner() + .next() + .map(|v| v.as_str().to_string()) + .unwrap_or_default() + } else { + variable.as_str().to_string() + }; let modifier = inner.next(); let parsed_modifier = if let Some(modifier) = modifier { @@ -2011,6 +2197,46 @@ fn parse_variable_expansion(part: Pair) -> Result { value, )?))) } + Rule::VAR_LONGEST_SUFFIX_REMOVE => { + let pattern = modifier.into_inner().next().unwrap(); + Some(Box::new(VariableModifier::LongestSuffixRemove( + parse_word(pattern)?, + ))) + } + Rule::VAR_SHORTEST_SUFFIX_REMOVE => { + let pattern = modifier.into_inner().next().unwrap(); + Some(Box::new(VariableModifier::ShortestSuffixRemove( + parse_word(pattern)?, + ))) + } + Rule::VAR_LONGEST_PREFIX_REMOVE => { + let pattern = modifier.into_inner().next().unwrap(); + Some(Box::new(VariableModifier::LongestPrefixRemove( + parse_word(pattern)?, + ))) + } + Rule::VAR_SHORTEST_PREFIX_REMOVE => { + let pattern = modifier.into_inner().next().unwrap(); + Some(Box::new(VariableModifier::ShortestPrefixRemove( + parse_word(pattern)?, + ))) + } + Rule::VAR_CHECK_SET => { + let value = if let Some(val) = modifier.into_inner().next() { + parse_word(val)? + } else { + Word::new_empty() + }; + Some(Box::new(VariableModifier::CheckSet(value))) + } + Rule::VAR_CHECK_UNSET => { + let value = if let Some(val) = modifier.into_inner().next() { + parse_word(val)? + } else { + Word::new_empty() + }; + Some(Box::new(VariableModifier::CheckUnset(value))) + } _ => { return Err(miette!( "Unexpected rule in variable expansion modifier: {:?}", @@ -2021,7 +2247,7 @@ fn parse_variable_expansion(part: Pair) -> Result { } else { None }; - Ok(WordPart::Variable(variable_name, parsed_modifier)) + Ok(WordPart::Variable(variable_name, parsed_modifier, indirect)) } fn parse_brace_expansion(pair: Pair) -> Result { @@ -2573,6 +2799,7 @@ mod test { Word(vec![WordPart::Variable( "MY_ENV".to_string(), None, + false, )]), ], } diff --git a/crates/deno_task_shell/src/shell/command.rs b/crates/deno_task_shell/src/shell/command.rs index 7cc47aa..82083db 100644 --- a/crates/deno_task_shell/src/shell/command.rs +++ b/crates/deno_task_shell/src/shell/command.rs @@ -206,6 +206,7 @@ async fn parse_shebang_args( CommandInner::ArithmeticExpression(_) => return err_unsupported(text), CommandInner::Case(_) => return err_unsupported(text), CommandInner::Function(_) => return err_unsupported(text), + CommandInner::ConditionExpression(_) => return err_unsupported(text), }; if !cmd.env_vars.is_empty() { return err_unsupported(text); diff --git a/crates/deno_task_shell/src/shell/execute.rs b/crates/deno_task_shell/src/shell/execute.rs index 4ef6613..964ff88 100644 --- a/crates/deno_task_shell/src/shell/execute.rs +++ b/crates/deno_task_shell/src/shell/execute.rs @@ -660,6 +660,27 @@ async fn execute_command( } } } + CommandInner::ConditionExpression(condition) => { + // Standalone conditional expression like [[ -d $DIR ]] or [ -f file ] + match evaluate_condition( + condition, + &mut state, + stdin, + stderr.clone(), + ) + .await + { + Ok(condition_result) => { + changes.extend(condition_result.changes); + let exit_code = if condition_result.value { 0 } else { 1 }; + ExecuteResult::Continue(exit_code, changes, Vec::new()) + } + Err(e) => { + let _ = stderr.write_line(&e.to_string()); + ExecuteResult::Continue(2, changes, Vec::new()) + } + } + } } } @@ -1237,7 +1258,7 @@ async fn execute_if_clause( state: &mut ShellState, stdin: ShellPipeReader, stdout: ShellPipeWriter, - mut stderr: ShellPipeWriter, + stderr: ShellPipeWriter, ) -> ExecuteResult { let mut current_condition = if_clause.condition; let mut current_body = if_clause.then_body; @@ -1245,86 +1266,87 @@ async fn execute_if_clause( let mut changes = Vec::new(); loop { - let condition_result = evaluate_condition( + // Execute the condition as a compound_list and check exit code. + let condition_result = execute_sequential_list( current_condition, - state, + state.with_child_token(), stdin.clone(), + ShellPipeWriter::null(), stderr.clone(), + AsyncCommandBehavior::Wait, ) .await; - match condition_result { - Ok(ConditionalResult { - value: true, - changes: env_changes, - }) => { - changes.extend(env_changes); - let exec_result = execute_sequential_list( - current_body, - state.clone(), - stdin, - stdout, - stderr, - AsyncCommandBehavior::Yield, - ) - .await; - match exec_result { - ExecuteResult::Exit(code, env_changes, handles) => { - changes.extend(env_changes); - return ExecuteResult::Exit(code, changes, handles); - } - ExecuteResult::Continue(code, env_changes, handles) => { - changes.extend(env_changes); - return ExecuteResult::Continue(code, changes, handles); - } + + let (condition_exit_code, env_changes) = match condition_result { + ExecuteResult::Exit(code, env_changes, _) => (code, env_changes), + ExecuteResult::Continue(code, env_changes, _) => { + (code, env_changes) + } + }; + state.apply_changes(&env_changes); + changes.extend(env_changes); + + if condition_exit_code == 0 { + // Condition succeeded — execute then body + let exec_result = execute_sequential_list( + current_body, + state.clone(), + stdin, + stdout, + stderr, + AsyncCommandBehavior::Yield, + ) + .await; + match exec_result { + ExecuteResult::Exit(code, env_changes, handles) => { + changes.extend(env_changes); + return ExecuteResult::Exit(code, changes, handles); + } + ExecuteResult::Continue(code, env_changes, handles) => { + changes.extend(env_changes); + return ExecuteResult::Continue(code, changes, handles); } } - Ok(ConditionalResult { - value: false, - changes: env_changes, - }) => { - changes.extend(env_changes); - match current_else { - Some(ElsePart::Elif(elif_clause)) => { - current_condition = elif_clause.condition; - current_body = elif_clause.then_body; - current_else = elif_clause.else_part; - } - Some(ElsePart::Else(else_body)) => { - let exec_result = execute_sequential_list( - else_body, - state.clone(), - stdin, - stdout, - stderr, - AsyncCommandBehavior::Yield, - ) - .await; - match exec_result { - ExecuteResult::Exit(code, env_changes, handles) => { - changes.extend(env_changes); - return ExecuteResult::Exit( - code, changes, handles, - ); - } - ExecuteResult::Continue( - code, - env_changes, - handles, - ) => { - changes.extend(env_changes); - return ExecuteResult::Continue( - code, changes, handles, - ); - } + } else { + // Condition failed — check else/elif + match current_else { + Some(ElsePart::Elif(elif_clause)) => { + current_condition = elif_clause.condition; + current_body = elif_clause.then_body; + current_else = elif_clause.else_part; + } + Some(ElsePart::Else(else_body)) => { + let exec_result = execute_sequential_list( + else_body, + state.clone(), + stdin, + stdout, + stderr, + AsyncCommandBehavior::Yield, + ) + .await; + match exec_result { + ExecuteResult::Exit(code, env_changes, handles) => { + changes.extend(env_changes); + return ExecuteResult::Exit( + code, changes, handles, + ); + } + ExecuteResult::Continue( + code, + env_changes, + handles, + ) => { + changes.extend(env_changes); + return ExecuteResult::Continue( + code, changes, handles, + ); } - } - None => { - return ExecuteResult::Continue(0, changes, Vec::new()); } } - } - Err(err) => { - return err.into_exit_code(&mut stderr); + None => { + return ExecuteResult::Continue(0, changes, Vec::new()); + } } } } @@ -1666,6 +1688,64 @@ async fn evaluate_condition( } .into()) } + ConditionInner::LogicalOr(left, right) => { + let left_result = Box::pin(evaluate_condition( + *left, + state, + stdin.clone(), + stderr.clone(), + )) + .await?; + changes.extend(left_result.changes); + if left_result.value { + Ok(ConditionalResult { + value: true, + changes, + }) + } else { + let right_result = Box::pin(evaluate_condition( + *right, + state, + stdin.clone(), + stderr.clone(), + )) + .await?; + changes.extend(right_result.changes); + Ok(ConditionalResult { + value: right_result.value, + changes, + }) + } + } + ConditionInner::LogicalAnd(left, right) => { + let left_result = Box::pin(evaluate_condition( + *left, + state, + stdin.clone(), + stderr.clone(), + )) + .await?; + changes.extend(left_result.changes); + if !left_result.value { + Ok(ConditionalResult { + value: false, + changes, + }) + } else { + let right_result = Box::pin(evaluate_condition( + *right, + state, + stdin.clone(), + stderr.clone(), + )) + .await?; + changes.extend(right_result.changes); + Ok(ConditionalResult { + value: right_result.value, + changes, + }) + } + } } } @@ -2139,8 +2219,171 @@ impl VariableModifier { Ok((v.value.into(), Some(v.changes))) } } + VariableModifier::LongestSuffixRemove(pattern) => { + let val = state.get_var(name).cloned().unwrap_or_default(); + let pat = evaluate_word_no_glob( + pattern.clone(), + state, + stdin, + stderr, + ) + .await + .into_diagnostic()?; + let result = + remove_suffix(&val, &pat.value, true); + Ok((result.into(), Some(pat.changes))) + } + VariableModifier::ShortestSuffixRemove(pattern) => { + let val = state.get_var(name).cloned().unwrap_or_default(); + let pat = evaluate_word_no_glob( + pattern.clone(), + state, + stdin, + stderr, + ) + .await + .into_diagnostic()?; + let result = + remove_suffix(&val, &pat.value, false); + Ok((result.into(), Some(pat.changes))) + } + VariableModifier::LongestPrefixRemove(pattern) => { + let val = state.get_var(name).cloned().unwrap_or_default(); + let pat = evaluate_word_no_glob( + pattern.clone(), + state, + stdin, + stderr, + ) + .await + .into_diagnostic()?; + let result = + remove_prefix(&val, &pat.value, true); + Ok((result.into(), Some(pat.changes))) + } + VariableModifier::ShortestPrefixRemove(pattern) => { + let val = state.get_var(name).cloned().unwrap_or_default(); + let pat = evaluate_word_no_glob( + pattern.clone(), + state, + stdin, + stderr, + ) + .await + .into_diagnostic()?; + let result = + remove_prefix(&val, &pat.value, false); + Ok((result.into(), Some(pat.changes))) + } + VariableModifier::CheckSet(word) => { + // ${var+word}: If var is SET (even if empty), substitute word + if state.get_var(name).is_some() { + let v = evaluate_word( + word.clone(), + state, + stdin, + stderr, + ) + .await + .into_diagnostic()?; + Ok((v.value.into(), Some(v.changes))) + } else { + Ok(("".to_string().into(), None)) + } + } + VariableModifier::CheckUnset(word) => { + // ${var-word}: If var is UNSET, substitute word + match state.get_var(name) { + Some(v) => Ok((v.clone().into(), None)), + None => { + let v = evaluate_word( + word.clone(), + state, + stdin, + stderr, + ) + .await + .into_diagnostic()?; + Ok((v.value.into(), Some(v.changes))) + } + } + } + } + } +} + +/// Remove a suffix pattern from a string. +/// Uses glob-style pattern matching where `*` matches any sequence of characters. +fn remove_suffix(value: &str, pattern: &str, longest: bool) -> String { + let chars: Vec = value.chars().collect(); + if longest { + // Try removing from the beginning (longest match) + for i in 0..=chars.len() { + let suffix: String = chars[i..].iter().collect(); + if glob_match(pattern, &suffix) { + return chars[..i].iter().collect(); + } + } + } else { + // Try removing from the end (shortest match) + for i in (0..=chars.len()).rev() { + let suffix: String = chars[i..].iter().collect(); + if glob_match(pattern, &suffix) { + return chars[..i].iter().collect(); + } } } + value.to_string() +} + +/// Remove a prefix pattern from a string. +/// Uses glob-style pattern matching where `*` matches any sequence of characters. +fn remove_prefix(value: &str, pattern: &str, longest: bool) -> String { + let chars: Vec = value.chars().collect(); + if longest { + // Try removing from the end (longest match) + for i in (0..=chars.len()).rev() { + let prefix: String = chars[..i].iter().collect(); + if glob_match(pattern, &prefix) { + return chars[i..].iter().collect(); + } + } + } else { + // Try removing from the beginning (shortest match) + for i in 0..=chars.len() { + let prefix: String = chars[..i].iter().collect(); + if glob_match(pattern, &prefix) { + return chars[i..].iter().collect(); + } + } + } + value.to_string() +} + +/// Simple glob matching that supports `*` (match any sequence) and `?` (match single char). +fn glob_match(pattern: &str, text: &str) -> bool { + let pat: Vec = pattern.chars().collect(); + let txt: Vec = text.chars().collect(); + glob_match_inner(&pat, &txt) +} + +fn glob_match_inner(pattern: &[char], text: &[char]) -> bool { + match (pattern.first(), text.first()) { + (None, None) => true, + (Some('*'), _) => { + // Try matching * with zero chars, or consuming one char from text + glob_match_inner(&pattern[1..], text) + || (!text.is_empty() + && glob_match_inner(pattern, &text[1..])) + } + (Some('?'), Some(_)) => { + glob_match_inner(&pattern[1..], &text[1..]) + } + (Some(p), Some(t)) if p == t => { + glob_match_inner(&pattern[1..], &text[1..]) + } + _ => false, + } } /// Whether to evaluate glob patterns in a word @@ -2395,11 +2638,21 @@ fn evaluate_word_parts( current_text.push(TextPart::Text(text)); continue; } - WordPart::Variable(name, modifier) => { + WordPart::Variable(name, modifier, indirect) => { + // For indirect expansion (${!var}), first resolve + // the variable name from the value of `name` + let resolved_name = if indirect { + state + .get_var(&name) + .map(|v| v.to_string()) + .unwrap_or_default() + } else { + name.clone() + }; if let Some(modifier) = modifier { let (text, env_changes) = modifier .apply( - &name, + &resolved_name, state, stdin.clone(), stderr.clone(), @@ -2409,8 +2662,9 @@ fn evaluate_word_parts( result.with_changes(env_changes); } Ok(Some(text)) - } else if let Some(val) = - state.get_var(&name).map(|v| v.to_string()) + } else if let Some(val) = state + .get_var(&resolved_name) + .map(|v| v.to_string()) { let t: Text = Text::new( [OtherText(val.clone().to_string())] @@ -2420,7 +2674,7 @@ fn evaluate_word_parts( } else { Err(miette::miette!( "Undefined variable: {}", - name + resolved_name )) } } diff --git a/crates/tests/src/conda_activation_tests.rs b/crates/tests/src/conda_activation_tests.rs index c03b3f4..4ec9a4e 100644 --- a/crates/tests/src/conda_activation_tests.rs +++ b/crates/tests/src/conda_activation_tests.rs @@ -118,7 +118,6 @@ async fn conda_flang_deactivate() { // === Rust activation (uses [[ ]] and mkdir) === #[tokio::test] -#[ignore = "shell does not yet support [[ ]] double-bracket conditionals"] async fn conda_rust_activate() { // rust-activation-feedstock: recipe/activate.sh (preprocessed) // This tests [[ ]] conditionals, ${VAR:-default}, and complex variable exports @@ -197,7 +196,6 @@ echo "$(_get_sourced_filename)" } #[tokio::test] -#[ignore = "shell does not yet support `function name() {}` syntax (bash keyword-style function definition)"] async fn conda_clang_activate_function_keyword() { // clang-compiler-activation-feedstock uses `function name() {` syntax run_conda_script( @@ -215,7 +213,6 @@ echo "$(_get_sourced_filename)" } #[tokio::test] -#[ignore = "shell does not yet support ${!var} indirect variable expansion"] async fn conda_tc_activation_pattern() { // The _tc_activation function pattern from ctng/clang compiler activation scripts. // Tests local variables, for loops, if/elif/else, export, and unset. @@ -334,3 +331,92 @@ echo "restored CC=$CC" ) .await; } + +#[tokio::test] +async fn conda_pattern_suffix_remove() { + // Test ${var%%pattern} - simplest case + TestBuilder::new() + .command(r#"val="hello.world.txt"; echo "${val%%.*}""#) + .assert_stdout("hello\n") + .run() + .await; + + // Test ${var%%,*} + TestBuilder::new() + .command(r#"val="CC,gcc"; echo "${val%%,*}""#) + .assert_stdout("CC\n") + .run() + .await; +} + +#[tokio::test] +async fn conda_pattern_prefix_remove() { + // Test ${var#pattern} and ${var##pattern} + TestBuilder::new() + .command(r#"val="CC,gcc"; echo "${val#*,}""#) + .assert_stdout("gcc\n") + .run() + .await; +} + +#[tokio::test] +async fn conda_check_set_modifier() { + // Test ${var+word} - substitute word if var is set + TestBuilder::new() + .command(r#"MY_VAR="hello"; echo "${MY_VAR+x}""#) + .assert_stdout("x\n") + .run() + .await; + + // Unset var should produce empty + TestBuilder::new() + .command(r#"unset NONEXISTENT_VAR; echo "${NONEXISTENT_VAR+x}""#) + .assert_stdout("\n") + .run() + .await; +} + +#[tokio::test] +async fn conda_indirect_expansion() { + // Test ${!var} + TestBuilder::new() + .command(r#"MY_VAR="hello"; ref="MY_VAR"; echo "${!ref}""#) + .assert_stdout("hello\n") + .run() + .await; +} + +#[tokio::test] +async fn conda_indirect_with_check_set() { + // Test ${!var+x} - indirect + check-if-set + TestBuilder::new() + .command( + r#"MY_VAR="hello"; ref="MY_VAR"; echo "${!ref+x}""#, + ) + .assert_stdout("x\n") + .run() + .await; +} + +#[tokio::test] +async fn conda_if_compound_condition() { + // Test if with compound conditions: if [ ... ] && [ ... ]; then + TestBuilder::new() + .command("if [ 1 = 1 ] && [ 2 = 2 ]; then echo yes; else echo no; fi") + .assert_stdout("yes\n") + .run() + .await; + + TestBuilder::new() + .command("if [ 1 = 2 ] && [ 2 = 2 ]; then echo yes; else echo no; fi") + .assert_stdout("no\n") + .run() + .await; + + // Test elif + TestBuilder::new() + .command(r#"FOO=2; if [[ $FOO == 1 ]]; then echo "one"; elif [[ $FOO -eq 2 ]]; then echo "two"; fi"#) + .assert_stdout("two\n") + .run() + .await; +} From 87d285f5999ac5a2db95a356990cbc4134f6a64d Mon Sep 17 00:00:00 2001 From: Wolf Vollprecht Date: Sat, 21 Mar 2026 07:07:23 +0100 Subject: [PATCH 3/3] format --- crates/deno_task_shell/src/parser.rs | 20 +++----- crates/deno_task_shell/src/shell/execute.rs | 52 ++++++--------------- crates/tests/src/conda_activation_tests.rs | 4 +- 3 files changed, 22 insertions(+), 54 deletions(-) diff --git a/crates/deno_task_shell/src/parser.rs b/crates/deno_task_shell/src/parser.rs index 240a576..2987dbb 100644 --- a/crates/deno_task_shell/src/parser.rs +++ b/crates/deno_task_shell/src/parser.rs @@ -1411,9 +1411,9 @@ fn parse_conditional_expression(pair: Pair) -> Result { continue; } let op_str = op_pair.as_str(); - let next = inner - .next() - .ok_or_else(|| miette!("Expected condition after logical operator"))?; + let next = inner.next().ok_or_else(|| { + miette!("Expected condition after logical operator") + })?; let right = match next.as_rule() { Rule::condition_inner => parse_condition_inner(next)?, Rule::unary_conditional_expression => { @@ -1431,15 +1431,9 @@ fn parse_conditional_expression(pair: Pair) -> Result { }; result = Condition { condition_inner: if op_str == "||" { - ConditionInner::LogicalOr( - Box::new(result), - Box::new(right), - ) + ConditionInner::LogicalOr(Box::new(result), Box::new(right)) } else { - ConditionInner::LogicalAnd( - Box::new(result), - Box::new(right), - ) + ConditionInner::LogicalAnd(Box::new(result), Box::new(right)) }, }; } @@ -1828,9 +1822,7 @@ fn parse_word(pair: Pair) -> Result { Rule::ARITHMETIC_EXPRESSION => { let arithmetic_expression = parse_arithmetic_expression(part)?; - parts.push(WordPart::Arithmetic( - arithmetic_expression, - )); + parts.push(WordPart::Arithmetic(arithmetic_expression)); } Rule::SUB_COMMAND => { let command = parse_complete_command( diff --git a/crates/deno_task_shell/src/shell/execute.rs b/crates/deno_task_shell/src/shell/execute.rs index 964ff88..cd93d3e 100644 --- a/crates/deno_task_shell/src/shell/execute.rs +++ b/crates/deno_task_shell/src/shell/execute.rs @@ -1328,15 +1328,9 @@ async fn execute_if_clause( match exec_result { ExecuteResult::Exit(code, env_changes, handles) => { changes.extend(env_changes); - return ExecuteResult::Exit( - code, changes, handles, - ); + return ExecuteResult::Exit(code, changes, handles); } - ExecuteResult::Continue( - code, - env_changes, - handles, - ) => { + ExecuteResult::Continue(code, env_changes, handles) => { changes.extend(env_changes); return ExecuteResult::Continue( code, changes, handles, @@ -2229,8 +2223,7 @@ impl VariableModifier { ) .await .into_diagnostic()?; - let result = - remove_suffix(&val, &pat.value, true); + let result = remove_suffix(&val, &pat.value, true); Ok((result.into(), Some(pat.changes))) } VariableModifier::ShortestSuffixRemove(pattern) => { @@ -2243,8 +2236,7 @@ impl VariableModifier { ) .await .into_diagnostic()?; - let result = - remove_suffix(&val, &pat.value, false); + let result = remove_suffix(&val, &pat.value, false); Ok((result.into(), Some(pat.changes))) } VariableModifier::LongestPrefixRemove(pattern) => { @@ -2257,8 +2249,7 @@ impl VariableModifier { ) .await .into_diagnostic()?; - let result = - remove_prefix(&val, &pat.value, true); + let result = remove_prefix(&val, &pat.value, true); Ok((result.into(), Some(pat.changes))) } VariableModifier::ShortestPrefixRemove(pattern) => { @@ -2271,21 +2262,15 @@ impl VariableModifier { ) .await .into_diagnostic()?; - let result = - remove_prefix(&val, &pat.value, false); + let result = remove_prefix(&val, &pat.value, false); Ok((result.into(), Some(pat.changes))) } VariableModifier::CheckSet(word) => { // ${var+word}: If var is SET (even if empty), substitute word if state.get_var(name).is_some() { - let v = evaluate_word( - word.clone(), - state, - stdin, - stderr, - ) - .await - .into_diagnostic()?; + let v = evaluate_word(word.clone(), state, stdin, stderr) + .await + .into_diagnostic()?; Ok((v.value.into(), Some(v.changes))) } else { Ok(("".to_string().into(), None)) @@ -2296,14 +2281,10 @@ impl VariableModifier { match state.get_var(name) { Some(v) => Ok((v.clone().into(), None)), None => { - let v = evaluate_word( - word.clone(), - state, - stdin, - stderr, - ) - .await - .into_diagnostic()?; + let v = + evaluate_word(word.clone(), state, stdin, stderr) + .await + .into_diagnostic()?; Ok((v.value.into(), Some(v.changes))) } } @@ -2373,12 +2354,9 @@ fn glob_match_inner(pattern: &[char], text: &[char]) -> bool { (Some('*'), _) => { // Try matching * with zero chars, or consuming one char from text glob_match_inner(&pattern[1..], text) - || (!text.is_empty() - && glob_match_inner(pattern, &text[1..])) - } - (Some('?'), Some(_)) => { - glob_match_inner(&pattern[1..], &text[1..]) + || (!text.is_empty() && glob_match_inner(pattern, &text[1..])) } + (Some('?'), Some(_)) => glob_match_inner(&pattern[1..], &text[1..]), (Some(p), Some(t)) if p == t => { glob_match_inner(&pattern[1..], &text[1..]) } diff --git a/crates/tests/src/conda_activation_tests.rs b/crates/tests/src/conda_activation_tests.rs index 4ec9a4e..98a33da 100644 --- a/crates/tests/src/conda_activation_tests.rs +++ b/crates/tests/src/conda_activation_tests.rs @@ -390,9 +390,7 @@ async fn conda_indirect_expansion() { async fn conda_indirect_with_check_set() { // Test ${!var+x} - indirect + check-if-set TestBuilder::new() - .command( - r#"MY_VAR="hello"; ref="MY_VAR"; echo "${!ref+x}""#, - ) + .command(r#"MY_VAR="hello"; ref="MY_VAR"; echo "${!ref+x}""#) .assert_stdout("x\n") .run() .await;