diff --git a/Cargo.lock b/Cargo.lock index f359c61..dd466e1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -740,7 +740,7 @@ dependencies = [ [[package]] name = "nh" -version = "3.5.25" +version = "4.0.0-alpha.1" dependencies = [ "ambassador", "anstyle", diff --git a/Cargo.toml b/Cargo.toml index 152c5ea..01afd41 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nh" -version = "3.5.25" +version = "4.0.0-alpha.1" edition = "2021" license = "EUPL-1.2" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/src/home.rs b/src/home.rs deleted file mode 100644 index d87e610..0000000 --- a/src/home.rs +++ /dev/null @@ -1,206 +0,0 @@ -use std::env; -use std::ops::Deref; -use std::path::PathBuf; - -use color_eyre::eyre::bail; -use color_eyre::Result; -use thiserror::Error; -use tracing::{debug, info, instrument}; - -use crate::*; -use crate::{ - interface::NHRunnable, - interface::{FlakeRef, HomeArgs, HomeRebuildArgs, HomeSubcommand}, - util::{compare_semver, get_nix_version}, -}; - -#[derive(Error, Debug)] -enum HomeRebuildError { - #[error("Configuration \"{0}\" doesn't exist")] - ConfigName(String), -} - -impl NHRunnable for HomeArgs { - fn run(&self) -> Result<()> { - // self.subcommand - match &self.subcommand { - HomeSubcommand::Switch(args) | HomeSubcommand::Build(args) => { - args.rebuild(&self.subcommand) - } - s => bail!("Subcommand {:?} not yet implemented", s), - } - } -} - -impl HomeRebuildArgs { - fn rebuild(&self, action: &HomeSubcommand) -> Result<()> { - let out_path: Box = match self.common.out_link { - Some(ref p) => Box::new(p.clone()), - None => Box::new({ - let dir = tempfile::Builder::new().prefix("nh-home").tempdir()?; - (dir.as_ref().join("result"), dir) - }), - }; - - debug!(?out_path); - - let username = std::env::var("USER").expect("Couldn't get username"); - - let hm_config_name = match &self.configuration { - Some(name) => { - if configuration_exists(&self.common.flakeref, name)? { - name.to_owned() - } else { - return Err(HomeRebuildError::ConfigName(name.to_owned()).into()); - } - } - None => get_home_output(&self.common.flakeref, &username)?, - }; - - debug!("hm_config_name: {}", hm_config_name); - - let flakeref = format!( - "{}#homeConfigurations.\"{}\".config.home.activationPackage", - &self.common.flakeref.deref(), - hm_config_name - ); - - if self.common.update { - // Get the Nix version - let nix_version = get_nix_version().unwrap_or_else(|_| { - panic!("Failed to get Nix version. Custom Nix fork?"); - }); - - // Default interface for updating flake inputs - let mut update_args = vec!["nix", "flake", "update"]; - - // If user is on Nix 2.19.0 or above, --flake must be passed - if let Ok(ordering) = compare_semver(&nix_version, "2.19.0") { - if ordering == std::cmp::Ordering::Greater { - update_args.push("--flake"); - } - } - - update_args.push(&self.common.flakeref); - - debug!("nix_version: {:?}", nix_version); - debug!("update_args: {:?}", update_args); - - commands::CommandBuilder::default() - .args(&update_args) - .message("Updating flake") - .build()? - .exec()?; - } - - commands::BuildCommandBuilder::default() - .flakeref(&flakeref) - .extra_args(["--out-link"]) - .extra_args([out_path.get_path()]) - .extra_args(&self.extra_args) - .message("Building home configuration") - .nom(!self.common.no_nom) - .build()? - .exec()?; - - let prev_generation: Option = [ - PathBuf::from("/nix/var/nix/profiles/per-user") - .join(username) - .join("home-manager"), - PathBuf::from(env::var("HOME").unwrap()).join(".local/state/nix/profiles/home-manager"), - ] - .into_iter() - .fold(None, |res, next| { - res.or_else(|| if next.exists() { Some(next) } else { None }) - }); - - debug!("prev_generation: {:?}", prev_generation); - - // just do nothing for None case (fresh installs) - if let Some(prev_gen) = prev_generation { - commands::CommandBuilder::default() - .args(self.common.diff_provider.split_ascii_whitespace()) - .args([(prev_gen.to_str().unwrap())]) - .args([out_path.get_path()]) - .message("Comparing changes") - .build()? - .exec()?; - } - - if self.common.dry || matches!(action, HomeSubcommand::Build(_)) { - return Ok(()); - } - - if self.common.ask { - info!("Apply the config?"); - let confirmation = dialoguer::Confirm::new().default(false).interact()?; - - if !confirmation { - bail!("User rejected the new config"); - } - } - - if let Some(ext) = &self.backup_extension { - info!("Using {} as the backup extension", ext); - env::set_var("HOME_MANAGER_BACKUP_EXT", ext); - } - - commands::CommandBuilder::default() - .args([out_path.get_path().join("activate")]) - .message("Activating configuration") - .build()? - .exec()?; - - // Make sure out_path is not accidentally dropped - // https://docs.rs/tempfile/3.12.0/tempfile/index.html#early-drop-pitfall - drop(out_path); - - Ok(()) - } -} - -fn get_home_output + std::fmt::Display>( - flakeref: &FlakeRef, - username: S, -) -> Result { - // Replicate these heuristics - // https://github.com/nix-community/home-manager/blob/433e8de330fd9c157b636f9ccea45e3eeaf69ad2/home-manager/home-manager#L110 - - let hostname = hostname::get() - .expect("Couldn't get hostname") - .into_string() - .unwrap(); - - let username_hostname = format!("{}@{}", username, &hostname); - - if configuration_exists(flakeref, &username_hostname)? { - Ok(username_hostname) - } else if configuration_exists(flakeref, username.as_ref())? { - Ok(username.to_string()) - } else { - bail!( - "Couldn't detect a home configuration for {}", - username_hostname - ); - } -} - -#[instrument(ret, err, level = "debug")] -fn configuration_exists(flakeref: &FlakeRef, configuration: &str) -> Result { - let output = format!("{}#homeConfigurations", flakeref.deref()); - let filter = format!(r#" x: x ? "{}" "#, configuration); - - let result = commands::CommandBuilder::default() - .args(["nix", "eval", &output, "--apply", &filter]) - .build()? - .exec_capture()? - .unwrap(); - - debug!(?result); - - match result.as_str().trim() { - "true" => Ok(true), - "false" => Ok(false), - _ => bail!("Failed to parse nix-eval output: {}", result), - } -} diff --git a/src/installables.rs b/src/installables.rs new file mode 100644 index 0000000..ce6d9d7 --- /dev/null +++ b/src/installables.rs @@ -0,0 +1,95 @@ +use core::fmt; +use std::fmt::Write; + +#[derive(Debug, Clone)] +pub enum Installable { + Flake(FlakeInstallable), +} + +#[derive(Debug, Clone)] +pub struct FlakeInstallable { + pub reference: String, + pub attribute: Vec, +} + +impl From<&str> for Installable { + fn from(value: &str) -> Self { + todo!() + } +} + +impl Installable { + pub fn flake(reference: S, attribute: &[S]) -> Self + where + S: AsRef, + { + Installable::Flake(FlakeInstallable { + reference: reference.as_ref().to_string(), + attribute: attribute.iter().map(|s| s.as_ref().to_string()).collect(), + }) + } + + pub fn to_args(&self) -> Vec { + let mut res = Vec::new(); + + match &self { + Installable::Flake(flake) => { + let mut f = String::new(); + write!(f, "{}", flake.reference).unwrap(); + + if !flake.attribute.is_empty() { + write!(f, "#").unwrap(); + + let mut first = true; + + for elem in &flake.attribute { + if !first { + write!(f, ".").unwrap(); + } + + if elem.contains('.') { + write!(f, r#""{}""#, elem).unwrap(); + } else { + write!(f, "{}", elem).unwrap(); + } + + first = false; + } + + res.push(f); + } + } + } + + return res; + } +} + +impl fmt::Display for Installable { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut first = true; + + for elem in self.to_args() { + if !first { + write!(f, " ")?; + } else { + first = false; + } + + write!(f, "{}", elem)?; + } + + Ok(()) + } +} + +#[test] +fn test_display() { + let installable = Installable::flake(".", &["foo", "bar.local", "baz"]); + + let args = installable.to_args(); + assert_eq!(args, vec![String::from(".#foo.\"bar.local\".baz")]); + + let displayed = format!("{}", installable); + assert_eq!(".#foo.\"bar.local\".baz", displayed); +} diff --git a/src/interface.rs b/src/interface.rs index 53371fd..6d2e588 100644 --- a/src/interface.rs +++ b/src/interface.rs @@ -4,6 +4,8 @@ use clap::{builder::Styles, Args, Parser, Subcommand}; use color_eyre::Result; use std::{ffi::OsString, ops::Deref, path::PathBuf}; +use crate::installables::Installable; + #[derive(Debug, Clone, Default)] pub struct FlakeRef(String); impl From<&str> for FlakeRef { @@ -61,7 +63,6 @@ pub trait NHRunnable { #[command(disable_help_subcommand = true)] pub enum NHCommand { Os(OsArgs), - Home(HomeArgs), Search(SearchArgs), Clean(CleanProxy), Completions(CompletionArgs), @@ -130,7 +131,7 @@ pub struct CommonRebuildArgs { /// Flake reference to build #[arg(env = "FLAKE", value_hint = clap::ValueHint::DirPath)] - pub flakeref: FlakeRef, + pub installable: Installable, /// Update flake inputs before building specified configuration #[arg(long, short = 'u')] diff --git a/src/main.rs b/src/main.rs index 2542f7a..8ef8b91 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,7 @@ mod clean; mod commands; mod completion; -mod home; +mod installables; mod interface; mod json; mod logging; diff --git a/src/nixos.rs b/src/nixos.rs index 32075d4..22f9d7b 100644 --- a/src/nixos.rs +++ b/src/nixos.rs @@ -6,6 +6,7 @@ use color_eyre::Result; use tracing::{debug, info, warn}; +use crate::installables::Installable; use crate::interface::NHRunnable; use crate::interface::OsRebuildType::{self, Boot, Build, Switch, Test}; use crate::interface::{self, OsRebuildArgs}; @@ -57,128 +58,112 @@ impl OsRebuildArgs { debug!(?out_path); - let flake_output = format!( - "{}#nixosConfigurations.\"{:?}\".config.system.build.toplevel", - &self.common.flakeref.deref(), - hostname - ); - - if self.common.update { - // Get the Nix version - let nix_version = get_nix_version().unwrap_or_else(|_| { - panic!("Failed to get Nix version. Custom Nix fork?"); - }); - - // Default interface for updating flake inputs - let mut update_args = vec!["nix", "flake", "update"]; - - // If user is on Nix 2.19.0 or above, --flake must be passed - if let Ok(ordering) = compare_semver(&nix_version, "2.19.0") { - if ordering == std::cmp::Ordering::Greater { - update_args.push("--flake"); - } - } - - update_args.push(&self.common.flakeref); - - debug!("nix_version: {:?}", nix_version); - debug!("update_args: {:?}", update_args); - - commands::CommandBuilder::default() - .args(&update_args) - .message("Updating flake") - .build()? - .exec()?; - } - - commands::BuildCommandBuilder::default() - .flakeref(flake_output) - .message("Building NixOS configuration") - .extra_args(["--out-link"]) - .extra_args([out_path.get_path()]) - .extra_args(&self.extra_args) - .nom(!self.common.no_nom) - .build()? - .exec()?; - - let current_specialisation = std::fs::read_to_string(SPEC_LOCATION).ok(); - - let target_specialisation = if self.no_specialisation { - None - } else { - current_specialisation.or_else(|| self.specialisation.to_owned()) - }; - - debug!("target_specialisation: {target_specialisation:?}"); - - let target_profile = match &target_specialisation { - None => out_path.get_path().to_owned(), - Some(spec) => out_path.get_path().join("specialisation").join(spec), - }; - - target_profile.try_exists().context("Doesn't exist")?; - - commands::CommandBuilder::default() - .args(self.common.diff_provider.split_ascii_whitespace()) - .args([CURRENT_PROFILE, target_profile.to_str().unwrap()]) - .message("Comparing changes") - .build()? - .exec()?; - - if self.common.dry || matches!(rebuild_type, OsRebuildType::Build(_)) { - return Ok(()); - } - - if self.common.ask { - info!("Apply the config?"); - let confirmation = dialoguer::Confirm::new().default(false).interact()?; - - if !confirmation { - bail!("User rejected the new config"); - } - } - - if let Test(_) | Switch(_) = rebuild_type { - // !! Use the target profile aka spec-namespaced - let switch_to_configuration = - target_profile.join("bin").join("switch-to-configuration"); - let switch_to_configuration = switch_to_configuration.to_str().unwrap(); - - commands::CommandBuilder::default() - .args(sudo_args) - .args([switch_to_configuration, "test"]) - .message("Activating configuration") - .build()? - .exec()?; - } - - if let Boot(_) | Switch(_) = rebuild_type { - commands::CommandBuilder::default() - .args(sudo_args) - .args(["nix-env", "--profile", SYSTEM_PROFILE, "--set"]) - .args([out_path.get_path()]) - .build()? - .exec()?; - - // !! Use the base profile aka no spec-namespace - let switch_to_configuration = out_path - .get_path() - .join("bin") - .join("switch-to-configuration"); - let switch_to_configuration = switch_to_configuration.to_str().unwrap(); - - commands::CommandBuilder::default() - .args(sudo_args) - .args([switch_to_configuration, "boot"]) - .message("Adding configuration to bootloader") - .build()? - .exec()?; - } - - // Make sure out_path is not accidentally dropped - // https://docs.rs/tempfile/3.12.0/tempfile/index.html#early-drop-pitfall - drop(out_path); - + // let target_installable = match self.common.installable { + // installables::Installable::Flake(flake) => { + // // FIXME + // let mut flake = flake.clone(); + // flake.attribute = vec![]; + // flake.attribute.push(String::from("nixosConfigurations")); + // flake.attribute.push(hostname.into_string().unwrap()); + // + // flake.attribute.push(String::from("config")); + // flake.attribute.push(String::from("build")); + // flake.attribute.push(String::from("system")); + // flake.attribute.push(String::from("toplevel")); + // + // Installable::Flake(flake) + // } + // }; + // + // commands::BuildCommandBuilder::default() + // .flakeref(format!("{}", target_installable)) // FIXME + // // .flakeref(flake_output) + // .message("Building NixOS configuration") + // .extra_args(["--out-link"]) + // .extra_args([out_path.get_path()]) + // .extra_args(&self.extra_args) + // .nom(!self.common.no_nom) + // .build()? + // .exec()?; + // + // let current_specialisation = std::fs::read_to_string(SPEC_LOCATION).ok(); + // + // let target_specialisation = if self.no_specialisation { + // None + // } else { + // current_specialisation.or_else(|| self.specialisation.to_owned()) + // }; + // + // debug!("target_specialisation: {target_specialisation:?}"); + // + // let target_profile = match &target_specialisation { + // None => out_path.get_path().to_owned(), + // Some(spec) => out_path.get_path().join("specialisation").join(spec), + // }; + // + // target_profile.try_exists().context("Doesn't exist")?; + // + // commands::CommandBuilder::default() + // .args(self.common.diff_provider.split_ascii_whitespace()) + // .args([CURRENT_PROFILE, target_profile.to_str().unwrap()]) + // .message("Comparing changes") + // .build()? + // .exec()?; + // + // if self.common.dry || matches!(rebuild_type, OsRebuildType::Build(_)) { + // return Ok(()); + // } + // + // if self.common.ask { + // info!("Apply the config?"); + // let confirmation = dialoguer::Confirm::new().default(false).interact()?; + // + // if !confirmation { + // bail!("User rejected the new config"); + // } + // } + // + // if let Test(_) | Switch(_) = rebuild_type { + // // !! Use the target profile aka spec-namespaced + // let switch_to_configuration = + // target_profile.join("bin").join("switch-to-configuration"); + // let switch_to_configuration = switch_to_configuration.to_str().unwrap(); + // + // commands::CommandBuilder::default() + // .args(sudo_args) + // .args([switch_to_configuration, "test"]) + // .message("Activating configuration") + // .build()? + // .exec()?; + // } + // + // if let Boot(_) | Switch(_) = rebuild_type { + // commands::CommandBuilder::default() + // .args(sudo_args) + // .args(["nix-env", "--profile", SYSTEM_PROFILE, "--set"]) + // .args([out_path.get_path()]) + // .build()? + // .exec()?; + // + // // !! Use the base profile aka no spec-namespace + // let switch_to_configuration = out_path + // .get_path() + // .join("bin") + // .join("switch-to-configuration"); + // let switch_to_configuration = switch_to_configuration.to_str().unwrap(); + // + // commands::CommandBuilder::default() + // .args(sudo_args) + // .args([switch_to_configuration, "boot"]) + // .message("Adding configuration to bootloader") + // .build()? + // .exec()?; + // } + // + // // Make sure out_path is not accidentally dropped + // // https://docs.rs/tempfile/3.12.0/tempfile/index.html#early-drop-pitfall + // drop(out_path); + // Ok(()) } }