util: block impure retries only when explicitly disabled

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I808c7976b97b3337c541f3bd4848eb486a6a6964
This commit is contained in:
raf 2026-04-23 17:43:35 +03:00
commit cd6a314bc8
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
5 changed files with 62 additions and 7 deletions

View file

@ -31,7 +31,10 @@ struct PackageOutputs {
outputs: HashMap<String, serde_json::Value>,
}
pub fn handle_info(args: &[String]) -> Result<i32> {
pub fn handle_info(
args: &[String],
cfg: &crate::config::CommandConfig,
) -> Result<i32> {
// Get the package argument (skip flags)
let pkg = args
.iter()
@ -63,7 +66,8 @@ pub fn handle_info(args: &[String]) -> Result<i32> {
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<i32> {
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<PackageOutputs> = if outputs_output.status.success() {

View file

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

View file

@ -55,7 +55,10 @@ fn prompt_input_selection(inputs: &[String]) -> Result<Vec<String>> {
///
/// 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<i32> {
pub fn handle_update(
args: &[String],
cfg: &crate::config::CommandConfig,
) -> Result<i32> {
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<i32> {
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);
}

View file

@ -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<T> = std::result::Result<T, EhError>;
@ -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(_)

View file

@ -485,6 +485,7 @@ pub fn handle_nix_with_retry(
fixer: &dyn NixFileFixer,
classifier: &dyn NixErrorClassifier,
interactive: bool,
cfg: &crate::config::CommandConfig,
) -> Result<i32> {
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 {