diff --git a/eh/src/build.rs b/eh/src/build.rs index 39da022..b2019d4 100644 --- a/eh/src/build.rs +++ b/eh/src/build.rs @@ -1,5 +1,85 @@ -use crate::util::run_nix_cmd; +use crate::command::{NixCommand, StdIoInterceptor}; +use crate::util::{HashExtractor, NixErrorClassifier, NixFileFixer}; +use std::io::Write; -pub fn handle_nix_build(args: &[String]) { - run_nix_cmd("build", args); +pub fn handle_nix_build( + args: &[String], + hash_extractor: &dyn HashExtractor, + fixer: &dyn NixFileFixer, + classifier: &dyn NixErrorClassifier, +) { + let mut cmd = NixCommand::new("build").print_build_logs(true); + for arg in args { + cmd = cmd.arg(arg); + } + let status = cmd + .run_with_logs(StdIoInterceptor) + .expect("failed to run nix build"); + if status.success() { + return; + } + + let output = NixCommand::new("build") + .print_build_logs(true) + .args(args.iter().cloned()) + .output() + .expect("failed to capture output"); + let stderr = String::from_utf8_lossy(&output.stderr); + + if let Some(new_hash) = hash_extractor.extract_hash(&stderr) { + if fixer.fix_hash_in_files(&new_hash) { + eprintln!("\x1b[32m✔ Fixed hash mismatch, retrying...\x1b[0m"); + let retry_status = NixCommand::new("build") + .print_build_logs(true) + .args(args.iter().cloned()) + .run_with_logs(StdIoInterceptor) + .unwrap(); + std::process::exit(retry_status.code().unwrap_or(1)); + } + } + + if classifier.should_retry(&stderr) { + if stderr.contains("unfree") { + eprintln!( + "\x1b[33m⚠ Unfree package detected, retrying with NIXPKGS_ALLOW_UNFREE=1...\x1b[0m" + ); + let retry_status = NixCommand::new("build") + .print_build_logs(true) + .args(args.iter().cloned()) + .env("NIXPKGS_ALLOW_UNFREE", "1") + .impure(true) + .run_with_logs(StdIoInterceptor) + .unwrap(); + std::process::exit(retry_status.code().unwrap_or(1)); + } + if stderr.contains("insecure") { + eprintln!( + "\x1b[33m⚠ Insecure package detected, retrying with NIXPKGS_ALLOW_INSECURE=1...\x1b[0m" + ); + let retry_status = NixCommand::new("build") + .print_build_logs(true) + .args(args.iter().cloned()) + .env("NIXPKGS_ALLOW_INSECURE", "1") + .impure(true) + .run_with_logs(StdIoInterceptor) + .unwrap(); + std::process::exit(retry_status.code().unwrap_or(1)); + } + if stderr.contains("broken") { + eprintln!( + "\x1b[33m⚠ Broken package detected, retrying with NIXPKGS_ALLOW_BROKEN=1...\x1b[0m" + ); + let retry_status = NixCommand::new("build") + .print_build_logs(true) + .args(args.iter().cloned()) + .env("NIXPKGS_ALLOW_BROKEN", "1") + .impure(true) + .run_with_logs(StdIoInterceptor) + .unwrap(); + std::process::exit(retry_status.code().unwrap_or(1)); + } + } + + std::io::stderr().write_all(output.stderr.as_ref()).unwrap(); + std::process::exit(status.code().unwrap_or(1)); } diff --git a/eh/src/command.rs b/eh/src/command.rs new file mode 100644 index 0000000..2ea1b11 --- /dev/null +++ b/eh/src/command.rs @@ -0,0 +1,157 @@ +use std::collections::VecDeque; +use std::io::{self, Read, Write}; +use std::process::{Command, ExitStatus, Output, Stdio}; + +/// Trait for log interception and output handling. +pub trait LogInterceptor: Send { + fn on_stderr(&mut self, chunk: &[u8]); + fn on_stdout(&mut self, chunk: &[u8]); +} + +/// Default log interceptor that just writes to stdio. +pub struct StdIoInterceptor; + +impl LogInterceptor for StdIoInterceptor { + fn on_stderr(&mut self, chunk: &[u8]) { + let _ = io::stderr().write_all(chunk); + } + fn on_stdout(&mut self, chunk: &[u8]) { + let _ = io::stdout().write_all(chunk); + } +} + +/// Builder and executor for Nix commands. +pub struct NixCommand { + subcommand: String, + args: Vec, + env: Vec<(String, String)>, + impure: bool, + print_build_logs: bool, +} + +impl NixCommand { + pub fn new>(subcommand: S) -> Self { + Self { + subcommand: subcommand.into(), + args: Vec::new(), + env: Vec::new(), + impure: false, + print_build_logs: true, + } + } + + pub fn arg>(mut self, arg: S) -> Self { + self.args.push(arg.into()); + self + } + + pub fn args(mut self, args: I) -> Self + where + I: IntoIterator, + S: Into, + { + self.args.extend(args.into_iter().map(Into::into)); + self + } + + pub fn env, V: Into>(mut self, key: K, value: V) -> Self { + self.env.push((key.into(), value.into())); + self + } + + pub fn impure(mut self, yes: bool) -> Self { + self.impure = yes; + self + } + + pub fn print_build_logs(mut self, yes: bool) -> Self { + self.print_build_logs = yes; + self + } + + /// Run the command, streaming output to the provided interceptor. + pub fn run_with_logs( + &self, + mut interceptor: I, + ) -> io::Result { + let mut cmd = Command::new("nix"); + 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); + 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 mut out_buf = [0u8; 4096]; + let mut err_buf = [0u8; 4096]; + + let mut out_queue = VecDeque::new(); + let mut err_queue = VecDeque::new(); + + loop { + let mut did_something = false; + + match stdout.read(&mut out_buf) { + Ok(0) => {} + Ok(n) => { + interceptor.on_stdout(&out_buf[..n]); + out_queue.push_back(Vec::from(&out_buf[..n])); + did_something = true; + } + Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => {} + Err(e) => return Err(e), + } + + match stderr.read(&mut err_buf) { + Ok(0) => {} + 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(e), + } + + if !did_something && child.try_wait()?.is_some() { + break; + } + } + + let status = child.wait()?; + Ok(status) + } + + /// Run the command and capture all output. + pub fn output(&self) -> io::Result { + let mut cmd = Command::new("nix"); + 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); + cmd.stdout(Stdio::piped()); + cmd.stderr(Stdio::piped()); + + cmd.output() + } +} diff --git a/eh/src/main.rs b/eh/src/main.rs index eeac29e..9b8bfa7 100644 --- a/eh/src/main.rs +++ b/eh/src/main.rs @@ -3,6 +3,7 @@ use std::env; use std::path::Path; mod build; +mod command; mod run; mod shell; mod util; @@ -45,17 +46,26 @@ fn main() { match app_name { "nr" => { let rest: Vec = args.collect(); - run::handle_nix_run(&rest); + let hash_extractor = util::RegexHashExtractor; + let fixer = util::DefaultNixFileFixer; + let classifier = util::DefaultNixErrorClassifier; + run::handle_nix_run(&rest, &hash_extractor, &fixer, &classifier); return; } "ns" => { let rest: Vec = args.collect(); - shell::handle_nix_shell(&rest); + let hash_extractor = util::RegexHashExtractor; + let fixer = util::DefaultNixFileFixer; + let classifier = util::DefaultNixErrorClassifier; + shell::handle_nix_shell(&rest, &hash_extractor, &fixer, &classifier); return; } "nb" => { let rest: Vec = args.collect(); - build::handle_nix_build(&rest); + let hash_extractor = util::RegexHashExtractor; + let fixer = util::DefaultNixFileFixer; + let classifier = util::DefaultNixErrorClassifier; + build::handle_nix_build(&rest, &hash_extractor, &fixer, &classifier); return; } _ => {} @@ -63,10 +73,20 @@ fn main() { let cli = Cli::parse(); + let hash_extractor = util::RegexHashExtractor; + let fixer = util::DefaultNixFileFixer; + let classifier = util::DefaultNixErrorClassifier; + match cli.command { - Some(Command::Run { args }) => run::handle_nix_run(&args), - Some(Command::Shell { args }) => shell::handle_nix_shell(&args), - Some(Command::Build { args }) => build::handle_nix_build(&args), + Some(Command::Run { args }) => { + run::handle_nix_run(&args, &hash_extractor, &fixer, &classifier); + } + Some(Command::Shell { args }) => { + shell::handle_nix_shell(&args, &hash_extractor, &fixer, &classifier); + } + Some(Command::Build { args }) => { + build::handle_nix_build(&args, &hash_extractor, &fixer, &classifier); + } None => { Cli::command().print_help().unwrap(); println!(); diff --git a/eh/src/run.rs b/eh/src/run.rs index 0fd0181..c8c1785 100644 --- a/eh/src/run.rs +++ b/eh/src/run.rs @@ -1,5 +1,85 @@ -use crate::util::run_nix_cmd; +use crate::command::{NixCommand, StdIoInterceptor}; +use crate::util::{HashExtractor, NixErrorClassifier, NixFileFixer}; +use std::io::Write; -pub fn handle_nix_run(args: &[String]) { - run_nix_cmd("run", args); +pub fn handle_nix_run( + args: &[String], + hash_extractor: &dyn HashExtractor, + fixer: &dyn NixFileFixer, + classifier: &dyn NixErrorClassifier, +) { + let mut cmd = NixCommand::new("run").print_build_logs(true); + for arg in args { + cmd = cmd.arg(arg); + } + let status = cmd + .run_with_logs(StdIoInterceptor) + .expect("failed to run nix run"); + if status.success() { + return; + } + + let output = NixCommand::new("run") + .print_build_logs(true) + .args(args.iter().cloned()) + .output() + .expect("failed to capture output"); + let stderr = String::from_utf8_lossy(&output.stderr); + + if let Some(new_hash) = hash_extractor.extract_hash(&stderr) { + if fixer.fix_hash_in_files(&new_hash) { + eprintln!("\x1b[32m✔ Fixed hash mismatch, retrying...\x1b[0m"); + let retry_status = NixCommand::new("run") + .print_build_logs(true) + .args(args.iter().cloned()) + .run_with_logs(StdIoInterceptor) + .unwrap(); + std::process::exit(retry_status.code().unwrap_or(1)); + } + } + + if classifier.should_retry(&stderr) { + if stderr.contains("unfree") { + eprintln!( + "\x1b[33m⚠ Unfree package detected, retrying with NIXPKGS_ALLOW_UNFREE=1...\x1b[0m" + ); + let retry_status = NixCommand::new("run") + .print_build_logs(true) + .args(args.iter().cloned()) + .env("NIXPKGS_ALLOW_UNFREE", "1") + .impure(true) + .run_with_logs(StdIoInterceptor) + .unwrap(); + std::process::exit(retry_status.code().unwrap_or(1)); + } + if stderr.contains("insecure") { + eprintln!( + "\x1b[33m⚠ Insecure package detected, retrying with NIXPKGS_ALLOW_INSECURE=1...\x1b[0m" + ); + let retry_status = NixCommand::new("run") + .print_build_logs(true) + .args(args.iter().cloned()) + .env("NIXPKGS_ALLOW_INSECURE", "1") + .impure(true) + .run_with_logs(StdIoInterceptor) + .unwrap(); + std::process::exit(retry_status.code().unwrap_or(1)); + } + if stderr.contains("broken") { + eprintln!( + "\x1b[33m⚠ Broken package detected, retrying with NIXPKGS_ALLOW_BROKEN=1...\x1b[0m" + ); + let retry_status = NixCommand::new("run") + .print_build_logs(true) + .args(args.iter().cloned()) + .env("NIXPKGS_ALLOW_BROKEN", "1") + .impure(true) + .run_with_logs(StdIoInterceptor) + .unwrap(); + std::process::exit(retry_status.code().unwrap_or(1)); + } + } + + std::io::stderr().write_all(output.stderr.as_ref()).unwrap(); + std::process::exit(status.code().unwrap_or(1)); } diff --git a/eh/src/shell.rs b/eh/src/shell.rs index 8a47b82..1fcb414 100644 --- a/eh/src/shell.rs +++ b/eh/src/shell.rs @@ -1,5 +1,85 @@ -use crate::util::run_nix_cmd; +use crate::command::{NixCommand, StdIoInterceptor}; +use crate::util::{HashExtractor, NixErrorClassifier, NixFileFixer}; +use std::io::Write; -pub fn handle_nix_shell(args: &[String]) { - run_nix_cmd("shell", args); +pub fn handle_nix_shell( + args: &[String], + hash_extractor: &dyn HashExtractor, + fixer: &dyn NixFileFixer, + classifier: &dyn NixErrorClassifier, +) { + let mut cmd = NixCommand::new("shell").print_build_logs(true); + for arg in args { + cmd = cmd.arg(arg); + } + let status = cmd + .run_with_logs(StdIoInterceptor) + .expect("failed to run nix shell"); + if status.success() { + return; + } + + let output = NixCommand::new("shell") + .print_build_logs(true) + .args(args.iter().cloned()) + .output() + .expect("failed to capture output"); + let stderr = String::from_utf8_lossy(&output.stderr); + + if let Some(new_hash) = hash_extractor.extract_hash(&stderr) { + if fixer.fix_hash_in_files(&new_hash) { + eprintln!("\x1b[32m✔ Fixed hash mismatch, retrying...\x1b[0m"); + let retry_status = NixCommand::new("shell") + .print_build_logs(true) + .args(args.iter().cloned()) + .run_with_logs(StdIoInterceptor) + .unwrap(); + std::process::exit(retry_status.code().unwrap_or(1)); + } + } + + if classifier.should_retry(&stderr) { + if stderr.contains("unfree") { + eprintln!( + "\x1b[33m⚠ Unfree package detected, retrying with NIXPKGS_ALLOW_UNFREE=1...\x1b[0m" + ); + let retry_status = NixCommand::new("shell") + .print_build_logs(true) + .args(args.iter().cloned()) + .env("NIXPKGS_ALLOW_UNFREE", "1") + .impure(true) + .run_with_logs(StdIoInterceptor) + .unwrap(); + std::process::exit(retry_status.code().unwrap_or(1)); + } + if stderr.contains("insecure") { + eprintln!( + "\x1b[33m⚠ Insecure package detected, retrying with NIXPKGS_ALLOW_INSECURE=1...\x1b[0m" + ); + let retry_status = NixCommand::new("shell") + .print_build_logs(true) + .args(args.iter().cloned()) + .env("NIXPKGS_ALLOW_INSECURE", "1") + .impure(true) + .run_with_logs(StdIoInterceptor) + .unwrap(); + std::process::exit(retry_status.code().unwrap_or(1)); + } + if stderr.contains("broken") { + eprintln!( + "\x1b[33m⚠ Broken package detected, retrying with NIXPKGS_ALLOW_BROKEN=1...\x1b[0m" + ); + let retry_status = NixCommand::new("shell") + .print_build_logs(true) + .args(args.iter().cloned()) + .env("NIXPKGS_ALLOW_BROKEN", "1") + .impure(true) + .run_with_logs(StdIoInterceptor) + .unwrap(); + std::process::exit(retry_status.code().unwrap_or(1)); + } + } + + std::io::stderr().write_all(output.stderr.as_ref()).unwrap(); + std::process::exit(status.code().unwrap_or(1)); } diff --git a/eh/src/util.rs b/eh/src/util.rs index 7d6369a..d45c77c 100644 --- a/eh/src/util.rs +++ b/eh/src/util.rs @@ -1,195 +1,123 @@ use regex::Regex; use std::fs; -use std::io::{self, Write}; -use std::path::Path; -use std::process::{Command as StdCommand, Stdio}; +use std::path::{Path, PathBuf}; -pub fn extract_hash_from_error(stderr: &str) -> Option { - let patterns = [ - r"got:\s+([a-zA-Z0-9+/=]+)", - r"actual:\s+([a-zA-Z0-9+/=]+)", - r"have:\s+([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) { - return Some(hash.as_str().to_string()); - } - } - } - } - None +pub trait HashExtractor { + fn extract_hash(&self, stderr: &str) -> Option; } -pub fn fix_hash_in_files(new_hash: &str) -> bool { - let nix_files = find_nix_files(); - let mut fixed = false; +pub struct RegexHashExtractor; - for file_path in nix_files { - if fix_hash_in_file(&file_path, new_hash) { - println!("Updated hash in {file_path}"); - fixed = true; - } - } - - fixed -} - -pub fn find_nix_files() -> Vec { - let mut files = Vec::new(); - - let candidates = [ - "default.nix", - "package.nix", - "shell.nix", - "flake.nix", - "nix/default.nix", - "nix/package.nix", - "nix/site.nix", - ]; - - for candidate in &candidates { - if Path::new(*candidate).exists() { - files.push((*candidate).to_string()); - } - } - - if let Ok(entries) = fs::read_dir(".") { - for entry in entries.flatten() { - if let Some(name) = entry.file_name().to_str() { - let path = std::path::Path::new(name); - if path - .extension() - .is_some_and(|ext| ext.eq_ignore_ascii_case("nix")) - && !files.contains(&name.to_string()) - { - files.push(name.to_string()); - } - } - } - } - - files -} - -pub fn fix_hash_in_file(file_path: &str, new_hash: &str) -> bool { - if let Ok(content) = fs::read_to_string(file_path) { +impl HashExtractor for RegexHashExtractor { + fn extract_hash(&self, stderr: &str) -> Option { 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}""#), - ), + r"got:\s+([a-zA-Z0-9+/=]+)", + r"actual:\s+([a-zA-Z0-9+/=]+)", + r"have:\s+([a-zA-Z0-9+/=]+)", ]; - - for (pattern, replacement) in &patterns { + for pattern in &patterns { if let Ok(re) = Regex::new(pattern) { - if re.is_match(&content) { - let new_content = re.replace_all(&content, replacement); - if fs::write(file_path, new_content.as_ref()).is_ok() { - return true; + if let Some(captures) = re.captures(stderr) { + if let Some(hash) = captures.get(1) { + return Some(hash.as_str().to_string()); } } } } + None } - false } -pub fn should_retry_nix_error(stderr: &str) -> bool { - if extract_hash_from_error(stderr).is_some() { - return true; - } - (stderr.contains("unfree") && stderr.contains("refusing")) - || (stderr.contains("insecure") && stderr.contains("refusing")) - || (stderr.contains("broken") && stderr.contains("refusing")) +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; } -pub fn handle_nix_error(subcommand: &str, args: &[String], stderr: &str) { - if let Some(new_hash) = extract_hash_from_error(stderr) { - if fix_hash_in_files(&new_hash) { - println!("Fixed hash mismatch, retrying..."); - run_nix_cmd(subcommand, args); - return; +pub struct DefaultNixFileFixer; + +impl NixFileFixer for DefaultNixFileFixer { + fn fix_hash_in_files(&self, new_hash: &str) -> 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) { + println!("Updated hash in {}", file_path.display()); + fixed = true; + } } + fixed } - if stderr.contains("unfree") && stderr.contains("refusing") { - println!("Unfree package detected, retrying with NIXPKGS_ALLOW_UNFREE=1..."); - run_nix_cmd_with_env(subcommand, args, "NIXPKGS_ALLOW_UNFREE", "1"); - return; - } - - if stderr.contains("insecure") && stderr.contains("refusing") { - println!("Insecure package detected, retrying with NIXPKGS_ALLOW_INSECURE=1..."); - run_nix_cmd_with_env(subcommand, args, "NIXPKGS_ALLOW_INSECURE", "1"); - return; - } - - if stderr.contains("broken") && stderr.contains("refusing") { - println!("Broken package detected, retrying with NIXPKGS_ALLOW_BROKEN=1..."); - run_nix_cmd_with_env(subcommand, args, "NIXPKGS_ALLOW_BROKEN", "1"); - return; - } - - io::stderr().write_all(stderr.as_bytes()).unwrap(); - std::process::exit(1); -} - -pub fn run_nix_cmd(subcommand: &str, args: &[String]) { - let mut cmd = StdCommand::new("nix"); - cmd.arg(subcommand); - - if !args.iter().any(|arg| arg == "--no-build-output") { - cmd.arg("--print-build-logs"); - } - - cmd.args(args); - cmd.stderr(Stdio::piped()); - cmd.stdout(Stdio::inherit()); - - let mut child = cmd.spawn().expect("Failed to start nix command"); - let stderr = child.stderr.take().unwrap(); - - let stderr_handle = std::thread::spawn(move || { - let mut buffer = Vec::new(); - std::io::copy(&mut std::io::BufReader::new(stderr), &mut buffer).unwrap(); - buffer - }); - - let exit_status = child.wait().expect("Failed to wait for nix command"); - let stderr_output = stderr_handle.join().unwrap(); - - let stderr_str = String::from_utf8_lossy(&stderr_output); - - if !exit_status.success() { - if !should_retry_nix_error(&stderr_str) { - io::stderr().write_all(&stderr_output).unwrap(); + fn find_nix_files(&self) -> Vec { + let mut files = Vec::new(); + let candidates = [ + "default.nix", + "package.nix", + "shell.nix", + "flake.nix", + "nix/default.nix", + "nix/package.nix", + "nix/site.nix", + ]; + for candidate in &candidates { + let path = Path::new(candidate); + if path.exists() { + files.push(path.to_path_buf()); + } } - handle_nix_error(subcommand, args, &stderr_str); + if let Ok(entries) = fs::read_dir(".") { + for entry in entries.flatten() { + let path = entry.path(); + if let Some(ext) = path.extension() { + if ext.eq_ignore_ascii_case("nix") && !files.contains(&path) { + files.push(path); + } + } + } + } + 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}""#), + ), + ]; + for (pattern, replacement) in &patterns { + if let Ok(re) = Regex::new(pattern) { + if re.is_match(&content) { + let new_content = re.replace_all(&content, replacement); + if fs::write(file_path, new_content.as_ref()).is_ok() { + return true; + } + } + } + } + } + false } } -pub fn run_nix_cmd_with_env(subcommand: &str, args: &[String], env_key: &str, env_value: &str) { - let mut cmd = StdCommand::new("nix"); - cmd.env(env_key, env_value); - cmd.arg(subcommand); - - // Add --impure for env var to take effect - cmd.arg("--impure"); - - if !args.iter().any(|arg| arg == "--no-build-output") { - cmd.arg("--print-build-logs"); - } - - cmd.args(args); - - let exit_status = cmd.status().expect("Failed to retry nix command"); - std::process::exit(exit_status.code().unwrap_or(1)); +pub trait NixErrorClassifier { + fn should_retry(&self, stderr: &str) -> bool; +} + +pub struct DefaultNixErrorClassifier; + +impl NixErrorClassifier for DefaultNixErrorClassifier { + fn should_retry(&self, stderr: &str) -> bool { + RegexHashExtractor.extract_hash(stderr).is_some() + || (stderr.contains("unfree") && stderr.contains("refusing")) + || (stderr.contains("insecure") && stderr.contains("refusing")) + || (stderr.contains("broken") && stderr.contains("refusing")) + } }