Compare commits
No commits in common. "main" and "amr-patch-1" have entirely different histories.
main
...
amr-patch-
15 changed files with 214 additions and 428 deletions
|
@ -1,24 +0,0 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
insert_final_newline = true
|
||||
tab_width = 2
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.{lock,diff,patch}]
|
||||
indent_style = unset
|
||||
indent_size = unset
|
||||
insert_final_newline = unset
|
||||
trim_trailing_whitespace = unset
|
||||
end_of_line = unset
|
||||
|
||||
|
164
Cargo.lock
generated
164
Cargo.lock
generated
|
@ -11,12 +11,56 @@ dependencies = [
|
|||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
"is_terminal_polyfill",
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "0.2.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"once_cell_polyfill",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.1"
|
||||
|
@ -39,17 +83,10 @@ version = "4.5.41"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "707eab41e9622f9139419d573eca0900137718000c517d47da73045f54331c3d"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_complete"
|
||||
version = "4.5.55"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a5abde44486daf70c5be8b8f8f1b66c49f86236edf6fa2abadb4d961c4c6229a"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"strsim",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -70,9 +107,15 @@ version = "0.7.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675"
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
|
||||
|
||||
[[package]]
|
||||
name = "eh"
|
||||
version = "0.1.1"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"regex",
|
||||
|
@ -87,6 +130,12 @@ version = "0.5.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
|
@ -121,6 +170,12 @@ version = "1.21.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell_polyfill"
|
||||
version = "1.70.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
|
||||
|
||||
[[package]]
|
||||
name = "overload"
|
||||
version = "0.1.1"
|
||||
|
@ -195,6 +250,12 @@ version = "1.15.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.104"
|
||||
|
@ -278,6 +339,12 @@ version = "1.0.18"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
version = "0.1.1"
|
||||
|
@ -306,13 +373,84 @@ version = "0.4.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.59.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "xtask"
|
||||
version = "0.1.1"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"clap_complete",
|
||||
"eh",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
14
Cargo.toml
14
Cargo.toml
|
@ -9,19 +9,11 @@ edition = "2024"
|
|||
license = "MPL-2.0"
|
||||
readme = true
|
||||
rust-version = "1.85"
|
||||
version = "0.1.1"
|
||||
version = "0.1.0"
|
||||
|
||||
[workspace.dependencies]
|
||||
clap = { default-features = false, features = [ "std", "help", "derive" ], version = "4.5" }
|
||||
clap_complete = "4.5"
|
||||
regex = "1.11"
|
||||
clap = { features = [ "derive" ], version = "4.5" }
|
||||
regex = "1.0"
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = "0.3"
|
||||
yansi = "1.0"
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
lto = true
|
||||
opt-level = "z"
|
||||
panic = "abort"
|
||||
strip = true
|
||||
|
|
|
@ -6,10 +6,6 @@ edition.workspace = true
|
|||
authors.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[lib]
|
||||
name = "eh"
|
||||
crate-type = ["lib"]
|
||||
|
||||
[dependencies]
|
||||
clap.workspace = true
|
||||
regex.workspace = true
|
||||
|
|
|
@ -98,7 +98,6 @@ impl NixCommand {
|
|||
if self.interactive {
|
||||
cmd.stdout(Stdio::inherit());
|
||||
cmd.stderr(Stdio::inherit());
|
||||
cmd.stdin(Stdio::inherit());
|
||||
return cmd.status();
|
||||
}
|
||||
|
||||
|
@ -168,7 +167,6 @@ impl NixCommand {
|
|||
if self.interactive {
|
||||
cmd.stdout(Stdio::inherit());
|
||||
cmd.stderr(Stdio::inherit());
|
||||
cmd.stdin(Stdio::inherit());
|
||||
} else {
|
||||
cmd.stdout(Stdio::piped());
|
||||
cmd.stderr(Stdio::piped());
|
||||
|
|
|
@ -1,34 +0,0 @@
|
|||
pub mod build;
|
||||
pub mod command;
|
||||
pub mod run;
|
||||
pub mod shell;
|
||||
pub mod util;
|
||||
|
||||
pub use clap::{CommandFactory, Parser, Subcommand};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "eh")]
|
||||
#[command(about = "Ergonomic Nix helper", long_about = None)]
|
||||
pub struct Cli {
|
||||
#[command(subcommand)]
|
||||
pub command: Option<Command>,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum Command {
|
||||
/// Run a Nix derivation
|
||||
Run {
|
||||
#[arg(trailing_var_arg = true)]
|
||||
args: Vec<String>,
|
||||
},
|
||||
/// Enter a Nix shell
|
||||
Shell {
|
||||
#[arg(trailing_var_arg = true)]
|
||||
args: Vec<String>,
|
||||
},
|
||||
/// Build a Nix derivation
|
||||
Build {
|
||||
#[arg(trailing_var_arg = true)]
|
||||
args: Vec<String>,
|
||||
},
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
use eh::{Cli, Command, CommandFactory, Parser};
|
||||
use clap::{CommandFactory, Parser, Subcommand};
|
||||
use std::env;
|
||||
use std::path::Path;
|
||||
|
||||
|
@ -8,6 +8,33 @@ mod run;
|
|||
mod shell;
|
||||
mod util;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "eh")]
|
||||
#[command(about = "Ergonomic Nix helper", long_about = None)]
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Option<Command>,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Command {
|
||||
/// Run a Nix derivation
|
||||
Run {
|
||||
#[arg(trailing_var_arg = true)]
|
||||
args: Vec<String>,
|
||||
},
|
||||
/// Enter a Nix shell
|
||||
Shell {
|
||||
#[arg(trailing_var_arg = true)]
|
||||
args: Vec<String>,
|
||||
},
|
||||
/// Build a Nix derivation
|
||||
Build {
|
||||
#[arg(trailing_var_arg = true)]
|
||||
args: Vec<String>,
|
||||
},
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let format = tracing_subscriber::fmt::format()
|
||||
.with_level(true) // don't include levels in formatted output
|
||||
|
|
|
@ -6,5 +6,5 @@ pub fn handle_nix_run(
|
|||
fixer: &dyn NixFileFixer,
|
||||
classifier: &dyn NixErrorClassifier,
|
||||
) {
|
||||
handle_nix_with_retry("run", args, hash_extractor, fixer, classifier, true);
|
||||
handle_nix_with_retry("run", args, hash_extractor, fixer, classifier, false);
|
||||
}
|
||||
|
|
102
eh/src/util.rs
102
eh/src/util.rs
|
@ -15,9 +15,9 @@ pub struct RegexHashExtractor;
|
|||
impl HashExtractor for RegexHashExtractor {
|
||||
fn extract_hash(&self, stderr: &str) -> Option<String> {
|
||||
let patterns = [
|
||||
r"got:\s+(sha256-[a-zA-Z0-9+/=]+)",
|
||||
r"actual:\s+(sha256-[a-zA-Z0-9+/=]+)",
|
||||
r"have:\s+(sha256-[a-zA-Z0-9+/=]+)",
|
||||
r"got:\s+([a-zA-Z0-9+/=]+)",
|
||||
r"actual:\s+([a-zA-Z0-9+/=]+)",
|
||||
r"have:\s+([a-zA-Z0-9+/=]+)",
|
||||
];
|
||||
for pattern in &patterns {
|
||||
if let Ok(re) = Regex::new(pattern) {
|
||||
|
@ -110,43 +110,6 @@ pub trait NixErrorClassifier {
|
|||
fn should_retry(&self, stderr: &str) -> bool;
|
||||
}
|
||||
|
||||
/// Pre-evaluate expression to catch errors early
|
||||
fn pre_evaluate(_subcommand: &str, args: &[String]) -> bool {
|
||||
// Find flake references or expressions to evaluate
|
||||
// Only take the first non-flag argument (the package/expression)
|
||||
let eval_arg = args.iter().find(|arg| !arg.starts_with('-'));
|
||||
|
||||
let Some(eval_arg) = eval_arg else {
|
||||
return true; // No expression to evaluate
|
||||
};
|
||||
|
||||
let eval_cmd = NixCommand::new("eval").arg(eval_arg).arg("--raw");
|
||||
|
||||
let output = match eval_cmd.output() {
|
||||
Ok(output) => output,
|
||||
Err(_) => return false,
|
||||
};
|
||||
|
||||
if output.status.success() {
|
||||
return true;
|
||||
}
|
||||
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
|
||||
// If eval fails due to unfree/insecure/broken, don't fail pre-evaluation
|
||||
// Let the main command handle it with retry logic
|
||||
if stderr.contains("has an unfree license")
|
||||
|| stderr.contains("refusing to evaluate")
|
||||
|| stderr.contains("has been marked as insecure")
|
||||
|| stderr.contains("has been marked as broken")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// For other eval failures, fail early
|
||||
false
|
||||
}
|
||||
|
||||
/// Shared retry logic for nix commands (build/run/shell).
|
||||
pub fn handle_nix_with_retry(
|
||||
subcommand: &str,
|
||||
|
@ -156,36 +119,29 @@ pub fn handle_nix_with_retry(
|
|||
classifier: &dyn NixErrorClassifier,
|
||||
interactive: bool,
|
||||
) -> ! {
|
||||
// Pre-evaluate for build commands to catch errors early
|
||||
if !pre_evaluate(subcommand, args) {
|
||||
eprintln!("Error: Expression evaluation failed");
|
||||
std::process::exit(1);
|
||||
let mut cmd = NixCommand::new(subcommand).print_build_logs(true);
|
||||
if interactive {
|
||||
cmd = cmd.interactive(true);
|
||||
}
|
||||
for arg in args {
|
||||
cmd = cmd.arg(arg);
|
||||
}
|
||||
let status = cmd
|
||||
.run_with_logs(StdIoInterceptor)
|
||||
.expect("failed to run nix command");
|
||||
if status.success() {
|
||||
std::process::exit(0);
|
||||
}
|
||||
|
||||
// For run commands, try interactive first to avoid breaking terminal
|
||||
if subcommand == "run" && interactive {
|
||||
let mut cmd = NixCommand::new(subcommand)
|
||||
.print_build_logs(true)
|
||||
.interactive(true);
|
||||
for arg in args {
|
||||
cmd = cmd.arg(arg);
|
||||
}
|
||||
let status = cmd
|
||||
.run_with_logs(StdIoInterceptor)
|
||||
.expect("failed to run nix command");
|
||||
if status.success() {
|
||||
std::process::exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
// First, always capture output to check for errors that need retry
|
||||
let output_cmd = NixCommand::new(subcommand)
|
||||
let mut output_cmd = NixCommand::new(subcommand)
|
||||
.print_build_logs(true)
|
||||
.args(args.iter().cloned());
|
||||
if interactive {
|
||||
output_cmd = output_cmd.interactive(true);
|
||||
}
|
||||
let output = output_cmd.output().expect("failed to capture 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) {
|
||||
if fixer.fix_hash_in_files(&new_hash) {
|
||||
info!("{}", Paint::green("✔ Fixed hash mismatch, retrying..."));
|
||||
|
@ -201,7 +157,7 @@ pub fn handle_nix_with_retry(
|
|||
}
|
||||
|
||||
if classifier.should_retry(&stderr) {
|
||||
if stderr.contains("has an unfree license") && stderr.contains("refusing") {
|
||||
if stderr.contains("unfree") {
|
||||
warn!(
|
||||
"{}",
|
||||
Paint::yellow("⚠ Unfree package detected, retrying with NIXPKGS_ALLOW_UNFREE=1...")
|
||||
|
@ -217,7 +173,7 @@ pub fn handle_nix_with_retry(
|
|||
let retry_status = retry_cmd.run_with_logs(StdIoInterceptor).unwrap();
|
||||
std::process::exit(retry_status.code().unwrap_or(1));
|
||||
}
|
||||
if stderr.contains("has been marked as insecure") && stderr.contains("refusing") {
|
||||
if stderr.contains("insecure") {
|
||||
warn!(
|
||||
"{}",
|
||||
Paint::yellow(
|
||||
|
@ -235,7 +191,7 @@ pub fn handle_nix_with_retry(
|
|||
let retry_status = retry_cmd.run_with_logs(StdIoInterceptor).unwrap();
|
||||
std::process::exit(retry_status.code().unwrap_or(1));
|
||||
}
|
||||
if stderr.contains("has been marked as broken") && stderr.contains("refusing") {
|
||||
if stderr.contains("broken") {
|
||||
warn!(
|
||||
"{}",
|
||||
Paint::yellow("⚠ Broken package detected, retrying with NIXPKGS_ALLOW_BROKEN=1...")
|
||||
|
@ -253,22 +209,16 @@ pub fn handle_nix_with_retry(
|
|||
}
|
||||
}
|
||||
|
||||
// If the first attempt succeeded, we're done
|
||||
if output.status.success() {
|
||||
std::process::exit(0);
|
||||
}
|
||||
|
||||
// Otherwise, show the error and exit
|
||||
std::io::stderr().write_all(output.stderr.as_ref()).unwrap();
|
||||
std::process::exit(output.status.code().unwrap_or(1));
|
||||
std::process::exit(status.code().unwrap_or(1));
|
||||
}
|
||||
pub struct DefaultNixErrorClassifier;
|
||||
|
||||
impl NixErrorClassifier for DefaultNixErrorClassifier {
|
||||
fn should_retry(&self, stderr: &str) -> bool {
|
||||
RegexHashExtractor.extract_hash(stderr).is_some()
|
||||
|| (stderr.contains("has an unfree license") && stderr.contains("refusing"))
|
||||
|| (stderr.contains("has been marked as insecure") && stderr.contains("refusing"))
|
||||
|| (stderr.contains("has been marked as broken") && stderr.contains("refusing"))
|
||||
|| (stderr.contains("unfree") && stderr.contains("refusing"))
|
||||
|| (stderr.contains("insecure") && stderr.contains("refusing"))
|
||||
|| (stderr.contains("broken") && stderr.contains("refusing"))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,178 +0,0 @@
|
|||
//! I hate writing tests, and I hate writing integration tests. This is the best
|
||||
//! that you are getting, deal with it.
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
#[test]
|
||||
fn nix_eval_validation() {
|
||||
// Test that invalid expressions are caught early for all commands
|
||||
let commands = ["build", "run", "shell"];
|
||||
|
||||
for cmd in &commands {
|
||||
let output = Command::new("timeout")
|
||||
.args([
|
||||
"10",
|
||||
"cargo",
|
||||
"run",
|
||||
"--bin",
|
||||
"eh",
|
||||
"--",
|
||||
cmd,
|
||||
"invalid-flake-ref",
|
||||
])
|
||||
.output()
|
||||
.expect("Failed to execute command");
|
||||
|
||||
// Should fail fast with eval error
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
assert!(stderr.contains("Error: Expression evaluation failed") || !output.status.success());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unfree_package_handling() {
|
||||
// Test that unfree packages are detected and handled correctly
|
||||
let output = Command::new("timeout")
|
||||
.args([
|
||||
"30",
|
||||
"cargo",
|
||||
"run",
|
||||
"--bin",
|
||||
"eh",
|
||||
"--",
|
||||
"build",
|
||||
"nixpkgs#discord",
|
||||
])
|
||||
.output()
|
||||
.expect("Failed to execute command");
|
||||
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let combined = format!("{}{}", stdout, stderr);
|
||||
|
||||
// Should detect unfree package and show appropriate message
|
||||
assert!(
|
||||
combined.contains("has an unfree license")
|
||||
|| combined.contains("NIXPKGS_ALLOW_UNFREE")
|
||||
|| combined.contains("⚠ Unfree package detected")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insecure_package_handling() {
|
||||
// Test that error classification works for insecure packages
|
||||
use eh::util::{DefaultNixErrorClassifier, NixErrorClassifier};
|
||||
|
||||
let classifier = DefaultNixErrorClassifier;
|
||||
let stderr_insecure =
|
||||
"Package 'example-1.0' has been marked as insecure, refusing to evaluate.";
|
||||
|
||||
assert!(classifier.should_retry(stderr_insecure));
|
||||
}
|
||||
|
||||
#[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);
|
||||
assert_eq!(
|
||||
extracted,
|
||||
Some("sha256-newhashbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb=".to_string())
|
||||
);
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
inputs.nixpkgs.url = "github:NixOS/nixpkgs?ref=nixos-unstable";
|
||||
description = "Rust Project Template";
|
||||
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
|
||||
outputs = {
|
||||
self,
|
||||
|
@ -7,11 +8,11 @@
|
|||
}: let
|
||||
systems = ["x86_64-linux" "aarch64-linux"];
|
||||
forEachSystem = nixpkgs.lib.genAttrs systems;
|
||||
|
||||
pkgsForEach = nixpkgs.legacyPackages;
|
||||
in {
|
||||
packages = forEachSystem (system: {
|
||||
eh = pkgsForEach.${system}.callPackage ./nix/package.nix {};
|
||||
default = self.packages.${system}.eh;
|
||||
default = pkgsForEach.${system}.callPackage ./nix/package.nix {};
|
||||
});
|
||||
|
||||
devShells = forEachSystem (system: {
|
||||
|
@ -19,6 +20,5 @@
|
|||
});
|
||||
|
||||
hydraJobs = self.packages;
|
||||
checks = self.packages // self.devShells;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
{
|
||||
lib,
|
||||
rustPlatform,
|
||||
stdenv,
|
||||
}:
|
||||
rustPlatform.buildRustPackage (finalAttrs: {
|
||||
pname = "eh";
|
||||
|
@ -22,28 +21,11 @@ rustPlatform.buildRustPackage (finalAttrs: {
|
|||
]);
|
||||
};
|
||||
|
||||
# xtask doesn't support passing --targe
|
||||
# but nix hooks expect the folder structure from when it's set
|
||||
env.CARGO_BUILD_TARGET = stdenv.hostPlatform.rust.cargoShortTarget;
|
||||
cargoLock.lockFile = "${finalAttrs.src}/Cargo.lock";
|
||||
enableParallelBuilding = true;
|
||||
|
||||
postInstall = ''
|
||||
# Install required files with the 'dist' task
|
||||
$out/bin/xtask multicall \
|
||||
--bin-dir $out/bin \
|
||||
--main-binary $out/bin/eh
|
||||
|
||||
# The xtask output has been built as a part of the build phase. If
|
||||
# we don't remove it, it'll be linked in $out/bin alongside the actual
|
||||
# binary and populate $PATH with a dedicated 'xtask' command. Remove.
|
||||
rm -rf $out/bin/xtask
|
||||
'';
|
||||
|
||||
meta = {
|
||||
description = "Ergonomic Nix CLI helper";
|
||||
maintainers = with lib.licenses; [NotAShelf];
|
||||
license = lib.licenses.mpl20;
|
||||
mainProgram = "eh";
|
||||
};
|
||||
})
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
rustfmt,
|
||||
clippy,
|
||||
cargo,
|
||||
taplo,
|
||||
rustPlatform,
|
||||
}:
|
||||
mkShell {
|
||||
|
@ -14,8 +13,6 @@ mkShell {
|
|||
rustfmt
|
||||
clippy
|
||||
cargo
|
||||
|
||||
taplo
|
||||
];
|
||||
|
||||
RUST_SRC_PATH = "${rustPlatform.rustLibSrc}";
|
||||
|
|
|
@ -10,5 +10,3 @@ publish = false
|
|||
|
||||
[dependencies]
|
||||
clap.workspace = true
|
||||
clap_complete.workspace = true
|
||||
eh = { path = "../eh" }
|
||||
|
|
|
@ -4,8 +4,7 @@ use std::{
|
|||
process,
|
||||
};
|
||||
|
||||
use clap::{CommandFactory, Parser};
|
||||
use clap_complete::{Shell, generate};
|
||||
use clap::Parser;
|
||||
|
||||
#[derive(clap::Parser)]
|
||||
struct Cli {
|
||||
|
@ -25,15 +24,6 @@ enum Command {
|
|||
#[arg(long, default_value = "target/release/eh")]
|
||||
main_binary: PathBuf,
|
||||
},
|
||||
/// Generate shell completion scripts
|
||||
Completions {
|
||||
/// Shell to generate completions for
|
||||
#[arg(value_enum)]
|
||||
shell: Shell,
|
||||
/// Directory to output completion files
|
||||
#[arg(long, default_value = "completions")]
|
||||
output_dir: PathBuf,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
|
@ -66,12 +56,6 @@ fn main() {
|
|||
process::exit(1);
|
||||
}
|
||||
}
|
||||
Command::Completions { shell, output_dir } => {
|
||||
if let Err(error) = generate_completions(shell, &output_dir) {
|
||||
eprintln!("error generating completions: {error}");
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -133,43 +117,3 @@ fn create_multicall_binaries(
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn generate_completions(shell: Shell, output_dir: &Path) -> Result<(), Box<dyn error::Error>> {
|
||||
println!("generating {} completions...", shell);
|
||||
|
||||
fs::create_dir_all(output_dir)?;
|
||||
|
||||
let mut cmd = eh::Cli::command();
|
||||
let bin_name = "eh";
|
||||
|
||||
let completion_file = output_dir.join(format!("{}.{}", bin_name, shell));
|
||||
let mut file = fs::File::create(&completion_file)?;
|
||||
|
||||
generate(shell, &mut cmd, bin_name, &mut file);
|
||||
|
||||
println!("completion file generated: {}", completion_file.display());
|
||||
|
||||
// Create symlinks for multicall binaries
|
||||
let multicall_names = ["nb", "nr", "ns"];
|
||||
for name in &multicall_names {
|
||||
let symlink_path = output_dir.join(format!("{}.{}", name, shell));
|
||||
if symlink_path.exists() {
|
||||
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!");
|
||||
Ok(())
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue