Compare commits
6 commits
185403436b
...
7498902d46
| Author | SHA1 | Date | |
|---|---|---|---|
|
7498902d46 |
|||
|
c3321858c4 |
|||
|
b0a9a9ba5b |
|||
|
261e834ec4 |
|||
|
4b347ee2cc |
|||
|
2739462ec0 |
14 changed files with 1073 additions and 816 deletions
|
|
@ -1 +1,27 @@
|
||||||
|
condense_wildcard_suffixes = true
|
||||||
|
doc_comment_code_block_width = 80
|
||||||
|
edition = "2024" # Keep in sync with Cargo.toml.
|
||||||
|
enum_discrim_align_threshold = 60
|
||||||
|
force_explicit_abi = false
|
||||||
|
force_multiline_blocks = true
|
||||||
|
format_code_in_doc_comments = true
|
||||||
|
format_macro_matchers = true
|
||||||
|
format_strings = true
|
||||||
|
group_imports = "StdExternalCrate"
|
||||||
|
hex_literal_case = "Upper"
|
||||||
|
imports_granularity = "Crate"
|
||||||
|
imports_layout = "HorizontalVertical"
|
||||||
|
inline_attribute_width = 60
|
||||||
|
match_block_trailing_comma = true
|
||||||
|
max_width = 80
|
||||||
|
newline_style = "Unix"
|
||||||
|
normalize_comments = true
|
||||||
|
normalize_doc_attributes = true
|
||||||
|
overflow_delimited_expr = true
|
||||||
|
struct_field_align_threshold = 60
|
||||||
|
tab_spaces = 2
|
||||||
|
unstable_features = true
|
||||||
|
use_field_init_shorthand = true
|
||||||
|
use_try_shorthand = true
|
||||||
|
wrap_comments = true
|
||||||
|
|
||||||
|
|
|
||||||
94
Cargo.lock
generated
94
Cargo.lock
generated
|
|
@ -17,6 +17,12 @@ version = "1.0.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd"
|
checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitflags"
|
||||||
|
version = "2.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cfg-if"
|
name = "cfg-if"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
|
|
@ -76,6 +82,7 @@ version = "0.1.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"regex",
|
"regex",
|
||||||
|
"tempfile",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
|
|
@ -83,6 +90,34 @@ dependencies = [
|
||||||
"yansi",
|
"yansi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "errno"
|
||||||
|
version = "0.3.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fastrand"
|
||||||
|
version = "2.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getrandom"
|
||||||
|
version = "0.3.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"r-efi",
|
||||||
|
"wasip2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "heck"
|
name = "heck"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
|
|
@ -95,6 +130,18 @@ version = "1.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libc"
|
||||||
|
version = "0.2.177"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "linux-raw-sys"
|
||||||
|
version = "0.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "log"
|
name = "log"
|
||||||
version = "0.4.27"
|
version = "0.4.27"
|
||||||
|
|
@ -146,6 +193,12 @@ dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "r-efi"
|
||||||
|
version = "5.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "1.12.2"
|
version = "1.12.2"
|
||||||
|
|
@ -175,6 +228,19 @@ version = "0.8.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustix"
|
||||||
|
version = "1.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"errno",
|
||||||
|
"libc",
|
||||||
|
"linux-raw-sys",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "same-file"
|
name = "same-file"
|
||||||
version = "1.0.6"
|
version = "1.0.6"
|
||||||
|
|
@ -210,6 +276,19 @@ dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tempfile"
|
||||||
|
version = "3.23.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16"
|
||||||
|
dependencies = [
|
||||||
|
"fastrand",
|
||||||
|
"getrandom",
|
||||||
|
"once_cell",
|
||||||
|
"rustix",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "2.0.17"
|
version = "2.0.17"
|
||||||
|
|
@ -318,6 +397,15 @@ dependencies = [
|
||||||
"winapi-util",
|
"winapi-util",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasip2"
|
||||||
|
version = "1.0.1+wasi-0.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7"
|
||||||
|
dependencies = [
|
||||||
|
"wit-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winapi-util"
|
name = "winapi-util"
|
||||||
version = "0.1.11"
|
version = "0.1.11"
|
||||||
|
|
@ -342,6 +430,12 @@ dependencies = [
|
||||||
"windows-link",
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wit-bindgen"
|
||||||
|
version = "0.46.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "xtask"
|
name = "xtask"
|
||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
|
|
|
||||||
|
|
@ -18,3 +18,6 @@ tracing.workspace = true
|
||||||
tracing-subscriber.workspace = true
|
tracing-subscriber.workspace = true
|
||||||
walkdir.workspace = true
|
walkdir.workspace = true
|
||||||
yansi.workspace = true
|
yansi.workspace = true
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tempfile = "3.0"
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,18 @@
|
||||||
use crate::error::Result;
|
use crate::{
|
||||||
use crate::util::{HashExtractor, NixErrorClassifier, NixFileFixer, handle_nix_with_retry};
|
error::Result,
|
||||||
|
util::{
|
||||||
|
HashExtractor,
|
||||||
|
NixErrorClassifier,
|
||||||
|
NixFileFixer,
|
||||||
|
handle_nix_with_retry,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
pub fn handle_nix_build(
|
pub fn handle_nix_build(
|
||||||
args: &[String],
|
args: &[String],
|
||||||
hash_extractor: &dyn HashExtractor,
|
hash_extractor: &dyn HashExtractor,
|
||||||
fixer: &dyn NixFileFixer,
|
fixer: &dyn NixFileFixer,
|
||||||
classifier: &dyn NixErrorClassifier,
|
classifier: &dyn NixErrorClassifier,
|
||||||
) -> Result<i32> {
|
) -> Result<i32> {
|
||||||
handle_nix_with_retry("build", args, hash_extractor, fixer, classifier, false)
|
handle_nix_with_retry("build", args, hash_extractor, fixer, classifier, false)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,27 @@
|
||||||
|
use std::{
|
||||||
|
collections::VecDeque,
|
||||||
|
io::{self, Read, Write},
|
||||||
|
process::{Command, ExitStatus, Output, Stdio},
|
||||||
|
};
|
||||||
|
|
||||||
use crate::error::{EhError, Result};
|
use crate::error::{EhError, Result};
|
||||||
use std::collections::VecDeque;
|
|
||||||
use std::io::{self, Read, Write};
|
|
||||||
use std::process::{Command, ExitStatus, Output, Stdio};
|
|
||||||
|
|
||||||
/// Trait for log interception and output handling.
|
/// 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.
|
/// Default log interceptor that just writes to stdio.
|
||||||
pub struct StdIoInterceptor;
|
pub struct StdIoInterceptor;
|
||||||
|
|
||||||
impl LogInterceptor for StdIoInterceptor {
|
impl LogInterceptor for StdIoInterceptor {
|
||||||
fn on_stderr(&mut self, chunk: &[u8]) {
|
fn on_stderr(&mut self, chunk: &[u8]) {
|
||||||
let _ = io::stderr().write_all(chunk);
|
let _ = io::stderr().write_all(chunk);
|
||||||
}
|
}
|
||||||
fn on_stdout(&mut self, chunk: &[u8]) {
|
fn on_stdout(&mut self, chunk: &[u8]) {
|
||||||
let _ = io::stdout().write_all(chunk);
|
let _ = io::stdout().write_all(chunk);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Default buffer size for reading command output
|
/// Default buffer size for reading command output
|
||||||
|
|
@ -26,167 +29,184 @@ const DEFAULT_BUFFER_SIZE: usize = 4096;
|
||||||
|
|
||||||
/// Builder and executor for Nix commands.
|
/// Builder and executor for Nix commands.
|
||||||
pub struct NixCommand {
|
pub struct NixCommand {
|
||||||
subcommand: String,
|
subcommand: String,
|
||||||
args: Vec<String>,
|
args: Vec<String>,
|
||||||
env: Vec<(String, String)>,
|
env: Vec<(String, String)>,
|
||||||
impure: bool,
|
impure: bool,
|
||||||
print_build_logs: bool,
|
print_build_logs: bool,
|
||||||
interactive: bool,
|
interactive: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NixCommand {
|
impl NixCommand {
|
||||||
pub fn new<S: Into<String>>(subcommand: S) -> Self {
|
pub fn new<S: Into<String>>(subcommand: S) -> Self {
|
||||||
Self {
|
Self {
|
||||||
subcommand: subcommand.into(),
|
subcommand: subcommand.into(),
|
||||||
args: Vec::new(),
|
args: Vec::new(),
|
||||||
env: Vec::new(),
|
env: Vec::new(),
|
||||||
impure: false,
|
impure: false,
|
||||||
print_build_logs: true,
|
print_build_logs: true,
|
||||||
interactive: false,
|
interactive: false,
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn arg<S: Into<String>>(mut self, arg: S) -> Self {
|
pub fn arg<S: Into<String>>(mut self, arg: S) -> Self {
|
||||||
self.args.push(arg.into());
|
self.args.push(arg.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn args<I, S>(mut self, args: I) -> Self
|
pub fn args<I, S>(mut self, args: I) -> Self
|
||||||
where
|
where
|
||||||
I: IntoIterator<Item = S>,
|
I: IntoIterator<Item = S>,
|
||||||
S: Into<String>,
|
S: Into<String>,
|
||||||
|
{
|
||||||
|
self.args.extend(args.into_iter().map(Into::into));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn args_ref(mut self, args: &[String]) -> Self {
|
||||||
|
self.args.extend(args.iter().cloned());
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn impure(mut self, yes: bool) -> Self {
|
||||||
|
self.impure = yes;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn interactive(mut self, yes: bool) -> Self {
|
||||||
|
self.interactive = yes;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const 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,
|
||||||
|
) -> 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")
|
||||||
{
|
{
|
||||||
self.args.extend(args.into_iter().map(Into::into));
|
cmd.arg("--print-build-logs");
|
||||||
self
|
}
|
||||||
|
if self.impure {
|
||||||
|
cmd.arg("--impure");
|
||||||
|
}
|
||||||
|
for (k, v) in &self.env {
|
||||||
|
cmd.env(k, v);
|
||||||
|
}
|
||||||
|
cmd.args(&self.args);
|
||||||
|
|
||||||
|
if self.interactive {
|
||||||
|
cmd.stdout(Stdio::inherit());
|
||||||
|
cmd.stderr(Stdio::inherit());
|
||||||
|
cmd.stdin(Stdio::inherit());
|
||||||
|
return Ok(cmd.status()?);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn env<K: Into<String>, V: Into<String>>(mut self, key: K, value: V) -> Self {
|
cmd.stdout(Stdio::piped());
|
||||||
self.env.push((key.into(), value.into()));
|
cmd.stderr(Stdio::piped());
|
||||||
self
|
|
||||||
|
let mut child = cmd.spawn()?;
|
||||||
|
let child_stdout = child.stdout.take().ok_or_else(|| {
|
||||||
|
EhError::CommandFailed {
|
||||||
|
command: format!("nix {}", self.subcommand),
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
let child_stderr = child.stderr.take().ok_or_else(|| {
|
||||||
|
EhError::CommandFailed {
|
||||||
|
command: format!("nix {}", self.subcommand),
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
let mut stdout = child_stdout;
|
||||||
|
let mut stderr = child_stderr;
|
||||||
|
|
||||||
|
let mut out_buf = [0u8; DEFAULT_BUFFER_SIZE];
|
||||||
|
let mut err_buf = [0u8; DEFAULT_BUFFER_SIZE];
|
||||||
|
|
||||||
|
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(EhError::Io(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(EhError::Io(e)),
|
||||||
|
}
|
||||||
|
|
||||||
|
if !did_something && child.try_wait()?.is_some() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
let status = child.wait()?;
|
||||||
pub const fn impure(mut self, yes: bool) -> Self {
|
Ok(status)
|
||||||
self.impure = yes;
|
}
|
||||||
self
|
|
||||||
|
/// Run the command and capture all output.
|
||||||
|
pub fn output(&self) -> 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);
|
||||||
|
|
||||||
|
if self.interactive {
|
||||||
|
cmd.stdout(Stdio::inherit());
|
||||||
|
cmd.stderr(Stdio::inherit());
|
||||||
|
cmd.stdin(Stdio::inherit());
|
||||||
|
} else {
|
||||||
|
cmd.stdout(Stdio::piped());
|
||||||
|
cmd.stderr(Stdio::piped());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
Ok(cmd.output()?)
|
||||||
pub const fn interactive(mut self, yes: bool) -> Self {
|
}
|
||||||
self.interactive = yes;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub const 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,
|
|
||||||
) -> 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);
|
|
||||||
|
|
||||||
if self.interactive {
|
|
||||||
cmd.stdout(Stdio::inherit());
|
|
||||||
cmd.stderr(Stdio::inherit());
|
|
||||||
cmd.stdin(Stdio::inherit());
|
|
||||||
return Ok(cmd.status()?);
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd.stdout(Stdio::piped());
|
|
||||||
cmd.stderr(Stdio::piped());
|
|
||||||
|
|
||||||
let mut child = cmd.spawn()?;
|
|
||||||
let child_stdout = child.stdout.take().ok_or_else(|| EhError::CommandFailed {
|
|
||||||
command: format!("nix {}", self.subcommand),
|
|
||||||
})?;
|
|
||||||
let child_stderr = child.stderr.take().ok_or_else(|| EhError::CommandFailed {
|
|
||||||
command: format!("nix {}", self.subcommand),
|
|
||||||
})?;
|
|
||||||
let mut stdout = child_stdout;
|
|
||||||
let mut stderr = child_stderr;
|
|
||||||
|
|
||||||
let mut out_buf = [0u8; DEFAULT_BUFFER_SIZE];
|
|
||||||
let mut err_buf = [0u8; DEFAULT_BUFFER_SIZE];
|
|
||||||
|
|
||||||
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(EhError::Io(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(EhError::Io(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) -> 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);
|
|
||||||
|
|
||||||
if self.interactive {
|
|
||||||
cmd.stdout(Stdio::inherit());
|
|
||||||
cmd.stderr(Stdio::inherit());
|
|
||||||
cmd.stdin(Stdio::inherit());
|
|
||||||
} else {
|
|
||||||
cmd.stdout(Stdio::piped());
|
|
||||||
cmd.stderr(Stdio::piped());
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(cmd.output()?)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,44 +2,44 @@ use thiserror::Error;
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum EhError {
|
pub enum EhError {
|
||||||
#[error("Nix command failed: {0}")]
|
#[error("Nix command failed: {0}")]
|
||||||
NixCommandFailed(String),
|
NixCommandFailed(String),
|
||||||
|
|
||||||
#[error("IO error: {0}")]
|
#[error("IO error: {0}")]
|
||||||
Io(#[from] std::io::Error),
|
Io(#[from] std::io::Error),
|
||||||
|
|
||||||
#[error("Regex error: {0}")]
|
#[error("Regex error: {0}")]
|
||||||
Regex(#[from] regex::Error),
|
Regex(#[from] regex::Error),
|
||||||
|
|
||||||
#[error("UTF-8 conversion error: {0}")]
|
#[error("UTF-8 conversion error: {0}")]
|
||||||
Utf8(#[from] std::string::FromUtf8Error),
|
Utf8(#[from] std::string::FromUtf8Error),
|
||||||
|
|
||||||
#[error("Hash extraction failed")]
|
#[error("Hash extraction failed")]
|
||||||
HashExtractionFailed,
|
HashExtractionFailed,
|
||||||
|
|
||||||
#[error("No Nix files found")]
|
#[error("No Nix files found")]
|
||||||
NoNixFilesFound,
|
NoNixFilesFound,
|
||||||
|
|
||||||
#[error("Failed to fix hash in file: {path}")]
|
#[error("Failed to fix hash in file: {path}")]
|
||||||
HashFixFailed { path: String },
|
HashFixFailed { path: String },
|
||||||
|
|
||||||
#[error("Process exited with code: {code}")]
|
#[error("Process exited with code: {code}")]
|
||||||
ProcessExit { code: i32 },
|
ProcessExit { code: i32 },
|
||||||
|
|
||||||
#[error("Command execution failed: {command}")]
|
#[error("Command execution failed: {command}")]
|
||||||
CommandFailed { command: String },
|
CommandFailed { command: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type Result<T> = std::result::Result<T, EhError>;
|
pub type Result<T> = std::result::Result<T, EhError>;
|
||||||
|
|
||||||
impl EhError {
|
impl EhError {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub const fn exit_code(&self) -> i32 {
|
pub const fn exit_code(&self) -> i32 {
|
||||||
match self {
|
match self {
|
||||||
Self::ProcessExit { code } => *code,
|
Self::ProcessExit { code } => *code,
|
||||||
Self::NixCommandFailed(_) => 1,
|
Self::NixCommandFailed(_) => 1,
|
||||||
Self::CommandFailed { .. } => 1,
|
Self::CommandFailed { .. } => 1,
|
||||||
_ => 1,
|
_ => 1,
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,26 +11,27 @@ pub use error::{EhError, Result};
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(name = "eh")]
|
#[command(name = "eh")]
|
||||||
#[command(about = "Ergonomic Nix helper", long_about = None)]
|
#[command(about = "Ergonomic Nix helper", long_about = None)]
|
||||||
|
#[command(version)]
|
||||||
pub struct Cli {
|
pub struct Cli {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
pub command: Option<Command>,
|
pub command: Option<Command>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
pub enum Command {
|
pub enum Command {
|
||||||
/// Run a Nix derivation
|
/// Run a Nix derivation
|
||||||
Run {
|
Run {
|
||||||
#[arg(trailing_var_arg = true)]
|
#[arg(trailing_var_arg = true)]
|
||||||
args: Vec<String>,
|
args: Vec<String>,
|
||||||
},
|
},
|
||||||
/// Enter a Nix shell
|
/// Enter a Nix shell
|
||||||
Shell {
|
Shell {
|
||||||
#[arg(trailing_var_arg = true)]
|
#[arg(trailing_var_arg = true)]
|
||||||
args: Vec<String>,
|
args: Vec<String>,
|
||||||
},
|
},
|
||||||
/// Build a Nix derivation
|
/// Build a Nix derivation
|
||||||
Build {
|
Build {
|
||||||
#[arg(trailing_var_arg = true)]
|
#[arg(trailing_var_arg = true)]
|
||||||
args: Vec<String>,
|
args: Vec<String>,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
145
eh/src/main.rs
145
eh/src/main.rs
|
|
@ -1,7 +1,7 @@
|
||||||
|
use std::{env, path::Path};
|
||||||
|
|
||||||
use eh::{Cli, Command, CommandFactory, Parser};
|
use eh::{Cli, Command, CommandFactory, Parser};
|
||||||
use error::Result;
|
use error::Result;
|
||||||
use std::env;
|
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
mod build;
|
mod build;
|
||||||
mod command;
|
mod command;
|
||||||
|
|
@ -11,91 +11,100 @@ mod shell;
|
||||||
mod util;
|
mod util;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let format = tracing_subscriber::fmt::format()
|
let format = tracing_subscriber::fmt::format()
|
||||||
.with_level(true) // don't include levels in formatted output
|
.with_level(true) // don't include levels in formatted output
|
||||||
.with_target(true) // don't include targets
|
.with_target(true) // don't include targets
|
||||||
.with_thread_ids(false) // include the thread ID of the current thread
|
.with_thread_ids(false) // include the thread ID of the current thread
|
||||||
.with_thread_names(false) // include the name of the current thread
|
.with_thread_names(false) // include the name of the current thread
|
||||||
.compact(); // use the `Compact` formatting style.
|
.compact(); // use the `Compact` formatting style.
|
||||||
tracing_subscriber::fmt().event_format(format).init();
|
tracing_subscriber::fmt().event_format(format).init();
|
||||||
|
|
||||||
let result = run_app();
|
let result = run_app();
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(code) => std::process::exit(code),
|
Ok(code) => std::process::exit(code),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Error: {e}");
|
eprintln!("Error: {e}");
|
||||||
std::process::exit(e.exit_code());
|
std::process::exit(e.exit_code());
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Design partially taken from Stash
|
// Design partially taken from Stash
|
||||||
fn dispatch_multicall(app_name: &str, args: std::env::Args) -> Option<Result<i32>> {
|
fn dispatch_multicall(
|
||||||
let rest: Vec<String> = args.collect();
|
app_name: &str,
|
||||||
let hash_extractor = util::RegexHashExtractor;
|
args: std::env::Args,
|
||||||
let fixer = util::DefaultNixFileFixer;
|
) -> Option<Result<i32>> {
|
||||||
let classifier = util::DefaultNixErrorClassifier;
|
let rest: Vec<String> = args.collect();
|
||||||
|
let hash_extractor = util::RegexHashExtractor;
|
||||||
|
let fixer = util::DefaultNixFileFixer;
|
||||||
|
let classifier = util::DefaultNixErrorClassifier;
|
||||||
|
|
||||||
match app_name {
|
match app_name {
|
||||||
"nr" => Some(run::handle_nix_run(
|
"nr" => {
|
||||||
&rest,
|
Some(run::handle_nix_run(
|
||||||
&hash_extractor,
|
&rest,
|
||||||
&fixer,
|
&hash_extractor,
|
||||||
&classifier,
|
&fixer,
|
||||||
)),
|
&classifier,
|
||||||
"ns" => Some(shell::handle_nix_shell(
|
))
|
||||||
&rest,
|
},
|
||||||
&hash_extractor,
|
"ns" => {
|
||||||
&fixer,
|
Some(shell::handle_nix_shell(
|
||||||
&classifier,
|
&rest,
|
||||||
)),
|
&hash_extractor,
|
||||||
"nb" => Some(build::handle_nix_build(
|
&fixer,
|
||||||
&rest,
|
&classifier,
|
||||||
&hash_extractor,
|
))
|
||||||
&fixer,
|
},
|
||||||
&classifier,
|
"nb" => {
|
||||||
)),
|
Some(build::handle_nix_build(
|
||||||
_ => None,
|
&rest,
|
||||||
}
|
&hash_extractor,
|
||||||
|
&fixer,
|
||||||
|
&classifier,
|
||||||
|
))
|
||||||
|
},
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_app() -> Result<i32> {
|
fn run_app() -> Result<i32> {
|
||||||
let mut args = env::args();
|
let mut args = env::args();
|
||||||
let bin = args.next().unwrap_or_else(|| "eh".to_string());
|
let bin = args.next().unwrap_or_else(|| "eh".to_string());
|
||||||
let app_name = Path::new(&bin)
|
let app_name = Path::new(&bin)
|
||||||
.file_name()
|
.file_name()
|
||||||
.and_then(|name| name.to_str())
|
.and_then(|name| name.to_str())
|
||||||
.unwrap_or("eh");
|
.unwrap_or("eh");
|
||||||
|
|
||||||
// If invoked as nr/ns/nb, dispatch directly and exit
|
// If invoked as nr/ns/nb, dispatch directly and exit
|
||||||
if let Some(result) = dispatch_multicall(app_name, args) {
|
if let Some(result) = dispatch_multicall(app_name, args) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
|
|
||||||
let hash_extractor = util::RegexHashExtractor;
|
let hash_extractor = util::RegexHashExtractor;
|
||||||
let fixer = util::DefaultNixFileFixer;
|
let fixer = util::DefaultNixFileFixer;
|
||||||
let classifier = util::DefaultNixErrorClassifier;
|
let classifier = util::DefaultNixErrorClassifier;
|
||||||
|
|
||||||
match cli.command {
|
match cli.command {
|
||||||
Some(Command::Run { args }) => {
|
Some(Command::Run { args }) => {
|
||||||
run::handle_nix_run(&args, &hash_extractor, &fixer, &classifier)
|
run::handle_nix_run(&args, &hash_extractor, &fixer, &classifier)
|
||||||
}
|
},
|
||||||
|
|
||||||
Some(Command::Shell { args }) => {
|
Some(Command::Shell { args }) => {
|
||||||
shell::handle_nix_shell(&args, &hash_extractor, &fixer, &classifier)
|
shell::handle_nix_shell(&args, &hash_extractor, &fixer, &classifier)
|
||||||
}
|
},
|
||||||
|
|
||||||
Some(Command::Build { args }) => {
|
Some(Command::Build { args }) => {
|
||||||
build::handle_nix_build(&args, &hash_extractor, &fixer, &classifier)
|
build::handle_nix_build(&args, &hash_extractor, &fixer, &classifier)
|
||||||
}
|
},
|
||||||
|
|
||||||
_ => {
|
_ => {
|
||||||
Cli::command().print_help()?;
|
Cli::command().print_help()?;
|
||||||
println!();
|
println!();
|
||||||
Ok(0)
|
Ok(0)
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,18 @@
|
||||||
use crate::error::Result;
|
use crate::{
|
||||||
use crate::util::{HashExtractor, NixErrorClassifier, NixFileFixer, handle_nix_with_retry};
|
error::Result,
|
||||||
|
util::{
|
||||||
|
HashExtractor,
|
||||||
|
NixErrorClassifier,
|
||||||
|
NixFileFixer,
|
||||||
|
handle_nix_with_retry,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
pub fn handle_nix_run(
|
pub fn handle_nix_run(
|
||||||
args: &[String],
|
args: &[String],
|
||||||
hash_extractor: &dyn HashExtractor,
|
hash_extractor: &dyn HashExtractor,
|
||||||
fixer: &dyn NixFileFixer,
|
fixer: &dyn NixFileFixer,
|
||||||
classifier: &dyn NixErrorClassifier,
|
classifier: &dyn NixErrorClassifier,
|
||||||
) -> Result<i32> {
|
) -> Result<i32> {
|
||||||
handle_nix_with_retry("run", args, hash_extractor, fixer, classifier, true)
|
handle_nix_with_retry("run", args, hash_extractor, fixer, classifier, true)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,18 @@
|
||||||
use crate::error::Result;
|
use crate::{
|
||||||
use crate::util::{HashExtractor, NixErrorClassifier, NixFileFixer, handle_nix_with_retry};
|
error::Result,
|
||||||
|
util::{
|
||||||
|
HashExtractor,
|
||||||
|
NixErrorClassifier,
|
||||||
|
NixFileFixer,
|
||||||
|
handle_nix_with_retry,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
pub fn handle_nix_shell(
|
pub fn handle_nix_shell(
|
||||||
args: &[String],
|
args: &[String],
|
||||||
hash_extractor: &dyn HashExtractor,
|
hash_extractor: &dyn HashExtractor,
|
||||||
fixer: &dyn NixFileFixer,
|
fixer: &dyn NixFileFixer,
|
||||||
classifier: &dyn NixErrorClassifier,
|
classifier: &dyn NixErrorClassifier,
|
||||||
) -> Result<i32> {
|
) -> Result<i32> {
|
||||||
handle_nix_with_retry("shell", args, hash_extractor, fixer, classifier, true)
|
handle_nix_with_retry("shell", args, hash_extractor, fixer, classifier, true)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
488
eh/src/util.rs
488
eh/src/util.rs
|
|
@ -1,292 +1,312 @@
|
||||||
use crate::command::{NixCommand, StdIoInterceptor};
|
use std::{
|
||||||
use crate::error::{EhError, Result};
|
fs,
|
||||||
|
io::Write,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use std::fs;
|
|
||||||
use std::io::Write;
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
use tracing::{info, warn};
|
use tracing::{info, warn};
|
||||||
use walkdir::WalkDir;
|
use walkdir::WalkDir;
|
||||||
use yansi::Paint;
|
use yansi::Paint;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
command::{NixCommand, StdIoInterceptor},
|
||||||
|
error::{EhError, Result},
|
||||||
|
};
|
||||||
|
|
||||||
pub trait HashExtractor {
|
pub trait HashExtractor {
|
||||||
fn extract_hash(&self, stderr: &str) -> Option<String>;
|
fn extract_hash(&self, stderr: &str) -> Option<String>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct RegexHashExtractor;
|
pub struct RegexHashExtractor;
|
||||||
|
|
||||||
impl HashExtractor for RegexHashExtractor {
|
impl HashExtractor for RegexHashExtractor {
|
||||||
fn extract_hash(&self, stderr: &str) -> Option<String> {
|
fn extract_hash(&self, stderr: &str) -> Option<String> {
|
||||||
let patterns = [
|
let patterns = [
|
||||||
r"got:\s+(sha256-[a-zA-Z0-9+/=]+)",
|
r"got:\s+(sha256-[a-zA-Z0-9+/=]+)",
|
||||||
r"actual:\s+(sha256-[a-zA-Z0-9+/=]+)",
|
r"actual:\s+(sha256-[a-zA-Z0-9+/=]+)",
|
||||||
r"have:\s+(sha256-[a-zA-Z0-9+/=]+)",
|
r"have:\s+(sha256-[a-zA-Z0-9+/=]+)",
|
||||||
];
|
];
|
||||||
for pattern in &patterns {
|
for pattern in &patterns {
|
||||||
if let Ok(re) = Regex::new(pattern)
|
if let Ok(re) = Regex::new(pattern)
|
||||||
&& let Some(captures) = re.captures(stderr)
|
&& let Some(captures) = re.captures(stderr)
|
||||||
&& let Some(hash) = captures.get(1)
|
&& let Some(hash) = captures.get(1)
|
||||||
{
|
{
|
||||||
return Some(hash.as_str().to_string());
|
return Some(hash.as_str().to_string());
|
||||||
}
|
}
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait NixFileFixer {
|
pub trait NixFileFixer {
|
||||||
fn fix_hash_in_files(&self, new_hash: &str) -> Result<bool>;
|
fn fix_hash_in_files(&self, new_hash: &str) -> Result<bool>;
|
||||||
fn find_nix_files(&self) -> Result<Vec<PathBuf>>;
|
fn find_nix_files(&self) -> Result<Vec<PathBuf>>;
|
||||||
fn fix_hash_in_file(&self, file_path: &Path, new_hash: &str) -> Result<bool>;
|
fn fix_hash_in_file(&self, file_path: &Path, new_hash: &str) -> Result<bool>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct DefaultNixFileFixer;
|
pub struct DefaultNixFileFixer;
|
||||||
|
|
||||||
impl NixFileFixer for DefaultNixFileFixer {
|
impl NixFileFixer for DefaultNixFileFixer {
|
||||||
fn fix_hash_in_files(&self, new_hash: &str) -> Result<bool> {
|
fn fix_hash_in_files(&self, new_hash: &str) -> Result<bool> {
|
||||||
let nix_files = self.find_nix_files()?;
|
let nix_files = self.find_nix_files()?;
|
||||||
let mut fixed = false;
|
let mut fixed = false;
|
||||||
for file_path in nix_files {
|
for file_path in nix_files {
|
||||||
if self.fix_hash_in_file(&file_path, new_hash)? {
|
if self.fix_hash_in_file(&file_path, new_hash)? {
|
||||||
println!("Updated hash in {}", file_path.display());
|
println!("Updated hash in {}", file_path.display());
|
||||||
fixed = true;
|
fixed = true;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
Ok(fixed)
|
|
||||||
}
|
}
|
||||||
|
Ok(fixed)
|
||||||
|
}
|
||||||
|
|
||||||
fn find_nix_files(&self) -> Result<Vec<PathBuf>> {
|
fn find_nix_files(&self) -> Result<Vec<PathBuf>> {
|
||||||
let files: Vec<PathBuf> = WalkDir::new(".")
|
let files: Vec<PathBuf> = WalkDir::new(".")
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|entry| entry.ok())
|
.filter_map(|entry| entry.ok())
|
||||||
.filter(|entry| {
|
.filter(|entry| {
|
||||||
entry.file_type().is_file()
|
entry.file_type().is_file()
|
||||||
&& entry
|
&& entry
|
||||||
.path()
|
.path()
|
||||||
.extension()
|
.extension()
|
||||||
.map(|ext| ext.eq_ignore_ascii_case("nix"))
|
.map(|ext| ext.eq_ignore_ascii_case("nix"))
|
||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
})
|
})
|
||||||
.map(|entry| entry.path().to_path_buf())
|
.map(|entry| entry.path().to_path_buf())
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
if files.is_empty() {
|
if files.is_empty() {
|
||||||
Err(EhError::NoNixFilesFound)
|
return Err(EhError::NoNixFilesFound);
|
||||||
} else {
|
|
||||||
Ok(files)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
Ok(files)
|
||||||
|
}
|
||||||
|
|
||||||
fn fix_hash_in_file(&self, file_path: &Path, new_hash: &str) -> Result<bool> {
|
fn fix_hash_in_file(&self, file_path: &Path, new_hash: &str) -> Result<bool> {
|
||||||
let content = fs::read_to_string(file_path)?;
|
let content = fs::read_to_string(file_path)?;
|
||||||
let patterns = [
|
let patterns = [
|
||||||
(r#"hash\s*=\s*"[^"]*""#, format!(r#"hash = "{new_hash}""#)),
|
(r#"hash\s*=\s*"[^"]*""#, format!(r#"hash = "{new_hash}""#)),
|
||||||
(
|
(
|
||||||
r#"sha256\s*=\s*"[^"]*""#,
|
r#"sha256\s*=\s*"[^"]*""#,
|
||||||
format!(r#"sha256 = "{new_hash}""#),
|
format!(r#"sha256 = "{new_hash}""#),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
r#"outputHash\s*=\s*"[^"]*""#,
|
r#"outputHash\s*=\s*"[^"]*""#,
|
||||||
format!(r#"outputHash = "{new_hash}""#),
|
format!(r#"outputHash = "{new_hash}""#),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
let mut new_content = content;
|
let mut new_content = content;
|
||||||
let mut replaced = false;
|
let mut replaced = false;
|
||||||
for (pattern, replacement) in &patterns {
|
for (pattern, replacement) in &patterns {
|
||||||
let re = Regex::new(pattern)?;
|
let re = Regex::new(pattern)?;
|
||||||
if re.is_match(&new_content) {
|
if re.is_match(&new_content) {
|
||||||
new_content = re
|
new_content = re
|
||||||
.replace_all(&new_content, replacement.as_str())
|
.replace_all(&new_content, replacement.as_str())
|
||||||
.into_owned();
|
.into_owned();
|
||||||
replaced = true;
|
replaced = true;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if replaced {
|
|
||||||
fs::write(file_path, new_content).map_err(|_| EhError::HashFixFailed {
|
|
||||||
path: file_path.to_string_lossy().to_string(),
|
|
||||||
})?;
|
|
||||||
Ok(true)
|
|
||||||
} else {
|
|
||||||
Ok(false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if replaced {
|
||||||
|
fs::write(file_path, new_content).map_err(|_| {
|
||||||
|
EhError::HashFixFailed {
|
||||||
|
path: file_path.to_string_lossy().to_string(),
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
Ok(true)
|
||||||
|
} else {
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait NixErrorClassifier {
|
pub trait NixErrorClassifier {
|
||||||
fn should_retry(&self, stderr: &str) -> bool;
|
fn should_retry(&self, stderr: &str) -> bool;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Pre-evaluate expression to catch errors early
|
/// Pre-evaluate expression to catch errors early
|
||||||
fn pre_evaluate(_subcommand: &str, args: &[String]) -> Result<bool> {
|
fn pre_evaluate(_subcommand: &str, args: &[String]) -> Result<bool> {
|
||||||
// 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('-'));
|
||||||
|
|
||||||
let Some(eval_arg) = eval_arg else {
|
let Some(eval_arg) = eval_arg else {
|
||||||
return Ok(true); // No expression to evaluate
|
return Ok(true); // No expression to evaluate
|
||||||
};
|
};
|
||||||
|
|
||||||
let eval_cmd = NixCommand::new("eval").arg(eval_arg).arg("--raw");
|
let eval_cmd = NixCommand::new("eval").arg(eval_arg).arg("--raw");
|
||||||
|
|
||||||
let output = eval_cmd.output()?;
|
let output = eval_cmd.output()?;
|
||||||
|
|
||||||
if output.status.success() {
|
if output.status.success() {
|
||||||
return Ok(true);
|
return Ok(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
|
|
||||||
// If eval fails due to unfree/insecure/broken, don't fail pre-evaluation
|
// If eval fails due to unfree/insecure/broken, don't fail pre-evaluation
|
||||||
// Let the main command handle it with retry logic
|
// Let the main command handle it with retry logic
|
||||||
if stderr.contains("has an unfree license")
|
if stderr.contains("has an unfree license")
|
||||||
|| stderr.contains("refusing to evaluate")
|
|| stderr.contains("refusing to evaluate")
|
||||||
|| stderr.contains("has been marked as insecure")
|
|| stderr.contains("has been marked as insecure")
|
||||||
|| stderr.contains("has been marked as broken")
|
|| stderr.contains("has been marked as broken")
|
||||||
{
|
{
|
||||||
return Ok(true);
|
return Ok(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// For other eval failures, fail early
|
// For other eval failures, fail early
|
||||||
Ok(false)
|
Ok(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shared retry logic for nix commands (build/run/shell).
|
/// Shared retry logic for nix commands (build/run/shell).
|
||||||
pub fn handle_nix_with_retry(
|
pub fn handle_nix_with_retry(
|
||||||
subcommand: &str,
|
subcommand: &str,
|
||||||
args: &[String],
|
args: &[String],
|
||||||
hash_extractor: &dyn HashExtractor,
|
hash_extractor: &dyn HashExtractor,
|
||||||
fixer: &dyn NixFileFixer,
|
fixer: &dyn NixFileFixer,
|
||||||
classifier: &dyn NixErrorClassifier,
|
classifier: &dyn NixErrorClassifier,
|
||||||
interactive: bool,
|
interactive: bool,
|
||||||
) -> Result<i32> {
|
) -> Result<i32> {
|
||||||
// Pre-evaluate for build commands to catch errors early
|
// Pre-evaluate for build commands to catch errors early
|
||||||
if !pre_evaluate(subcommand, args)? {
|
if !pre_evaluate(subcommand, args)? {
|
||||||
return Err(EhError::NixCommandFailed(
|
return Err(EhError::NixCommandFailed(
|
||||||
"Expression evaluation failed".to_string(),
|
"Expression evaluation failed".to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// For run commands, try interactive first to avoid breaking terminal
|
// For run commands, try interactive first to avoid breaking terminal
|
||||||
if subcommand == "run" && interactive {
|
if subcommand == "run" && interactive {
|
||||||
let mut cmd = NixCommand::new(subcommand)
|
let status = NixCommand::new(subcommand)
|
||||||
.print_build_logs(true)
|
.print_build_logs(true)
|
||||||
.interactive(true);
|
.interactive(true)
|
||||||
for arg in args {
|
.args_ref(args)
|
||||||
cmd = cmd.arg(arg);
|
.run_with_logs(StdIoInterceptor)?;
|
||||||
}
|
if status.success() {
|
||||||
let status = cmd.run_with_logs(StdIoInterceptor)?;
|
return Ok(0);
|
||||||
if status.success() {
|
|
||||||
return Ok(0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// First, always capture output to check for errors that need retry
|
// First, always capture output to check for errors that need retry
|
||||||
let output_cmd = NixCommand::new(subcommand)
|
let output_cmd = NixCommand::new(subcommand)
|
||||||
|
.print_build_logs(true)
|
||||||
|
.args_ref(args);
|
||||||
|
let output = output_cmd.output()?;
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
|
|
||||||
|
// Check if we need to retry with special flags
|
||||||
|
if let Some(new_hash) = hash_extractor.extract_hash(&stderr) {
|
||||||
|
match fixer.fix_hash_in_files(&new_hash) {
|
||||||
|
Ok(true) => {
|
||||||
|
info!("{}", Paint::green("✔ Fixed hash mismatch, retrying..."));
|
||||||
|
let mut retry_cmd = NixCommand::new(subcommand)
|
||||||
|
.print_build_logs(true)
|
||||||
|
.args_ref(args);
|
||||||
|
if interactive {
|
||||||
|
retry_cmd = retry_cmd.interactive(true);
|
||||||
|
}
|
||||||
|
let retry_status = retry_cmd.run_with_logs(StdIoInterceptor)?;
|
||||||
|
return Ok(retry_status.code().unwrap_or(1));
|
||||||
|
},
|
||||||
|
Ok(false) => {
|
||||||
|
// No files were fixed, continue with normal error handling
|
||||||
|
},
|
||||||
|
Err(EhError::NoNixFilesFound) => {
|
||||||
|
warn!("No .nix files found to fix hash in");
|
||||||
|
// Continue with normal error handling
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
return Err(e);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else if stderr.contains("hash") || stderr.contains("sha256") {
|
||||||
|
// If there's a hash-related error but we couldn't extract it, that's a
|
||||||
|
// failure
|
||||||
|
return Err(EhError::HashExtractionFailed);
|
||||||
|
}
|
||||||
|
|
||||||
|
if classifier.should_retry(&stderr) {
|
||||||
|
if stderr.contains("has an unfree license") && stderr.contains("refusing") {
|
||||||
|
warn!(
|
||||||
|
"{}",
|
||||||
|
Paint::yellow(
|
||||||
|
"⚠ Unfree package detected, retrying with NIXPKGS_ALLOW_UNFREE=1..."
|
||||||
|
)
|
||||||
|
);
|
||||||
|
let mut retry_cmd = NixCommand::new(subcommand)
|
||||||
.print_build_logs(true)
|
.print_build_logs(true)
|
||||||
.args(args.iter().cloned());
|
.args_ref(args)
|
||||||
let output = output_cmd.output()?;
|
.env("NIXPKGS_ALLOW_UNFREE", "1")
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
.impure(true);
|
||||||
|
if interactive {
|
||||||
// Check if we need to retry with special flags
|
retry_cmd = retry_cmd.interactive(true);
|
||||||
if let Some(new_hash) = hash_extractor.extract_hash(&stderr) {
|
}
|
||||||
match fixer.fix_hash_in_files(&new_hash) {
|
let retry_status = retry_cmd.run_with_logs(StdIoInterceptor)?;
|
||||||
Ok(true) => {
|
return Ok(retry_status.code().unwrap_or(1));
|
||||||
info!("{}", Paint::green("✔ Fixed hash mismatch, retrying..."));
|
|
||||||
let mut retry_cmd = NixCommand::new(subcommand)
|
|
||||||
.print_build_logs(true)
|
|
||||||
.args(args.iter().cloned());
|
|
||||||
if interactive {
|
|
||||||
retry_cmd = retry_cmd.interactive(true);
|
|
||||||
}
|
|
||||||
let retry_status = retry_cmd.run_with_logs(StdIoInterceptor)?;
|
|
||||||
return Ok(retry_status.code().unwrap_or(1));
|
|
||||||
}
|
|
||||||
Ok(false) => {
|
|
||||||
// No files were fixed, continue with normal error handling
|
|
||||||
}
|
|
||||||
Err(EhError::NoNixFilesFound) => {
|
|
||||||
warn!("No .nix files found to fix hash in");
|
|
||||||
// Continue with normal error handling
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
return Err(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if stderr.contains("hash") || stderr.contains("sha256") {
|
|
||||||
// If there's a hash-related error but we couldn't extract it, that's a failure
|
|
||||||
return Err(EhError::HashExtractionFailed);
|
|
||||||
}
|
}
|
||||||
|
if stderr.contains("has been marked as insecure")
|
||||||
if classifier.should_retry(&stderr) {
|
&& stderr.contains("refusing")
|
||||||
if stderr.contains("has an unfree license") && stderr.contains("refusing") {
|
{
|
||||||
warn!(
|
warn!(
|
||||||
"{}",
|
"{}",
|
||||||
Paint::yellow("⚠ Unfree package detected, retrying with NIXPKGS_ALLOW_UNFREE=1...")
|
Paint::yellow(
|
||||||
);
|
"⚠ Insecure package detected, retrying with \
|
||||||
let mut retry_cmd = NixCommand::new(subcommand)
|
NIXPKGS_ALLOW_INSECURE=1..."
|
||||||
.print_build_logs(true)
|
)
|
||||||
.args(args.iter().cloned())
|
);
|
||||||
.env("NIXPKGS_ALLOW_UNFREE", "1")
|
let mut retry_cmd = NixCommand::new(subcommand)
|
||||||
.impure(true);
|
.print_build_logs(true)
|
||||||
if interactive {
|
.args_ref(args)
|
||||||
retry_cmd = retry_cmd.interactive(true);
|
.env("NIXPKGS_ALLOW_INSECURE", "1")
|
||||||
}
|
.impure(true);
|
||||||
let retry_status = retry_cmd.run_with_logs(StdIoInterceptor)?;
|
if interactive {
|
||||||
return Ok(retry_status.code().unwrap_or(1));
|
retry_cmd = retry_cmd.interactive(true);
|
||||||
}
|
}
|
||||||
if stderr.contains("has been marked as insecure") && stderr.contains("refusing") {
|
let retry_status = retry_cmd.run_with_logs(StdIoInterceptor)?;
|
||||||
warn!(
|
return Ok(retry_status.code().unwrap_or(1));
|
||||||
"{}",
|
|
||||||
Paint::yellow(
|
|
||||||
"⚠ Insecure package detected, retrying with NIXPKGS_ALLOW_INSECURE=1..."
|
|
||||||
)
|
|
||||||
);
|
|
||||||
let mut retry_cmd = NixCommand::new(subcommand)
|
|
||||||
.print_build_logs(true)
|
|
||||||
.args(args.iter().cloned())
|
|
||||||
.env("NIXPKGS_ALLOW_INSECURE", "1")
|
|
||||||
.impure(true);
|
|
||||||
if interactive {
|
|
||||||
retry_cmd = retry_cmd.interactive(true);
|
|
||||||
}
|
|
||||||
let retry_status = retry_cmd.run_with_logs(StdIoInterceptor)?;
|
|
||||||
return Ok(retry_status.code().unwrap_or(1));
|
|
||||||
}
|
|
||||||
if stderr.contains("has been marked as broken") && stderr.contains("refusing") {
|
|
||||||
warn!(
|
|
||||||
"{}",
|
|
||||||
Paint::yellow("⚠ Broken package detected, retrying with NIXPKGS_ALLOW_BROKEN=1...")
|
|
||||||
);
|
|
||||||
let mut retry_cmd = NixCommand::new(subcommand)
|
|
||||||
.print_build_logs(true)
|
|
||||||
.args(args.iter().cloned())
|
|
||||||
.env("NIXPKGS_ALLOW_BROKEN", "1")
|
|
||||||
.impure(true);
|
|
||||||
if interactive {
|
|
||||||
retry_cmd = retry_cmd.interactive(true);
|
|
||||||
}
|
|
||||||
let retry_status = retry_cmd.run_with_logs(StdIoInterceptor)?;
|
|
||||||
return Ok(retry_status.code().unwrap_or(1));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if stderr.contains("has been marked as broken")
|
||||||
// If the first attempt succeeded, we're done
|
&& stderr.contains("refusing")
|
||||||
if output.status.success() {
|
{
|
||||||
return Ok(0);
|
warn!(
|
||||||
|
"{}",
|
||||||
|
Paint::yellow(
|
||||||
|
"⚠ Broken package detected, retrying with NIXPKGS_ALLOW_BROKEN=1..."
|
||||||
|
)
|
||||||
|
);
|
||||||
|
let mut retry_cmd = NixCommand::new(subcommand)
|
||||||
|
.print_build_logs(true)
|
||||||
|
.args_ref(args)
|
||||||
|
.env("NIXPKGS_ALLOW_BROKEN", "1")
|
||||||
|
.impure(true);
|
||||||
|
if interactive {
|
||||||
|
retry_cmd = retry_cmd.interactive(true);
|
||||||
|
}
|
||||||
|
let retry_status = retry_cmd.run_with_logs(StdIoInterceptor)?;
|
||||||
|
return Ok(retry_status.code().unwrap_or(1));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Otherwise, show the error and return error
|
// If the first attempt succeeded, we're done
|
||||||
std::io::stderr().write_all(&output.stderr)?;
|
if output.status.success() {
|
||||||
Err(EhError::ProcessExit {
|
return Ok(0);
|
||||||
code: output.status.code().unwrap_or(1),
|
}
|
||||||
})
|
|
||||||
|
// Otherwise, show the error and return error
|
||||||
|
std::io::stderr()
|
||||||
|
.write_all(&output.stderr)
|
||||||
|
.map_err(EhError::Io)?;
|
||||||
|
Err(EhError::ProcessExit {
|
||||||
|
code: output.status.code().unwrap_or(1),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
pub struct DefaultNixErrorClassifier;
|
pub struct DefaultNixErrorClassifier;
|
||||||
|
|
||||||
impl NixErrorClassifier for DefaultNixErrorClassifier {
|
impl NixErrorClassifier for DefaultNixErrorClassifier {
|
||||||
fn should_retry(&self, stderr: &str) -> bool {
|
fn should_retry(&self, stderr: &str) -> bool {
|
||||||
RegexHashExtractor.extract_hash(stderr).is_some()
|
RegexHashExtractor.extract_hash(stderr).is_some()
|
||||||
|| (stderr.contains("has an unfree license") && stderr.contains("refusing"))
|
|| (stderr.contains("has an unfree license")
|
||||||
|| (stderr.contains("has been marked as insecure") && stderr.contains("refusing"))
|
&& stderr.contains("refusing"))
|
||||||
|| (stderr.contains("has been marked as broken") && stderr.contains("refusing"))
|
|| (stderr.contains("has been marked as insecure")
|
||||||
}
|
&& stderr.contains("refusing"))
|
||||||
|
|| (stderr.contains("has been marked as broken")
|
||||||
|
&& stderr.contains("refusing"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,178 +1,234 @@
|
||||||
//! I hate writing tests, and I hate writing integration tests. This is the best
|
use std::{fs, process::Command};
|
||||||
//! that you are getting, deal with it.
|
|
||||||
use std::process::{Command, Stdio};
|
use eh::util::{
|
||||||
|
DefaultNixErrorClassifier,
|
||||||
|
DefaultNixFileFixer,
|
||||||
|
HashExtractor,
|
||||||
|
NixErrorClassifier,
|
||||||
|
NixFileFixer,
|
||||||
|
RegexHashExtractor,
|
||||||
|
};
|
||||||
|
use tempfile::TempDir;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn nix_eval_validation() {
|
fn test_hash_extraction_from_real_nix_errors() {
|
||||||
// Test that invalid expressions are caught early for all commands
|
// Test hash extraction from actual Nix error messages
|
||||||
let commands = ["build", "run", "shell"];
|
let extractor = RegexHashExtractor;
|
||||||
|
|
||||||
for cmd in &commands {
|
let test_cases = [
|
||||||
let output = Command::new("timeout")
|
(
|
||||||
.args([
|
r#"error: hash mismatch in fixed-output derivation '/nix/store/xxx-foo.drv':
|
||||||
"10",
|
specified: sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
|
||||||
"cargo",
|
got: sha256-BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB="#,
|
||||||
"run",
|
Some("sha256-BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=".to_string()),
|
||||||
"--bin",
|
),
|
||||||
"eh",
|
(
|
||||||
"--",
|
"actual: sha256-abc123def456",
|
||||||
cmd,
|
Some("sha256-abc123def456".to_string()),
|
||||||
"invalid-flake-ref",
|
),
|
||||||
])
|
("have: sha256-xyz789", Some("sha256-xyz789".to_string())),
|
||||||
.output()
|
("no hash here", None),
|
||||||
.expect("Failed to execute command");
|
];
|
||||||
|
|
||||||
// Should fail fast with eval error
|
for (input, expected) in test_cases {
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
assert_eq!(extractor.extract_hash(input), expected);
|
||||||
assert!(stderr.contains("Error: Expression evaluation failed") || !output.status.success());
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn unfree_package_handling() {
|
fn test_error_classification_for_retry_logic() {
|
||||||
// Test that unfree packages are detected and handled correctly
|
// Test that the classifier correctly identifies errors that should be retried
|
||||||
let output = Command::new("timeout")
|
let classifier = DefaultNixErrorClassifier;
|
||||||
.args([
|
|
||||||
"30",
|
|
||||||
"cargo",
|
|
||||||
"run",
|
|
||||||
"--bin",
|
|
||||||
"eh",
|
|
||||||
"--",
|
|
||||||
"build",
|
|
||||||
"nixpkgs#discord",
|
|
||||||
])
|
|
||||||
.output()
|
|
||||||
.expect("Failed to execute command");
|
|
||||||
|
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
// These should trigger retries
|
||||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
let retry_cases = [
|
||||||
let combined = format!("{}{}", stdout, stderr);
|
"Package 'discord-1.0.0' has an unfree license ('unfree'), refusing to \
|
||||||
|
evaluate.",
|
||||||
|
"Package 'openssl-1.1.1' has been marked as insecure, refusing to \
|
||||||
|
evaluate.",
|
||||||
|
"Package 'broken-1.0' has been marked as broken, refusing to evaluate.",
|
||||||
|
"hash mismatch in fixed-output derivation\ngot: sha256-newhash",
|
||||||
|
];
|
||||||
|
|
||||||
// Should detect unfree package and show appropriate message
|
for error in retry_cases {
|
||||||
|
assert!(classifier.should_retry(error), "Should retry: {}", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// These should NOT trigger retries
|
||||||
|
let no_retry_cases = [
|
||||||
|
"build failed",
|
||||||
|
"random error",
|
||||||
|
"permission denied",
|
||||||
|
"network error",
|
||||||
|
];
|
||||||
|
|
||||||
|
for error in no_retry_cases {
|
||||||
assert!(
|
assert!(
|
||||||
combined.contains("has an unfree license")
|
!classifier.should_retry(error),
|
||||||
|| combined.contains("NIXPKGS_ALLOW_UNFREE")
|
"Should not retry: {}",
|
||||||
|| combined.contains("⚠ Unfree package detected")
|
error
|
||||||
);
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn insecure_package_handling() {
|
fn test_hash_fixing_in_nix_files() {
|
||||||
// Test that error classification works for insecure packages
|
// Test that hash fixing actually works on real Nix files
|
||||||
use eh::util::{DefaultNixErrorClassifier, NixErrorClassifier};
|
let temp_dir = TempDir::new().expect("Failed to create temp dir");
|
||||||
|
let fixer = DefaultNixFileFixer;
|
||||||
|
|
||||||
let classifier = DefaultNixErrorClassifier;
|
// Create a mock Nix file with various hash formats
|
||||||
let stderr_insecure =
|
let nix_content = r#"
|
||||||
"Package 'example-1.0' has been marked as insecure, refusing to evaluate.";
|
stdenv.mkDerivation {
|
||||||
|
name = "test-package";
|
||||||
|
src = fetchurl {
|
||||||
|
url = "https://example.com.tar.gz";
|
||||||
|
hash = "sha256-oldhash123";
|
||||||
|
};
|
||||||
|
|
||||||
assert!(classifier.should_retry(stderr_insecure));
|
buildInputs = [ fetchurl {
|
||||||
|
url = "https://deps.com.tar.gz";
|
||||||
|
sha256 = "sha256-oldhash456";
|
||||||
|
}];
|
||||||
|
|
||||||
|
outputHash = "sha256-oldhash789";
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn broken_package_handling() {
|
|
||||||
// Test that error classification works for broken packages
|
|
||||||
use eh::util::{DefaultNixErrorClassifier, NixErrorClassifier};
|
|
||||||
|
|
||||||
let classifier = DefaultNixErrorClassifier;
|
|
||||||
let stderr_broken = "Package 'example-1.0' has been marked as broken, refusing to evaluate.";
|
|
||||||
|
|
||||||
assert!(classifier.should_retry(stderr_broken));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn multicall_binary_dispatch() {
|
|
||||||
// Test that nb/nr/ns dispatch correctly based on binary name
|
|
||||||
let commands = [("nb", "build"), ("nr", "run"), ("ns", "shell")];
|
|
||||||
|
|
||||||
for (binary_name, _expected_cmd) in &commands {
|
|
||||||
let output = Command::new("timeout")
|
|
||||||
.args(["10", "cargo", "run", "--bin", "eh"])
|
|
||||||
.env("CARGO_BIN_NAME", binary_name)
|
|
||||||
.arg("nixpkgs#hello")
|
|
||||||
.arg("--help") // Use help to avoid actually building
|
|
||||||
.output()
|
|
||||||
.expect("Failed to execute command");
|
|
||||||
|
|
||||||
// Should execute without panicking (status code may vary)
|
|
||||||
assert!(output.status.code().is_some());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn interactive_mode_inheritance() {
|
|
||||||
// Test that run commands inherit stdio properly
|
|
||||||
let mut child = Command::new("timeout")
|
|
||||||
.args([
|
|
||||||
"10",
|
|
||||||
"cargo",
|
|
||||||
"run",
|
|
||||||
"--bin",
|
|
||||||
"eh",
|
|
||||||
"--",
|
|
||||||
"run",
|
|
||||||
"nixpkgs#echo",
|
|
||||||
"test",
|
|
||||||
])
|
|
||||||
.stdin(Stdio::piped())
|
|
||||||
.stdout(Stdio::piped())
|
|
||||||
.stderr(Stdio::piped())
|
|
||||||
.spawn()
|
|
||||||
.expect("Failed to spawn command");
|
|
||||||
|
|
||||||
let status = child.wait().expect("Failed to wait for child");
|
|
||||||
|
|
||||||
// Should complete without hanging
|
|
||||||
assert!(status.code().is_some());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn hash_extraction() {
|
|
||||||
use eh::util::{HashExtractor, RegexHashExtractor};
|
|
||||||
|
|
||||||
let extractor = RegexHashExtractor;
|
|
||||||
let stderr = "error: hash mismatch in fixed-output derivation '/nix/store/...':
|
|
||||||
specified: sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
|
|
||||||
got: sha256-BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=";
|
|
||||||
|
|
||||||
let hash = extractor.extract_hash(stderr);
|
|
||||||
assert!(hash.is_some());
|
|
||||||
assert_eq!(
|
|
||||||
hash.unwrap(),
|
|
||||||
"sha256-BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB="
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn error_classification() {
|
|
||||||
use eh::util::{DefaultNixErrorClassifier, NixErrorClassifier};
|
|
||||||
|
|
||||||
let classifier = DefaultNixErrorClassifier;
|
|
||||||
|
|
||||||
assert!(classifier.should_retry("has an unfree license ('unfree'), refusing to evaluate"));
|
|
||||||
assert!(classifier.should_retry("has been marked as insecure, refusing to evaluate"));
|
|
||||||
assert!(classifier.should_retry("has been marked as broken, refusing to evaluate"));
|
|
||||||
assert!(!classifier.should_retry("random build error"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn hash_mismatch_auto_fix() {
|
|
||||||
// Test that hash mismatches are automatically detected and fixed
|
|
||||||
// This is harder to test without creating actual files, so we test the regex
|
|
||||||
// for the time being. Alternatively I could do this inside a temporary directory
|
|
||||||
// but cba for now.
|
|
||||||
use eh::util::{HashExtractor, RegexHashExtractor};
|
|
||||||
|
|
||||||
let extractor = RegexHashExtractor;
|
|
||||||
let stderr_with_mismatch = r#"
|
|
||||||
error: hash mismatch in fixed-output derivation
|
|
||||||
specified: sha256-oldhashaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=
|
|
||||||
got: sha256-newhashbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb=
|
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
let extracted = extractor.extract_hash(stderr_with_mismatch);
|
let file_path = temp_dir.path().join("test.nix");
|
||||||
assert_eq!(
|
fs::write(&file_path, nix_content).expect("Failed to write test file");
|
||||||
extracted,
|
|
||||||
Some("sha256-newhashbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb=".to_string())
|
// Test hash replacement
|
||||||
);
|
let new_hash = "sha256-newhashabc";
|
||||||
|
let was_fixed = fixer
|
||||||
|
.fix_hash_in_file(&file_path, new_hash)
|
||||||
|
.expect("Failed to fix hash");
|
||||||
|
|
||||||
|
assert!(was_fixed, "File should have been modified");
|
||||||
|
|
||||||
|
let updated_content =
|
||||||
|
fs::read_to_string(&file_path).expect("Failed to read updated file");
|
||||||
|
|
||||||
|
// All hash formats should be updated
|
||||||
|
assert!(updated_content.contains(&format!(r#"hash = "{}""#, new_hash)));
|
||||||
|
assert!(updated_content.contains(&format!(r#"sha256 = "{}""#, new_hash)));
|
||||||
|
assert!(updated_content.contains(&format!(r#"outputHash = "{}""#, new_hash)));
|
||||||
|
|
||||||
|
// Old hashes should be gone
|
||||||
|
assert!(!updated_content.contains("oldhash123"));
|
||||||
|
assert!(!updated_content.contains("oldhash456"));
|
||||||
|
assert!(!updated_content.contains("oldhash789"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_multicall_binary_dispatch() {
|
||||||
|
// Test that multicall binaries work without needing actual Nix evaluation
|
||||||
|
let commands = [("nb", "build"), ("nr", "run"), ("ns", "shell")];
|
||||||
|
|
||||||
|
for (binary_name, _expected_command) in &commands {
|
||||||
|
// Test that the binary starts and handles invalid arguments gracefully
|
||||||
|
let output = Command::new("timeout")
|
||||||
|
.args(["5", "cargo", "run", "--bin", "eh", "--"])
|
||||||
|
.env("CARGO_BIN_NAME", binary_name)
|
||||||
|
.arg("invalid-package-ref")
|
||||||
|
.output()
|
||||||
|
.expect("Failed to execute command");
|
||||||
|
|
||||||
|
// Should fail gracefully (not panic or hang)
|
||||||
|
assert!(
|
||||||
|
output.status.code().is_some(),
|
||||||
|
"{} should exit with a code",
|
||||||
|
binary_name
|
||||||
|
);
|
||||||
|
|
||||||
|
// Should show an error message, not crash
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
|
assert!(
|
||||||
|
stderr.contains("Error:")
|
||||||
|
|| stderr.contains("error:")
|
||||||
|
|| stderr.contains("failed"),
|
||||||
|
"{} should show error for invalid package",
|
||||||
|
binary_name
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_expression_handling() {
|
||||||
|
// Test that invalid Nix expressions fail fast with proper error messages
|
||||||
|
let invalid_refs = [
|
||||||
|
"invalid-flake-ref",
|
||||||
|
"nonexistent-package",
|
||||||
|
"file:///nonexistent/path",
|
||||||
|
];
|
||||||
|
|
||||||
|
for invalid_ref in invalid_refs {
|
||||||
|
let output = Command::new("timeout")
|
||||||
|
.args([
|
||||||
|
"10",
|
||||||
|
"cargo",
|
||||||
|
"run",
|
||||||
|
"--bin",
|
||||||
|
"eh",
|
||||||
|
"--",
|
||||||
|
"build",
|
||||||
|
invalid_ref,
|
||||||
|
])
|
||||||
|
.output()
|
||||||
|
.expect("Failed to execute command");
|
||||||
|
|
||||||
|
// Should fail with a proper error, not hang or crash
|
||||||
|
assert!(
|
||||||
|
!output.status.success(),
|
||||||
|
"Invalid ref '{}' should fail",
|
||||||
|
invalid_ref
|
||||||
|
);
|
||||||
|
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
|
assert!(
|
||||||
|
stderr.contains("Error:")
|
||||||
|
|| stderr.contains("error:")
|
||||||
|
|| stderr.contains("failed"),
|
||||||
|
"Should show error message for invalid ref '{}': {}",
|
||||||
|
invalid_ref,
|
||||||
|
stderr
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_nix_file_discovery() {
|
||||||
|
// Test that the fixer can find Nix files in a directory structure
|
||||||
|
let temp_dir = TempDir::new().expect("Failed to create temp dir");
|
||||||
|
let fixer = DefaultNixFileFixer;
|
||||||
|
|
||||||
|
// Create directory structure with Nix files
|
||||||
|
fs::create_dir_all(temp_dir.path().join("subdir"))
|
||||||
|
.expect("Failed to create subdir");
|
||||||
|
|
||||||
|
let files = [
|
||||||
|
("test.nix", "stdenv.mkDerivation { name = \"test\"; }"),
|
||||||
|
("subdir/other.nix", "pkgs.hello"),
|
||||||
|
("not-nix.txt", "not a nix file"),
|
||||||
|
("default.nix", "import ./test.nix"),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (path, content) in files {
|
||||||
|
fs::write(temp_dir.path().join(path), content)
|
||||||
|
.expect("Failed to write file");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Change to temp dir for file discovery
|
||||||
|
let original_dir =
|
||||||
|
std::env::current_dir().expect("Failed to get current dir");
|
||||||
|
std::env::set_current_dir(temp_dir.path())
|
||||||
|
.expect("Failed to change directory");
|
||||||
|
|
||||||
|
let found_files = fixer.find_nix_files().expect("Failed to find Nix files");
|
||||||
|
|
||||||
|
// Should find 3 .nix files (not the .txt file)
|
||||||
|
assert_eq!(found_files.len(), 3, "Should find exactly 3 .nix files");
|
||||||
|
|
||||||
|
// Restore original directory
|
||||||
|
std::env::set_current_dir(original_dir).expect("Failed to restore directory");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,26 @@
|
||||||
{
|
{
|
||||||
mkShell,
|
mkShell,
|
||||||
rust-analyzer,
|
rustc,
|
||||||
|
cargo,
|
||||||
rustfmt,
|
rustfmt,
|
||||||
clippy,
|
clippy,
|
||||||
cargo,
|
|
||||||
taplo,
|
taplo,
|
||||||
|
rust-analyzer-unwrapped,
|
||||||
rustPlatform,
|
rustPlatform,
|
||||||
}:
|
}:
|
||||||
mkShell {
|
mkShell {
|
||||||
name = "rust";
|
name = "rust";
|
||||||
|
|
||||||
packages = [
|
packages = [
|
||||||
rust-analyzer
|
rustc
|
||||||
rustfmt
|
|
||||||
clippy
|
|
||||||
cargo
|
cargo
|
||||||
|
|
||||||
|
(rustfmt.override {asNightly = true;})
|
||||||
|
clippy
|
||||||
|
cargo
|
||||||
taplo
|
taplo
|
||||||
|
rust-analyzer-unwrapped
|
||||||
];
|
];
|
||||||
|
|
||||||
RUST_SRC_PATH = "${rustPlatform.rustLibSrc}";
|
env.RUST_SRC_PATH = "${rustPlatform.rustLibSrc}";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
use std::{
|
use std::{
|
||||||
error, fs,
|
error,
|
||||||
path::{Path, PathBuf},
|
fs,
|
||||||
process,
|
path::{Path, PathBuf},
|
||||||
|
process,
|
||||||
};
|
};
|
||||||
|
|
||||||
use clap::{CommandFactory, Parser};
|
use clap::{CommandFactory, Parser};
|
||||||
|
|
@ -9,167 +10,169 @@ use clap_complete::{Shell, generate};
|
||||||
|
|
||||||
#[derive(clap::Parser)]
|
#[derive(clap::Parser)]
|
||||||
struct Cli {
|
struct Cli {
|
||||||
#[clap(subcommand)]
|
#[clap(subcommand)]
|
||||||
command: Command,
|
command: Command,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(clap::Subcommand)]
|
#[derive(clap::Subcommand)]
|
||||||
enum Command {
|
enum Command {
|
||||||
/// Create multicall binaries (hardlinks or copies).
|
/// Create multicall binaries (hardlinks or copies).
|
||||||
Multicall {
|
Multicall {
|
||||||
/// Directory to install multicall binaries.
|
/// Directory to install multicall binaries.
|
||||||
#[arg(long, default_value = "bin")]
|
#[arg(long, default_value = "bin")]
|
||||||
bin_dir: PathBuf,
|
bin_dir: PathBuf,
|
||||||
|
|
||||||
/// Path to the main binary.
|
/// Path to the main binary.
|
||||||
#[arg(long, default_value = "target/release/eh")]
|
#[arg(long, default_value = "target/release/eh")]
|
||||||
main_binary: PathBuf,
|
main_binary: PathBuf,
|
||||||
},
|
},
|
||||||
/// Generate shell completion scripts
|
/// Generate shell completion scripts
|
||||||
Completions {
|
Completions {
|
||||||
/// Shell to generate completions for
|
/// Shell to generate completions for
|
||||||
#[arg(value_enum)]
|
#[arg(value_enum)]
|
||||||
shell: Shell,
|
shell: Shell,
|
||||||
/// Directory to output completion files
|
/// Directory to output completion files
|
||||||
#[arg(long, default_value = "completions")]
|
#[arg(long, default_value = "completions")]
|
||||||
output_dir: PathBuf,
|
output_dir: PathBuf,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
enum Binary {
|
enum Binary {
|
||||||
Nr,
|
Nr,
|
||||||
Ns,
|
Ns,
|
||||||
Nb,
|
Nb,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Binary {
|
impl Binary {
|
||||||
const fn name(self) -> &'static str {
|
const fn name(self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Self::Nr => "nr",
|
Self::Nr => "nr",
|
||||||
Self::Ns => "ns",
|
Self::Ns => "ns",
|
||||||
Self::Nb => "nb",
|
Self::Nb => "nb",
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
|
|
||||||
match cli.command {
|
match cli.command {
|
||||||
Command::Multicall {
|
Command::Multicall {
|
||||||
bin_dir,
|
bin_dir,
|
||||||
main_binary,
|
main_binary,
|
||||||
} => {
|
} => {
|
||||||
if let Err(error) = create_multicall_binaries(&bin_dir, &main_binary) {
|
if let Err(error) = create_multicall_binaries(&bin_dir, &main_binary) {
|
||||||
eprintln!("error creating multicall binaries: {error}");
|
eprintln!("error creating multicall binaries: {error}");
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
Command::Completions { shell, output_dir } => {
|
Command::Completions { shell, output_dir } => {
|
||||||
if let Err(error) = generate_completions(shell, &output_dir) {
|
if let Err(error) = generate_completions(shell, &output_dir) {
|
||||||
eprintln!("error generating completions: {error}");
|
eprintln!("error generating completions: {error}");
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_multicall_binaries(
|
fn create_multicall_binaries(
|
||||||
bin_dir: &Path,
|
bin_dir: &Path,
|
||||||
main_binary: &Path,
|
main_binary: &Path,
|
||||||
) -> Result<(), Box<dyn error::Error>> {
|
) -> Result<(), Box<dyn error::Error>> {
|
||||||
println!("creating multicall binaries...");
|
println!("creating multicall binaries...");
|
||||||
|
|
||||||
fs::create_dir_all(bin_dir)?;
|
fs::create_dir_all(bin_dir)?;
|
||||||
|
|
||||||
if !main_binary.exists() {
|
if !main_binary.exists() {
|
||||||
return Err(format!("main binary not found at: {}", main_binary.display()).into());
|
return Err(
|
||||||
|
format!("main binary not found at: {}", main_binary.display()).into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let multicall_binaries = [Binary::Nr, Binary::Ns, Binary::Nb];
|
||||||
|
let bin_path = Path::new(bin_dir);
|
||||||
|
|
||||||
|
for binary in multicall_binaries {
|
||||||
|
let target_path = bin_path.join(binary.name());
|
||||||
|
|
||||||
|
if target_path.exists() {
|
||||||
|
fs::remove_file(&target_path)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let multicall_binaries = [Binary::Nr, Binary::Ns, Binary::Nb];
|
if let Err(e) = fs::hard_link(main_binary, &target_path) {
|
||||||
let bin_path = Path::new(bin_dir);
|
eprintln!(
|
||||||
|
" warning: could not create hardlink for {}: {e}",
|
||||||
|
binary.name(),
|
||||||
|
);
|
||||||
|
eprintln!(" warning: falling back to copying binary...");
|
||||||
|
|
||||||
for binary in multicall_binaries {
|
fs::copy(main_binary, &target_path)?;
|
||||||
let target_path = bin_path.join(binary.name());
|
|
||||||
|
|
||||||
if target_path.exists() {
|
#[cfg(unix)]
|
||||||
fs::remove_file(&target_path)?;
|
{
|
||||||
}
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
let mut perms = fs::metadata(&target_path)?.permissions();
|
||||||
|
perms.set_mode(perms.mode() | 0o755);
|
||||||
|
fs::set_permissions(&target_path, perms)?;
|
||||||
|
}
|
||||||
|
|
||||||
match fs::hard_link(main_binary, &target_path) {
|
println!(" created copy: {}", target_path.display());
|
||||||
Ok(()) => {
|
} else {
|
||||||
println!(
|
println!(
|
||||||
" created hardlink: {} points to {}",
|
" created hardlink: {} points to {}",
|
||||||
target_path.display(),
|
target_path.display(),
|
||||||
main_binary.display(),
|
main_binary.display(),
|
||||||
);
|
);
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!(
|
|
||||||
" warning: could not create hardlink for {}: {e}",
|
|
||||||
binary.name(),
|
|
||||||
);
|
|
||||||
eprintln!(" warning: falling back to copying binary...");
|
|
||||||
|
|
||||||
fs::copy(main_binary, &target_path)?;
|
|
||||||
|
|
||||||
#[cfg(unix)]
|
|
||||||
{
|
|
||||||
use std::os::unix::fs::PermissionsExt;
|
|
||||||
let mut perms = fs::metadata(&target_path)?.permissions();
|
|
||||||
perms.set_mode(perms.mode() | 0o755);
|
|
||||||
fs::set_permissions(&target_path, perms)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
println!(" created copy: {}", target_path.display());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
println!("multicall binaries created successfully!");
|
println!("multicall binaries created successfully!");
|
||||||
println!("multicall binaries are in: {}", bin_dir.display());
|
println!("multicall binaries are in: {}", bin_dir.display());
|
||||||
println!();
|
println!();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn generate_completions(shell: Shell, output_dir: &Path) -> Result<(), Box<dyn error::Error>> {
|
fn generate_completions(
|
||||||
println!("generating {shell} completions...");
|
shell: Shell,
|
||||||
|
output_dir: &Path,
|
||||||
|
) -> Result<(), Box<dyn error::Error>> {
|
||||||
|
println!("generating {shell} completions...");
|
||||||
|
|
||||||
fs::create_dir_all(output_dir)?;
|
fs::create_dir_all(output_dir)?;
|
||||||
|
|
||||||
let mut cmd = eh::Cli::command();
|
let mut cmd = eh::Cli::command();
|
||||||
let bin_name = "eh";
|
let bin_name = "eh";
|
||||||
|
|
||||||
let completion_file = output_dir.join(format!("{bin_name}.{shell}"));
|
let completion_file = output_dir.join(format!("{bin_name}.{shell}"));
|
||||||
let mut file = fs::File::create(&completion_file)?;
|
let mut file = fs::File::create(&completion_file)?;
|
||||||
|
|
||||||
generate(shell, &mut cmd, bin_name, &mut file);
|
generate(shell, &mut cmd, bin_name, &mut file);
|
||||||
|
|
||||||
println!("completion file generated: {}", completion_file.display());
|
println!("completion file generated: {}", completion_file.display());
|
||||||
|
|
||||||
// Create symlinks for multicall binaries
|
// Create symlinks for multicall binaries
|
||||||
let multicall_names = ["nb", "nr", "ns"];
|
let multicall_names = ["nb", "nr", "ns"];
|
||||||
for name in &multicall_names {
|
for name in &multicall_names {
|
||||||
let symlink_path = output_dir.join(format!("{name}.{shell}"));
|
let symlink_path = output_dir.join(format!("{name}.{shell}"));
|
||||||
if symlink_path.exists() {
|
if symlink_path.exists() {
|
||||||
fs::remove_file(&symlink_path)?;
|
fs::remove_file(&symlink_path)?;
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(unix)]
|
|
||||||
{
|
|
||||||
std::os::unix::fs::symlink(&completion_file, &symlink_path)?;
|
|
||||||
println!("completion symlink created: {}", symlink_path.display());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(unix))]
|
|
||||||
{
|
|
||||||
fs::copy(&completion_file, &symlink_path)?;
|
|
||||||
println!("completion copy created: {}", symlink_path.display());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("completions generated successfully!");
|
#[cfg(unix)]
|
||||||
Ok(())
|
{
|
||||||
|
std::os::unix::fs::symlink(&completion_file, &symlink_path)?;
|
||||||
|
println!("completion symlink created: {}", symlink_path.display());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(unix))]
|
||||||
|
{
|
||||||
|
fs::copy(&completion_file, &symlink_path)?;
|
||||||
|
println!("completion copy created: {}", symlink_path.display());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("completions generated successfully!");
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue