Skip to content
Closed
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
191 changes: 74 additions & 117 deletions src/darwin.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
use std::env;

use color_eyre::eyre::{bail, Context};
use tracing::{debug, info, warn};
use tracing::{debug, warn};

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::update::update;
use crate::util::get_hostname;
use crate::util::platform;
use crate::Result;

/// System profile path for darwin configurations
const SYSTEM_PROFILE: &str = "/nix/var/nix/profiles/system";
/// Current system profile path for darwin
const CURRENT_PROFILE: &str = "/run/current-system";

impl DarwinArgs {
/// Entry point for processing darwin commands
///
/// Handles the different subcommands for darwin configurations:
/// - Switch: Builds and activates the configuration
/// - Build: Only builds the configuration
/// - Repl: Opens a REPL for exploring the configuration
pub fn run(self) -> Result<()> {
use DarwinRebuildVariant::{Build, Switch};
match self.subcommand {
Expand All @@ -31,116 +36,92 @@ impl DarwinArgs {
}
}

/// Variants of the darwin rebuild operation
///
/// Each variant represents a different mode of operation:
/// - Switch: Build and activate the configuration
/// - Build: Only build the configuration without activation
enum DarwinRebuildVariant {
Switch,
Build,
}

impl DarwinRebuildArgs {
/// Performs the rebuild operation for darwin configurations
///
/// This function handles building and potentially activating darwin configurations.
/// It first builds the configuration, then shows a diff of changes compared to the
/// current system, and finally activates the configuration if needed.
///
/// The darwin activation process is unique and requires special handling compared
/// to `NixOS`, particularly around determining whether root privileges are needed.
fn rebuild(self, variant: DarwinRebuildVariant) -> Result<()> {
use DarwinRebuildVariant::{Build, Switch};

if nix::unistd::Uid::effective().is_root() {
bail!("Don't run nh os as root. I will call sudo internally as needed");
}
// Check if running as root
platform::check_not_root(false)?;

if self.update_args.update {
update(&self.common.installable, self.update_args.update_input)?;
}

let hostname = self.hostname.ok_or(()).or_else(|()| get_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)
}),
};

// Create temporary output path
let out_path = platform::create_output_path(self.common.out_link, "nh-darwin")?;
debug!(?out_path);

// Use NH_DARWIN_FLAKE if available, otherwise use the provided installable
let installable = if let Ok(darwin_flake) = env::var("NH_DARWIN_FLAKE") {
debug!("Using NH_DARWIN_FLAKE: {}", darwin_flake);

let mut elems = darwin_flake.splitn(2, '#');
let reference = elems.next().unwrap().to_owned();
let attribute = elems
.next()
.map(crate::installable::parse_attribute)
.unwrap_or_default();

Installable::Flake {
reference,
attribute,
}
} else {
self.common.installable.clone()
};

let mut processed_installable = installable;
if let Installable::Flake {
ref mut attribute, ..
} = processed_installable
{
// If user explicitly 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, processed_installable, "toplevel");

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.ask && !self.common.dry && !matches!(variant, Build) {
info!("Apply the config?");
let confirmation = dialoguer::Confirm::new().default(false).interact()?;

if !confirmation {
bail!("User rejected the new config");
}
// Resolve the installable from env var or from the provided argument
let installable =
platform::resolve_env_installable("NH_DARWIN_FLAKE", self.common.installable.clone());

// Build the darwin configuration with proper attribute path handling
let target_profile = platform::handle_rebuild_workflow(
installable,
"darwinConfigurations",
&["toplevel"],
Some(hostname),
out_path.as_ref(),
&self.extra_args,
None, // Darwin doesn't use remote builders
"Building Darwin configuration",
self.common.no_nom,
"", // Darwin doesn't use specialisations like NixOS
false,
None,
CURRENT_PROFILE,
false,
)?;

// Allow users to confirm before applying changes
if !platform::confirm_action(
self.common.ask && !matches!(variant, Build),
self.common.dry,
)? {
return Ok(());
}

if matches!(variant, Switch) {
Command::new("nix")
.args(["build", "--no-link", "--profile", SYSTEM_PROFILE])
.arg(out_path.get_path())
.arg(&target_profile)
.elevate(true)
.dry(self.common.dry)
.run()?;

let darwin_rebuild = out_path.get_path().join("sw/bin/darwin-rebuild");
let activate_user = out_path.get_path().join("activate-user");
let darwin_rebuild = target_profile.join("sw/bin/darwin-rebuild");
let activate_user = target_profile.join("activate-user");

// Determine if we need to elevate privileges
// Darwin activation may or may not need root privileges
// This checks if we need elevation based on the activation-user script
let needs_elevation = !activate_user
.try_exists()
.context("Failed to check if activate-user file exists")?
|| std::fs::read_to_string(&activate_user)
.context("Failed to read activate-user file")?
.contains("# nix-darwin: deprecated");

// Create and run the activation command with or without elevation
// Actually activate the configuration using darwin-rebuild
Command::new(darwin_rebuild)
.arg("activate")
.message("Activating configuration")
Expand All @@ -151,54 +132,30 @@ impl DarwinRebuildArgs {

// 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 {
/// Opens a Nix REPL for exploring darwin configurations
///
/// Provides an interactive environment to explore and evaluate
/// components of a darwin configuration.
fn run(self) -> Result<()> {
// Use NH_DARWIN_FLAKE if available, otherwise use the provided installable
let mut target_installable = if let Ok(darwin_flake) = env::var("NH_DARWIN_FLAKE") {
debug!("Using NH_DARWIN_FLAKE: {}", darwin_flake);

let mut elems = darwin_flake.splitn(2, '#');
let reference = elems.next().unwrap().to_owned();
let attribute = elems
.next()
.map(crate::installable::parse_attribute)
.unwrap_or_default();

Installable::Flake {
reference,
attribute,
}
} else {
self.installable
};

if matches!(target_installable, Installable::Store { .. }) {
if let Installable::Store { .. } = self.installable {
bail!("Nix doesn't support nix store installables.");
}

let hostname = self.hostname.ok_or(()).or_else(|()| get_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(())
// Open an interactive REPL session for exploring darwin configurations
platform::run_repl(
platform::resolve_env_installable("NH_DARWIN_FLAKE", self.installable),
"darwinConfigurations",
&[], // REPL doesn't need additional path elements
Some(hostname),
&[], // No extra REPL args
)
}
}
2 changes: 1 addition & 1 deletion src/generations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ pub fn from_dir(generation_dir: &Path) -> Option<u64> {
})
}

pub fn describe(generation_dir: &Path, current_profile: &Path) -> Option<GenerationInfo> {
pub fn describe(generation_dir: &Path, _current_profile: &Path) -> Option<GenerationInfo> {
let generation_number = from_dir(generation_dir)?;

// Get metadata once and reuse for both date and existence checks
Expand Down
Loading