cleanup
This commit is contained in:
		
					parent
					
						
							
								4fbf30a076
							
						
					
				
			
			
				commit
				
					
						bce7fe6d24
					
				
			
		
					 6 changed files with 529 additions and 184 deletions
				
			
		|  | @ -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)); | ||||
| } | ||||
|  |  | |||
							
								
								
									
										157
									
								
								eh/src/command.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										157
									
								
								eh/src/command.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -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<String>, | ||||
|     env: Vec<(String, String)>, | ||||
|     impure: bool, | ||||
|     print_build_logs: bool, | ||||
| } | ||||
| 
 | ||||
| impl NixCommand { | ||||
|     pub fn new<S: Into<String>>(subcommand: S) -> Self { | ||||
|         Self { | ||||
|             subcommand: subcommand.into(), | ||||
|             args: Vec::new(), | ||||
|             env: Vec::new(), | ||||
|             impure: false, | ||||
|             print_build_logs: true, | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     pub fn arg<S: Into<String>>(mut self, arg: S) -> Self { | ||||
|         self.args.push(arg.into()); | ||||
|         self | ||||
|     } | ||||
| 
 | ||||
|     pub fn args<I, S>(mut self, args: I) -> Self | ||||
|     where | ||||
|         I: IntoIterator<Item = S>, | ||||
|         S: Into<String>, | ||||
|     { | ||||
|         self.args.extend(args.into_iter().map(Into::into)); | ||||
|         self | ||||
|     } | ||||
| 
 | ||||
|     pub fn env<K: Into<String>, V: Into<String>>(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<I: LogInterceptor + 'static>( | ||||
|         &self, | ||||
|         mut interceptor: I, | ||||
|     ) -> io::Result<ExitStatus> { | ||||
|         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<Output> { | ||||
|         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() | ||||
|     } | ||||
| } | ||||
|  | @ -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<String> = 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<String> = 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<String> = 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!(); | ||||
|  |  | |||
|  | @ -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)); | ||||
| } | ||||
|  |  | |||
|  | @ -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)); | ||||
| } | ||||
|  |  | |||
							
								
								
									
										266
									
								
								eh/src/util.rs
									
										
									
									
									
								
							
							
						
						
									
										266
									
								
								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<String> { | ||||
|     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<String>; | ||||
| } | ||||
| 
 | ||||
| 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<String> { | ||||
|     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<String> { | ||||
|         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<PathBuf>; | ||||
|     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<PathBuf> { | ||||
|         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")) | ||||
|     } | ||||
| } | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue