diff --git a/Cargo.lock b/Cargo.lock index 1be9c6c..065156d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,18 +26,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "ambassador" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b27ba24e4d8a188489d5a03c7fabc167a60809a383cdb4d15feb37479cd2a48" -dependencies = [ - "itertools", - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "anstream" version = "0.6.18" @@ -215,7 +203,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.87", + "syn", ] [[package]] @@ -262,72 +250,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "darling" -version = "0.20.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" -dependencies = [ - "darling_core", - "darling_macro", -] - -[[package]] -name = "darling_core" -version = "0.20.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn 2.0.87", -] - -[[package]] -name = "darling_macro" -version = "0.20.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" -dependencies = [ - "darling_core", - "quote", - "syn 2.0.87", -] - -[[package]] -name = "derive_builder" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" -dependencies = [ - "derive_builder_macro", -] - -[[package]] -name = "derive_builder_core" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" -dependencies = [ - "darling", - "proc-macro2", - "quote", - "syn 2.0.87", -] - -[[package]] -name = "derive_builder_macro" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" -dependencies = [ - "derive_builder_core", - "syn 2.0.87", -] - [[package]] name = "dialoguer" version = "0.11.0" @@ -347,15 +269,9 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn", ] -[[package]] -name = "either" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" - [[package]] name = "elasticsearch-dsl" version = "0.4.22" @@ -724,15 +640,9 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn", ] -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - [[package]] name = "idna" version = "1.0.3" @@ -783,15 +693,6 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" -[[package]] -name = "itertools" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" -dependencies = [ - "either", -] - [[package]] name = "itoa" version = "1.0.11" @@ -815,9 +716,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.162" +version = "0.2.164" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398" +checksum = "433bfe06b8c75da9b2e3fbea6e5329ff87748f0b144ef75306e674c3f6f7c13f" [[package]] name = "linux-raw-sys" @@ -887,16 +788,14 @@ dependencies = [ [[package]] name = "nh" -version = "3.6.0" +version = "4.0.0-alpha.1" dependencies = [ - "ambassador", "anstyle", "clap", "clap_builder", "clap_complete", "clean-path", "color-eyre", - "derive_builder", "dialoguer", "elasticsearch-dsl", "hostname", @@ -1254,9 +1153,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.16" +version = "0.23.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eee87ff5d9b36712a58574e12e9f0ea80f915a5b0ac518d322b24a465617925e" +checksum = "7f1a745511c54ba6d4465e8d5dfbd81b45791756de28d4981af70d6dca128f1e" dependencies = [ "once_cell", "ring", @@ -1324,7 +1223,7 @@ checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn", ] [[package]] @@ -1443,17 +1342,6 @@ version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c0a1e5168041f5f3ff68ff7d95dcb9c8749df29f6e7e89ada40dd4c9de404ee" -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - [[package]] name = "syn" version = "2.0.87" @@ -1482,7 +1370,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn", ] [[package]] @@ -1546,7 +1434,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn", ] [[package]] @@ -1557,7 +1445,7 @@ checksum = "f077553d607adc1caf65430528a576c757a71ed73944b66ebb58ef2bbd243568" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn", ] [[package]] @@ -1652,7 +1540,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn", ] [[package]] @@ -1805,7 +1693,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.87", + "syn", "wasm-bindgen-shared", ] @@ -1839,7 +1727,7 @@ checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -2130,7 +2018,7 @@ checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn", "synstructure", ] @@ -2152,7 +2040,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn", ] [[package]] @@ -2172,7 +2060,7 @@ checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn", "synstructure", ] @@ -2201,5 +2089,5 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn", ] diff --git a/Cargo.toml b/Cargo.toml index 98c8ca2..1ae0419 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,11 @@ [package] name = "nh" -version = "3.6.0" +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 [dependencies] -ambassador = "0.4.0" anstyle = "1.0.0" clap = { version = "4.0", features = [ "cargo", @@ -21,7 +20,6 @@ clean-path = "0.2" color-eyre = { version = "0.6.2", default-features = false, features = [ "track-caller", ] } -derive_builder = "0.20.0" dialoguer = { version = "0.11.0", default-features = false } elasticsearch-dsl = "0.4.19" hostname = "0.4" diff --git a/flake.nix b/flake.nix index 4f9248f..5f68285 100644 --- a/flake.nix +++ b/flake.nix @@ -34,7 +34,5 @@ devShells = forAllSystems (pkgs: { default = pkgs.callPackage ./devshell.nix {}; }); - - nixosModules.default = import ./module.nix; }; } diff --git a/module.nix b/module.nix deleted file mode 100644 index 449dc95..0000000 --- a/module.nix +++ /dev/null @@ -1,13 +0,0 @@ -builtins.throw '' - github:viperML/nh: the NixOS module has been upstreamed into nixpkgs - - https://github.com/NixOS/nixpkgs/pull/294923 - - To migrate, please replace the import to this flake's module: - >>> inputs.nh.nixosModules.default - With just swapping the nh package: - <<< { programs.nh.package = inputs.nh.packages.x86_64-linux.default; } - - The nh options are now in the programs.* namespace - You may need to adjust nh.enable -> programs.nh.enable , etc. -'' diff --git a/package.nix b/package.nix index 22caa5b..c8f4cde 100644 --- a/package.nix +++ b/package.nix @@ -43,9 +43,9 @@ in preFixup = '' mkdir completions - $out/bin/nh completions --shell bash > completions/nh.bash - $out/bin/nh completions --shell zsh > completions/nh.zsh - $out/bin/nh completions --shell fish > completions/nh.fish + $out/bin/nh completions bash > completions/nh.bash + $out/bin/nh completions zsh > completions/nh.zsh + $out/bin/nh completions fish > completions/nh.fish installShellCompletion completions/* ''; diff --git a/src/clean.rs b/src/clean.rs index 50518a7..faac403 100644 --- a/src/clean.rs +++ b/src/clean.rs @@ -5,7 +5,7 @@ use std::{ time::SystemTime, }; -use crate::*; +use crate::{commands::Command, *}; use color_eyre::eyre::{bail, eyre, Context, ContextCompat}; use nix::errno::Errno; use nix::{ @@ -31,8 +31,8 @@ type ToBeRemoved = bool; type GenerationsTagged = BTreeMap; type ProfilesTagged = HashMap; -impl NHRunnable for interface::CleanMode { - fn run(&self) -> Result<()> { +impl interface::CleanMode { + pub fn run(&self) -> Result<()> { let mut profiles = Vec::new(); let mut gcroots_tagged: HashMap = HashMap::new(); let now = SystemTime::now(); @@ -221,12 +221,11 @@ impl NHRunnable for interface::CleanMode { } } - commands::CommandBuilder::default() - .args(["nix", "store", "gc"]) + Command::new("nix") + .args(["store", "gc"]) .dry(args.dry) .message("Performing garbage collection on the nix store") - .build()? - .exec()?; + .run()?; Ok(()) } diff --git a/src/commands.rs b/src/commands.rs index 176a1c3..5a207c7 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -1,6 +1,6 @@ use color_eyre::{ eyre::{bail, Context}, - Result, + install, Result, }; use std::ffi::{OsStr, OsString}; @@ -9,47 +9,72 @@ use thiserror::Error; use subprocess::{Exec, ExitStatus, Redirection}; use tracing::{debug, info}; -#[derive(Debug, derive_builder::Builder)] -#[builder(derive(Debug), setter(into))] +use crate::installable::Installable; + +#[derive(Debug)] pub struct Command { - /// Whether to actually run the command or just log it - #[builder(default = "false")] dry: bool, - /// Human-readable message regarding what the command does - #[builder(setter(strip_option), default = "None")] message: Option, - /// Arguments 0..N - #[builder(setter(custom))] + command: OsString, args: Vec, + elevate: bool, } -impl CommandBuilder { - pub fn args(&mut self, input: I) -> &mut Self +impl Command { + pub fn new>(command: S) -> Self { + Self { + dry: false, + message: None, + command: command.as_ref().to_os_string(), + args: vec![], + elevate: false, + } + } + + pub fn elevate(mut self, elevate: bool) -> Self { + self.elevate = elevate; + self + } + + pub fn dry(mut self, dry: bool) -> Self { + self.dry = dry; + self + } + + pub fn arg>(mut self, arg: S) -> Self { + self.args.push(arg.as_ref().to_os_string()); + self + } + + pub fn args(mut self, args: I) -> Self where - S: AsRef, - I: IntoIterator, + I: IntoIterator, + I::Item: AsRef, { - self.args - .get_or_insert_with(Default::default) - .extend(input.into_iter().map(|s| s.as_ref().to_owned())); + for elem in args { + self.args.push(elem.as_ref().to_os_string()); + } self } -} -impl Command { - pub fn exec(&self) -> Result<()> { - let [head, tail @ ..] = &*self.args else { - bail!("Args was length 0"); - }; + pub fn message>(mut self, message: S) -> Self { + self.message = Some(message.as_ref().to_string()); + self + } - let cmd = Exec::cmd(head) - .args(tail) - .stderr(Redirection::None) - .stdout(Redirection::None); + pub fn run(&self) -> Result<()> { + let cmd = if self.elevate { + Exec::cmd("sudo").arg(&self.command).args(&self.args) + } else { + Exec::cmd(&self.command).args(&self.args) + } + .stderr(Redirection::None) + .stdout(Redirection::None); if let Some(m) = &self.message { info!("{}", m); } + debug!(?cmd); if !self.dry { @@ -63,19 +88,16 @@ impl Command { Ok(()) } - pub fn exec_capture(&self) -> Result> { - let [head, tail @ ..] = &*self.args else { - bail!("Args was length 0"); - }; - - let cmd = Exec::cmd(head) - .args(tail) + pub fn run_capture(&self) -> Result> { + let cmd = Exec::cmd(&self.command) + .args(&self.args) .stderr(Redirection::None) .stdout(Redirection::Pipe); if let Some(m) = &self.message { info!("{}", m); } + debug!(?cmd); if !self.dry { @@ -86,47 +108,63 @@ impl Command { } } -#[derive(Debug, derive_builder::Builder)] -#[builder(setter(into))] -pub struct BuildCommand { - /// Human-readable message regarding what the command does - message: String, - // Flakeref to build - flakeref: String, - // Extra arguments passed to nix build - #[builder(setter(custom))] +#[derive(Debug)] +pub struct Build { + message: Option, + installable: Installable, extra_args: Vec, - /// Use nom for the nix build nom: bool, } -impl BuildCommandBuilder { - pub fn extra_args(&mut self, input: I) -> &mut Self +impl Build { + pub fn new(installable: Installable) -> Self { + Self { + message: None, + installable, + extra_args: vec![], + nom: false, + } + } + + pub fn message>(mut self, message: S) -> Self { + self.message = Some(message.as_ref().to_string()); + self + } + + pub fn extra_arg>(mut self, arg: S) -> Self { + self.extra_args.push(arg.as_ref().to_os_string()); + self + } + + pub fn nom(mut self, yes: bool) -> Self { + self.nom = yes; + self + } + + pub fn extra_args(mut self, args: I) -> Self where - S: AsRef, - I: IntoIterator, + I: IntoIterator, + I::Item: AsRef, { - self.extra_args - .get_or_insert_with(Default::default) - .extend(input.into_iter().map(|s| s.as_ref().to_owned())); + for elem in args { + self.extra_args.push(elem.as_ref().to_os_string()); + } self } -} -impl BuildCommand { - pub fn exec(&self) -> Result<()> { - info!("{}", self.message); + pub fn run(&self) -> Result<()> { + if let Some(m) = &self.message { + info!("{}", m); + } + + let installable_args = self.installable.to_args(); let exit = if self.nom { let cmd = { Exec::cmd("nix") - .args(&[ - "build", - &self.flakeref, - "--log-format", - "internal-json", - "--verbose", - ]) + .arg("build") + .args(&installable_args) + .args(&["--log-format", "internal-json", "--verbose"]) .args(&self.extra_args) .stdout(Redirection::Pipe) .stderr(Redirection::Merge) @@ -137,7 +175,8 @@ impl BuildCommand { cmd.join() } else { let cmd = Exec::cmd("nix") - .args(&["build", &self.flakeref]) + .arg("build") + .args(&installable_args) .args(&self.extra_args) .stdout(Redirection::None) .stderr(Redirection::Merge); @@ -146,7 +185,7 @@ impl BuildCommand { cmd.join() }; - match exit.wrap_err(self.message.clone())? { + match exit? { ExitStatus::Exited(0) => (), other => bail!(ExitError(other)), } diff --git a/src/completion.rs b/src/completion.rs index 4e6a883..723d7a3 100644 --- a/src/completion.rs +++ b/src/completion.rs @@ -1,12 +1,13 @@ +use crate::interface::Main; use crate::*; use clap_complete::generate; use color_eyre::Result; use tracing::instrument; -impl NHRunnable for interface::CompletionArgs { +impl interface::CompletionArgs { #[instrument(ret, level = "trace")] - fn run(&self) -> Result<()> { - let mut cmd = ::command(); + pub fn run(&self) -> Result<()> { + let mut cmd =
::command(); generate(self.shell, &mut cmd, "nh", &mut std::io::stdout()); Ok(()) } 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/installable.rs b/src/installable.rs new file mode 100644 index 0000000..966f7f1 --- /dev/null +++ b/src/installable.rs @@ -0,0 +1,241 @@ +use std::env; +use std::path::PathBuf; + +use clap::error::ErrorKind; +use clap::{Arg, ArgAction, Args, FromArgMatches}; +use color_eyre::owo_colors::OwoColorize; + +// Reference: https://nix.dev/manual/nix/2.18/command-ref/new-cli/nix + +#[derive(Debug, Clone)] +pub enum Installable { + Flake { + reference: String, + attribute: Vec, + }, + File { + path: PathBuf, + attribute: Vec, + }, + // TODO: + // Store { + // path: PathBuf, + // }, + Expression { + expression: String, + attribute: Vec, + }, +} + +impl FromArgMatches for Installable { + fn from_arg_matches(matches: &clap::ArgMatches) -> Result { + let mut matches = matches.clone(); + Self::from_arg_matches_mut(&mut matches) + } + + fn from_arg_matches_mut(matches: &mut clap::ArgMatches) -> Result { + let installable = matches.get_one::("installable"); + let file = matches.get_one::("file"); + let expr = matches.get_one::("expr"); + + if let Some(f) = file { + return Ok(Self::File { + path: PathBuf::from(f), + attribute: parse_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()), + }); + } + + if let Some(i) = installable { + let mut elems = i.splitn(2, '#'); + let reference = elems.next().unwrap().to_owned(); + return Ok(Self::Flake { + reference, + attribute: parse_attribute(elems.next().map(|s| s.to_string()).unwrap_or_default()), + }); + } + + // env var fallacks + + if let Ok(f) = env::var("NH_FLAKE") { + let mut elems = f.splitn(2, "#"); + return Ok(Self::Flake { + reference: elems.next().unwrap().to_owned(), + attribute: parse_attribute(elems.next().map(|s| s.to_string()).unwrap_or_default()), + }); + } + + if let Ok(f) = env::var("NH_FILE") { + return Ok(Self::File { + path: PathBuf::from(f), + attribute: parse_attribute(env::var("NH_ATTR").unwrap_or_default()), + }); + } + + return Err(clap::Error::new(ErrorKind::TooFewValues)); + } + + fn update_from_arg_matches(&mut self, matches: &clap::ArgMatches) -> Result<(), clap::Error> { + todo!() + } +} + +impl Args for Installable { + fn augment_args(cmd: clap::Command) -> clap::Command { + cmd.arg( + Arg::new("file") + .short('f') + .long("file") + .action(ArgAction::Set) + .hide(true), + ) + .arg( + Arg::new("expr") + .short('E') + .long("expr") + .conflicts_with("file") + .hide(true) + .action(ArgAction::Set), + ) + .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={}] + +{}, {} [ATTRPATH] + Path to file with an optional attribute path. + [env: NH_FILE={}] + [env: NH_ATTR={}] + +{}, {} [ATTRPATH] + Nix expression with an optional attribute path. +"#, + env::var("NH_FLAKE").unwrap_or_default(), + "-f".yellow(), + "--file".yellow(), + env::var("NH_FILE").unwrap_or_default(), + env::var("NH_ATTRP").unwrap_or_default(), + "-e".yellow(), + "--expr".yellow(), + )), + ) + } + + fn augment_args_for_update(cmd: clap::Command) -> clap::Command { + Self::augment_args(cmd) + } +} + +// TODO: should handle quoted attributes, like foo."bar.baz" -> ["foo", "bar.baz"] +// maybe use chumsky? +fn parse_attribute(s: S) -> Vec +where + S: AsRef, +{ + let s = s.as_ref(); + let mut res = Vec::new(); + + if !s.is_empty() { + for elem in s.split(".") { + res.push(elem.to_string()) + } + } + + return res; +} + +impl Installable { + pub fn to_args(&self) -> Vec { + let mut res = Vec::new(); + match self { + Installable::Flake { + reference, + attribute, + } => { + res.push(format!("{reference}#{}", join_attribute(attribute))); + } + Installable::File { path, attribute } => { + res.push(String::from("--file")); + res.push(path.to_str().unwrap().to_string()); + res.push(join_attribute(attribute)); + } + Installable::Expression { + expression, + attribute, + } => { + res.push(String::from("--expr")); + res.push(expression.to_string()); + res.push(join_attribute(attribute)); + } + } + + return res; + } +} + +#[test] +fn test_installable_to_args() { + assert_eq!( + (Installable::Flake { + reference: String::from("w"), + attribute: ["x", "y.z"].into_iter().map(str::to_string).collect() + }) + .to_args(), + vec![r#"w#x."y.z""#] + ); + + assert_eq!( + (Installable::File { + path: PathBuf::from("w"), + attribute: ["x", "y.z"].into_iter().map(str::to_string).collect() + }) + .to_args(), + vec!["--file", "w", r#"x."y.z""#] + ); +} + +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_str("."); + } + + let s = elem.as_ref(); + + if s.contains(".") { + res.push_str(&format!(r#""{}""#, s)); + } else { + res.push_str(s); + } + } + + return 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""#); +} diff --git a/src/interface.rs b/src/interface.rs index 43f8dfa..3ce005b 100644 --- a/src/interface.rs +++ b/src/interface.rs @@ -1,29 +1,10 @@ -use ambassador::{delegatable_trait, Delegate}; +use crate::Result; use anstyle::Style; +use clap::ValueEnum; use clap::{builder::Styles, Args, Parser, Subcommand}; -use color_eyre::Result; use std::{ffi::OsString, ops::Deref, path::PathBuf}; -#[derive(Debug, Clone, Default)] -pub struct FlakeRef(String); -impl From<&str> for FlakeRef { - fn from(s: &str) -> Self { - FlakeRef(s.to_string()) - } -} -// impl std::fmt::Display for FlakeRef { -// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { -// write!(f, "{}", self.0) -// } -// } - -impl Deref for FlakeRef { - type Target = String; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} +use crate::installable::Installable; fn make_style() -> Styles { Styles::plain().header(Style::new().bold()).literal( @@ -49,7 +30,7 @@ fn make_style() -> Styles { " )] /// Yet another nix helper -pub struct NHParser { +pub struct Main { #[arg(short, long, global = true)] /// Show debug logs pub verbose: bool, @@ -58,39 +39,25 @@ pub struct NHParser { pub command: NHCommand, } -#[delegatable_trait] -pub trait NHRunnable { - fn run(&self) -> Result<()>; -} - -#[derive(Subcommand, Debug, Delegate)] -#[delegate(NHRunnable)] +#[derive(Subcommand, Debug)] #[command(disable_help_subcommand = true)] pub enum NHCommand { Os(OsArgs), - Home(HomeArgs), Search(SearchArgs), Clean(CleanProxy), + #[command(hide = true)] Completions(CompletionArgs), } -#[derive(Debug, Args)] -pub struct CommonReplArgs { - /// Flake reference to build - #[arg(env = "FLAKE", value_hint = clap::ValueHint::DirPath)] - pub flakeref: FlakeRef, - - /// Output to choose from the flakeref. Hostname is used by default - #[arg(long, short = 'H', global = true)] - pub hostname: Option, - - /// Name of the flake homeConfigurations attribute, like username@hostname - #[arg(long, short, conflicts_with = "flakeref")] - pub configuration: Option, - - /// Extra arguments passed verbatim to nix repl. - #[arg(last = true)] - pub extra_args: Vec, +impl NHCommand { + pub fn run(self) -> Result<()> { + match self { + NHCommand::Os(args) => args.run(), + NHCommand::Search(args) => todo!(), + NHCommand::Clean(proxy) => proxy.command.run(), + NHCommand::Completions(args) => args.run(), + } + } } #[derive(Args, Debug)] @@ -100,41 +67,32 @@ pub struct CommonReplArgs { /// Implements functionality mostly around but not exclusive to nixos-rebuild pub struct OsArgs { #[command(subcommand)] - pub action: OsCommandType, + pub subcommand: OsSubcommand, } #[derive(Debug, Subcommand)] -pub enum OsCommandType { +pub enum OsSubcommand { /// Build and activate the new configuration, and make it the boot default - Switch(OsSubcommandArgs), + Switch(OsRebuildArgs), /// Build the new configuration and make it the boot default - Boot(OsSubcommandArgs), + Boot(OsRebuildArgs), /// Build and activate the new configuration - Test(OsSubcommandArgs), + Test(OsRebuildArgs), /// Build the new configuration - Build(OsSubcommandArgs), - - /// Enter a Nix REPL with the target installable - /// - /// For now, this only supports NixOS configurations via `nh os repl` - Repl(CommonReplArgs), - - /// Show an overview of the system's info - #[command(hide = true)] - Info, + Build(OsRebuildArgs), } #[derive(Debug, Args)] -pub struct OsSubcommandArgs { +pub struct OsRebuildArgs { #[command(flatten)] pub common: CommonRebuildArgs, - /// Output to choose from the flakeref. Hostname is used by default + /// Output to choose from the installable #[arg(long, short = 'H', global = true)] - pub hostname: Option, + pub hostname: Option, /// Name of the specialisation #[arg(long, short)] @@ -164,28 +122,13 @@ pub struct CommonRebuildArgs { pub ask: bool, /// Flake reference to build - #[arg(env = "FLAKE", value_hint = clap::ValueHint::DirPath)] - pub flakeref: FlakeRef, - - /// Update flake inputs before building specified configuration - #[arg(long, short = 'u')] - pub update: bool, + #[command(flatten)] + pub installable: Installable, /// Don't use nix-output-monitor for the build process #[arg(long)] pub no_nom: bool, - /// Closure diff provider - /// - /// Default is "nvd diff", but "nix store diff-closures" is also supported - #[arg( - long, - short = 'D', - env = "NH_DIFF_PROVIDER", - default_value = "nvd diff" - )] - pub diff_provider: String, - /// Path to save the result link. Defaults to using a temporary directory. #[arg(long, short)] pub out_link: Option, @@ -205,14 +148,12 @@ pub struct SearchArgs { /// Name of the package to search pub query: String, - #[arg(short, long, env = "FLAKE", value_hint = clap::ValueHint::DirPath)] - /// Flake to read what nixpkgs channels to search for - pub flake: Option, + #[command(flatten)] + pub installable: Installable, } // Needed a struct to have multiple sub-subcommands -#[derive(Debug, Clone, Args, Delegate)] -#[delegate(NHRunnable)] +#[derive(Debug, Clone, Args)] pub struct CleanProxy { #[clap(subcommand)] command: CleanMode, @@ -317,6 +258,5 @@ pub struct HomeRebuildArgs { /// Generate shell completion files into stdout pub struct CompletionArgs { /// Name of the shell - #[arg(long, short)] pub shell: clap_complete::Shell, } diff --git a/src/json.rs b/src/json.rs index 2e0ee35..8a52e9e 100644 --- a/src/json.rs +++ b/src/json.rs @@ -1,7 +1,5 @@ use std::fmt::Display; - - #[derive(Debug, Clone)] pub struct Value<'v> { pub inner: &'v serde_json::Value, diff --git a/src/main.rs b/src/main.rs index c0543ac..a9e1e25 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,28 +1,39 @@ mod clean; mod commands; mod completion; -mod home; +mod installable; mod interface; mod json; mod logging; mod nixos; -mod repl; -mod search; +// mod search; mod util; -use crate::interface::NHParser; -use crate::interface::NHRunnable; use color_eyre::Result; use tracing::debug; const NH_VERSION: &str = env!("CARGO_PKG_VERSION"); fn main() -> Result<()> { - let args = ::parse(); + let mut do_warn = false; + if let Ok(f) = std::env::var("FLAKE") { + do_warn = true; + if let Err(_) = std::env::var("NH_FLAKE") { + std::env::set_var("NH_FLAKE", f); + } + } + + let args = ::parse(); crate::logging::setup_logging(args.verbose)?; - tracing::debug!(?args); + tracing::debug!("{args:#?}"); tracing::debug!(%NH_VERSION); + if do_warn { + tracing::warn!( + "nh {NH_VERSION} now uses NH_FLAKE instead of FLAKE, please modify your configuration" + ); + } + args.command.run() } diff --git a/src/nixos.rs b/src/nixos.rs index 02733bc..b21da7c 100644 --- a/src/nixos.rs +++ b/src/nixos.rs @@ -6,31 +6,45 @@ use color_eyre::Result; use tracing::{debug, info, warn}; -use crate::interface::NHRunnable; -use crate::interface::OsCommandType::{self, Boot, Build, Repl, Switch, Test}; -use crate::interface::{self, OsSubcommandArgs}; -use crate::repl::ReplVariant; +use crate::commands::{Build, Command}; +use crate::interface::OsSubcommand::{self}; +use crate::interface::{self, OsRebuildArgs}; +// use crate::repl::ReplVariant; use crate::util::{compare_semver, get_nix_version}; use crate::*; +use self::installable::Installable; + const SYSTEM_PROFILE: &str = "/nix/var/nix/profiles/system"; const CURRENT_PROFILE: &str = "/run/current-system"; const SPEC_LOCATION: &str = "/etc/specialisation"; -impl NHRunnable for interface::OsArgs { - fn run(&self) -> Result<()> { - match &self.action { - Switch(args) | Boot(args) | Test(args) | Build(args) => args.rebuild(&self.action), - Repl(args) => args.repl(ReplVariant::OsRepl), - s => bail!("Subcommand {:?} not yet implemented", s), +impl interface::OsArgs { + pub fn run(self) -> Result<()> { + use OsRebuildVariant::*; + match self.subcommand { + OsSubcommand::Boot(args) => args.rebuild(Boot), + OsSubcommand::Test(args) => args.rebuild(Test), + OsSubcommand::Switch(args) => args.rebuild(Switch), + OsSubcommand::Build(args) => args.rebuild(Build), } } } -impl OsSubcommandArgs { - pub fn rebuild(&self, rebuild_type: &OsCommandType) -> Result<()> { - let use_sudo = if self.bypass_root_check { +#[derive(Debug)] +enum OsRebuildVariant { + Build, + Switch, + Boot, + Test, +} + +impl OsRebuildArgs { + fn rebuild(self, variant: OsRebuildVariant) -> Result<()> { + use OsRebuildVariant::*; + + let elevate = if self.bypass_root_check { warn!("Bypassing root check, now running nix as root"); false } else { @@ -40,13 +54,13 @@ impl OsSubcommandArgs { true }; - // TODO: add a .maybe_arg to CommandBuilder - // so that I can do .maybe_arg( Option ) - let sudo_args: &[_] = if use_sudo { &["sudo"] } else { &[] }; - let hostname = match &self.hostname { Some(h) => h.to_owned(), - None => hostname::get().context("Failed to get hostname")?, + None => hostname::get() + .context("Failed to get hostname")? + .to_str() + .unwrap() + .to_owned(), }; let out_path: Box = match self.common.out_link { @@ -59,49 +73,15 @@ impl OsSubcommandArgs { 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()?; - } + let toplevel = toplevel_for(hostname, self.common.installable.clone()); - commands::BuildCommandBuilder::default() - .flakeref(flake_output) - .message("Building NixOS configuration") - .extra_args(["--out-link"]) - .extra_args([out_path.get_path()]) + commands::Build::new(toplevel) + .extra_arg("--out-link") + .extra_arg(out_path.get_path()) .extra_args(&self.extra_args) + .message("Building NixOS configuration") .nom(!self.common.no_nom) - .build()? - .exec()?; + .run()?; let current_specialisation = std::fs::read_to_string(SPEC_LOCATION).ok(); @@ -120,14 +100,14 @@ impl OsSubcommandArgs { 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()]) + Command::new("nvd") + .arg("diff") + .arg(CURRENT_PROFILE) + .arg(&target_profile) .message("Comparing changes") - .build()? - .exec()?; + .run()?; - if self.common.dry || matches!(rebuild_type, OsCommandType::Build(_)) { + if self.common.dry || matches!(variant, Build) { return Ok(()); } @@ -140,41 +120,37 @@ impl OsSubcommandArgs { } } - if let Test(_) | Switch(_) = rebuild_type { + if let Test | Switch = variant { // !! 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"]) + Command::new(switch_to_configuration) + .arg("test") .message("Activating configuration") - .build()? - .exec()?; + .elevate(elevate) + .run()?; } - 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()?; + if let Boot | Switch = variant { + Command::new("nix") + .elevate(elevate) + .args(["build", "--profile", SYSTEM_PROFILE]) + .arg(out_path.get_path()) + .run()?; // !! 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"]) + Command::new(switch_to_configuration) + .arg("boot") + .elevate(elevate) .message("Adding configuration to bootloader") - .build()? - .exec()?; + .run()?; } // Make sure out_path is not accidentally dropped @@ -184,3 +160,34 @@ impl OsSubcommandArgs { Ok(()) } } + +fn toplevel_for>(hostname: S, installable: Installable) -> Installable { + let mut res = installable.clone(); + let hostname = hostname.as_ref().to_owned(); + + let toplevel = ["config", "system", "build", "toplevel"] + .into_iter() + .map(String::from); + + match res { + Installable::Flake { + ref mut attribute, .. + } => { + attribute.push(String::from("nixosConfigurations")); + attribute.push(hostname); + attribute.extend(toplevel); + } + Installable::File { + ref mut attribute, .. + } => { + attribute.extend(toplevel); + } + Installable::Expression { + ref mut attribute, .. + } => { + attribute.extend(toplevel); + } + } + + return res; +} diff --git a/src/repl.rs b/src/repl.rs deleted file mode 100644 index 807e721..0000000 --- a/src/repl.rs +++ /dev/null @@ -1,67 +0,0 @@ -use std::vec; - -use color_eyre::eyre::{bail, Context}; -use color_eyre::Result; - -use tracing::debug; - -use crate::interface::CommonReplArgs; -use crate::*; - -/// ReplVariant represents the variant of the target REPL. It should be one of -/// OsRepl or HomeRepl while beig passed to the repl function as an argument. -/// -/// Only OsRepl (nh os repl) is supported for the time being! -pub enum ReplVariant { - OsRepl, - HomeRepl, -} - -impl CommonReplArgs { - pub fn repl(&self, repl_variant: ReplVariant) -> Result<()> { - let mut repl_command = vec!["nix", "repl"]; - - // Push extra arguments to the repl command BEFORE the installable, is passed - // to the REPL. - if !&self.extra_args.is_empty() { - for arg in &self.extra_args { - repl_command.push(arg); - } - }; - - let hostname = match &self.hostname { - Some(h) => h.to_owned(), - None => hostname::get().context("Failed to get hostname")?, - }; - - // When (or if) HomeRepl is implemented, this can be changed to a more generic value - // and made mutable, so that the value is set in the match based on the variant of - // the REPL. For the time being, I am setting it here to ensure it lives long enough - // to be borrowed later. - // P.S. "flakeref" is an incredibly vague name, make sure to change it. - let flakeref = format!( - "{}#nixosConfigurations.{}", - self.flakeref.as_str(), - hostname.to_string_lossy() - ); - - // TODO: Implement match case for HomeRepl. - // See nixos.rs for the OsRepl implementation, the interface is now general enough - // to be reused without friction. - match repl_variant { - ReplVariant::OsRepl => repl_command.push(&flakeref), - ReplVariant::HomeRepl => bail!("OsRepl is not yet supported."), - } - - debug!("flakeref: {:?}", flakeref); - debug!("repl_command: {:?}", repl_command); - - commands::CommandBuilder::default() - .args(repl_command) - .message("Entering Nix REPL") - .build()? - .exec() - .unwrap(); - Ok(()) - } -} diff --git a/src/search.rs b/src/search.rs index 4a47291..1d29bf9 100644 --- a/src/search.rs +++ b/src/search.rs @@ -2,7 +2,7 @@ use std::{process::Stdio, time::Instant}; use color_eyre::eyre::{eyre, Context, ContextCompat}; use elasticsearch_dsl::*; -use interface::{FlakeRef, SearchArgs}; +use interface::SearchArgs; use regex::Regex; use serde::Deserialize; use tracing::{debug, trace, warn}; @@ -40,8 +40,8 @@ macro_rules! print_hyperlink { }; } -impl NHRunnable for SearchArgs { - fn run(&self) -> Result<()> { +impl SearchArgs { + pub fn run(&self) -> Result<()> { trace!("args: {self:?}"); let nixpkgs_path = std::thread::spawn(|| { diff --git a/test/nixos.nix b/test/nixos.nix new file mode 100644 index 0000000..32263fe --- /dev/null +++ b/test/nixos.nix @@ -0,0 +1,4 @@ +import { + configuration = {}; +} +