diff --git a/crates/system-manager-engine/src/lib.rs b/crates/system-manager-engine/src/lib.rs index 5657f8e0..96f07c7a 100644 --- a/crates/system-manager-engine/src/lib.rs +++ b/crates/system-manager-engine/src/lib.rs @@ -105,7 +105,9 @@ pub struct NixOptions { } pub struct NixBuildOptions { - pub flake_uri: String, + pub is_flake: bool, + pub path: String, + pub attr: Option, pub refresh: bool, } diff --git a/crates/system-manager-engine/src/register.rs b/crates/system-manager-engine/src/register.rs index 40e6c98c..ee0e0510 100644 --- a/crates/system-manager-engine/src/register.rs +++ b/crates/system-manager-engine/src/register.rs @@ -1,4 +1,4 @@ -use anyhow::{anyhow, Context, Result}; +use anyhow::{Context, Result}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::fs::DirBuilder; @@ -68,15 +68,13 @@ fn create_gcroot(gcroot_path: &str, profile_path: &Path) -> Result<()> { create_store_link(&store_path, Path::new(gcroot_path)) } -pub fn build( - nix_build_options: &mut NixBuildOptions, - nix_options: &NixOptions, -) -> Result { - nix_build_options.flake_uri = find_flake_attr(nix_build_options, nix_options)?; +pub fn build(nix_build_options: &NixBuildOptions, nix_options: &NixOptions) -> Result { + let attr = find_flake_attr(nix_build_options, nix_options)?; log::info!("Building new system-manager generation..."); log::info!("Running nix build..."); - let store_path = run_nix_build(nix_build_options, nix_options).and_then(get_store_path)?; + let store_path = + run_nix_build(nix_build_options, attr, nix_options).and_then(get_store_path)?; log::info!("Built system-manager profile {store_path}"); Ok(store_path) } @@ -85,50 +83,27 @@ fn find_flake_attr( nix_build_options: &NixBuildOptions, nix_options: &NixOptions, ) -> Result { - let flake_uri = nix_build_options.flake_uri.trim_end_matches('#'); - let mut splitted = flake_uri.split('#'); - let flake = splitted - .next() - .ok_or_else(|| anyhow!("Invalid flake URI: {flake_uri}"))?; - let attr = splitted.next(); - - if splitted.next().is_some() { - anyhow::bail!("Invalid flake URI, too many '#'s: {flake_uri}"); - } - let system = get_nix_system(nix_options)?; - - if let Some(attr) = attr { - let Some(full_uri) = - try_flake_attr(flake, attr, nix_options, &system, nix_build_options.refresh)? - else { + let path = &nix_build_options.path; + if let Some(attr) = &nix_build_options.attr { + let Some(full_attr) = try_flake_attr(nix_build_options, attr, nix_options, &system)? else { anyhow::bail!( - "Explicitly provided flake URI does not point to a valid system-manager configuration: {flake}#{attr}" + "Explicitly provided flake URI does not point to a valid system-manager configuration: {path}#{attr}" ) }; - return Ok(full_uri); + return Ok(full_attr); } let hostname_os = nix::unistd::gethostname()?; let hostname = escape_nix_string(&hostname_os.to_string_lossy()); let default = "default"; - if let Some(full_uri) = try_flake_attr( - flake, - &hostname, - nix_options, - &system, - nix_build_options.refresh, - )? { - return Ok(full_uri); - } else if let Some(full_uri) = try_flake_attr( - flake, - default, - nix_options, - &system, - nix_build_options.refresh, - )? { - return Ok(full_uri); + if let Some(full_attr) = try_flake_attr(nix_build_options, &hostname, nix_options, &system)? { + return Ok(full_attr); + } else if let Some(full_attr) = + try_flake_attr(nix_build_options, default, nix_options, &system)? + { + return Ok(full_attr); }; anyhow::bail!("No suitable flake attribute found, giving up."); } @@ -147,21 +122,20 @@ fn escape_nix_string(s: &str) -> String { } fn try_flake_attr( - flake: &str, + nix_build_options: &NixBuildOptions, attr: &str, nix_options: &NixOptions, system: &str, - refresh: bool, ) -> Result> { let try_flake_attr_impl = |attr: &str| { - let full_uri = format!("{flake}#{FLAKE_ATTR}.{attr}"); - log::info!("Trying flake URI: {full_uri}..."); - let status = try_nix_eval(flake, attr, nix_options, refresh)?; + let full_attr = format!("{FLAKE_ATTR}.{attr}"); + log::info!("Trying attribute: {full_attr}..."); + let status = try_nix_eval(nix_build_options, attr, nix_options)?; if status { - log::info!("Success, using {full_uri}"); - Ok(Some(full_uri)) + log::info!("Success, using {full_attr}"); + Ok(Some(full_attr)) } else { - log::info!("Attribute {full_uri} not found in flake."); + log::info!("Attribute {full_attr} not found."); Ok(None) } }; @@ -199,12 +173,18 @@ fn parse_nix_build_output(output: String) -> Result { fn run_nix_build( nix_build_options: &NixBuildOptions, + attr: String, nix_options: &NixOptions, ) -> Result { + let path = &nix_build_options.path; let mut cmd = nix_cmd(nix_options); - cmd.arg("build") - .arg(&nix_build_options.flake_uri) - .arg("--json"); + cmd.arg("build"); + if nix_build_options.is_flake { + cmd.arg(format!("{path}#{attr}")); + } else { + cmd.arg("-f").arg(path).arg(attr); + } + cmd.arg("--json"); if nix_build_options.refresh { cmd.arg("--refresh"); } @@ -220,14 +200,23 @@ fn run_nix_build( Ok(output) } -fn try_nix_eval(flake: &str, attr: &str, nix_options: &NixOptions, refresh: bool) -> Result { +fn try_nix_eval( + nix_build_options: &NixBuildOptions, + attr: &str, + nix_options: &NixOptions, +) -> Result { + let path = &nix_build_options.path; let mut cmd = nix_cmd(nix_options); - cmd.arg("eval") - .arg(format!("{flake}#{FLAKE_ATTR}")) - .arg("--json") + cmd.arg("eval"); + if nix_build_options.is_flake { + cmd.arg(format!("{path}#{FLAKE_ATTR}")); + } else { + cmd.arg("-f").arg(path).arg(FLAKE_ATTR); + } + cmd.arg("--json") .arg("--apply") .arg(format!("_: _ ? {attr}")); - if refresh { + if nix_build_options.refresh { cmd.arg("--refresh"); } @@ -279,12 +268,47 @@ mod tests { #[test] fn test_try_nix_eval() { - let flake = "./test/rust/register"; + let nix_build_options_flake = NixBuildOptions { + is_flake: true, + path: "./test/rust/register".to_string(), + attr: None, + refresh: false, + }; + let nix_build_options_classic = NixBuildOptions { + is_flake: false, + path: "./test/rust/register".to_string(), + attr: None, + refresh: false, + }; let nix_options = &NixOptions::new(vec![]); - assert!(try_nix_eval(flake, "identifier-key", nix_options, false).unwrap()); - assert!(try_nix_eval(flake, "\"string.literal/key\"", nix_options, false).unwrap()); - assert!(!try_nix_eval(flake, "_identifier-key", nix_options, false).unwrap()); - assert!(!try_nix_eval(flake, "\"_string.literal/key\"", nix_options, false).unwrap()); + assert!(try_nix_eval(&nix_build_options_flake, "identifier-key", nix_options).unwrap()); + assert!(try_nix_eval(&nix_build_options_classic, "identifier-key", nix_options).unwrap()); + assert!(try_nix_eval( + &nix_build_options_flake, + "\"string.literal/key\"", + nix_options + ) + .unwrap()); + assert!(try_nix_eval( + &nix_build_options_classic, + "\"string.literal/key\"", + nix_options + ) + .unwrap()); + assert!(!try_nix_eval(&nix_build_options_flake, "_identifier-key", nix_options).unwrap()); + assert!(!try_nix_eval(&nix_build_options_classic, "_identifier-key", nix_options).unwrap()); + assert!(!try_nix_eval( + &nix_build_options_flake, + "\"_string.literal/key\"", + nix_options + ) + .unwrap()); + assert!(!try_nix_eval( + &nix_build_options_classic, + "\"_string.literal/key\"", + nix_options + ) + .unwrap()); } } diff --git a/crates/system-manager-engine/test/rust/register/default.nix b/crates/system-manager-engine/test/rust/register/default.nix new file mode 100644 index 00000000..67010155 --- /dev/null +++ b/crates/system-manager-engine/test/rust/register/default.nix @@ -0,0 +1,6 @@ +{ + systemConfigs = { + identifier-key = "value"; + "string.literal/key" = "value"; + }; +} diff --git a/crates/system-manager-engine/test/rust/register/flake.nix b/crates/system-manager-engine/test/rust/register/flake.nix index c39d6b7a..2a3cf328 100644 --- a/crates/system-manager-engine/test/rust/register/flake.nix +++ b/crates/system-manager-engine/test/rust/register/flake.nix @@ -1,9 +1,4 @@ { inputs = { }; - outputs = inputs: { - systemConfigs = { - identifier-key = "value"; - "string.literal/key" = "value"; - }; - }; + outputs = inputs: import ./.; } diff --git a/crates/system-manager/src/main.rs b/crates/system-manager/src/main.rs index 910ab327..16446afa 100644 --- a/crates/system-manager/src/main.rs +++ b/crates/system-manager/src/main.rs @@ -141,36 +141,96 @@ struct InitArgs { } #[derive(clap::Args, Debug)] -struct BuildArgs { - /// The flake URI defining the system-manager profile +#[group(multiple = false)] +pub struct BuildUri { + /// The flake URI defining the system-manager profile [default: ~/.config/system-manager] #[arg( long = "flake", name = "FLAKE_URI", - default_value = DEFAULT_FLAKE_PATH, - value_parser = |src: &str| -> Result { - if src.starts_with("~") { - if let Some(home) = std::env::home_dir() { - let expanded = src.replace("~", &home.to_string_lossy()); - return Ok(expanded); - } - bail!("Failed to determine a home directory for the flake URI.") + value_parser = |src: &str| -> Result<(String, Option)> { + let mut splitted = src.trim_end_matches('#').split('#'); + let path = splitted.next() + .ok_or_else(|| anyhow!("Invalid flake URI: {src}")); + let attr = splitted.next(); + if splitted.next().is_some() { + anyhow::bail!("Invalid flake URI, too many '#'s: {src}"); } - Ok(src.to_string()) + path.map(|p| (p.to_string(), attr.map(|a| a.to_string()))) }, )] - flake_uri: String, + flake_uri: Option<(String, Option)>, + + #[clap(flatten)] + file_args: Option, +} + +#[derive(clap::Args, Debug)] +struct FileArgs { + /// Path to a (non-flake) nix file to build the system-manager configuration from [default: ~/.config/system-manager]. + #[arg(long)] + file: Option, + + /// Specific attribute path to the system-manager configuration from. + #[arg(long)] + attr: Option, +} + +#[derive(clap::Args, Debug)] +struct BuildArgs { + #[clap(flatten)] + uri: BuildUri, #[arg(long, action)] /// Bypass the flake evaluation cache and fetch remote flakes fresh refresh: bool, } -impl From<&BuildArgs> for NixBuildOptions { - fn from(build_args: &BuildArgs) -> Self { - Self { - flake_uri: build_args.flake_uri.clone(), - refresh: build_args.refresh, +impl TryFrom<&BuildArgs> for NixBuildOptions { + type Error = anyhow::Error; + fn try_from(build_args: &BuildArgs) -> Result { + // Work-around https://github.com/clap-rs/clap/issues/5253 + if build_args.uri.flake_uri.is_some() && build_args.uri.file_args.is_some() { + bail!("the argument '--flake ' cannot be used with '--file | --attr '") } + + let mut path = build_args + .uri + .file_args + .as_ref() + .and_then(|f_args| f_args.file.clone()) + .unwrap_or_else(|| { + build_args + .uri + .flake_uri + .as_ref() + .map(|flake| flake.0.clone()) + .unwrap_or_else(|| DEFAULT_FLAKE_PATH.to_string()) + }); + if path.starts_with("~") { + if let Some(home) = std::env::home_dir() { + path = path.replace("~", &home.to_string_lossy()); + } else { + bail!("Failed to determine a home directory for the flake URI.") + } + } + + Ok(Self { + is_flake: build_args.uri.file_args.is_none(), + path: path.to_string(), + attr: build_args + .uri + .file_args + .as_ref() + .and_then(|f_args| f_args.attr.clone()) + .or_else(|| { + build_args + .uri + .flake_uri + .as_ref() + .and_then(|flake| flake.1.clone()) + }), + refresh: build_args.refresh, + }) } } @@ -189,19 +249,12 @@ struct OptionalStorePathArg { } #[derive(clap::Args, Debug)] -struct OptionalFlakeUriArg { - #[arg(long = "flake", name = "FLAKE_URI")] - /// The flake URI defining the system-manager profile - maybe_flake_uri: Option, -} - -#[derive(clap::Args, Debug)] -struct StoreOrFlakeArgs { +struct StoreOrBuildArgs { #[command(flatten)] optional_store_path_arg: OptionalStorePathArg, #[command(flatten)] - optional_flake_uri_arg: OptionalFlakeUriArg, + optional_build_uri_arg: BuildUri, #[arg(long, action)] /// Bypass the flake evaluation cache and fetch remote flakes fresh @@ -227,7 +280,7 @@ enum Action { /// Build a new system-manager generation and register it as the active system-manager profile Register { #[command(flatten)] - store_or_flake_args: StoreOrFlakeArgs, + store_or_build_args: StoreOrBuildArgs, #[command(flatten)] sudo_args: SudoArgs, }, @@ -246,7 +299,7 @@ enum Action { /// Put all files defined by the given generation in place, but do not start services PrePopulate { #[command(flatten)] - store_or_flake_args: StoreOrFlakeArgs, + store_or_build_args: StoreOrBuildArgs, #[command(flatten)] activation_args: ActivationArgs, #[command(flatten)] @@ -303,13 +356,13 @@ fn go(args: Args) -> Result<()> { match action { Action::PrePopulate { - store_or_flake_args, + store_or_build_args, activation_args: ActivationArgs { ephemeral }, sudo_args, } => { let sudo_options = sudo_args.to_sudo_options(legacy_use_remote_sudo)?; prepopulate( - store_or_flake_args, + store_or_build_args, ephemeral, &target_host, &sudo_options, @@ -339,12 +392,12 @@ fn go(args: Args) -> Result<()> { } Action::Register { - store_or_flake_args, + store_or_build_args, sudo_args, } => { let sudo_options = sudo_args.to_sudo_options(legacy_use_remote_sudo)?; register( - store_or_flake_args, + store_or_build_args, &target_host, &sudo_options, &nix_options, @@ -405,8 +458,7 @@ fn go(args: Args) -> Result<()> { build_args, activation_args: ActivationArgs { ephemeral }, sudo_args, - } => { - let mut nix_build_options = NixBuildOptions::from(&build_args); + } => NixBuildOptions::try_from(&build_args).and_then(|mut nix_build_options| { let sudo_options = sudo_args.to_sudo_options(legacy_use_remote_sudo)?; let store_path = do_build(&mut nix_build_options, &nix_options)?; copy_closure(&store_path, &target_host, &ssh_options)?; @@ -425,7 +477,7 @@ fn go(args: Args) -> Result<()> { &ssh_options, verbose, ) - } + }), Action::Activate { store_path, @@ -490,10 +542,11 @@ fn build( nix_options: &NixOptions, ssh_options: &[String], ) -> Result { - let mut nix_build_options = NixBuildOptions::from(build_args); - let store_path = do_build(&mut nix_build_options, nix_options)?; - copy_closure(&store_path, target_host, ssh_options)?; - Ok(store_path) + NixBuildOptions::try_from(build_args).and_then(|mut nix_build_options| { + let store_path = do_build(&mut nix_build_options, nix_options)?; + copy_closure(&store_path, target_host, ssh_options)?; + Ok(store_path) + }) } fn do_build( @@ -504,7 +557,7 @@ fn do_build( } fn register( - args: StoreOrFlakeArgs, + args: StoreOrBuildArgs, target_host: &Option, sudo_options: &SudoOptions, nix_options: &NixOptions, @@ -512,31 +565,32 @@ fn register( verbose: bool, ) -> Result { match args { - StoreOrFlakeArgs { + StoreOrBuildArgs { optional_store_path_arg: OptionalStorePathArg { maybe_store_path: None, }, - optional_flake_uri_arg: - OptionalFlakeUriArg { - maybe_flake_uri: Some(flake_uri), - }, + optional_build_uri_arg, refresh, - } => { - let mut nix_build_options = NixBuildOptions { flake_uri, refresh }; + } => NixBuildOptions::try_from(&BuildArgs { + uri: optional_build_uri_arg, + refresh, + }) + .and_then(|mut nix_build_options| { let store_path = do_build(&mut nix_build_options, nix_options)?; copy_closure(&store_path, target_host, ssh_options)?; invoke_engine_register(&store_path, target_host, sudo_options, ssh_options, verbose)?; Ok(store_path) - } - StoreOrFlakeArgs { + }), + StoreOrBuildArgs { optional_store_path_arg: OptionalStorePathArg { maybe_store_path: Some(store_path), }, - optional_flake_uri_arg: - OptionalFlakeUriArg { - maybe_flake_uri: None, + optional_build_uri_arg: + BuildUri { + flake_uri: None, + file_args: None, }, refresh: _, } => { @@ -551,7 +605,7 @@ fn register( } fn prepopulate( - args: StoreOrFlakeArgs, + args: StoreOrBuildArgs, ephemeral: bool, target_host: &Option, sudo_options: &SudoOptions, @@ -560,18 +614,18 @@ fn prepopulate( verbose: bool, ) -> Result { match args { - StoreOrFlakeArgs { + StoreOrBuildArgs { optional_store_path_arg: OptionalStorePathArg { maybe_store_path: None, }, - optional_flake_uri_arg: - OptionalFlakeUriArg { - maybe_flake_uri: Some(flake_uri), - }, + optional_build_uri_arg, refresh, - } => { - let mut nix_build_options = NixBuildOptions { flake_uri, refresh }; + } => NixBuildOptions::try_from(&BuildArgs { + uri: optional_build_uri_arg, + refresh, + }) + .and_then(|mut nix_build_options| { let store_path = do_build(&mut nix_build_options, nix_options)?; copy_closure(&store_path, target_host, ssh_options)?; invoke_engine_register(&store_path, target_host, sudo_options, ssh_options, verbose)?; @@ -584,12 +638,13 @@ fn prepopulate( verbose, )?; Ok(store_path) - } - StoreOrFlakeArgs { + }), + StoreOrBuildArgs { optional_store_path_arg: OptionalStorePathArg { maybe_store_path }, - optional_flake_uri_arg: - OptionalFlakeUriArg { - maybe_flake_uri: None, + optional_build_uri_arg: + BuildUri { + flake_uri: None, + file_args: None, }, refresh: _, } => {