eh: modernize error handling; bump dependencies

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I63e346cd38bfb6cd277f6675fcefe64e6a6a6964
This commit is contained in:
raf 2025-11-12 23:50:58 +03:00
commit caa6dc9951
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
12 changed files with 265 additions and 172 deletions

93
Cargo.lock generated
View file

@ -25,9 +25,9 @@ checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.5.41" version = "4.5.51"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be92d32e80243a54711e5d7ce823c35c41c9d929dc4ab58e1276f625841aadf9" checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5"
dependencies = [ dependencies = [
"clap_builder", "clap_builder",
"clap_derive", "clap_derive",
@ -35,9 +35,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_builder" name = "clap_builder"
version = "4.5.41" version = "4.5.51"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "707eab41e9622f9139419d573eca0900137718000c517d47da73045f54331c3d" checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a"
dependencies = [ dependencies = [
"anstyle", "anstyle",
"clap_lex", "clap_lex",
@ -45,18 +45,18 @@ dependencies = [
[[package]] [[package]]
name = "clap_complete" name = "clap_complete"
version = "4.5.55" version = "4.5.60"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5abde44486daf70c5be8b8f8f1b66c49f86236edf6fa2abadb4d961c4c6229a" checksum = "8e602857739c5a4291dfa33b5a298aeac9006185229a700e5810a3ef7272d971"
dependencies = [ dependencies = [
"clap", "clap",
] ]
[[package]] [[package]]
name = "clap_derive" name = "clap_derive"
version = "4.5.41" version = "4.5.49"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef4f52386a59ca4c860f7393bcf8abd8dfd91ecccc0f774635ff68e92eeef491" checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671"
dependencies = [ dependencies = [
"heck", "heck",
"proc-macro2", "proc-macro2",
@ -72,10 +72,11 @@ checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675"
[[package]] [[package]]
name = "eh" name = "eh"
version = "0.1.1" version = "0.1.2"
dependencies = [ dependencies = [
"clap", "clap",
"regex", "regex",
"thiserror",
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",
"yansi", "yansi",
@ -107,12 +108,11 @@ checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
[[package]] [[package]]
name = "nu-ansi-term" name = "nu-ansi-term"
version = "0.46.0" version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [ dependencies = [
"overload", "windows-sys",
"winapi",
] ]
[[package]] [[package]]
@ -121,12 +121,6 @@ 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 = "overload"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]] [[package]]
name = "pin-project-lite" name = "pin-project-lite"
version = "0.2.16" version = "0.2.16"
@ -153,9 +147,9 @@ dependencies = [
[[package]] [[package]]
name = "regex" name = "regex"
version = "1.11.1" version = "1.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
@ -165,9 +159,9 @@ dependencies = [
[[package]] [[package]]
name = "regex-automata" name = "regex-automata"
version = "0.4.9" version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
@ -206,6 +200,26 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "thiserror"
version = "2.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "thread_local" name = "thread_local"
version = "1.1.9" version = "1.1.9"
@ -260,9 +274,9 @@ dependencies = [
[[package]] [[package]]
name = "tracing-subscriber" name = "tracing-subscriber"
version = "0.3.19" version = "0.3.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5"
dependencies = [ dependencies = [
"nu-ansi-term", "nu-ansi-term",
"sharded-slab", "sharded-slab",
@ -285,30 +299,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
[[package]] [[package]]
name = "winapi" name = "windows-link"
version = "0.3.9" version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [ dependencies = [
"winapi-i686-pc-windows-gnu", "windows-link",
"winapi-x86_64-pc-windows-gnu",
] ]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]] [[package]]
name = "xtask" name = "xtask"
version = "0.1.1" version = "0.1.2"
dependencies = [ dependencies = [
"clap", "clap",
"clap_complete", "clap_complete",

View file

@ -8,16 +8,17 @@ description = "Ergonomic Nix CLI helper"
edition = "2024" edition = "2024"
license = "MPL-2.0" license = "MPL-2.0"
readme = true readme = true
rust-version = "1.85" rust-version = "1.89"
version = "0.1.1" version = "0.1.2"
[workspace.dependencies] [workspace.dependencies]
clap = { default-features = false, features = [ "std", "help", "derive" ], version = "4.5" } clap = { default-features = false, features = [ "std", "help", "derive" ], version = "4.5.51" }
clap_complete = "4.5" clap_complete = "4.5.60"
regex = "1.11" regex = "1.12.2"
tracing = "0.1" thiserror = "2.0.17"
tracing-subscriber = "0.3" tracing = "0.1.41"
yansi = "1.0" tracing-subscriber = "0.3.20"
yansi = "1.0.1"
[profile.release] [profile.release]
codegen-units = 1 codegen-units = 1

View file

@ -13,6 +13,7 @@ crate-type = ["lib"]
[dependencies] [dependencies]
clap.workspace = true clap.workspace = true
regex.workspace = true regex.workspace = true
thiserror.workspace = true
tracing.workspace = true tracing.workspace = true
tracing-subscriber.workspace = true tracing-subscriber.workspace = true
yansi.workspace = true yansi.workspace = true

View file

@ -1,3 +1,4 @@
use crate::error::Result;
use crate::util::{HashExtractor, NixErrorClassifier, NixFileFixer, handle_nix_with_retry}; use crate::util::{HashExtractor, NixErrorClassifier, NixFileFixer, handle_nix_with_retry};
pub fn handle_nix_build( pub fn handle_nix_build(
@ -5,6 +6,6 @@ pub fn handle_nix_build(
hash_extractor: &dyn HashExtractor, hash_extractor: &dyn HashExtractor,
fixer: &dyn NixFileFixer, fixer: &dyn NixFileFixer,
classifier: &dyn NixErrorClassifier, classifier: &dyn NixErrorClassifier,
) { ) -> Result<i32> {
handle_nix_with_retry("build", args, hash_extractor, fixer, classifier, false); handle_nix_with_retry("build", args, hash_extractor, fixer, classifier, false)
} }

View file

@ -1,3 +1,4 @@
use crate::error::{EhError, Result};
use std::collections::VecDeque; use std::collections::VecDeque;
use std::io::{self, Read, Write}; use std::io::{self, Read, Write};
use std::process::{Command, ExitStatus, Output, Stdio}; use std::process::{Command, ExitStatus, Output, Stdio};
@ -61,17 +62,17 @@ impl NixCommand {
self self
} }
pub fn impure(mut self, yes: bool) -> Self { #[must_use] pub const fn impure(mut self, yes: bool) -> Self {
self.impure = yes; self.impure = yes;
self self
} }
pub fn interactive(mut self, yes: bool) -> Self { #[must_use] pub const fn interactive(mut self, yes: bool) -> Self {
self.interactive = yes; self.interactive = yes;
self self
} }
pub fn print_build_logs(mut self, yes: bool) -> Self { #[must_use] pub const fn print_build_logs(mut self, yes: bool) -> Self {
self.print_build_logs = yes; self.print_build_logs = yes;
self self
} }
@ -80,7 +81,7 @@ impl NixCommand {
pub fn run_with_logs<I: LogInterceptor + 'static>( pub fn run_with_logs<I: LogInterceptor + 'static>(
&self, &self,
mut interceptor: I, mut interceptor: I,
) -> io::Result<ExitStatus> { ) -> Result<ExitStatus> {
let mut cmd = Command::new("nix"); let mut cmd = Command::new("nix");
cmd.arg(&self.subcommand); cmd.arg(&self.subcommand);
@ -99,15 +100,21 @@ impl NixCommand {
cmd.stdout(Stdio::inherit()); cmd.stdout(Stdio::inherit());
cmd.stderr(Stdio::inherit()); cmd.stderr(Stdio::inherit());
cmd.stdin(Stdio::inherit()); cmd.stdin(Stdio::inherit());
return cmd.status(); return Ok(cmd.status()?);
} }
cmd.stdout(Stdio::piped()); cmd.stdout(Stdio::piped());
cmd.stderr(Stdio::piped()); cmd.stderr(Stdio::piped());
let mut child = cmd.spawn()?; let mut child = cmd.spawn()?;
let mut stdout = child.stdout.take().unwrap(); let child_stdout = child.stdout.take().ok_or_else(|| EhError::CommandFailed {
let mut stderr = child.stderr.take().unwrap(); command: format!("nix {}", self.subcommand),
})?;
let child_stderr = child.stderr.take().ok_or_else(|| EhError::CommandFailed {
command: format!("nix {}", self.subcommand),
})?;
let mut stdout = child_stdout;
let mut stderr = child_stderr;
let mut out_buf = [0u8; 4096]; let mut out_buf = [0u8; 4096];
let mut err_buf = [0u8; 4096]; let mut err_buf = [0u8; 4096];
@ -126,7 +133,7 @@ impl NixCommand {
did_something = true; did_something = true;
} }
Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => {} Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => {}
Err(e) => return Err(e), Err(e) => return Err(EhError::Io(e)),
} }
match stderr.read(&mut err_buf) { match stderr.read(&mut err_buf) {
@ -137,7 +144,7 @@ impl NixCommand {
did_something = true; did_something = true;
} }
Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => {} Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => {}
Err(e) => return Err(e), Err(e) => return Err(EhError::Io(e)),
} }
if !did_something && child.try_wait()?.is_some() { if !did_something && child.try_wait()?.is_some() {
@ -150,7 +157,7 @@ impl NixCommand {
} }
/// Run the command and capture all output. /// Run the command and capture all output.
pub fn output(&self) -> io::Result<Output> { pub fn output(&self) -> Result<Output> {
let mut cmd = Command::new("nix"); let mut cmd = Command::new("nix");
cmd.arg(&self.subcommand); cmd.arg(&self.subcommand);
@ -174,6 +181,6 @@ impl NixCommand {
cmd.stderr(Stdio::piped()); cmd.stderr(Stdio::piped());
} }
cmd.output() Ok(cmd.output()?)
} }
} }

44
eh/src/error.rs Normal file
View file

@ -0,0 +1,44 @@
use thiserror::Error;
#[derive(Error, Debug)]
pub enum EhError {
#[error("Nix command failed: {0}")]
NixCommandFailed(String),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Regex error: {0}")]
Regex(#[from] regex::Error),
#[error("UTF-8 conversion error: {0}")]
Utf8(#[from] std::string::FromUtf8Error),
#[error("Hash extraction failed")]
HashExtractionFailed,
#[error("No Nix files found")]
NoNixFilesFound,
#[error("Failed to fix hash in file: {path}")]
HashFixFailed { path: String },
#[error("Process exited with code: {code}")]
ProcessExit { code: i32 },
#[error("Command execution failed: {command}")]
CommandFailed { command: String },
}
pub type Result<T> = std::result::Result<T, EhError>;
impl EhError {
#[must_use] pub const fn exit_code(&self) -> i32 {
match self {
Self::ProcessExit { code } => *code,
Self::NixCommandFailed(_) => 1,
Self::CommandFailed { .. } => 1,
_ => 1,
}
}
}

View file

@ -1,10 +1,12 @@
pub mod build; pub mod build;
pub mod command; pub mod command;
pub mod error;
pub mod run; pub mod run;
pub mod shell; pub mod shell;
pub mod util; pub mod util;
pub use clap::{CommandFactory, Parser, Subcommand}; pub use clap::{CommandFactory, Parser, Subcommand};
pub use error::{EhError, Result};
#[derive(Parser)] #[derive(Parser)]
#[command(name = "eh")] #[command(name = "eh")]

View file

@ -1,9 +1,11 @@
use eh::{Cli, Command, CommandFactory, Parser}; use eh::{Cli, Command, CommandFactory, Parser};
use error::Result;
use std::env; use std::env;
use std::path::Path; use std::path::Path;
mod build; mod build;
mod command; mod command;
mod error;
mod run; mod run;
mod shell; mod shell;
mod util; mod util;
@ -17,6 +19,18 @@ fn main() {
.compact(); // use the `Compact` formatting style. .compact(); // use the `Compact` formatting style.
tracing_subscriber::fmt().event_format(format).init(); tracing_subscriber::fmt().event_format(format).init();
let result = run_app();
match result {
Ok(code) => std::process::exit(code),
Err(e) => {
eprintln!("Error: {e}");
std::process::exit(e.exit_code());
}
}
}
fn run_app() -> Result<i32> {
let mut args = env::args(); let mut args = env::args();
let bin = args.next().unwrap_or_else(|| "eh".to_string()); let bin = args.next().unwrap_or_else(|| "eh".to_string());
let app_name = Path::new(&bin) let app_name = Path::new(&bin)
@ -31,8 +45,7 @@ fn main() {
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;
run::handle_nix_run(&rest, &hash_extractor, &fixer, &classifier); return run::handle_nix_run(&rest, &hash_extractor, &fixer, &classifier);
return;
} }
"ns" => { "ns" => {
@ -40,8 +53,7 @@ fn main() {
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;
shell::handle_nix_shell(&rest, &hash_extractor, &fixer, &classifier); return shell::handle_nix_shell(&rest, &hash_extractor, &fixer, &classifier);
return;
} }
"nb" => { "nb" => {
@ -49,8 +61,7 @@ fn main() {
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;
build::handle_nix_build(&rest, &hash_extractor, &fixer, &classifier); return build::handle_nix_build(&rest, &hash_extractor, &fixer, &classifier);
return;
} }
_ => {} _ => {}
} }
@ -63,21 +74,21 @@ fn main() {
match cli.command { match cli.command {
Some(Command::Run { args }) => { Some(Command::Run { args }) => {
run::handle_nix_run(&args, &hash_extractor, &fixer, &classifier); run::handle_nix_run(&args, &hash_extractor, &fixer, &classifier)
} }
Some(Command::Shell { args }) => { Some(Command::Shell { args }) => {
shell::handle_nix_shell(&args, &hash_extractor, &fixer, &classifier); shell::handle_nix_shell(&args, &hash_extractor, &fixer, &classifier)
} }
Some(Command::Build { args }) => { Some(Command::Build { args }) => {
build::handle_nix_build(&args, &hash_extractor, &fixer, &classifier); build::handle_nix_build(&args, &hash_extractor, &fixer, &classifier)
} }
_ => { _ => {
Cli::command().print_help().unwrap(); Cli::command().print_help()?;
println!(); println!();
std::process::exit(0); Ok(0)
} }
} }
} }

View file

@ -1,3 +1,4 @@
use crate::error::Result;
use crate::util::{HashExtractor, NixErrorClassifier, NixFileFixer, handle_nix_with_retry}; use crate::util::{HashExtractor, NixErrorClassifier, NixFileFixer, handle_nix_with_retry};
pub fn handle_nix_run( pub fn handle_nix_run(
@ -5,6 +6,6 @@ pub fn handle_nix_run(
hash_extractor: &dyn HashExtractor, hash_extractor: &dyn HashExtractor,
fixer: &dyn NixFileFixer, fixer: &dyn NixFileFixer,
classifier: &dyn NixErrorClassifier, classifier: &dyn NixErrorClassifier,
) { ) -> Result<i32> {
handle_nix_with_retry("run", args, hash_extractor, fixer, classifier, true); handle_nix_with_retry("run", args, hash_extractor, fixer, classifier, true)
} }

View file

@ -1,3 +1,4 @@
use crate::error::Result;
use crate::util::{HashExtractor, NixErrorClassifier, NixFileFixer, handle_nix_with_retry}; use crate::util::{HashExtractor, NixErrorClassifier, NixFileFixer, handle_nix_with_retry};
pub fn handle_nix_shell( pub fn handle_nix_shell(
@ -5,6 +6,6 @@ pub fn handle_nix_shell(
hash_extractor: &dyn HashExtractor, hash_extractor: &dyn HashExtractor,
fixer: &dyn NixFileFixer, fixer: &dyn NixFileFixer,
classifier: &dyn NixErrorClassifier, classifier: &dyn NixErrorClassifier,
) { ) -> Result<i32> {
handle_nix_with_retry("shell", args, hash_extractor, fixer, classifier, true); handle_nix_with_retry("shell", args, hash_extractor, fixer, classifier, true)
} }

View file

@ -1,4 +1,5 @@
use crate::command::{NixCommand, StdIoInterceptor}; use crate::command::{NixCommand, StdIoInterceptor};
use crate::error::{EhError, Result};
use regex::Regex; use regex::Regex;
use std::fs; use std::fs;
use std::io::Write; use std::io::Write;
@ -20,61 +21,61 @@ impl HashExtractor for RegexHashExtractor {
r"have:\s+(sha256-[a-zA-Z0-9+/=]+)", r"have:\s+(sha256-[a-zA-Z0-9+/=]+)",
]; ];
for pattern in &patterns { for pattern in &patterns {
if let Ok(re) = Regex::new(pattern) { if let Ok(re) = Regex::new(pattern)
if let Some(captures) = re.captures(stderr) { && let Some(captures) = re.captures(stderr)
if 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());
} }
} }
}
}
None None
} }
} }
pub trait NixFileFixer { pub trait NixFileFixer {
fn fix_hash_in_files(&self, new_hash: &str) -> bool; fn fix_hash_in_files(&self, new_hash: &str) -> Result<bool>;
fn find_nix_files(&self) -> Vec<PathBuf>; fn find_nix_files(&self) -> Result<Vec<PathBuf>>;
fn fix_hash_in_file(&self, file_path: &Path, new_hash: &str) -> bool; fn fix_hash_in_file(&self, file_path: &Path, 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) -> bool { fn fix_hash_in_files(&self, 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, new_hash)? {
println!("Updated hash in {}", file_path.display()); println!("Updated hash in {}", file_path.display());
fixed = true; fixed = true;
} }
} }
fixed Ok(fixed)
} }
fn find_nix_files(&self) -> Vec<PathBuf> { fn find_nix_files(&self) -> Result<Vec<PathBuf>> {
let mut files = Vec::new(); let mut files = Vec::new();
let mut stack = vec![PathBuf::from(".")]; let mut stack = vec![PathBuf::from(".")];
while let Some(dir) = stack.pop() { while let Some(dir) = stack.pop() {
if let Ok(entries) = fs::read_dir(&dir) { let entries = fs::read_dir(&dir)?;
for entry in entries.flatten() { for entry in entries.flatten() {
let path = entry.path(); let path = entry.path();
if path.is_dir() { if path.is_dir() {
stack.push(path); stack.push(path);
} else if let Some(ext) = path.extension() { } else if let Some(ext) = path.extension()
if ext.eq_ignore_ascii_case("nix") { && ext.eq_ignore_ascii_case("nix") {
files.push(path); files.push(path);
} }
} }
} }
if files.is_empty() {
Err(EhError::NoNixFilesFound)
} else {
Ok(files)
} }
} }
files
}
fn fix_hash_in_file(&self, file_path: &Path, new_hash: &str) -> bool { fn fix_hash_in_file(&self, file_path: &Path, new_hash: &str) -> Result<bool> {
if let Ok(content) = fs::read_to_string(file_path) { let content = fs::read_to_string(file_path)?;
let patterns = [ let patterns = [
(r#"hash\s*=\s*"[^"]*""#, format!(r#"hash = "{new_hash}""#)), (r#"hash\s*=\s*"[^"]*""#, format!(r#"hash = "{new_hash}""#)),
( (
@ -86,10 +87,10 @@ impl NixFileFixer for DefaultNixFileFixer {
format!(r#"outputHash = "{new_hash}""#), format!(r#"outputHash = "{new_hash}""#),
), ),
]; ];
let mut new_content = content.clone(); let mut new_content = content;
let mut replaced = false; let mut replaced = false;
for (pattern, replacement) in &patterns { for (pattern, replacement) in &patterns {
if let Ok(re) = Regex::new(pattern) { let re = Regex::new(pattern)?;
if re.is_match(&new_content) { if re.is_match(&new_content) {
new_content = re new_content = re
.replace_all(&new_content, replacement.as_str()) .replace_all(&new_content, replacement.as_str())
@ -97,12 +98,15 @@ impl NixFileFixer for DefaultNixFileFixer {
replaced = true; replaced = true;
} }
} }
if replaced {
fs::write(file_path, new_content)
.map_err(|_| EhError::HashFixFailed {
path: file_path.to_string_lossy().to_string()
})?;
Ok(true)
} else {
Ok(false)
} }
if replaced && fs::write(file_path, new_content).is_ok() {
return true;
}
}
false
} }
} }
@ -111,24 +115,21 @@ pub trait NixErrorClassifier {
} }
/// Pre-evaluate expression to catch errors early /// Pre-evaluate expression to catch errors early
fn pre_evaluate(_subcommand: &str, args: &[String]) -> bool { fn pre_evaluate(_subcommand: &str, args: &[String]) -> Result<bool> {
// 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 true; // No expression to evaluate return Ok(true); // No expression to evaluate
}; };
let eval_cmd = NixCommand::new("eval").arg(eval_arg).arg("--raw"); let eval_cmd = NixCommand::new("eval").arg(eval_arg).arg("--raw");
let output = match eval_cmd.output() { let output = eval_cmd.output()?;
Ok(output) => output,
Err(_) => return false,
};
if output.status.success() { if output.status.success() {
return true; return Ok(true);
} }
let stderr = String::from_utf8_lossy(&output.stderr); let stderr = String::from_utf8_lossy(&output.stderr);
@ -140,11 +141,11 @@ fn pre_evaluate(_subcommand: &str, args: &[String]) -> bool {
|| stderr.contains("has been marked as insecure") || stderr.contains("has been marked as insecure")
|| stderr.contains("has been marked as broken") || stderr.contains("has been marked as broken")
{ {
return true; return Ok(true);
} }
// For other eval failures, fail early // For other eval failures, fail early
false Ok(false)
} }
/// Shared retry logic for nix commands (build/run/shell). /// Shared retry logic for nix commands (build/run/shell).
@ -155,11 +156,12 @@ pub fn handle_nix_with_retry(
fixer: &dyn NixFileFixer, fixer: &dyn NixFileFixer,
classifier: &dyn NixErrorClassifier, classifier: &dyn NixErrorClassifier,
interactive: bool, interactive: bool,
) -> ! { ) -> Result<i32> {
// Pre-evaluate for build commands to catch errors early // Pre-evaluate for build commands to catch errors early
if !pre_evaluate(subcommand, args) { if !pre_evaluate(subcommand, args)? {
eprintln!("Error: Expression evaluation failed"); return Err(EhError::NixCommandFailed(
std::process::exit(1); "Expression evaluation failed".to_string(),
));
} }
// For run commands, try interactive first to avoid breaking terminal // For run commands, try interactive first to avoid breaking terminal
@ -170,11 +172,9 @@ pub fn handle_nix_with_retry(
for arg in args { for arg in args {
cmd = cmd.arg(arg); cmd = cmd.arg(arg);
} }
let status = cmd let status = cmd.run_with_logs(StdIoInterceptor)?;
.run_with_logs(StdIoInterceptor)
.expect("failed to run nix command");
if status.success() { if status.success() {
std::process::exit(0); return Ok(0);
} }
} }
@ -182,12 +182,13 @@ pub fn handle_nix_with_retry(
let output_cmd = NixCommand::new(subcommand) let output_cmd = NixCommand::new(subcommand)
.print_build_logs(true) .print_build_logs(true)
.args(args.iter().cloned()); .args(args.iter().cloned());
let output = output_cmd.output().expect("failed to capture 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 if we need to retry with special flags
if let Some(new_hash) = hash_extractor.extract_hash(&stderr) { if let Some(new_hash) = hash_extractor.extract_hash(&stderr) {
if fixer.fix_hash_in_files(&new_hash) { match fixer.fix_hash_in_files(&new_hash) {
Ok(true) => {
info!("{}", Paint::green("✔ Fixed hash mismatch, retrying...")); info!("{}", Paint::green("✔ Fixed hash mismatch, retrying..."));
let mut retry_cmd = NixCommand::new(subcommand) let mut retry_cmd = NixCommand::new(subcommand)
.print_build_logs(true) .print_build_logs(true)
@ -195,9 +196,23 @@ pub fn handle_nix_with_retry(
if interactive { if interactive {
retry_cmd = retry_cmd.interactive(true); retry_cmd = retry_cmd.interactive(true);
} }
let retry_status = retry_cmd.run_with_logs(StdIoInterceptor).unwrap(); let retry_status = retry_cmd.run_with_logs(StdIoInterceptor)?;
std::process::exit(retry_status.code().unwrap_or(1)); return Ok(retry_status.code().unwrap_or(1));
} }
Ok(false) => {
// No files were fixed, continue with normal error handling
}
Err(EhError::NoNixFilesFound) => {
warn!("No .nix files found to fix hash in");
// Continue with normal error handling
}
Err(e) => {
return Err(e);
}
}
} else if stderr.contains("hash") || stderr.contains("sha256") {
// If there's a hash-related error but we couldn't extract it, that's a failure
return Err(EhError::HashExtractionFailed);
} }
if classifier.should_retry(&stderr) { if classifier.should_retry(&stderr) {
@ -214,8 +229,8 @@ pub fn handle_nix_with_retry(
if interactive { if interactive {
retry_cmd = retry_cmd.interactive(true); retry_cmd = retry_cmd.interactive(true);
} }
let retry_status = retry_cmd.run_with_logs(StdIoInterceptor).unwrap(); let retry_status = retry_cmd.run_with_logs(StdIoInterceptor)?;
std::process::exit(retry_status.code().unwrap_or(1)); return Ok(retry_status.code().unwrap_or(1));
} }
if stderr.contains("has been marked as insecure") && stderr.contains("refusing") { if stderr.contains("has been marked as insecure") && stderr.contains("refusing") {
warn!( warn!(
@ -232,8 +247,8 @@ pub fn handle_nix_with_retry(
if interactive { if interactive {
retry_cmd = retry_cmd.interactive(true); retry_cmd = retry_cmd.interactive(true);
} }
let retry_status = retry_cmd.run_with_logs(StdIoInterceptor).unwrap(); let retry_status = retry_cmd.run_with_logs(StdIoInterceptor)?;
std::process::exit(retry_status.code().unwrap_or(1)); return Ok(retry_status.code().unwrap_or(1));
} }
if stderr.contains("has been marked as broken") && stderr.contains("refusing") { if stderr.contains("has been marked as broken") && stderr.contains("refusing") {
warn!( warn!(
@ -248,19 +263,21 @@ pub fn handle_nix_with_retry(
if interactive { if interactive {
retry_cmd = retry_cmd.interactive(true); retry_cmd = retry_cmd.interactive(true);
} }
let retry_status = retry_cmd.run_with_logs(StdIoInterceptor).unwrap(); let retry_status = retry_cmd.run_with_logs(StdIoInterceptor)?;
std::process::exit(retry_status.code().unwrap_or(1)); return Ok(retry_status.code().unwrap_or(1));
} }
} }
// If the first attempt succeeded, we're done // If the first attempt succeeded, we're done
if output.status.success() { if output.status.success() {
std::process::exit(0); return Ok(0);
} }
// Otherwise, show the error and exit // Otherwise, show the error and return error
std::io::stderr().write_all(output.stderr.as_ref()).unwrap(); std::io::stderr().write_all(&output.stderr)?;
std::process::exit(output.status.code().unwrap_or(1)); Err(EhError::ProcessExit {
code: output.status.code().unwrap_or(1),
})
} }
pub struct DefaultNixErrorClassifier; pub struct DefaultNixErrorClassifier;

View file

@ -44,7 +44,7 @@ enum Binary {
} }
impl Binary { impl Binary {
fn name(self) -> &'static str { const fn name(self) -> &'static str {
match self { match self {
Self::Nr => "nr", Self::Nr => "nr",
Self::Ns => "ns", Self::Ns => "ns",
@ -135,14 +135,14 @@ fn create_multicall_binaries(
} }
fn generate_completions(shell: Shell, output_dir: &Path) -> Result<(), Box<dyn error::Error>> { fn generate_completions(shell: Shell, output_dir: &Path) -> Result<(), Box<dyn error::Error>> {
println!("generating {} completions...", shell); println!("generating {shell} completions...");
fs::create_dir_all(output_dir)?; fs::create_dir_all(output_dir)?;
let mut cmd = eh::Cli::command(); let mut cmd = eh::Cli::command();
let bin_name = "eh"; let bin_name = "eh";
let completion_file = output_dir.join(format!("{}.{}", bin_name, shell)); let completion_file = output_dir.join(format!("{bin_name}.{shell}"));
let mut file = fs::File::create(&completion_file)?; let mut file = fs::File::create(&completion_file)?;
generate(shell, &mut cmd, bin_name, &mut file); generate(shell, &mut cmd, bin_name, &mut file);
@ -152,7 +152,7 @@ fn generate_completions(shell: Shell, output_dir: &Path) -> Result<(), Box<dyn e
// Create symlinks for multicall binaries // Create symlinks for multicall binaries
let multicall_names = ["nb", "nr", "ns"]; let multicall_names = ["nb", "nr", "ns"];
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() {
fs::remove_file(&symlink_path)?; fs::remove_file(&symlink_path)?;
} }