diff --git a/Cargo.lock b/Cargo.lock index 7751eb3..24a7b33 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -250,6 +250,22 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "dialoguer" version = "0.11.0" @@ -810,6 +826,7 @@ dependencies = [ "serde_json", "subprocess", "supports-hyperlinks", + "system-configuration", "tempfile", "textwrap", "thiserror 2.0.3", @@ -1373,6 +1390,27 @@ dependencies = [ "syn", ] +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.6.0", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tempfile" version = "3.14.0" diff --git a/Cargo.toml b/Cargo.toml index 5a55b3e..22bfc7b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,3 +50,6 @@ tracing-subscriber = { version = "0.3.18", features = [ "std" ] } uzers = { version = "0.12.0", default-features = false } + +[target.'cfg(target_os="macos")'.dependencies] +system-configuration = "0.6.1" diff --git a/src/clean.rs b/src/clean.rs index a84f35f..163bfeb 100644 --- a/src/clean.rs +++ b/src/clean.rs @@ -56,9 +56,17 @@ impl interface::CleanMode { let path = read_dir?.path(); profiles.extend(profiles_in_dir(path)); } - debug!("Scanning XDG profiles for users 0, 1000-1100"); + + // Most unix systems start regular users at uid 1000+, but macos is special at 501+ + // https://en.wikipedia.org/wiki/User_identifier + #[cfg(target_os = "linux")] + let uid_min = 1000; + #[cfg(target_os = "macos")] + let uid_min = 501; + let uid_max = uid_min + 100; + debug!("Scanning XDG profiles for users 0, ${uid_min}-${uid_max}"); for user in unsafe { uzers::all_users() } { - if user.uid() >= 1000 && user.uid() < 1100 || user.uid() == 0 { + if user.uid() >= uid_min && user.uid() < uid_max || user.uid() == 0 { debug!(?user, "Adding XDG profiles for user"); profiles.extend(profiles_in_dir( user.home_dir().join(".local/state/nix/profiles"), diff --git a/src/darwin.rs b/src/darwin.rs index 4e5e963..846f479 100644 --- a/src/darwin.rs +++ b/src/darwin.rs @@ -1,6 +1,16 @@ +use color_eyre::eyre::{bail, Context}; +use tracing::{debug, info}; + +use crate::commands; +use crate::commands::Command; +use crate::installable::Installable; use crate::interface::{DarwinArgs, DarwinRebuildArgs, DarwinReplArgs, DarwinSubcommand}; +use crate::nixos::toplevel_for; use crate::Result; +const SYSTEM_PROFILE: &str = "/nix/var/nix/profiles/system"; +const CURRENT_PROFILE: &str = "/run/current-system"; + impl DarwinArgs { pub fn run(self) -> Result<()> { use DarwinRebuildVariant::*; @@ -17,14 +27,156 @@ enum DarwinRebuildVariant { Build, } +fn get_hostname(hostname: Option) -> Result { + match &hostname { + Some(h) => Ok(h.to_owned()), + None => { + #[cfg(not(target_os = "macos"))] + { + Ok(hostname::get() + .context("Failed to get hostname")? + .to_str() + .unwrap() + .to_string()) + } + #[cfg(target_os = "macos")] + { + use system_configuration::{ + core_foundation::{base::TCFType, string::CFString}, + sys::dynamic_store_copy_specific::SCDynamicStoreCopyLocalHostName, + }; + + let ptr = unsafe { SCDynamicStoreCopyLocalHostName(std::ptr::null()) }; + if ptr.is_null() { + bail!("Failed to get hostname"); + } + let name = unsafe { CFString::wrap_under_get_rule(ptr) }; + + Ok(name.to_string()) + } + } + } +} + impl DarwinRebuildArgs { - fn rebuild(self, _variant: DarwinRebuildVariant) -> Result<()> { - todo!(); + fn rebuild(self, variant: DarwinRebuildVariant) -> Result<()> { + use DarwinRebuildVariant::*; + + if nix::unistd::Uid::effective().is_root() { + bail!("Don't run nh os as root. I will call sudo internally as needed"); + } + + let hostname = get_hostname(self.hostname)?; + + 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-os").tempdir()?; + (dir.as_ref().join("result"), dir) + }), + }; + + debug!(?out_path); + + let mut installable = self.common.installable.clone(); + if let Installable::Flake { + ref mut attribute, .. + } = installable + { + // If user explicitely selects some other attribute, don't push darwinConfigurations + if attribute.is_empty() { + attribute.push(String::from("darwinConfigurations")); + attribute.push(hostname.clone()); + } + } + + let toplevel = toplevel_for(hostname, installable); + + commands::Build::new(toplevel) + .extra_arg("--out-link") + .extra_arg(out_path.get_path()) + .extra_args(&self.extra_args) + .message("Building Darwin configuration") + .nom(!self.common.no_nom) + .run()?; + + let target_profile = out_path.get_path().to_owned(); + + target_profile.try_exists().context("Doesn't exist")?; + + Command::new("nvd") + .arg("diff") + .arg(CURRENT_PROFILE) + .arg(&target_profile) + .message("Comparing changes") + .run()?; + + if self.common.dry || matches!(variant, 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 Switch = variant { + Command::new("nix") + .args(["build", "--profile", SYSTEM_PROFILE]) + .arg(out_path.get_path()) + .run()?; + + let switch_to_configuration = out_path.get_path().join("activate-user"); + + Command::new(switch_to_configuration) + .message("Activating configuration for user") + .run()?; + + let switch_to_configuration = out_path.get_path().join("activate"); + + Command::new(switch_to_configuration) + .elevate(true) + .message("Activating configuration") + .run()?; + } + + // 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(()) } } impl DarwinReplArgs { fn run(self) -> Result<()> { - todo!(); + let mut target_installable = self.installable; + + if matches!(target_installable, Installable::Store { .. }) { + bail!("Nix doesn't support nix store installables."); + } + + let hostname = get_hostname(self.hostname)?; + + if let Installable::Flake { + ref mut attribute, .. + } = target_installable + { + if attribute.is_empty() { + attribute.push(String::from("darwinConfigurations")); + attribute.push(hostname); + } + } + + Command::new("nix") + .arg("repl") + .args(target_installable.to_args()) + .run()?; + + Ok(()) } } diff --git a/src/interface.rs b/src/interface.rs index 1a336b7..e9a3dc6 100644 --- a/src/interface.rs +++ b/src/interface.rs @@ -282,6 +282,9 @@ pub struct CompletionArgs { pub shell: clap_complete::Shell, } +/// Nix-darwin functionality +/// +/// Implements functionality mostly around but not exclusive to darwin-rebuild #[derive(Debug, Args)] pub struct DarwinArgs { #[command(subcommand)] @@ -299,10 +302,22 @@ pub enum DarwinSubcommand { pub struct DarwinRebuildArgs { #[command(flatten)] pub common: CommonRebuildArgs, + + /// When using a flake installable, select this hostname from darwinConfigurations + #[arg(long, short = 'H', global = true)] + pub hostname: Option, + + /// Extra arguments passed to nix build + #[arg(last = true)] + pub extra_args: Vec, } #[derive(Debug, Args)] pub struct DarwinReplArgs { #[command(flatten)] pub installable: Installable, + + /// When using a flake installable, select this hostname from darwinConfigurations + #[arg(long, short = 'H', global = true)] + pub hostname: Option, } diff --git a/src/nixos.rs b/src/nixos.rs index 5e99c81..21df571 100644 --- a/src/nixos.rs +++ b/src/nixos.rs @@ -155,7 +155,7 @@ impl OsRebuildArgs { } } -fn toplevel_for>(hostname: S, installable: Installable) -> Installable { +pub fn toplevel_for>(hostname: S, installable: Installable) -> Installable { let mut res = installable.clone(); let hostname = hostname.as_ref().to_owned();