Compare commits
8 commits
ab35745f53
...
7b3452ef18
| Author | SHA1 | Date | |
|---|---|---|---|
|
7b3452ef18 |
|||
|
08c4048bd3 |
|||
|
a53664be83 |
|||
|
cd6a314bc8 |
|||
|
e385c74b57 |
|||
|
8836eacb95 |
|||
|
7e2338b017 |
|||
|
7f9364eb88 |
14 changed files with 1143 additions and 57 deletions
377
Cargo.lock
generated
377
Cargo.lock
generated
|
|
@ -13,15 +13,21 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.13"
|
||||
version = "1.0.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
|
||||
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.102"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.10.0"
|
||||
version = "2.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
|
||||
checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
|
|
@ -31,9 +37,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
|||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.6.0"
|
||||
version = "4.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351"
|
||||
checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
|
|
@ -51,18 +57,18 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "clap_complete"
|
||||
version = "4.6.0"
|
||||
version = "4.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19c9f1dde76b736e3681f28cec9d5a61299cbaae0fce80a68e43724ad56031eb"
|
||||
checksum = "3ff7a1dccbdd8b078c2bdebff47e404615151534d5043da397ec50286816f9cb"
|
||||
dependencies = [
|
||||
"clap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.6.0"
|
||||
version = "4.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a"
|
||||
checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
|
|
@ -78,13 +84,12 @@ checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
|
|||
|
||||
[[package]]
|
||||
name = "console"
|
||||
version = "0.16.2"
|
||||
version = "0.16.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03e45a4a8926227e4197636ba97a9fc9b00477e9f4bd711395687c5f0734bec4"
|
||||
checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87"
|
||||
dependencies = [
|
||||
"encode_unicode",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"unicode-width",
|
||||
"windows-sys",
|
||||
]
|
||||
|
|
@ -101,22 +106,25 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "eh"
|
||||
version = "0.1.7"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"dialoguer",
|
||||
"eh-log",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"textwrap",
|
||||
"thiserror",
|
||||
"toml",
|
||||
"walkdir",
|
||||
"yansi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "eh-log"
|
||||
version = "0.1.7"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"yansi",
|
||||
]
|
||||
|
|
@ -127,6 +135,12 @@ version = "1.0.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.14"
|
||||
|
|
@ -139,22 +153,44 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.3.0"
|
||||
version = "2.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||
checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.3.4"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
||||
checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"wasip2",
|
||||
"wasip3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
||||
dependencies = [
|
||||
"foldhash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
|
|
@ -162,16 +198,40 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.17"
|
||||
name = "id-arena"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
||||
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.17.0",
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "leb128fmt"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.183"
|
||||
version = "0.2.185"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
|
||||
checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f"
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
|
|
@ -180,16 +240,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.6"
|
||||
name = "log"
|
||||
version = "0.4.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
|
||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "prettyplease"
|
||||
version = "0.2.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
|
|
@ -211,9 +287,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "5.3.0"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
|
|
@ -229,9 +305,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.13"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c"
|
||||
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
|
|
@ -240,9 +316,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.8"
|
||||
version = "0.8.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
|
||||
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
|
|
@ -266,6 +342,12 @@ dependencies = [
|
|||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
|
|
@ -273,6 +355,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -308,12 +391,27 @@ dependencies = [
|
|||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shell-words"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77"
|
||||
|
||||
[[package]]
|
||||
name = "smawk"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.117"
|
||||
|
|
@ -338,6 +436,17 @@ dependencies = [
|
|||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "textwrap"
|
||||
version = "0.16.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057"
|
||||
dependencies = [
|
||||
"smawk",
|
||||
"unicode-linebreak",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.18"
|
||||
|
|
@ -359,10 +468,47 @@ dependencies = [
|
|||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.22"
|
||||
name = "toml"
|
||||
version = "1.1.2+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
|
||||
checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"toml_parser",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "1.1.1+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_parser"
|
||||
version = "1.1.2+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
|
||||
dependencies = [
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-linebreak"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
|
|
@ -370,6 +516,12 @@ version = "0.2.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.5.0"
|
||||
|
|
@ -382,11 +534,54 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wasip2"
|
||||
version = "1.0.2+wasi-0.2.9"
|
||||
version = "1.0.3+wasi-0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
|
||||
checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6"
|
||||
dependencies = [
|
||||
"wit-bindgen",
|
||||
"wit-bindgen 0.57.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasip3"
|
||||
version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
|
||||
dependencies = [
|
||||
"wit-bindgen 0.51.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-encoder"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
|
||||
dependencies = [
|
||||
"leb128fmt",
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-metadata"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"indexmap",
|
||||
"wasm-encoder",
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasmparser"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"hashbrown 0.15.5",
|
||||
"indexmap",
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -413,15 +608,109 @@ dependencies = [
|
|||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0"
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
|
||||
dependencies = [
|
||||
"wit-bindgen-rust-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.57.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-core"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"heck",
|
||||
"wit-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rust"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"heck",
|
||||
"indexmap",
|
||||
"prettyplease",
|
||||
"syn",
|
||||
"wasm-metadata",
|
||||
"wit-bindgen-core",
|
||||
"wit-component",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rust-macro"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"prettyplease",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wit-bindgen-core",
|
||||
"wit-bindgen-rust",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-component"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags",
|
||||
"indexmap",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"wasm-encoder",
|
||||
"wasm-metadata",
|
||||
"wasmparser",
|
||||
"wit-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-parser"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"id-arena",
|
||||
"indexmap",
|
||||
"log",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"unicode-xid",
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xtask"
|
||||
version = "0.1.7"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"clap_complete",
|
||||
|
|
@ -436,6 +725,6 @@ checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
|
|||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.17"
|
||||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02aae0f83f69aafc94776e879363e9771d7ecbffe2c7fbb6c14c5e00dfe88439"
|
||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||
|
|
|
|||
|
|
@ -9,17 +9,20 @@ description = "Ergonomic Nix CLI helper"
|
|||
edition = "2024"
|
||||
license = "MPL-2.0"
|
||||
readme = true
|
||||
rust-version = "1.91.0"
|
||||
version = "0.1.7"
|
||||
rust-version = "1.94.0"
|
||||
version = "0.2.0"
|
||||
|
||||
[workspace.dependencies]
|
||||
clap = { default-features = false, features = [ "std", "help", "derive" ], version = "4.6.0" }
|
||||
clap_complete = "4.6.0"
|
||||
dialoguer = { default-features = false, version = "0.12.0" }
|
||||
regex = "1.12.3"
|
||||
serde = { features = [ "derive" ], version = "1.0.149" }
|
||||
serde_json = "1.0.149"
|
||||
tempfile = "3.27.0"
|
||||
textwrap = "0.16.2"
|
||||
thiserror = "2.0.18"
|
||||
toml = { default-features = false, features = [ "parse", "serde" ], version = "1.1.2" }
|
||||
walkdir = "2.5.0"
|
||||
yansi = "1.0.1"
|
||||
|
||||
|
|
|
|||
90
README.md
90
README.md
|
|
@ -27,6 +27,10 @@ of building the package. The following variables are supported:
|
|||
- **Insecure packages**: Sets `NIXPKGS_ALLOW_INSECURE=1`
|
||||
- **Broken packages**: Sets `NIXPKGS_ALLOW_BROKEN=1`
|
||||
|
||||
Auto-retry requires that `--impure` is not explicitly disabled for the relevant
|
||||
command in the config file. By default retries are automatic. See
|
||||
[Configuration](#configuration).
|
||||
|
||||
### Hash Auto-Fix
|
||||
|
||||
When a hash mismatch is detected in the underlying `nix build`, `eh` can
|
||||
|
|
@ -55,6 +59,92 @@ nb .#myPackage # nix build .#myPackage
|
|||
nu # nix flake update
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
`eh` reads configuration from the first `.eh.toml` found by walking up from the
|
||||
current directory, falling back to `~/.config/eh/config.toml`. If no file
|
||||
exists, all defaults apply and no extra flags are passed to Nix.
|
||||
|
||||
### Global settings
|
||||
|
||||
Top-level keys apply to every command unless overridden per-command:
|
||||
|
||||
```toml
|
||||
# Explicitly enable --impure for all commands (also passes it on initial run).
|
||||
impure = true
|
||||
|
||||
# Explicitly disable impure retries for all commands.
|
||||
impure = false
|
||||
```
|
||||
|
||||
When `impure` is absent (the default), auto-retry with `--impure` is
|
||||
**automatic** — `eh` will add `--impure` and the appropriate `NIXPKGS_ALLOW_*`
|
||||
variable whenever it detects an unfree, insecure, or broken package.
|
||||
|
||||
<!--markdownlint-disable MD013-->
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
| -------- | ---- | ------- | -------------------------------------------------------------- |
|
||||
| `impure` | bool | - | `true` passes `--impure` always; `false` blocks impure retries |
|
||||
|
||||
<!--markdownlint-enable MD013-->
|
||||
|
||||
### Per-command settings
|
||||
|
||||
Each command can be configured independently under `[commands.<name>]`. A
|
||||
per-command setting takes precedence over the global one; the global setting
|
||||
applies to commands that do not have their own entry.
|
||||
|
||||
```toml
|
||||
[commands.build]
|
||||
impure = true
|
||||
env = { NIXPKGS_ALLOW_UNFREE = "1" }
|
||||
|
||||
[commands.develop]
|
||||
impure = false
|
||||
|
||||
[commands.develop.env]
|
||||
MY_DEV_VAR = "1"
|
||||
```
|
||||
|
||||
<!--markdownlint-disable MD013-->
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
| -------- | ----- | ------- | ------------------------------------------------------------------------------- |
|
||||
| `impure` | bool | - | `true` passes `--impure` always; `false` blocks impure retries for this command |
|
||||
| `env` | table | `{}` | Extra environment variables to set for the command |
|
||||
|
||||
<!--markdownlint-enable MD013-->
|
||||
|
||||
### Impure mode and unfree/insecure/broken packages
|
||||
|
||||
When `eh` detects that a package requires `--impure` (unfree, insecure, or
|
||||
broken), it retries automatically with the appropriate `NIXPKGS_ALLOW_*`
|
||||
variable and `--impure` by default.
|
||||
|
||||
If `impure = false` is set for the active command (or globally), the retry is
|
||||
blocked and an error is shown instead:
|
||||
|
||||
```plaintext
|
||||
! package has an unfree license but `--impure` is disabled for `build` in config
|
||||
~ set `impure = true` for this command (or globally) in .eh.toml or
|
||||
~/.config/eh/config.toml, or pass `--impure` manually
|
||||
```
|
||||
|
||||
To explicitly enable `--impure` for a specific command (also adds it to the
|
||||
initial run, not just retries):
|
||||
|
||||
```toml
|
||||
[commands.build]
|
||||
impure = true
|
||||
```
|
||||
|
||||
To disable impure retries globally:
|
||||
|
||||
```toml
|
||||
impure = false
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
<!--markdownlint-disable MD059-->
|
||||
|
|
|
|||
|
|
@ -42,6 +42,8 @@ enum Binary {
|
|||
Nr,
|
||||
Ns,
|
||||
Nb,
|
||||
Nd,
|
||||
Ni,
|
||||
Nu,
|
||||
}
|
||||
|
||||
|
|
@ -51,6 +53,8 @@ impl Binary {
|
|||
Self::Nr => "nr",
|
||||
Self::Ns => "ns",
|
||||
Self::Nb => "nb",
|
||||
Self::Nd => "nd",
|
||||
Self::Ni => "ni",
|
||||
Self::Nu => "nu",
|
||||
}
|
||||
}
|
||||
|
|
@ -92,7 +96,14 @@ fn create_multicall_binaries(
|
|||
);
|
||||
}
|
||||
|
||||
let multicall_binaries = [Binary::Nr, Binary::Ns, Binary::Nb, Binary::Nu];
|
||||
let multicall_binaries = [
|
||||
Binary::Nr,
|
||||
Binary::Ns,
|
||||
Binary::Nb,
|
||||
Binary::Nd,
|
||||
Binary::Ni,
|
||||
Binary::Nu,
|
||||
];
|
||||
let bin_path = Path::new(bin_dir);
|
||||
|
||||
for binary in multicall_binaries {
|
||||
|
|
@ -155,7 +166,7 @@ fn generate_completions(
|
|||
println!("completion file generated: {}", completion_file.display());
|
||||
|
||||
// Create symlinks for multicall binaries
|
||||
let multicall_names = ["nb", "nr", "ns", "nu"];
|
||||
let multicall_names = ["nb", "nd", "ni", "nr", "ns", "nu"];
|
||||
for name in &multicall_names {
|
||||
let symlink_path = output_dir.join(format!("{name}.{shell}"));
|
||||
if symlink_path.exists() {
|
||||
|
|
|
|||
|
|
@ -15,8 +15,11 @@ clap.workspace = true
|
|||
dialoguer.workspace = true
|
||||
eh-log.workspace = true
|
||||
regex.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
tempfile.workspace = true
|
||||
textwrap.workspace = true
|
||||
thiserror.workspace = true
|
||||
toml.workspace = true
|
||||
walkdir.workspace = true
|
||||
yansi.workspace = true
|
||||
|
|
|
|||
295
eh/src/commands/info.rs
Normal file
295
eh/src/commands/info.rs
Normal file
|
|
@ -0,0 +1,295 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use eh_log::{log_error, log_info};
|
||||
use serde::Deserialize;
|
||||
use yansi::Paint;
|
||||
|
||||
use crate::{
|
||||
commands::NixCommand,
|
||||
error::{EhError, Result},
|
||||
util::{make_eval_expr, print_error_suggestions},
|
||||
};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct PackageMeta {
|
||||
name: String,
|
||||
version: Option<String>,
|
||||
description: Option<String>,
|
||||
long_description: Option<String>,
|
||||
license: Option<serde_json::Value>,
|
||||
homepage: Option<String>,
|
||||
platforms: Option<Vec<String>>,
|
||||
broken: Option<bool>,
|
||||
insecure: Option<bool>,
|
||||
#[serde(rename = "unfree")]
|
||||
unfree: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct PackageOutputs {
|
||||
#[serde(flatten)]
|
||||
outputs: HashMap<String, serde_json::Value>,
|
||||
}
|
||||
|
||||
pub fn handle_info(
|
||||
args: &[String],
|
||||
cfg: &crate::config::CommandConfig,
|
||||
) -> Result<i32> {
|
||||
// Get the package argument (skip flags)
|
||||
let pkg = args
|
||||
.iter()
|
||||
.find(|arg| !arg.starts_with('-'))
|
||||
.cloned()
|
||||
.unwrap_or_else(|| ".".to_string());
|
||||
|
||||
let eval_arg = make_eval_expr(&pkg);
|
||||
let pkg_name: String = if eval_arg.contains("#") {
|
||||
eval_arg
|
||||
.split("#")
|
||||
.last()
|
||||
.unwrap_or(&eval_arg)
|
||||
.trim_end_matches(".meta")
|
||||
.to_string()
|
||||
} else {
|
||||
eval_arg.trim_end_matches(".meta").to_string()
|
||||
};
|
||||
// Handle .# case - show "default" as the package name
|
||||
let pkg_name = if pkg_name.is_empty() {
|
||||
"default".to_string()
|
||||
} else {
|
||||
pkg_name
|
||||
};
|
||||
|
||||
log_info!("Fetching info for {}", pkg_name.bold());
|
||||
|
||||
// Fetch metadata
|
||||
let meta_cmd = NixCommand::new("eval")
|
||||
.arg("--json")
|
||||
.arg(&eval_arg)
|
||||
.print_build_logs(false)
|
||||
.with_config(cfg);
|
||||
|
||||
let meta_output = meta_cmd.output()?;
|
||||
|
||||
if !meta_output.status.success() {
|
||||
log_error!("Failed to fetch package info");
|
||||
print_error_suggestions(&meta_output.stderr);
|
||||
return Err(EhError::NixCommandFailed {
|
||||
command: "eval".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let meta: PackageMeta =
|
||||
serde_json::from_slice(&meta_output.stdout).map_err(|e| {
|
||||
EhError::Io(std::io::Error::other(format!(
|
||||
"Failed to parse package metadata: {}",
|
||||
e
|
||||
)))
|
||||
})?;
|
||||
|
||||
// Fetch outputs
|
||||
let outputs_expr = eval_arg
|
||||
.strip_suffix(".meta")
|
||||
.unwrap_or(&eval_arg)
|
||||
.to_string();
|
||||
let outputs_cmd = NixCommand::new("eval")
|
||||
.arg("--json")
|
||||
.arg(format!("{}.outputs", outputs_expr))
|
||||
.print_build_logs(false)
|
||||
.with_config(cfg);
|
||||
|
||||
let outputs_output = outputs_cmd.output()?;
|
||||
let outputs: Option<PackageOutputs> = if outputs_output.status.success() {
|
||||
serde_json::from_slice(&outputs_output.stdout).ok()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Print formatted info
|
||||
print_package_info(&meta, outputs.as_ref(), &pkg);
|
||||
|
||||
Ok(0)
|
||||
}
|
||||
|
||||
fn print_package_info(
|
||||
meta: &PackageMeta,
|
||||
outputs: Option<&PackageOutputs>,
|
||||
pkg_ref: &str,
|
||||
) {
|
||||
println!();
|
||||
|
||||
// Header
|
||||
println!(" {} {}", "Package:".bold(), meta.name);
|
||||
|
||||
if let Some(ref version) = meta.version {
|
||||
println!(" {} {}", "Version:".bold(), version);
|
||||
}
|
||||
|
||||
if let Some(ref desc) = meta.description {
|
||||
println!(" {} {}", "Description:".bold(), desc);
|
||||
}
|
||||
|
||||
// Show long description if available and different from short description
|
||||
if let Some(ref long_desc) = meta.long_description {
|
||||
let should_show = meta
|
||||
.description
|
||||
.as_ref()
|
||||
.map(|d| d != long_desc)
|
||||
.unwrap_or(true);
|
||||
if should_show {
|
||||
println!();
|
||||
// Wrap long description to 70 chars for readability
|
||||
let wrapped = textwrap::fill(long_desc, 70);
|
||||
for line in wrapped.lines() {
|
||||
println!(" {}", line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// License
|
||||
if let Some(ref license) = meta.license {
|
||||
let license_str = match license {
|
||||
serde_json::Value::String(s) => s.clone(),
|
||||
serde_json::Value::Object(obj) => {
|
||||
obj
|
||||
.get("spdxId")
|
||||
.and_then(|v| v.as_str())
|
||||
.or_else(|| obj.get("shortName").and_then(|v| v.as_str()))
|
||||
.unwrap_or("Unknown")
|
||||
.to_string()
|
||||
},
|
||||
serde_json::Value::Array(licenses) => {
|
||||
// Handle multiple licenses (e.g., neovim has Apache-2.0 AND Vim)
|
||||
let license_names: Vec<String> = licenses
|
||||
.iter()
|
||||
.filter_map(|lic| {
|
||||
match lic {
|
||||
serde_json::Value::Object(obj) => {
|
||||
obj
|
||||
.get("spdxId")
|
||||
.and_then(|v| v.as_str())
|
||||
.or_else(|| obj.get("shortName").and_then(|v| v.as_str()))
|
||||
.map(|s| s.to_string())
|
||||
},
|
||||
serde_json::Value::String(s) => Some(s.clone()),
|
||||
_ => None,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
if license_names.is_empty() {
|
||||
"Unknown".to_string()
|
||||
} else {
|
||||
license_names.join(", ")
|
||||
}
|
||||
},
|
||||
_ => "Unknown".to_string(),
|
||||
};
|
||||
println!(" {} {}", "License:".bold(), license_str);
|
||||
}
|
||||
|
||||
// Homepage
|
||||
if let Some(ref homepage) = meta.homepage {
|
||||
println!(" {} {}", "Homepage:".bold(), homepage);
|
||||
}
|
||||
|
||||
// Meta section
|
||||
println!();
|
||||
println!(" {}", "Meta:".bold());
|
||||
|
||||
// Status indicators
|
||||
let mut status_parts = Vec::new();
|
||||
if meta.broken == Some(true) {
|
||||
status_parts.push("Broken".red().to_string());
|
||||
}
|
||||
if meta.insecure == Some(true) {
|
||||
status_parts.push("Insecure".red().to_string());
|
||||
}
|
||||
if meta.unfree == Some(true) {
|
||||
status_parts.push("Unfree".yellow().to_string());
|
||||
}
|
||||
|
||||
if status_parts.is_empty() {
|
||||
println!(" {} {}", "Status:".bold(), "✓ Available".green());
|
||||
} else {
|
||||
println!(" {} {}", "Status:".bold(), status_parts.join(", "));
|
||||
}
|
||||
|
||||
// Platforms
|
||||
if let Some(ref platforms) = meta.platforms {
|
||||
let platform_list: Vec<_> = platforms.iter().take(4).cloned().collect();
|
||||
let platform_str = if platforms.len() > 4 {
|
||||
format!(
|
||||
"{} + {} more",
|
||||
platform_list.join(", "),
|
||||
platforms.len() - 4
|
||||
)
|
||||
} else {
|
||||
platform_list.join(", ")
|
||||
};
|
||||
println!(" {} {}", "Platforms:".bold(), platform_str);
|
||||
}
|
||||
|
||||
// Outputs section
|
||||
if let Some(outputs) = outputs {
|
||||
println!();
|
||||
println!(" {}", "Outputs:".bold());
|
||||
let output_names: Vec<_> = outputs.outputs.keys().cloned().collect();
|
||||
for name in output_names {
|
||||
let marker = if name == "out" { " (default)" } else { "" };
|
||||
println!(" • {}{}", name, marker.dim());
|
||||
}
|
||||
}
|
||||
|
||||
// Usage section
|
||||
println!();
|
||||
println!(" {}", "Usage:".bold());
|
||||
println!(
|
||||
" {} {} {}",
|
||||
"eh run".dim(),
|
||||
pkg_ref,
|
||||
"# Run the package".dim()
|
||||
);
|
||||
println!(
|
||||
" {} {} {}",
|
||||
"eh shell".dim(),
|
||||
pkg_ref,
|
||||
"# Enter shell with package".dim()
|
||||
);
|
||||
println!();
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_package_meta_deserialization() {
|
||||
let json = r#"{
|
||||
"name": "hello",
|
||||
"version": "2.12.1",
|
||||
"description": "A greeting program",
|
||||
"license": "GPL-3.0",
|
||||
"homepage": "https://example.com",
|
||||
"platforms": ["x86_64-linux"],
|
||||
"broken": false,
|
||||
"insecure": false,
|
||||
"unfree": false
|
||||
}"#;
|
||||
|
||||
let meta: PackageMeta = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(meta.name, "hello");
|
||||
assert_eq!(meta.version, Some("2.12.1".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_license_object_parsing() {
|
||||
let json = r#"{
|
||||
"name": "test",
|
||||
"license": {"spdxId": "MIT", "fullName": "MIT License"}
|
||||
}"#;
|
||||
|
||||
let meta: PackageMeta = serde_json::from_str(json).unwrap();
|
||||
assert!(meta.license.is_some());
|
||||
}
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@ use crate::{
|
|||
},
|
||||
};
|
||||
|
||||
pub mod info;
|
||||
pub mod update;
|
||||
|
||||
const DEFAULT_BUFFER_SIZE: usize = 4096;
|
||||
|
|
@ -130,6 +131,21 @@ impl NixCommand {
|
|||
self
|
||||
}
|
||||
|
||||
/// Apply per-command configuration: sets `--impure` (when explicitly enabled)
|
||||
/// and any extra environment variables declared in the config file. Call
|
||||
/// this before any retry-specific overrides so that retry logic can still
|
||||
/// force `impure(true)` afterwards.
|
||||
#[must_use]
|
||||
pub fn with_config(mut self, cfg: &crate::config::CommandConfig) -> Self {
|
||||
if cfg.impure == Some(true) {
|
||||
self = self.impure(true);
|
||||
}
|
||||
for (k, v) in &cfg.env {
|
||||
self = self.env(k, v);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
fn build_command(&self) -> Command {
|
||||
let mut cmd = Command::new("nix");
|
||||
cmd.arg(&self.subcommand);
|
||||
|
|
@ -320,6 +336,7 @@ pub fn handle_nix_command(
|
|||
hash_extractor: &dyn HashExtractor,
|
||||
fixer: &dyn NixFileFixer,
|
||||
classifier: &dyn NixErrorClassifier,
|
||||
cfg: &crate::config::CommandConfig,
|
||||
) -> Result<i32> {
|
||||
let intercept_env = matches!(command, "run" | "shell");
|
||||
handle_nix_with_retry(
|
||||
|
|
@ -329,6 +346,7 @@ pub fn handle_nix_command(
|
|||
fixer,
|
||||
classifier,
|
||||
intercept_env,
|
||||
cfg,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -55,7 +55,10 @@ fn prompt_input_selection(inputs: &[String]) -> Result<Vec<String>> {
|
|||
///
|
||||
/// If `args` is non-empty, use them as explicit input names.
|
||||
/// Otherwise, fetch inputs interactively and prompt for selection.
|
||||
pub fn handle_update(args: &[String]) -> Result<i32> {
|
||||
pub fn handle_update(
|
||||
args: &[String],
|
||||
cfg: &crate::config::CommandConfig,
|
||||
) -> Result<i32> {
|
||||
let selected = if args.is_empty() {
|
||||
let inputs = fetch_flake_inputs()?;
|
||||
if inputs.is_empty() {
|
||||
|
|
@ -66,7 +69,7 @@ pub fn handle_update(args: &[String]) -> Result<i32> {
|
|||
args.to_vec()
|
||||
};
|
||||
|
||||
let mut cmd = NixCommand::new("flake").arg("lock");
|
||||
let mut cmd = NixCommand::new("flake").arg("lock").with_config(cfg);
|
||||
for name in &selected {
|
||||
cmd = cmd.arg("--update-input").arg(name);
|
||||
}
|
||||
|
|
|
|||
236
eh/src/config.rs
Normal file
236
eh/src/config.rs
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
use std::{
|
||||
collections::HashMap,
|
||||
env,
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Deserialize, Default)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Config {
|
||||
/// When `Some(true)`, pass `--impure` to every Nix command.
|
||||
/// When `Some(false)`, block automatic impure retries for every command.
|
||||
/// When absent (`None`), retry behaviour is automatic (default).
|
||||
#[serde(default)]
|
||||
pub impure: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub commands: HashMap<String, CommandConfig>,
|
||||
}
|
||||
|
||||
/// Per-command configuration.
|
||||
#[derive(Debug, Deserialize, Default, Clone)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct CommandConfig {
|
||||
/// When `Some(true)`, pass `--impure` to the underlying Nix command.
|
||||
/// When `Some(false)`, block automatic impure retries for this command.
|
||||
/// When absent (`None`), the global setting is used; if that is also absent,
|
||||
/// retry behaviour is automatic (default).
|
||||
#[serde(default)]
|
||||
pub impure: Option<bool>,
|
||||
/// Additional environment variables to set for the Nix command.
|
||||
#[serde(default)]
|
||||
pub env: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Return the [`CommandConfig`] for `command`.
|
||||
///
|
||||
/// Resolution order: per-command `impure` takes precedence over the global
|
||||
/// `impure`. Neither being set means automatic retry behaviour.
|
||||
pub fn for_command(&self, command: &str) -> CommandConfig {
|
||||
let mut cmd = self.commands.get(command).cloned().unwrap_or_default();
|
||||
// Per-command setting wins; fall back to global.
|
||||
if cmd.impure.is_none() {
|
||||
cmd.impure = self.impure;
|
||||
}
|
||||
cmd
|
||||
}
|
||||
}
|
||||
|
||||
/// Load configuration from the first `.eh.toml` found by walking up from the
|
||||
/// current directory, or from `~/.config/eh/config.toml` as a global
|
||||
/// fallback. Returns a default (empty) config if no file is found or if
|
||||
/// parsing fails.
|
||||
pub fn load() -> Config {
|
||||
if let Some(path) = find_project_config()
|
||||
&& let Some(cfg) = load_from_file(&path)
|
||||
{
|
||||
return cfg;
|
||||
}
|
||||
|
||||
if let Some(path) = global_config_path()
|
||||
&& let Some(cfg) = load_from_file(&path)
|
||||
{
|
||||
return cfg;
|
||||
}
|
||||
|
||||
Config::default()
|
||||
}
|
||||
|
||||
fn find_project_config() -> Option<PathBuf> {
|
||||
let mut dir = env::current_dir().ok()?;
|
||||
loop {
|
||||
let candidate = dir.join(".eh.toml");
|
||||
if candidate.exists() {
|
||||
return Some(candidate);
|
||||
}
|
||||
if !dir.pop() {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn global_config_path() -> Option<PathBuf> {
|
||||
let home = env::var("HOME").ok()?;
|
||||
Some(
|
||||
PathBuf::from(home)
|
||||
.join(".config")
|
||||
.join("eh")
|
||||
.join("config.toml"),
|
||||
)
|
||||
}
|
||||
|
||||
fn load_from_file(path: &Path) -> Option<Config> {
|
||||
let content = fs::read_to_string(path).ok()?;
|
||||
match toml::de::from_str::<Config>(&content) {
|
||||
Ok(cfg) => Some(cfg),
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"eh: warning: failed to parse config file {}: {}",
|
||||
path.display(),
|
||||
e
|
||||
);
|
||||
None
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_empty_config_defaults() {
|
||||
let cfg: Config = toml::from_str("").unwrap();
|
||||
assert!(cfg.impure.is_none());
|
||||
assert!(cfg.commands.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_command_config_impure_true() {
|
||||
let cfg: Config = toml::from_str(
|
||||
r#"
|
||||
[commands.build]
|
||||
impure = true
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(cfg.for_command("build").impure, Some(true));
|
||||
assert_eq!(cfg.for_command("run").impure, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_command_config_impure_false() {
|
||||
let cfg: Config = toml::from_str(
|
||||
r#"
|
||||
[commands.build]
|
||||
impure = false
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(cfg.for_command("build").impure, Some(false));
|
||||
assert_eq!(cfg.for_command("run").impure, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_global_impure_propagates_to_unconfigured_commands() {
|
||||
let cfg: Config = toml::from_str("impure = true").unwrap();
|
||||
// Commands with no per-command entry inherit global.
|
||||
assert_eq!(cfg.for_command("build").impure, Some(true));
|
||||
assert_eq!(cfg.for_command("nonexistent").impure, Some(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_global_impure_false_propagates_to_unconfigured_commands() {
|
||||
let cfg: Config = toml::from_str("impure = false").unwrap();
|
||||
assert_eq!(cfg.for_command("build").impure, Some(false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_per_command_impure_overrides_global() {
|
||||
// Per-command setting wins over global.
|
||||
let cfg: Config = toml::from_str(
|
||||
r#"
|
||||
impure = false
|
||||
|
||||
[commands.build]
|
||||
impure = true
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(cfg.for_command("build").impure, Some(true));
|
||||
// Command without per-command entry falls back to global false.
|
||||
assert_eq!(cfg.for_command("run").impure, Some(false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_command_config_env() {
|
||||
let cfg: Config = toml::from_str(
|
||||
r#"
|
||||
[commands.develop]
|
||||
env = { FOO = "bar", BAZ = "1" }
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let dev = cfg.for_command("develop");
|
||||
assert_eq!(dev.env.get("FOO").map(String::as_str), Some("bar"));
|
||||
assert_eq!(dev.env.get("BAZ").map(String::as_str), Some("1"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_command_config_env_table_syntax() {
|
||||
let cfg: Config = toml::from_str(
|
||||
r#"
|
||||
[commands.shell]
|
||||
impure = true
|
||||
|
||||
[commands.shell.env]
|
||||
MY_VAR = "hello"
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let shell = cfg.for_command("shell");
|
||||
assert_eq!(shell.impure, Some(true));
|
||||
assert_eq!(shell.env.get("MY_VAR").map(String::as_str), Some("hello"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_for_command_missing_returns_default() {
|
||||
let cfg = Config::default();
|
||||
let cc = cfg.for_command("nonexistent");
|
||||
assert_eq!(cc.impure, None);
|
||||
assert!(cc.env.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unknown_top_level_key_is_rejected() {
|
||||
let result = toml::de::from_str::<Config>("unknown_key = true");
|
||||
assert!(result.is_err(), "unknown top-level keys should be rejected");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unknown_command_key_is_rejected() {
|
||||
let result = toml::de::from_str::<Config>(
|
||||
r#"
|
||||
[commands.build]
|
||||
typo_key = true
|
||||
"#,
|
||||
);
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"unknown per-command keys should be rejected"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -54,6 +54,11 @@ pub enum EhError {
|
|||
|
||||
#[error("no inputs selected")]
|
||||
UpdateCancelled,
|
||||
|
||||
#[error(
|
||||
"package {reason} but `--impure` is disabled for `{command}` in config"
|
||||
)]
|
||||
ImpureRequired { command: String, reason: String },
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, EhError>;
|
||||
|
|
@ -77,6 +82,7 @@ impl EhError {
|
|||
Self::JsonParse { .. } => 13,
|
||||
Self::NoFlakeInputs => 14,
|
||||
Self::UpdateCancelled => 0,
|
||||
Self::ImpureRequired { .. } => 15,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -110,6 +116,12 @@ impl EhError {
|
|||
Self::NoFlakeInputs => {
|
||||
Some("run this from a directory with a flake.lock that has inputs")
|
||||
},
|
||||
Self::ImpureRequired { .. } => {
|
||||
Some(
|
||||
"set `impure = true` for this command (or globally) in .eh.toml or \
|
||||
~/.config/eh/config.toml, or pass `--impure` manually",
|
||||
)
|
||||
},
|
||||
Self::Io(_)
|
||||
| Self::Regex(_)
|
||||
| Self::Utf8(_)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
pub mod commands;
|
||||
pub mod config;
|
||||
pub mod error;
|
||||
pub mod util;
|
||||
|
||||
|
|
@ -31,6 +32,16 @@ pub enum Command {
|
|||
#[arg(trailing_var_arg = true)]
|
||||
args: Vec<String>,
|
||||
},
|
||||
/// Enter a Nix development shell
|
||||
Develop {
|
||||
#[arg(trailing_var_arg = true)]
|
||||
args: Vec<String>,
|
||||
},
|
||||
/// Show package information
|
||||
Info {
|
||||
#[arg(trailing_var_arg = true)]
|
||||
args: Vec<String>,
|
||||
},
|
||||
/// Update flake inputs interactively
|
||||
Update {
|
||||
#[arg(trailing_var_arg = true)]
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ use eh::{Cli, Command, CommandFactory, Parser};
|
|||
use yansi::Paint;
|
||||
|
||||
mod commands;
|
||||
mod config;
|
||||
mod error;
|
||||
mod util;
|
||||
|
||||
|
|
@ -29,16 +30,21 @@ fn handle_command(command: &str, args: &[String]) -> error::Result<i32> {
|
|||
let hash_extractor = util::RegexHashExtractor;
|
||||
let fixer = util::DefaultNixFileFixer;
|
||||
let classifier = util::DefaultNixErrorClassifier;
|
||||
let cfg = config::load();
|
||||
let cmd_cfg = cfg.for_command(command);
|
||||
|
||||
match command {
|
||||
"update" => commands::update::handle_update(args),
|
||||
"run" | "shell" | "build" => {
|
||||
"info" => commands::info::handle_info(args, &cmd_cfg),
|
||||
|
||||
"update" => commands::update::handle_update(args, &cmd_cfg),
|
||||
"run" | "shell" | "build" | "develop" => {
|
||||
commands::handle_nix_command(
|
||||
command,
|
||||
args,
|
||||
&hash_extractor,
|
||||
&fixer,
|
||||
&classifier,
|
||||
&cmd_cfg,
|
||||
)
|
||||
},
|
||||
_ => unreachable!(),
|
||||
|
|
@ -55,6 +61,8 @@ fn dispatch_multicall(
|
|||
"nr" => "run",
|
||||
"ns" => "shell",
|
||||
"nb" => "build",
|
||||
"nd" => "develop",
|
||||
"ni" => "info",
|
||||
"nu" => "update",
|
||||
_ => return None,
|
||||
};
|
||||
|
|
@ -104,6 +112,10 @@ fn run_app() -> error::Result<i32> {
|
|||
|
||||
Some(Command::Build { args }) => handle_command("build", &args),
|
||||
|
||||
Some(Command::Develop { args }) => handle_command("develop", &args),
|
||||
|
||||
Some(Command::Info { args }) => handle_command("info", &args),
|
||||
|
||||
Some(Command::Update { args }) => handle_command("update", &args),
|
||||
|
||||
None => {
|
||||
|
|
|
|||
113
eh/src/util.rs
113
eh/src/util.rs
|
|
@ -1,5 +1,5 @@
|
|||
use std::{
|
||||
io::{BufWriter, Write},
|
||||
io::{BufWriter, IsTerminal, Write},
|
||||
path::{Path, PathBuf},
|
||||
sync::LazyLock,
|
||||
};
|
||||
|
|
@ -42,6 +42,13 @@ static HASH_FIX_PATTERNS: LazyLock<[Regex; 3]> = LazyLock::new(|| {
|
|||
]
|
||||
});
|
||||
|
||||
/// Regex to extract suggestions from Nix's "Did you mean" error line.
|
||||
/// Matches patterns like:
|
||||
/// - "Did you mean one of hello, world, or foo?"
|
||||
/// - "Did you mean lib.hello?"
|
||||
static DID_YOU_MEAN_PATTERN: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new(r#"Did you mean (?:one of )?(.+?)\?"#).unwrap());
|
||||
|
||||
/// Trait for extracting store paths and hashes from nix output.
|
||||
pub trait HashExtractor {
|
||||
/// Extract the new store path/hash from nix output.
|
||||
|
|
@ -311,9 +318,20 @@ fn is_hash_mismatch_error(stderr: &str) -> bool {
|
|||
|
||||
/// Construct the eval expression for a given argument.
|
||||
/// Handles both plain package names and flake references.
|
||||
fn make_eval_expr(eval_arg: &str) -> String {
|
||||
pub fn make_eval_expr(eval_arg: &str) -> String {
|
||||
// Handle . (current directory) as .# (default package of current flake)
|
||||
// Nix treats `nix build .` and `nix build .#` as equivalent
|
||||
let eval_arg = if eval_arg == "." { ".#" } else { eval_arg };
|
||||
|
||||
if eval_arg.contains('#') {
|
||||
format!("{eval_arg}.meta")
|
||||
// Handle .# (current flake default package) case
|
||||
// .# needs to become .#default for meta evaluation to work
|
||||
// because .#.meta evaluates 'meta' on the flake itself, not the package
|
||||
if eval_arg.ends_with('#') {
|
||||
format!("{eval_arg}default.meta")
|
||||
} else {
|
||||
format!("{eval_arg}.meta")
|
||||
}
|
||||
} else {
|
||||
format!("nixpkgs#{eval_arg}.meta")
|
||||
}
|
||||
|
|
@ -467,6 +485,7 @@ pub fn handle_nix_with_retry(
|
|||
fixer: &dyn NixFileFixer,
|
||||
classifier: &dyn NixErrorClassifier,
|
||||
interactive: bool,
|
||||
cfg: &crate::config::CommandConfig,
|
||||
) -> Result<i32> {
|
||||
validate_nix_args(args)?;
|
||||
|
||||
|
|
@ -476,10 +495,17 @@ pub fn handle_nix_with_retry(
|
|||
let pkg = package_name(args);
|
||||
let pre_eval_action = pre_evaluate(args)?;
|
||||
if let Some((env_var, reason)) = pre_eval_action.env_override() {
|
||||
if cfg.impure == Some(false) {
|
||||
return Err(EhError::ImpureRequired {
|
||||
command: subcommand.to_string(),
|
||||
reason: reason.to_string(),
|
||||
});
|
||||
}
|
||||
print_retry_msg(pkg, reason, env_var);
|
||||
let mut retry_cmd = NixCommand::new(subcommand)
|
||||
.print_build_logs(true)
|
||||
.args_ref(args)
|
||||
.with_config(cfg)
|
||||
.env(env_var, "1")
|
||||
.impure(true);
|
||||
if interactive {
|
||||
|
|
@ -495,6 +521,7 @@ pub fn handle_nix_with_retry(
|
|||
.print_build_logs(true)
|
||||
.interactive(true)
|
||||
.args_ref(args)
|
||||
.with_config(cfg)
|
||||
.run_with_logs(StdIoInterceptor)?;
|
||||
if status.success() {
|
||||
return Ok(0);
|
||||
|
|
@ -504,13 +531,38 @@ pub fn handle_nix_with_retry(
|
|||
// Capture output to check for errors that need retry (hash mismatches etc.)
|
||||
let output_cmd = NixCommand::new(subcommand)
|
||||
.print_build_logs(true)
|
||||
.args_ref(args);
|
||||
.args_ref(args)
|
||||
.with_config(cfg);
|
||||
let output = output_cmd.output()?;
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
|
||||
// Check for hash mismatch errors
|
||||
if let Some(new_hash) = hash_extractor.extract_hash(&stderr) {
|
||||
let old_hash = hash_extractor.extract_old_hash(&stderr);
|
||||
|
||||
// Ask for confirmation before fixing hash (skip in non-interactive mode)
|
||||
let should_fix = if std::io::stdin().is_terminal() {
|
||||
dialoguer::Confirm::new()
|
||||
.with_prompt(format!(
|
||||
"Hash mismatch detected for {}. Update hash in local .nix files?",
|
||||
pkg.bold()
|
||||
))
|
||||
.default(true)
|
||||
.interact()
|
||||
.map_err(|e| EhError::Io(std::io::Error::other(e)))?
|
||||
} else {
|
||||
log_warn!(
|
||||
"{}: hash mismatch detected in non-interactive mode, skipping auto-fix",
|
||||
pkg.bold()
|
||||
);
|
||||
false
|
||||
};
|
||||
|
||||
if !should_fix {
|
||||
log_warn!("{}: hash fix cancelled", pkg.bold());
|
||||
return Err(EhError::ProcessExit { code: 1 });
|
||||
}
|
||||
|
||||
match fixer.fix_hash_in_files(old_hash.as_deref(), &new_hash) {
|
||||
Ok(true) => {
|
||||
log_info!(
|
||||
|
|
@ -519,7 +571,8 @@ pub fn handle_nix_with_retry(
|
|||
);
|
||||
let mut retry_cmd = NixCommand::new(subcommand)
|
||||
.print_build_logs(true)
|
||||
.args_ref(args);
|
||||
.args_ref(args)
|
||||
.with_config(cfg);
|
||||
if interactive {
|
||||
retry_cmd = retry_cmd.interactive(true);
|
||||
}
|
||||
|
|
@ -552,10 +605,17 @@ pub fn handle_nix_with_retry(
|
|||
if classifier.should_retry(&stderr) {
|
||||
let action = classify_retry_action(&stderr);
|
||||
if let Some((env_var, reason)) = action.env_override() {
|
||||
if cfg.impure == Some(false) {
|
||||
return Err(EhError::ImpureRequired {
|
||||
command: subcommand.to_string(),
|
||||
reason: reason.to_string(),
|
||||
});
|
||||
}
|
||||
print_retry_msg(pkg, reason, env_var);
|
||||
let mut retry_cmd = NixCommand::new(subcommand)
|
||||
.print_build_logs(true)
|
||||
.args_ref(args)
|
||||
.with_config(cfg)
|
||||
.env(env_var, "1")
|
||||
.impure(true);
|
||||
if interactive {
|
||||
|
|
@ -576,6 +636,9 @@ pub fn handle_nix_with_retry(
|
|||
.write_all(&output.stderr)
|
||||
.map_err(EhError::Io)?;
|
||||
|
||||
// Print contextual suggestions for common errors
|
||||
print_error_suggestions(&output.stderr);
|
||||
|
||||
match output.status.code() {
|
||||
Some(code) => Err(EhError::ProcessExit { code }),
|
||||
// No exit code means the process was killed by a signal
|
||||
|
|
@ -595,6 +658,46 @@ impl NixErrorClassifier for DefaultNixErrorClassifier {
|
|||
}
|
||||
}
|
||||
|
||||
/// Parse suggestions from Nix's "Did you mean" error line.
|
||||
/// Input: "Did you mean one of neovim, hevi, navi, neo or neo4j?"
|
||||
/// Output: vec!["neovim", "hevi", "navi", "neo", "neo4j"]
|
||||
fn parse_nix_suggestions(did_you_mean_line: &str) -> Vec<String> {
|
||||
DID_YOU_MEAN_PATTERN
|
||||
.captures(did_you_mean_line)
|
||||
.and_then(|caps| caps.get(1))
|
||||
.map(|m| m.as_str())
|
||||
.map(|suggestions| {
|
||||
suggestions
|
||||
.split(", ")
|
||||
.flat_map(|part| part.split(" or "))
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Print contextual error suggestions when a command fails.
|
||||
/// Parses Nix's own "Did you mean" suggestions from stderr and presents them
|
||||
/// nicely to the user.
|
||||
pub fn print_error_suggestions(stderr: &[u8]) {
|
||||
let stderr_str = String::from_utf8_lossy(stderr);
|
||||
|
||||
// Look for Nix's "Did you mean" line in the error output
|
||||
if let Some(line) = stderr_str.lines().find(|l| l.contains("Did you mean")) {
|
||||
let suggestions = parse_nix_suggestions(line);
|
||||
|
||||
if !suggestions.is_empty() {
|
||||
let formatted = suggestions
|
||||
.iter()
|
||||
.map(|s| s.bold().to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
log_info!("Did you mean: {}?", formatted);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::io::Write;
|
||||
|
|
|
|||
6
flake.lock
generated
6
flake.lock
generated
|
|
@ -2,11 +2,11 @@
|
|||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1769461804,
|
||||
"narHash": "sha256-msG8SU5WsBUfVVa/9RPLaymvi5bI8edTavbIq3vRlhI=",
|
||||
"lastModified": 1776548001,
|
||||
"narHash": "sha256-ZSK0NL4a1BwVbbTBoSnWgbJy9HeZFXLYQizjb2DPF24=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "bfc1b8a4574108ceef22f02bafcf6611380c100d",
|
||||
"rev": "b12141ef619e0a9c1c84dc8c684040326f27cdcc",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue