Skip to content

Commit

Permalink
Add nix-darwin support
Browse files Browse the repository at this point in the history
  • Loading branch information
ToyVo committed Nov 19, 2024
1 parent 9e53b0a commit fa1973d
Show file tree
Hide file tree
Showing 6 changed files with 222 additions and 6 deletions.
38 changes: 38 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
12 changes: 10 additions & 2 deletions src/clean.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
158 changes: 155 additions & 3 deletions src/darwin.rs
Original file line number Diff line number Diff line change
@@ -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::*;
Expand All @@ -17,14 +27,156 @@ enum DarwinRebuildVariant {
Build,
}

fn get_hostname(hostname: Option<String>) -> Result<String> {
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<dyn crate::util::MaybeTempPath> = 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(())
}
}
15 changes: 15 additions & 0 deletions src/interface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand All @@ -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<String>,

/// Extra arguments passed to nix build
#[arg(last = true)]
pub extra_args: Vec<String>,
}

#[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<String>,
}
2 changes: 1 addition & 1 deletion src/nixos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ impl OsRebuildArgs {
}
}

fn toplevel_for<S: AsRef<str>>(hostname: S, installable: Installable) -> Installable {
pub fn toplevel_for<S: AsRef<str>>(hostname: S, installable: Installable) -> Installable {
let mut res = installable.clone();
let hostname = hostname.as_ref().to_owned();

Expand Down

0 comments on commit fa1973d

Please sign in to comment.