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]]
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",

View file

@ -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

View file

@ -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

View file

@ -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<i32> {
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::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<I: LogInterceptor + 'static>(
&self,
mut interceptor: I,
) -> io::Result<ExitStatus> {
) -> Result<ExitStatus> {
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<Output> {
pub fn output(&self) -> Result<Output> {
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()?)
}
}

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 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")]

View file

@ -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<i32> {
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)
}
}
}

View file

@ -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<i32> {
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};
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<i32> {
handle_nix_with_retry("shell", args, hash_extractor, fixer, classifier, true)
}

View file

@ -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<PathBuf>;
fn fix_hash_in_file(&self, file_path: &Path, new_hash: &str) -> bool;
fn fix_hash_in_files(&self, new_hash: &str) -> Result<bool>;
fn find_nix_files(&self) -> Result<Vec<PathBuf>>;
fn fix_hash_in_file(&self, file_path: &Path, new_hash: &str) -> Result<bool>;
}
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<bool> {
let nix_files = self.find_nix_files()?;
let mut fixed = false;
for file_path in nix_files {
if self.fix_hash_in_file(&file_path, 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<PathBuf> {
fn find_nix_files(&self) -> Result<Vec<PathBuf>> {
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<bool> {
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<bool> {
// Find flake references or expressions to evaluate
// Only take the first non-flag argument (the package/expression)
let eval_arg = args.iter().find(|arg| !arg.starts_with('-'));
let Some(eval_arg) = eval_arg else {
return 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<i32> {
// 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;

View file

@ -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<dyn error::Error>> {
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<dyn e
// Create symlinks for multicall binaries
let multicall_names = ["nb", "nr", "ns"];
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() {
fs::remove_file(&symlink_path)?;
}