diff --git a/Cargo.lock b/Cargo.lock index fd4b33ff..3cd9a985 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -182,6 +182,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + [[package]] name = "chrono" version = "0.4.35" @@ -921,12 +927,13 @@ dependencies = [ [[package]] name = "nix" -version = "0.27.1" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" +checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" dependencies = [ "bitflags 2.4.2", "cfg-if", + "cfg_aliases", "libc", ] diff --git a/Cargo.toml b/Cargo.toml index ad74a778..7232c4f3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,12 +8,6 @@ license = "LGPL-2.1" repository = "https://github.com/DeterminateSystems/nix-installer" documentation = "https://docs.rs/nix-installer/latest/nix_installer" -[package.metadata.riff.targets.aarch64-apple-darwin] -build-inputs = ["darwin.apple_sdk.frameworks.Security"] - -[package.metadata.riff.targets.x86_64-apple-darwin] -build-inputs = ["darwin.apple_sdk.frameworks.Security"] - [features] default = ["cli", "diagnostics"] cli = ["eyre", "color-eyre", "clap", "tracing-subscriber", "tracing-error"] @@ -30,7 +24,7 @@ clap = { version = "4", features = ["std", "color", "usage", "help", "error-cont color-eyre = { version = "0.6.2", default-features = false, features = [ "track-caller", "issue-url", "tracing-error", "capture-spantrace", "color-spantrace" ], optional = true } eyre = { version = "0.6.8", default-features = false, features = [ "track-caller" ], optional = true } glob = { version = "0.3.0", default-features = false } -nix = { version = "0.27.0", default-features = false, features = ["user", "fs", "process", "term"] } +nix = { version = "0.28.0", default-features = false, features = ["user", "fs", "process", "term"] } owo-colors = { version = "4.0.0", default-features = false, features = [ "supports-colors" ] } reqwest = { version = "0.11.11", default-features = false, features = ["rustls-tls-native-roots", "stream", "socks"] } serde = { version = "1.0.144", default-features = false, features = [ "std", "derive" ] } diff --git a/src/action/common/configure_enterprise_edition_init_service.rs b/src/action/common/configure_enterprise_edition_init_service.rs new file mode 100644 index 00000000..d4242fbd --- /dev/null +++ b/src/action/common/configure_enterprise_edition_init_service.rs @@ -0,0 +1,188 @@ +use std::path::PathBuf; + +#[cfg(target_os = "macos")] +use serde::{Deserialize, Serialize}; +#[cfg(target_os = "macos")] +use tokio::io::AsyncWriteExt; +use tokio::process::Command; +use tracing::{span, Span}; + +use crate::action::{ActionError, ActionErrorKind, ActionTag, StatefulAction}; +use crate::execute_command; + +use crate::action::{Action, ActionDescription}; + +#[cfg(target_os = "macos")] +const DARWIN_ENTERPRISE_EDITION_DAEMON_DEST: &str = + "/Library/LaunchDaemons/systems.determinate.nix-daemon.plist"; +/** +Configure the init to run the Nix daemon +*/ +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] +pub struct ConfigureEnterpriseEditionInitService { + start_daemon: bool, +} + +impl ConfigureEnterpriseEditionInitService { + #[tracing::instrument(level = "debug", skip_all)] + pub async fn plan(start_daemon: bool) -> Result, ActionError> { + Ok(Self { start_daemon }.into()) + } +} + +#[async_trait::async_trait] +#[typetag::serde(name = "configure_enterprise_edition_init_service")] +impl Action for ConfigureEnterpriseEditionInitService { + fn action_tag() -> ActionTag { + ActionTag("configure_enterprise_edition_init_service") + } + fn tracing_synopsis(&self) -> String { + "Configure the Determinate Nix Enterprise Edition daemon related settings with launchctl" + .to_string() + } + + fn tracing_span(&self) -> Span { + span!( + tracing::Level::DEBUG, + "configure_enterprise_edition_init_service" + ) + } + + fn execute_description(&self) -> Vec { + let mut explanation = vec![format!("Create `{DARWIN_ENTERPRISE_EDITION_DAEMON_DEST}`")]; + if self.start_daemon { + explanation.push(format!( + "Run `launchctl load {DARWIN_ENTERPRISE_EDITION_DAEMON_DEST}`" + )); + } + + vec![ActionDescription::new(self.tracing_synopsis(), explanation)] + } + + #[tracing::instrument(level = "debug", skip_all)] + async fn execute(&mut self) -> Result<(), ActionError> { + let Self { start_daemon } = self; + + let daemon_file = DARWIN_ENTERPRISE_EDITION_DAEMON_DEST; + let domain = "system"; + let service = "systems.determinate.nix-daemon"; + + let generated_plist = generate_plist(); + + let mut options = tokio::fs::OpenOptions::new(); + options.create(true).write(true).read(true); + + let mut file = options + .open(&daemon_file) + .await + .map_err(|e| Self::error(ActionErrorKind::Open(PathBuf::from(daemon_file), e)))?; + + let mut buf = Vec::new(); + plist::to_writer_xml(&mut buf, &generated_plist).map_err(Self::error)?; + file.write_all(&buf) + .await + .map_err(|e| Self::error(ActionErrorKind::Write(PathBuf::from(daemon_file), e)))?; + + execute_command( + Command::new("launchctl") + .process_group(0) + .args(["load", "-w"]) + .arg(daemon_file) + .stdin(std::process::Stdio::null()), + ) + .await + .map_err(Self::error)?; + + let is_disabled = crate::action::macos::service_is_disabled(domain, service) + .await + .map_err(Self::error)?; + if is_disabled { + execute_command( + Command::new("launchctl") + .process_group(0) + .arg("enable") + .arg(&format!("{domain}/{service}")) + .stdin(std::process::Stdio::null()), + ) + .await + .map_err(Self::error)?; + } + + if *start_daemon { + execute_command( + Command::new("launchctl") + .process_group(0) + .arg("kickstart") + .arg("-k") + .arg(&format!("{domain}/{service}")) + .stdin(std::process::Stdio::null()), + ) + .await + .map_err(Self::error)?; + } + + Ok(()) + } + + fn revert_description(&self) -> Vec { + vec![ActionDescription::new( + "Unconfigure Nix daemon related settings with launchctl".to_string(), + vec![format!( + "Run `launchctl unload {DARWIN_ENTERPRISE_EDITION_DAEMON_DEST}`" + )], + )] + } + + #[tracing::instrument(level = "debug", skip_all)] + async fn revert(&mut self) -> Result<(), ActionError> { + execute_command( + Command::new("launchctl") + .process_group(0) + .arg("unload") + .arg(DARWIN_ENTERPRISE_EDITION_DAEMON_DEST), + ) + .await + .map_err(Self::error)?; + + Ok(()) + } +} + +#[non_exhaustive] +#[derive(Debug, thiserror::Error)] +pub enum ConfigureEnterpriseEditionNixDaemonServiceError {} + +#[cfg(target_os = "macos")] +#[derive(Deserialize, Clone, Debug, Serialize, PartialEq)] +#[serde(rename_all = "PascalCase")] +pub struct DeterminateNixDaemonPlist { + label: String, + program: String, + keep_alive: bool, + run_at_load: bool, + standard_error_path: String, + standard_out_path: String, + soft_resource_limits: ResourceLimits, +} + +#[cfg(target_os = "macos")] +#[derive(Deserialize, Clone, Debug, Serialize, PartialEq)] +#[serde(rename_all = "PascalCase")] +pub struct ResourceLimits { + number_of_files: usize, +} + +#[cfg(target_os = "macos")] +fn generate_plist() -> DeterminateNixDaemonPlist { + DeterminateNixDaemonPlist { + keep_alive: true, + run_at_load: true, + label: "systems.determinate.nix-daemon".into(), + program: "/usr/local/bin/determinate-nix-ee".into(), + standard_error_path: "/var/log/determinate-nix-daemon.log".into(), + standard_out_path: "/var/log/determinate-nix-daemon.log".into(), + soft_resource_limits: ResourceLimits { + number_of_files: 1048576, + }, + } +} diff --git a/src/action/common/configure_init_service.rs b/src/action/common/configure_init_service.rs index a8c8e2ee..3abee543 100644 --- a/src/action/common/configure_init_service.rs +++ b/src/action/common/configure_init_service.rs @@ -1,6 +1,9 @@ #[cfg(target_os = "linux")] use std::path::Path; use std::path::PathBuf; + +#[cfg(target_os = "macos")] +use serde::{Deserialize, Serialize}; use tokio::process::Command; use tracing::{span, Span}; @@ -130,7 +133,7 @@ impl Action for ConfigureInitService { } fn tracing_span(&self) -> Span { - span!(tracing::Level::DEBUG, "configure_init_service",) + span!(tracing::Level::DEBUG, "configure_init_service") } fn execute_description(&self) -> Vec { @@ -152,7 +155,7 @@ impl Action for ConfigureInitService { #[cfg(target_os = "macos")] InitSystem::Launchd => { let mut explanation = vec![format!( - "Copy `{DARWIN_NIX_DAEMON_SOURCE}` to `DARWIN_NIX_DAEMON_DEST`" + "Copy `{DARWIN_NIX_DAEMON_SOURCE}` to `{DARWIN_NIX_DAEMON_DEST}`" )]; if self.start_daemon { explanation.push(format!("Run `launchctl load {DARWIN_NIX_DAEMON_DEST}`")); @@ -172,30 +175,29 @@ impl Action for ConfigureInitService { match init { #[cfg(target_os = "macos")] InitSystem::Launchd => { + let daemon_file = DARWIN_NIX_DAEMON_DEST; + let domain = "system"; + let service = "org.nixos.nix-daemon"; let src = std::path::Path::new(DARWIN_NIX_DAEMON_SOURCE); - tokio::fs::copy(src, DARWIN_NIX_DAEMON_DEST) - .await - .map_err(|e| { - Self::error(ActionErrorKind::Copy( - src.to_path_buf(), - PathBuf::from(DARWIN_NIX_DAEMON_DEST), - e, - )) - })?; + + tokio::fs::copy(src, daemon_file).await.map_err(|e| { + Self::error(ActionErrorKind::Copy( + src.to_path_buf(), + PathBuf::from(daemon_file), + e, + )) + })?; execute_command( Command::new("launchctl") .process_group(0) .args(["load", "-w"]) - .arg(DARWIN_NIX_DAEMON_DEST) + .arg(daemon_file) .stdin(std::process::Stdio::null()), ) .await .map_err(Self::error)?; - let domain = "system"; - let service = "org.nixos.nix-daemon"; - let is_disabled = crate::action::macos::service_is_disabled(domain, service) .await .map_err(Self::error)?; @@ -525,6 +527,26 @@ pub enum ConfigureNixDaemonServiceError { InitNotSupported, } +#[cfg(target_os = "macos")] +#[derive(Deserialize, Clone, Debug, Serialize, PartialEq)] +#[serde(rename_all = "PascalCase")] +pub struct DeterminateNixDaemonPlist { + label: String, + program: String, + keep_alive: bool, + run_at_load: bool, + standard_error_path: String, + standard_out_path: String, + soft_resource_limits: ResourceLimits, +} + +#[cfg(target_os = "macos")] +#[derive(Deserialize, Clone, Debug, Serialize, PartialEq)] +#[serde(rename_all = "PascalCase")] +pub struct ResourceLimits { + number_of_files: usize, +} + #[cfg(target_os = "linux")] async fn stop(unit: &str) -> Result<(), ActionErrorKind> { let mut command = Command::new("systemctl"); diff --git a/src/action/common/mod.rs b/src/action/common/mod.rs index 9ca61983..1bca186c 100644 --- a/src/action/common/mod.rs +++ b/src/action/common/mod.rs @@ -1,5 +1,7 @@ //! [`Action`](crate::action::Action)s which only call other base plugins +#[cfg(target_os = "macos")] +pub(crate) mod configure_enterprise_edition_init_service; pub(crate) mod configure_init_service; pub(crate) mod configure_nix; pub(crate) mod configure_shell_profile; @@ -9,6 +11,8 @@ pub(crate) mod delete_users; pub(crate) mod place_nix_configuration; pub(crate) mod provision_nix; +#[cfg(target_os = "macos")] +pub use configure_enterprise_edition_init_service::ConfigureEnterpriseEditionInitService; pub use configure_init_service::{ConfigureInitService, ConfigureNixDaemonServiceError}; pub use configure_nix::ConfigureNix; pub use configure_shell_profile::ConfigureShellProfile; diff --git a/src/action/macos/create_enterprise_edition_volume.rs b/src/action/macos/create_enterprise_edition_volume.rs new file mode 100644 index 00000000..2492ac86 --- /dev/null +++ b/src/action/macos/create_enterprise_edition_volume.rs @@ -0,0 +1,283 @@ +use crate::action::{ + base::{create_or_insert_into_file, CreateOrInsertIntoFile}, + macos::{ + CreateApfsVolume, CreateSyntheticObjects, EnableOwnership, EncryptApfsVolume, + UnmountApfsVolume, + }, + Action, ActionDescription, ActionError, ActionErrorKind, ActionTag, StatefulAction, +}; +use std::{ + path::{Path, PathBuf}, + time::Duration, +}; +use tokio::process::Command; +use tracing::{span, Span}; + +use super::create_fstab_entry::CreateFstabEntry; + +/// Create an APFS volume +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] +pub struct CreateEnterpriseEditionVolume { + disk: PathBuf, + name: String, + case_sensitive: bool, + create_or_append_synthetic_conf: StatefulAction, + create_synthetic_objects: StatefulAction, + unmount_volume: StatefulAction, + create_volume: StatefulAction, + create_fstab_entry: StatefulAction, + encrypt_volume: StatefulAction, + enable_ownership: StatefulAction, +} + +impl CreateEnterpriseEditionVolume { + #[tracing::instrument(level = "debug", skip_all)] + pub async fn plan( + disk: impl AsRef, + name: String, + case_sensitive: bool, + ) -> Result, ActionError> { + let disk = disk.as_ref(); + let create_or_append_synthetic_conf = CreateOrInsertIntoFile::plan( + "/etc/synthetic.conf", + None, + None, + None, + "nix\n".into(), /* The newline is required otherwise it segfaults */ + create_or_insert_into_file::Position::End, + ) + .await + .map_err(Self::error)?; + + let create_synthetic_objects = CreateSyntheticObjects::plan().await.map_err(Self::error)?; + + let unmount_volume = UnmountApfsVolume::plan(disk, name.clone()) + .await + .map_err(Self::error)?; + + let create_volume = CreateApfsVolume::plan(disk, name.clone(), case_sensitive) + .await + .map_err(Self::error)?; + + let create_fstab_entry = CreateFstabEntry::plan(name.clone(), &create_volume) + .await + .map_err(Self::error)?; + + let encrypt_volume = EncryptApfsVolume::plan(true, disk, &name, &create_volume).await?; + + let enable_ownership = EnableOwnership::plan("/nix").await.map_err(Self::error)?; + + Ok(Self { + disk: disk.to_path_buf(), + name, + case_sensitive, + create_or_append_synthetic_conf, + create_synthetic_objects, + unmount_volume, + create_volume, + create_fstab_entry, + encrypt_volume, + enable_ownership, + } + .into()) + } +} + +#[async_trait::async_trait] +#[typetag::serde(name = "create_apfs_enterprise_volume")] +impl Action for CreateEnterpriseEditionVolume { + fn action_tag() -> ActionTag { + ActionTag("create_enterprise_edition_volume") + } + fn tracing_synopsis(&self) -> String { + format!( + "Create an encrypted APFS volume `{name}` for Nix on `{disk}` and add it to `/etc/fstab` mounting on `/nix`", + name = self.name, + disk = self.disk.display(), + ) + } + + fn tracing_span(&self) -> Span { + span!( + tracing::Level::DEBUG, + "create_apfs_volume", + disk = tracing::field::display(self.disk.display()), + name = self.name + ) + } + + fn execute_description(&self) -> Vec { + let explanation = vec![ + self.create_or_append_synthetic_conf.tracing_synopsis(), + self.create_synthetic_objects.tracing_synopsis(), + self.unmount_volume.tracing_synopsis(), + self.create_volume.tracing_synopsis(), + self.create_fstab_entry.tracing_synopsis(), + self.encrypt_volume.tracing_synopsis(), + self.enable_ownership.tracing_synopsis(), + ]; + + vec![ActionDescription::new(self.tracing_synopsis(), explanation)] + } + + #[tracing::instrument(level = "debug", skip_all)] + async fn execute(&mut self) -> Result<(), ActionError> { + self.create_or_append_synthetic_conf + .try_execute() + .await + .map_err(Self::error)?; + self.create_synthetic_objects + .try_execute() + .await + .map_err(Self::error)?; + self.unmount_volume.try_execute().await.ok(); // We actually expect this may fail. + self.create_volume + .try_execute() + .await + .map_err(Self::error)?; + + let mut retry_tokens: usize = 50; + loop { + let mut command = Command::new("/usr/sbin/diskutil"); + command.args(["info", "-plist"]); + command.arg(&self.name); + command.stderr(std::process::Stdio::null()); + command.stdout(std::process::Stdio::null()); + tracing::trace!(%retry_tokens, command = ?command.as_std(), "Checking for Nix Store volume existence"); + let output = command + .output() + .await + .map_err(|e| ActionErrorKind::command(&command, e)) + .map_err(Self::error)?; + if output.status.success() { + break; + } else if retry_tokens == 0 { + return Err(Self::error(ActionErrorKind::command_output( + &command, output, + ))); + } else { + retry_tokens = retry_tokens.saturating_sub(1); + } + tokio::time::sleep(Duration::from_millis(100)).await; + } + + self.create_fstab_entry + .try_execute() + .await + .map_err(Self::error)?; + + self.encrypt_volume + .try_execute() + .await + .map_err(Self::error)?; + + let mut command = Command::new("/usr/local/bin/determinate-nix-ee"); + command.args(["--stop-after", "mount"]); + command.stderr(std::process::Stdio::piped()); + command.stdout(std::process::Stdio::piped()); + tracing::trace!(command = ?command.as_std(), "Mounting /nix"); + let output = command + .output() + .await + .map_err(|e| ActionErrorKind::command(&command, e)) + .map_err(Self::error)?; + if !output.status.success() { + return Err(Self::error(ActionErrorKind::command_output( + &command, output, + ))); + } + + let mut retry_tokens: usize = 50; + loop { + let mut command = Command::new("/usr/sbin/diskutil"); + command.args(["info", "/nix"]); + command.stderr(std::process::Stdio::null()); + command.stdout(std::process::Stdio::null()); + tracing::trace!(%retry_tokens, command = ?command.as_std(), "Checking for Nix Store mount path existence"); + let output = command + .output() + .await + .map_err(|e| ActionErrorKind::command(&command, e)) + .map_err(Self::error)?; + if output.status.success() { + break; + } else if retry_tokens == 0 { + return Err(Self::error(ActionErrorKind::command_output( + &command, output, + ))); + } else { + retry_tokens = retry_tokens.saturating_sub(1); + } + tokio::time::sleep(Duration::from_millis(100)).await; + } + + self.enable_ownership + .try_execute() + .await + .map_err(Self::error)?; + + Ok(()) + } + + fn revert_description(&self) -> Vec { + let explanation = vec![ + self.create_or_append_synthetic_conf.tracing_synopsis(), + self.create_synthetic_objects.tracing_synopsis(), + self.unmount_volume.tracing_synopsis(), + self.create_volume.tracing_synopsis(), + self.create_fstab_entry.tracing_synopsis(), + self.encrypt_volume.tracing_synopsis(), + self.enable_ownership.tracing_synopsis(), + ]; + + vec![ActionDescription::new( + format!( + "Remove the APFS volume `{}` on `{}`", + self.name, + self.disk.display() + ), + explanation, + )] + } + + #[tracing::instrument(level = "debug", skip_all)] + async fn revert(&mut self) -> Result<(), ActionError> { + let mut errors = vec![]; + + if let Err(err) = self.enable_ownership.try_revert().await { + errors.push(err) + }; + if let Err(err) = self.encrypt_volume.try_revert().await { + errors.push(err) + } + if let Err(err) = self.create_fstab_entry.try_revert().await { + errors.push(err) + } + + if let Err(err) = self.unmount_volume.try_revert().await { + errors.push(err) + } + if let Err(err) = self.create_volume.try_revert().await { + errors.push(err) + } + + // Purposefully not reversed + if let Err(err) = self.create_or_append_synthetic_conf.try_revert().await { + errors.push(err) + } + if let Err(err) = self.create_synthetic_objects.try_revert().await { + errors.push(err) + } + + if errors.is_empty() { + Ok(()) + } else if errors.len() == 1 { + Err(errors + .into_iter() + .next() + .expect("Expected 1 len Vec to have at least 1 item")) + } else { + Err(Self::error(ActionErrorKind::MultipleChildren(errors))) + } + } +} diff --git a/src/action/macos/create_nix_volume.rs b/src/action/macos/create_nix_volume.rs index 9c4fec5b..e629a39d 100644 --- a/src/action/macos/create_nix_volume.rs +++ b/src/action/macos/create_nix_volume.rs @@ -71,7 +71,7 @@ impl CreateNixVolume { .map_err(Self::error)?; let encrypt_volume = if encrypt { - Some(EncryptApfsVolume::plan(disk, &name, &create_volume).await?) + Some(EncryptApfsVolume::plan(false, disk, &name, &create_volume).await?) } else { None }; @@ -154,11 +154,9 @@ impl Action for CreateNixVolume { if let Some(encrypt_volume) = &self.encrypt_volume { explanation.push(encrypt_volume.tracing_synopsis()); } - explanation.append(&mut vec![ - self.setup_volume_daemon.tracing_synopsis(), - self.bootstrap_volume.tracing_synopsis(), - self.enable_ownership.tracing_synopsis(), - ]); + explanation.push(self.setup_volume_daemon.tracing_synopsis()); + explanation.push(self.bootstrap_volume.tracing_synopsis()); + explanation.push(self.enable_ownership.tracing_synopsis()); vec![ActionDescription::new(self.tracing_synopsis(), explanation)] } @@ -220,6 +218,7 @@ impl Action for CreateNixVolume { .try_execute() .await .map_err(Self::error)?; + self.kickstart_launchctl_service .try_execute() .await @@ -268,11 +267,9 @@ impl Action for CreateNixVolume { if let Some(encrypt_volume) = &self.encrypt_volume { explanation.push(encrypt_volume.tracing_synopsis()); } - explanation.append(&mut vec![ - self.setup_volume_daemon.tracing_synopsis(), - self.bootstrap_volume.tracing_synopsis(), - self.enable_ownership.tracing_synopsis(), - ]); + explanation.push(self.setup_volume_daemon.tracing_synopsis()); + explanation.push(self.bootstrap_volume.tracing_synopsis()); + explanation.push(self.enable_ownership.tracing_synopsis()); vec![ActionDescription::new( format!( @@ -293,13 +290,14 @@ impl Action for CreateNixVolume { }; if let Err(err) = self.kickstart_launchctl_service.try_revert().await { errors.push(err) - }; + } if let Err(err) = self.bootstrap_volume.try_revert().await { errors.push(err) - }; + } if let Err(err) = self.setup_volume_daemon.try_revert().await { errors.push(err) - }; + } + if let Some(encrypt_volume) = &mut self.encrypt_volume { if let Err(err) = encrypt_volume.try_revert().await { errors.push(err) diff --git a/src/action/macos/encrypt_apfs_volume.rs b/src/action/macos/encrypt_apfs_volume.rs index 78b8f005..aa2f60ae 100644 --- a/src/action/macos/encrypt_apfs_volume.rs +++ b/src/action/macos/encrypt_apfs_volume.rs @@ -21,6 +21,7 @@ Encrypt an APFS volume */ #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] pub struct EncryptApfsVolume { + enterprise_edition: bool, disk: PathBuf, name: String, } @@ -28,6 +29,7 @@ pub struct EncryptApfsVolume { impl EncryptApfsVolume { #[tracing::instrument(level = "debug", skip_all)] pub async fn plan( + enterprise_edition: bool, disk: impl AsRef, name: impl AsRef, planned_create_apfs_volume: &StatefulAction, @@ -57,7 +59,11 @@ impl EncryptApfsVolume { // The user has a password matching what we would create. if planned_create_apfs_volume.state == ActionState::Completed { // We detected a created volume already, and a password exists, so we can keep using that and skip doing anything - return Ok(StatefulAction::completed(Self { name, disk })); + return Ok(StatefulAction::completed(Self { + enterprise_edition, + name, + disk, + })); } // Ask the user to remove it @@ -87,13 +93,21 @@ impl EncryptApfsVolume { EncryptApfsVolumeError::ExistingVolumeNotEncrypted(name, disk), )); } else { - return Ok(StatefulAction::completed(Self { disk, name })); + return Ok(StatefulAction::completed(Self { + enterprise_edition, + disk, + name, + })); } } } } - Ok(StatefulAction::uncompleted(Self { name, disk })) + Ok(StatefulAction::uncompleted(Self { + enterprise_edition, + name, + disk, + })) } } @@ -127,7 +141,11 @@ impl Action for EncryptApfsVolume { disk = %self.disk.display(), ))] async fn execute(&mut self) -> Result<(), ActionError> { - let Self { disk, name } = self; + let Self { + enterprise_edition, + disk, + name, + } = self; // Generate a random password. let password: String = { @@ -152,35 +170,38 @@ impl Action for EncryptApfsVolume { .map_err(Self::error)?; // Add the password to the user keychain so they can unlock it later. - execute_command( - Command::new("/usr/bin/security").process_group(0).args([ - "add-generic-password", - "-a", - name.as_str(), - "-s", - "Nix Store", - "-l", - format!("{} encryption password", disk_str).as_str(), - "-D", - "Encrypted volume password", - "-j", - format!( - "Added automatically by the Nix installer for use by {NIX_VOLUME_MOUNTD_DEST}" - ) + let mut cmd = Command::new("/usr/bin/security"); + cmd.process_group(0).args([ + "add-generic-password", + "-a", + name.as_str(), + "-s", + "Nix Store", + "-l", + format!("{} encryption password", disk_str).as_str(), + "-D", + "Encrypted volume password", + "-j", + format!("Added automatically by the Nix installer for use by {NIX_VOLUME_MOUNTD_DEST}") .as_str(), - "-w", - password.as_str(), - "-T", - "/System/Library/CoreServices/APFSUserAgent", - "-T", - "/System/Library/CoreServices/CSUserAgent", - "-T", - "/usr/bin/security", - "/Library/Keychains/System.keychain", - ]), - ) - .await - .map_err(Self::error)?; + "-w", + password.as_str(), + "-T", + "/System/Library/CoreServices/APFSUserAgent", + "-T", + "/System/Library/CoreServices/CSUserAgent", + "-T", + "/usr/bin/security", + ]); + + if *enterprise_edition { + cmd.args(["-T", "/usr/local/bin/determinate-nix-ee"]); + } + + cmd.arg("/Library/Keychains/System.keychain"); + + // Add the password to the user keychain so they can unlock it later. + execute_command(&mut cmd).await.map_err(Self::error)?; // Encrypt the mounted volume execute_command(Command::new("/usr/sbin/diskutil").process_group(0).args([ diff --git a/src/action/macos/mod.rs b/src/action/macos/mod.rs index 7ad01fb9..e5c12fc3 100644 --- a/src/action/macos/mod.rs +++ b/src/action/macos/mod.rs @@ -4,6 +4,7 @@ pub(crate) mod bootstrap_launchctl_service; pub(crate) mod configure_remote_building; pub(crate) mod create_apfs_volume; +pub(crate) mod create_enterprise_edition_volume; pub(crate) mod create_fstab_entry; pub(crate) mod create_nix_hook_service; pub(crate) mod create_nix_volume; @@ -19,6 +20,7 @@ pub(crate) mod unmount_apfs_volume; pub use bootstrap_launchctl_service::BootstrapLaunchctlService; pub use configure_remote_building::ConfigureRemoteBuilding; pub use create_apfs_volume::CreateApfsVolume; +pub use create_enterprise_edition_volume::CreateEnterpriseEditionVolume; pub use create_nix_hook_service::CreateNixHookService; pub use create_nix_volume::{CreateNixVolume, NIX_VOLUME_MOUNTD_DEST}; pub use create_synthetic_objects::CreateSyntheticObjects; diff --git a/src/cli/arg/instrumentation.rs b/src/cli/arg/instrumentation.rs index 9a177b8b..d4ecf652 100644 --- a/src/cli/arg/instrumentation.rs +++ b/src/cli/arg/instrumentation.rs @@ -137,7 +137,7 @@ impl Instrumentation { _ => return Err(e).wrap_err_with(|| "parsing RUST_LOG directives"), } } - EnvFilter::try_new(&format!( + EnvFilter::try_new(format!( "{}={}", env!("CARGO_PKG_NAME").replace('-', "_"), self.log_level() diff --git a/src/cli/subcommand/uninstall.rs b/src/cli/subcommand/uninstall.rs index 158fa0db..6237a946 100644 --- a/src/cli/subcommand/uninstall.rs +++ b/src/cli/subcommand/uninstall.rs @@ -90,7 +90,7 @@ impl CommandExecute for Uninstall { }) .collect() }; - let temp_exe = temp.join(&format!("nix-installer-{random_trailer}")); + let temp_exe = temp.join(format!("nix-installer-{random_trailer}")); tokio::fs::copy(¤t_exe, &temp_exe) .await .wrap_err("Copying nix-installer to tempdir")?; diff --git a/src/os/darwin.rs b/src/os/darwin/diskutil.rs similarity index 100% rename from src/os/darwin.rs rename to src/os/darwin/diskutil.rs diff --git a/src/os/darwin/mod.rs b/src/os/darwin/mod.rs new file mode 100644 index 00000000..4f9e3f8a --- /dev/null +++ b/src/os/darwin/mod.rs @@ -0,0 +1,3 @@ +pub mod diskutil; + +pub use diskutil::{DiskUtilApfsListOutput, DiskUtilInfoOutput}; diff --git a/src/planner/macos.rs b/src/planner/macos.rs index 347e4bb3..bb287b73 100644 --- a/src/planner/macos.rs +++ b/src/planner/macos.rs @@ -13,7 +13,8 @@ use crate::{ base::RemoveDirectory, common::{ConfigureInitService, ConfigureNix, CreateUsersAndGroups, ProvisionNix}, macos::{ - ConfigureRemoteBuilding, CreateNixHookService, CreateNixVolume, SetTmutilExclusions, + ConfigureRemoteBuilding, CreateEnterpriseEditionVolume, CreateNixHookService, + CreateNixVolume, SetTmutilExclusions, }, StatefulAction, }, @@ -25,6 +26,9 @@ use crate::{ Action, BuiltinPlanner, }; +#[cfg(target_os = "macos")] +use crate::action::common::ConfigureEnterpriseEditionInitService; + /// A planner for MacOS (Darwin) systems #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[cfg_attr(feature = "cli", derive(clap::Parser))] @@ -54,6 +58,16 @@ pub struct Macos { ) )] pub case_sensitive: bool, + /// Enable Determinate Nix Enterprise Edition. See: https://determinate.systems/enterprise + #[cfg_attr( + feature = "cli", + clap( + long, + env = "NIX_INSTALLER_ENTERPRISE_EDITION", + default_value = "false" + ) + )] + pub enterprise_edition: bool, /// The label for the created APFS volume #[cfg_attr( feature = "cli", @@ -88,6 +102,7 @@ impl Planner for Macos { root_disk: Some(default_root_disk().await?), case_sensitive: false, encrypt: None, + enterprise_edition: false, volume_label: "Nix Store".into(), }) } @@ -110,9 +125,13 @@ impl Planner for Macos { }, }; - let encrypt = match self.encrypt { - Some(choice) => choice, - None => { + // The encrypt variable isn't used in the enterprise edition since we have our own plan step for it, + // however this match accounts for enterprise edition so the receipt indicates encrypt: true. + // This is a goofy thing to do, but it is in an attempt to make a more globally coherent plan / receipt. + let encrypt = match (self.enterprise_edition, self.encrypt) { + (true, _) => true, + (false, Some(choice)) => choice, + (false, None) => { let output = Command::new("/usr/bin/fdesetup") .arg("isactive") .stdout(std::process::Stdio::null()) @@ -131,17 +150,31 @@ impl Planner for Macos { let mut plan = vec![]; - plan.push( - CreateNixVolume::plan( - root_disk.unwrap(), /* We just ensured it was populated */ - self.volume_label.clone(), - self.case_sensitive, - encrypt, - ) - .await - .map_err(PlannerError::Action)? - .boxed(), - ); + if self.enterprise_edition { + plan.push( + CreateEnterpriseEditionVolume::plan( + root_disk.unwrap(), /* We just ensured it was populated */ + self.volume_label.clone(), + self.case_sensitive, + ) + .await + .map_err(PlannerError::Action)? + .boxed(), + ); + } else { + plan.push( + CreateNixVolume::plan( + root_disk.unwrap(), /* We just ensured it was populated */ + self.volume_label.clone(), + self.case_sensitive, + encrypt, + ) + .await + .map_err(PlannerError::Action)? + .boxed(), + ); + } + plan.push( ProvisionNix::plan(&self.settings) .await @@ -184,12 +217,21 @@ impl Planner for Macos { ); } - plan.push( - ConfigureInitService::plan(InitSystem::Launchd, true) - .await - .map_err(PlannerError::Action)? - .boxed(), - ); + if self.enterprise_edition { + plan.push( + ConfigureEnterpriseEditionInitService::plan(true) + .await + .map_err(PlannerError::Action)? + .boxed(), + ); + } else { + plan.push( + ConfigureInitService::plan(InitSystem::Launchd, true) + .await + .map_err(PlannerError::Action)? + .boxed(), + ); + } plan.push( RemoveDirectory::plan(crate::settings::SCRATCH_DIR) .await @@ -204,6 +246,7 @@ impl Planner for Macos { let Self { settings, encrypt, + enterprise_edition, volume_label, case_sensitive, root_disk, @@ -211,6 +254,10 @@ impl Planner for Macos { let mut map = HashMap::default(); map.extend(settings.settings()?); + map.insert( + "enterprise_edition".into(), + serde_json::to_value(enterprise_edition)?, + ); map.insert("volume_encrypt".into(), serde_json::to_value(encrypt)?); map.insert("volume_label".into(), serde_json::to_value(volume_label)?); map.insert("root_disk".into(), serde_json::to_value(root_disk)?); @@ -260,6 +307,9 @@ impl Planner for Macos { async fn pre_install_check(&self) -> Result<(), PlannerError> { check_not_running_in_rosetta()?; + if self.enterprise_edition { + check_enterprise_edition_available().await?; + } Ok(()) } @@ -314,6 +364,14 @@ fn check_not_running_in_rosetta() -> Result<(), PlannerError> { Ok(()) } +async fn check_enterprise_edition_available() -> Result<(), PlannerError> { + tokio::fs::metadata("/usr/local/bin/determinate-nix-ee") + .await + .map_err(|_| PlannerError::EnterpriseEditionUnavailable)?; + + Ok(()) +} + #[non_exhaustive] #[derive(thiserror::Error, Debug)] pub enum MacosError { diff --git a/src/planner/mod.rs b/src/planner/mod.rs index 97e72503..3d776545 100644 --- a/src/planner/mod.rs +++ b/src/planner/mod.rs @@ -415,6 +415,8 @@ pub enum PlannerError { Sysctl(#[from] sysctl::SysctlError), #[error("Detected that this process is running under Rosetta, using Nix in Rosetta is not supported (Please open an issue with your use case)")] RosettaDetected, + #[error("Determinate Nix Enterprise Edition is not available. See: https://determinate.systems/enterprise")] + EnterpriseEditionUnavailable, /// A Linux SELinux related error #[error("Unable to install on an SELinux system without common SELinux tooling, the binaries `restorecon`, and `semodule` are required")] SelinuxRequirements, @@ -447,6 +449,7 @@ impl HasExpectedErrors for PlannerError { PlannerError::Plist(_) => None, PlannerError::Sysctl(_) => None, this @ PlannerError::RosettaDetected => Some(Box::new(this)), + this @ PlannerError::EnterpriseEditionUnavailable => Some(Box::new(this)), PlannerError::OsRelease(_) => None, PlannerError::Utf8(_) => None, PlannerError::SelinuxRequirements => Some(Box::new(self)), diff --git a/tests/fixtures/linux/linux.json b/tests/fixtures/linux/linux.json index e30c6e5e..fad76419 100644 --- a/tests/fixtures/linux/linux.json +++ b/tests/fixtures/linux/linux.json @@ -388,7 +388,8 @@ "action": "configure_init_service", "init": "Systemd", "start_daemon": true, - "ssl_cert_file": null + "ssl_cert_file": null, + "enterprise_edition": false }, "state": "Uncompleted" }, @@ -435,4 +436,4 @@ "ssl_cert_file": null, "failure_chain": null } -} +} \ No newline at end of file diff --git a/tests/fixtures/linux/steam-deck.json b/tests/fixtures/linux/steam-deck.json index 311ab87b..5b21dc1d 100644 --- a/tests/fixtures/linux/steam-deck.json +++ b/tests/fixtures/linux/steam-deck.json @@ -363,7 +363,8 @@ "action": "configure_init_service", "init": "Systemd", "start_daemon": true, - "ssl_cert_file": null + "ssl_cert_file": null, + "enterprise_edition": false }, "state": "Uncompleted" }, @@ -415,4 +416,4 @@ "ssl_cert_file": null, "failure_chain": null } -} +} \ No newline at end of file diff --git a/tests/fixtures/macos/macos.json b/tests/fixtures/macos/macos.json index 6efbfe26..8e96b65d 100644 --- a/tests/fixtures/macos/macos.json +++ b/tests/fixtures/macos/macos.json @@ -8,6 +8,7 @@ "name": "Nix Store", "case_sensitive": false, "encrypt": false, + "enterprise_edition": false, "create_or_append_synthetic_conf": { "action": { "path": "/etc/synthetic.conf", @@ -399,7 +400,8 @@ "action": "configure_init_service", "init": "Launchd", "start_daemon": true, - "ssl_cert_file": null + "ssl_cert_file": null, + "enterprise_edition": false }, "state": "Uncompleted" }, @@ -429,6 +431,7 @@ "force": false, "diagnostic_endpoint": "https://install.determinate.systems/nix/diagnostic" }, + "enterprise_edition": false, "encrypt": null, "case_sensitive": false, "volume_label": "Nix Store", @@ -446,4 +449,4 @@ "ssl_cert_file": null, "failure_chain": null } -} +} \ No newline at end of file