treewide: move per-command logic into a commands module
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: Ia7260a8691eea628f559ab8866aa51de6a6a6964
This commit is contained in:
parent
502636ff69
commit
ccbcce8c08
11 changed files with 538 additions and 535 deletions
|
|
@ -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<i32> {
|
|
||||||
handle_nix_with_retry("build", args, hash_extractor, fixer, classifier, false)
|
|
||||||
}
|
|
||||||
|
|
@ -6,15 +6,26 @@ use std::{
|
||||||
time::{Duration, Instant},
|
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 {
|
pub trait LogInterceptor: Send {
|
||||||
fn on_stderr(&mut self, chunk: &[u8]);
|
fn on_stderr(&mut self, chunk: &[u8]);
|
||||||
fn on_stdout(&mut self, chunk: &[u8]);
|
fn on_stdout(&mut self, chunk: &[u8]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Default log interceptor that just writes to stdio.
|
|
||||||
pub struct StdIoInterceptor;
|
pub struct StdIoInterceptor;
|
||||||
|
|
||||||
impl LogInterceptor for 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 {
|
enum PipeEvent {
|
||||||
Stdout(Vec<u8>),
|
Stdout(Vec<u8>),
|
||||||
Stderr(Vec<u8>),
|
Stderr(Vec<u8>),
|
||||||
Error(io::Error),
|
Error(io::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Drain a pipe reader, sending chunks through the channel.
|
|
||||||
fn read_pipe<R: Read>(
|
fn read_pipe<R: Read>(
|
||||||
mut reader: R,
|
mut reader: R,
|
||||||
tx: mpsc::Sender<PipeEvent>,
|
tx: mpsc::Sender<PipeEvent>,
|
||||||
|
|
@ -66,7 +70,6 @@ fn read_pipe<R: Read>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Builder and executor for Nix commands.
|
|
||||||
pub struct NixCommand {
|
pub struct NixCommand {
|
||||||
subcommand: String,
|
subcommand: String,
|
||||||
args: Vec<String>,
|
args: Vec<String>,
|
||||||
|
|
@ -126,8 +129,6 @@ impl NixCommand {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build the underlying `std::process::Command` with all configured
|
|
||||||
/// arguments, environment variables, and flags.
|
|
||||||
fn build_command(&self) -> Command {
|
fn build_command(&self) -> Command {
|
||||||
let mut cmd = Command::new("nix");
|
let mut cmd = Command::new("nix");
|
||||||
cmd.arg(&self.subcommand);
|
cmd.arg(&self.subcommand);
|
||||||
|
|
@ -147,10 +148,6 @@ impl NixCommand {
|
||||||
cmd
|
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<I: LogInterceptor + 'static>(
|
pub fn run_with_logs<I: LogInterceptor + 'static>(
|
||||||
&self,
|
&self,
|
||||||
mut interceptor: I,
|
mut interceptor: I,
|
||||||
|
|
@ -212,7 +209,6 @@ impl NixCommand {
|
||||||
return Err(EhError::Io(e));
|
return Err(EhError::Io(e));
|
||||||
},
|
},
|
||||||
Err(mpsc::RecvTimeoutError::Timeout) => {},
|
Err(mpsc::RecvTimeoutError::Timeout) => {},
|
||||||
// All senders dropped — both reader threads finished
|
|
||||||
Err(mpsc::RecvTimeoutError::Disconnected) => break,
|
Err(mpsc::RecvTimeoutError::Disconnected) => break,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -224,7 +220,6 @@ impl NixCommand {
|
||||||
Ok(status)
|
Ok(status)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run the command and capture all output (with timeout).
|
|
||||||
pub fn output(&self) -> Result<Output> {
|
pub fn output(&self) -> Result<Output> {
|
||||||
let mut cmd = self.build_command();
|
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<i32> {
|
||||||
|
let intercept_env = matches!(command, "run" | "shell");
|
||||||
|
handle_nix_with_retry(
|
||||||
|
command,
|
||||||
|
args,
|
||||||
|
hash_extractor,
|
||||||
|
fixer,
|
||||||
|
classifier,
|
||||||
|
intercept_env,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -1,14 +1,15 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
command::{NixCommand, StdIoInterceptor},
|
commands::{NixCommand, StdIoInterceptor},
|
||||||
error::{EhError, Result},
|
error::{EhError, Result},
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Parse flake input names from `nix flake metadata --json` output.
|
/// Parse flake input names from `nix flake metadata --json` output.
|
||||||
pub fn parse_flake_inputs(stdout: &str) -> Result<Vec<String>> {
|
pub fn parse_flake_inputs(stdout: &str) -> Result<Vec<String>> {
|
||||||
let value: serde_json::Value =
|
let value: serde_json::Value = serde_json::from_str(stdout).map_err(|e| {
|
||||||
serde_json::from_str(stdout).map_err(|e| EhError::JsonParse {
|
EhError::JsonParse {
|
||||||
detail: e.to_string(),
|
detail: e.to_string(),
|
||||||
})?;
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
let inputs = value
|
let inputs = value
|
||||||
.get("locks")
|
.get("locks")
|
||||||
|
|
@ -81,7 +81,7 @@ impl EhError {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn hint(&self) -> Option<&str> {
|
pub const fn hint(&self) -> Option<&str> {
|
||||||
match self {
|
match self {
|
||||||
Self::NixCommandFailed { .. } => {
|
Self::NixCommandFailed { .. } => {
|
||||||
Some("run with --show-trace for more details")
|
Some("run with --show-trace for more details")
|
||||||
|
|
@ -175,13 +175,7 @@ mod tests {
|
||||||
12
|
12
|
||||||
);
|
);
|
||||||
assert_eq!(EhError::ProcessExit { code: 42 }.exit_code(), 42);
|
assert_eq!(EhError::ProcessExit { code: 42 }.exit_code(), 42);
|
||||||
assert_eq!(
|
assert_eq!(EhError::JsonParse { detail: "x".into() }.exit_code(), 13);
|
||||||
EhError::JsonParse {
|
|
||||||
detail: "x".into(),
|
|
||||||
}
|
|
||||||
.exit_code(),
|
|
||||||
13
|
|
||||||
);
|
|
||||||
assert_eq!(EhError::NoFlakeInputs.exit_code(), 14);
|
assert_eq!(EhError::NoFlakeInputs.exit_code(), 14);
|
||||||
assert_eq!(EhError::UpdateCancelled.exit_code(), 0);
|
assert_eq!(EhError::UpdateCancelled.exit_code(), 0);
|
||||||
}
|
}
|
||||||
|
|
@ -249,13 +243,7 @@ mod tests {
|
||||||
.hint()
|
.hint()
|
||||||
.is_some()
|
.is_some()
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(EhError::JsonParse { detail: "x".into() }.hint().is_some());
|
||||||
EhError::JsonParse {
|
|
||||||
detail: "x".into(),
|
|
||||||
}
|
|
||||||
.hint()
|
|
||||||
.is_some()
|
|
||||||
);
|
|
||||||
assert!(EhError::NoFlakeInputs.hint().is_some());
|
assert!(EhError::NoFlakeInputs.hint().is_some());
|
||||||
// Variants without hints
|
// Variants without hints
|
||||||
assert!(
|
assert!(
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,5 @@
|
||||||
pub mod build;
|
pub mod commands;
|
||||||
pub mod command;
|
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod run;
|
|
||||||
pub mod shell;
|
|
||||||
pub mod update;
|
|
||||||
pub mod util;
|
pub mod util;
|
||||||
|
|
||||||
pub use clap::{CommandFactory, Parser, Subcommand};
|
pub use clap::{CommandFactory, Parser, Subcommand};
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,8 @@ use eh::{Cli, Command, CommandFactory, Parser};
|
||||||
use error::Result;
|
use error::Result;
|
||||||
use yansi::Paint;
|
use yansi::Paint;
|
||||||
|
|
||||||
mod build;
|
mod commands;
|
||||||
mod command;
|
|
||||||
mod error;
|
mod error;
|
||||||
mod run;
|
|
||||||
mod shell;
|
|
||||||
mod update;
|
|
||||||
mod util;
|
mod util;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
|
@ -66,7 +62,7 @@ fn dispatch_multicall(
|
||||||
}
|
}
|
||||||
|
|
||||||
if subcommand == "update" {
|
if subcommand == "update" {
|
||||||
return Some(update::handle_update(&rest));
|
return Some(commands::update::handle_update(&rest));
|
||||||
}
|
}
|
||||||
|
|
||||||
let hash_extractor = util::RegexHashExtractor;
|
let hash_extractor = util::RegexHashExtractor;
|
||||||
|
|
@ -74,12 +70,14 @@ fn dispatch_multicall(
|
||||||
let classifier = util::DefaultNixErrorClassifier;
|
let classifier = util::DefaultNixErrorClassifier;
|
||||||
|
|
||||||
Some(match subcommand {
|
Some(match subcommand {
|
||||||
"run" => run::handle_nix_run(&rest, &hash_extractor, &fixer, &classifier),
|
"run" | "shell" | "build" => {
|
||||||
"shell" => {
|
commands::handle_nix_command(
|
||||||
shell::handle_nix_shell(&rest, &hash_extractor, &fixer, &classifier)
|
subcommand,
|
||||||
},
|
&rest,
|
||||||
"build" => {
|
&hash_extractor,
|
||||||
build::handle_nix_build(&rest, &hash_extractor, &fixer, &classifier)
|
&fixer,
|
||||||
|
&classifier,
|
||||||
|
)
|
||||||
},
|
},
|
||||||
// subcommand is assigned from the match on app_name above;
|
// subcommand is assigned from the match on app_name above;
|
||||||
// only "run"/"shell"/"build" are possible values.
|
// only "run"/"shell"/"build" are possible values.
|
||||||
|
|
@ -108,18 +106,36 @@ fn run_app() -> Result<i32> {
|
||||||
|
|
||||||
match cli.command {
|
match cli.command {
|
||||||
Some(Command::Run { args }) => {
|
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 }) => {
|
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 }) => {
|
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 => {
|
None => {
|
||||||
Cli::command().print_help()?;
|
Cli::command().print_help()?;
|
||||||
|
|
|
||||||
|
|
@ -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<i32> {
|
|
||||||
handle_nix_with_retry("run", args, hash_extractor, fixer, classifier, true)
|
|
||||||
}
|
|
||||||
|
|
@ -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<i32> {
|
|
||||||
handle_nix_with_retry("shell", args, hash_extractor, fixer, classifier, true)
|
|
||||||
}
|
|
||||||
|
|
@ -11,7 +11,7 @@ use walkdir::WalkDir;
|
||||||
use yansi::Paint;
|
use yansi::Paint;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
command::{NixCommand, StdIoInterceptor},
|
commands::{NixCommand, StdIoInterceptor},
|
||||||
error::{EhError, Result},
|
error::{EhError, Result},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -144,7 +144,7 @@ impl NixFileFixer for DefaultNixFileFixer {
|
||||||
let mut result_content = content;
|
let mut result_content = content;
|
||||||
|
|
||||||
if let Some(old) = old_hash {
|
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 `=`.
|
// old hash. Uses regexes to handle variable whitespace around `=`.
|
||||||
let old_escaped = regex::escape(old);
|
let old_escaped = regex::escape(old);
|
||||||
let targeted_patterns = [
|
let targeted_patterns = [
|
||||||
|
|
@ -171,7 +171,7 @@ impl NixFileFixer for DefaultNixFileFixer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Fallback: replace all hash attributes (original behavior)
|
// Fallback: replace all hash attributes
|
||||||
let replacements = [
|
let replacements = [
|
||||||
format!(r#"hash = "{new_hash}""#),
|
format!(r#"hash = "{new_hash}""#),
|
||||||
format!(r#"sha256 = "{new_hash}""#),
|
format!(r#"sha256 = "{new_hash}""#),
|
||||||
|
|
@ -222,9 +222,11 @@ pub enum RetryAction {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RetryAction {
|
impl RetryAction {
|
||||||
/// Returns `(env_var, reason)` for this retry action,
|
/// # Returns
|
||||||
/// or `None` if no retry is needed.
|
///
|
||||||
fn env_override(&self) -> Option<(&str, &str)> {
|
/// `(env_var, reason)` for this retry action, or `None` if no retry is
|
||||||
|
/// needed.
|
||||||
|
const fn env_override(&self) -> Option<(&str, &str)> {
|
||||||
match self {
|
match self {
|
||||||
Self::AllowUnfree => {
|
Self::AllowUnfree => {
|
||||||
Some(("NIXPKGS_ALLOW_UNFREE", "has an unfree license"))
|
Some(("NIXPKGS_ALLOW_UNFREE", "has an unfree license"))
|
||||||
|
|
@ -245,8 +247,7 @@ fn package_name(args: &[String]) -> &str {
|
||||||
args
|
args
|
||||||
.iter()
|
.iter()
|
||||||
.find(|a| !a.starts_with('-'))
|
.find(|a| !a.starts_with('-'))
|
||||||
.map(String::as_str)
|
.map_or("<unknown>", String::as_str)
|
||||||
.unwrap_or("<unknown>")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Print a retry message with consistent formatting.
|
/// 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.
|
/// Classify stderr into a retry action.
|
||||||
|
#[must_use]
|
||||||
pub fn classify_retry_action(stderr: &str) -> RetryAction {
|
pub fn classify_retry_action(stderr: &str) -> RetryAction {
|
||||||
if stderr.contains("has an unfree license") && stderr.contains("refusing") {
|
if stderr.contains("has an unfree license") && stderr.contains("refusing") {
|
||||||
RetryAction::AllowUnfree
|
RetryAction::AllowUnfree
|
||||||
|
|
@ -284,12 +286,52 @@ fn is_hash_mismatch_error(stderr: &str) -> bool {
|
||||||
|| (stderr.contains("specified:") && stderr.contains("got:"))
|
|| (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<RetryAction> {
|
||||||
|
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.
|
/// 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
|
/// (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<RetryAction> {
|
fn pre_evaluate(args: &[String]) -> Result<RetryAction> {
|
||||||
|
// 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
|
// Find flake references or expressions to evaluate
|
||||||
// Only take the first non-flag argument (the package/expression)
|
// Only take the first non-flag argument (the package/expression)
|
||||||
let eval_arg = args.iter().find(|arg| !arg.starts_with('-'));
|
let eval_arg = args.iter().find(|arg| !arg.starts_with('-'));
|
||||||
|
|
@ -311,12 +353,13 @@ fn pre_evaluate(args: &[String]) -> Result<RetryAction> {
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
|
|
||||||
// Classify whether this is a retryable error (unfree/insecure/broken)
|
// 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);
|
let action = classify_retry_action(&stderr);
|
||||||
if action != RetryAction::None {
|
if action != RetryAction::None {
|
||||||
return Ok(action);
|
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.
|
// rather than running the full command and showing the same error again.
|
||||||
let stderr_clean = stderr
|
let stderr_clean = stderr
|
||||||
.trim()
|
.trim()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue