diff --git a/Cargo.lock b/Cargo.lock index 562e139..5893630 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -25,9 +25,9 @@ checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" [[package]] name = "clap" -version = "4.5.41" +version = "4.5.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be92d32e80243a54711e5d7ce823c35c41c9d929dc4ab58e1276f625841aadf9" +checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5" dependencies = [ "clap_builder", "clap_derive", @@ -35,9 +35,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.41" +version = "4.5.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707eab41e9622f9139419d573eca0900137718000c517d47da73045f54331c3d" +checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a" dependencies = [ "anstyle", "clap_lex", @@ -45,18 +45,18 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.5.55" +version = "4.5.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5abde44486daf70c5be8b8f8f1b66c49f86236edf6fa2abadb4d961c4c6229a" +checksum = "8e602857739c5a4291dfa33b5a298aeac9006185229a700e5810a3ef7272d971" dependencies = [ "clap", ] [[package]] name = "clap_derive" -version = "4.5.41" +version = "4.5.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef4f52386a59ca4c860f7393bcf8abd8dfd91ecccc0f774635ff68e92eeef491" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" dependencies = [ "heck", "proc-macro2", @@ -72,10 +72,11 @@ checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" [[package]] name = "eh" -version = "0.1.1" +version = "0.1.2" dependencies = [ "clap", "regex", + "thiserror", "tracing", "tracing-subscriber", "yansi", @@ -107,12 +108,11 @@ checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" [[package]] name = "nu-ansi-term" -version = "0.46.0" +version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "overload", - "winapi", + "windows-sys", ] [[package]] @@ -121,12 +121,6 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" - [[package]] name = "pin-project-lite" version = "0.2.16" @@ -153,9 +147,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.11.1" +version = "1.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" dependencies = [ "aho-corasick", "memchr", @@ -165,9 +159,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.9" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" dependencies = [ "aho-corasick", "memchr", @@ -206,6 +200,26 @@ dependencies = [ "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]] name = "thread_local" version = "1.1.9" @@ -260,9 +274,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.19" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" dependencies = [ "nu-ansi-term", "sharded-slab", @@ -285,30 +299,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] -name = "winapi" -version = "0.3.9" +name = "windows-link" +version = "0.2.1" 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 = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", + "windows-link", ] -[[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]] name = "xtask" -version = "0.1.1" +version = "0.1.2" dependencies = [ "clap", "clap_complete", diff --git a/Cargo.toml b/Cargo.toml index 38fd81f..699f488 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,16 +8,17 @@ description = "Ergonomic Nix CLI helper" edition = "2024" license = "MPL-2.0" readme = true -rust-version = "1.85" -version = "0.1.1" +rust-version = "1.89" +version = "0.1.2" [workspace.dependencies] -clap = { default-features = false, features = [ "std", "help", "derive" ], version = "4.5" } -clap_complete = "4.5" -regex = "1.11" -tracing = "0.1" -tracing-subscriber = "0.3" -yansi = "1.0" +clap = { default-features = false, features = [ "std", "help", "derive" ], version = "4.5.51" } +clap_complete = "4.5.60" +regex = "1.12.2" +thiserror = "2.0.17" +tracing = "0.1.41" +tracing-subscriber = "0.3.20" +yansi = "1.0.1" [profile.release] codegen-units = 1 diff --git a/eh/Cargo.toml b/eh/Cargo.toml index 8da1e92..bda9168 100644 --- a/eh/Cargo.toml +++ b/eh/Cargo.toml @@ -13,6 +13,7 @@ crate-type = ["lib"] [dependencies] clap.workspace = true regex.workspace = true +thiserror.workspace = true tracing.workspace = true tracing-subscriber.workspace = true yansi.workspace = true diff --git a/eh/src/build.rs b/eh/src/build.rs index 137b111..7f5ac0d 100644 --- a/eh/src/build.rs +++ b/eh/src/build.rs @@ -1,3 +1,4 @@ +use crate::error::Result; use crate::util::{HashExtractor, NixErrorClassifier, NixFileFixer, handle_nix_with_retry}; pub fn handle_nix_build( @@ -5,6 +6,6 @@ pub fn handle_nix_build( hash_extractor: &dyn HashExtractor, fixer: &dyn NixFileFixer, classifier: &dyn NixErrorClassifier, -) { - handle_nix_with_retry("build", args, hash_extractor, fixer, classifier, false); +) -> Result { + handle_nix_with_retry("build", args, hash_extractor, fixer, classifier, false) } diff --git a/eh/src/command.rs b/eh/src/command.rs index 75e6970..2f1fb8d 100644 --- a/eh/src/command.rs +++ b/eh/src/command.rs @@ -1,3 +1,4 @@ +use crate::error::{EhError, Result}; use std::collections::VecDeque; use std::io::{self, Read, Write}; use std::process::{Command, ExitStatus, Output, Stdio}; @@ -61,17 +62,17 @@ impl NixCommand { self } - pub fn impure(mut self, yes: bool) -> Self { + #[must_use] pub const fn impure(mut self, yes: bool) -> Self { self.impure = yes; self } - pub fn interactive(mut self, yes: bool) -> Self { + #[must_use] pub const fn interactive(mut self, yes: bool) -> Self { self.interactive = yes; 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 } @@ -80,7 +81,7 @@ impl NixCommand { pub fn run_with_logs( &self, mut interceptor: I, - ) -> io::Result { + ) -> Result { let mut cmd = Command::new("nix"); cmd.arg(&self.subcommand); @@ -99,15 +100,21 @@ impl NixCommand { cmd.stdout(Stdio::inherit()); cmd.stderr(Stdio::inherit()); cmd.stdin(Stdio::inherit()); - return cmd.status(); + return Ok(cmd.status()?); } cmd.stdout(Stdio::piped()); cmd.stderr(Stdio::piped()); let mut child = cmd.spawn()?; - let mut stdout = child.stdout.take().unwrap(); - let mut stderr = child.stderr.take().unwrap(); + let child_stdout = child.stdout.take().ok_or_else(|| EhError::CommandFailed { + 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 err_buf = [0u8; 4096]; @@ -126,7 +133,7 @@ impl NixCommand { did_something = true; } 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) { @@ -137,7 +144,7 @@ impl NixCommand { did_something = true; } 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() { @@ -150,7 +157,7 @@ impl NixCommand { } /// Run the command and capture all output. - pub fn output(&self) -> io::Result { + pub fn output(&self) -> Result { let mut cmd = Command::new("nix"); cmd.arg(&self.subcommand); @@ -174,6 +181,6 @@ impl NixCommand { cmd.stderr(Stdio::piped()); } - cmd.output() + Ok(cmd.output()?) } } diff --git a/eh/src/error.rs b/eh/src/error.rs new file mode 100644 index 0000000..5cb90fb --- /dev/null +++ b/eh/src/error.rs @@ -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 = std::result::Result; + +impl EhError { + #[must_use] pub const fn exit_code(&self) -> i32 { + match self { + Self::ProcessExit { code } => *code, + Self::NixCommandFailed(_) => 1, + Self::CommandFailed { .. } => 1, + _ => 1, + } + } +} diff --git a/eh/src/lib.rs b/eh/src/lib.rs index df062fa..83c467f 100644 --- a/eh/src/lib.rs +++ b/eh/src/lib.rs @@ -1,10 +1,12 @@ pub mod build; pub mod command; +pub mod error; pub mod run; pub mod shell; pub mod util; pub use clap::{CommandFactory, Parser, Subcommand}; +pub use error::{EhError, Result}; #[derive(Parser)] #[command(name = "eh")] diff --git a/eh/src/main.rs b/eh/src/main.rs index 4aab85f..e5363b0 100644 --- a/eh/src/main.rs +++ b/eh/src/main.rs @@ -1,9 +1,11 @@ use eh::{Cli, Command, CommandFactory, Parser}; +use error::Result; use std::env; use std::path::Path; mod build; mod command; +mod error; mod run; mod shell; mod util; @@ -17,6 +19,18 @@ fn main() { .compact(); // use the `Compact` formatting style. tracing_subscriber::fmt().event_format(format).init(); + let result = run_app(); + + match result { + Ok(code) => std::process::exit(code), + Err(e) => { + eprintln!("Error: {e}"); + std::process::exit(e.exit_code()); + } + } +} + +fn run_app() -> Result { let mut args = env::args(); let bin = args.next().unwrap_or_else(|| "eh".to_string()); let app_name = Path::new(&bin) @@ -31,8 +45,7 @@ fn main() { let hash_extractor = util::RegexHashExtractor; let fixer = util::DefaultNixFileFixer; let classifier = util::DefaultNixErrorClassifier; - run::handle_nix_run(&rest, &hash_extractor, &fixer, &classifier); - return; + return run::handle_nix_run(&rest, &hash_extractor, &fixer, &classifier); } "ns" => { @@ -40,8 +53,7 @@ fn main() { let hash_extractor = util::RegexHashExtractor; let fixer = util::DefaultNixFileFixer; let classifier = util::DefaultNixErrorClassifier; - shell::handle_nix_shell(&rest, &hash_extractor, &fixer, &classifier); - return; + return shell::handle_nix_shell(&rest, &hash_extractor, &fixer, &classifier); } "nb" => { @@ -49,8 +61,7 @@ fn main() { let hash_extractor = util::RegexHashExtractor; let fixer = util::DefaultNixFileFixer; let classifier = util::DefaultNixErrorClassifier; - build::handle_nix_build(&rest, &hash_extractor, &fixer, &classifier); - return; + return build::handle_nix_build(&rest, &hash_extractor, &fixer, &classifier); } _ => {} } @@ -63,21 +74,21 @@ fn main() { match cli.command { 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 }) => { - shell::handle_nix_shell(&args, &hash_extractor, &fixer, &classifier); + shell::handle_nix_shell(&args, &hash_extractor, &fixer, &classifier) } 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!(); - std::process::exit(0); + Ok(0) } } } diff --git a/eh/src/run.rs b/eh/src/run.rs index 0fa322c..11a5a12 100644 --- a/eh/src/run.rs +++ b/eh/src/run.rs @@ -1,3 +1,4 @@ +use crate::error::Result; use crate::util::{HashExtractor, NixErrorClassifier, NixFileFixer, handle_nix_with_retry}; pub fn handle_nix_run( @@ -5,6 +6,6 @@ pub fn handle_nix_run( hash_extractor: &dyn HashExtractor, fixer: &dyn NixFileFixer, classifier: &dyn NixErrorClassifier, -) { - handle_nix_with_retry("run", args, hash_extractor, fixer, classifier, true); +) -> Result { + handle_nix_with_retry("run", args, hash_extractor, fixer, classifier, true) } diff --git a/eh/src/shell.rs b/eh/src/shell.rs index 523049b..9458b08 100644 --- a/eh/src/shell.rs +++ b/eh/src/shell.rs @@ -1,3 +1,4 @@ +use crate::error::Result; use crate::util::{HashExtractor, NixErrorClassifier, NixFileFixer, handle_nix_with_retry}; pub fn handle_nix_shell( @@ -5,6 +6,6 @@ pub fn handle_nix_shell( hash_extractor: &dyn HashExtractor, fixer: &dyn NixFileFixer, classifier: &dyn NixErrorClassifier, -) { - handle_nix_with_retry("shell", args, hash_extractor, fixer, classifier, true); +) -> Result { + handle_nix_with_retry("shell", args, hash_extractor, fixer, classifier, true) } diff --git a/eh/src/util.rs b/eh/src/util.rs index 46dbb38..e93e417 100644 --- a/eh/src/util.rs +++ b/eh/src/util.rs @@ -1,4 +1,5 @@ use crate::command::{NixCommand, StdIoInterceptor}; +use crate::error::{EhError, Result}; use regex::Regex; use std::fs; use std::io::Write; @@ -20,89 +21,92 @@ impl HashExtractor for RegexHashExtractor { r"have:\s+(sha256-[a-zA-Z0-9+/=]+)", ]; for pattern in &patterns { - if let Ok(re) = Regex::new(pattern) { - if let Some(captures) = re.captures(stderr) { - if let Some(hash) = captures.get(1) { + if let Ok(re) = Regex::new(pattern) + && let Some(captures) = re.captures(stderr) + && let Some(hash) = captures.get(1) { return Some(hash.as_str().to_string()); } - } - } } None } } pub trait NixFileFixer { - fn fix_hash_in_files(&self, new_hash: &str) -> bool; - fn find_nix_files(&self) -> Vec; - fn fix_hash_in_file(&self, file_path: &Path, new_hash: &str) -> bool; + fn fix_hash_in_files(&self, new_hash: &str) -> Result; + fn find_nix_files(&self) -> Result>; + fn fix_hash_in_file(&self, file_path: &Path, new_hash: &str) -> Result; } pub struct DefaultNixFileFixer; impl NixFileFixer for DefaultNixFileFixer { - fn fix_hash_in_files(&self, new_hash: &str) -> bool { - let nix_files = self.find_nix_files(); + fn fix_hash_in_files(&self, new_hash: &str) -> Result { + let nix_files = self.find_nix_files()?; let mut fixed = false; for file_path in nix_files { - if self.fix_hash_in_file(&file_path, new_hash) { + if self.fix_hash_in_file(&file_path, new_hash)? { println!("Updated hash in {}", file_path.display()); fixed = true; } } - fixed + Ok(fixed) } - fn find_nix_files(&self) -> Vec { + fn find_nix_files(&self) -> Result> { let mut files = Vec::new(); let mut stack = vec![PathBuf::from(".")]; while let Some(dir) = stack.pop() { - if let Ok(entries) = fs::read_dir(&dir) { - for entry in entries.flatten() { - let path = entry.path(); - if path.is_dir() { - stack.push(path); - } else if let Some(ext) = path.extension() { - if ext.eq_ignore_ascii_case("nix") { - files.push(path); - } + let entries = fs::read_dir(&dir)?; + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() { + stack.push(path); + } else if let Some(ext) = path.extension() + && ext.eq_ignore_ascii_case("nix") { + files.push(path); } - } } } - files + if files.is_empty() { + Err(EhError::NoNixFilesFound) + } else { + Ok(files) + } } - fn fix_hash_in_file(&self, file_path: &Path, new_hash: &str) -> bool { - if let Ok(content) = fs::read_to_string(file_path) { - let patterns = [ - (r#"hash\s*=\s*"[^"]*""#, format!(r#"hash = "{new_hash}""#)), - ( - r#"sha256\s*=\s*"[^"]*""#, - format!(r#"sha256 = "{new_hash}""#), - ), - ( - r#"outputHash\s*=\s*"[^"]*""#, - format!(r#"outputHash = "{new_hash}""#), - ), - ]; - let mut new_content = content.clone(); - let mut replaced = false; - for (pattern, replacement) in &patterns { - if let Ok(re) = Regex::new(pattern) { - if re.is_match(&new_content) { - new_content = re - .replace_all(&new_content, replacement.as_str()) - .into_owned(); - replaced = true; - } - } - } - if replaced && fs::write(file_path, new_content).is_ok() { - return true; + fn fix_hash_in_file(&self, file_path: &Path, new_hash: &str) -> Result { + let content = fs::read_to_string(file_path)?; + let patterns = [ + (r#"hash\s*=\s*"[^"]*""#, format!(r#"hash = "{new_hash}""#)), + ( + r#"sha256\s*=\s*"[^"]*""#, + format!(r#"sha256 = "{new_hash}""#), + ), + ( + r#"outputHash\s*=\s*"[^"]*""#, + format!(r#"outputHash = "{new_hash}""#), + ), + ]; + let mut new_content = content; + let mut replaced = false; + for (pattern, replacement) in &patterns { + let re = Regex::new(pattern)?; + if re.is_match(&new_content) { + new_content = re + .replace_all(&new_content, replacement.as_str()) + .into_owned(); + replaced = true; } } - false + 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) + } } } @@ -111,24 +115,21 @@ pub trait NixErrorClassifier { } /// Pre-evaluate expression to catch errors early -fn pre_evaluate(_subcommand: &str, args: &[String]) -> bool { +fn pre_evaluate(_subcommand: &str, args: &[String]) -> Result { // Find flake references or expressions to evaluate // Only take the first non-flag argument (the package/expression) let eval_arg = args.iter().find(|arg| !arg.starts_with('-')); let Some(eval_arg) = eval_arg else { - return true; // No expression to evaluate + return Ok(true); // No expression to evaluate }; let eval_cmd = NixCommand::new("eval").arg(eval_arg).arg("--raw"); - let output = match eval_cmd.output() { - Ok(output) => output, - Err(_) => return false, - }; + let output = eval_cmd.output()?; if output.status.success() { - return true; + return Ok(true); } 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 broken") { - return true; + return Ok(true); } // For other eval failures, fail early - false + Ok(false) } /// Shared retry logic for nix commands (build/run/shell). @@ -155,11 +156,12 @@ pub fn handle_nix_with_retry( fixer: &dyn NixFileFixer, classifier: &dyn NixErrorClassifier, interactive: bool, -) -> ! { +) -> Result { // Pre-evaluate for build commands to catch errors early - if !pre_evaluate(subcommand, args) { - eprintln!("Error: Expression evaluation failed"); - std::process::exit(1); + if !pre_evaluate(subcommand, args)? { + return Err(EhError::NixCommandFailed( + "Expression evaluation failed".to_string(), + )); } // For run commands, try interactive first to avoid breaking terminal @@ -170,11 +172,9 @@ pub fn handle_nix_with_retry( for arg in args { cmd = cmd.arg(arg); } - let status = cmd - .run_with_logs(StdIoInterceptor) - .expect("failed to run nix command"); + let status = cmd.run_with_logs(StdIoInterceptor)?; if status.success() { - std::process::exit(0); + return Ok(0); } } @@ -182,22 +182,37 @@ pub fn handle_nix_with_retry( let output_cmd = NixCommand::new(subcommand) .print_build_logs(true) .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); // Check if we need to retry with special flags if let Some(new_hash) = hash_extractor.extract_hash(&stderr) { - if fixer.fix_hash_in_files(&new_hash) { - info!("{}", Paint::green("✔ Fixed hash mismatch, retrying...")); - let mut retry_cmd = NixCommand::new(subcommand) - .print_build_logs(true) - .args(args.iter().cloned()); - if interactive { - retry_cmd = retry_cmd.interactive(true); + match fixer.fix_hash_in_files(&new_hash) { + Ok(true) => { + info!("{}", Paint::green("✔ Fixed hash mismatch, retrying...")); + let mut retry_cmd = NixCommand::new(subcommand) + .print_build_logs(true) + .args(args.iter().cloned()); + 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)); + } + 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); } - let retry_status = retry_cmd.run_with_logs(StdIoInterceptor).unwrap(); - std::process::exit(retry_status.code().unwrap_or(1)); } + } 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) { @@ -214,8 +229,8 @@ pub fn handle_nix_with_retry( if interactive { retry_cmd = retry_cmd.interactive(true); } - let retry_status = retry_cmd.run_with_logs(StdIoInterceptor).unwrap(); - std::process::exit(retry_status.code().unwrap_or(1)); + 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!( @@ -232,8 +247,8 @@ pub fn handle_nix_with_retry( if interactive { retry_cmd = retry_cmd.interactive(true); } - let retry_status = retry_cmd.run_with_logs(StdIoInterceptor).unwrap(); - std::process::exit(retry_status.code().unwrap_or(1)); + 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!( @@ -248,19 +263,21 @@ pub fn handle_nix_with_retry( if interactive { retry_cmd = retry_cmd.interactive(true); } - let retry_status = retry_cmd.run_with_logs(StdIoInterceptor).unwrap(); - std::process::exit(retry_status.code().unwrap_or(1)); + let retry_status = retry_cmd.run_with_logs(StdIoInterceptor)?; + return Ok(retry_status.code().unwrap_or(1)); } } // If the first attempt succeeded, we're done if output.status.success() { - std::process::exit(0); + return Ok(0); } - // Otherwise, show the error and exit - std::io::stderr().write_all(output.stderr.as_ref()).unwrap(); - std::process::exit(output.status.code().unwrap_or(1)); + // Otherwise, show the error and return error + std::io::stderr().write_all(&output.stderr)?; + Err(EhError::ProcessExit { + code: output.status.code().unwrap_or(1), + }) } pub struct DefaultNixErrorClassifier; diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 4d92b6a..b747f05 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -44,7 +44,7 @@ enum Binary { } impl Binary { - fn name(self) -> &'static str { + const fn name(self) -> &'static str { match self { Self::Nr => "nr", Self::Ns => "ns", @@ -135,14 +135,14 @@ fn create_multicall_binaries( } fn generate_completions(shell: Shell, output_dir: &Path) -> Result<(), Box> { - println!("generating {} completions...", shell); + println!("generating {shell} completions..."); fs::create_dir_all(output_dir)?; let mut cmd = eh::Cli::command(); 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)?; generate(shell, &mut cmd, bin_name, &mut file); @@ -152,7 +152,7 @@ fn generate_completions(shell: Shell, output_dir: &Path) -> Result<(), Box