Compare commits
10 commits
0d50b374bd
...
6f9c6893e1
| Author | SHA1 | Date | |
|---|---|---|---|
|
6f9c6893e1 |
|||
|
5dc7b1dcd4 |
|||
|
045d1632cb |
|||
|
e6d1b90b97 |
|||
|
be3226bc3a |
|||
|
9b632788c2 |
|||
|
304a7e1a1a |
|||
|
4355f1d2c7 |
|||
|
237bfec0d4 |
|||
|
6224f2f2d6 |
16 changed files with 1482 additions and 617 deletions
281
Cargo.lock
generated
281
Cargo.lock
generated
|
|
@ -4,18 +4,18 @@ version = 4
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aho-corasick"
|
name = "aho-corasick"
|
||||||
version = "1.1.3"
|
version = "1.1.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
|
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anstyle"
|
name = "anstyle"
|
||||||
version = "1.0.11"
|
version = "1.0.13"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd"
|
checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
|
|
@ -25,15 +25,15 @@ checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cfg-if"
|
name = "cfg-if"
|
||||||
version = "1.0.1"
|
version = "1.0.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
|
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap"
|
name = "clap"
|
||||||
version = "4.5.51"
|
version = "4.5.56"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5"
|
checksum = "a75ca66430e33a14957acc24c5077b503e7d374151b2b4b3a10c83b4ceb4be0e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap_builder",
|
"clap_builder",
|
||||||
"clap_derive",
|
"clap_derive",
|
||||||
|
|
@ -41,9 +41,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_builder"
|
name = "clap_builder"
|
||||||
version = "4.5.51"
|
version = "4.5.56"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a"
|
checksum = "793207c7fa6300a0608d1080b858e5fdbe713cdc1c8db9fb17777d8a13e63df0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anstyle",
|
"anstyle",
|
||||||
"clap_lex",
|
"clap_lex",
|
||||||
|
|
@ -51,18 +51,18 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_complete"
|
name = "clap_complete"
|
||||||
version = "4.5.60"
|
version = "4.5.65"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8e602857739c5a4291dfa33b5a298aeac9006185229a700e5810a3ef7272d971"
|
checksum = "430b4dc2b5e3861848de79627b2bedc9f3342c7da5173a14eaa5d0f8dc18ae5d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_derive"
|
name = "clap_derive"
|
||||||
version = "4.5.49"
|
version = "4.5.55"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671"
|
checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck",
|
"heck",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
|
|
@ -72,24 +72,61 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_lex"
|
name = "clap_lex"
|
||||||
version = "0.7.5"
|
version = "0.7.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675"
|
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",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "eh"
|
name = "eh"
|
||||||
version = "0.1.2"
|
version = "0.1.4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
|
"dialoguer",
|
||||||
|
"eh-log",
|
||||||
"regex",
|
"regex",
|
||||||
|
"serde_json",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tracing",
|
|
||||||
"tracing-subscriber",
|
|
||||||
"walkdir",
|
"walkdir",
|
||||||
"yansi",
|
"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]]
|
[[package]]
|
||||||
name = "errno"
|
name = "errno"
|
||||||
version = "0.3.14"
|
version = "0.3.14"
|
||||||
|
|
@ -125,16 +162,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lazy_static"
|
name = "itoa"
|
||||||
version = "1.5.0"
|
version = "1.0.17"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.177"
|
version = "0.2.180"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
|
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "linux-raw-sys"
|
name = "linux-raw-sys"
|
||||||
|
|
@ -142,26 +179,11 @@ version = "0.11.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
|
checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "log"
|
|
||||||
version = "0.4.27"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
version = "2.7.5"
|
version = "2.7.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
|
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
|
||||||
|
|
||||||
[[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]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
|
|
@ -169,26 +191,20 @@ version = "1.21.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pin-project-lite"
|
|
||||||
version = "0.2.16"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.95"
|
version = "1.0.106"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
|
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.40"
|
version = "1.0.44"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
|
checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
|
|
@ -224,15 +240,15 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex-syntax"
|
name = "regex-syntax"
|
||||||
version = "0.8.5"
|
version = "0.8.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustix"
|
name = "rustix"
|
||||||
version = "1.1.2"
|
version = "1.1.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e"
|
checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"errno",
|
"errno",
|
||||||
|
|
@ -251,25 +267,58 @@ dependencies = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sharded-slab"
|
name = "serde"
|
||||||
version = "0.1.7"
|
version = "1.0.228"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
|
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"lazy_static",
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "smallvec"
|
name = "serde_core"
|
||||||
version = "1.15.1"
|
version = "1.0.228"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
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"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.104"
|
version = "2.0.114"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40"
|
checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
|
@ -278,9 +327,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tempfile"
|
name = "tempfile"
|
||||||
version = "3.23.0"
|
version = "3.24.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16"
|
checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"fastrand",
|
"fastrand",
|
||||||
"getrandom",
|
"getrandom",
|
||||||
|
|
@ -291,101 +340,35 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "2.0.17"
|
version = "2.0.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8"
|
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"thiserror-impl",
|
"thiserror-impl",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror-impl"
|
name = "thiserror-impl"
|
||||||
version = "2.0.17"
|
version = "2.0.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
|
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "thread_local"
|
|
||||||
version = "1.1.9"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tracing"
|
|
||||||
version = "0.1.41"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
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]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.18"
|
version = "1.0.22"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
|
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "valuable"
|
name = "unicode-width"
|
||||||
version = "0.1.1"
|
version = "0.2.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
|
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "walkdir"
|
name = "walkdir"
|
||||||
|
|
@ -399,9 +382,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasip2"
|
name = "wasip2"
|
||||||
version = "1.0.1+wasi-0.2.4"
|
version = "1.0.2+wasi-0.2.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7"
|
checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"wit-bindgen",
|
"wit-bindgen",
|
||||||
]
|
]
|
||||||
|
|
@ -432,13 +415,13 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wit-bindgen"
|
name = "wit-bindgen"
|
||||||
version = "0.46.0"
|
version = "0.51.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
|
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "xtask"
|
name = "xtask"
|
||||||
version = "0.1.2"
|
version = "0.1.4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"clap_complete",
|
"clap_complete",
|
||||||
|
|
@ -450,3 +433,9 @@ name = "yansi"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
|
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zmij"
|
||||||
|
version = "1.0.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "02aae0f83f69aafc94776e879363e9771d7ecbffe2c7fbb6c14c5e00dfe88439"
|
||||||
|
|
|
||||||
22
Cargo.toml
22
Cargo.toml
|
|
@ -1,5 +1,6 @@
|
||||||
[workspace]
|
[workspace]
|
||||||
members = [ "eh", "xtask" ]
|
default-members = [ "eh" ]
|
||||||
|
members = [ "eh", "crates/*" ]
|
||||||
resolver = "3"
|
resolver = "3"
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
|
|
@ -8,20 +9,23 @@ description = "Ergonomic Nix CLI helper"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MPL-2.0"
|
license = "MPL-2.0"
|
||||||
readme = true
|
readme = true
|
||||||
rust-version = "1.89"
|
rust-version = "1.90"
|
||||||
version = "0.1.2"
|
version = "0.1.4"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
clap = { default-features = false, features = [ "std", "help", "derive" ], version = "4.5.51" }
|
clap = { default-features = false, features = [ "std", "help", "derive" ], version = "4.5.56" }
|
||||||
clap_complete = "4.5.60"
|
clap_complete = "4.5.65"
|
||||||
|
dialoguer = { default-features = false, version = "0.12.0" }
|
||||||
regex = "1.12.2"
|
regex = "1.12.2"
|
||||||
tempfile = "3.23.0"
|
serde_json = "1.0.149"
|
||||||
thiserror = "2.0.17"
|
tempfile = "3.24.0"
|
||||||
tracing = "0.1.41"
|
thiserror = "2.0.18"
|
||||||
tracing-subscriber = "0.3.20"
|
|
||||||
walkdir = "2.5.0"
|
walkdir = "2.5.0"
|
||||||
yansi = "1.0.1"
|
yansi = "1.0.1"
|
||||||
|
|
||||||
|
eh = { path = "./eh" }
|
||||||
|
eh-log = { path = "./crates/eh-log" }
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
codegen-units = 1
|
codegen-units = 1
|
||||||
lto = true
|
lto = true
|
||||||
|
|
|
||||||
10
crates/eh-log/Cargo.toml
Normal file
10
crates/eh-log/Cargo.toml
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
[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
|
||||||
31
crates/eh-log/src/lib.rs
Normal file
31
crates/eh-log/src/lib.rs
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
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,4 +11,4 @@ publish = false
|
||||||
[dependencies]
|
[dependencies]
|
||||||
clap.workspace = true
|
clap.workspace = true
|
||||||
clap_complete.workspace = true
|
clap_complete.workspace = true
|
||||||
eh = { path = "../eh" }
|
eh.workspace = true
|
||||||
|
|
@ -42,6 +42,7 @@ enum Binary {
|
||||||
Nr,
|
Nr,
|
||||||
Ns,
|
Ns,
|
||||||
Nb,
|
Nb,
|
||||||
|
Nu,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Binary {
|
impl Binary {
|
||||||
|
|
@ -50,6 +51,7 @@ impl Binary {
|
||||||
Self::Nr => "nr",
|
Self::Nr => "nr",
|
||||||
Self::Ns => "ns",
|
Self::Ns => "ns",
|
||||||
Self::Nb => "nb",
|
Self::Nb => "nb",
|
||||||
|
Self::Nu => "nu",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -90,7 +92,7 @@ fn create_multicall_binaries(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let multicall_binaries = [Binary::Nr, Binary::Ns, Binary::Nb];
|
let multicall_binaries = [Binary::Nr, Binary::Ns, Binary::Nb, Binary::Nu];
|
||||||
let bin_path = Path::new(bin_dir);
|
let bin_path = Path::new(bin_dir);
|
||||||
|
|
||||||
for binary in multicall_binaries {
|
for binary in multicall_binaries {
|
||||||
|
|
@ -153,7 +155,7 @@ fn generate_completions(
|
||||||
println!("completion file generated: {}", completion_file.display());
|
println!("completion file generated: {}", completion_file.display());
|
||||||
|
|
||||||
// Create symlinks for multicall binaries
|
// Create symlinks for multicall binaries
|
||||||
let multicall_names = ["nb", "nr", "ns"];
|
let multicall_names = ["nb", "nr", "ns", "nu"];
|
||||||
for name in &multicall_names {
|
for name in &multicall_names {
|
||||||
let symlink_path = output_dir.join(format!("{name}.{shell}"));
|
let symlink_path = output_dir.join(format!("{name}.{shell}"));
|
||||||
if symlink_path.exists() {
|
if symlink_path.exists() {
|
||||||
|
|
@ -12,10 +12,11 @@ name = "eh"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
clap.workspace = true
|
clap.workspace = true
|
||||||
|
dialoguer.workspace = true
|
||||||
|
eh-log.workspace = true
|
||||||
regex.workspace = true
|
regex.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
tempfile.workspace = true
|
tempfile.workspace = true
|
||||||
thiserror.workspace = true
|
thiserror.workspace = true
|
||||||
tracing.workspace = true
|
|
||||||
tracing-subscriber.workspace = true
|
|
||||||
walkdir.workspace = true
|
walkdir.workspace = true
|
||||||
yansi.workspace = true
|
yansi.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
use std::{
|
use std::{
|
||||||
collections::VecDeque,
|
|
||||||
io::{self, Read, Write},
|
io::{self, Read, Write},
|
||||||
process::{Command, ExitStatus, Output, Stdio},
|
process::{Command, ExitStatus, Output, Stdio},
|
||||||
|
sync::mpsc,
|
||||||
|
thread,
|
||||||
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::error::{EhError, Result};
|
use crate::error::{EhError, Result};
|
||||||
|
|
@ -27,6 +29,43 @@ impl LogInterceptor for StdIoInterceptor {
|
||||||
/// Default buffer size for reading command output
|
/// Default buffer size for reading command output
|
||||||
const DEFAULT_BUFFER_SIZE: usize = 4096;
|
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.
|
/// Builder and executor for Nix commands.
|
||||||
pub struct NixCommand {
|
pub struct NixCommand {
|
||||||
subcommand: String,
|
subcommand: String,
|
||||||
|
|
@ -54,16 +93,6 @@ impl NixCommand {
|
||||||
self
|
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]
|
#[must_use]
|
||||||
pub fn args_ref(mut self, args: &[String]) -> Self {
|
pub fn args_ref(mut self, args: &[String]) -> Self {
|
||||||
self.args.extend(args.iter().cloned());
|
self.args.extend(args.iter().cloned());
|
||||||
|
|
@ -97,11 +126,9 @@ impl NixCommand {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run the command, streaming output to the provided interceptor.
|
/// Build the underlying `std::process::Command` with all configured
|
||||||
pub fn run_with_logs<I: LogInterceptor + 'static>(
|
/// arguments, environment variables, and flags.
|
||||||
&self,
|
fn build_command(&self) -> Command {
|
||||||
mut interceptor: I,
|
|
||||||
) -> Result<ExitStatus> {
|
|
||||||
let mut cmd = Command::new("nix");
|
let mut cmd = Command::new("nix");
|
||||||
cmd.arg(&self.subcommand);
|
cmd.arg(&self.subcommand);
|
||||||
|
|
||||||
|
|
@ -117,6 +144,18 @@ impl NixCommand {
|
||||||
cmd.env(k, v);
|
cmd.env(k, v);
|
||||||
}
|
}
|
||||||
cmd.args(&self.args);
|
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 {
|
if self.interactive {
|
||||||
cmd.stdout(Stdio::inherit());
|
cmd.stdout(Stdio::inherit());
|
||||||
|
|
@ -129,86 +168,152 @@ impl NixCommand {
|
||||||
cmd.stderr(Stdio::piped());
|
cmd.stderr(Stdio::piped());
|
||||||
|
|
||||||
let mut child = cmd.spawn()?;
|
let mut child = cmd.spawn()?;
|
||||||
let child_stdout = child.stdout.take().ok_or_else(|| {
|
let stdout = child.stdout.take().ok_or_else(|| {
|
||||||
EhError::CommandFailed {
|
EhError::CommandFailed {
|
||||||
command: format!("nix {}", self.subcommand),
|
command: format!("nix {}", self.subcommand),
|
||||||
}
|
}
|
||||||
})?;
|
})?;
|
||||||
let child_stderr = child.stderr.take().ok_or_else(|| {
|
let stderr = child.stderr.take().ok_or_else(|| {
|
||||||
EhError::CommandFailed {
|
EhError::CommandFailed {
|
||||||
command: format!("nix {}", self.subcommand),
|
command: format!("nix {}", self.subcommand),
|
||||||
}
|
}
|
||||||
})?;
|
})?;
|
||||||
let mut stdout = child_stdout;
|
|
||||||
let mut stderr = child_stderr;
|
|
||||||
|
|
||||||
let mut out_buf = [0u8; DEFAULT_BUFFER_SIZE];
|
let (tx, rx) = mpsc::channel();
|
||||||
let mut err_buf = [0u8; DEFAULT_BUFFER_SIZE];
|
|
||||||
|
|
||||||
let mut out_queue = VecDeque::new();
|
let tx_out = tx.clone();
|
||||||
let mut err_queue = VecDeque::new();
|
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();
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let mut did_something = false;
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
match stdout.read(&mut out_buf) {
|
match rx.recv_timeout(Duration::from_millis(100)) {
|
||||||
Ok(0) => {},
|
Ok(PipeEvent::Stdout(data)) => interceptor.on_stdout(&data),
|
||||||
Ok(n) => {
|
Ok(PipeEvent::Stderr(data)) => interceptor.on_stderr(&data),
|
||||||
interceptor.on_stdout(&out_buf[..n]);
|
Ok(PipeEvent::Error(e)) => {
|
||||||
out_queue.push_back(Vec::from(&out_buf[..n]));
|
let _ = child.kill();
|
||||||
did_something = true;
|
let _ = stdout_thread.join();
|
||||||
|
let _ = stderr_thread.join();
|
||||||
|
let _ = child.wait();
|
||||||
|
return Err(EhError::Io(e));
|
||||||
},
|
},
|
||||||
Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => {},
|
Err(mpsc::RecvTimeoutError::Timeout) => {},
|
||||||
Err(e) => return Err(EhError::Io(e)),
|
// All senders dropped — both reader threads finished
|
||||||
|
Err(mpsc::RecvTimeoutError::Disconnected) => break,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
match stderr.read(&mut err_buf) {
|
let _ = stdout_thread.join();
|
||||||
Ok(0) => {},
|
let _ = stderr_thread.join();
|
||||||
Ok(n) => {
|
|
||||||
interceptor.on_stderr(&err_buf[..n]);
|
|
||||||
err_queue.push_back(Vec::from(&err_buf[..n]));
|
|
||||||
did_something = true;
|
|
||||||
},
|
|
||||||
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 status = child.wait()?;
|
let status = child.wait()?;
|
||||||
Ok(status)
|
Ok(status)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run the command and capture all output.
|
/// Run the command and capture all output (with timeout).
|
||||||
pub fn output(&self) -> Result<Output> {
|
pub fn output(&self) -> Result<Output> {
|
||||||
let mut cmd = Command::new("nix");
|
let mut cmd = self.build_command();
|
||||||
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 {
|
if self.interactive {
|
||||||
cmd.stdout(Stdio::inherit());
|
cmd.stdout(Stdio::inherit());
|
||||||
cmd.stderr(Stdio::inherit());
|
cmd.stderr(Stdio::inherit());
|
||||||
cmd.stdin(Stdio::inherit());
|
cmd.stdin(Stdio::inherit());
|
||||||
} else {
|
return Ok(cmd.output()?);
|
||||||
cmd.stdout(Stdio::piped());
|
|
||||||
cmd.stderr(Stdio::piped());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(cmd.output()?)
|
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,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
253
eh/src/error.rs
253
eh/src/error.rs
|
|
@ -1,36 +1,59 @@
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum EhError {
|
pub enum EhError {
|
||||||
#[error("Nix command failed: {0}")]
|
#[error("nix {command} failed")]
|
||||||
NixCommandFailed(String),
|
NixCommandFailed { command: String },
|
||||||
|
|
||||||
#[error("IO error: {0}")]
|
#[error("io: {0}")]
|
||||||
Io(#[from] std::io::Error),
|
Io(#[from] std::io::Error),
|
||||||
|
|
||||||
#[error("Regex error: {0}")]
|
#[error("regex: {0}")]
|
||||||
Regex(#[from] regex::Error),
|
Regex(#[from] regex::Error),
|
||||||
|
|
||||||
#[error("UTF-8 conversion error: {0}")]
|
#[error("utf-8 conversion: {0}")]
|
||||||
Utf8(#[from] std::string::FromUtf8Error),
|
Utf8(#[from] std::string::FromUtf8Error),
|
||||||
|
|
||||||
#[error("Hash extraction failed")]
|
#[error("could not extract hash from nix output")]
|
||||||
HashExtractionFailed,
|
HashExtractionFailed { stderr: String },
|
||||||
|
|
||||||
#[error("No Nix files found")]
|
#[error("no .nix files found in the current directory")]
|
||||||
NoNixFilesFound,
|
NoNixFilesFound,
|
||||||
|
|
||||||
#[error("Failed to fix hash in file: {path}")]
|
#[error("could not update hash in {path}")]
|
||||||
HashFixFailed { path: String },
|
HashFixFailed { path: String },
|
||||||
|
|
||||||
#[error("Process exited with code: {code}")]
|
#[error("process exited with code {code}")]
|
||||||
ProcessExit { code: i32 },
|
ProcessExit { code: i32 },
|
||||||
|
|
||||||
#[error("Command execution failed: {command}")]
|
#[error("command '{command}' failed")]
|
||||||
CommandFailed { command: String },
|
CommandFailed { command: String },
|
||||||
|
|
||||||
#[error("Invalid input: {input} - {reason}")]
|
#[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}")]
|
||||||
InvalidInput { input: String, reason: String },
|
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>;
|
pub type Result<T> = std::result::Result<T, EhError>;
|
||||||
|
|
@ -40,9 +63,209 @@ impl EhError {
|
||||||
pub const fn exit_code(&self) -> i32 {
|
pub const fn exit_code(&self) -> i32 {
|
||||||
match self {
|
match self {
|
||||||
Self::ProcessExit { code } => *code,
|
Self::ProcessExit { code } => *code,
|
||||||
Self::NixCommandFailed(_) => 1,
|
Self::NixCommandFailed { .. } => 2,
|
||||||
Self::CommandFailed { .. } => 1,
|
Self::CommandFailed { .. } => 3,
|
||||||
_ => 1,
|
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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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,6 +3,7 @@ pub mod command;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod run;
|
pub mod run;
|
||||||
pub mod shell;
|
pub mod shell;
|
||||||
|
pub mod update;
|
||||||
pub mod util;
|
pub mod util;
|
||||||
|
|
||||||
pub use clap::{CommandFactory, Parser, Subcommand};
|
pub use clap::{CommandFactory, Parser, Subcommand};
|
||||||
|
|
@ -34,4 +35,9 @@ pub enum Command {
|
||||||
#[arg(trailing_var_arg = true)]
|
#[arg(trailing_var_arg = true)]
|
||||||
args: Vec<String>,
|
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 eh::{Cli, Command, CommandFactory, Parser};
|
||||||
use error::Result;
|
use error::Result;
|
||||||
|
use yansi::Paint;
|
||||||
|
|
||||||
mod build;
|
mod build;
|
||||||
mod command;
|
mod command;
|
||||||
mod error;
|
mod error;
|
||||||
mod run;
|
mod run;
|
||||||
mod shell;
|
mod shell;
|
||||||
|
mod update;
|
||||||
mod util;
|
mod util;
|
||||||
|
|
||||||
fn main() {
|
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();
|
let result = run_app();
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(code) => std::process::exit(code),
|
Ok(code) => std::process::exit(code),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Error: {e}");
|
let code = e.exit_code();
|
||||||
std::process::exit(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);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -37,42 +37,54 @@ fn dispatch_multicall(
|
||||||
) -> Option<Result<i32>> {
|
) -> Option<Result<i32>> {
|
||||||
let rest: Vec<String> = args.collect();
|
let rest: Vec<String> = args.collect();
|
||||||
|
|
||||||
// Validate arguments before processing
|
let subcommand = match app_name {
|
||||||
if let Err(e) = util::validate_nix_args(&rest) {
|
"nr" => "run",
|
||||||
return Some(Err(e));
|
"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));
|
||||||
}
|
}
|
||||||
|
|
||||||
let hash_extractor = util::RegexHashExtractor;
|
let hash_extractor = util::RegexHashExtractor;
|
||||||
let fixer = util::DefaultNixFileFixer;
|
let fixer = util::DefaultNixFileFixer;
|
||||||
let classifier = util::DefaultNixErrorClassifier;
|
let classifier = util::DefaultNixErrorClassifier;
|
||||||
|
|
||||||
match app_name {
|
Some(match subcommand {
|
||||||
"nr" => {
|
"run" => run::handle_nix_run(&rest, &hash_extractor, &fixer, &classifier),
|
||||||
Some(run::handle_nix_run(
|
"shell" => {
|
||||||
&rest,
|
shell::handle_nix_shell(&rest, &hash_extractor, &fixer, &classifier)
|
||||||
&hash_extractor,
|
|
||||||
&fixer,
|
|
||||||
&classifier,
|
|
||||||
))
|
|
||||||
},
|
},
|
||||||
"ns" => {
|
"build" => {
|
||||||
Some(shell::handle_nix_shell(
|
build::handle_nix_build(&rest, &hash_extractor, &fixer, &classifier)
|
||||||
&rest,
|
|
||||||
&hash_extractor,
|
|
||||||
&fixer,
|
|
||||||
&classifier,
|
|
||||||
))
|
|
||||||
},
|
},
|
||||||
"nb" => {
|
// subcommand is assigned from the match on app_name above;
|
||||||
Some(build::handle_nix_build(
|
// only "run"/"shell"/"build" are possible values.
|
||||||
&rest,
|
_ => unreachable!(),
|
||||||
&hash_extractor,
|
})
|
||||||
&fixer,
|
|
||||||
&classifier,
|
|
||||||
))
|
|
||||||
},
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_app() -> Result<i32> {
|
fn run_app() -> Result<i32> {
|
||||||
|
|
@ -107,7 +119,9 @@ fn run_app() -> Result<i32> {
|
||||||
build::handle_nix_build(&args, &hash_extractor, &fixer, &classifier)
|
build::handle_nix_build(&args, &hash_extractor, &fixer, &classifier)
|
||||||
},
|
},
|
||||||
|
|
||||||
_ => {
|
Some(Command::Update { args }) => update::handle_update(&args),
|
||||||
|
|
||||||
|
None => {
|
||||||
Cli::command().print_help()?;
|
Cli::command().print_help()?;
|
||||||
println!();
|
println!();
|
||||||
Ok(0)
|
Ok(0)
|
||||||
|
|
|
||||||
115
eh/src/update.rs
Normal file
115
eh/src/update.rs
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
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)));
|
||||||
|
}
|
||||||
|
}
|
||||||
625
eh/src/util.rs
625
eh/src/util.rs
|
|
@ -1,11 +1,12 @@
|
||||||
use std::{
|
use std::{
|
||||||
io::{BufWriter, Write},
|
io::{BufWriter, Write},
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
|
sync::LazyLock,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use eh_log::{log_info, log_warn};
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use tempfile::NamedTempFile;
|
use tempfile::NamedTempFile;
|
||||||
use tracing::{info, warn};
|
|
||||||
use walkdir::WalkDir;
|
use walkdir::WalkDir;
|
||||||
use yansi::Paint;
|
use yansi::Paint;
|
||||||
|
|
||||||
|
|
@ -14,22 +15,41 @@ use crate::{
|
||||||
error::{EhError, Result},
|
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 {
|
pub trait HashExtractor {
|
||||||
fn extract_hash(&self, stderr: &str) -> Option<String>;
|
fn extract_hash(&self, stderr: &str) -> Option<String>;
|
||||||
|
fn extract_old_hash(&self, stderr: &str) -> Option<String>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct RegexHashExtractor;
|
pub struct RegexHashExtractor;
|
||||||
|
|
||||||
impl HashExtractor for RegexHashExtractor {
|
impl HashExtractor for RegexHashExtractor {
|
||||||
fn extract_hash(&self, stderr: &str) -> Option<String> {
|
fn extract_hash(&self, stderr: &str) -> Option<String> {
|
||||||
let patterns = [
|
for re in HASH_EXTRACT_PATTERNS.iter() {
|
||||||
r"got:\s+(sha256-[a-zA-Z0-9+/=]+)",
|
if let Some(captures) = re.captures(stderr)
|
||||||
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)
|
&& let Some(hash) = captures.get(1)
|
||||||
{
|
{
|
||||||
return Some(hash.as_str().to_string());
|
return Some(hash.as_str().to_string());
|
||||||
|
|
@ -37,23 +57,43 @@ impl HashExtractor for RegexHashExtractor {
|
||||||
}
|
}
|
||||||
None
|
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 {
|
pub trait NixFileFixer {
|
||||||
fn fix_hash_in_files(&self, new_hash: &str) -> Result<bool>;
|
fn fix_hash_in_files(
|
||||||
|
&self,
|
||||||
|
old_hash: Option<&str>,
|
||||||
|
new_hash: &str,
|
||||||
|
) -> Result<bool>;
|
||||||
fn find_nix_files(&self) -> Result<Vec<PathBuf>>;
|
fn find_nix_files(&self) -> Result<Vec<PathBuf>>;
|
||||||
fn fix_hash_in_file(&self, file_path: &Path, new_hash: &str) -> Result<bool>;
|
fn fix_hash_in_file(
|
||||||
|
&self,
|
||||||
|
file_path: &Path,
|
||||||
|
old_hash: Option<&str>,
|
||||||
|
new_hash: &str,
|
||||||
|
) -> Result<bool>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct DefaultNixFileFixer;
|
pub struct DefaultNixFileFixer;
|
||||||
|
|
||||||
impl NixFileFixer for DefaultNixFileFixer {
|
impl NixFileFixer for DefaultNixFileFixer {
|
||||||
fn fix_hash_in_files(&self, new_hash: &str) -> Result<bool> {
|
fn fix_hash_in_files(
|
||||||
|
&self,
|
||||||
|
old_hash: Option<&str>,
|
||||||
|
new_hash: &str,
|
||||||
|
) -> Result<bool> {
|
||||||
let nix_files = self.find_nix_files()?;
|
let nix_files = self.find_nix_files()?;
|
||||||
let mut fixed = false;
|
let mut fixed = false;
|
||||||
for file_path in nix_files {
|
for file_path in nix_files {
|
||||||
if self.fix_hash_in_file(&file_path, new_hash)? {
|
if self.fix_hash_in_file(&file_path, old_hash, new_hash)? {
|
||||||
println!("Updated hash in {}", file_path.display());
|
log_info!("updated hash in {}", file_path.display().bold());
|
||||||
fixed = true;
|
fixed = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -61,8 +101,20 @@ impl NixFileFixer for DefaultNixFileFixer {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn find_nix_files(&self) -> Result<Vec<PathBuf>> {
|
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(".")
|
let files: Vec<PathBuf> = WalkDir::new(".")
|
||||||
|
.max_depth(3)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
.filter_entry(|e| !should_skip(e))
|
||||||
.filter_map(std::result::Result::ok)
|
.filter_map(std::result::Result::ok)
|
||||||
.filter(|entry| {
|
.filter(|entry| {
|
||||||
entry.file_type().is_file()
|
entry.file_type().is_file()
|
||||||
|
|
@ -80,34 +132,37 @@ impl NixFileFixer for DefaultNixFileFixer {
|
||||||
Ok(files)
|
Ok(files)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fix_hash_in_file(&self, file_path: &Path, new_hash: &str) -> Result<bool> {
|
fn fix_hash_in_file(
|
||||||
// Pre-compile regex patterns once to avoid repeated compilation
|
&self,
|
||||||
let patterns: Vec<(Regex, String)> = [
|
file_path: &Path,
|
||||||
(r#"hash\s*=\s*"[^"]*""#, format!(r#"hash = "{new_hash}""#)),
|
old_hash: Option<&str>,
|
||||||
(
|
new_hash: &str,
|
||||||
r#"sha256\s*=\s*"[^"]*""#,
|
) -> Result<bool> {
|
||||||
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
|
// Read the entire file content
|
||||||
let content = std::fs::read_to_string(file_path)?;
|
let content = std::fs::read_to_string(file_path)?;
|
||||||
let mut replaced = false;
|
let mut replaced = false;
|
||||||
let mut result_content = content;
|
let mut result_content = content;
|
||||||
|
|
||||||
// Apply replacements
|
if let Some(old) = old_hash {
|
||||||
for (re, replacement) in &patterns {
|
// 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) {
|
if re.is_match(&result_content) {
|
||||||
result_content = re
|
result_content = re
|
||||||
.replace_all(&result_content, replacement.as_str())
|
.replace_all(&result_content, replacement.as_str())
|
||||||
|
|
@ -115,6 +170,23 @@ impl NixFileFixer for DefaultNixFileFixer {
|
||||||
replaced = true;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Write back to file atomically
|
// Write back to file atomically
|
||||||
if replaced {
|
if replaced {
|
||||||
|
|
@ -140,38 +212,121 @@ pub trait NixErrorClassifier {
|
||||||
fn should_retry(&self, stderr: &str) -> bool;
|
fn should_retry(&self, stderr: &str) -> bool;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Pre-evaluate expression to catch errors early
|
/// Classifies what retry action should be taken based on nix stderr output.
|
||||||
fn pre_evaluate(_subcommand: &str, args: &[String]) -> Result<bool> {
|
#[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> {
|
||||||
// Find flake references or expressions to evaluate
|
// Find flake references or expressions to evaluate
|
||||||
// Only take the first non-flag argument (the package/expression)
|
// Only take the first non-flag argument (the package/expression)
|
||||||
let eval_arg = args.iter().find(|arg| !arg.starts_with('-'));
|
let eval_arg = args.iter().find(|arg| !arg.starts_with('-'));
|
||||||
|
|
||||||
let Some(eval_arg) = eval_arg else {
|
let Some(eval_arg) = eval_arg else {
|
||||||
return Ok(true); // No expression to evaluate
|
return Ok(RetryAction::None); // No expression to evaluate
|
||||||
};
|
};
|
||||||
|
|
||||||
let eval_cmd = NixCommand::new("eval").arg(eval_arg).arg("--raw");
|
let eval_cmd = NixCommand::new("eval")
|
||||||
|
.arg(eval_arg)
|
||||||
|
.print_build_logs(false);
|
||||||
|
|
||||||
let output = eval_cmd.output()?;
|
let output = eval_cmd.output()?;
|
||||||
|
|
||||||
if output.status.success() {
|
if output.status.success() {
|
||||||
return Ok(true);
|
return Ok(RetryAction::None);
|
||||||
}
|
}
|
||||||
|
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
|
|
||||||
// If eval fails due to unfree/insecure/broken, don't fail pre-evaluation
|
// Classify whether this is a retryable error (unfree/insecure/broken)
|
||||||
// Let the main command handle it with retry logic
|
let action = classify_retry_action(&stderr);
|
||||||
if stderr.contains("has an unfree license")
|
if action != RetryAction::None {
|
||||||
|| stderr.contains("refusing to evaluate")
|
return Ok(action);
|
||||||
|| stderr.contains("has been marked as insecure")
|
|
||||||
|| stderr.contains("has been marked as broken")
|
|
||||||
{
|
|
||||||
return Ok(true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// For other eval failures, fail early
|
// Non-retryable eval failure — fail fast with a clear message
|
||||||
Ok(false)
|
// 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(),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn validate_nix_args(args: &[String]) -> Result<()> {
|
pub fn validate_nix_args(args: &[String]) -> Result<()> {
|
||||||
|
|
@ -202,15 +357,28 @@ pub fn handle_nix_with_retry(
|
||||||
interactive: bool,
|
interactive: bool,
|
||||||
) -> Result<i32> {
|
) -> Result<i32> {
|
||||||
validate_nix_args(args)?;
|
validate_nix_args(args)?;
|
||||||
// Pre-evaluate for build commands to catch errors early
|
|
||||||
if !pre_evaluate(subcommand, args)? {
|
// Pre-evaluate to detect retryable errors (unfree/insecure/broken) before
|
||||||
return Err(EhError::NixCommandFailed(
|
// running the actual command. This avoids streaming verbose nix error output
|
||||||
"Expression evaluation failed".to_string(),
|
// 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));
|
||||||
}
|
}
|
||||||
|
|
||||||
// For run commands, try interactive first to avoid breaking terminal
|
// For run/shell commands, try interactive mode now that pre-eval passed
|
||||||
if subcommand == "run" && interactive {
|
if interactive {
|
||||||
let status = NixCommand::new(subcommand)
|
let status = NixCommand::new(subcommand)
|
||||||
.print_build_logs(true)
|
.print_build_logs(true)
|
||||||
.interactive(true)
|
.interactive(true)
|
||||||
|
|
@ -221,18 +389,22 @@ pub fn handle_nix_with_retry(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// First, always capture output to check for errors that need retry
|
// Capture output to check for errors that need retry (hash mismatches etc.)
|
||||||
let output_cmd = NixCommand::new(subcommand)
|
let output_cmd = NixCommand::new(subcommand)
|
||||||
.print_build_logs(true)
|
.print_build_logs(true)
|
||||||
.args_ref(args);
|
.args_ref(args);
|
||||||
let output = output_cmd.output()?;
|
let output = output_cmd.output()?;
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
|
|
||||||
// Check if we need to retry with special flags
|
// Check for hash mismatch errors
|
||||||
if let Some(new_hash) = hash_extractor.extract_hash(&stderr) {
|
if let Some(new_hash) = hash_extractor.extract_hash(&stderr) {
|
||||||
match fixer.fix_hash_in_files(&new_hash) {
|
let old_hash = hash_extractor.extract_old_hash(&stderr);
|
||||||
|
match fixer.fix_hash_in_files(old_hash.as_deref(), &new_hash) {
|
||||||
Ok(true) => {
|
Ok(true) => {
|
||||||
info!("{}", Paint::green("✔ Fixed hash mismatch, retrying..."));
|
log_info!(
|
||||||
|
"{}: hash mismatch corrected in local files, rebuilding",
|
||||||
|
pkg.bold()
|
||||||
|
);
|
||||||
let mut retry_cmd = NixCommand::new(subcommand)
|
let mut retry_cmd = NixCommand::new(subcommand)
|
||||||
.print_build_logs(true)
|
.print_build_logs(true)
|
||||||
.args_ref(args);
|
.args_ref(args);
|
||||||
|
|
@ -246,72 +418,33 @@ pub fn handle_nix_with_retry(
|
||||||
// No files were fixed, continue with normal error handling
|
// No files were fixed, continue with normal error handling
|
||||||
},
|
},
|
||||||
Err(EhError::NoNixFilesFound) => {
|
Err(EhError::NoNixFilesFound) => {
|
||||||
warn!("No .nix files found to fix hash in");
|
log_warn!(
|
||||||
|
"{}: hash mismatch detected but no .nix files found to update",
|
||||||
|
pkg.bold()
|
||||||
|
);
|
||||||
// Continue with normal error handling
|
// Continue with normal error handling
|
||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
return Err(e);
|
return Err(e);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
} else if stderr.contains("hash") || stderr.contains("sha256") {
|
} else if is_hash_mismatch_error(&stderr) {
|
||||||
// If there's a hash-related error but we couldn't extract it, that's a
|
// There's a genuine hash mismatch but we couldn't extract the new hash
|
||||||
// failure
|
return Err(EhError::HashExtractionFailed {
|
||||||
return Err(EhError::HashExtractionFailed);
|
stderr: stderr.to_string(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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) {
|
if classifier.should_retry(&stderr) {
|
||||||
if stderr.contains("has an unfree license") && stderr.contains("refusing") {
|
let action = classify_retry_action(&stderr);
|
||||||
warn!(
|
if let Some((env_var, reason)) = action.env_override() {
|
||||||
"{}",
|
print_retry_msg(pkg, reason, env_var);
|
||||||
Paint::yellow(
|
|
||||||
"⚠ Unfree package detected, retrying with NIXPKGS_ALLOW_UNFREE=1..."
|
|
||||||
)
|
|
||||||
);
|
|
||||||
let mut retry_cmd = NixCommand::new(subcommand)
|
let mut retry_cmd = NixCommand::new(subcommand)
|
||||||
.print_build_logs(true)
|
.print_build_logs(true)
|
||||||
.args_ref(args)
|
.args_ref(args)
|
||||||
.env("NIXPKGS_ALLOW_UNFREE", "1")
|
.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));
|
|
||||||
}
|
|
||||||
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);
|
.impure(true);
|
||||||
if interactive {
|
if interactive {
|
||||||
retry_cmd = retry_cmd.interactive(true);
|
retry_cmd = retry_cmd.interactive(true);
|
||||||
|
|
@ -330,22 +463,23 @@ pub fn handle_nix_with_retry(
|
||||||
std::io::stderr()
|
std::io::stderr()
|
||||||
.write_all(&output.stderr)
|
.write_all(&output.stderr)
|
||||||
.map_err(EhError::Io)?;
|
.map_err(EhError::Io)?;
|
||||||
Err(EhError::ProcessExit {
|
|
||||||
code: output.status.code().unwrap_or(1),
|
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(),
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct DefaultNixErrorClassifier;
|
pub struct DefaultNixErrorClassifier;
|
||||||
|
|
||||||
impl NixErrorClassifier for DefaultNixErrorClassifier {
|
impl NixErrorClassifier for DefaultNixErrorClassifier {
|
||||||
fn should_retry(&self, stderr: &str) -> bool {
|
fn should_retry(&self, stderr: &str) -> bool {
|
||||||
RegexHashExtractor.extract_hash(stderr).is_some()
|
classify_retry_action(stderr) != RetryAction::None
|
||||||
|| (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"))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -379,7 +513,7 @@ mod tests {
|
||||||
|
|
||||||
let fixer = DefaultNixFileFixer;
|
let fixer = DefaultNixFileFixer;
|
||||||
let result = fixer
|
let result = fixer
|
||||||
.fix_hash_in_file(file_path, "sha256-newhash999")
|
.fix_hash_in_file(file_path, None, "sha256-newhash999")
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert!(result, "Hash replacement should return true");
|
assert!(result, "Hash replacement should return true");
|
||||||
|
|
@ -413,7 +547,7 @@ mod tests {
|
||||||
// Test hash replacement
|
// Test hash replacement
|
||||||
let fixer = DefaultNixFileFixer;
|
let fixer = DefaultNixFileFixer;
|
||||||
let result = fixer
|
let result = fixer
|
||||||
.fix_hash_in_file(&file_path, "sha256-newhash999")
|
.fix_hash_in_file(&file_path, None, "sha256-newhash999")
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
|
|
@ -448,7 +582,7 @@ mod tests {
|
||||||
// Test that streaming can handle large files without memory issues
|
// Test that streaming can handle large files without memory issues
|
||||||
let fixer = DefaultNixFileFixer;
|
let fixer = DefaultNixFileFixer;
|
||||||
let result = fixer
|
let result = fixer
|
||||||
.fix_hash_in_file(file_path, "sha256-newhash999")
|
.fix_hash_in_file(file_path, None, "sha256-newhash999")
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert!(result, "Hash replacement should work for large files");
|
assert!(result, "Hash replacement should work for large files");
|
||||||
|
|
@ -483,7 +617,9 @@ mod tests {
|
||||||
|
|
||||||
// Test hash replacement
|
// Test hash replacement
|
||||||
let fixer = DefaultNixFileFixer;
|
let fixer = DefaultNixFileFixer;
|
||||||
let result = fixer.fix_hash_in_file(file_path, "sha256-newhash").unwrap();
|
let result = fixer
|
||||||
|
.fix_hash_in_file(file_path, None, "sha256-newhash")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
assert!(result, "Hash replacement should succeed");
|
assert!(result, "Hash replacement should succeed");
|
||||||
|
|
||||||
|
|
@ -538,4 +674,233 @@ mod tests {
|
||||||
safe_args
|
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": {
|
"nodes": {
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1762844143,
|
"lastModified": 1769461804,
|
||||||
"narHash": "sha256-SlybxLZ1/e4T2lb1czEtWVzDCVSTvk9WLwGhmxFmBxI=",
|
"narHash": "sha256-msG8SU5WsBUfVVa/9RPLaymvi5bI8edTavbIq3vRlhI=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "9da7f1cf7f8a6e2a7cb3001b048546c92a8258b4",
|
"rev": "bfc1b8a4574108ceef22f02bafcf6611380c100d",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue