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,
|
||||
Ns,
|
||||
Nb,
|
||||
Nu,
|
||||
}
|
||||
|
||||
impl Binary {
|
||||
|
|
@ -50,6 +51,7 @@ impl Binary {
|
|||
Self::Nr => "nr",
|
||||
Self::Ns => "ns",
|
||||
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);
|
||||
|
||||
for binary in multicall_binaries {
|
||||
|
|
@ -153,7 +155,7 @@ fn generate_completions(
|
|||
println!("completion file generated: {}", completion_file.display());
|
||||
|
||||
// Create symlinks for multicall binaries
|
||||
let multicall_names = ["nb", "nr", "ns"];
|
||||
let multicall_names = ["nb", "nr", "ns", "nu"];
|
||||
for name in &multicall_names {
|
||||
let symlink_path = output_dir.join(format!("{name}.{shell}"));
|
||||
if symlink_path.exists() {
|
||||
|
|
|
|||
|
|
@ -12,8 +12,10 @@ name = "eh"
|
|||
|
||||
[dependencies]
|
||||
clap.workspace = true
|
||||
dialoguer.workspace = true
|
||||
eh-log.workspace = true
|
||||
regex.workspace = true
|
||||
serde_json.workspace = true
|
||||
tempfile.workspace = true
|
||||
thiserror.workspace = true
|
||||
walkdir.workspace = true
|
||||
|
|
|
|||
|
|
@ -45,6 +45,15 @@ pub enum EhError {
|
|||
|
||||
#[error("invalid input '{input}': {reason}")]
|
||||
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>;
|
||||
|
|
@ -65,6 +74,9 @@ impl EhError {
|
|||
Self::Utf8(_) => 10,
|
||||
Self::Timeout { .. } => 11,
|
||||
Self::PreEvalFailed { .. } => 12,
|
||||
Self::JsonParse { .. } => 13,
|
||||
Self::NoFlakeInputs => 14,
|
||||
Self::UpdateCancelled => 0,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -92,12 +104,19 @@ impl EhError {
|
|||
Self::InvalidInput { .. } => {
|
||||
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::Regex(_)
|
||||
| Self::Utf8(_)
|
||||
| Self::HashFixFailed { .. }
|
||||
| Self::ProcessExit { .. }
|
||||
| Self::CommandFailed { .. } => None,
|
||||
| Self::CommandFailed { .. }
|
||||
| Self::UpdateCancelled => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -156,6 +175,15 @@ mod tests {
|
|||
12
|
||||
);
|
||||
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]
|
||||
|
|
@ -221,6 +249,14 @@ mod tests {
|
|||
.hint()
|
||||
.is_some()
|
||||
);
|
||||
assert!(
|
||||
EhError::JsonParse {
|
||||
detail: "x".into(),
|
||||
}
|
||||
.hint()
|
||||
.is_some()
|
||||
);
|
||||
assert!(EhError::NoFlakeInputs.hint().is_some());
|
||||
// Variants without hints
|
||||
assert!(
|
||||
EhError::CommandFailed {
|
||||
|
|
@ -230,5 +266,6 @@ mod tests {
|
|||
.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 run;
|
||||
pub mod shell;
|
||||
pub mod update;
|
||||
pub mod util;
|
||||
|
||||
pub use clap::{CommandFactory, Parser, Subcommand};
|
||||
|
|
@ -34,4 +35,9 @@ pub enum Command {
|
|||
#[arg(trailing_var_arg = true)]
|
||||
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 run;
|
||||
mod shell;
|
||||
mod update;
|
||||
mod util;
|
||||
|
||||
fn main() {
|
||||
|
|
@ -17,11 +18,14 @@ fn main() {
|
|||
match result {
|
||||
Ok(code) => std::process::exit(code),
|
||||
Err(e) => {
|
||||
let code = e.exit_code();
|
||||
if code != 0 {
|
||||
eh_log::log_error!("{e}");
|
||||
if let Some(hint) = e.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",
|
||||
"ns" => "shell",
|
||||
"nb" => "build",
|
||||
"nu" => "update",
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
|
|
@ -60,6 +65,10 @@ fn dispatch_multicall(
|
|||
return Some(Ok(0));
|
||||
}
|
||||
|
||||
if subcommand == "update" {
|
||||
return Some(update::handle_update(&rest));
|
||||
}
|
||||
|
||||
let hash_extractor = util::RegexHashExtractor;
|
||||
let fixer = util::DefaultNixFileFixer;
|
||||
let classifier = util::DefaultNixErrorClassifier;
|
||||
|
|
@ -110,6 +119,8 @@ fn run_app() -> Result<i32> {
|
|||
build::handle_nix_build(&args, &hash_extractor, &fixer, &classifier)
|
||||
},
|
||||
|
||||
Some(Command::Update { args }) => update::handle_update(&args),
|
||||
|
||||
None => {
|
||||
Cli::command().print_help()?;
|
||||
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