From cd6a314bc882a6242ddf6129e726117267a32437 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Thu, 23 Apr 2026 17:43:35 +0300 Subject: [PATCH] util: block impure retries only when explicitly disabled Signed-off-by: NotAShelf Change-Id: I808c7976b97b3337c541f3bd4848eb486a6a6964 --- eh/src/commands/info.rs | 11 ++++++++--- eh/src/commands/mod.rs | 17 +++++++++++++++++ eh/src/commands/update.rs | 7 +++++-- eh/src/error.rs | 12 ++++++++++++ eh/src/util.rs | 22 ++++++++++++++++++++-- 5 files changed, 62 insertions(+), 7 deletions(-) diff --git a/eh/src/commands/info.rs b/eh/src/commands/info.rs index 03b487a..e0a34c3 100644 --- a/eh/src/commands/info.rs +++ b/eh/src/commands/info.rs @@ -31,7 +31,10 @@ struct PackageOutputs { outputs: HashMap, } -pub fn handle_info(args: &[String]) -> Result { +pub fn handle_info( + args: &[String], + cfg: &crate::config::CommandConfig, +) -> Result { // Get the package argument (skip flags) let pkg = args .iter() @@ -63,7 +66,8 @@ pub fn handle_info(args: &[String]) -> Result { let meta_cmd = NixCommand::new("eval") .arg("--json") .arg(&eval_arg) - .print_build_logs(false); + .print_build_logs(false) + .with_config(cfg); let meta_output = meta_cmd.output()?; @@ -91,7 +95,8 @@ pub fn handle_info(args: &[String]) -> Result { let outputs_cmd = NixCommand::new("eval") .arg("--json") .arg(format!("{}.outputs", outputs_expr)) - .print_build_logs(false); + .print_build_logs(false) + .with_config(cfg); let outputs_output = outputs_cmd.output()?; let outputs: Option = if outputs_output.status.success() { diff --git a/eh/src/commands/mod.rs b/eh/src/commands/mod.rs index 843b886..eeacf35 100644 --- a/eh/src/commands/mod.rs +++ b/eh/src/commands/mod.rs @@ -131,6 +131,21 @@ impl NixCommand { self } + /// Apply per-command configuration: sets `--impure` (when explicitly enabled) + /// and any extra environment variables declared in the config file. Call + /// this before any retry-specific overrides so that retry logic can still + /// force `impure(true)` afterwards. + #[must_use] + pub fn with_config(mut self, cfg: &crate::config::CommandConfig) -> Self { + if cfg.impure == Some(true) { + self = self.impure(true); + } + for (k, v) in &cfg.env { + self = self.env(k, v); + } + self + } + fn build_command(&self) -> Command { let mut cmd = Command::new("nix"); cmd.arg(&self.subcommand); @@ -321,6 +336,7 @@ pub fn handle_nix_command( hash_extractor: &dyn HashExtractor, fixer: &dyn NixFileFixer, classifier: &dyn NixErrorClassifier, + cfg: &crate::config::CommandConfig, ) -> Result { let intercept_env = matches!(command, "run" | "shell"); handle_nix_with_retry( @@ -330,6 +346,7 @@ pub fn handle_nix_command( fixer, classifier, intercept_env, + cfg, ) } diff --git a/eh/src/commands/update.rs b/eh/src/commands/update.rs index 4640b13..bd2634c 100644 --- a/eh/src/commands/update.rs +++ b/eh/src/commands/update.rs @@ -55,7 +55,10 @@ fn prompt_input_selection(inputs: &[String]) -> Result> { /// /// If `args` is non-empty, use them as explicit input names. /// Otherwise, fetch inputs interactively and prompt for selection. -pub fn handle_update(args: &[String]) -> Result { +pub fn handle_update( + args: &[String], + cfg: &crate::config::CommandConfig, +) -> Result { let selected = if args.is_empty() { let inputs = fetch_flake_inputs()?; if inputs.is_empty() { @@ -66,7 +69,7 @@ pub fn handle_update(args: &[String]) -> Result { args.to_vec() }; - let mut cmd = NixCommand::new("flake").arg("lock"); + let mut cmd = NixCommand::new("flake").arg("lock").with_config(cfg); for name in &selected { cmd = cmd.arg("--update-input").arg(name); } diff --git a/eh/src/error.rs b/eh/src/error.rs index 2846bb9..478c49f 100644 --- a/eh/src/error.rs +++ b/eh/src/error.rs @@ -54,6 +54,11 @@ pub enum EhError { #[error("no inputs selected")] UpdateCancelled, + + #[error( + "package {reason} but `--impure` is disabled for `{command}` in config" + )] + ImpureRequired { command: String, reason: String }, } pub type Result = std::result::Result; @@ -77,6 +82,7 @@ impl EhError { Self::JsonParse { .. } => 13, Self::NoFlakeInputs => 14, Self::UpdateCancelled => 0, + Self::ImpureRequired { .. } => 15, } } @@ -110,6 +116,12 @@ impl EhError { Self::NoFlakeInputs => { Some("run this from a directory with a flake.lock that has inputs") }, + Self::ImpureRequired { .. } => { + Some( + "set `impure = true` for this command (or globally) in .eh.toml or \ + ~/.config/eh/config.toml, or pass `--impure` manually", + ) + }, Self::Io(_) | Self::Regex(_) | Self::Utf8(_) diff --git a/eh/src/util.rs b/eh/src/util.rs index fce099b..9076834 100644 --- a/eh/src/util.rs +++ b/eh/src/util.rs @@ -485,6 +485,7 @@ pub fn handle_nix_with_retry( fixer: &dyn NixFileFixer, classifier: &dyn NixErrorClassifier, interactive: bool, + cfg: &crate::config::CommandConfig, ) -> Result { validate_nix_args(args)?; @@ -494,10 +495,17 @@ pub fn handle_nix_with_retry( let pkg = package_name(args); let pre_eval_action = pre_evaluate(args)?; if let Some((env_var, reason)) = pre_eval_action.env_override() { + if cfg.impure == Some(false) { + return Err(EhError::ImpureRequired { + command: subcommand.to_string(), + reason: reason.to_string(), + }); + } print_retry_msg(pkg, reason, env_var); let mut retry_cmd = NixCommand::new(subcommand) .print_build_logs(true) .args_ref(args) + .with_config(cfg) .env(env_var, "1") .impure(true); if interactive { @@ -513,6 +521,7 @@ pub fn handle_nix_with_retry( .print_build_logs(true) .interactive(true) .args_ref(args) + .with_config(cfg) .run_with_logs(StdIoInterceptor)?; if status.success() { return Ok(0); @@ -522,7 +531,8 @@ pub fn handle_nix_with_retry( // Capture output to check for errors that need retry (hash mismatches etc.) let output_cmd = NixCommand::new(subcommand) .print_build_logs(true) - .args_ref(args); + .args_ref(args) + .with_config(cfg); let output = output_cmd.output()?; let stderr = String::from_utf8_lossy(&output.stderr); @@ -561,7 +571,8 @@ pub fn handle_nix_with_retry( ); let mut retry_cmd = NixCommand::new(subcommand) .print_build_logs(true) - .args_ref(args); + .args_ref(args) + .with_config(cfg); if interactive { retry_cmd = retry_cmd.interactive(true); } @@ -594,10 +605,17 @@ pub fn handle_nix_with_retry( if classifier.should_retry(&stderr) { let action = classify_retry_action(&stderr); if let Some((env_var, reason)) = action.env_override() { + if cfg.impure == Some(false) { + return Err(EhError::ImpureRequired { + command: subcommand.to_string(), + reason: reason.to_string(), + }); + } print_retry_msg(pkg, reason, env_var); let mut retry_cmd = NixCommand::new(subcommand) .print_build_logs(true) .args_ref(args) + .with_config(cfg) .env(env_var, "1") .impure(true); if interactive {