Compare commits
No commits in common. "6f9c6893e1f419194ff00296486e64fd7f0304ec" and "0d50b374bdd883c406c50b11532088a3deac55f8" have entirely different histories.
6f9c6893e1
...
0d50b374bd
16 changed files with 617 additions and 1482 deletions
287
Cargo.lock
generated
287
Cargo.lock
generated
|
|
@ -4,18 +4,18 @@ version = 4
|
|||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.4"
|
||||
version = "1.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
|
||||
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.13"
|
||||
version = "1.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
|
||||
checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
|
|
@ -25,15 +25,15 @@ checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
|
|||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.56"
|
||||
version = "4.5.51"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a75ca66430e33a14957acc24c5077b503e7d374151b2b4b3a10c83b4ceb4be0e"
|
||||
checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
|
|
@ -41,9 +41,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.56"
|
||||
version = "4.5.51"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "793207c7fa6300a0608d1080b858e5fdbe713cdc1c8db9fb17777d8a13e63df0"
|
||||
checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
|
|
@ -51,18 +51,18 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "clap_complete"
|
||||
version = "4.5.65"
|
||||
version = "4.5.60"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "430b4dc2b5e3861848de79627b2bedc9f3342c7da5173a14eaa5d0f8dc18ae5d"
|
||||
checksum = "8e602857739c5a4291dfa33b5a298aeac9006185229a700e5810a3ef7272d971"
|
||||
dependencies = [
|
||||
"clap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.5.55"
|
||||
version = "4.5.49"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5"
|
||||
checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
|
|
@ -72,61 +72,24 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "0.7.7"
|
||||
version = "0.7.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32"
|
||||
|
||||
[[package]]
|
||||
name = "console"
|
||||
version = "0.16.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03e45a4a8926227e4197636ba97a9fc9b00477e9f4bd711395687c5f0734bec4"
|
||||
dependencies = [
|
||||
"encode_unicode",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"unicode-width",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dialoguer"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "25f104b501bf2364e78d0d3974cbc774f738f5865306ed128e1e0d7499c0ad96"
|
||||
dependencies = [
|
||||
"console",
|
||||
"shell-words",
|
||||
]
|
||||
checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675"
|
||||
|
||||
[[package]]
|
||||
name = "eh"
|
||||
version = "0.1.4"
|
||||
version = "0.1.2"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"dialoguer",
|
||||
"eh-log",
|
||||
"regex",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"thiserror",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"walkdir",
|
||||
"yansi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "eh-log"
|
||||
version = "0.1.4"
|
||||
dependencies = [
|
||||
"yansi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "encode_unicode"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.14"
|
||||
|
|
@ -162,16 +125,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.17"
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.180"
|
||||
version = "0.2.177"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
|
||||
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
|
|
@ -180,10 +143,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.6"
|
||||
name = "log"
|
||||
version = "0.4.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
|
||||
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
|
||||
|
||||
[[package]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.50.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
|
|
@ -192,19 +170,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.95"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.44"
|
||||
version = "1.0.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
|
||||
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
|
@ -240,15 +224,15 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.8"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
|
||||
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "1.1.3"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34"
|
||||
checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"errno",
|
||||
|
|
@ -267,58 +251,25 @@ dependencies = [
|
|||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
name = "sharded-slab"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
name = "smallvec"
|
||||
version = "1.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.149"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shell-words"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77"
|
||||
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.114"
|
||||
version = "2.0.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a"
|
||||
checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
|
@ -327,9 +278,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.24.0"
|
||||
version = "3.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c"
|
||||
checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"getrandom",
|
||||
|
|
@ -340,18 +291,18 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.18"
|
||||
version = "2.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
|
||||
checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.18"
|
||||
version = "2.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
|
||||
checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
|
@ -359,16 +310,82 @@ dependencies = [
|
|||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.22"
|
||||
name = "thread_local"
|
||||
version = "1.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
|
||||
checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.2.2"
|
||||
name = "tracing"
|
||||
version = "0.1.41"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
|
||||
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
|
||||
dependencies = [
|
||||
"pin-project-lite",
|
||||
"tracing-attributes",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-attributes"
|
||||
version = "0.1.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-core"
|
||||
version = "0.1.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"valuable",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-log"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
|
||||
dependencies = [
|
||||
"log",
|
||||
"once_cell",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-subscriber"
|
||||
version = "0.3.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5"
|
||||
dependencies = [
|
||||
"nu-ansi-term",
|
||||
"sharded-slab",
|
||||
"smallvec",
|
||||
"thread_local",
|
||||
"tracing-core",
|
||||
"tracing-log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
|
|
@ -382,9 +399,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wasip2"
|
||||
version = "1.0.2+wasi-0.2.9"
|
||||
version = "1.0.1+wasi-0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
|
||||
checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7"
|
||||
dependencies = [
|
||||
"wit-bindgen",
|
||||
]
|
||||
|
|
@ -415,13 +432,13 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.51.0"
|
||||
version = "0.46.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
|
||||
checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
|
||||
|
||||
[[package]]
|
||||
name = "xtask"
|
||||
version = "0.1.4"
|
||||
version = "0.1.2"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"clap_complete",
|
||||
|
|
@ -433,9 +450,3 @@ name = "yansi"
|
|||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02aae0f83f69aafc94776e879363e9771d7ecbffe2c7fbb6c14c5e00dfe88439"
|
||||
|
|
|
|||
30
Cargo.toml
30
Cargo.toml
|
|
@ -1,7 +1,6 @@
|
|||
[workspace]
|
||||
default-members = [ "eh" ]
|
||||
members = [ "eh", "crates/*" ]
|
||||
resolver = "3"
|
||||
members = [ "eh", "xtask" ]
|
||||
resolver = "3"
|
||||
|
||||
[workspace.package]
|
||||
authors = [ "NotAShelf <raf@notashelf.dev>" ]
|
||||
|
|
@ -9,22 +8,19 @@ description = "Ergonomic Nix CLI helper"
|
|||
edition = "2024"
|
||||
license = "MPL-2.0"
|
||||
readme = true
|
||||
rust-version = "1.90"
|
||||
version = "0.1.4"
|
||||
rust-version = "1.89"
|
||||
version = "0.1.2"
|
||||
|
||||
[workspace.dependencies]
|
||||
clap = { default-features = false, features = [ "std", "help", "derive" ], version = "4.5.56" }
|
||||
clap_complete = "4.5.65"
|
||||
dialoguer = { default-features = false, version = "0.12.0" }
|
||||
regex = "1.12.2"
|
||||
serde_json = "1.0.149"
|
||||
tempfile = "3.24.0"
|
||||
thiserror = "2.0.18"
|
||||
walkdir = "2.5.0"
|
||||
yansi = "1.0.1"
|
||||
|
||||
eh = { path = "./eh" }
|
||||
eh-log = { path = "./crates/eh-log" }
|
||||
clap = { default-features = false, features = [ "std", "help", "derive" ], version = "4.5.51" }
|
||||
clap_complete = "4.5.60"
|
||||
regex = "1.12.2"
|
||||
tempfile = "3.23.0"
|
||||
thiserror = "2.0.17"
|
||||
tracing = "0.1.41"
|
||||
tracing-subscriber = "0.3.20"
|
||||
walkdir = "2.5.0"
|
||||
yansi = "1.0.1"
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
|
|
|
|||
|
|
@ -1,10 +0,0 @@
|
|||
[package]
|
||||
name = "eh-log"
|
||||
description = "Styled logging for eh"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
yansi.workspace = true
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
use std::fmt;
|
||||
|
||||
use yansi::Paint;
|
||||
|
||||
pub fn info(args: fmt::Arguments) {
|
||||
eprintln!(" {} {args}", "->".green().bold());
|
||||
}
|
||||
|
||||
pub fn warn(args: fmt::Arguments) {
|
||||
eprintln!(" {} {args}", "->".yellow().bold());
|
||||
}
|
||||
|
||||
pub fn error(args: fmt::Arguments) {
|
||||
eprintln!(" {} {args}", "!".red().bold());
|
||||
}
|
||||
|
||||
pub fn hint(args: fmt::Arguments) {
|
||||
eprintln!(" {} {args}", "~".yellow().dim());
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! log_info { ($($t:tt)*) => { $crate::info(format_args!($($t)*)) } }
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! log_warn { ($($t:tt)*) => { $crate::warn(format_args!($($t)*)) } }
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! log_error { ($($t:tt)*) => { $crate::error(format_args!($($t)*)) } }
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! log_hint { ($($t:tt)*) => { $crate::hint(format_args!($($t)*)) } }
|
||||
|
|
@ -11,12 +11,11 @@ crate-type = [ "lib" ]
|
|||
name = "eh"
|
||||
|
||||
[dependencies]
|
||||
clap.workspace = true
|
||||
dialoguer.workspace = true
|
||||
eh-log.workspace = true
|
||||
regex.workspace = true
|
||||
serde_json.workspace = true
|
||||
tempfile.workspace = true
|
||||
thiserror.workspace = true
|
||||
walkdir.workspace = true
|
||||
yansi.workspace = true
|
||||
clap.workspace = true
|
||||
regex.workspace = true
|
||||
tempfile.workspace = true
|
||||
thiserror.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
walkdir.workspace = true
|
||||
yansi.workspace = true
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
use std::{
|
||||
collections::VecDeque,
|
||||
io::{self, Read, Write},
|
||||
process::{Command, ExitStatus, Output, Stdio},
|
||||
sync::mpsc,
|
||||
thread,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use crate::error::{EhError, Result};
|
||||
|
|
@ -29,43 +27,6 @@ impl LogInterceptor for StdIoInterceptor {
|
|||
/// Default buffer size for reading command output
|
||||
const DEFAULT_BUFFER_SIZE: usize = 4096;
|
||||
|
||||
/// Default timeout for command execution
|
||||
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(300); // 5 minutes
|
||||
|
||||
enum PipeEvent {
|
||||
Stdout(Vec<u8>),
|
||||
Stderr(Vec<u8>),
|
||||
Error(io::Error),
|
||||
}
|
||||
|
||||
/// Drain a pipe reader, sending chunks through the channel.
|
||||
fn read_pipe<R: Read>(
|
||||
mut reader: R,
|
||||
tx: mpsc::Sender<PipeEvent>,
|
||||
is_stderr: bool,
|
||||
) {
|
||||
let mut buf = [0u8; DEFAULT_BUFFER_SIZE];
|
||||
loop {
|
||||
match reader.read(&mut buf) {
|
||||
Ok(0) => break,
|
||||
Ok(n) => {
|
||||
let event = if is_stderr {
|
||||
PipeEvent::Stderr(buf[..n].to_vec())
|
||||
} else {
|
||||
PipeEvent::Stdout(buf[..n].to_vec())
|
||||
};
|
||||
if tx.send(event).is_err() {
|
||||
break;
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
let _ = tx.send(PipeEvent::Error(e));
|
||||
break;
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder and executor for Nix commands.
|
||||
pub struct NixCommand {
|
||||
subcommand: String,
|
||||
|
|
@ -93,6 +54,16 @@ impl NixCommand {
|
|||
self
|
||||
}
|
||||
|
||||
#[allow(dead_code, reason = "FIXME")]
|
||||
pub fn args<I, S>(mut self, args: I) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = S>,
|
||||
S: Into<String>,
|
||||
{
|
||||
self.args.extend(args.into_iter().map(Into::into));
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn args_ref(mut self, args: &[String]) -> Self {
|
||||
self.args.extend(args.iter().cloned());
|
||||
|
|
@ -126,9 +97,11 @@ impl NixCommand {
|
|||
self
|
||||
}
|
||||
|
||||
/// Build the underlying `std::process::Command` with all configured
|
||||
/// arguments, environment variables, and flags.
|
||||
fn build_command(&self) -> Command {
|
||||
/// Run the command, streaming output to the provided interceptor.
|
||||
pub fn run_with_logs<I: LogInterceptor + 'static>(
|
||||
&self,
|
||||
mut interceptor: I,
|
||||
) -> Result<ExitStatus> {
|
||||
let mut cmd = Command::new("nix");
|
||||
cmd.arg(&self.subcommand);
|
||||
|
||||
|
|
@ -144,18 +117,6 @@ impl NixCommand {
|
|||
cmd.env(k, v);
|
||||
}
|
||||
cmd.args(&self.args);
|
||||
cmd
|
||||
}
|
||||
|
||||
/// Run the command, streaming output to the provided interceptor.
|
||||
///
|
||||
/// Stdout and stderr are read concurrently using background threads
|
||||
/// so that neither pipe blocks the other.
|
||||
pub fn run_with_logs<I: LogInterceptor + 'static>(
|
||||
&self,
|
||||
mut interceptor: I,
|
||||
) -> Result<ExitStatus> {
|
||||
let mut cmd = self.build_command();
|
||||
|
||||
if self.interactive {
|
||||
cmd.stdout(Stdio::inherit());
|
||||
|
|
@ -168,152 +129,86 @@ impl NixCommand {
|
|||
cmd.stderr(Stdio::piped());
|
||||
|
||||
let mut child = cmd.spawn()?;
|
||||
let stdout = child.stdout.take().ok_or_else(|| {
|
||||
let child_stdout = child.stdout.take().ok_or_else(|| {
|
||||
EhError::CommandFailed {
|
||||
command: format!("nix {}", self.subcommand),
|
||||
}
|
||||
})?;
|
||||
let stderr = child.stderr.take().ok_or_else(|| {
|
||||
let child_stderr = child.stderr.take().ok_or_else(|| {
|
||||
EhError::CommandFailed {
|
||||
command: format!("nix {}", self.subcommand),
|
||||
}
|
||||
})?;
|
||||
let mut stdout = child_stdout;
|
||||
let mut stderr = child_stderr;
|
||||
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let mut out_buf = [0u8; DEFAULT_BUFFER_SIZE];
|
||||
let mut err_buf = [0u8; DEFAULT_BUFFER_SIZE];
|
||||
|
||||
let tx_out = tx.clone();
|
||||
let stdout_thread = thread::spawn(move || read_pipe(stdout, tx_out, false));
|
||||
|
||||
let tx_err = tx;
|
||||
let stderr_thread = thread::spawn(move || read_pipe(stderr, tx_err, true));
|
||||
|
||||
let start_time = Instant::now();
|
||||
let mut out_queue = VecDeque::new();
|
||||
let mut err_queue = VecDeque::new();
|
||||
|
||||
loop {
|
||||
if start_time.elapsed() > DEFAULT_TIMEOUT {
|
||||
let _ = child.kill();
|
||||
let _ = stdout_thread.join();
|
||||
let _ = stderr_thread.join();
|
||||
let _ = child.wait();
|
||||
return Err(EhError::Timeout {
|
||||
command: format!("nix {}", self.subcommand),
|
||||
duration: DEFAULT_TIMEOUT,
|
||||
});
|
||||
let mut did_something = false;
|
||||
|
||||
match stdout.read(&mut out_buf) {
|
||||
Ok(0) => {},
|
||||
Ok(n) => {
|
||||
interceptor.on_stdout(&out_buf[..n]);
|
||||
out_queue.push_back(Vec::from(&out_buf[..n]));
|
||||
did_something = true;
|
||||
},
|
||||
Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => {},
|
||||
Err(e) => return Err(EhError::Io(e)),
|
||||
}
|
||||
|
||||
match rx.recv_timeout(Duration::from_millis(100)) {
|
||||
Ok(PipeEvent::Stdout(data)) => interceptor.on_stdout(&data),
|
||||
Ok(PipeEvent::Stderr(data)) => interceptor.on_stderr(&data),
|
||||
Ok(PipeEvent::Error(e)) => {
|
||||
let _ = child.kill();
|
||||
let _ = stdout_thread.join();
|
||||
let _ = stderr_thread.join();
|
||||
let _ = child.wait();
|
||||
return Err(EhError::Io(e));
|
||||
match stderr.read(&mut err_buf) {
|
||||
Ok(0) => {},
|
||||
Ok(n) => {
|
||||
interceptor.on_stderr(&err_buf[..n]);
|
||||
err_queue.push_back(Vec::from(&err_buf[..n]));
|
||||
did_something = true;
|
||||
},
|
||||
Err(mpsc::RecvTimeoutError::Timeout) => {},
|
||||
// All senders dropped — both reader threads finished
|
||||
Err(mpsc::RecvTimeoutError::Disconnected) => break,
|
||||
Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => {},
|
||||
Err(e) => return Err(EhError::Io(e)),
|
||||
}
|
||||
|
||||
if !did_something && child.try_wait()?.is_some() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let _ = stdout_thread.join();
|
||||
let _ = stderr_thread.join();
|
||||
|
||||
let status = child.wait()?;
|
||||
Ok(status)
|
||||
}
|
||||
|
||||
/// Run the command and capture all output (with timeout).
|
||||
/// Run the command and capture all output.
|
||||
pub fn output(&self) -> Result<Output> {
|
||||
let mut cmd = self.build_command();
|
||||
let mut cmd = Command::new("nix");
|
||||
cmd.arg(&self.subcommand);
|
||||
|
||||
if self.print_build_logs
|
||||
&& !self.args.iter().any(|a| a == "--no-build-output")
|
||||
{
|
||||
cmd.arg("--print-build-logs");
|
||||
}
|
||||
if self.impure {
|
||||
cmd.arg("--impure");
|
||||
}
|
||||
for (k, v) in &self.env {
|
||||
cmd.env(k, v);
|
||||
}
|
||||
cmd.args(&self.args);
|
||||
|
||||
if self.interactive {
|
||||
cmd.stdout(Stdio::inherit());
|
||||
cmd.stderr(Stdio::inherit());
|
||||
cmd.stdin(Stdio::inherit());
|
||||
return Ok(cmd.output()?);
|
||||
} else {
|
||||
cmd.stdout(Stdio::piped());
|
||||
cmd.stderr(Stdio::piped());
|
||||
}
|
||||
|
||||
cmd.stdout(Stdio::piped());
|
||||
cmd.stderr(Stdio::piped());
|
||||
|
||||
let mut child = cmd.spawn()?;
|
||||
let stdout = child.stdout.take();
|
||||
let stderr = child.stderr.take();
|
||||
|
||||
let (tx, rx) = mpsc::channel();
|
||||
|
||||
let tx_out = tx.clone();
|
||||
let stdout_thread = thread::spawn(move || {
|
||||
let mut buf = Vec::new();
|
||||
if let Some(mut r) = stdout {
|
||||
let _ = r.read_to_end(&mut buf);
|
||||
}
|
||||
let _ = tx_out.send((false, buf));
|
||||
});
|
||||
|
||||
let tx_err = tx;
|
||||
let stderr_thread = thread::spawn(move || {
|
||||
let mut buf = Vec::new();
|
||||
if let Some(mut r) = stderr {
|
||||
let _ = r.read_to_end(&mut buf);
|
||||
}
|
||||
let _ = tx_err.send((true, buf));
|
||||
});
|
||||
|
||||
let start_time = Instant::now();
|
||||
let mut stdout_buf = Vec::new();
|
||||
let mut stderr_buf = Vec::new();
|
||||
let mut received = 0;
|
||||
|
||||
while received < 2 {
|
||||
let remaining = DEFAULT_TIMEOUT
|
||||
.checked_sub(start_time.elapsed())
|
||||
.unwrap_or(Duration::ZERO);
|
||||
|
||||
if remaining.is_zero() {
|
||||
let _ = child.kill();
|
||||
let _ = stdout_thread.join();
|
||||
let _ = stderr_thread.join();
|
||||
let _ = child.wait();
|
||||
return Err(EhError::Timeout {
|
||||
command: format!("nix {}", self.subcommand),
|
||||
duration: DEFAULT_TIMEOUT,
|
||||
});
|
||||
}
|
||||
|
||||
match rx.recv_timeout(remaining) {
|
||||
Ok((true, buf)) => {
|
||||
stderr_buf = buf;
|
||||
received += 1;
|
||||
},
|
||||
Ok((false, buf)) => {
|
||||
stdout_buf = buf;
|
||||
received += 1;
|
||||
},
|
||||
Err(mpsc::RecvTimeoutError::Timeout) => {
|
||||
let _ = child.kill();
|
||||
let _ = stdout_thread.join();
|
||||
let _ = stderr_thread.join();
|
||||
let _ = child.wait();
|
||||
return Err(EhError::Timeout {
|
||||
command: format!("nix {}", self.subcommand),
|
||||
duration: DEFAULT_TIMEOUT,
|
||||
});
|
||||
},
|
||||
Err(mpsc::RecvTimeoutError::Disconnected) => break,
|
||||
}
|
||||
}
|
||||
|
||||
let _ = stdout_thread.join();
|
||||
let _ = stderr_thread.join();
|
||||
|
||||
let status = child.wait()?;
|
||||
Ok(Output {
|
||||
status,
|
||||
stdout: stdout_buf,
|
||||
stderr: stderr_buf,
|
||||
})
|
||||
Ok(cmd.output()?)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
253
eh/src/error.rs
253
eh/src/error.rs
|
|
@ -1,59 +1,36 @@
|
|||
use std::time::Duration;
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum EhError {
|
||||
#[error("nix {command} failed")]
|
||||
NixCommandFailed { command: String },
|
||||
#[error("Nix command failed: {0}")]
|
||||
NixCommandFailed(String),
|
||||
|
||||
#[error("io: {0}")]
|
||||
#[error("IO error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
#[error("regex: {0}")]
|
||||
#[error("Regex error: {0}")]
|
||||
Regex(#[from] regex::Error),
|
||||
|
||||
#[error("utf-8 conversion: {0}")]
|
||||
#[error("UTF-8 conversion error: {0}")]
|
||||
Utf8(#[from] std::string::FromUtf8Error),
|
||||
|
||||
#[error("could not extract hash from nix output")]
|
||||
HashExtractionFailed { stderr: String },
|
||||
#[error("Hash extraction failed")]
|
||||
HashExtractionFailed,
|
||||
|
||||
#[error("no .nix files found in the current directory")]
|
||||
#[error("No Nix files found")]
|
||||
NoNixFilesFound,
|
||||
|
||||
#[error("could not update hash in {path}")]
|
||||
#[error("Failed to fix hash in file: {path}")]
|
||||
HashFixFailed { path: String },
|
||||
|
||||
#[error("process exited with code {code}")]
|
||||
#[error("Process exited with code: {code}")]
|
||||
ProcessExit { code: i32 },
|
||||
|
||||
#[error("command '{command}' failed")]
|
||||
#[error("Command execution failed: {command}")]
|
||||
CommandFailed { command: String },
|
||||
|
||||
#[error("nix {command} timed out after {} seconds", duration.as_secs())]
|
||||
Timeout {
|
||||
command: String,
|
||||
duration: Duration,
|
||||
},
|
||||
|
||||
#[error("'{expression}' failed to evaluate: {stderr}")]
|
||||
PreEvalFailed {
|
||||
expression: String,
|
||||
stderr: String,
|
||||
},
|
||||
|
||||
#[error("invalid input '{input}': {reason}")]
|
||||
#[error("Invalid input: {input} - {reason}")]
|
||||
InvalidInput { input: String, reason: String },
|
||||
|
||||
#[error("failed to parse JSON from nix output: {detail}")]
|
||||
JsonParse { detail: String },
|
||||
|
||||
#[error("no flake inputs found in lock file")]
|
||||
NoFlakeInputs,
|
||||
|
||||
#[error("no inputs selected")]
|
||||
UpdateCancelled,
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, EhError>;
|
||||
|
|
@ -63,209 +40,9 @@ impl EhError {
|
|||
pub const fn exit_code(&self) -> i32 {
|
||||
match self {
|
||||
Self::ProcessExit { code } => *code,
|
||||
Self::NixCommandFailed { .. } => 2,
|
||||
Self::CommandFailed { .. } => 3,
|
||||
Self::HashExtractionFailed { .. } => 4,
|
||||
Self::NoNixFilesFound => 5,
|
||||
Self::HashFixFailed { .. } => 6,
|
||||
Self::InvalidInput { .. } => 7,
|
||||
Self::Io(_) => 8,
|
||||
Self::Regex(_) => 9,
|
||||
Self::Utf8(_) => 10,
|
||||
Self::Timeout { .. } => 11,
|
||||
Self::PreEvalFailed { .. } => 12,
|
||||
Self::JsonParse { .. } => 13,
|
||||
Self::NoFlakeInputs => 14,
|
||||
Self::UpdateCancelled => 0,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn hint(&self) -> Option<&str> {
|
||||
match self {
|
||||
Self::NixCommandFailed { .. } => {
|
||||
Some("run with --show-trace for more details")
|
||||
},
|
||||
Self::PreEvalFailed { .. } => {
|
||||
Some("check that the expression exists and is spelled correctly")
|
||||
},
|
||||
Self::HashExtractionFailed { .. } => {
|
||||
Some("nix reported a hash mismatch but the hash could not be parsed")
|
||||
},
|
||||
Self::NoNixFilesFound => {
|
||||
Some("run this command from a directory containing .nix files")
|
||||
},
|
||||
Self::Timeout { .. } => {
|
||||
Some(
|
||||
"the command took too long; try a faster network or a smaller \
|
||||
derivation",
|
||||
)
|
||||
},
|
||||
Self::InvalidInput { .. } => {
|
||||
Some("avoid shell metacharacters in nix arguments")
|
||||
},
|
||||
Self::JsonParse { .. } => {
|
||||
Some("ensure 'nix flake metadata --json' produces valid output")
|
||||
},
|
||||
Self::NoFlakeInputs => {
|
||||
Some("run this from a directory with a flake.lock that has inputs")
|
||||
},
|
||||
Self::Io(_)
|
||||
| Self::Regex(_)
|
||||
| Self::Utf8(_)
|
||||
| Self::HashFixFailed { .. }
|
||||
| Self::ProcessExit { .. }
|
||||
| Self::CommandFailed { .. }
|
||||
| Self::UpdateCancelled => None,
|
||||
Self::NixCommandFailed(_) => 1,
|
||||
Self::CommandFailed { .. } => 1,
|
||||
_ => 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_exit_codes() {
|
||||
assert_eq!(
|
||||
EhError::NixCommandFailed {
|
||||
command: "build".into(),
|
||||
}
|
||||
.exit_code(),
|
||||
2
|
||||
);
|
||||
assert_eq!(
|
||||
EhError::CommandFailed {
|
||||
command: "x".into(),
|
||||
}
|
||||
.exit_code(),
|
||||
3
|
||||
);
|
||||
assert_eq!(
|
||||
EhError::HashExtractionFailed {
|
||||
stderr: String::new(),
|
||||
}
|
||||
.exit_code(),
|
||||
4
|
||||
);
|
||||
assert_eq!(EhError::NoNixFilesFound.exit_code(), 5);
|
||||
assert_eq!(EhError::HashFixFailed { path: "x".into() }.exit_code(), 6);
|
||||
assert_eq!(
|
||||
EhError::InvalidInput {
|
||||
input: "x".into(),
|
||||
reason: "y".into(),
|
||||
}
|
||||
.exit_code(),
|
||||
7
|
||||
);
|
||||
assert_eq!(
|
||||
EhError::Timeout {
|
||||
command: "build".into(),
|
||||
duration: Duration::from_secs(300),
|
||||
}
|
||||
.exit_code(),
|
||||
11
|
||||
);
|
||||
assert_eq!(
|
||||
EhError::PreEvalFailed {
|
||||
expression: "x".into(),
|
||||
stderr: "y".into(),
|
||||
}
|
||||
.exit_code(),
|
||||
12
|
||||
);
|
||||
assert_eq!(EhError::ProcessExit { code: 42 }.exit_code(), 42);
|
||||
assert_eq!(
|
||||
EhError::JsonParse {
|
||||
detail: "x".into(),
|
||||
}
|
||||
.exit_code(),
|
||||
13
|
||||
);
|
||||
assert_eq!(EhError::NoFlakeInputs.exit_code(), 14);
|
||||
assert_eq!(EhError::UpdateCancelled.exit_code(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_messages() {
|
||||
let err = EhError::Timeout {
|
||||
command: "build".into(),
|
||||
duration: Duration::from_secs(300),
|
||||
};
|
||||
assert_eq!(err.to_string(), "nix build timed out after 300 seconds");
|
||||
|
||||
let err = EhError::PreEvalFailed {
|
||||
expression: "nixpkgs#hello".into(),
|
||||
stderr: "attribute not found".into(),
|
||||
};
|
||||
assert!(err.to_string().contains("nixpkgs#hello"));
|
||||
assert!(err.to_string().contains("attribute not found"));
|
||||
|
||||
let err = EhError::HashExtractionFailed {
|
||||
stderr: "some output".into(),
|
||||
};
|
||||
assert!(err.to_string().contains("could not extract hash"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hints() {
|
||||
assert!(
|
||||
EhError::PreEvalFailed {
|
||||
expression: "x".into(),
|
||||
stderr: "y".into(),
|
||||
}
|
||||
.hint()
|
||||
.is_some()
|
||||
);
|
||||
assert!(
|
||||
EhError::HashExtractionFailed {
|
||||
stderr: String::new(),
|
||||
}
|
||||
.hint()
|
||||
.is_some()
|
||||
);
|
||||
assert!(EhError::NoNixFilesFound.hint().is_some());
|
||||
assert!(
|
||||
EhError::Timeout {
|
||||
command: "x".into(),
|
||||
duration: Duration::from_secs(1),
|
||||
}
|
||||
.hint()
|
||||
.is_some()
|
||||
);
|
||||
assert!(
|
||||
EhError::InvalidInput {
|
||||
input: "x".into(),
|
||||
reason: "y".into(),
|
||||
}
|
||||
.hint()
|
||||
.is_some()
|
||||
);
|
||||
// Variants with hints
|
||||
assert!(
|
||||
EhError::NixCommandFailed {
|
||||
command: "build".into(),
|
||||
}
|
||||
.hint()
|
||||
.is_some()
|
||||
);
|
||||
assert!(
|
||||
EhError::JsonParse {
|
||||
detail: "x".into(),
|
||||
}
|
||||
.hint()
|
||||
.is_some()
|
||||
);
|
||||
assert!(EhError::NoFlakeInputs.hint().is_some());
|
||||
// Variants without hints
|
||||
assert!(
|
||||
EhError::CommandFailed {
|
||||
command: "x".into(),
|
||||
}
|
||||
.hint()
|
||||
.is_none()
|
||||
);
|
||||
assert!(EhError::ProcessExit { code: 1 }.hint().is_none());
|
||||
assert!(EhError::UpdateCancelled.hint().is_none());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ pub mod command;
|
|||
pub mod error;
|
||||
pub mod run;
|
||||
pub mod shell;
|
||||
pub mod update;
|
||||
pub mod util;
|
||||
|
||||
pub use clap::{CommandFactory, Parser, Subcommand};
|
||||
|
|
@ -35,9 +34,4 @@ pub enum Command {
|
|||
#[arg(trailing_var_arg = true)]
|
||||
args: Vec<String>,
|
||||
},
|
||||
/// Update flake inputs interactively
|
||||
Update {
|
||||
#[arg(trailing_var_arg = true)]
|
||||
args: Vec<String>,
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,30 +2,30 @@ use std::{env, path::Path};
|
|||
|
||||
use eh::{Cli, Command, CommandFactory, Parser};
|
||||
use error::Result;
|
||||
use yansi::Paint;
|
||||
|
||||
mod build;
|
||||
mod command;
|
||||
mod error;
|
||||
mod run;
|
||||
mod shell;
|
||||
mod update;
|
||||
mod util;
|
||||
|
||||
fn main() {
|
||||
let format = tracing_subscriber::fmt::format()
|
||||
.with_level(true) // don't include levels in formatted output
|
||||
.with_target(true) // don't include targets
|
||||
.with_thread_ids(false) // include the thread ID of the current thread
|
||||
.with_thread_names(false) // include the name of the current thread
|
||||
.compact(); // use the `Compact` formatting style.
|
||||
tracing_subscriber::fmt().event_format(format).init();
|
||||
|
||||
let result = run_app();
|
||||
|
||||
match result {
|
||||
Ok(code) => std::process::exit(code),
|
||||
Err(e) => {
|
||||
let code = e.exit_code();
|
||||
if code != 0 {
|
||||
eh_log::log_error!("{e}");
|
||||
if let Some(hint) = e.hint() {
|
||||
eh_log::log_hint!("{hint}");
|
||||
}
|
||||
}
|
||||
std::process::exit(code);
|
||||
eprintln!("Error: {e}");
|
||||
std::process::exit(e.exit_code());
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -37,54 +37,42 @@ fn dispatch_multicall(
|
|||
) -> Option<Result<i32>> {
|
||||
let rest: Vec<String> = args.collect();
|
||||
|
||||
let subcommand = match app_name {
|
||||
"nr" => "run",
|
||||
"ns" => "shell",
|
||||
"nb" => "build",
|
||||
"nu" => "update",
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
// Handle --help/-h/--version before forwarding to nix
|
||||
if rest.iter().any(|a| a == "--help" || a == "-h") {
|
||||
eprintln!(
|
||||
"{}: shorthand for '{}'",
|
||||
app_name.bold(),
|
||||
format!("eh {subcommand}").bold()
|
||||
);
|
||||
eprintln!(" {} {app_name} [args...]", "usage:".green().bold());
|
||||
eprintln!(
|
||||
" All arguments are forwarded to '{}'.",
|
||||
format!("nix {subcommand}").dim()
|
||||
);
|
||||
return Some(Ok(0));
|
||||
}
|
||||
|
||||
if rest.iter().any(|a| a == "--version") {
|
||||
eprintln!("{} (eh {})", app_name.bold(), env!("CARGO_PKG_VERSION"));
|
||||
return Some(Ok(0));
|
||||
}
|
||||
|
||||
if subcommand == "update" {
|
||||
return Some(update::handle_update(&rest));
|
||||
// Validate arguments before processing
|
||||
if let Err(e) = util::validate_nix_args(&rest) {
|
||||
return Some(Err(e));
|
||||
}
|
||||
|
||||
let hash_extractor = util::RegexHashExtractor;
|
||||
let fixer = util::DefaultNixFileFixer;
|
||||
let classifier = util::DefaultNixErrorClassifier;
|
||||
|
||||
Some(match subcommand {
|
||||
"run" => run::handle_nix_run(&rest, &hash_extractor, &fixer, &classifier),
|
||||
"shell" => {
|
||||
shell::handle_nix_shell(&rest, &hash_extractor, &fixer, &classifier)
|
||||
match app_name {
|
||||
"nr" => {
|
||||
Some(run::handle_nix_run(
|
||||
&rest,
|
||||
&hash_extractor,
|
||||
&fixer,
|
||||
&classifier,
|
||||
))
|
||||
},
|
||||
"build" => {
|
||||
build::handle_nix_build(&rest, &hash_extractor, &fixer, &classifier)
|
||||
"ns" => {
|
||||
Some(shell::handle_nix_shell(
|
||||
&rest,
|
||||
&hash_extractor,
|
||||
&fixer,
|
||||
&classifier,
|
||||
))
|
||||
},
|
||||
// subcommand is assigned from the match on app_name above;
|
||||
// only "run"/"shell"/"build" are possible values.
|
||||
_ => unreachable!(),
|
||||
})
|
||||
"nb" => {
|
||||
Some(build::handle_nix_build(
|
||||
&rest,
|
||||
&hash_extractor,
|
||||
&fixer,
|
||||
&classifier,
|
||||
))
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn run_app() -> Result<i32> {
|
||||
|
|
@ -119,9 +107,7 @@ fn run_app() -> Result<i32> {
|
|||
build::handle_nix_build(&args, &hash_extractor, &fixer, &classifier)
|
||||
},
|
||||
|
||||
Some(Command::Update { args }) => update::handle_update(&args),
|
||||
|
||||
None => {
|
||||
_ => {
|
||||
Cli::command().print_help()?;
|
||||
println!();
|
||||
Ok(0)
|
||||
|
|
|
|||
115
eh/src/update.rs
115
eh/src/update.rs
|
|
@ -1,115 +0,0 @@
|
|||
use crate::{
|
||||
command::{NixCommand, StdIoInterceptor},
|
||||
error::{EhError, Result},
|
||||
};
|
||||
|
||||
/// Parse flake input names from `nix flake metadata --json` output.
|
||||
pub fn parse_flake_inputs(stdout: &str) -> Result<Vec<String>> {
|
||||
let value: serde_json::Value =
|
||||
serde_json::from_str(stdout).map_err(|e| EhError::JsonParse {
|
||||
detail: e.to_string(),
|
||||
})?;
|
||||
|
||||
let inputs = value
|
||||
.get("locks")
|
||||
.and_then(|l| l.get("nodes"))
|
||||
.and_then(|n| n.get("root"))
|
||||
.and_then(|r| r.get("inputs"))
|
||||
.and_then(|i| i.as_object())
|
||||
.ok_or(EhError::NoFlakeInputs)?;
|
||||
|
||||
let mut names: Vec<String> = inputs.keys().cloned().collect();
|
||||
names.sort();
|
||||
Ok(names)
|
||||
}
|
||||
|
||||
/// Fetch flake input names by running `nix flake metadata --json`.
|
||||
fn fetch_flake_inputs() -> Result<Vec<String>> {
|
||||
let output = NixCommand::new("flake")
|
||||
.arg("metadata")
|
||||
.arg("--json")
|
||||
.print_build_logs(false)
|
||||
.output()?;
|
||||
|
||||
let stdout = String::from_utf8(output.stdout)?;
|
||||
parse_flake_inputs(&stdout)
|
||||
}
|
||||
|
||||
/// Prompt the user to select inputs via a multi-select dialog.
|
||||
fn prompt_input_selection(inputs: &[String]) -> Result<Vec<String>> {
|
||||
let selections = dialoguer::MultiSelect::new()
|
||||
.with_prompt("Select inputs to update")
|
||||
.items(inputs)
|
||||
.interact()
|
||||
.map_err(|e| EhError::Io(std::io::Error::other(e)))?;
|
||||
|
||||
if selections.is_empty() {
|
||||
return Err(EhError::UpdateCancelled);
|
||||
}
|
||||
|
||||
Ok(selections.iter().map(|&i| inputs[i].clone()).collect())
|
||||
}
|
||||
|
||||
/// Entry point for the `update` subcommand.
|
||||
///
|
||||
/// 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> {
|
||||
let selected = if args.is_empty() {
|
||||
let inputs = fetch_flake_inputs()?;
|
||||
if inputs.is_empty() {
|
||||
return Err(EhError::NoFlakeInputs);
|
||||
}
|
||||
prompt_input_selection(&inputs)?
|
||||
} else {
|
||||
args.to_vec()
|
||||
};
|
||||
|
||||
let mut cmd = NixCommand::new("flake").arg("lock");
|
||||
for name in &selected {
|
||||
cmd = cmd.arg("--update-input").arg(name);
|
||||
}
|
||||
|
||||
eh_log::log_info!("updating inputs: {}", selected.join(", "));
|
||||
|
||||
let status = cmd.run_with_logs(StdIoInterceptor)?;
|
||||
Ok(status.code().unwrap_or(1))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_flake_inputs() {
|
||||
let json = r#"{
|
||||
"locks": {
|
||||
"nodes": {
|
||||
"root": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs_2",
|
||||
"home-manager": "home-manager_2",
|
||||
"flake-utils": "flake-utils_2"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}"#;
|
||||
|
||||
let inputs = parse_flake_inputs(json).unwrap();
|
||||
assert_eq!(inputs, vec!["flake-utils", "home-manager", "nixpkgs"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_flake_inputs_invalid_json() {
|
||||
let result = parse_flake_inputs("not json");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_flake_inputs_no_inputs() {
|
||||
let json = r#"{"locks": {"nodes": {"root": {}}}}"#;
|
||||
let result = parse_flake_inputs(json);
|
||||
assert!(matches!(result, Err(EhError::NoFlakeInputs)));
|
||||
}
|
||||
}
|
||||
637
eh/src/util.rs
637
eh/src/util.rs
|
|
@ -1,12 +1,11 @@
|
|||
use std::{
|
||||
io::{BufWriter, Write},
|
||||
path::{Path, PathBuf},
|
||||
sync::LazyLock,
|
||||
};
|
||||
|
||||
use eh_log::{log_info, log_warn};
|
||||
use regex::Regex;
|
||||
use tempfile::NamedTempFile;
|
||||
use tracing::{info, warn};
|
||||
use walkdir::WalkDir;
|
||||
use yansi::Paint;
|
||||
|
||||
|
|
@ -15,41 +14,22 @@ use crate::{
|
|||
error::{EhError, Result},
|
||||
};
|
||||
|
||||
/// Compiled regex patterns for extracting the actual hash from nix stderr.
|
||||
static HASH_EXTRACT_PATTERNS: LazyLock<[Regex; 3]> = LazyLock::new(|| {
|
||||
[
|
||||
Regex::new(r"got:\s+(sha256-[a-zA-Z0-9+/=]+)").unwrap(),
|
||||
Regex::new(r"actual:\s+(sha256-[a-zA-Z0-9+/=]+)").unwrap(),
|
||||
Regex::new(r"have:\s+(sha256-[a-zA-Z0-9+/=]+)").unwrap(),
|
||||
]
|
||||
});
|
||||
|
||||
/// Compiled regex pattern for extracting the old (specified) hash from nix
|
||||
/// stderr.
|
||||
static HASH_OLD_EXTRACT_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
|
||||
Regex::new(r"specified:\s+(sha256-[a-zA-Z0-9+/=]+)").unwrap()
|
||||
});
|
||||
|
||||
/// Compiled regex patterns for matching hash attributes in .nix files.
|
||||
static HASH_FIX_PATTERNS: LazyLock<[Regex; 3]> = LazyLock::new(|| {
|
||||
[
|
||||
Regex::new(r#"hash\s*=\s*"[^"]*""#).unwrap(),
|
||||
Regex::new(r#"sha256\s*=\s*"[^"]*""#).unwrap(),
|
||||
Regex::new(r#"outputHash\s*=\s*"[^"]*""#).unwrap(),
|
||||
]
|
||||
});
|
||||
|
||||
pub trait HashExtractor {
|
||||
fn extract_hash(&self, stderr: &str) -> Option<String>;
|
||||
fn extract_old_hash(&self, stderr: &str) -> Option<String>;
|
||||
}
|
||||
|
||||
pub struct RegexHashExtractor;
|
||||
|
||||
impl HashExtractor for RegexHashExtractor {
|
||||
fn extract_hash(&self, stderr: &str) -> Option<String> {
|
||||
for re in HASH_EXTRACT_PATTERNS.iter() {
|
||||
if let Some(captures) = re.captures(stderr)
|
||||
let patterns = [
|
||||
r"got:\s+(sha256-[a-zA-Z0-9+/=]+)",
|
||||
r"actual:\s+(sha256-[a-zA-Z0-9+/=]+)",
|
||||
r"have:\s+(sha256-[a-zA-Z0-9+/=]+)",
|
||||
];
|
||||
for pattern in &patterns {
|
||||
if let Ok(re) = Regex::new(pattern)
|
||||
&& let Some(captures) = re.captures(stderr)
|
||||
&& let Some(hash) = captures.get(1)
|
||||
{
|
||||
return Some(hash.as_str().to_string());
|
||||
|
|
@ -57,43 +37,23 @@ impl HashExtractor for RegexHashExtractor {
|
|||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn extract_old_hash(&self, stderr: &str) -> Option<String> {
|
||||
HASH_OLD_EXTRACT_PATTERN
|
||||
.captures(stderr)
|
||||
.and_then(|c| c.get(1))
|
||||
.map(|m| m.as_str().to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub trait NixFileFixer {
|
||||
fn fix_hash_in_files(
|
||||
&self,
|
||||
old_hash: Option<&str>,
|
||||
new_hash: &str,
|
||||
) -> Result<bool>;
|
||||
fn fix_hash_in_files(&self, new_hash: &str) -> Result<bool>;
|
||||
fn find_nix_files(&self) -> Result<Vec<PathBuf>>;
|
||||
fn fix_hash_in_file(
|
||||
&self,
|
||||
file_path: &Path,
|
||||
old_hash: Option<&str>,
|
||||
new_hash: &str,
|
||||
) -> Result<bool>;
|
||||
fn fix_hash_in_file(&self, file_path: &Path, new_hash: &str) -> Result<bool>;
|
||||
}
|
||||
|
||||
pub struct DefaultNixFileFixer;
|
||||
|
||||
impl NixFileFixer for DefaultNixFileFixer {
|
||||
fn fix_hash_in_files(
|
||||
&self,
|
||||
old_hash: Option<&str>,
|
||||
new_hash: &str,
|
||||
) -> Result<bool> {
|
||||
fn fix_hash_in_files(&self, new_hash: &str) -> Result<bool> {
|
||||
let nix_files = self.find_nix_files()?;
|
||||
let mut fixed = false;
|
||||
for file_path in nix_files {
|
||||
if self.fix_hash_in_file(&file_path, old_hash, new_hash)? {
|
||||
log_info!("updated hash in {}", file_path.display().bold());
|
||||
if self.fix_hash_in_file(&file_path, new_hash)? {
|
||||
println!("Updated hash in {}", file_path.display());
|
||||
fixed = true;
|
||||
}
|
||||
}
|
||||
|
|
@ -101,20 +61,8 @@ impl NixFileFixer for DefaultNixFileFixer {
|
|||
}
|
||||
|
||||
fn find_nix_files(&self) -> Result<Vec<PathBuf>> {
|
||||
let should_skip = |entry: &walkdir::DirEntry| -> bool {
|
||||
// Never skip the root entry, otherwise the entire walk is empty
|
||||
if entry.depth() == 0 || !entry.file_type().is_dir() {
|
||||
return false;
|
||||
}
|
||||
let name = entry.file_name().to_string_lossy();
|
||||
name.starts_with('.')
|
||||
|| matches!(name.as_ref(), "node_modules" | "target" | "result")
|
||||
};
|
||||
|
||||
let files: Vec<PathBuf> = WalkDir::new(".")
|
||||
.max_depth(3)
|
||||
.into_iter()
|
||||
.filter_entry(|e| !should_skip(e))
|
||||
.filter_map(std::result::Result::ok)
|
||||
.filter(|entry| {
|
||||
entry.file_type().is_file()
|
||||
|
|
@ -132,59 +80,39 @@ impl NixFileFixer for DefaultNixFileFixer {
|
|||
Ok(files)
|
||||
}
|
||||
|
||||
fn fix_hash_in_file(
|
||||
&self,
|
||||
file_path: &Path,
|
||||
old_hash: Option<&str>,
|
||||
new_hash: &str,
|
||||
) -> Result<bool> {
|
||||
fn fix_hash_in_file(&self, file_path: &Path, new_hash: &str) -> Result<bool> {
|
||||
// Pre-compile regex patterns once to avoid repeated compilation
|
||||
let patterns: Vec<(Regex, String)> = [
|
||||
(r#"hash\s*=\s*"[^"]*""#, format!(r#"hash = "{new_hash}""#)),
|
||||
(
|
||||
r#"sha256\s*=\s*"[^"]*""#,
|
||||
format!(r#"sha256 = "{new_hash}""#),
|
||||
),
|
||||
(
|
||||
r#"outputHash\s*=\s*"[^"]*""#,
|
||||
format!(r#"outputHash = "{new_hash}""#),
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|(pattern, replacement)| {
|
||||
Regex::new(pattern)
|
||||
.map(|re| (re, replacement))
|
||||
.map_err(EhError::Regex)
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
// Read the entire file content
|
||||
let content = std::fs::read_to_string(file_path)?;
|
||||
let mut replaced = false;
|
||||
let mut result_content = content;
|
||||
|
||||
if let Some(old) = old_hash {
|
||||
// Targeted replacement: only replace attributes whose value matches the
|
||||
// old hash. Uses regexes to handle variable whitespace around `=`.
|
||||
let old_escaped = regex::escape(old);
|
||||
let targeted_patterns = [
|
||||
(
|
||||
Regex::new(&format!(r#"hash\s*=\s*"{old_escaped}""#)).unwrap(),
|
||||
format!(r#"hash = "{new_hash}""#),
|
||||
),
|
||||
(
|
||||
Regex::new(&format!(r#"sha256\s*=\s*"{old_escaped}""#)).unwrap(),
|
||||
format!(r#"sha256 = "{new_hash}""#),
|
||||
),
|
||||
(
|
||||
Regex::new(&format!(r#"outputHash\s*=\s*"{old_escaped}""#)).unwrap(),
|
||||
format!(r#"outputHash = "{new_hash}""#),
|
||||
),
|
||||
];
|
||||
|
||||
for (re, replacement) in &targeted_patterns {
|
||||
if re.is_match(&result_content) {
|
||||
result_content = re
|
||||
.replace_all(&result_content, replacement.as_str())
|
||||
.into_owned();
|
||||
replaced = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Fallback: replace all hash attributes (original behavior)
|
||||
let replacements = [
|
||||
format!(r#"hash = "{new_hash}""#),
|
||||
format!(r#"sha256 = "{new_hash}""#),
|
||||
format!(r#"outputHash = "{new_hash}""#),
|
||||
];
|
||||
|
||||
for (re, replacement) in HASH_FIX_PATTERNS.iter().zip(&replacements) {
|
||||
if re.is_match(&result_content) {
|
||||
result_content = re
|
||||
.replace_all(&result_content, replacement.as_str())
|
||||
.into_owned();
|
||||
replaced = true;
|
||||
}
|
||||
// Apply replacements
|
||||
for (re, replacement) in &patterns {
|
||||
if re.is_match(&result_content) {
|
||||
result_content = re
|
||||
.replace_all(&result_content, replacement.as_str())
|
||||
.into_owned();
|
||||
replaced = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -212,121 +140,38 @@ pub trait NixErrorClassifier {
|
|||
fn should_retry(&self, stderr: &str) -> bool;
|
||||
}
|
||||
|
||||
/// Classifies what retry action should be taken based on nix stderr output.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum RetryAction {
|
||||
AllowUnfree,
|
||||
AllowInsecure,
|
||||
AllowBroken,
|
||||
None,
|
||||
}
|
||||
|
||||
impl RetryAction {
|
||||
/// Returns `(env_var, reason)` for this retry action,
|
||||
/// or `None` if no retry is needed.
|
||||
fn env_override(&self) -> Option<(&str, &str)> {
|
||||
match self {
|
||||
Self::AllowUnfree => {
|
||||
Some(("NIXPKGS_ALLOW_UNFREE", "has an unfree license"))
|
||||
},
|
||||
Self::AllowInsecure => {
|
||||
Some(("NIXPKGS_ALLOW_INSECURE", "has been marked as insecure"))
|
||||
},
|
||||
Self::AllowBroken => {
|
||||
Some(("NIXPKGS_ALLOW_BROKEN", "has been marked as broken"))
|
||||
},
|
||||
Self::None => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract the package/expression name from args (first non-flag argument).
|
||||
fn package_name(args: &[String]) -> &str {
|
||||
args
|
||||
.iter()
|
||||
.find(|a| !a.starts_with('-'))
|
||||
.map(String::as_str)
|
||||
.unwrap_or("<unknown>")
|
||||
}
|
||||
|
||||
/// Print a retry message with consistent formatting.
|
||||
/// Format: ` -> <pkg>: <reason>, setting <ENV>=1`
|
||||
fn print_retry_msg(pkg: &str, reason: &str, env_var: &str) {
|
||||
log_warn!(
|
||||
"{}: {}, setting {}",
|
||||
pkg.bold(),
|
||||
reason,
|
||||
format!("{env_var}=1").bold()
|
||||
);
|
||||
}
|
||||
|
||||
/// Classify stderr into a retry action.
|
||||
pub fn classify_retry_action(stderr: &str) -> RetryAction {
|
||||
if stderr.contains("has an unfree license") && stderr.contains("refusing") {
|
||||
RetryAction::AllowUnfree
|
||||
} else if stderr.contains("has been marked as insecure")
|
||||
&& stderr.contains("refusing")
|
||||
{
|
||||
RetryAction::AllowInsecure
|
||||
} else if stderr.contains("has been marked as broken")
|
||||
&& stderr.contains("refusing")
|
||||
{
|
||||
RetryAction::AllowBroken
|
||||
} else {
|
||||
RetryAction::None
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if stderr looks like a genuine hash mismatch error
|
||||
/// (not just any mention of "hash" or "sha256").
|
||||
fn is_hash_mismatch_error(stderr: &str) -> bool {
|
||||
stderr.contains("hash mismatch")
|
||||
|| (stderr.contains("specified:") && stderr.contains("got:"))
|
||||
}
|
||||
|
||||
/// Pre-evaluate expression to catch errors early.
|
||||
///
|
||||
/// Returns a `RetryAction` if the evaluation fails with a retryable error
|
||||
/// (unfree/insecure/broken), allowing the caller to retry with the right
|
||||
/// environment variables without ever streaming the verbose nix error output.
|
||||
fn pre_evaluate(args: &[String]) -> Result<RetryAction> {
|
||||
/// Pre-evaluate expression to catch errors early
|
||||
fn pre_evaluate(_subcommand: &str, args: &[String]) -> Result<bool> {
|
||||
// Find flake references or expressions to evaluate
|
||||
// Only take the first non-flag argument (the package/expression)
|
||||
let eval_arg = args.iter().find(|arg| !arg.starts_with('-'));
|
||||
|
||||
let Some(eval_arg) = eval_arg else {
|
||||
return Ok(RetryAction::None); // No expression to evaluate
|
||||
return Ok(true); // No expression to evaluate
|
||||
};
|
||||
|
||||
let eval_cmd = NixCommand::new("eval")
|
||||
.arg(eval_arg)
|
||||
.print_build_logs(false);
|
||||
let eval_cmd = NixCommand::new("eval").arg(eval_arg).arg("--raw");
|
||||
|
||||
let output = eval_cmd.output()?;
|
||||
|
||||
if output.status.success() {
|
||||
return Ok(RetryAction::None);
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
|
||||
// Classify whether this is a retryable error (unfree/insecure/broken)
|
||||
let action = classify_retry_action(&stderr);
|
||||
if action != RetryAction::None {
|
||||
return Ok(action);
|
||||
// If eval fails due to unfree/insecure/broken, don't fail pre-evaluation
|
||||
// Let the main command handle it with retry logic
|
||||
if stderr.contains("has an unfree license")
|
||||
|| stderr.contains("refusing to evaluate")
|
||||
|| stderr.contains("has been marked as insecure")
|
||||
|| stderr.contains("has been marked as broken")
|
||||
{
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
// Non-retryable eval failure — fail fast with a clear message
|
||||
// rather than running the full command and showing the same error again.
|
||||
let stderr_clean = stderr
|
||||
.trim()
|
||||
.strip_prefix("error:")
|
||||
.unwrap_or(stderr.trim())
|
||||
.trim();
|
||||
Err(EhError::PreEvalFailed {
|
||||
expression: eval_arg.clone(),
|
||||
stderr: stderr_clean.to_string(),
|
||||
})
|
||||
// For other eval failures, fail early
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
pub fn validate_nix_args(args: &[String]) -> Result<()> {
|
||||
|
|
@ -357,28 +202,15 @@ pub fn handle_nix_with_retry(
|
|||
interactive: bool,
|
||||
) -> Result<i32> {
|
||||
validate_nix_args(args)?;
|
||||
|
||||
// Pre-evaluate to detect retryable errors (unfree/insecure/broken) before
|
||||
// running the actual command. This avoids streaming verbose nix error output
|
||||
// only to retry immediately after.
|
||||
let pkg = package_name(args);
|
||||
let pre_eval_action = pre_evaluate(args)?;
|
||||
if let Some((env_var, reason)) = pre_eval_action.env_override() {
|
||||
print_retry_msg(pkg, reason, env_var);
|
||||
let mut retry_cmd = NixCommand::new(subcommand)
|
||||
.print_build_logs(true)
|
||||
.args_ref(args)
|
||||
.env(env_var, "1")
|
||||
.impure(true);
|
||||
if interactive {
|
||||
retry_cmd = retry_cmd.interactive(true);
|
||||
}
|
||||
let retry_status = retry_cmd.run_with_logs(StdIoInterceptor)?;
|
||||
return Ok(retry_status.code().unwrap_or(1));
|
||||
// Pre-evaluate for build commands to catch errors early
|
||||
if !pre_evaluate(subcommand, args)? {
|
||||
return Err(EhError::NixCommandFailed(
|
||||
"Expression evaluation failed".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// For run/shell commands, try interactive mode now that pre-eval passed
|
||||
if interactive {
|
||||
// For run commands, try interactive first to avoid breaking terminal
|
||||
if subcommand == "run" && interactive {
|
||||
let status = NixCommand::new(subcommand)
|
||||
.print_build_logs(true)
|
||||
.interactive(true)
|
||||
|
|
@ -389,22 +221,18 @@ pub fn handle_nix_with_retry(
|
|||
}
|
||||
}
|
||||
|
||||
// Capture output to check for errors that need retry (hash mismatches etc.)
|
||||
// First, always capture output to check for errors that need retry
|
||||
let output_cmd = NixCommand::new(subcommand)
|
||||
.print_build_logs(true)
|
||||
.args_ref(args);
|
||||
let output = output_cmd.output()?;
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
|
||||
// Check for hash mismatch errors
|
||||
// Check if we need to retry with special flags
|
||||
if let Some(new_hash) = hash_extractor.extract_hash(&stderr) {
|
||||
let old_hash = hash_extractor.extract_old_hash(&stderr);
|
||||
match fixer.fix_hash_in_files(old_hash.as_deref(), &new_hash) {
|
||||
match fixer.fix_hash_in_files(&new_hash) {
|
||||
Ok(true) => {
|
||||
log_info!(
|
||||
"{}: hash mismatch corrected in local files, rebuilding",
|
||||
pkg.bold()
|
||||
);
|
||||
info!("{}", Paint::green("✔ Fixed hash mismatch, retrying..."));
|
||||
let mut retry_cmd = NixCommand::new(subcommand)
|
||||
.print_build_logs(true)
|
||||
.args_ref(args);
|
||||
|
|
@ -418,33 +246,72 @@ pub fn handle_nix_with_retry(
|
|||
// No files were fixed, continue with normal error handling
|
||||
},
|
||||
Err(EhError::NoNixFilesFound) => {
|
||||
log_warn!(
|
||||
"{}: hash mismatch detected but no .nix files found to update",
|
||||
pkg.bold()
|
||||
);
|
||||
warn!("No .nix files found to fix hash in");
|
||||
// Continue with normal error handling
|
||||
},
|
||||
Err(e) => {
|
||||
return Err(e);
|
||||
},
|
||||
}
|
||||
} else if is_hash_mismatch_error(&stderr) {
|
||||
// There's a genuine hash mismatch but we couldn't extract the new hash
|
||||
return Err(EhError::HashExtractionFailed {
|
||||
stderr: stderr.to_string(),
|
||||
});
|
||||
} else if stderr.contains("hash") || stderr.contains("sha256") {
|
||||
// If there's a hash-related error but we couldn't extract it, that's a
|
||||
// failure
|
||||
return Err(EhError::HashExtractionFailed);
|
||||
}
|
||||
|
||||
// Fallback: check for unfree/insecure/broken in captured output
|
||||
// (in case pre_evaluate didn't catch it, e.g. from a dependency)
|
||||
if classifier.should_retry(&stderr) {
|
||||
let action = classify_retry_action(&stderr);
|
||||
if let Some((env_var, reason)) = action.env_override() {
|
||||
print_retry_msg(pkg, reason, env_var);
|
||||
if stderr.contains("has an unfree license") && stderr.contains("refusing") {
|
||||
warn!(
|
||||
"{}",
|
||||
Paint::yellow(
|
||||
"⚠ Unfree package detected, retrying with NIXPKGS_ALLOW_UNFREE=1..."
|
||||
)
|
||||
);
|
||||
let mut retry_cmd = NixCommand::new(subcommand)
|
||||
.print_build_logs(true)
|
||||
.args_ref(args)
|
||||
.env(env_var, "1")
|
||||
.env("NIXPKGS_ALLOW_UNFREE", "1")
|
||||
.impure(true);
|
||||
if interactive {
|
||||
retry_cmd = retry_cmd.interactive(true);
|
||||
}
|
||||
let retry_status = retry_cmd.run_with_logs(StdIoInterceptor)?;
|
||||
return Ok(retry_status.code().unwrap_or(1));
|
||||
}
|
||||
if stderr.contains("has been marked as insecure")
|
||||
&& stderr.contains("refusing")
|
||||
{
|
||||
warn!(
|
||||
"{}",
|
||||
Paint::yellow(
|
||||
"⚠ Insecure package detected, retrying with \
|
||||
NIXPKGS_ALLOW_INSECURE=1..."
|
||||
)
|
||||
);
|
||||
let mut retry_cmd = NixCommand::new(subcommand)
|
||||
.print_build_logs(true)
|
||||
.args_ref(args)
|
||||
.env("NIXPKGS_ALLOW_INSECURE", "1")
|
||||
.impure(true);
|
||||
if interactive {
|
||||
retry_cmd = retry_cmd.interactive(true);
|
||||
}
|
||||
let retry_status = retry_cmd.run_with_logs(StdIoInterceptor)?;
|
||||
return Ok(retry_status.code().unwrap_or(1));
|
||||
}
|
||||
if stderr.contains("has been marked as broken")
|
||||
&& stderr.contains("refusing")
|
||||
{
|
||||
warn!(
|
||||
"{}",
|
||||
Paint::yellow(
|
||||
"⚠ Broken package detected, retrying with NIXPKGS_ALLOW_BROKEN=1..."
|
||||
)
|
||||
);
|
||||
let mut retry_cmd = NixCommand::new(subcommand)
|
||||
.print_build_logs(true)
|
||||
.args_ref(args)
|
||||
.env("NIXPKGS_ALLOW_BROKEN", "1")
|
||||
.impure(true);
|
||||
if interactive {
|
||||
retry_cmd = retry_cmd.interactive(true);
|
||||
|
|
@ -463,23 +330,22 @@ pub fn handle_nix_with_retry(
|
|||
std::io::stderr()
|
||||
.write_all(&output.stderr)
|
||||
.map_err(EhError::Io)?;
|
||||
|
||||
match output.status.code() {
|
||||
Some(code) => Err(EhError::ProcessExit { code }),
|
||||
// No exit code means the process was killed by a signal
|
||||
None => {
|
||||
Err(EhError::NixCommandFailed {
|
||||
command: subcommand.to_string(),
|
||||
})
|
||||
},
|
||||
}
|
||||
Err(EhError::ProcessExit {
|
||||
code: output.status.code().unwrap_or(1),
|
||||
})
|
||||
}
|
||||
|
||||
pub struct DefaultNixErrorClassifier;
|
||||
|
||||
impl NixErrorClassifier for DefaultNixErrorClassifier {
|
||||
fn should_retry(&self, stderr: &str) -> bool {
|
||||
classify_retry_action(stderr) != RetryAction::None
|
||||
RegexHashExtractor.extract_hash(stderr).is_some()
|
||||
|| (stderr.contains("has an unfree license")
|
||||
&& stderr.contains("refusing"))
|
||||
|| (stderr.contains("has been marked as insecure")
|
||||
&& stderr.contains("refusing"))
|
||||
|| (stderr.contains("has been marked as broken")
|
||||
&& stderr.contains("refusing"))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -513,7 +379,7 @@ mod tests {
|
|||
|
||||
let fixer = DefaultNixFileFixer;
|
||||
let result = fixer
|
||||
.fix_hash_in_file(file_path, None, "sha256-newhash999")
|
||||
.fix_hash_in_file(file_path, "sha256-newhash999")
|
||||
.unwrap();
|
||||
|
||||
assert!(result, "Hash replacement should return true");
|
||||
|
|
@ -547,7 +413,7 @@ mod tests {
|
|||
// Test hash replacement
|
||||
let fixer = DefaultNixFileFixer;
|
||||
let result = fixer
|
||||
.fix_hash_in_file(&file_path, None, "sha256-newhash999")
|
||||
.fix_hash_in_file(&file_path, "sha256-newhash999")
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
|
|
@ -582,7 +448,7 @@ mod tests {
|
|||
// Test that streaming can handle large files without memory issues
|
||||
let fixer = DefaultNixFileFixer;
|
||||
let result = fixer
|
||||
.fix_hash_in_file(file_path, None, "sha256-newhash999")
|
||||
.fix_hash_in_file(file_path, "sha256-newhash999")
|
||||
.unwrap();
|
||||
|
||||
assert!(result, "Hash replacement should work for large files");
|
||||
|
|
@ -617,9 +483,7 @@ mod tests {
|
|||
|
||||
// Test hash replacement
|
||||
let fixer = DefaultNixFileFixer;
|
||||
let result = fixer
|
||||
.fix_hash_in_file(file_path, None, "sha256-newhash")
|
||||
.unwrap();
|
||||
let result = fixer.fix_hash_in_file(file_path, "sha256-newhash").unwrap();
|
||||
|
||||
assert!(result, "Hash replacement should succeed");
|
||||
|
||||
|
|
@ -674,233 +538,4 @@ mod tests {
|
|||
safe_args
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_input_validation_empty_args() {
|
||||
let result = validate_nix_args(&[]);
|
||||
assert!(result.is_ok(), "Empty args should be accepted");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hash_extraction_got_pattern() {
|
||||
let stderr = "hash mismatch in fixed-output derivation\n specified: \
|
||||
sha256-AAAA\n got: \
|
||||
sha256-BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=";
|
||||
let extractor = RegexHashExtractor;
|
||||
let hash = extractor.extract_hash(stderr);
|
||||
assert!(hash.is_some());
|
||||
assert!(hash.unwrap().starts_with("sha256-"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hash_extraction_actual_pattern() {
|
||||
let stderr = "hash mismatch\n actual: \
|
||||
sha256-CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC=";
|
||||
let extractor = RegexHashExtractor;
|
||||
let hash = extractor.extract_hash(stderr);
|
||||
assert!(hash.is_some());
|
||||
assert!(hash.unwrap().starts_with("sha256-"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hash_extraction_have_pattern() {
|
||||
let stderr = "hash mismatch\n have: \
|
||||
sha256-DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD=";
|
||||
let extractor = RegexHashExtractor;
|
||||
let hash = extractor.extract_hash(stderr);
|
||||
assert!(hash.is_some());
|
||||
assert!(hash.unwrap().starts_with("sha256-"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hash_extraction_no_match() {
|
||||
let stderr = "error: some other nix error without hashes";
|
||||
let extractor = RegexHashExtractor;
|
||||
assert!(extractor.extract_hash(stderr).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hash_extraction_partial_match() {
|
||||
// Contains "got:" but no sha256 hash
|
||||
let stderr = "got: some-other-value";
|
||||
let extractor = RegexHashExtractor;
|
||||
assert!(extractor.extract_hash(stderr).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_false_positive_hash_detection() {
|
||||
// Normal nix output mentioning "hash" or "sha256" without being a mismatch
|
||||
let cases = [
|
||||
"evaluating attribute 'sha256' of derivation 'hello'",
|
||||
"building '/nix/store/hash-something.drv'",
|
||||
"copying path '/nix/store/sha256-abcdef-hello'",
|
||||
"this derivation has a hash attribute set",
|
||||
];
|
||||
for stderr in &cases {
|
||||
assert!(
|
||||
!is_hash_mismatch_error(stderr),
|
||||
"Should not detect hash mismatch in: {stderr}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_genuine_hash_mismatch_detection() {
|
||||
assert!(is_hash_mismatch_error(
|
||||
"hash mismatch in fixed-output derivation"
|
||||
));
|
||||
assert!(is_hash_mismatch_error(
|
||||
"specified: sha256-AAAA\n got: sha256-BBBB"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_classify_retry_action_unfree() {
|
||||
let stderr =
|
||||
"error: Package 'foo' has an unfree license, refusing to evaluate.";
|
||||
assert_eq!(classify_retry_action(stderr), RetryAction::AllowUnfree);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_classify_retry_action_insecure() {
|
||||
let stderr =
|
||||
"error: Package 'bar' has been marked as insecure, refusing to evaluate.";
|
||||
assert_eq!(classify_retry_action(stderr), RetryAction::AllowInsecure);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_classify_retry_action_broken() {
|
||||
let stderr =
|
||||
"error: Package 'baz' has been marked as broken, refusing to evaluate.";
|
||||
assert_eq!(classify_retry_action(stderr), RetryAction::AllowBroken);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_classify_retry_action_none() {
|
||||
let stderr = "error: attribute 'nonexistent' not found";
|
||||
assert_eq!(classify_retry_action(stderr), RetryAction::None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_retry_action_env_overrides() {
|
||||
let (var, reason) = RetryAction::AllowUnfree.env_override().unwrap();
|
||||
assert_eq!(var, "NIXPKGS_ALLOW_UNFREE");
|
||||
assert!(reason.contains("unfree"));
|
||||
|
||||
let (var, reason) = RetryAction::AllowInsecure.env_override().unwrap();
|
||||
assert_eq!(var, "NIXPKGS_ALLOW_INSECURE");
|
||||
assert!(reason.contains("insecure"));
|
||||
|
||||
let (var, reason) = RetryAction::AllowBroken.env_override().unwrap();
|
||||
assert_eq!(var, "NIXPKGS_ALLOW_BROKEN");
|
||||
assert!(reason.contains("broken"));
|
||||
|
||||
assert_eq!(RetryAction::None.env_override(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_classifier_should_retry() {
|
||||
let classifier = DefaultNixErrorClassifier;
|
||||
assert!(
|
||||
classifier.should_retry(
|
||||
"Package 'x' has an unfree license, refusing to evaluate"
|
||||
)
|
||||
);
|
||||
assert!(classifier.should_retry(
|
||||
"Package 'x' has been marked as insecure, refusing to evaluate"
|
||||
));
|
||||
assert!(classifier.should_retry(
|
||||
"Package 'x' has been marked as broken, refusing to evaluate"
|
||||
));
|
||||
assert!(!classifier.should_retry("error: attribute not found"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_old_hash_extraction() {
|
||||
let stderr =
|
||||
"hash mismatch in fixed-output derivation\n specified: \
|
||||
sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\n got: \
|
||||
sha256-BBBB=";
|
||||
let extractor = RegexHashExtractor;
|
||||
let old = extractor.extract_old_hash(stderr);
|
||||
assert!(old.is_some());
|
||||
assert_eq!(
|
||||
old.unwrap(),
|
||||
"sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_old_hash_extraction_missing() {
|
||||
let stderr = "hash mismatch\n got: sha256-BBBB=";
|
||||
let extractor = RegexHashExtractor;
|
||||
assert!(extractor.extract_old_hash(stderr).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_targeted_hash_replacement_only_matching() {
|
||||
let temp_file = NamedTempFile::new().unwrap();
|
||||
let file_path = temp_file.path();
|
||||
|
||||
// File with two derivations, each with a different hash
|
||||
let test_content = r#"{ pkgs }:
|
||||
{
|
||||
a = pkgs.fetchurl {
|
||||
url = "https://example.com/a.tar.gz";
|
||||
hash = "sha256-AAAA";
|
||||
};
|
||||
b = pkgs.fetchurl {
|
||||
url = "https://example.com/b.tar.gz";
|
||||
hash = "sha256-BBBB";
|
||||
};
|
||||
}"#;
|
||||
|
||||
let mut file = std::fs::File::create(file_path).unwrap();
|
||||
file.write_all(test_content.as_bytes()).unwrap();
|
||||
file.flush().unwrap();
|
||||
|
||||
let fixer = DefaultNixFileFixer;
|
||||
// Only replace the hash matching "sha256-AAAA"
|
||||
let result = fixer
|
||||
.fix_hash_in_file(file_path, Some("sha256-AAAA"), "sha256-NEWW")
|
||||
.unwrap();
|
||||
|
||||
assert!(result, "Targeted replacement should return true");
|
||||
|
||||
let updated = std::fs::read_to_string(file_path).unwrap();
|
||||
assert!(
|
||||
updated.contains(r#"hash = "sha256-NEWW""#),
|
||||
"Matching hash should be replaced"
|
||||
);
|
||||
assert!(
|
||||
updated.contains(r#"hash = "sha256-BBBB""#),
|
||||
"Non-matching hash should be untouched"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_targeted_hash_replacement_no_match() {
|
||||
let temp_file = NamedTempFile::new().unwrap();
|
||||
let file_path = temp_file.path();
|
||||
|
||||
let test_content = r#"{ hash = "sha256-XXXX"; }"#;
|
||||
|
||||
let mut file = std::fs::File::create(file_path).unwrap();
|
||||
file.write_all(test_content.as_bytes()).unwrap();
|
||||
file.flush().unwrap();
|
||||
|
||||
let fixer = DefaultNixFileFixer;
|
||||
// old_hash doesn't match anything in the file
|
||||
let result = fixer
|
||||
.fix_hash_in_file(file_path, Some("sha256-NOMATCH"), "sha256-NEWW")
|
||||
.unwrap();
|
||||
|
||||
assert!(!result, "Should return false when old hash doesn't match");
|
||||
|
||||
let updated = std::fs::read_to_string(file_path).unwrap();
|
||||
assert!(
|
||||
updated.contains("sha256-XXXX"),
|
||||
"Original hash should be untouched"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
6
flake.lock
generated
6
flake.lock
generated
|
|
@ -2,11 +2,11 @@
|
|||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1769461804,
|
||||
"narHash": "sha256-msG8SU5WsBUfVVa/9RPLaymvi5bI8edTavbIq3vRlhI=",
|
||||
"lastModified": 1762844143,
|
||||
"narHash": "sha256-SlybxLZ1/e4T2lb1czEtWVzDCVSTvk9WLwGhmxFmBxI=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "bfc1b8a4574108ceef22f02bafcf6611380c100d",
|
||||
"rev": "9da7f1cf7f8a6e2a7cb3001b048546c92a8258b4",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
|
|||
|
|
@ -11,4 +11,4 @@ publish = false
|
|||
[dependencies]
|
||||
clap.workspace = true
|
||||
clap_complete.workspace = true
|
||||
eh.workspace = true
|
||||
eh = { path = "../eh" }
|
||||
|
|
@ -42,7 +42,6 @@ enum Binary {
|
|||
Nr,
|
||||
Ns,
|
||||
Nb,
|
||||
Nu,
|
||||
}
|
||||
|
||||
impl Binary {
|
||||
|
|
@ -51,7 +50,6 @@ impl Binary {
|
|||
Self::Nr => "nr",
|
||||
Self::Ns => "ns",
|
||||
Self::Nb => "nb",
|
||||
Self::Nu => "nu",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -92,7 +90,7 @@ fn create_multicall_binaries(
|
|||
);
|
||||
}
|
||||
|
||||
let multicall_binaries = [Binary::Nr, Binary::Ns, Binary::Nb, Binary::Nu];
|
||||
let multicall_binaries = [Binary::Nr, Binary::Ns, Binary::Nb];
|
||||
let bin_path = Path::new(bin_dir);
|
||||
|
||||
for binary in multicall_binaries {
|
||||
|
|
@ -155,7 +153,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", "nr", "ns"];
|
||||
for name in &multicall_names {
|
||||
let symlink_path = output_dir.join(format!("{name}.{shell}"));
|
||||
if symlink_path.exists() {
|
||||
Loading…
Add table
Add a link
Reference in a new issue