diff --git a/Cargo.lock b/Cargo.lock index d68884d..2beabba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -81,6 +81,7 @@ name = "eh" version = "0.1.3" dependencies = [ "clap", + "eh-log", "regex", "tempfile", "thiserror", @@ -88,6 +89,13 @@ dependencies = [ "yansi", ] +[[package]] +name = "eh-log" +version = "0.1.3" +dependencies = [ + "yansi", +] + [[package]] name = "errno" version = "0.3.14" diff --git a/Cargo.toml b/Cargo.toml index 59825fb..7b3f31b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,8 @@ thiserror = "2.0.17" walkdir = "2.5.0" yansi = "1.0.1" -eh = { path = "./eh" } +eh = { path = "./eh" } +eh-log = { path = "./crates/eh-log" } [profile.release] codegen-units = 1 diff --git a/crates/eh-log/Cargo.toml b/crates/eh-log/Cargo.toml new file mode 100644 index 0000000..e3ef1f4 --- /dev/null +++ b/crates/eh-log/Cargo.toml @@ -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 diff --git a/crates/eh-log/src/lib.rs b/crates/eh-log/src/lib.rs new file mode 100644 index 0000000..365deff --- /dev/null +++ b/crates/eh-log/src/lib.rs @@ -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)*)) } } diff --git a/eh/Cargo.toml b/eh/Cargo.toml index f194a62..d221297 100644 --- a/eh/Cargo.toml +++ b/eh/Cargo.toml @@ -12,6 +12,7 @@ name = "eh" [dependencies] clap.workspace = true +eh-log.workspace = true regex.workspace = true tempfile.workspace = true thiserror.workspace = true diff --git a/eh/src/error.rs b/eh/src/error.rs index 403e2d1..909453a 100644 --- a/eh/src/error.rs +++ b/eh/src/error.rs @@ -4,46 +4,46 @@ use thiserror::Error; #[derive(Error, Debug)] pub enum EhError { - #[error("Nix command 'nix {command}' failed")] + #[error("nix {command} failed")] NixCommandFailed { command: String }, - #[error("IO error: {0}")] + #[error("io: {0}")] Io(#[from] std::io::Error), - #[error("Regex error: {0}")] + #[error("regex: {0}")] Regex(#[from] regex::Error), - #[error("UTF-8 conversion error: {0}")] + #[error("utf-8 conversion: {0}")] Utf8(#[from] std::string::FromUtf8Error), - #[error("Hash extraction failed: could not parse hash from nix output")] + #[error("could not extract hash from nix output")] HashExtractionFailed { stderr: String }, - #[error("No Nix files found in the current directory")] + #[error("no .nix files found in the current directory")] NoNixFilesFound, - #[error("Failed to fix hash in file: {path}")] + #[error("could not update hash in {path}")] HashFixFailed { path: String }, - #[error("Process exited with code: {code}")] + #[error("process exited with code {code}")] ProcessExit { code: i32 }, - #[error("Command execution failed: {command}")] + #[error("command '{command}' failed")] CommandFailed { command: String }, - #[error("Command '{command}' timed out after {} seconds", duration.as_secs())] + #[error("nix {command} timed out after {} seconds", duration.as_secs())] Timeout { command: String, duration: Duration, }, - #[error("Pre-evaluation of '{expression}' failed: {stderr}")] + #[error("'{expression}' failed to evaluate: {stderr}")] PreEvalFailed { expression: String, stderr: String, }, - #[error("Invalid input: {input} - {reason}")] + #[error("invalid input '{input}': {reason}")] InvalidInput { input: String, reason: String }, } @@ -72,31 +72,25 @@ impl EhError { pub fn hint(&self) -> Option<&str> { match self { Self::NixCommandFailed { .. } => { - Some("Run with --show-trace for more details from nix") + Some("run with --show-trace for more details") }, Self::PreEvalFailed { .. } => { - Some( - "Check that the expression exists in the flake and is spelled \ - correctly", - ) + Some("check that the expression exists and is spelled correctly") }, Self::HashExtractionFailed { .. } => { - Some( - "The nix output contained a hash mismatch but the hash could not be \ - parsed", - ) + 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") + 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 \ + "the command took too long; try a faster network or a smaller \ derivation", ) }, Self::InvalidInput { .. } => { - Some("Avoid shell metacharacters in nix arguments") + Some("avoid shell metacharacters in nix arguments") }, Self::Io(_) | Self::Regex(_) @@ -170,10 +164,7 @@ mod tests { command: "build".into(), duration: Duration::from_secs(300), }; - assert_eq!( - err.to_string(), - "Command 'build' timed out after 300 seconds" - ); + assert_eq!(err.to_string(), "nix build timed out after 300 seconds"); let err = EhError::PreEvalFailed { expression: "nixpkgs#hello".into(), @@ -185,7 +176,7 @@ mod tests { let err = EhError::HashExtractionFailed { stderr: "some output".into(), }; - assert!(err.to_string().contains("could not parse hash")); + assert!(err.to_string().contains("could not extract hash")); } #[test] diff --git a/eh/src/main.rs b/eh/src/main.rs index e090375..8cc4d11 100644 --- a/eh/src/main.rs +++ b/eh/src/main.rs @@ -2,6 +2,7 @@ use std::{env, path::Path}; use eh::{Cli, Command, CommandFactory, Parser}; use error::Result; +use yansi::Paint; mod build; mod command; @@ -16,9 +17,9 @@ fn main() { match result { Ok(code) => std::process::exit(code), Err(e) => { - eprintln!("Error: {e}"); + eh_log::log_error!("{e}"); if let Some(hint) = e.hint() { - eprintln!("Hint: {hint}"); + eh_log::log_hint!("{hint}"); } std::process::exit(e.exit_code()); }, @@ -41,14 +42,21 @@ fn dispatch_multicall( // Handle --help/-h/--version before forwarding to nix if rest.iter().any(|a| a == "--help" || a == "-h") { - eprintln!("{app_name}: shorthand for 'eh {subcommand}'"); - eprintln!("Usage: {app_name} [args...]"); - eprintln!("All arguments are forwarded to 'nix {subcommand}'."); + 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!("{app_name} (eh {})", env!("CARGO_PKG_VERSION")); + eprintln!("{} (eh {})", app_name.bold(), env!("CARGO_PKG_VERSION")); return Some(Ok(0)); } diff --git a/eh/src/util.rs b/eh/src/util.rs index 78671a9..10227de 100644 --- a/eh/src/util.rs +++ b/eh/src/util.rs @@ -4,6 +4,7 @@ use std::{ sync::LazyLock, }; +use eh_log::{log_info, log_warn}; use regex::Regex; use tempfile::NamedTempFile; use walkdir::WalkDir; @@ -92,7 +93,7 @@ impl NixFileFixer for DefaultNixFileFixer { let mut fixed = false; for file_path in nix_files { 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; } } @@ -251,12 +252,11 @@ fn package_name(args: &[String]) -> &str { /// Print a retry message with consistent formatting. /// Format: ` -> : , setting =1` fn print_retry_msg(pkg: &str, reason: &str, env_var: &str) { - eprintln!( - " {} {}: {}, setting {}", - "->".yellow().bold(), + log_warn!( + "{}: {}, setting {}", pkg.bold(), reason, - format!("{env_var}=1").bold(), + format!("{env_var}=1").bold() ); } @@ -316,14 +316,17 @@ fn pre_evaluate(args: &[String]) -> Result { return Ok(action); } - // For other eval failures, warn but let the actual command handle the - // error with full streaming output rather than halting here. - let err = EhError::PreEvalFailed { + // Non-retryable eval failure — fail fast with a clear message + // rather than running the full command and showing the same error again. + let stderr_clean = stderr + .trim() + .strip_prefix("error:") + .unwrap_or(stderr.trim()) + .trim(); + Err(EhError::PreEvalFailed { expression: eval_arg.clone(), - stderr: stderr.trim().to_string(), - }; - eprintln!(" {} {}", "->".yellow().bold(), err,); - Ok(RetryAction::None) + stderr: stderr_clean.to_string(), + }) } pub fn validate_nix_args(args: &[String]) -> Result<()> { @@ -398,10 +401,9 @@ pub fn handle_nix_with_retry( let old_hash = hash_extractor.extract_old_hash(&stderr); match fixer.fix_hash_in_files(old_hash.as_deref(), &new_hash) { Ok(true) => { - eprintln!( - " {} {}: hash mismatch corrected in local files, rebuilding", - "->".green().bold(), - pkg.bold(), + log_info!( + "{}: hash mismatch corrected in local files, rebuilding", + pkg.bold() ); let mut retry_cmd = NixCommand::new(subcommand) .print_build_logs(true) @@ -416,10 +418,9 @@ pub fn handle_nix_with_retry( // No files were fixed, continue with normal error handling }, Err(EhError::NoNixFilesFound) => { - eprintln!( - " {} {}: hash mismatch detected but no .nix files found to update", - "->".yellow().bold(), - pkg.bold(), + log_warn!( + "{}: hash mismatch detected but no .nix files found to update", + pkg.bold() ); // Continue with normal error handling },