diff --git a/src/commands.rs b/src/commands.rs index 76e9b7c6..ca8597a9 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -1124,7 +1124,7 @@ mod tests { fn test_build_new() { let installable = Installable::Flake { reference: "github:user/repo".to_string(), - attribute: vec!["package".to_string()], + attribute: "package".to_string(), }; let build = Build::new(installable.clone()); @@ -1140,7 +1140,7 @@ mod tests { fn test_build_builder_pattern() { let installable = Installable::Flake { reference: "github:user/repo".to_string(), - attribute: vec!["package".to_string()], + attribute: "package".to_string(), }; let build = Build::new(installable) diff --git a/src/darwin.rs b/src/darwin.rs index 55fc17d8..a6f69e27 100644 --- a/src/darwin.rs +++ b/src/darwin.rs @@ -86,10 +86,7 @@ impl DarwinRebuildArgs { Some(r) => r.to_owned(), None => return Err(eyre!("NH_DARWIN_FLAKE missing reference part")), }; - let attribute = elems - .next() - .map(crate::installable::parse_attribute) - .unwrap_or_default(); + let attribute = elems.next().unwrap_or_default().to_string(); Installable::Flake { reference, @@ -107,8 +104,7 @@ impl DarwinRebuildArgs { // If user explicitly selects some other attribute, don't push // darwinConfigurations if attribute.is_empty() { - attribute.push(String::from("darwinConfigurations")); - attribute.push(hostname.clone()); + *attribute = format!("darwinConfigurations.{hostname}"); } } @@ -205,10 +201,7 @@ impl DarwinReplArgs { Some(r) => r.to_owned(), None => return Err(eyre!("NH_DARWIN_FLAKE missing reference part")), }; - let attribute = elems - .next() - .map(crate::installable::parse_attribute) - .unwrap_or_default(); + let attribute = elems.next().unwrap_or_default().to_string(); Installable::Flake { reference, @@ -229,8 +222,7 @@ impl DarwinReplArgs { } = target_installable { if attribute.is_empty() { - attribute.push(String::from("darwinConfigurations")); - attribute.push(hostname); + *attribute = format!("darwinConfigurations.{hostname}"); } } diff --git a/src/home.rs b/src/home.rs index a67fdb1e..b79a3d51 100644 --- a/src/home.rs +++ b/src/home.rs @@ -69,10 +69,7 @@ impl HomeRebuildArgs { Some(r) => r.to_owned(), None => return Err(eyre!("NH_HOME_FLAKE missing reference part")), }; - let attribute = elems - .next() - .map(crate::installable::parse_attribute) - .unwrap_or_default(); + let attribute = elems.next().unwrap_or_default().to_string(); Installable::Flake { reference, @@ -223,7 +220,7 @@ where return Ok(res); } - attribute.push(String::from("homeConfigurations")); + *attribute = String::from("homeConfigurations"); let flake_reference = reference.clone(); let mut found_config = false; @@ -254,7 +251,7 @@ where if check_res.map(|s| s.trim().to_owned()).as_deref() == Some("true") { debug!("Using explicit configuration from flag: {config_name:?}"); - attribute.push(config_name); + attribute.push_str(&config_name); if push_drv { attribute.extend(toplevel.clone()); } @@ -264,7 +261,7 @@ where // Explicit config provided but not found let tried_attr_path = { let mut attr_path = attribute.clone(); - attr_path.push(config_name); + attr_path.push_str(&config_name); Installable::Flake { reference: flake_reference, attribute: attr_path, @@ -309,7 +306,7 @@ where let current_try_attr = { let mut attr_path = attribute.clone(); - attr_path.push(attr_name.clone()); + attr_path.push_str(&attr_name); attr_path }; tried.push(current_try_attr.clone()); @@ -318,7 +315,7 @@ where check_res.map(|s| s.trim().to_owned()).as_deref() { debug!("Using automatically detected configuration: {}", attr_name); - attribute.push(attr_name); + attribute.push_str(&attr_name); if push_drv { attribute.extend(toplevel.clone()); } @@ -379,10 +376,7 @@ impl HomeReplArgs { Some(r) => r.to_owned(), None => return Err(eyre!("NH_HOME_FLAKE missing reference part")), }; - let attribute = elems - .next() - .map(crate::installable::parse_attribute) - .unwrap_or_default(); + let attribute = elems.next().unwrap_or_default().to_string(); Installable::Flake { reference, diff --git a/src/installable.rs b/src/installable.rs index cc63aeb1..00777903 100644 --- a/src/installable.rs +++ b/src/installable.rs @@ -10,18 +10,18 @@ use yansi::{Color, Paint}; pub enum Installable { Flake { reference: String, - attribute: Vec, + attribute: String, }, File { path: PathBuf, - attribute: Vec, + attribute: String, }, Store { path: PathBuf, }, Expression { expression: String, - attribute: Vec, + attribute: String, }, } @@ -51,14 +51,14 @@ impl FromArgMatches for Installable { if let Some(f) = file { return Ok(Self::File { path: PathBuf::from(f), - attribute: parse_attribute(installable.cloned().unwrap_or_default()), + attribute: installable.cloned().unwrap_or_default(), }); } if let Some(e) = expr { return Ok(Self::Expression { expression: e.to_string(), - attribute: parse_attribute(installable.cloned().unwrap_or_default()), + attribute: installable.cloned().unwrap_or_default(), }); } @@ -67,12 +67,10 @@ impl FromArgMatches for Installable { let reference = elems.next().unwrap().to_owned(); return Ok(Self::Flake { reference, - attribute: parse_attribute( - elems - .next() - .map(std::string::ToString::to_string) - .unwrap_or_default(), - ), + attribute: elems + .next() + .map(std::string::ToString::to_string) + .unwrap_or_default(), }); } @@ -82,12 +80,10 @@ impl FromArgMatches for Installable { let mut elems = f.splitn(2, '#'); Installable::Flake { reference: elems.next().unwrap().to_owned(), - attribute: parse_attribute( - elems - .next() - .map(std::string::ToString::to_string) - .unwrap_or_default(), - ), + attribute: elems + .next() + .map(std::string::ToString::to_string) + .unwrap_or_default(), } }) } @@ -124,7 +120,7 @@ impl FromArgMatches for Installable { if let Ok(f) = env::var("NH_FILE") { return Ok(Self::File { path: PathBuf::from(f), - attribute: parse_attribute(env::var("NH_ATTRP").unwrap_or_default()), + attribute: env::var("NH_ATTRP").unwrap_or_default(), }); } @@ -141,6 +137,41 @@ impl FromArgMatches for Installable { impl Args for Installable { fn augment_args(cmd: clap::Command) -> clap::Command { + let nh_flake = env::var("NH_FLAKE").unwrap_or_default(); + let nh_os_flake = env::var("NH_OS_FLAKE").unwrap_or_default(); + let nh_home_flake = env::var("NH_HOME_FLAKE").unwrap_or_default(); + let nh_darwin_flake = env::var("NH_DARWIN_FLAKE").unwrap_or_default(); + let nh_file = env::var("NH_FILE").unwrap_or_default(); + let nh_attr = env::var("NH_ATTR").unwrap_or_default(); + + let long_help = format!( + r"Which installable to use. +Nix accepts various kinds of installables: + +[FLAKEREF[#ATTRPATH]] + Flake reference with an optional attribute path. + [env: NH_FLAKE={nh_flake}] + [env: NH_OS_FLAKE={nh_os_flake}] + [env: NH_HOME_FLAKE={nh_home_flake}] + [env: NH_DARWIN_FLAKE={nh_darwin_flake}] + +{f_short}, {f_long} [ATTRPATH] + Path to file with an optional attribute path. + [env: NH_FILE={nh_file}] + [env: NH_ATTRP={nh_attr}] + +{e_short}, {e_long} [ATTRPATH] + Nix expression with an optional attribute path. + +[PATH] + Path or symlink to a /nix/store path +", + f_short = "-f".yellow(), + f_long = "--file".yellow(), + e_short = "-e".yellow(), + e_long = "--expr".yellow(), + ); + cmd .arg( Arg::new("file") @@ -153,48 +184,16 @@ impl Args for Installable { Arg::new("expr") .short('E') .long("expr") - .conflicts_with("file") + .action(ArgAction::Set) .hide(true) - .action(ArgAction::Set), + .conflicts_with("file"), ) .arg( Arg::new("installable") .action(ArgAction::Set) .value_name("INSTALLABLE") .help("Which installable to use") - .long_help(format!( - r"Which installable to use. -Nix accepts various kinds of installables: - -[FLAKEREF[#ATTRPATH]] - Flake reference with an optional attribute path. - [env: NH_FLAKE={}] - [env: NH_OS_FLAKE={}] - [env: NH_HOME_FLAKE={}] - [env: NH_DARWIN_FLAKE={}] - -{}, {} [ATTRPATH] - Path to file with an optional attribute path. - [env: NH_FILE={}] - [env: NH_ATTRP={}] - -{}, {} [ATTRPATH] - Nix expression with an optional attribute path. - -[PATH] - Path or symlink to a /nix/store path -", - env::var("NH_FLAKE").unwrap_or_default(), - env::var("NH_OS_FLAKE").unwrap_or_default(), - env::var("NH_HOME_FLAKE").unwrap_or_default(), - env::var("NH_DARWIN_FLAKE").unwrap_or_default(), - Paint::new("-f").fg(Color::Yellow), - Paint::new("--file").fg(Color::Yellow), - env::var("NH_FILE").unwrap_or_default(), - env::var("NH_ATTR").unwrap_or_default(), - Paint::new("-e").fg(Color::Yellow), - Paint::new("--expr").fg(Color::Yellow), - )), + .long_help(long_help), ) } @@ -203,82 +202,35 @@ Nix accepts various kinds of installables: } } -// TODO: should handle quoted attributes, like foo."bar.baz" -> ["foo", -// "bar.baz"] maybe use chumsky? -pub fn parse_attribute(s: S) -> Vec -where - S: AsRef, -{ - let s = s.as_ref(); - let mut res = Vec::new(); - - if s.is_empty() { - return res; - } - - let mut in_quote = false; - - let mut elem = String::new(); - for char in s.chars() { - match char { - '.' => { - if in_quote { - elem.push(char); - } else { - res.push(elem.clone()); - elem = String::new(); - } - }, - '"' => { - in_quote = !in_quote; - }, - _ => elem.push(char), - } - } - - res.push(elem); - - assert!(!in_quote, "Failed to parse attribute: {s}"); - - res -} - -#[test] -fn test_parse_attribute() { - assert_eq!(parse_attribute(r"foo.bar"), vec!["foo", "bar"]); - assert_eq!(parse_attribute(r#"foo."bar.baz""#), vec!["foo", "bar.baz"]); - let v: Vec = vec![]; - assert_eq!(parse_attribute(""), v); -} - impl Installable { #[must_use] pub fn to_args(&self) -> Vec { - let mut res = Vec::new(); match self { Self::Flake { reference, attribute, } => { - res.push(format!("{reference}#{}", join_attribute(attribute))); + vec![format!("{reference}#{attribute}")] }, Self::File { path, attribute } => { - res.push(String::from("--file")); - res.push(path.to_str().unwrap().to_string()); - res.push(join_attribute(attribute)); + vec![ + String::from("--file"), + path.to_str().unwrap().to_string(), + attribute.to_string(), + ] }, Self::Expression { expression, attribute, } => { - res.push(String::from("--expr")); - res.push(expression.to_string()); - res.push(join_attribute(attribute)); + vec![ + String::from("--expr"), + expression.to_string(), + attribute.to_string(), + ] }, - Self::Store { path } => res.push(path.to_str().unwrap().to_string()), + Self::Store { path } => vec![path.to_str().unwrap().to_string()], } - - res } } @@ -303,38 +255,6 @@ fn test_installable_to_args() { ); } -fn join_attribute(attribute: I) -> String -where - I: IntoIterator, - I::Item: AsRef, -{ - let mut res = String::new(); - let mut first = true; - for elem in attribute { - if first { - first = false; - } else { - res.push('.'); - } - - let s = elem.as_ref(); - - if s.contains('.') { - res.push_str(&format!(r#""{s}""#)); - } else { - res.push_str(s); - } - } - - res -} - -#[test] -fn test_join_attribute() { - assert_eq!(join_attribute(vec!["foo", "bar"]), "foo.bar"); - assert_eq!(join_attribute(vec!["foo", "bar.baz"]), r#"foo."bar.baz""#); -} - impl Installable { #[must_use] pub const fn str_kind(&self) -> &str { diff --git a/src/nixos.rs b/src/nixos.rs index 8f7b6f8a..0d97448a 100644 --- a/src/nixos.rs +++ b/src/nixos.rs @@ -718,10 +718,7 @@ fn get_nh_os_flake_env() -> Result> { .next() .ok_or_else(|| eyre!("NH_OS_FLAKE missing reference part"))? .to_owned(); - let attribute = elems - .next() - .map(crate::installable::parse_attribute) - .unwrap_or_default(); + let attribute = elems.next().unwrap_or_default().to_string(); Ok(Some(Installable::Flake { reference, @@ -840,9 +837,7 @@ pub fn toplevel_for>( let mut res = installable; let hostname_str = hostname.as_ref(); - let toplevel = ["config", "system", "build", final_attr] - .into_iter() - .map(String::from); + let toplevel = format!(".config.system.build.{final_attr}"); match res { Installable::Flake { @@ -851,17 +846,16 @@ pub fn toplevel_for>( // If user explicitly selects some other attribute, don't push // nixosConfigurations if attribute.is_empty() { - attribute.push(String::from("nixosConfigurations")); - attribute.push(hostname_str.to_owned()); + *attribute = format!("nixosConfigurations.{hostname_str}"); } - attribute.extend(toplevel); + attribute.push_str(&toplevel); }, Installable::File { ref mut attribute, .. } | Installable::Expression { ref mut attribute, .. - } => attribute.extend(toplevel), + } => attribute.push_str(&toplevel), Installable::Store { .. } => {}, } @@ -890,8 +884,7 @@ impl OsReplArgs { } = target_installable { if attribute.is_empty() { - attribute.push(String::from("nixosConfigurations")); - attribute.push(hostname); + *attribute = format!("nixosConfigurations.{hostname}"); } } diff --git a/src/util/platform.rs b/src/util/platform.rs index bab80ddb..4296ab5d 100644 --- a/src/util/platform.rs +++ b/src/util/platform.rs @@ -1,379 +1,388 @@ -use std::env; -use std::ffi::OsString; -use std::path::{Path, PathBuf}; - -use color_eyre::Result; -use color_eyre::eyre::WrapErr; -use color_eyre::eyre::bail; +use std::{ + env, + ffi::OsString, + path::{Path, PathBuf}, +}; + +use color_eyre::{ + Result, + eyre::{WrapErr, bail}, +}; use tracing::{debug, info, warn}; -use crate::commands; -use crate::installable::Installable; -use crate::interface::NixBuildPassthroughArgs; +use crate::{ + commands, + installable::Installable, + interface::NixBuildPassthroughArgs, + util::get_hostname, +}; /// Resolves an Installable from an environment variable. /// -/// Returns `Some(Installable)` if the environment variable is set and can be parsed, -/// or `None` if the environment variable is not set. +/// Returns `Some(Installable)` if the environment variable is set and can be +/// parsed, or `None` if the environment variable is not set. pub fn resolve_env_installable(var: &str) -> Option { - env::var(var).ok().map(|val| { - let mut elems = val.splitn(2, '#'); - let reference = elems.next().unwrap().to_owned(); - let attribute = elems - .next() - .map(crate::installable::parse_attribute) - .unwrap_or_default(); - Installable::Flake { - reference, - attribute, - } - }) + env::var(var).ok().map(|val| { + let mut elems = val.splitn(2, '#'); + let reference = elems.next().unwrap().to_owned(); + let attribute = elems + .next() + .map(crate::installable::parse_attribute) + .unwrap_or_default(); + Installable::Flake { + reference, + attribute, + } + }) } /// Extends an Installable with the appropriate attribute path for a platform. /// -/// - `config_type`: e.g. "homeConfigurations", "nixosConfigurations", "darwinConfigurations" +/// - `config_type`: e.g. "homeConfigurations", "nixosConfigurations", +/// "darwinConfigurations" /// - `extra_path`: e.g. ["config", "home", "activationPackage"] /// - `config_name`: Optional configuration name (e.g. username@hostname) /// - `push_drv`: Whether to push the drv path (platform-specific) /// - `extra_args`: Extra args for nix eval (for config detection) pub fn extend_installable_for_platform( - mut installable: Installable, - config_type: &str, - extra_path: &[&str], - config_name: Option, - push_drv: bool, - extra_args: &[OsString], + mut installable: Installable, + config_type: &str, + extra_path: &[&str], + config_name: Option, + push_drv: bool, + extra_args: &[OsString], ) -> Result { - use tracing::debug; - - use crate::util::get_hostname; - - match &mut installable { - Installable::Flake { - reference, - attribute, - } => { - // If attribute path is already specified, use it as-is - if !attribute.is_empty() { - debug!( - "Using explicit attribute path from installable: {:?}", - attribute - ); - return Ok(installable); - } - - // Otherwise, build the attribute path - attribute.push(config_type.to_string()); - let flake_reference = reference.clone(); - - // Try to find the configuration by name if one was provided - if let Some(config_name) = config_name { - if find_config_in_flake( - &config_name, - attribute, - &flake_reference, - extra_args, - push_drv, - extra_path, - )? { - return Ok(installable); - } - - return Err(color_eyre::eyre::eyre!( - "Explicitly specified configuration not found in flake." - )); - } - - // Try to auto-detect the configuration - let username = std::env::var("USER").unwrap_or_else(|_| "user".to_string()); - let hostname = get_hostname().unwrap_or_else(|_| "host".to_string()); - - for attr_name in [format!("{username}@{hostname}"), username] { - if find_config_in_flake( - &attr_name, - attribute, - &flake_reference, - extra_args, - push_drv, - extra_path, - )? { - return Ok(installable); - } - } - - return Err(color_eyre::eyre::eyre!( - "Couldn't find configuration automatically in flake." - )); - } - Installable::File { attribute, .. } | Installable::Expression { attribute, .. } => { - if push_drv { - attribute.extend(extra_path.iter().map(|s| (*s).to_string())); - } + match &mut installable { + Installable::Flake { + reference, + attribute, + } => { + // If attribute path is already specified, use it as-is + if !attribute.is_empty() { + debug!("Using explicit attribute path from installable: {attribute}"); + return Ok(installable); + } + + // Otherwise, build the attribute path + attribute.push(config_type.to_string()); + let flake_reference = reference.clone(); + + // Try to find the configuration by name if one was provided + if let Some(config_name) = config_name { + if find_config_in_flake( + &config_name, + attribute, + &flake_reference, + extra_args, + push_drv, + extra_path, + )? { + return Ok(installable); } - Installable::Store { .. } => { - // Nothing to do for store paths + + return Err(color_eyre::eyre::eyre!( + "Explicitly specified configuration not found in flake." + )); + } + + // Try to auto-detect the configuration + let username = + std::env::var("USER").unwrap_or_else(|_| "user".to_string()); + let hostname = get_hostname().unwrap_or_else(|_| "host".to_string()); + + for attr_name in [format!("{username}@{hostname}"), username] { + if find_config_in_flake( + &attr_name, + attribute, + &flake_reference, + extra_args, + push_drv, + extra_path, + )? { + return Ok(installable); } - } - Ok(installable) + } + + return Err(color_eyre::eyre::eyre!( + "Couldn't find configuration automatically in flake." + )); + }, + Installable::File { attribute, .. } + | Installable::Expression { attribute, .. } => { + if push_drv { + attribute.extend(extra_path.iter().map(|s| (*s).to_string())); + } + }, + Installable::Store { .. } => { + // Nothing to do for store paths + }, + } + Ok(installable) } /// Find a configuration in a flake /// /// Returns true if the configuration was found, false otherwise fn find_config_in_flake( - config_name: &str, - attribute: &mut Vec, - flake_reference: &str, - extra_args: &[OsString], - push_drv: bool, - extra_path: &[&str], -) -> Result { - let func = format!(r#"x: x ? "{config_name}""#); - let check_res = commands::Command::new("nix") - .arg("eval") - .args(extra_args) - .arg("--apply") - .arg(&func) - .args( - (Installable::Flake { - reference: flake_reference.to_string(), - attribute: attribute.clone(), - }) - .to_args(), - ) - .run_capture(); - - if let Ok(res) = check_res { - if res.map(|s| s.trim().to_owned()).as_deref() == Some("true") { - debug!("Found configuration: {}", config_name); - attribute.push(config_name.to_string()); - - if push_drv { - attribute.extend(extra_path.iter().map(|s| (*s).to_string())); - } - - return Ok(true); - } + config_name: &str, + attribute: &mut Vec, + flake_reference: &str, + extra_args: &[OsString], + push_drv: bool, + extra_path: &[&str], +) -> bool { + let func = format!(r#"x: x ? "{config_name}""#); + let check_res = commands::Command::new("nix") + .arg("eval") + .args(extra_args) + .arg("--apply") + .arg(&func) + .args( + (Installable::Flake { + reference: flake_reference.to_string(), + attribute: attribute.clone(), + }) + .to_args(), + ) + .run_capture(); + + if let Ok(res) = check_res { + if res.map(|s| s.trim().to_owned()).as_deref() == Some("true") { + debug!("Found configuration: {}", config_name); + attribute.push(config_name.to_string()); + + if push_drv { + attribute.extend(extra_path.iter().map(|s| (*s).to_string())); + } + + return true; } + } - Ok(false) + false } /// Handles common specialisation logic for all platforms pub fn handle_specialisation( - specialisation_path: &str, - no_specialisation: bool, - explicit_specialisation: Option, + specialisation_path: &str, + no_specialisation: bool, + explicit_specialisation: Option, ) -> Option { - if no_specialisation { - None - } else { - let current_specialisation = std::fs::read_to_string(specialisation_path).ok(); - explicit_specialisation.or(current_specialisation) - } + if no_specialisation { + None + } else { + let current_specialisation = + std::fs::read_to_string(specialisation_path).ok(); + explicit_specialisation.or(current_specialisation) + } } /// Checks if the user wants to proceed with applying the configuration pub fn confirm_action(ask: bool, dry: bool) -> Result { - use tracing::{info, warn}; + use tracing::{info, warn}; - if dry { - if ask { - warn!("--ask has no effect as dry run was requested"); - } - return Ok(false); + if dry { + if ask { + warn!("--ask has no effect as dry run was requested"); } + return Ok(false); + } - if ask { - info!("Apply the config?"); - let confirmation = Confirm::new("Apply the config?") - .with_default(false) - .prompt()?; + if ask { + info!("Apply the config?"); + let confirmation = Confirm::new("Apply the config?") + .with_default(false) + .prompt()?; - if !confirmation { - bail!("User rejected the new config"); - } + if !confirmation { + bail!("User rejected the new config"); } + } - Ok(true) + Ok(true) } /// Common function to ensure we're not running as root pub fn check_not_root(bypass_root_check: bool) -> Result { - use tracing::warn; + use tracing::warn; - if bypass_root_check { - warn!("Bypassing root check, now running nix as root"); - return Ok(false); - } + if bypass_root_check { + warn!("Bypassing root check, now running nix as root"); + return Ok(false); + } - if nix::unistd::Uid::effective().is_root() { - // Protect users from themselves - bail!("Don't run nh os as root. I will call sudo internally as needed"); - } + if nix::unistd::Uid::effective().is_root() { + // Protect users from themselves + bail!("Don't run nh os as root. I will call sudo internally as needed"); + } - Ok(true) + Ok(true) } /// Creates a temporary output path for build results pub fn create_output_path( - out_link: Option>, - prefix: &str, + out_link: Option>, + prefix: &str, ) -> Result> { - let out_path: Box = match out_link { - Some(ref p) => Box::new(std::path::PathBuf::from(p.as_ref())), - None => Box::new({ - let dir = tempfile::Builder::new().prefix(prefix).tempdir()?; - (dir.as_ref().join("result"), dir) - }), - }; - - Ok(out_path) + let out_path: Box = match out_link { + Some(ref p) => Box::new(std::path::PathBuf::from(p.as_ref())), + None => { + Box::new({ + let dir = tempfile::Builder::new().prefix(prefix).tempdir()?; + (dir.as_ref().join("result"), dir) + }) + }, + }; + + Ok(out_path) } /// Compare configurations using nvd diff pub fn compare_configurations( - current_profile: &str, - target_profile: &std::path::Path, - skip_compare: bool, - message: &str, + current_profile: &str, + target_profile: &std::path::Path, + skip_compare: bool, + message: &str, ) -> Result<()> { - if skip_compare { - debug!("Skipping configuration comparison"); - return Ok(()); - } + if skip_compare { + debug!("Skipping configuration comparison"); + return Ok(()); + } + + commands::Command::new("nvd") + .arg("diff") + .arg(current_profile) + .arg(target_profile) + .message(message) + .run() + .with_context(|| { + format!( + "Failed to compare configurations with nvd: {} vs {}", + current_profile, + target_profile.display() + ) + })?; - commands::Command::new("nvd") - .arg("diff") - .arg(current_profile) - .arg(target_profile) - .message(message) - .run() - .with_context(|| { - format!( - "Failed to compare configurations with nvd: {} vs {}", - current_profile, - target_profile.display() - ) - })?; - - Ok(()) + Ok(()) } /// Build a configuration using the nix build command pub fn build_configuration( - installable: Installable, - out_path: &dyn crate::util::MaybeTempPath, - extra_args: &[impl AsRef], - builder: Option, - message: &str, - no_nom: bool, - passthrough_args: NixBuildPassthroughArgs, + installable: Installable, + out_path: &dyn crate::util::MaybeTempPath, + extra_args: &[impl AsRef], + builder: Option, + message: &str, + no_nom: bool, + passthrough_args: NixBuildPassthroughArgs, ) -> Result<()> { - let passthrough = passthrough_args.parse_passthrough_args()?; - - commands::Build::new(installable) - .extra_arg("--out-link") - .extra_arg(out_path.get_path()) - .extra_args(extra_args) - .passthrough(&self.passthrough) - .builder(builder) - .message(message) - .nom(!no_nom) - .run() - .with_context(|| format!("Failed to build configuration: {}", message))?; - - Ok(()) + let passthrough = passthrough_args.parse_passthrough_args()?; + + commands::Build::new(installable) + .extra_arg("--out-link") + .extra_arg(out_path.get_path()) + .extra_args(extra_args) + .passthrough(&self.passthrough) + .builder(builder) + .message(message) + .nom(!no_nom) + .run() + .with_context(|| format!("Failed to build configuration: {}", message))?; + + Ok(()) } /// Determine the target profile path considering specialisation pub fn get_target_profile( - out_path: &dyn crate::util::MaybeTempPath, - target_specialisation: &Option, + out_path: &dyn crate::util::MaybeTempPath, + target_specialisation: &Option, ) -> PathBuf { - match target_specialisation { - None => out_path.get_path().to_owned(), - Some(spec) => out_path.get_path().join("specialisation").join(spec), - } + match target_specialisation { + None => out_path.get_path().to_owned(), + Some(spec) => out_path.get_path().join("specialisation").join(spec), + } } /// Common logic for handling REPL for different platforms pub fn run_repl( - installable: Installable, - config_type: &str, - extra_path: &[&str], - config_name: Option, - extra_args: &[String], + installable: Installable, + config_type: &str, + extra_path: &[&str], + config_name: Option, + extra_args: &[String], ) -> Result<()> { - // Store paths don't work with REPL - if let Installable::Store { .. } = installable { - bail!("Nix doesn't support nix store installables with repl."); - } - - let installable = extend_installable_for_platform( - installable, - config_type, - extra_path, - config_name, - false, - &[], - )?; - - debug!("Running nix repl with installable: {:?}", installable); - - // NOTE: Using stdlib Command directly is necessary for interactive REPL - // Interactivity implodes otherwise. - use std::process::{Command as StdCommand, Stdio}; - - let mut command = StdCommand::new("nix"); - command.arg("repl"); - - // Add installable arguments - for arg in installable.to_args() { - command.arg(arg); - } - - // Add any extra arguments - for arg in extra_args { - command.arg(arg); - } - - // Configure for interactive use - command - .stdin(Stdio::inherit()) - .stdout(Stdio::inherit()) - .stderr(Stdio::inherit()); - - // Execute and wait for completion - let status = command.status()?; - - if !status.success() { - bail!("nix repl exited with non-zero status: {}", status); - } - - Ok(()) + // Store paths don't work with REPL + if let Installable::Store { .. } = installable { + bail!("Nix doesn't support nix store installables with repl."); + } + + let installable = extend_installable_for_platform( + installable, + config_type, + extra_path, + config_name, + false, + &[], + )?; + + debug!("Running nix repl with installable: {:?}", installable); + + // NOTE: Using stdlib Command directly is necessary for interactive REPL + // Interactivity implodes otherwise. + use std::process::{Command as StdCommand, Stdio}; + + let mut command = StdCommand::new("nix"); + command.arg("repl"); + + // Add installable arguments + for arg in installable.to_args() { + command.arg(arg); + } + + // Add any extra arguments + for arg in extra_args { + command.arg(arg); + } + + // Configure for interactive use + command + .stdin(Stdio::inherit()) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()); + + // Execute and wait for completion + let status = command.status()?; + + if !status.success() { + bail!("nix repl exited with non-zero status: {}", status); + } + + Ok(()) } /// Process the target specialisation based on common patterns pub fn process_specialisation( - no_specialisation: bool, - specialisation: Option, - specialisation_path: &str, + no_specialisation: bool, + specialisation: Option, + specialisation_path: &str, ) -> Result> { - let target_specialisation = - handle_specialisation(specialisation_path, no_specialisation, specialisation); + let target_specialisation = handle_specialisation( + specialisation_path, + no_specialisation, + specialisation, + ); - debug!("target_specialisation: {target_specialisation:?}"); + debug!("target_specialisation: {target_specialisation:?}"); - Ok(target_specialisation) + Ok(target_specialisation) } /// Execute common actions for a rebuild operation across platforms /// /// This function handles the core workflow for building and managing system /// configurations across different platforms (`NixOS`, Darwin, Home Manager). -/// It unifies what would otherwise be duplicated across platform-specific modules. +/// It unifies what would otherwise be duplicated across platform-specific +/// modules. /// /// The function takes care of: /// 1. Properly configuring the attribute path based on platform type @@ -384,7 +393,8 @@ pub fn process_specialisation( /// # Arguments /// /// * `installable` - The Nix installable representing the configuration -/// * `config_type` - The configuration type (e.g., "nixosConfigurations", "darwinConfigurations") +/// * `config_type` - The configuration type (e.g., "nixosConfigurations", +/// "darwinConfigurations") /// * `extra_path` - Additional path elements for the attribute path /// * `config_name` - Optional hostname or configuration name /// * `out_path` - Output path for the build result @@ -396,270 +406,287 @@ pub fn process_specialisation( /// * `no_specialisation` - Whether to ignore specialisations /// * `specialisation` - Optional explicit specialisation to use /// * `current_profile` - Path to the current system profile for comparison -/// * `skip_compare` - Whether to skip comparing the new and current configuration +/// * `skip_compare` - Whether to skip comparing the new and current +/// configuration /// /// # Returns /// /// The path to the built configuration, which can be used for activation #[allow(clippy::too_many_arguments)] pub fn handle_rebuild_workflow( - installable: Installable, - config_type: &str, - extra_path: &[&str], - config_name: Option, - out_path: &dyn crate::util::MaybeTempPath, - extra_args: &[impl AsRef], - builder: Option, - message: &str, - no_nom: bool, - specialisation_path: &str, - no_specialisation: bool, - specialisation: Option, - current_profile: &str, - skip_compare: bool, - passthrough_args: NixBuildPassthroughArgs, + installable: Installable, + config_type: &str, + extra_path: &[&str], + config_name: Option, + out_path: &dyn crate::util::MaybeTempPath, + extra_args: &[impl AsRef], + builder: Option, + message: &str, + no_nom: bool, + specialisation_path: &str, + no_specialisation: bool, + specialisation: Option, + current_profile: &str, + skip_compare: bool, + passthrough_args: NixBuildPassthroughArgs, ) -> Result { - // Convert the extra_args to OsString for the config struct - let extra_args_vec: Vec = extra_args - .iter() - .map(|arg| arg.as_ref().to_os_string()) - .collect(); - - // Create a config struct from the parameters - let config = RebuildWorkflowConfig { - installable, - config_type, - extra_path, - config_name, - out_path, - extra_args: extra_args_vec, - builder, - message, - no_nom, - specialisation_path, - no_specialisation, - specialisation, - current_profile, - skip_compare, - passthrough_args, - }; - - // Delegate to the new implementation - handle_rebuild_workflow_with_config(config) + // Convert the extra_args to OsString for the config struct + let extra_args_vec: Vec = extra_args + .iter() + .map(|arg| arg.as_ref().to_os_string()) + .collect(); + + // Create a config struct from the parameters + let config = RebuildWorkflowConfig { + installable, + config_type, + extra_path, + config_name, + out_path, + extra_args: extra_args_vec, + builder, + message, + no_nom, + specialisation_path, + no_specialisation, + specialisation, + current_profile, + skip_compare, + passthrough_args, + }; + + // Delegate to the new implementation + handle_rebuild_workflow_with_config(config) } /// Determine proper hostname based on provided or automatically detected pub fn get_target_hostname( - explicit_hostname: Option, - skip_if_mismatch: bool, + explicit_hostname: Option, + skip_if_mismatch: bool, ) -> Result<(String, bool)> { - let system_hostname = match crate::util::get_hostname() { - Ok(hostname) => { - debug!("Auto-detected hostname: {}", hostname); - Some(hostname) - } - Err(err) => { - warn!("Failed to detect hostname: {}", err); - None - } - }; - - let target_hostname = match explicit_hostname { + let system_hostname = match crate::util::get_hostname() { + Ok(hostname) => { + debug!("Auto-detected hostname: {}", hostname); + Some(hostname) + }, + Err(err) => { + warn!("Failed to detect hostname: {}", err); + None + }, + }; + + let target_hostname = match explicit_hostname { + Some(hostname) => hostname, + None => { + match system_hostname.clone() { Some(hostname) => hostname, - None => match system_hostname.clone() { - Some(hostname) => hostname, - None => bail!( - "Unable to fetch hostname automatically. Please specify explicitly with --hostname." - ), + None => { + bail!( + "Unable to fetch hostname automatically. Please specify \ + explicitly with --hostname." + ) }, - }; - - // Skip comparison when system hostname != target hostname if requested - let hostname_mismatch = skip_if_mismatch - && system_hostname.is_some() - && system_hostname.unwrap() != target_hostname; - - debug!( - ?target_hostname, - ?hostname_mismatch, - "Determined target hostname" - ); - Ok((target_hostname, hostname_mismatch)) + } + }, + }; + + // Skip comparison when system hostname != target hostname if requested + let hostname_mismatch = skip_if_mismatch + && system_hostname.is_some() + && system_hostname.unwrap() != target_hostname; + + debug!( + ?target_hostname, + ?hostname_mismatch, + "Determined target hostname" + ); + Ok((target_hostname, hostname_mismatch)) } /// Common function to activate configurations in `NixOS` pub fn activate_nixos_configuration( - target_profile: &Path, - variant: &str, - target_host: Option, - elevate: bool, - message: &str, + target_profile: &Path, + variant: &str, + target_host: Option, + elevate: bool, + message: &str, ) -> Result<()> { - let switch_to_configuration = target_profile.join("bin").join("switch-to-configuration"); - let switch_to_configuration = switch_to_configuration.canonicalize().map_err(|e| { - color_eyre::eyre::eyre!("Failed to canonicalize switch-to-configuration path: {}", e) + let switch_to_configuration = + target_profile.join("bin").join("switch-to-configuration"); + let switch_to_configuration = + switch_to_configuration.canonicalize().map_err(|e| { + color_eyre::eyre::eyre!( + "Failed to canonicalize switch-to-configuration path: {}", + e + ) })?; - commands::Command::new(switch_to_configuration) - .arg(variant) - .ssh(target_host) - .message(message) - .elevate(elevate) - .run() + commands::Command::new(switch_to_configuration) + .arg(variant) + .ssh(target_host) + .message(message) + .elevate(elevate) + .run() } /// Configuration options for rebuilding workflows pub struct RebuildWorkflowConfig<'a> { - /// The Nix installable representing the configuration - pub installable: Installable, + /// The Nix installable representing the configuration + pub installable: Installable, - /// The configuration type (e.g., "nixosConfigurations", "darwinConfigurations") - pub config_type: &'a str, + /// The configuration type (e.g., "nixosConfigurations", + /// "darwinConfigurations") + pub config_type: &'a str, - /// Additional path elements for the attribute path - pub extra_path: &'a [&'a str], + /// Additional path elements for the attribute path + pub extra_path: &'a [&'a str], - /// Optional hostname or configuration name - pub config_name: Option, + /// Optional hostname or configuration name + pub config_name: Option, - /// Output path for the build result - pub out_path: &'a dyn crate::util::MaybeTempPath, + /// Output path for the build result + pub out_path: &'a dyn crate::util::MaybeTempPath, - /// Additional arguments to pass to the build command as OsStrings - pub extra_args: Vec, + /// Additional arguments to pass to the build command as OsStrings + pub extra_args: Vec, - /// Optional remote builder to use - pub builder: Option, + /// Optional remote builder to use + pub builder: Option, - /// Message to display during the build process - pub message: &'a str, + /// Message to display during the build process + pub message: &'a str, - /// Whether to disable nix-output-monitor - pub no_nom: bool, + /// Whether to disable nix-output-monitor + pub no_nom: bool, - /// Path to read specialisations from - pub specialisation_path: &'a str, + /// Path to read specialisations from + pub specialisation_path: &'a str, - /// Whether to ignore specialisations - pub no_specialisation: bool, + /// Whether to ignore specialisations + pub no_specialisation: bool, - /// Optional explicit specialisation to use - pub specialisation: Option, + /// Optional explicit specialisation to use + pub specialisation: Option, - /// Path to the current system profile for comparison - pub current_profile: &'a str, + /// Path to the current system profile for comparison + pub current_profile: &'a str, - /// Whether to skip comparing the new and current configuration - pub skip_compare: bool, + /// Whether to skip comparing the new and current configuration + pub skip_compare: bool, - /// Arguments to pass to Nix - pub passthrough_args: NixBuildPassthroughArgs, + /// Arguments to pass to Nix + pub passthrough_args: NixBuildPassthroughArgs, } -/// Execute common actions for a rebuild operation across platforms using configuration struct +/// Execute common actions for a rebuild operation across platforms using +/// configuration struct /// -/// This function takes a configuration struct instead of many individual parameters -fn handle_rebuild_workflow_with_config(config: RebuildWorkflowConfig) -> Result { - // Special handling for darwin configurations - if config.config_type == "darwinConfigurations" { - // First construct the proper attribute path for darwin configs - let mut processed_installable = config.installable; - if let Installable::Flake { - ref mut attribute, .. - } = processed_installable - { - // Only set the attribute path if user hasn't already specified one - if attribute.is_empty() { - attribute.push(String::from(config.config_type)); - if let Some(name) = &config.config_name { - attribute.push(name.clone()); - } - } - } - - // Next, add config.system.build. to the path to access the derivation - let mut toplevel_attr = processed_installable; - if let Installable::Flake { - ref mut attribute, .. - } = toplevel_attr - { - // All darwin configurations expose their outputs under system.build - let toplevel_path = ["config", "system", "build"]; - attribute.extend(toplevel_path.iter().map(|s| (*s).to_string())); - - // Add the final component (usually "toplevel") - if !config.extra_path.is_empty() { - attribute.push(config.extra_path[0].to_string()); - } - } - - // Build the configuration - build_configuration( - toplevel_attr, - config.out_path, - &config.extra_args, - config.builder.clone(), - config.message, - config.no_nom, - config.passthrough_args, - )?; - - // Darwin doesn't use the specialisation mechanism like NixOS - let target_profile = config.out_path.get_path().to_owned(); - - // Run the diff to show changes - if !config.skip_compare { - compare_configurations( - config.current_profile, - &target_profile, - false, - "Comparing changes", - )?; +/// This function takes a configuration struct instead of many individual +/// parameters +fn handle_rebuild_workflow_with_config( + config: RebuildWorkflowConfig, +) -> Result { + // Special handling for darwin configurations + if config.config_type == "darwinConfigurations" { + // First construct the proper attribute path for darwin configs + let mut processed_installable = config.installable; + if let Installable::Flake { + ref mut attribute, .. + } = processed_installable + { + // Only set the attribute path if user hasn't already specified one + if attribute.is_empty() { + attribute.push(String::from(config.config_type)); + if let Some(name) = &config.config_name { + attribute.push(name.clone()); } - - return Ok(target_profile); + } } - // Configure the installable with platform-specific attributes - let configured_installable = extend_installable_for_platform( - config.installable, - config.config_type, - config.extra_path, - config.config_name.clone(), - true, - &config.extra_args, - )?; + // Next, add config.system.build. to the path to access the derivation + let mut toplevel_attr = processed_installable; + if let Installable::Flake { + ref mut attribute, .. + } = toplevel_attr + { + // All darwin configurations expose their outputs under system.build + let toplevel_path = ["config", "system", "build"]; + attribute.extend(toplevel_path.iter().map(|s| (*s).to_string())); + + // Add the final component (usually "toplevel") + if !config.extra_path.is_empty() { + attribute.push(config.extra_path[0].to_string()); + } + } // Build the configuration build_configuration( - configured_installable, - config.out_path, - &config.extra_args, - config.builder.clone(), - config.message, - config.no_nom, - config.passthrough_args, - )?; - - // Process any specialisations (NixOS/Home-Manager specific feature) - let target_specialisation = process_specialisation( - config.no_specialisation, - config.specialisation.clone(), - config.specialisation_path, + toplevel_attr, + config.out_path, + &config.extra_args, + config.builder.clone(), + config.message, + config.no_nom, + config.passthrough_args, )?; - // Get target profile path - let target_profile = get_target_profile(config.out_path, &target_specialisation); + // Darwin doesn't use the specialisation mechanism like NixOS + let target_profile = config.out_path.get_path().to_owned(); - // Compare configurations if applicable + // Run the diff to show changes if !config.skip_compare { - compare_configurations( - config.current_profile, - &target_profile, - false, - "Comparing changes", - )?; + compare_configurations( + config.current_profile, + &target_profile, + false, + "Comparing changes", + )?; } - Ok(target_profile) + return Ok(target_profile); + } + + // Configure the installable with platform-specific attributes + let configured_installable = extend_installable_for_platform( + config.installable, + config.config_type, + config.extra_path, + config.config_name.clone(), + true, + &config.extra_args, + )?; + + // Build the configuration + build_configuration( + configured_installable, + config.out_path, + &config.extra_args, + config.builder.clone(), + config.message, + config.no_nom, + config.passthrough_args, + )?; + + // Process any specialisations (NixOS/Home-Manager specific feature) + let target_specialisation = process_specialisation( + config.no_specialisation, + config.specialisation.clone(), + config.specialisation_path, + )?; + + // Get target profile path + let target_profile = + get_target_profile(config.out_path, &target_specialisation); + + // Compare configurations if applicable + if !config.skip_compare { + compare_configurations( + config.current_profile, + &target_profile, + false, + "Comparing changes", + )?; + } + + Ok(target_profile) }