diff --git a/Cargo.lock b/Cargo.lock index d4a87ecc0c..17c48caaaf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -264,6 +264,20 @@ dependencies = [ "rustversion", ] +[[package]] +name = "archspec" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9db67cd9cf4f56a10d2cbae6a3b552e5bd36701fd37b74a18c14a231bdf446c7" +dependencies = [ + "cfg-if", + "itertools 0.12.1", + "libc", + "serde", + "serde_json", + "sysctl", +] + [[package]] name = "arrayref" version = "0.3.9" @@ -325,6 +339,37 @@ dependencies = [ "serde_json", ] +[[package]] +name = "astral-tokio-tar" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec179a06c1769b1e42e1e2cbe74c7dcdb3d6383c838454d063eaac5bbb7ebbe5" +dependencies = [ + "filetime", + "futures-core", + "libc", + "portable-atomic", + "rustc-hash 2.1.1", + "tokio", + "tokio-stream", + "xattr", +] + +[[package]] +name = "astral_async_zip" +version = "0.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab72a761e6085828cc8f0e05ed332b2554701368c5dc54de551bfaec466518ba" +dependencies = [ + "async-compression", + "crc32fast", + "futures-lite", + "pin-project", + "thiserror 1.0.69", + "tokio", + "tokio-util", +] + [[package]] name = "async-backtrace" version = "0.2.7" @@ -360,10 +405,32 @@ checksum = "d10e4f991a553474232bc0a31799f6d24b034a84c0971d80d2e2f78b2e576e40" dependencies = [ "compression-codecs", "compression-core", + "futures-io", "pin-project-lite", "tokio", ] +[[package]] +name = "async-fd-lock" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7569377d7062165f6f7834d9cb3051974a2d141433cc201c2f94c149e993cccf" +dependencies = [ + "async-trait", + "cfg-if", + "pin-project", + "rustix 0.38.44", + "thiserror 1.0.69", + "tokio", + "windows-sys 0.52.0", +] + +[[package]] +name = "async-once-cell" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288f83726785267c6f2ef073a3d83dc3f9b81464e9f99898240cced85fce35a" + [[package]] name = "async-recursion" version = "1.1.1" @@ -375,6 +442,16 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "async-spooled-tempfile" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd9c58a1dbecfc23aa470d57e5aff60877ab1b459bf05e60e861dd18fcaa5f5" +dependencies = [ + "tempfile", + "tokio", +] + [[package]] name = "async-trait" version = "0.1.89" @@ -944,7 +1021,16 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0481a0e032742109b1133a095184ee93d88f3dc9e0d28a5d033dc77a073f44f" dependencies = [ - "bit-vec", + "bit-vec 0.7.0", +] + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec 0.8.0", ] [[package]] @@ -953,12 +1039,39 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2c54ff287cfc0a34f38a6b832ea1bd8e448a330b3e40a50859e6488bee07f22" +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + [[package]] name = "bitflags" version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + [[package]] name = "blake3" version = "1.8.3" @@ -1089,6 +1202,12 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "cache_control" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bf2a5fb3207c12b5d208ebc145f967fea5cac41a021c37417ccc31ba40f39ee" + [[package]] name = "calm_io" version = "0.1.1" @@ -1244,7 +1363,7 @@ checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" dependencies = [ "glob", "libc", - "libloading", + "libloading 0.8.9", ] [[package]] @@ -1327,6 +1446,16 @@ dependencies = [ "cc", ] +[[package]] +name = "coalesced_map" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cf5a7a58a9d5b914bddb0a3a2bd920af2be897114dc8128af022af81fc43b8b" +dependencies = [ + "dashmap 6.1.0", + "tokio", +] + [[package]] name = "color-eyre" version = "0.6.5" @@ -1417,6 +1546,7 @@ version = "0.4.36" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00828ba6fd27b45a448e57dbfe84f1029d4c9f26b368157e9a448a5f49a2ec2a" dependencies = [ + "bzip2 0.6.1", "compression-core", "flate2", "memchr", @@ -1430,6 +1560,21 @@ version = "0.4.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d" +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "configparser" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57e3272f0190c3f1584272d613719ba5fc7df7f4942fe542e63d949cf3a649b" + [[package]] name = "confique" version = "0.3.1" @@ -1666,7 +1811,7 @@ dependencies = [ "crossterm_winapi", "document-features", "parking_lot", - "rustix", + "rustix 1.1.3", "winapi", ] @@ -2226,6 +2371,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "elsa" +version = "1.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9abf33c656a7256451ebb7d0082c5a471820c31269e49d807c538c252352186e" +dependencies = [ + "stable_deref_trait", +] + [[package]] name = "encode_unicode" version = "1.0.0" @@ -2241,6 +2395,30 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "enum-as-inner" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "enum_dispatch" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa18ce2bc66555b3218614519ac839ddb759a7d6720732f979ef8d13be147ecd" +dependencies = [ + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "env_filter" version = "0.1.4" @@ -2328,6 +2506,17 @@ dependencies = [ "libc", ] +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + [[package]] name = "exec" version = "0.3.1" @@ -2365,6 +2554,17 @@ dependencies = [ "once_cell", ] +[[package]] +name = "fancy-regex" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "998b056554fbe42e03ae0e152895cd1a7e1002aec800fdc6635d20270260c46f" +dependencies = [ + "bit-set 0.8.0", + "regex-automata", + "regex-syntax 0.8.8", +] + [[package]] name = "faster-hex" version = "0.10.0" @@ -2397,6 +2597,19 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +[[package]] +name = "file_url" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81d37aab514a05a249a5b15408dc74d716f5745a2c5daf22e40a245ffd38fa84" +dependencies = [ + "itertools 0.14.0", + "percent-encoding", + "thiserror 2.0.18", + "typed-path 0.12.2", + "url", +] + [[package]] name = "filetime" version = "0.2.27" @@ -2457,6 +2670,15 @@ dependencies = [ "zlib-rs", ] +[[package]] +name = "float-cmp" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8" +dependencies = [ + "num-traits", +] + [[package]] name = "fluent" version = "0.16.1" @@ -2543,6 +2765,28 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs-err" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73fde052dbfc920003cfd2c8e2c6e6d4cc7c1091538c3a24226cec0665ab08c0" +dependencies = [ + "autocfg", + "tokio", +] + +[[package]] +name = "fs4" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8640e34b88f7652208ce9e88b1a37a2ae95227d84abec377ccd3c5cfeb141ed4" +dependencies = [ + "fs-err", + "rustix 1.1.3", + "tokio", + "windows-sys 0.59.0", +] + [[package]] name = "fs_extra" version = "1.3.0" @@ -2568,6 +2812,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futures" version = "0.3.31" @@ -2616,6 +2866,19 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + [[package]] name = "futures-macro" version = "0.3.31" @@ -2685,6 +2948,7 @@ version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ + "serde", "typenum", "version_check", "zeroize", @@ -3175,7 +3439,7 @@ dependencies = [ "itoa", "libc", "memmap2", - "rustix", + "rustix 1.1.3", "smallvec", "thiserror 2.0.18", ] @@ -3330,7 +3594,7 @@ dependencies = [ "gix-command", "gix-config-value", "parking_lot", - "rustix", + "rustix 1.1.3", "thiserror 2.0.18", ] @@ -3726,6 +3990,16 @@ dependencies = [ "tracing", ] +[[package]] +name = "halfbrown" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c7ed2f2edad8a14c8186b847909a41fbb9c3eafa44f88bd891114ed5019da09" +dependencies = [ + "hashbrown 0.16.1", + "serde", +] + [[package]] name = "hash32" version = "0.3.1" @@ -3756,6 +4030,8 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ + "allocator-api2", + "equivalent", "foldhash 0.1.5", ] @@ -3797,6 +4073,9 @@ name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +dependencies = [ + "serde", +] [[package]] name = "hkdf" @@ -3892,6 +4171,29 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "http-cache-semantics" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4311240f94cb6fe622337dc580b09ddd6ae4a891eb121dba20cf4f77ca4e7129" +dependencies = [ + "http 1.4.0", + "http-serde", + "reqwest 0.12.28", + "serde", + "time", +] + +[[package]] +name = "http-serde" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f056c8559e3757392c8d091e796416e4649d8e49e88b8d76df6c002f05027fd" +dependencies = [ + "http 1.4.0", + "serde", +] + [[package]] name = "httparse" version = "1.10.1" @@ -3919,6 +4221,12 @@ dependencies = [ "libm", ] +[[package]] +name = "humantime" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" + [[package]] name = "hyper" version = "0.14.32" @@ -4037,7 +4345,7 @@ dependencies = [ "tokio", "tower-service", "tracing", - "windows-registry", + "windows-registry 0.6.1", ] [[package]] @@ -4434,6 +4742,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.13.0" @@ -4553,6 +4870,18 @@ dependencies = [ "smallvec", ] +[[package]] +name = "json-patch" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f300e415e2134745ef75f04562dd0145405c2f7fd92065db029ac4b16b57fe90" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror 1.0.69", +] + [[package]] name = "json-syntax" version = "0.12.5" @@ -4572,6 +4901,16 @@ dependencies = [ "utf8-decode", ] +[[package]] +name = "jsonptr" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5a3cc660ba5d72bce0b3bb295bf20847ccbb40fd423f3f05b61273672e561fe" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "junction" version = "1.4.1" @@ -4609,7 +4948,16 @@ dependencies = [ ] [[package]] -name = "kstring" +name = "known-folders" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "770919970f7d2f74fea948900d35e2ef64f44129e8ae4015f59de1f0aca7c2a5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "kstring" version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "558bf9508a558512042d3095138b1f7b8fe90c5467d94f9f1da28b3731c5dbd1" @@ -4737,6 +5085,16 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "libloading" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "754ca22de805bb5744484a5b151a9e1a8e837d5dc232c2d7d8c2e3492edc8b60" +dependencies = [ + "cfg-if", + "windows-link 0.2.1", +] + [[package]] name = "libm" version = "0.2.16" @@ -4754,6 +5112,12 @@ dependencies = [ "redox_syscall 0.7.0", ] +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + [[package]] name = "linux-raw-sys" version = "0.11.0" @@ -5124,6 +5488,12 @@ dependencies = [ "petgraph", "pretty_assertions", "rand 0.9.2", + "rattler", + "rattler_conda_types", + "rattler_package_streaming", + "rattler_repodata_gateway", + "rattler_solve", + "rattler_virtual_packages", "regex", "reqwest 0.12.28", "rmcp", @@ -5346,6 +5716,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "nom-language" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2de2bc5b451bfedaef92c90b8939a8fff5770bdcc1fafd6239d086aab8fa6b29" +dependencies = [ + "nom 8.0.0", +] + [[package]] name = "nt-time" version = "0.8.1" @@ -5776,6 +6155,12 @@ dependencies = [ "unicode-width 0.2.2", ] +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.12.5" @@ -5843,6 +6228,21 @@ dependencies = [ "once_cell", ] +[[package]] +name = "path_resolver" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6a4d9af27fc7240c8270eff3eeaad11bc9ff950d37be041f3e3e7ad98d3d9db" +dependencies = [ + "ahash 0.8.12", + "fs-err", + "indexmap 2.13.0", + "itertools 0.14.0", + "proptest", + "tempfile", + "tracing", +] + [[package]] name = "pbkdf2" version = "0.12.2" @@ -6053,6 +6453,19 @@ version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f21de1852251c849a53467e0ce8b97cca9d11fd4efa3930145c5d5f02f24447" +[[package]] +name = "plist" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" +dependencies = [ + "base64 0.22.1", + "indexmap 2.13.0", + "quick-xml 0.38.4", + "serde", + "time", +] + [[package]] name = "poly1305" version = "0.8.0" @@ -6216,6 +6629,25 @@ dependencies = [ "parking_lot", ] +[[package]] +name = "proptest" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566cb3fdacef14c0737f9546df7cfeadbfbc9fef10991038bf5015d0c80532" +dependencies = [ + "bit-set 0.8.0", + "bit-vec 0.8.0", + "bitflags", + "num-traits", + "rand 0.9.2", + "rand_chacha 0.9.0", + "rand_xorshift", + "regex-syntax 0.8.8", + "rusty-fork", + "tempfile", + "unarray", +] + [[package]] name = "prost" version = "0.14.3" @@ -6302,6 +6734,24 @@ dependencies = [ "prost", ] +[[package]] +name = "purl" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60ebe4262ae91ddd28c8721111a0a6e9e58860e211fc92116c4bb85c98fd96ad" +dependencies = [ + "hex", + "percent-encoding", + "serde", + "thiserror 2.0.18", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + [[package]] name = "quick-xml" version = "0.37.5" @@ -6311,6 +6761,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "quick-xml" +version = "0.38.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +dependencies = [ + "memchr", +] + [[package]] name = "quinn" version = "0.11.9" @@ -6382,6 +6841,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rand" version = "0.8.5" @@ -6394,51 +6859,464 @@ dependencies = [ ] [[package]] -name = "rand" -version = "0.9.2" +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core 0.9.5", +] + +[[package]] +name = "rattler" +version = "0.39.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa33859aa5310222070ed0d542fbaec201fc2cb2a203e36c2d4a41d81a8ce454" +dependencies = [ + "anyhow", + "digest", + "dirs", + "filetime", + "fs-err", + "futures", + "humantime", + "indexmap 2.13.0", + "itertools 0.14.0", + "memchr", + "memmap2", + "once_cell", + "parking_lot", + "path_resolver", + "rattler_cache", + "rattler_conda_types", + "rattler_digest", + "rattler_menuinst", + "rattler_networking", + "rattler_package_streaming", + "rattler_shell", + "rayon", + "reflink-copy", + "regex", + "reqwest 0.12.28", + "reqwest-middleware", + "serde", + "serde_json", + "simple_spawn_blocking", + "smallvec", + "tempfile", + "thiserror 2.0.18", + "tokio", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "rattler_cache" +version = "0.6.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe24d25387a11f011f987e86ef85db3a5f966bdf75cc3c8bd0f4c8d44797fff" +dependencies = [ + "ahash 0.8.12", + "anyhow", + "dashmap 6.1.0", + "digest", + "dirs", + "fs-err", + "fs4", + "futures", + "itertools 0.14.0", + "parking_lot", + "rattler_conda_types", + "rattler_digest", + "rattler_networking", + "rattler_package_streaming", + "rattler_redaction", + "rayon", + "reqwest 0.12.28", + "reqwest-middleware", + "serde_json", + "simple_spawn_blocking", + "tempfile", + "thiserror 2.0.18", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "rattler_conda_types" +version = "0.43.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4195ca32c110895ec622711fe6f2cacc3560cab0b0ef075677a0c6e480c2ad22" +dependencies = [ + "ahash 0.8.12", + "chrono", + "core-foundation 0.10.1", + "dirs", + "fancy-regex", + "file_url", + "fs-err", + "glob", + "hex", + "indexmap 2.13.0", + "itertools 0.14.0", + "lazy-regex", + "memmap2", + "nom 8.0.0", + "nom-language", + "purl", + "rattler_digest", + "rattler_macros", + "rattler_redaction", + "regex", + "serde", + "serde-untagged", + "serde_json", + "serde_repr", + "serde_with", + "serde_yaml", + "simd-json", + "smallvec", + "strum", + "tempfile", + "thiserror 2.0.18", + "tracing", + "typed-path 0.12.2", + "url", +] + +[[package]] +name = "rattler_digest" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4109fd4ea54f1e34812f9db9166013325418ba92ff5693136afe681a56039fe" +dependencies = [ + "blake2", + "digest", + "generic-array", + "hex", + "md-5", + "serde", + "serde_bytes", + "serde_with", + "sha2", + "tokio", + "url", +] + +[[package]] +name = "rattler_macros" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d0d45ce3ae00333421569d2fafa4b877708a901c3cb217f8d4acfab4328df0" +dependencies = [ + "quote", + "syn 2.0.114", +] + +[[package]] +name = "rattler_menuinst" +version = "0.2.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7bbf3ac0ca5c3416ed3d95228f426cd5be045ce4554d94c89a55107cdb25205" +dependencies = [ + "chrono", + "configparser", + "dirs", + "fs-err", + "indexmap 2.13.0", + "known-folders", + "once_cell", + "plist", + "quick-xml 0.37.5", + "rattler_conda_types", + "rattler_shell", + "regex", + "serde", + "serde_json", + "sha2", + "shlex", + "tempfile", + "thiserror 2.0.18", + "tracing", + "unicode-normalization", + "which 8.0.0", + "windows 0.61.3", + "windows-registry 0.5.3", +] + +[[package]] +name = "rattler_networking" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ebcccad5b14e122a0ddd03a8d4e9810c50e17ec23d5d5354fd6394218cc6db" +dependencies = [ + "anyhow", + "async-once-cell", + "async-trait", + "base64 0.22.1", + "fs-err", + "getrandom 0.3.4", + "http 1.4.0", + "itertools 0.14.0", + "reqwest 0.12.28", + "reqwest-middleware", + "retry-policies", + "serde", + "serde_json", + "tempfile", + "thiserror 2.0.18", + "tracing", + "url", +] + +[[package]] +name = "rattler_package_streaming" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bd3dce7e4f4e2031aa809daa049bc1a5ca9e712ca202cc84f69f2b0128008e8" +dependencies = [ + "astral-tokio-tar", + "astral_async_zip", + "async-compression", + "async-spooled-tempfile", + "bzip2 0.6.1", + "chrono", + "fs-err", + "futures", + "futures-util", + "getrandom 0.2.17", + "getrandom 0.3.4", + "num_cpus", + "rattler_conda_types", + "rattler_digest", + "rattler_networking", + "rattler_redaction", + "reqwest 0.12.28", + "reqwest-middleware", + "serde_json", + "simple_spawn_blocking", + "tar", + "tempfile", + "thiserror 2.0.18", + "tokio", + "tokio-util", + "tracing", + "url", + "zip 6.0.0", + "zstd", +] + +[[package]] +name = "rattler_pty" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec435d69bcc064b5cb0f6a49d8cfc1dbed93f0ec233d4499156ae7c3bc7f90d7" +dependencies = [ + "libc", + "nix 0.30.1", + "signal-hook 0.3.18", + "tokio", +] + +[[package]] +name = "rattler_redaction" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "961121cad9792daafc176a7f4caafc0fc889fb17507c23a4c86e1777a8b5e179" +dependencies = [ + "reqwest 0.12.28", + "reqwest-middleware", + "url", +] + +[[package]] +name = "rattler_repodata_gateway" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e85601ebcd52acb50fe1e15fb863868719ad9005752908431ba12cf433e5d0a" +dependencies = [ + "ahash 0.8.12", + "anyhow", + "async-compression", + "async-fd-lock", + "async-trait", + "blake2", + "bytes", + "cache_control", + "cfg-if", + "chrono", + "coalesced_map", + "dashmap 6.1.0", + "dirs", + "file_url", + "fs-err", + "fslock", + "futures", + "hashbrown 0.15.5", + "hex", + "http 1.4.0", + "http-cache-semantics", + "humansize", + "humantime", + "itertools 0.14.0", + "json-patch", + "libc", + "memmap2", + "parking_lot", + "pin-project-lite", + "rattler_cache", + "rattler_conda_types", + "rattler_digest", + "rattler_networking", + "rattler_package_streaming", + "rattler_redaction", + "reqwest 0.12.28", + "reqwest-middleware", + "retry-policies", + "rmp-serde", + "self_cell 1.2.2", + "serde", + "serde_json", + "serde_with", + "simple_spawn_blocking", + "strum", + "superslice", + "tempfile", + "thiserror 2.0.18", + "tokio", + "tokio-util", + "tracing", + "url", + "wasmtimer", + "windows-sys 0.61.2", + "zstd", +] + +[[package]] +name = "rattler_shell" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +checksum = "6bbd794ff61cd08a8a0260e2288cc4c3b3194dda9689498933655951b16027fd" dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.5", + "anyhow", + "enum_dispatch", + "fs-err", + "indexmap 2.13.0", + "itertools 0.14.0", + "rattler_conda_types", + "rattler_pty", + "serde_json", + "shlex", + "tempfile", + "thiserror 2.0.18", + "tracing", ] [[package]] -name = "rand_chacha" -version = "0.3.1" +name = "rattler_solve" +version = "4.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +checksum = "e0aed2bd89330030bcd19216ddd92a2dbddfaec87697ad4d35533785bed206c7" dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", + "chrono", + "futures", + "humantime", + "itertools 0.14.0", + "rattler_conda_types", + "rattler_digest", + "resolvo", + "tempfile", + "thiserror 2.0.18", + "tracing", ] [[package]] -name = "rand_chacha" -version = "0.9.0" +name = "rattler_virtual_packages" +version = "2.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +checksum = "6bb5cadf2c3cff426ca0e9cb9ae0e9defe6da72244969edaf92f847f1d7e6504" dependencies = [ - "ppv-lite86", - "rand_core 0.9.5", + "archspec", + "libloading 0.9.0", + "nom 8.0.0", + "once_cell", + "plist", + "rattler_conda_types", + "regex", + "serde", + "thiserror 2.0.18", + "tracing", + "winver", ] [[package]] -name = "rand_core" -version = "0.6.4" +name = "rayon" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" dependencies = [ - "getrandom 0.2.17", + "either", + "rayon-core", ] [[package]] -name = "rand_core" -version = "0.9.5" +name = "rayon-core" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" dependencies = [ - "getrandom 0.3.4", + "crossbeam-deque", + "crossbeam-utils", ] [[package]] @@ -6490,6 +7368,18 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "reflink-copy" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23bbed272e39c47a095a5242218a67412a220006842558b03fe2935e8f3d7b92" +dependencies = [ + "cfg-if", + "libc", + "rustix 1.1.3", + "windows 0.61.3", +] + [[package]] name = "regex" version = "1.12.2" @@ -6627,6 +7517,47 @@ dependencies = [ "web-sys", ] +[[package]] +name = "reqwest-middleware" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57f17d28a6e6acfe1733fe24bcd30774d13bffa4b8a22535b4c8c98423088d4e" +dependencies = [ + "anyhow", + "async-trait", + "http 1.4.0", + "reqwest 0.12.28", + "serde", + "thiserror 1.0.69", + "tower-service", +] + +[[package]] +name = "resolvo" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57a085c6cecab01bf88df532b7a03f066d5ab92e461bf4614a9de13f562eb163" +dependencies = [ + "ahash 0.8.12", + "bitvec", + "elsa", + "event-listener", + "futures", + "indexmap 2.13.0", + "itertools 0.14.0", + "petgraph", + "tracing", +] + +[[package]] +name = "retry-policies" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a4bd6027df676bcb752d3724db0ea3c0c5fc1dd0376fec51ac7dcaf9cc69be" +dependencies = [ + "rand 0.9.2", +] + [[package]] name = "rfc6979" version = "0.4.0" @@ -6844,6 +7775,19 @@ dependencies = [ "nom 7.1.3", ] +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags", + "errno 0.3.14", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + [[package]] name = "rustix" version = "1.1.3" @@ -6853,7 +7797,7 @@ dependencies = [ "bitflags", "errno 0.3.14", "libc", - "linux-raw-sys", + "linux-raw-sys 0.11.0", "windows-sys 0.61.2", ] @@ -6962,6 +7906,18 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "rusty-fork" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + [[package]] name = "ryu" version = "1.0.22" @@ -7184,7 +8140,7 @@ dependencies = [ "hyper 1.8.1", "indicatif", "log", - "quick-xml", + "quick-xml 0.37.5", "regex", "reqwest 0.12.28", "self-replace", @@ -7213,6 +8169,18 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + [[package]] name = "serde-value" version = "0.7.0" @@ -7223,6 +8191,16 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + [[package]] name = "serde_core" version = "1.0.228" @@ -7413,7 +8391,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26482cf1ecce4540dc782fc70019eba89ffc4d87b3717eb5ec524b5db6fdefef" dependencies = [ - "bit-set", + "bit-set 0.6.0", "byteorder", "crc", "filetime_creation", @@ -7689,12 +8667,41 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +[[package]] +name = "simd-json" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4255126f310d2ba20048db6321c81ab376f6a6735608bf11f0785c41f01f64e3" +dependencies = [ + "halfbrown", + "ref-cast", + "serde", + "serde_json", + "simdutf8", + "value-trait", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "similar" version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" +[[package]] +name = "simple_spawn_blocking" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55c0b0b683828aa9d4f5c0e59b0c856a12c30a65b5f1ca4292664734d76fa9c2" +dependencies = [ + "tokio", +] + [[package]] name = "siphasher" version = "1.0.2" @@ -7732,6 +8739,9 @@ name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] [[package]] name = "snafu" @@ -7849,6 +8859,12 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "superslice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab16ced94dbd8a46c82fd81e3ed9a8727dac2977ea869d217bcc4ea1f122e81f" + [[package]] name = "supports-color" version = "3.0.2" @@ -7922,6 +8938,20 @@ dependencies = [ "libc", ] +[[package]] +name = "sysctl" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7dddc5f0fee506baf8b9fdb989e242f17e4b11c61dfbb0635b705217199eea" +dependencies = [ + "bitflags", + "byteorder", + "enum-as-inner", + "libc", + "thiserror 1.0.69", + "walkdir", +] + [[package]] name = "system-configuration" version = "0.7.0" @@ -7969,6 +8999,12 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "taplo" version = "0.14.0" @@ -8010,7 +9046,7 @@ dependencies = [ "fastrand", "getrandom 0.3.4", "once_cell", - "rustix", + "rustix 1.1.3", "windows-sys 0.61.2", ] @@ -8051,7 +9087,7 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" dependencies = [ - "rustix", + "rustix 1.1.3", "windows-sys 0.60.2", ] @@ -8300,6 +9336,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.18" @@ -8308,6 +9355,7 @@ checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ "bytes", "futures-core", + "futures-io", "futures-sink", "pin-project-lite", "tokio", @@ -8618,6 +9666,12 @@ dependencies = [ "arrayvec", ] +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + [[package]] name = "unic-langid" version = "0.9.6" @@ -8789,7 +9843,9 @@ version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f" dependencies = [ + "getrandom 0.3.4", "js-sys", + "rand 0.9.2", "wasm-bindgen", ] @@ -8799,6 +9855,18 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "value-trait" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e80f0c733af0720a501b3905d22e2f97662d8eacfe082a75ed7ffb5ab08cb59" +dependencies = [ + "float-cmp", + "halfbrown", + "itoa", + "ryu", +] + [[package]] name = "vcpkg" version = "0.2.15" @@ -8873,6 +9941,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + [[package]] name = "walkdir" version = "2.5.0" @@ -8979,6 +10056,20 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wasmtimer" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c598d6b99ea013e35844697fc4670d08339d5cda15588f193c6beedd12f644b" +dependencies = [ + "futures", + "js-sys", + "parking_lot", + "pin-utils", + "slab", + "wasm-bindgen", +] + [[package]] name = "web-sys" version = "0.3.85" @@ -9041,7 +10132,7 @@ checksum = "24d643ce3fd3e5b54854602a080f34fb10ab75e0b813ee32d00ca2b44fa74762" dependencies = [ "either", "env_home", - "rustix", + "rustix 1.1.3", "winsafe", ] @@ -9052,7 +10143,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3fabb953106c3c8eea8306e4393700d7657561cb43122571b172bbfb7c7ba1d" dependencies = [ "env_home", - "rustix", + "rustix 1.1.3", "winsafe", ] @@ -9205,6 +10296,17 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-registry" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" +dependencies = [ + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + [[package]] name = "windows-registry" version = "0.6.1" @@ -9573,6 +10675,15 @@ version = "0.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" +[[package]] +name = "winver" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e0e7162b9e282fd75a0a832cce93994bdb21208d848a418cd05a5fdd9b9ab33" +dependencies = [ + "windows 0.48.0", +] + [[package]] name = "wiremock" version = "0.6.5" @@ -9608,6 +10719,15 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "x25519-dalek" version = "2.0.1" @@ -9658,7 +10778,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" dependencies = [ "libc", - "rustix", + "rustix 1.1.3", ] [[package]] @@ -9872,6 +10992,21 @@ dependencies = [ "zstd", ] +[[package]] +name = "zip" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb2a05c7c36fde6c09b08576c9f7fb4cda705990f73b58fe011abf7dfb24168b" +dependencies = [ + "arbitrary", + "crc32fast", + "flate2", + "indexmap 2.13.0", + "memchr", + "time", + "zopfli", +] + [[package]] name = "zip" version = "7.2.0" diff --git a/Cargo.toml b/Cargo.toml index 5e3dc671ec..ae244fcd08 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -129,6 +129,16 @@ os-release = "0.1" path-absolutize = { version = "3", features = ["unsafe_cache"] } petgraph = "0.8" rand = "0.9" +rattler = { version = "0.39", default-features = false } +rattler_conda_types = { version = "0.43", default-features = false } +rattler_repodata_gateway = { version = "0.26", default-features = false, features = [ + "gateway", +] } +rattler_solve = { version = "4", default-features = false, features = [ + "resolvo", +] } +rattler_package_streaming = { version = "0.24", default-features = false } +rattler_virtual_packages = { version = "2", default-features = false } regex = "1" reqwest = { version = "0.12", default-features = false, features = [ "json", @@ -233,6 +243,8 @@ default = ["native-tls", "vfox/vendored-lua", "self_update"] native-tls = [ "gix/blocking-http-transport-reqwest-native-tls", "reqwest/native-tls", + "rattler/native-tls", + "rattler_repodata_gateway/native-tls", "sigstore-verification/native-tls", "ubi/native-tls", "vfox/native-tls", @@ -241,6 +253,8 @@ native-tls = [ rustls = [ "gix/blocking-http-transport-reqwest-rust-tls", "reqwest/rustls-tls", + "rattler/rustls-tls", + "rattler_repodata_gateway/rustls-tls", "self_update/rustls", "sigstore-verification/rustls", "ubi/rustls-tls", @@ -250,6 +264,8 @@ rustls = [ rustls-native-roots = [ "gix/blocking-http-transport-reqwest-rust-tls", "reqwest/rustls-tls-native-roots", + "rattler/rustls-tls", + "rattler_repodata_gateway/rustls-tls", "self_update/rustls", "sigstore-verification/rustls-native-roots", "ubi/rustls-tls-native-roots", diff --git a/src/backend/conda.rs b/src/backend/conda.rs index 78ac8d1f83..5800858c54 100644 --- a/src/backend/conda.rs +++ b/src/backend/conda.rs @@ -2,40 +2,35 @@ use crate::backend::VersionInfo; use crate::backend::backend_type::BackendType; use crate::backend::platform_target::PlatformTarget; use crate::cli::args::BackendArg; -use crate::cli::version::{ARCH, OS}; use crate::config::Config; use crate::config::Settings; -use crate::file::{self, TarOptions}; -use crate::http::HTTP_FETCH; +use crate::http::HTTP; use crate::install_context::InstallContext; use crate::lockfile::{self, Lockfile, PlatformInfo}; use crate::toolset::ToolSource; use crate::toolset::ToolVersion; -use crate::{backend::Backend, dirs, hash, http::HTTP, parallel}; +use crate::{backend::Backend, dirs, parallel}; +use crate::{file, hash}; use async_trait::async_trait; -use eyre::{Result, bail}; +use eyre::Result; use itertools::Itertools; +use rattler::install::{InstallDriver, InstallOptions, link_package}; +use rattler_conda_types::{ + Channel, ChannelConfig, GenericVirtualPackage, MatchSpec, ParseStrictness, + Platform as CondaPlatform, RepoDataRecord, prefix::Prefix, +}; +use rattler_repodata_gateway::{Gateway, RepoData}; +use rattler_solve::{ + ChannelPriority, SolveStrategy, SolverImpl, SolverTask, resolvo::Solver as ResolvoSolver, +}; +use rattler_virtual_packages::{VirtualPackageOverrides, VirtualPackages}; use serde::{Deserialize, Serialize}; -use std::collections::{BTreeMap, HashMap, HashSet}; +use std::collections::BTreeMap; use std::fmt::Debug; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use std::sync::Arc; use versions::Versioning; -// Shared utilities for platform-specific library path fixing -#[cfg(any(target_os = "linux", target_os = "macos"))] -#[path = "conda_common.rs"] -mod conda_common; - -// Platform-specific library path fixing modules -#[cfg(target_os = "linux")] -#[path = "conda_linux.rs"] -mod platform; - -#[cfg(target_os = "macos")] -#[path = "conda_macos.rs"] -mod platform; - /// Conda package info stored in the shared conda-packages section of lockfiles #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct CondaPackageInfo { @@ -52,25 +47,12 @@ pub struct CondaBackend { ba: Arc, } -/// Map OS/arch pair to conda subdir format -fn platform_to_conda_subdir(os: &str, arch: &str) -> &'static str { - match (os, arch) { - ("linux", "x64") => "linux-64", - ("linux", "arm64") => "linux-aarch64", - ("macos", "x64") => "osx-64", - ("macos", "arm64") => "osx-arm64", - ("windows", "x64") => "win-64", - _ => "noarch", - } -} - impl CondaBackend { pub fn from_arg(ba: BackendArg) -> Self { Self { ba: Arc::new(ba) } } - /// Get the conda channel from settings or tool options - fn channel(&self) -> String { + fn channel_name(&self) -> String { self.ba .opts() .get("channel") @@ -78,457 +60,402 @@ impl CondaBackend { .unwrap_or_else(|| Settings::get().conda.channel.clone()) } - /// Map mise OS/ARCH to conda subdir - fn conda_subdir() -> &'static str { - platform_to_conda_subdir(OS.as_str(), ARCH.as_str()) + fn channel(&self) -> Result { + let name = self.channel_name(); + let root_dir = std::env::current_dir().unwrap_or_else(|_| dirs::HOME.to_path_buf()); + let config = ChannelConfig::default_with_root_dir(root_dir); + Channel::from_str(&name, &config) + .map_err(|e| eyre::eyre!("invalid conda channel '{}': {}", name, e)) } - /// Map PlatformTarget to conda subdir for lockfile resolution - fn conda_subdir_for_platform(target: &PlatformTarget) -> &'static str { - platform_to_conda_subdir(target.os_name(), target.arch_name()) + fn create_gateway() -> Gateway { + Gateway::builder() + .with_cache_dir(dirs::CACHE.join("conda")) + .finish() } - /// Build a proper download URL from the API response - fn build_download_url(download_url: &str) -> String { - if download_url.starts_with("//") { - format!("https:{}", download_url) - } else if download_url.starts_with('/') { - format!("https://conda.anaconda.org{}", download_url) - } else { - download_url.to_string() + /// Map a mise PlatformTarget to a rattler conda Platform + fn target_to_conda_platform(target: &PlatformTarget) -> CondaPlatform { + match (target.os_name(), target.arch_name()) { + ("linux", "x64") => CondaPlatform::Linux64, + ("linux", "arm64") => CondaPlatform::LinuxAarch64, + ("macos", "x64") => CondaPlatform::Osx64, + ("macos", "arm64") => CondaPlatform::OsxArm64, + ("windows", "x64") => CondaPlatform::Win64, + _ => CondaPlatform::NoArch, } } - /// Fetch package files from the anaconda.org API for a given package - async fn fetch_package_files_for(&self, package_name: &str) -> Result> { - let channel = self.channel(); - let url = format!( - "https://api.anaconda.org/package/{}/{}/files", - channel, package_name - ); - let files: Vec = HTTP_FETCH.json(&url).await?; - Ok(files) + fn detect_virtual_packages(platform: CondaPlatform) -> Vec { + VirtualPackages::detect_for_platform(platform, &VirtualPackageOverrides::default()) + .map(|vp| vp.into_generic_virtual_packages().collect()) + .unwrap_or_default() } - /// Fetch package files from the anaconda.org API for this tool - async fn fetch_package_files(&self) -> Result> { - self.fetch_package_files_for(&self.tool_name()).await + /// Flatten gateway RepoData into owned records for the solver + fn flatten_repodata(repodata: &[RepoData]) -> Vec { + repodata.iter().flat_map(|rd| rd.iter().cloned()).collect() } - /// Find the best package file for a given version and platform - /// Prefers platform-specific packages over noarch, and .conda format over .tar.bz2 - fn find_package_file<'a>( - files: &'a [CondaPackageFile], - version: Option<&str>, - subdir: &str, - ) -> Option<&'a CondaPackageFile> { - // Try platform-specific packages first, then fall back to noarch - Self::find_package_file_for_subdir(files, version, subdir) - .or_else(|| Self::find_package_file_for_subdir(files, version, "noarch")) - } - - /// Find the best package file for a given version and specific subdir - /// Prefers .conda format over .tar.bz2 (newer, faster) - fn find_package_file_for_subdir<'a>( - files: &'a [CondaPackageFile], - version: Option<&str>, - subdir: &str, - ) -> Option<&'a CondaPackageFile> { - // Filter by exact platform match - let platform_files: Vec<_> = files.iter().filter(|f| f.attrs.subdir == subdir).collect(); - - if platform_files.is_empty() { - return None; - } - - // Find files matching the version spec - let matching: Vec<_> = if let Some(ver) = version { - platform_files - .iter() - .filter(|f| Self::version_matches(&f.version, ver)) - .copied() - .collect() - } else { - // No version spec - get latest - let latest = platform_files - .iter() - .max_by_key(|f| Versioning::new(&f.version))?; - platform_files - .iter() - .filter(|f| f.version == latest.version) - .copied() - .collect() + /// Fetch repodata and solve the conda environment for the given specs and platform. + async fn solve_packages( + &self, + specs: Vec, + platform: CondaPlatform, + ) -> Result> { + let channel = self.channel()?; + let gateway = Self::create_gateway(); + + let repodata: Vec = gateway + .query([channel], [platform, CondaPlatform::NoArch], specs.clone()) + .recursive(true) + .await + .map_err(|e| eyre::eyre!("failed to fetch repodata: {}", e))?; + + let flat_records = Self::flatten_repodata(&repodata); + let virtual_packages = Self::detect_virtual_packages(platform); + + let task = SolverTask { + available_packages: [flat_records.as_slice()], + specs, + virtual_packages, + locked_packages: vec![], + pinned_packages: vec![], + constraints: vec![], + timeout: None, + channel_priority: ChannelPriority::Strict, + exclude_newer: None, + min_age: None, + strategy: SolveStrategy::Highest, }; - if matching.is_empty() { - return None; - } - - // Prefer .conda format over .tar.bz2 - // Among matches, pick the latest version - let best_version = matching - .iter() - .max_by_key(|f| Versioning::new(&f.version))?; + let mut solver = ResolvoSolver; + let result = solver + .solve(task) + .map_err(|e| eyre::eyre!("conda solve failed: {}", e))?; - matching - .iter() - .filter(|f| f.version == best_version.version) - .find(|f| f.basename.ends_with(".conda")) - .or_else(|| { - matching - .iter() - .filter(|f| f.version == best_version.version) - .find(|f| f.basename.ends_with(".tar.bz2")) - }) - .copied() + Ok(result.records) } - /// Check if a version matches a conda version spec - /// Supports: exact match, prefix match, wildcard (*), and comparison operators - fn version_matches(version: &str, spec: &str) -> bool { - // Exact match - if version == spec { - return true; - } - - // Wildcard pattern like "6.9.*" -> matches "6.9.anything" - if let Some(prefix) = spec.strip_suffix(".*") - && version.starts_with(prefix) - && version - .chars() - .nth(prefix.len()) - .map(|c| c == '.') - .unwrap_or(false) - { - return true; - } - - // Single wildcard like "6.*" matches "6.anything" - if let Some(prefix) = spec.strip_suffix('*') - && version.starts_with(prefix) - { - return true; - } + /// Shared data dir for all conda package archives (shared across tools) + fn conda_data_dir() -> PathBuf { + dirs::DATA.join("conda-packages") + } - // Prefix match (e.g., "1.7" matches "1.7.1") - if version.starts_with(spec) - && version - .chars() - .nth(spec.len()) - .map(|c| c == '.') - .unwrap_or(false) - { - return true; - } + /// Get the filename portion of a package URL + fn url_filename(url: &url::Url) -> String { + url.path_segments() + .and_then(|mut s| s.next_back()) + .unwrap_or("package") + .to_string() + } - // Handle compound specs like ">=1.0,<2.0" by splitting on comma - if spec.contains(',') { - return spec - .split(',') - .all(|part| Self::version_matches(version, part.trim())); - } + /// Strip .conda or .tar.bz2 extension to get the basename key + fn record_basename(record: &RepoDataRecord) -> String { + let filename = Self::url_filename(&record.url); + filename + .strip_suffix(".conda") + .or_else(|| filename.strip_suffix(".tar.bz2")) + .unwrap_or(&filename) + .to_string() + } - // Comparison operators (>=, <=, >, <, ==, !=) - Self::check_version_constraint(version, spec) + /// Format sha256 as "sha256:" if present + fn format_sha256(record: &RepoDataRecord) -> Option { + record + .package_record + .sha256 + .as_ref() + .map(|h| format!("sha256:{}", hex::encode(h))) } - /// Check a single version constraint like ">=1.0" or "<2.0" - fn check_version_constraint(version: &str, constraint: &str) -> bool { - let v = match Versioning::new(version) { - Some(v) => v, - None => return false, + /// Verify a file's sha256 against an expected "sha256:" checksum. + /// Returns Ok(true) if matches, Ok(false) if mismatches, or Ok(true) + /// if no expected checksum is provided (skip verification). + fn verify_checksum(path: &std::path::Path, expected: Option<&str>) -> Result { + let Some(expected) = expected else { + return Ok(true); + }; + let Some(expected_hex) = expected.strip_prefix("sha256:") else { + return Ok(true); }; + let actual_hex = hash::file_hash_sha256(path, None)?; + Ok(actual_hex == expected_hex) + } - if let Some(spec_ver) = constraint.strip_prefix(">=") { - if let Some(s) = Versioning::new(spec_ver) { - return v >= s; - } - } else if let Some(spec_ver) = constraint.strip_prefix("<=") { - if let Some(s) = Versioning::new(spec_ver) { - return v <= s; - } - } else if let Some(spec_ver) = constraint.strip_prefix("==") { - if let Some(s) = Versioning::new(spec_ver) { - return v == s; - } - } else if let Some(spec_ver) = constraint.strip_prefix("!=") { - if let Some(s) = Versioning::new(spec_ver) { - return v != s; - } - } else if let Some(spec_ver) = constraint.strip_prefix('>') { - if let Some(s) = Versioning::new(spec_ver) { - return v > s; - } - } else if let Some(spec_ver) = constraint.strip_prefix('<') - && let Some(s) = Versioning::new(spec_ver) - { - return v < s; + /// Download a file to dest with optional checksum verification. + /// Uses atomic writes: downloads to a temp file, verifies, then renames. + /// If dest already exists and checksum matches, skips download. + async fn download_to(url: &str, dest: &std::path::Path, checksum: Option<&str>) -> Result<()> { + if dest.exists() && Self::verify_checksum(dest, checksum)? { + return Ok(()); } - false - } - - /// Extract a conda package (.conda or .tar.bz2) to the install path - fn extract_conda_package( - &self, - ctx: &InstallContext, - tarball_path: &std::path::Path, - install_path: &std::path::Path, - ) -> Result<()> { - let filename = tarball_path - .file_name() - .and_then(|s| s.to_str()) - .unwrap_or(""); - - if filename.ends_with(".conda") { - // .conda format: ZIP containing pkg-*.tar.zst - self.extract_conda_format(ctx, tarball_path, install_path)?; - } else if filename.ends_with(".tar.bz2") { - // Legacy format: plain tar.bz2 - ctx.pr.set_message(format!("extract {filename}")); - let tar_opts = TarOptions { - format: file::TarFormat::TarBz2, - pr: Some(ctx.pr.as_ref()), - ..Default::default() - }; - file::untar(tarball_path, install_path, &tar_opts)?; - } else { - bail!("unsupported conda package format: {}", filename); + file::create_dir_all(Self::conda_data_dir())?; + let temp = dest.with_extension("tmp"); + HTTP.download_file(url, &temp, None).await?; + + if !Self::verify_checksum(&temp, checksum)? { + let _ = file::remove_all(&temp); + let display_checksum = checksum.unwrap_or("unknown"); + return Err(eyre::eyre!( + "checksum mismatch for {}: expected {}", + url, + display_checksum, + )); } + file::rename(&temp, dest)?; Ok(()) } - /// Extract .conda format (ZIP with inner tar.zst) - fn extract_conda_format( - &self, - ctx: &InstallContext, - conda_path: &std::path::Path, - install_path: &std::path::Path, - ) -> Result<()> { - let filename = conda_path - .file_name() - .and_then(|s| s.to_str()) - .unwrap_or("package.conda"); - ctx.pr.set_message(format!("extract {filename}")); - - // Create a unique temp directory for extraction to avoid race conditions - // when multiple processes extract different packages simultaneously - let parent_dir = conda_path.parent().unwrap(); - let temp_dir = tempfile::tempdir_in(parent_dir)?; - - // Unzip the .conda file - file::unzip(conda_path, temp_dir.path(), &Default::default())?; - - // Find and extract pkg-*.tar.zst - let pkg_tar = std::fs::read_dir(temp_dir.path())? - .filter_map(|e| e.ok()) - .map(|e| e.path()) - .find(|p| { - p.file_name() - .and_then(|n| n.to_str()) - .is_some_and(|n| n.starts_with("pkg-") && n.ends_with(".tar.zst")) - }); - - if let Some(pkg_tar_path) = pkg_tar { - let tar_opts = TarOptions { - format: file::TarFormat::TarZst, - pr: Some(ctx.pr.as_ref()), - ..Default::default() - }; - file::untar(&pkg_tar_path, install_path, &tar_opts)?; - } else { - bail!("could not find pkg-*.tar.zst in .conda archive"); - } + /// Download a single package archive to the shared conda data dir. + async fn download_record(record: RepoDataRecord) -> Result { + let url_str = record.url.to_string(); + let filename = Self::url_filename(&record.url); + let dest = Self::conda_data_dir().join(&filename); + let checksum = Self::format_sha256(&record); - // temp_dir is automatically cleaned up when dropped - Ok(()) + Self::download_to(&url_str, &dest, checksum.as_deref()).await?; + Ok(dest) } - /// Verify SHA256 checksum if available - fn verify_checksum(tarball_path: &Path, expected_sha256: Option<&str>) -> Result<()> { - if let Some(expected) = expected_sha256 { - hash::ensure_checksum(tarball_path, expected, None, "sha256")?; - } + /// Download a package by URL with optional checksum (for locked installs). + async fn download_url_with_checksum( + (url_str, checksum): (String, Option), + ) -> Result { + let filename = url_str.rsplit('/').next().unwrap_or("package").to_string(); + let dest = Self::conda_data_dir().join(&filename); + + Self::download_to(&url_str, &dest, checksum.as_deref()).await?; + Ok(dest) + } + + /// Extract a downloaded conda package archive into dest using rattler. + async fn extract_package(archive: &std::path::Path, dest: &std::path::Path) -> Result<()> { + rattler_package_streaming::tokio::fs::extract(archive, dest) + .await + .map_err(|e| eyre::eyre!("failed to extract {}: {}", archive.display(), e))?; Ok(()) } - /// Get the shared conda package data directory - /// All conda packages (main + deps) are stored here for sharing across tools - fn conda_data_dir() -> PathBuf { - dirs::DATA.join("conda-packages") + /// Extract a package to a temp dir and link it into the prefix using rattler. + /// + /// This handles text and binary prefix replacement (replacing conda build + /// placeholders with the actual install path), file permissions, and macOS + /// code signing — all via rattler's link_package. + async fn install_package( + archive: &std::path::Path, + prefix: &Prefix, + driver: &InstallDriver, + ) -> Result<()> { + let temp_dir = tempfile::tempdir()?; + Self::extract_package(archive, temp_dir.path()).await?; + link_package(temp_dir.path(), prefix, driver, InstallOptions::default()) + .await + .map_err(|e| eyre::eyre!("failed to link {}: {}", archive.display(), e))?; + Ok(()) } - /// Get path for a specific package file in the data directory - /// Uses basename which includes version+build: "clang-21.1.7-default_h489deba_0.conda" - fn package_path(basename: &str) -> PathBuf { - let filename = Path::new(basename) - .file_name() - .and_then(|s| s.to_str()) - .unwrap_or(basename); - Self::conda_data_dir().join(filename) + fn read_lockfile_for_tool(&self, tv: &ToolVersion) -> Result { + match tv.request.source() { + ToolSource::MiseToml(path) => { + let (lockfile_path, _) = lockfile::lockfile_path_for_config(path); + Lockfile::read(&lockfile_path) + } + _ => Ok(Lockfile::default()), + } } - /// Recursively resolve dependencies for a package - async fn resolve_dependencies( + /// Install from a fresh solve (no lockfile deps). + async fn install_fresh( &self, - pkg_file: &CondaPackageFile, - subdir: &str, - resolved: &mut HashMap, - visited: &mut HashSet, + ctx: &InstallContext, + tv: &mut ToolVersion, + platform_key: &str, ) -> Result<()> { - // Extract version pins from parent package's build string. - // e.g., vim's build string "py310pl5321h..." tells us it needs Python 3.10 - let build_pins = extract_build_pins(&pkg_file.basename); + let tool_name = self.tool_name(); + let spec_str = format!("{}=={}", tool_name, tv.version); + let match_spec = MatchSpec::from_str(&spec_str, ParseStrictness::Lenient) + .map_err(|e| eyre::eyre!("invalid conda spec '{}': {}", spec_str, e))?; + + ctx.pr.set_message("fetching repodata".to_string()); + let records = self + .solve_packages(vec![match_spec], CondaPlatform::current()) + .await?; - for dep in &pkg_file.attrs.depends { - let Some((name, version_spec)) = parse_dependency(dep) else { - continue; - }; + // Separate main package from deps + let tool_name_norm = tool_name.to_lowercase(); + let (main_vec, dep_records): (Vec<_>, Vec<_>) = records + .into_iter() + .partition(|r| r.package_record.name.as_normalized() == tool_name_norm); - // Skip if already resolved or being visited (circular dep protection) - if resolved.contains_key(&name) || visited.contains(&name) { - continue; - } - visited.insert(name.clone()); + let main_record = main_vec + .into_iter() + .next() + .ok_or_else(|| eyre::eyre!("main package {} not found in solve result", tool_name))?; - // Fetch dependency package files - let dep_files = match self.fetch_package_files_for(&name).await { - Ok(files) => files, - Err(e) => { - bail!("failed to fetch dependency '{}': {}", name, e); - } - }; - - // If parent's build string pins this dependency version, try that first. - // e.g., vim built with py310 should get Python 3.10.*, not 3.15 - let pinned_spec = build_pins.get(&name).map(|v| format!("{}.*", v)); - - let matched = if let Some(ref pinned) = pinned_spec { - Self::find_package_file(&dep_files, Some(pinned.as_str()), subdir).or_else(|| { - // Fall back to original spec if pinned version not available - debug!( - "pinned {} {} not found for {}, falling back to {:?}", - name, pinned, subdir, version_spec - ); - Self::find_package_file(&dep_files, version_spec.as_deref(), subdir) - }) - } else { - Self::find_package_file(&dep_files, version_spec.as_deref(), subdir) - }; + // Build ordered list: deps first, main last + let mut all_records = dep_records; + all_records.push(main_record.clone()); - let Some(matched) = matched else { - // Skip dependencies not available for this platform - // This is common - many conda packages have platform-specific deps - debug!( - "skipping dependency '{}' (spec: {:?}) - not available for platform {}", - name, version_spec, subdir - ); - continue; - }; + // Download all in parallel + ctx.pr + .set_message(format!("downloading {} packages", all_records.len())); + let downloaded = parallel::parallel(all_records.clone(), Self::download_record).await?; + + // Create conda prefix and install driver + let install_path = tv.install_path(); + file::remove_all(&install_path)?; + file::create_dir_all(&install_path)?; + let prefix = Prefix::create(&install_path) + .map_err(|e| eyre::eyre!("failed to create conda prefix: {}", e))?; + let driver = InstallDriver::default(); + + for (record, archive) in all_records.iter().zip(downloaded.iter()) { + let name = record.package_record.name.as_normalized(); + ctx.pr.set_message(format!("installing {name}")); + Self::install_package(archive, &prefix, &driver).await?; + } + + Self::make_bins_executable(&install_path)?; + + // Store lockfile info + let n_deps = all_records.len() - 1; // all except main + let dep_basenames: Vec = all_records[..n_deps] + .iter() + .map(Self::record_basename) + .collect(); - resolved.insert(name.clone(), matched.to_resolved_package(&name)); + let platform_info = tv + .lock_platforms + .entry(platform_key.to_string()) + .or_default(); + platform_info.url = Some(main_record.url.to_string()); + platform_info.checksum = Self::format_sha256(&main_record); + platform_info.conda_deps = Some(dep_basenames.clone()); - // Recurse into this dependency's dependencies - Box::pin(self.resolve_dependencies(matched, subdir, resolved, visited)).await?; + // Store dep package info in tv.conda_packages for lockfile update + for record in &all_records[..n_deps] { + let basename = Self::record_basename(record); + tv.conda_packages.insert( + (platform_key.to_string(), basename), + CondaPackageInfo { + url: record.url.to_string(), + checksum: Self::format_sha256(record), + }, + ); } + Ok(()) } - /// Download a conda package to shared data directory (standalone for parallel::parallel) - async fn download_package(pkg: ResolvedPackage) -> Result { - use eyre::WrapErr; + /// Install using URLs stored in the lockfile (deterministic/reproducible path). + async fn install_from_locked( + &self, + ctx: &InstallContext, + tv: &ToolVersion, + platform_key: &str, + ) -> Result<()> { + ctx.pr.set_message("using locked dependencies".to_string()); + + let platform_info = tv + .lock_platforms + .get(platform_key) + .ok_or_else(|| eyre::eyre!("no lock info for platform {}", platform_key))?; - let data_dir = Self::conda_data_dir(); - file::create_dir_all(&data_dir) - .wrap_err_with(|| format!("failed to create conda data dir for {}", pkg.name))?; + let main_url = platform_info + .url + .as_ref() + .ok_or_else(|| eyre::eyre!("no URL in lockfile for {}", self.tool_name()))? + .clone(); + let main_checksum = platform_info.checksum.clone(); - let tarball_path = Self::package_path(&pkg.basename); + let dep_basenames = platform_info.conda_deps.clone().unwrap_or_default(); + let lockfile = self.read_lockfile_for_tool(tv)?; - // Check if file already exists with valid checksum - if tarball_path.exists() { - if Self::verify_checksum(&tarball_path, pkg.sha256.as_deref()).is_ok() { - return Ok(tarball_path); + // Collect dep (url, checksum) pairs from lockfile (deps first, main last) + let mut downloads: Vec<(String, Option)> = vec![]; + for basename in &dep_basenames { + if let Some(pkg_info) = lockfile.get_conda_package(platform_key, basename) { + downloads.push((pkg_info.url.clone(), pkg_info.checksum.clone())); + } else { + return Err(eyre::eyre!( + "conda package {} not found in lockfile for {}", + basename, + platform_key + )); } - // Corrupted file - delete it - let _ = std::fs::remove_file(&tarball_path); } + downloads.push((main_url, main_checksum)); - // Download to a temp file first, then rename after verification - // This ensures the final path never contains a corrupted file - let temp_path = tarball_path.with_extension(format!( - "{}.tmp.{}", - tarball_path - .extension() - .and_then(|e| e.to_str()) - .unwrap_or(""), - std::process::id() - )); - - // Clean up any stale temp file from previous runs - let _ = std::fs::remove_file(&temp_path); - - HTTP.download_file(&pkg.download_url, &temp_path, None) - .await - .wrap_err_with(|| format!("failed to download {}", pkg.download_url))?; - - // Verify checksum of downloaded file - let file_size = std::fs::metadata(&temp_path).map(|m| m.len()).unwrap_or(0); - Self::verify_checksum(&temp_path, pkg.sha256.as_deref()).wrap_err_with(|| { - format!( - "checksum verification failed for {} (file size: {} bytes)", - pkg.name, file_size - ) - })?; + ctx.pr + .set_message(format!("downloading {} packages", downloads.len())); + let downloaded = parallel::parallel(downloads, Self::download_url_with_checksum).await?; - // Rename temp file to final path (atomic on most filesystems) - std::fs::rename(&temp_path, &tarball_path) - .wrap_err_with(|| format!("failed to rename temp file for {}", pkg.name))?; + let install_path = tv.install_path(); + file::remove_all(&install_path)?; + file::create_dir_all(&install_path)?; + let prefix = Prefix::create(&install_path) + .map_err(|e| eyre::eyre!("failed to create conda prefix: {}", e))?; + let driver = InstallDriver::default(); + + for archive in &downloaded { + let filename = archive.file_name().and_then(|n| n.to_str()).unwrap_or("?"); + ctx.pr.set_message(format!("installing {filename}")); + Self::install_package(archive, &prefix, &driver).await?; + } - Ok(tarball_path) + Self::make_bins_executable(&install_path)?; + + Ok(()) } - /// Read the lockfile for the tool's source config - fn read_lockfile_for_tool(&self, tv: &ToolVersion) -> Result { - match tv.request.source() { - ToolSource::MiseToml(path) => { - let (lockfile_path, _) = lockfile::lockfile_path_for_config(path); - Lockfile::read(&lockfile_path) + fn make_bins_executable(install_path: &std::path::Path) -> Result<()> { + let bin_path = if cfg!(windows) { + install_path.join("Library").join("bin") + } else { + install_path.join("bin") + }; + if bin_path.exists() { + for entry in std::fs::read_dir(&bin_path)? { + let entry = entry?; + let path = entry.path(); + if path.is_file() { + file::make_executable(&path)?; + } } - _ => Ok(Lockfile::default()), } + Ok(()) } - /// Resolve conda packages for the lockfile's shared conda-packages section. - /// Returns a map of basename -> CondaPackageInfo for the given platform. + /// Resolve conda packages for lockfile's shared conda-packages section. + /// Returns a map of basename -> CondaPackageInfo for deps of this tool on the given platform. pub async fn resolve_conda_packages( &self, tv: &ToolVersion, target: &PlatformTarget, ) -> Result> { - let files = self.fetch_package_files().await?; - let subdir = Self::conda_subdir_for_platform(target); - - let Some(pkg_file) = Self::find_package_file(&files, Some(&tv.version), subdir) else { - return Ok(BTreeMap::new()); - }; + let platform = Self::target_to_conda_platform(target); + let tool_name = self.tool_name(); + let spec_str = format!("{}=={}", tool_name, tv.version); + let match_spec = MatchSpec::from_str(&spec_str, ParseStrictness::Lenient) + .map_err(|e| eyre::eyre!("invalid conda spec '{}': {}", spec_str, e))?; - // Resolve dependencies for this platform - let mut resolved = HashMap::new(); - let mut visited = HashSet::new(); - visited.insert(self.tool_name()); - self.resolve_dependencies(pkg_file, subdir, &mut resolved, &mut visited) - .await?; + let records = self.solve_packages(vec![match_spec], platform).await?; - // Convert to CondaPackageInfo map keyed by basename + let tool_name_norm = tool_name.to_lowercase(); let mut result = BTreeMap::new(); - for pkg in resolved.values() { - let basename = strip_conda_extension(&pkg.basename).to_string(); + for record in &records { + if record.package_record.name.as_normalized() == tool_name_norm { + continue; + } + let basename = Self::record_basename(record); result.insert( basename, CondaPackageInfo { - url: pkg.download_url.clone(), - checksum: pkg.sha256.as_ref().map(|s| format!("sha256:{}", s)), + url: record.url.to_string(), + checksum: Self::format_sha256(record), }, ); } @@ -548,36 +475,35 @@ impl Backend for CondaBackend { } async fn _list_remote_versions(&self, _config: &Arc) -> Result> { - let files = self.fetch_package_files().await?; - let subdir = Self::conda_subdir(); - - // Filter by current platform and group by version to get the latest upload time per version - let mut version_times: std::collections::HashMap> = - std::collections::HashMap::new(); + let channel = self.channel()?; + let current_platform = CondaPlatform::current(); + let tool_name = self.tool_name(); + + let gateway = Self::create_gateway(); + let match_spec = MatchSpec::from_str(&tool_name, ParseStrictness::Lenient) + .map_err(|e| eyre::eyre!("invalid match spec for '{}': {}", tool_name, e))?; + + let repodata: Vec = gateway + .query( + [channel], + [current_platform, CondaPlatform::NoArch], + [match_spec], + ) + .await + .map_err(|e| eyre::eyre!("failed to list versions for '{}': {}", tool_name, e))?; - for f in files - .iter() - .filter(|f| f.attrs.subdir == subdir || f.attrs.subdir == "noarch") - { - version_times - .entry(f.version.clone()) - .and_modify(|existing| { - // Keep the latest upload time for each version - if let Some(new_time) = &f.upload_time - && (existing.is_none() || existing.as_ref().is_some_and(|e| new_time > e)) - { - *existing = Some(new_time.clone()); - } - }) - .or_insert_with(|| f.upload_time.clone()); + // Collect unique versions across all repodata results + let mut version_set: std::collections::HashSet = std::collections::HashSet::new(); + for data in &repodata { + for record in data { + version_set.insert(record.package_record.version.to_string()); + } } - // Convert to VersionInfo and sort by version - let versions: Vec = version_times + let versions = version_set .into_iter() - .map(|(version, created_at)| VersionInfo { + .map(|version| VersionInfo { version, - created_at, ..Default::default() }) .sorted_by_cached_key(|v| Versioning::new(&v.version)) @@ -601,170 +527,18 @@ impl Backend for CondaBackend { mut tv: ToolVersion, ) -> Result { Settings::get().ensure_experimental("conda backend")?; - let files = self.fetch_package_files().await?; - let subdir = Self::conda_subdir(); - let platform_key = self.get_platform_key(); - - // Find the package file for this version (prefers platform-specific over noarch) - let pkg_file = Self::find_package_file(&files, Some(&tv.version), subdir); - - let pkg_file = match pkg_file { - Some(f) => f, - None => bail!( - "conda package {}@{} not found for platform {}", - self.tool_name(), - tv.version, - subdir - ), - }; - - // Build main package info - let main_pkg = pkg_file.to_resolved_package(&self.tool_name()); - // Check if we have locked dependencies - let locked_deps = tv + let platform_key = self.get_platform_key(); + let has_locked = tv .lock_platforms .get(&platform_key) - .and_then(|p| p.conda_deps.as_ref()); - - // Resolve dependencies - either from lockfile or dynamically - let (resolved, dep_basenames) = if let Some(basenames) = locked_deps { - // Use locked dependencies - look them up in the lockfile - ctx.pr.set_message("using locked dependencies".to_string()); - let lockfile = self.read_lockfile_for_tool(&tv)?; - - let mut resolved = HashMap::new(); - for basename in basenames { - if let Some(pkg_info) = lockfile.get_conda_package(&platform_key, basename) { - let full_basename = extract_basename_from_url(&pkg_info.url); - resolved.insert( - basename.clone(), - ResolvedPackage { - name: basename.clone(), // Use basename as the key - download_url: pkg_info.url.clone(), - sha256: pkg_info.checksum.as_ref().map(|c: &String| { - c.strip_prefix("sha256:").unwrap_or(c).to_string() - }), - basename: full_basename, - }, - ); - } else { - warn!( - "conda package {} not found in lockfile for platform {}", - basename, platform_key - ); - } - } - (resolved, basenames.clone()) - } else { - // Resolve dynamically (current behavior) - ctx.pr.set_message("resolving dependencies".to_string()); - let mut resolved = HashMap::new(); - let mut visited = HashSet::new(); - // Add main package to visited to prevent circular resolution back to it - visited.insert(self.tool_name()); - self.resolve_dependencies(pkg_file, subdir, &mut resolved, &mut visited) - .await?; - - // Convert to basename keys for lockfile - let dep_basenames: Vec = resolved - .values() - .map(|p| strip_conda_extension(&p.basename).to_string()) - .collect(); - - // Re-key resolved by basename for consistency - let resolved_by_basename: HashMap = resolved - .into_values() - .map(|p| (strip_conda_extension(&p.basename).to_string(), p)) - .collect(); - - (resolved_by_basename, dep_basenames) - }; - - // Build list of all packages to download (deps + main) - let mut all_packages: Vec = resolved.values().cloned().collect(); - all_packages.push(main_pkg.clone()); - - // Download all packages in parallel - ctx.pr - .set_message(format!("downloading {} packages", all_packages.len())); - let downloaded_paths = - parallel::parallel(all_packages.clone(), Self::download_package).await?; - - // Create map of package basename -> downloaded path - let path_map: HashMap = all_packages - .iter() - .zip(downloaded_paths.iter()) - .map(|(pkg, path)| { - ( - strip_conda_extension(&pkg.basename).to_string(), - path.clone(), - ) - }) - .collect(); - - let install_path = tv.install_path(); - file::remove_all(&install_path)?; - file::create_dir_all(&install_path)?; - - // Extract dependencies first (sequential to avoid conflicts) - for basename in &dep_basenames { - if let Some(tarball_path) = path_map.get(basename) { - ctx.pr.set_message(format!("extract {basename}")); - self.extract_conda_package(ctx, tarball_path, &install_path)?; - } - } - - // Extract main package last (so its files take precedence) - let main_basename = strip_conda_extension(&main_pkg.basename); - if let Some(main_tarball) = path_map.get(main_basename) { - ctx.pr.set_message(format!("extract {}", self.tool_name())); - self.extract_conda_package(ctx, main_tarball, &install_path)?; - } - - // Fix hardcoded library paths in binaries and shared libraries - // This patches conda build paths to point to the actual install directory - #[cfg(any(target_os = "macos", target_os = "linux"))] - platform::fix_library_paths(ctx, &install_path)?; + .and_then(|p| p.url.as_ref()) + .is_some(); - // Fix hardcoded conda build prefixes in text files (shell scripts, etc.) - // Conda packages use a placeholder prefix that must be replaced at install time - #[cfg(any(target_os = "macos", target_os = "linux"))] - conda_common::fix_text_prefixes(&install_path); - - // Store lockfile info - let platform_info = tv.lock_platforms.entry(platform_key.clone()).or_default(); - platform_info.url = Some(main_pkg.download_url.clone()); - if let Some(sha256) = &main_pkg.sha256 { - platform_info.checksum = Some(format!("sha256:{}", sha256)); - } - platform_info.conda_deps = Some(dep_basenames.clone()); - - // Store resolved packages in tv.conda_packages for lockfile update - for (basename, pkg) in &resolved { - tv.conda_packages.insert( - (platform_key.clone(), basename.clone()), - CondaPackageInfo { - url: pkg.download_url.clone(), - checksum: pkg.sha256.as_ref().map(|s| format!("sha256:{}", s)), - }, - ); - } - - // Make binaries executable (use same path logic as list_bin_paths) - let bin_path = if cfg!(windows) { - install_path.join("Library").join("bin") + if has_locked { + self.install_from_locked(ctx, &tv, &platform_key).await?; } else { - install_path.join("bin") - }; - if bin_path.exists() { - for entry in std::fs::read_dir(&bin_path)? { - let entry = entry?; - let path = entry.path(); - if path.is_file() { - file::make_executable(&path)?; - } - } + self.install_fresh(ctx, &mut tv, &platform_key).await?; } Ok(tv) @@ -775,51 +549,52 @@ impl Backend for CondaBackend { tv: &ToolVersion, target: &PlatformTarget, ) -> Result { - let files = self.fetch_package_files().await?; - let subdir = Self::conda_subdir_for_platform(target); - - // Find the package file for this version and platform (prefers platform-specific over noarch) - let pkg_file = Self::find_package_file(&files, Some(&tv.version), subdir); - - match pkg_file { - Some(pkg_file) => { - let download_url = Self::build_download_url(&pkg_file.download_url); - - // Resolve dependencies for this platform - let mut resolved = HashMap::new(); - let mut visited = HashSet::new(); - visited.insert(self.tool_name()); - self.resolve_dependencies(pkg_file, subdir, &mut resolved, &mut visited) - .await?; - - // Get dependency basenames - let conda_deps: Vec = resolved - .values() - .map(|p| strip_conda_extension(&p.basename).to_string()) - .collect(); - - Ok(PlatformInfo { - url: Some(download_url), - checksum: pkg_file.sha256.as_ref().map(|s| format!("sha256:{}", s)), - size: None, - url_api: None, - conda_deps: if conda_deps.is_empty() { - None - } else { - Some(conda_deps) - }, - }) + let platform = Self::target_to_conda_platform(target); + let tool_name = self.tool_name(); + let spec_str = format!("{}=={}", tool_name, tv.version); + + let match_spec = match MatchSpec::from_str(&spec_str, ParseStrictness::Lenient) { + Ok(s) => s, + Err(e) => { + debug!("invalid conda spec '{}': {}", spec_str, e); + return Ok(PlatformInfo::default()); } - None => { - // No package available for this platform - Ok(PlatformInfo { - url: None, - checksum: None, - size: None, - url_api: None, - conda_deps: None, - }) + }; + + let records = match self.solve_packages(vec![match_spec], platform).await { + Ok(r) => r, + Err(e) => { + debug!( + "failed to resolve {} for {}: {}", + tool_name, + target.to_key(), + e + ); + return Ok(PlatformInfo::default()); } + }; + + let tool_name_norm = tool_name.to_lowercase(); + let mut main_record = None; + let mut dep_basenames: Vec = vec![]; + + for record in &records { + if record.package_record.name.as_normalized() == tool_name_norm { + main_record = Some(record.clone()); + } else { + dep_basenames.push(Self::record_basename(record)); + } + } + + match main_record { + Some(main) => Ok(PlatformInfo { + url: Some(main.url.to_string()), + checksum: Self::format_sha256(&main), + size: None, + url_api: None, + conda_deps: Some(dep_basenames), + }), + None => Ok(PlatformInfo::default()), } } @@ -830,155 +605,14 @@ impl Backend for CondaBackend { ) -> Result> { let install_path = tv.install_path(); if cfg!(windows) { - // Windows conda packages put binaries in Library/bin - Ok(vec![install_path.join("Library").join("bin")]) + // Conda packages on Windows can put binaries in either location + // depending on the build variant (MSVC vs MSYS2/MinGW) + Ok(vec![ + install_path.join("Library").join("bin"), + install_path.join("bin"), + ]) } else { - // Unix conda packages put binaries in bin Ok(vec![install_path.join("bin")]) } } } - -/// Represents a conda package file from the anaconda.org API -#[derive(Debug, Deserialize)] -struct CondaPackageFile { - version: String, - basename: String, - download_url: String, - sha256: Option, - upload_time: Option, - #[serde(default)] - attrs: CondaPackageAttrs, -} - -impl CondaPackageFile { - /// Convert to a ResolvedPackage with the given name - fn to_resolved_package(&self, name: &str) -> ResolvedPackage { - ResolvedPackage { - name: name.to_string(), - download_url: CondaBackend::build_download_url(&self.download_url), - // Filter out empty strings - API sometimes returns "" instead of null - sha256: self.sha256.as_ref().filter(|s| !s.is_empty()).cloned(), - basename: self.basename.clone(), - } - } -} - -/// Package attributes including platform info -#[derive(Debug, Default, Deserialize)] -struct CondaPackageAttrs { - #[serde(default)] - subdir: String, - #[serde(default)] - depends: Vec, -} - -/// Resolved package ready for download -#[derive(Debug, Clone)] -struct ResolvedPackage { - name: String, - download_url: String, - sha256: Option, - basename: String, -} - -/// Packages to skip during dependency resolution: -/// - Virtual packages (__osx, __glibc, etc.) represent system requirements -/// - Build-only constraints (python_abi) don't provide runtime files -/// - System-provided libraries (gcc, vc runtime) should be installed separately -/// -/// Note: python/perl/ruby are NOT skipped because some tools (e.g. vim) dynamically -/// link against libpython/libperl and need the shared libraries at runtime. -const SKIP_PACKAGES: &[&str] = &[ - "python_abi", - // Linux system libraries (provided by distro) - "libgcc-ng", - "libstdcxx-ng", - // Windows Visual C++ runtime (requires Visual Studio or VC++ redistributable) - "ucrt", - "vc", - "vc14_runtime", - "vs2015_runtime", -]; - -/// Parse a conda dependency specification -/// Returns (package_name, optional_version_spec) or None if should be skipped -fn parse_dependency(dep: &str) -> Option<(String, Option)> { - // Skip virtual packages (start with __) - if dep.starts_with("__") { - return None; - } - - // Parse "package_name [version_spec] [build_spec]" - let parts: Vec<&str> = dep.split_whitespace().collect(); - let name = parts.first()?.to_string(); - - // Skip runtime dependencies that are typically not needed for standalone tools - if SKIP_PACKAGES.contains(&name.as_str()) { - return None; - } - - // Get version spec if present (ignore build spec) - let version = parts.get(1).map(|s| s.to_string()); - Some((name, version)) -} - -/// Extract dependency version pins from a conda package's build string. -/// -/// Conda build strings encode key dependency versions at the start: -/// - `py310` → Python 3.10 (first digit = major, rest = minor) -/// - `pl5321` → Perl 5.32 (first digit = major, next 2 digits = minor) -/// -/// Returns a map of (dep_name → pinned_version_prefix), e.g. {"python": "3.10"} -fn extract_build_pins(basename: &str) -> HashMap { - let name = strip_conda_extension(basename); - // Build string is the last segment: "pkg-version-buildstring" - let build_string = name.rsplit('-').next().unwrap_or(""); - - let mut pins = HashMap::new(); - let mut remaining = build_string; - - loop { - if let Some(rest) = remaining.strip_prefix("py") { - // Python: first digit = major, rest = minor → py310 = 3.10 - let digits: String = rest.chars().take_while(|c| c.is_ascii_digit()).collect(); - if digits.len() >= 2 { - pins.insert( - "python".to_string(), - format!("{}.{}", &digits[..1], &digits[1..]), - ); - remaining = &rest[digits.len()..]; - continue; - } - } else if let Some(rest) = remaining.strip_prefix("pl") { - // Perl: first digit = major, next 2 = minor → pl5321 = 5.32 - let digits: String = rest.chars().take_while(|c| c.is_ascii_digit()).collect(); - if digits.len() >= 3 { - pins.insert( - "perl".to_string(), - format!("{}.{}", &digits[..1], &digits[1..3]), - ); - remaining = &rest[digits.len()..]; - continue; - } - } - break; - } - - pins -} - -/// Strip conda extension from basename -/// "ncurses-6.4-h7ea286d_0.conda" -> "ncurses-6.4-h7ea286d_0" -fn strip_conda_extension(basename: &str) -> &str { - basename - .strip_suffix(".conda") - .or_else(|| basename.strip_suffix(".tar.bz2")) - .unwrap_or(basename) -} - -/// Extract basename from URL -/// "https://conda.anaconda.org/.../ncurses-6.4-h7ea286d_0.conda" -> "ncurses-6.4-h7ea286d_0.conda" -fn extract_basename_from_url(url: &str) -> String { - url.rsplit('/').next().unwrap_or(url).to_string() -} diff --git a/src/backend/conda_common.rs b/src/backend/conda_common.rs deleted file mode 100644 index 137e178116..0000000000 --- a/src/backend/conda_common.rs +++ /dev/null @@ -1,189 +0,0 @@ -//! Shared utilities for platform-specific conda library path fixing -//! -//! This module provides common functionality used by both macOS and Linux -//! implementations for fixing hardcoded library paths in conda packages. -//! -//! Note: Some items are only used on specific platforms, so allow dead_code. - -#![allow(dead_code)] - -use std::io::Read; -use std::path::{Path, PathBuf}; -use walkdir::WalkDir; - -/// Fix hardcoded conda build prefixes in text files (shell scripts, configs, etc.) -/// -/// Conda packages are built with a long placeholder prefix (containing -/// `_h_env_placehold` padding) that normally gets replaced by conda during -/// installation. Since we extract packages directly, we need to do this -/// replacement ourselves. -pub fn fix_text_prefixes(install_dir: &Path) { - let Some(install_str) = install_dir.to_str() else { - return; - }; - - let Some(prefix) = find_conda_prefix(install_dir) else { - return; - }; - - debug!( - "replacing conda prefix ({} chars) with {}", - prefix.len(), - install_str - ); - - for entry in WalkDir::new(install_dir).into_iter().filter_map(|e| e.ok()) { - let path = entry.path(); - if !path.is_file() { - continue; - } - let Ok(bytes) = std::fs::read(path) else { - continue; - }; - // Skip binary files (contain null bytes in first chunk) - if bytes.iter().take(512).any(|&b| b == 0) { - continue; - } - let Ok(text) = std::str::from_utf8(&bytes) else { - continue; - }; - if !text.contains(&prefix) { - continue; - } - let new_text = text.replace(&prefix, install_str); - if let Err(e) = std::fs::write(path, new_text) { - debug!("failed to fix prefix in {}: {e}", path.display()); - } - } -} - -/// Find the conda build prefix by scanning files for the `_h_env_placehold` marker. -/// Returns the full prefix path (from leading `/` to end of placeholder segment). -fn find_conda_prefix(install_dir: &Path) -> Option { - // Check bin/ first since that's where shell script wrappers live - let bin_dir = install_dir.join("bin"); - if bin_dir.exists() { - for entry in WalkDir::new(&bin_dir) - .max_depth(1) - .into_iter() - .filter_map(|e| e.ok()) - { - let path = entry.path(); - if !path.is_file() { - continue; - } - if let Ok(content) = std::fs::read_to_string(path) - && let Some(prefix) = extract_placeholder_prefix(&content) - { - return Some(prefix); - } - } - } - - // Fall back to scanning all text files - for entry in WalkDir::new(install_dir).into_iter().filter_map(|e| e.ok()) { - let path = entry.path(); - if !path.is_file() { - continue; - } - let Ok(bytes) = std::fs::read(path) else { - continue; - }; - if bytes.iter().take(512).any(|&b| b == 0) { - continue; - } - if let Ok(content) = std::str::from_utf8(&bytes) - && let Some(prefix) = extract_placeholder_prefix(content) - { - return Some(prefix); - } - } - - None -} - -/// Extract the conda placeholder prefix from text content. -/// The prefix is an absolute path containing `_h_env_placehold` padding, -/// e.g. `/home/conda/feedstock_root/.../ghc_163.../_h_env_placehold_placehold_...` -fn extract_placeholder_prefix(content: &str) -> Option { - let marker = "_h_env_placehold"; - let idx = content.find(marker)?; - - // Walk backward to find start of the absolute path - let before = &content[..idx]; - let pos = before - .rfind(|c: char| !c.is_alphanumeric() && !matches!(c, '/' | '_' | '-' | '.' | '+'))?; - // Skip past the delimiter character (handles multi-byte UTF-8 correctly) - let start = pos + before[pos..].chars().next()?.len_utf8(); - - // Validate it starts with / - if !content[start..].starts_with('/') { - return None; - } - - // Walk forward from marker to find end of the placeholder segment - let after = &content[idx..]; - let end = after - .find(['/', '"', '\'', '\n', ' ', ':']) - .unwrap_or(after.len()); - - let prefix = &content[start..idx + end]; - if prefix.len() > 20 { - Some(prefix.to_string()) - } else { - None - } -} - -/// Mach-O magic numbers for binary detection (macOS) -pub const MACHO_MAGIC_32: [u8; 4] = [0xfe, 0xed, 0xfa, 0xce]; -pub const MACHO_MAGIC_64: [u8; 4] = [0xfe, 0xed, 0xfa, 0xcf]; -pub const MACHO_CIGAM_32: [u8; 4] = [0xce, 0xfa, 0xed, 0xfe]; -pub const MACHO_CIGAM_64: [u8; 4] = [0xcf, 0xfa, 0xed, 0xfe]; -pub const FAT_MAGIC: [u8; 4] = [0xca, 0xfe, 0xba, 0xbe]; -pub const FAT_CIGAM: [u8; 4] = [0xbe, 0xba, 0xfe, 0xca]; - -/// ELF magic number (Linux) -pub const ELF_MAGIC: [u8; 4] = [0x7f, b'E', b'L', b'F']; - -/// Read the first 4 bytes (magic number) from a file -pub fn read_magic(path: &Path) -> Option<[u8; 4]> { - let mut file = std::fs::File::open(path).ok()?; - let mut magic = [0u8; 4]; - file.read_exact(&mut magic).ok()?; - Some(magic) -} - -/// Check if a file is a Mach-O binary (macOS executable or dylib) -pub fn is_macho_file(path: &Path) -> bool { - read_magic(path).is_some_and(|magic| { - matches!( - magic, - MACHO_MAGIC_32 - | MACHO_MAGIC_64 - | MACHO_CIGAM_32 - | MACHO_CIGAM_64 - | FAT_MAGIC - | FAT_CIGAM - ) - }) -} - -/// Check if a file is an ELF binary (Linux executable or shared object) -pub fn is_elf_file(path: &Path) -> bool { - read_magic(path).is_some_and(|magic| magic == ELF_MAGIC) -} - -/// Find all files in a directory that match a predicate -pub fn find_binary_files(dir: &Path, is_binary: F) -> Vec -where - F: Fn(&Path) -> bool, -{ - WalkDir::new(dir) - .into_iter() - .filter_map(|e| e.ok()) - .filter(|e| e.path().is_file()) - .filter(|e| is_binary(e.path())) - .map(|e| e.path().to_path_buf()) - .collect() -} diff --git a/src/backend/conda_linux.rs b/src/backend/conda_linux.rs deleted file mode 100644 index 8d4d9ee414..0000000000 --- a/src/backend/conda_linux.rs +++ /dev/null @@ -1,244 +0,0 @@ -//! Linux-specific library path fixing for conda packages -//! -//! Uses `patchelf` to set RPATH entries for proper library resolution. - -use super::conda_common::{find_binary_files, is_elf_file}; -use crate::install_context::InstallContext; -use eyre::Result; -use std::path::{Path, PathBuf}; -use std::process::Command; -use walkdir::WalkDir; - -/// Known dynamic linker names per architecture -const LINKER_NAMES: &[&str] = &["ld-linux-x86-64.so.2", "ld-linux-aarch64.so.1"]; - -/// System paths to search for dynamic linkers (in priority order) -const SYSTEM_LINKER_PATHS: &[&str] = &[ - "/lib64/ld-linux-x86-64.so.2", - "/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2", - "/lib/ld-linux-x86-64.so.2", - "/lib64/ld-linux-aarch64.so.1", - "/lib/aarch64-linux-gnu/ld-linux-aarch64.so.1", - "/lib/ld-linux-aarch64.so.1", -]; - -/// Fix library paths on Linux using patchelf -pub fn fix_library_paths(ctx: &InstallContext, install_dir: &Path) -> Result<()> { - ctx.pr.set_message("fixing library paths".to_string()); - - // Check if patchelf is available - if !patchelf_available() { - debug!("patchelf not found, skipping library path fixes"); - return Ok(()); - } - - // Discover all directories containing .so files for comprehensive RPATH - let lib_dirs = find_lib_dirs(install_dir); - - // Find all ELF files - let files = find_binary_files(install_dir, is_elf_file); - - for file_path in &files { - // Only fix interpreter for executables (files in bin/), not all ELF files. - // Shared libraries don't have PT_INTERP, so this avoids unnecessary - // patchelf --print-interpreter calls for large installs. - if file_path - .parent() - .is_some_and(|p| p.ends_with("bin") || p.ends_with("libexec")) - { - fix_interpreter(file_path, install_dir); - } - let rpath = build_rpath(file_path, install_dir, &lib_dirs); - set_rpath(file_path, &rpath); - } - - Ok(()) -} - -/// Check if patchelf is available -fn patchelf_available() -> bool { - Command::new("patchelf") - .arg("--version") - .output() - .is_ok_and(|o| o.status.success()) -} - -/// Find all directories under install_dir that contain shared libraries (.so files) -/// -/// Excludes sysroot directories which contain compilation stubs (e.g. libc.so) -/// that must not be loaded at runtime — using them causes symbol lookup errors -/// because they're incompatible with the system's dynamic linker. -fn find_lib_dirs(install_dir: &Path) -> Vec { - let mut dirs = std::collections::HashSet::new(); - for entry in WalkDir::new(install_dir).into_iter().filter_map(|e| e.ok()) { - let path = entry.path(); - if path.is_file() - && let Some(name) = path.file_name().and_then(|n| n.to_str()) - && (name.ends_with(".so") || name.contains(".so.")) - && let Some(parent) = path.parent() - && !path_contains_sysroot(parent) - { - dirs.insert(parent.to_path_buf()); - } - } - - let mut sorted: Vec<_> = dirs.into_iter().collect(); - sorted.sort(); - sorted -} - -/// Build RPATH string for a binary, including all library directories -/// Uses $ORIGIN-relative paths where possible -fn build_rpath(path: &Path, install_dir: &Path, lib_dirs: &[PathBuf]) -> String { - let mut entries = Vec::new(); - - if path.parent().is_some_and(|p| p.ends_with("bin")) { - entries.push("$ORIGIN/../lib".to_string()); - } else if let Some(parent) = path.parent() { - // For libraries, add $ORIGIN so they can find siblings - entries.push("$ORIGIN".to_string()); - // Also add path to lib/ from wherever this file is - if let Ok(rel) = parent.strip_prefix(install_dir) { - let depth = rel.components().count(); - if depth > 0 { - let up = "../".repeat(depth); - entries.push(format!("$ORIGIN/{}lib", up)); - } - } - } - - // Add all discovered lib directories as $ORIGIN-relative paths - for lib_dir in lib_dirs { - if let Ok(rel_path) = lib_dir.strip_prefix(install_dir) { - let Some(rel) = rel_path.to_str() else { - continue; // Skip non-UTF8 paths - }; - if let Some(parent) = path.parent() - && let Ok(from_parent) = parent.strip_prefix(install_dir) - { - let depth = from_parent.components().count(); - let up = "../".repeat(depth); - let entry = format!("$ORIGIN/{}{}", up, rel); - if !entries.contains(&entry) { - entries.push(entry); - } - } - } - } - - entries.join(":") -} - -/// Fix ELF interpreter if it points to a conda build path -fn fix_interpreter(path: &Path, install_dir: &Path) { - let Some(path_str) = path.to_str() else { - debug!( - "skipping non-UTF8 path in fix_interpreter: {}", - path.display() - ); - return; - }; - - // Read current interpreter - let output = match Command::new("patchelf") - .args(["--print-interpreter", path_str]) - .output() - { - Ok(o) => o, - Err(e) => { - debug!("patchelf --print-interpreter failed to spawn for {path_str}: {e}"); - return; - } - }; - - if !output.status.success() { - return; // Shared libraries don't have PT_INTERP - } - - let interp = String::from_utf8_lossy(&output.stdout).trim().to_string(); - - // Check if interpreter points to a conda build path - if !is_conda_build_path(&interp) { - return; - } - - // Try local linker first (check all known architectures) - for linker_name in LINKER_NAMES { - let local_linker = install_dir.join("lib").join(linker_name); - if local_linker.exists() - && let Some(local_str) = local_linker.to_str() - && run_set_interpreter(path_str, local_str) - { - return; - } - } - - // Fall back to system linker — only break on successful patchelf - for system_linker in SYSTEM_LINKER_PATHS { - if Path::new(system_linker).exists() && run_set_interpreter(path_str, system_linker) { - return; - } - } - - debug!("no working linker found for {path_str} (original: {interp})"); -} - -/// Run patchelf --set-interpreter, returning true on success -fn run_set_interpreter(path_str: &str, interpreter: &str) -> bool { - match Command::new("patchelf") - .args(["--set-interpreter", interpreter, path_str]) - .output() - { - Ok(o) if o.status.success() => true, - Ok(o) => { - debug!( - "patchelf --set-interpreter {interpreter} failed for {path_str}: {}", - String::from_utf8_lossy(&o.stderr) - ); - false - } - Err(e) => { - debug!("patchelf --set-interpreter failed to spawn for {path_str}: {e}"); - false - } - } -} - -/// Check if a path is inside a sysroot directory (compilation stubs, not runtime libs) -fn path_contains_sysroot(path: &Path) -> bool { - path.components() - .any(|c| c.as_os_str() == "sysroot" || c.as_os_str().to_string_lossy().contains("sysroot")) -} - -/// Check if a path looks like a conda build-time path -fn is_conda_build_path(path: &str) -> bool { - path.contains("conda-bld") - || path.contains("_build_env") - || path.contains("_h_env_placehold") - || path.contains("/home/conda/") - || path.contains("/Users/runner/miniforge3/") - || path.contains("/opt/conda/") -} - -/// Set RPATH on a binary using patchelf -fn set_rpath(path: &Path, rpath: &str) { - let Some(path_str) = path.to_str() else { - debug!("skipping non-UTF8 path in set_rpath: {}", path.display()); - return; - }; - match Command::new("patchelf") - .args(["--set-rpath", rpath, path_str]) - .output() - { - Ok(o) if !o.status.success() => { - debug!( - "patchelf --set-rpath failed for {path_str}: {}", - String::from_utf8_lossy(&o.stderr) - ); - } - Err(e) => { - debug!("patchelf --set-rpath failed to spawn for {path_str}: {e}"); - } - _ => {} - } -} diff --git a/src/backend/conda_macos.rs b/src/backend/conda_macos.rs deleted file mode 100644 index ed1436115a..0000000000 --- a/src/backend/conda_macos.rs +++ /dev/null @@ -1,205 +0,0 @@ -//! macOS-specific library path fixing for conda packages -//! -//! Uses `install_name_tool` to rewrite hardcoded build paths and -//! `codesign` to re-sign binaries after modification. - -use super::conda_common::{find_binary_files, is_macho_file}; -use crate::install_context::InstallContext; -use eyre::Result; -use std::path::{Path, PathBuf}; -use std::process::Command; - -/// Fix library paths in all binaries and shared libraries after extraction. -/// This patches hardcoded build paths to point to the actual install directory. -pub fn fix_library_paths(ctx: &InstallContext, install_dir: &Path) -> Result<()> { - ctx.pr.set_message("fixing library paths".to_string()); - - // Remove quarantine/provenance attributes that can prevent execution - // These are added by macOS when files are downloaded from the internet - remove_quarantine_attrs(install_dir); - - let lib_dir = install_dir.join("lib"); - let bin_dir = install_dir.join("bin"); - - // Find all Mach-O files (dylibs and executables) - let files = find_binary_files(install_dir, is_macho_file); - - if files.is_empty() { - return Ok(()); - } - - for file_path in &files { - // Get current library dependencies using otool - let deps = match get_library_dependencies(file_path) { - Some(d) => d, - None => continue, - }; - - // Collect paths that need fixing - let paths_to_fix = find_paths_to_fix(&deps, &lib_dir); - - // Check if this is a library that needs its ID fixed - let is_dylib = file_path.extension().is_some_and(|e| e == "dylib"); - let needs_id_fix = is_dylib && file_path.starts_with(&lib_dir); - - // Skip binaries that don't need any modification to preserve their checksum - // This is important for tools like Santa that allowlist by checksum - if paths_to_fix.is_empty() && !needs_id_fix { - continue; - } - - // Verify the original binary has a valid signature before modifying - verify_signature(file_path); - - // Fix each hardcoded path - for (old_path, new_path) in &paths_to_fix { - fix_library_reference(file_path, old_path, new_path); - } - - // Fix the library's own ID if it's a dylib in the lib directory - if needs_id_fix { - fix_library_id(file_path); - } - - // Add rpath entries for lib directory - add_rpath_entries(file_path, &lib_dir, &bin_dir); - - // Re-sign the binary with an ad-hoc signature - // This is required because install_name_tool invalidates the original signature - resign_binary(file_path); - } - - Ok(()) -} - -/// Remove macOS quarantine and provenance attributes -fn remove_quarantine_attrs(dir: &Path) { - let dir_str = dir.to_str().unwrap_or(""); - let _ = Command::new("xattr") - .args(["-r", "-d", "com.apple.quarantine", dir_str]) - .output(); - let _ = Command::new("xattr") - .args(["-r", "-d", "com.apple.provenance", dir_str]) - .output(); -} - -/// Get library dependencies using otool -L -fn get_library_dependencies(path: &Path) -> Option { - let output = Command::new("otool") - .args(["-L", path.to_str().unwrap_or("")]) - .output() - .ok()?; - Some(String::from_utf8_lossy(&output.stdout).to_string()) -} - -/// Find paths that need fixing in the otool output -fn find_paths_to_fix(deps: &str, lib_dir: &Path) -> Vec<(String, PathBuf)> { - deps.lines() - .skip(1) // Skip the first line (file path) - .filter_map(|line| { - let line = line.trim(); - if let Some(old_path) = extract_build_path(line) - && let Some(lib_name) = Path::new(&old_path).file_name() - { - let new_path = lib_dir.join(lib_name); - if new_path.exists() { - return Some((old_path, new_path)); - } - } - None - }) - .collect() -} - -/// Verify binary signature (logging only) -fn verify_signature(path: &Path) { - let result = Command::new("codesign") - .args(["--verify", path.to_str().unwrap_or("")]) - .output(); - - if let Ok(output) = result - && !output.status.success() - { - debug!( - "Binary {} has invalid or missing signature, skipping signature verification", - path.display() - ); - } -} - -/// Fix a single library reference using install_name_tool -fn fix_library_reference(binary: &Path, old_path: &str, new_path: &Path) { - let _ = Command::new("install_name_tool") - .args([ - "-change", - old_path, - new_path.to_str().unwrap_or(""), - binary.to_str().unwrap_or(""), - ]) - .output(); -} - -/// Fix the library's own ID to use @rpath -fn fix_library_id(path: &Path) { - if let Some(filename) = path.file_name() { - let new_id = format!("@rpath/{}", filename.to_str().unwrap_or("")); - let _ = Command::new("install_name_tool") - .args(["-id", &new_id, path.to_str().unwrap_or("")]) - .output(); - } -} - -/// Add rpath entries for library resolution -fn add_rpath_entries(path: &Path, lib_dir: &Path, bin_dir: &Path) { - let path_str = path.to_str().unwrap_or(""); - - // For binaries in bin/, add @executable_path/../lib - // For libraries in lib/, add @loader_path - if path.starts_with(bin_dir) { - let _ = Command::new("install_name_tool") - .args(["-add_rpath", "@executable_path/../lib", path_str]) - .output(); - } else if path.starts_with(lib_dir) { - let _ = Command::new("install_name_tool") - .args(["-add_rpath", "@loader_path", path_str]) - .output(); - } - - // Also add absolute path to lib directory as fallback - if lib_dir.exists() { - let _ = Command::new("install_name_tool") - .args(["-add_rpath", lib_dir.to_str().unwrap_or(""), path_str]) - .output(); - } -} - -/// Re-sign the binary with an ad-hoc signature -fn resign_binary(path: &Path) { - let _ = Command::new("codesign") - .args(["--force", "--sign", "-", path.to_str().unwrap_or("")]) - .output(); -} - -/// Extract a build path from an otool -L output line -/// Returns Some(path) if the line contains a hardcoded conda build path -fn extract_build_path(line: &str) -> Option { - // otool output format: "\t/path/to/lib.dylib (compatibility version ...)" - let path = line.split('(').next()?.trim(); - - // Only fix paths that look like conda build paths - // These typically contain patterns like: - // - /Users/runner/miniforge3/conda-bld/ - // - /home/conda/feedstock_root/ - // - /opt/conda/conda-bld/ - // - paths with _h_env_placehold_ (conda placeholder paths) - if path.contains("conda-bld") - || path.contains("feedstock_root") - || path.contains("_h_env_placehold") - || path.contains("_build_env") - || path.contains("/conda/") - { - Some(path.to_string()) - } else { - None - } -}