From ccbcce8c0825ff2758356326a29f344cbecfdf76 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Tue, 3 Mar 2026 20:59:37 +0300 Subject: [PATCH] treewide: move per-command logic into a commands module Signed-off-by: NotAShelf Change-Id: Ia7260a8691eea628f559ab8866aa51de6a6a6964 --- eh/src/build.rs | 18 ------- eh/src/{command.rs => commands/mod.rs} | 51 ++++++++++++-------- eh/src/{ => commands}/update.rs | 9 ++-- eh/src/error.rs | 18 ++----- eh/src/lib.rs | 6 +-- eh/src/main.rs | 48 ++++++++++++------- eh/src/run.rs | 18 ------- eh/src/shell.rs | 18 ------- eh/src/util.rs | 65 +++++++++++++++++++++----- 9 files changed, 127 insertions(+), 124 deletions(-) delete mode 100644 eh/src/build.rs rename eh/src/{command.rs => commands/mod.rs} (89%) rename eh/src/{ => commands}/update.rs (94%) delete mode 100644 eh/src/run.rs delete mode 100644 eh/src/shell.rs diff --git a/eh/src/build.rs b/eh/src/build.rs deleted file mode 100644 index 4ec4540..0000000 --- a/eh/src/build.rs +++ /dev/null @@ -1,18 +0,0 @@ -use crate::{ - error::Result, - util::{ - HashExtractor, - NixErrorClassifier, - NixFileFixer, - handle_nix_with_retry, - }, -}; - -pub fn handle_nix_build( - args: &[String], - hash_extractor: &dyn HashExtractor, - fixer: &dyn NixFileFixer, - classifier: &dyn NixErrorClassifier, -) -> Result { - handle_nix_with_retry("build", args, hash_extractor, fixer, classifier, false) -} diff --git a/eh/src/command.rs b/eh/src/commands/mod.rs similarity index 89% rename from eh/src/command.rs rename to eh/src/commands/mod.rs index a6ff260..6a46f5f 100644 --- a/eh/src/command.rs +++ b/eh/src/commands/mod.rs @@ -6,15 +6,26 @@ use std::{ time::{Duration, Instant}, }; -use crate::error::{EhError, Result}; +use crate::{ + error::{EhError, Result}, + util::{ + HashExtractor, + NixErrorClassifier, + NixFileFixer, + handle_nix_with_retry, + }, +}; + +pub mod update; + +const DEFAULT_BUFFER_SIZE: usize = 4096; +const DEFAULT_TIMEOUT: Duration = Duration::from_secs(300); -/// 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 { @@ -26,19 +37,12 @@ impl LogInterceptor for StdIoInterceptor { } } -/// Default buffer size for reading command output -const DEFAULT_BUFFER_SIZE: usize = 4096; - -/// Default timeout for command execution -const DEFAULT_TIMEOUT: Duration = Duration::from_secs(300); // 5 minutes - enum PipeEvent { Stdout(Vec), Stderr(Vec), Error(io::Error), } -/// Drain a pipe reader, sending chunks through the channel. fn read_pipe( mut reader: R, tx: mpsc::Sender, @@ -66,7 +70,6 @@ fn read_pipe( } } -/// Builder and executor for Nix commands. pub struct NixCommand { subcommand: String, args: Vec, @@ -126,8 +129,6 @@ impl NixCommand { self } - /// Build the underlying `std::process::Command` with all configured - /// arguments, environment variables, and flags. fn build_command(&self) -> Command { let mut cmd = Command::new("nix"); cmd.arg(&self.subcommand); @@ -147,10 +148,6 @@ impl NixCommand { cmd } - /// Run the command, streaming output to the provided interceptor. - /// - /// Stdout and stderr are read concurrently using background threads - /// so that neither pipe blocks the other. pub fn run_with_logs( &self, mut interceptor: I, @@ -212,7 +209,6 @@ impl NixCommand { return Err(EhError::Io(e)); }, Err(mpsc::RecvTimeoutError::Timeout) => {}, - // All senders dropped — both reader threads finished Err(mpsc::RecvTimeoutError::Disconnected) => break, } } @@ -224,7 +220,6 @@ impl NixCommand { Ok(status) } - /// Run the command and capture all output (with timeout). pub fn output(&self) -> Result { let mut cmd = self.build_command(); @@ -317,3 +312,21 @@ impl NixCommand { }) } } + +pub fn handle_nix_command( + command: &str, + args: &[String], + hash_extractor: &dyn HashExtractor, + fixer: &dyn NixFileFixer, + classifier: &dyn NixErrorClassifier, +) -> Result { + let intercept_env = matches!(command, "run" | "shell"); + handle_nix_with_retry( + command, + args, + hash_extractor, + fixer, + classifier, + intercept_env, + ) +} diff --git a/eh/src/update.rs b/eh/src/commands/update.rs similarity index 94% rename from eh/src/update.rs rename to eh/src/commands/update.rs index df4184e..4640b13 100644 --- a/eh/src/update.rs +++ b/eh/src/commands/update.rs @@ -1,14 +1,15 @@ use crate::{ - command::{NixCommand, StdIoInterceptor}, + commands::{NixCommand, StdIoInterceptor}, error::{EhError, Result}, }; /// Parse flake input names from `nix flake metadata --json` output. pub fn parse_flake_inputs(stdout: &str) -> Result> { - let value: serde_json::Value = - serde_json::from_str(stdout).map_err(|e| EhError::JsonParse { + let value: serde_json::Value = serde_json::from_str(stdout).map_err(|e| { + EhError::JsonParse { detail: e.to_string(), - })?; + } + })?; let inputs = value .get("locks") diff --git a/eh/src/error.rs b/eh/src/error.rs index 9ebc9b6..2846bb9 100644 --- a/eh/src/error.rs +++ b/eh/src/error.rs @@ -81,7 +81,7 @@ impl EhError { } #[must_use] - pub fn hint(&self) -> Option<&str> { + pub const fn hint(&self) -> Option<&str> { match self { Self::NixCommandFailed { .. } => { Some("run with --show-trace for more details") @@ -175,13 +175,7 @@ mod tests { 12 ); assert_eq!(EhError::ProcessExit { code: 42 }.exit_code(), 42); - assert_eq!( - EhError::JsonParse { - detail: "x".into(), - } - .exit_code(), - 13 - ); + assert_eq!(EhError::JsonParse { detail: "x".into() }.exit_code(), 13); assert_eq!(EhError::NoFlakeInputs.exit_code(), 14); assert_eq!(EhError::UpdateCancelled.exit_code(), 0); } @@ -249,13 +243,7 @@ mod tests { .hint() .is_some() ); - assert!( - EhError::JsonParse { - detail: "x".into(), - } - .hint() - .is_some() - ); + assert!(EhError::JsonParse { detail: "x".into() }.hint().is_some()); assert!(EhError::NoFlakeInputs.hint().is_some()); // Variants without hints assert!( diff --git a/eh/src/lib.rs b/eh/src/lib.rs index 23c5e78..f76e705 100644 --- a/eh/src/lib.rs +++ b/eh/src/lib.rs @@ -1,9 +1,5 @@ -pub mod build; -pub mod command; +pub mod commands; pub mod error; -pub mod run; -pub mod shell; -pub mod update; pub mod util; pub use clap::{CommandFactory, Parser, Subcommand}; diff --git a/eh/src/main.rs b/eh/src/main.rs index 8ce8b6b..890e474 100644 --- a/eh/src/main.rs +++ b/eh/src/main.rs @@ -4,12 +4,8 @@ use eh::{Cli, Command, CommandFactory, Parser}; use error::Result; use yansi::Paint; -mod build; -mod command; +mod commands; mod error; -mod run; -mod shell; -mod update; mod util; fn main() { @@ -66,7 +62,7 @@ fn dispatch_multicall( } if subcommand == "update" { - return Some(update::handle_update(&rest)); + return Some(commands::update::handle_update(&rest)); } let hash_extractor = util::RegexHashExtractor; @@ -74,12 +70,14 @@ fn dispatch_multicall( let classifier = util::DefaultNixErrorClassifier; Some(match subcommand { - "run" => run::handle_nix_run(&rest, &hash_extractor, &fixer, &classifier), - "shell" => { - shell::handle_nix_shell(&rest, &hash_extractor, &fixer, &classifier) - }, - "build" => { - build::handle_nix_build(&rest, &hash_extractor, &fixer, &classifier) + "run" | "shell" | "build" => { + commands::handle_nix_command( + subcommand, + &rest, + &hash_extractor, + &fixer, + &classifier, + ) }, // subcommand is assigned from the match on app_name above; // only "run"/"shell"/"build" are possible values. @@ -108,18 +106,36 @@ fn run_app() -> Result { match cli.command { Some(Command::Run { args }) => { - run::handle_nix_run(&args, &hash_extractor, &fixer, &classifier) + commands::handle_nix_command( + "run", + &args, + &hash_extractor, + &fixer, + &classifier, + ) }, Some(Command::Shell { args }) => { - shell::handle_nix_shell(&args, &hash_extractor, &fixer, &classifier) + commands::handle_nix_command( + "shell", + &args, + &hash_extractor, + &fixer, + &classifier, + ) }, Some(Command::Build { args }) => { - build::handle_nix_build(&args, &hash_extractor, &fixer, &classifier) + commands::handle_nix_command( + "build", + &args, + &hash_extractor, + &fixer, + &classifier, + ) }, - Some(Command::Update { args }) => update::handle_update(&args), + Some(Command::Update { args }) => commands::update::handle_update(&args), None => { Cli::command().print_help()?; diff --git a/eh/src/run.rs b/eh/src/run.rs deleted file mode 100644 index fff81a1..0000000 --- a/eh/src/run.rs +++ /dev/null @@ -1,18 +0,0 @@ -use crate::{ - error::Result, - util::{ - HashExtractor, - NixErrorClassifier, - NixFileFixer, - handle_nix_with_retry, - }, -}; - -pub fn handle_nix_run( - args: &[String], - hash_extractor: &dyn HashExtractor, - fixer: &dyn NixFileFixer, - classifier: &dyn NixErrorClassifier, -) -> Result { - handle_nix_with_retry("run", args, hash_extractor, fixer, classifier, true) -} diff --git a/eh/src/shell.rs b/eh/src/shell.rs deleted file mode 100644 index c0f4409..0000000 --- a/eh/src/shell.rs +++ /dev/null @@ -1,18 +0,0 @@ -use crate::{ - error::Result, - util::{ - HashExtractor, - NixErrorClassifier, - NixFileFixer, - handle_nix_with_retry, - }, -}; - -pub fn handle_nix_shell( - args: &[String], - hash_extractor: &dyn HashExtractor, - fixer: &dyn NixFileFixer, - classifier: &dyn NixErrorClassifier, -) -> 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 10227de..9bab5fe 100644 --- a/eh/src/util.rs +++ b/eh/src/util.rs @@ -11,7 +11,7 @@ use walkdir::WalkDir; use yansi::Paint; use crate::{ - command::{NixCommand, StdIoInterceptor}, + commands::{NixCommand, StdIoInterceptor}, error::{EhError, Result}, }; @@ -144,7 +144,7 @@ impl NixFileFixer for DefaultNixFileFixer { let mut result_content = content; if let Some(old) = old_hash { - // Targeted replacement: only replace attributes whose value matches the + // Only replace attributes whose value matches the // old hash. Uses regexes to handle variable whitespace around `=`. let old_escaped = regex::escape(old); let targeted_patterns = [ @@ -171,7 +171,7 @@ impl NixFileFixer for DefaultNixFileFixer { } } } else { - // Fallback: replace all hash attributes (original behavior) + // Fallback: replace all hash attributes let replacements = [ format!(r#"hash = "{new_hash}""#), format!(r#"sha256 = "{new_hash}""#), @@ -222,9 +222,11 @@ pub enum RetryAction { } impl RetryAction { - /// Returns `(env_var, reason)` for this retry action, - /// or `None` if no retry is needed. - fn env_override(&self) -> Option<(&str, &str)> { + /// # Returns + /// + /// `(env_var, reason)` for this retry action, or `None` if no retry is + /// needed. + const fn env_override(&self) -> Option<(&str, &str)> { match self { Self::AllowUnfree => { Some(("NIXPKGS_ALLOW_UNFREE", "has an unfree license")) @@ -245,8 +247,7 @@ fn package_name(args: &[String]) -> &str { args .iter() .find(|a| !a.starts_with('-')) - .map(String::as_str) - .unwrap_or("") + .map_or("", String::as_str) } /// Print a retry message with consistent formatting. @@ -261,6 +262,7 @@ fn print_retry_msg(pkg: &str, reason: &str, env_var: &str) { } /// Classify stderr into a retry action. +#[must_use] pub fn classify_retry_action(stderr: &str) -> RetryAction { if stderr.contains("has an unfree license") && stderr.contains("refusing") { RetryAction::AllowUnfree @@ -284,12 +286,52 @@ fn is_hash_mismatch_error(stderr: &str) -> bool { || (stderr.contains("specified:") && stderr.contains("got:")) } +/// Check if a package has an unfree, insecure, or broken attribute set. +/// Returns the appropriate `RetryAction` if any of these are true. +fn check_package_flags(args: &[String]) -> Result { + let eval_arg = args.iter().find(|arg| !arg.starts_with('-')); + + let Some(eval_arg) = eval_arg else { + return Ok(RetryAction::None); + }; + + let flags = [ + ("unfree", RetryAction::AllowUnfree), + ("insecure", RetryAction::AllowInsecure), + ("broken", RetryAction::AllowBroken), + ]; + + for (flag, action) in flags { + let eval_expr = format!("nixpkgs#{eval_arg}.meta.{flag}"); + let eval_cmd = NixCommand::new("eval") + .arg(&eval_expr) + .print_build_logs(false); + + if let Ok(output) = eval_cmd.output() + && output.status.success() + { + let stdout = String::from_utf8_lossy(&output.stdout); + if stdout.trim() == "true" { + return Ok(action); + } + } + } + + Ok(RetryAction::None) +} + /// Pre-evaluate expression to catch errors early. /// -/// Returns a `RetryAction` if the evaluation fails with a retryable error +/// Returns a `RetryAction` if the package has retryable flags /// (unfree/insecure/broken), allowing the caller to retry with the right -/// environment variables without ever streaming the verbose nix error output. +/// environment variables. fn pre_evaluate(args: &[String]) -> Result { + // First, check package meta flags directly to avoid error message parsing + let action = check_package_flags(args)?; + if action != RetryAction::None { + return Ok(action); + } + // 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('-')); @@ -311,12 +353,13 @@ fn pre_evaluate(args: &[String]) -> Result { let stderr = String::from_utf8_lossy(&output.stderr); // Classify whether this is a retryable error (unfree/insecure/broken) + // Fallback for errors that slip through (e.g., from dependencies) let action = classify_retry_action(&stderr); if action != RetryAction::None { return Ok(action); } - // Non-retryable eval failure — fail fast with a clear message + // Non-retryable eval failure, we should fail fast with a clear message // rather than running the full command and showing the same error again. let stderr_clean = stderr .trim()