eh: add eh update or nu in symlink
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: Iee1d7c2ed2c4b2cd5520c68ceb2b5e6d6a6a6964
This commit is contained in:
parent
045d1632cb
commit
5dc7b1dcd4
7 changed files with 188 additions and 15 deletions
|
|
@ -42,6 +42,7 @@ enum Binary {
|
||||||
Nr,
|
Nr,
|
||||||
Ns,
|
Ns,
|
||||||
Nb,
|
Nb,
|
||||||
|
Nu,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Binary {
|
impl Binary {
|
||||||
|
|
@ -50,6 +51,7 @@ impl Binary {
|
||||||
Self::Nr => "nr",
|
Self::Nr => "nr",
|
||||||
Self::Ns => "ns",
|
Self::Ns => "ns",
|
||||||
Self::Nb => "nb",
|
Self::Nb => "nb",
|
||||||
|
Self::Nu => "nu",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -90,7 +92,7 @@ fn create_multicall_binaries(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let multicall_binaries = [Binary::Nr, Binary::Ns, Binary::Nb];
|
let multicall_binaries = [Binary::Nr, Binary::Ns, Binary::Nb, Binary::Nu];
|
||||||
let bin_path = Path::new(bin_dir);
|
let bin_path = Path::new(bin_dir);
|
||||||
|
|
||||||
for binary in multicall_binaries {
|
for binary in multicall_binaries {
|
||||||
|
|
@ -153,7 +155,7 @@ fn generate_completions(
|
||||||
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", "nu"];
|
||||||
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() {
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,10 @@ name = "eh"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
clap.workspace = true
|
clap.workspace = true
|
||||||
|
dialoguer.workspace = true
|
||||||
eh-log.workspace = true
|
eh-log.workspace = true
|
||||||
regex.workspace = true
|
regex.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
tempfile.workspace = true
|
tempfile.workspace = true
|
||||||
thiserror.workspace = true
|
thiserror.workspace = true
|
||||||
walkdir.workspace = true
|
walkdir.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,15 @@ pub enum EhError {
|
||||||
|
|
||||||
#[error("invalid input '{input}': {reason}")]
|
#[error("invalid input '{input}': {reason}")]
|
||||||
InvalidInput { input: String, reason: String },
|
InvalidInput { input: String, reason: String },
|
||||||
|
|
||||||
|
#[error("failed to parse JSON from nix output: {detail}")]
|
||||||
|
JsonParse { detail: String },
|
||||||
|
|
||||||
|
#[error("no flake inputs found in lock file")]
|
||||||
|
NoFlakeInputs,
|
||||||
|
|
||||||
|
#[error("no inputs selected")]
|
||||||
|
UpdateCancelled,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type Result<T> = std::result::Result<T, EhError>;
|
pub type Result<T> = std::result::Result<T, EhError>;
|
||||||
|
|
@ -65,6 +74,9 @@ impl EhError {
|
||||||
Self::Utf8(_) => 10,
|
Self::Utf8(_) => 10,
|
||||||
Self::Timeout { .. } => 11,
|
Self::Timeout { .. } => 11,
|
||||||
Self::PreEvalFailed { .. } => 12,
|
Self::PreEvalFailed { .. } => 12,
|
||||||
|
Self::JsonParse { .. } => 13,
|
||||||
|
Self::NoFlakeInputs => 14,
|
||||||
|
Self::UpdateCancelled => 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -92,12 +104,19 @@ impl EhError {
|
||||||
Self::InvalidInput { .. } => {
|
Self::InvalidInput { .. } => {
|
||||||
Some("avoid shell metacharacters in nix arguments")
|
Some("avoid shell metacharacters in nix arguments")
|
||||||
},
|
},
|
||||||
|
Self::JsonParse { .. } => {
|
||||||
|
Some("ensure 'nix flake metadata --json' produces valid output")
|
||||||
|
},
|
||||||
|
Self::NoFlakeInputs => {
|
||||||
|
Some("run this from a directory with a flake.lock that has inputs")
|
||||||
|
},
|
||||||
Self::Io(_)
|
Self::Io(_)
|
||||||
| Self::Regex(_)
|
| Self::Regex(_)
|
||||||
| Self::Utf8(_)
|
| Self::Utf8(_)
|
||||||
| Self::HashFixFailed { .. }
|
| Self::HashFixFailed { .. }
|
||||||
| Self::ProcessExit { .. }
|
| Self::ProcessExit { .. }
|
||||||
| Self::CommandFailed { .. } => None,
|
| Self::CommandFailed { .. }
|
||||||
|
| Self::UpdateCancelled => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -156,6 +175,15 @@ mod tests {
|
||||||
12
|
12
|
||||||
);
|
);
|
||||||
assert_eq!(EhError::ProcessExit { code: 42 }.exit_code(), 42);
|
assert_eq!(EhError::ProcessExit { code: 42 }.exit_code(), 42);
|
||||||
|
assert_eq!(
|
||||||
|
EhError::JsonParse {
|
||||||
|
detail: "x".into(),
|
||||||
|
}
|
||||||
|
.exit_code(),
|
||||||
|
13
|
||||||
|
);
|
||||||
|
assert_eq!(EhError::NoFlakeInputs.exit_code(), 14);
|
||||||
|
assert_eq!(EhError::UpdateCancelled.exit_code(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -221,6 +249,14 @@ mod tests {
|
||||||
.hint()
|
.hint()
|
||||||
.is_some()
|
.is_some()
|
||||||
);
|
);
|
||||||
|
assert!(
|
||||||
|
EhError::JsonParse {
|
||||||
|
detail: "x".into(),
|
||||||
|
}
|
||||||
|
.hint()
|
||||||
|
.is_some()
|
||||||
|
);
|
||||||
|
assert!(EhError::NoFlakeInputs.hint().is_some());
|
||||||
// Variants without hints
|
// Variants without hints
|
||||||
assert!(
|
assert!(
|
||||||
EhError::CommandFailed {
|
EhError::CommandFailed {
|
||||||
|
|
@ -230,5 +266,6 @@ mod tests {
|
||||||
.is_none()
|
.is_none()
|
||||||
);
|
);
|
||||||
assert!(EhError::ProcessExit { code: 1 }.hint().is_none());
|
assert!(EhError::ProcessExit { code: 1 }.hint().is_none());
|
||||||
|
assert!(EhError::UpdateCancelled.hint().is_none());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ pub mod command;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod run;
|
pub mod run;
|
||||||
pub mod shell;
|
pub mod shell;
|
||||||
|
pub mod update;
|
||||||
pub mod util;
|
pub mod util;
|
||||||
|
|
||||||
pub use clap::{CommandFactory, Parser, Subcommand};
|
pub use clap::{CommandFactory, Parser, Subcommand};
|
||||||
|
|
@ -34,4 +35,9 @@ pub enum Command {
|
||||||
#[arg(trailing_var_arg = true)]
|
#[arg(trailing_var_arg = true)]
|
||||||
args: Vec<String>,
|
args: Vec<String>,
|
||||||
},
|
},
|
||||||
|
/// Update flake inputs interactively
|
||||||
|
Update {
|
||||||
|
#[arg(trailing_var_arg = true)]
|
||||||
|
args: Vec<String>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ mod command;
|
||||||
mod error;
|
mod error;
|
||||||
mod run;
|
mod run;
|
||||||
mod shell;
|
mod shell;
|
||||||
|
mod update;
|
||||||
mod util;
|
mod util;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
|
@ -17,11 +18,14 @@ fn main() {
|
||||||
match result {
|
match result {
|
||||||
Ok(code) => std::process::exit(code),
|
Ok(code) => std::process::exit(code),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
let code = e.exit_code();
|
||||||
|
if code != 0 {
|
||||||
eh_log::log_error!("{e}");
|
eh_log::log_error!("{e}");
|
||||||
if let Some(hint) = e.hint() {
|
if let Some(hint) = e.hint() {
|
||||||
eh_log::log_hint!("{hint}");
|
eh_log::log_hint!("{hint}");
|
||||||
}
|
}
|
||||||
std::process::exit(e.exit_code());
|
}
|
||||||
|
std::process::exit(code);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -37,6 +41,7 @@ fn dispatch_multicall(
|
||||||
"nr" => "run",
|
"nr" => "run",
|
||||||
"ns" => "shell",
|
"ns" => "shell",
|
||||||
"nb" => "build",
|
"nb" => "build",
|
||||||
|
"nu" => "update",
|
||||||
_ => return None,
|
_ => return None,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -60,6 +65,10 @@ fn dispatch_multicall(
|
||||||
return Some(Ok(0));
|
return Some(Ok(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if subcommand == "update" {
|
||||||
|
return Some(update::handle_update(&rest));
|
||||||
|
}
|
||||||
|
|
||||||
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;
|
||||||
|
|
@ -110,6 +119,8 @@ fn run_app() -> Result<i32> {
|
||||||
build::handle_nix_build(&args, &hash_extractor, &fixer, &classifier)
|
build::handle_nix_build(&args, &hash_extractor, &fixer, &classifier)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
Some(Command::Update { args }) => update::handle_update(&args),
|
||||||
|
|
||||||
None => {
|
None => {
|
||||||
Cli::command().print_help()?;
|
Cli::command().print_help()?;
|
||||||
println!();
|
println!();
|
||||||
|
|
|
||||||
115
eh/src/update.rs
Normal file
115
eh/src/update.rs
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
use crate::{
|
||||||
|
command::{NixCommand, StdIoInterceptor},
|
||||||
|
error::{EhError, Result},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Parse flake input names from `nix flake metadata --json` output.
|
||||||
|
pub fn parse_flake_inputs(stdout: &str) -> Result<Vec<String>> {
|
||||||
|
let value: serde_json::Value =
|
||||||
|
serde_json::from_str(stdout).map_err(|e| EhError::JsonParse {
|
||||||
|
detail: e.to_string(),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let inputs = value
|
||||||
|
.get("locks")
|
||||||
|
.and_then(|l| l.get("nodes"))
|
||||||
|
.and_then(|n| n.get("root"))
|
||||||
|
.and_then(|r| r.get("inputs"))
|
||||||
|
.and_then(|i| i.as_object())
|
||||||
|
.ok_or(EhError::NoFlakeInputs)?;
|
||||||
|
|
||||||
|
let mut names: Vec<String> = inputs.keys().cloned().collect();
|
||||||
|
names.sort();
|
||||||
|
Ok(names)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetch flake input names by running `nix flake metadata --json`.
|
||||||
|
fn fetch_flake_inputs() -> Result<Vec<String>> {
|
||||||
|
let output = NixCommand::new("flake")
|
||||||
|
.arg("metadata")
|
||||||
|
.arg("--json")
|
||||||
|
.print_build_logs(false)
|
||||||
|
.output()?;
|
||||||
|
|
||||||
|
let stdout = String::from_utf8(output.stdout)?;
|
||||||
|
parse_flake_inputs(&stdout)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prompt the user to select inputs via a multi-select dialog.
|
||||||
|
fn prompt_input_selection(inputs: &[String]) -> Result<Vec<String>> {
|
||||||
|
let selections = dialoguer::MultiSelect::new()
|
||||||
|
.with_prompt("Select inputs to update")
|
||||||
|
.items(inputs)
|
||||||
|
.interact()
|
||||||
|
.map_err(|e| EhError::Io(std::io::Error::other(e)))?;
|
||||||
|
|
||||||
|
if selections.is_empty() {
|
||||||
|
return Err(EhError::UpdateCancelled);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(selections.iter().map(|&i| inputs[i].clone()).collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Entry point for the `update` subcommand.
|
||||||
|
///
|
||||||
|
/// If `args` is non-empty, use them as explicit input names.
|
||||||
|
/// Otherwise, fetch inputs interactively and prompt for selection.
|
||||||
|
pub fn handle_update(args: &[String]) -> Result<i32> {
|
||||||
|
let selected = if args.is_empty() {
|
||||||
|
let inputs = fetch_flake_inputs()?;
|
||||||
|
if inputs.is_empty() {
|
||||||
|
return Err(EhError::NoFlakeInputs);
|
||||||
|
}
|
||||||
|
prompt_input_selection(&inputs)?
|
||||||
|
} else {
|
||||||
|
args.to_vec()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut cmd = NixCommand::new("flake").arg("lock");
|
||||||
|
for name in &selected {
|
||||||
|
cmd = cmd.arg("--update-input").arg(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
eh_log::log_info!("updating inputs: {}", selected.join(", "));
|
||||||
|
|
||||||
|
let status = cmd.run_with_logs(StdIoInterceptor)?;
|
||||||
|
Ok(status.code().unwrap_or(1))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_flake_inputs() {
|
||||||
|
let json = r#"{
|
||||||
|
"locks": {
|
||||||
|
"nodes": {
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": "nixpkgs_2",
|
||||||
|
"home-manager": "home-manager_2",
|
||||||
|
"flake-utils": "flake-utils_2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}"#;
|
||||||
|
|
||||||
|
let inputs = parse_flake_inputs(json).unwrap();
|
||||||
|
assert_eq!(inputs, vec!["flake-utils", "home-manager", "nixpkgs"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_flake_inputs_invalid_json() {
|
||||||
|
let result = parse_flake_inputs("not json");
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_flake_inputs_no_inputs() {
|
||||||
|
let json = r#"{"locks": {"nodes": {"root": {}}}}"#;
|
||||||
|
let result = parse_flake_inputs(json);
|
||||||
|
assert!(matches!(result, Err(EhError::NoFlakeInputs)));
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue