diff --git a/Cargo.lock b/Cargo.lock index edde270..2eb4ed8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -498,6 +498,26 @@ dependencies = [ "syn", ] +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + [[package]] name = "encoding_rs" version = "0.8.31" @@ -684,6 +704,17 @@ dependencies = [ "slab", ] +[[package]] +name = "getrandom" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "ghost" version = "0.1.6" @@ -749,6 +780,7 @@ dependencies = [ "clap", "color-eyre", "crossterm", + "dirs", "eyre", "futures 0.3.24", "glob", @@ -1380,6 +1412,17 @@ dependencies = [ "bitflags", ] +[[package]] +name = "redox_users" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +dependencies = [ + "getrandom", + "redox_syscall", + "thiserror", +] + [[package]] name = "regex" version = "1.6.0" diff --git a/Cargo.toml b/Cargo.toml index 6f6f0e0..5a6bed3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,3 +39,4 @@ sxd-xpath = "0.4.2" xz2 = { version = "0.1.7", features = ["static", "tokio"] } sxd-document = "0.3.2" plist = "1.3.1" +dirs = "4.0.0" diff --git a/src/actions/base/configure_nix_daemon_service.rs b/src/actions/base/configure_nix_daemon_service.rs index bba95de..8c234fb 100644 --- a/src/actions/base/configure_nix_daemon_service.rs +++ b/src/actions/base/configure_nix_daemon_service.rs @@ -1,6 +1,7 @@ use std::path::{Path, PathBuf}; use serde::Serialize; +use target_lexicon::OperatingSystem; use tokio::fs::remove_file; use tokio::process::Command; @@ -21,9 +22,20 @@ pub struct ConfigureNixDaemonService { impl ConfigureNixDaemonService { #[tracing::instrument(skip_all)] pub async fn plan() -> Result { - if !Path::new("/run/systemd/system").exists() { - return Err(ConfigureNixDaemonServiceError::InitNotSupported); - } + match OperatingSystem::host() { + OperatingSystem::MacOSX { + major: _, + minor: _, + patch: _, + } + | OperatingSystem::Darwin => (), + _ => { + if !Path::new("/run/systemd/system").exists() { + return Err(ConfigureNixDaemonServiceError::InitNotSupported); + } + }, + }; + Ok(Self { action_state: ActionState::Uncompleted, }) @@ -59,32 +71,68 @@ impl Actionable for ConfigureNixDaemonService { } tracing::debug!("Configuring nix daemon service"); - tracing::trace!(src = TMPFILES_SRC, dest = TMPFILES_DEST, "Symlinking"); - tokio::fs::symlink(TMPFILES_SRC, TMPFILES_DEST) - .await - .map_err(|e| { - Self::Error::Symlink(PathBuf::from(TMPFILES_SRC), PathBuf::from(TMPFILES_DEST), e) - })?; + match OperatingSystem::host() { + OperatingSystem::MacOSX { + major: _, + minor: _, + patch: _, + } + | OperatingSystem::Darwin => { + const DARWIN_NIX_DAEMON_DEST: &str = + "/Library/LaunchDaemons/org.nixos.nix-daemon.plist"; - execute_command( - Command::new("systemd-tmpfiles") - .arg("--create") - .arg("--prefix=/nix/var/nix"), - ) - .await - .map_err(Self::Error::CommandFailed)?; + let src = Path::new("/nix/var/nix/profiles/default").join(DARWIN_NIX_DAEMON_DEST); + tokio::fs::copy(src.clone(), DARWIN_NIX_DAEMON_DEST) + .await + .map_err(|e| { + Self::Error::Copy( + src.to_path_buf(), + PathBuf::from(DARWIN_NIX_DAEMON_DEST), + e, + ) + })?; - execute_command(Command::new("systemctl").arg("link").arg(SERVICE_SRC)) - .await - .map_err(Self::Error::CommandFailed)?; + execute_command( + Command::new("launchctl") + .arg("load") + .arg(DARWIN_NIX_DAEMON_DEST), + ) + .await + .map_err(Self::Error::CommandFailed)?; + }, + _ => { + tracing::trace!(src = TMPFILES_SRC, dest = TMPFILES_DEST, "Symlinking"); + tokio::fs::symlink(TMPFILES_SRC, TMPFILES_DEST) + .await + .map_err(|e| { + Self::Error::Symlink( + PathBuf::from(TMPFILES_SRC), + PathBuf::from(TMPFILES_DEST), + e, + ) + })?; - execute_command(Command::new("systemctl").arg("link").arg(SOCKET_SRC)) - .await - .map_err(Self::Error::CommandFailed)?; + execute_command( + Command::new("systemd-tmpfiles") + .arg("--create") + .arg("--prefix=/nix/var/nix"), + ) + .await + .map_err(Self::Error::CommandFailed)?; - execute_command(Command::new("systemctl").arg("daemon-reload")) - .await - .map_err(Self::Error::CommandFailed)?; + execute_command(Command::new("systemctl").arg("link").arg(SERVICE_SRC)) + .await + .map_err(Self::Error::CommandFailed)?; + + execute_command(Command::new("systemctl").arg("link").arg(SOCKET_SRC)) + .await + .map_err(Self::Error::CommandFailed)?; + + execute_command(Command::new("systemctl").arg("daemon-reload")) + .await + .map_err(Self::Error::CommandFailed)?; + }, + }; tracing::trace!("Configured nix daemon service"); *action_state = ActionState::Completed; @@ -176,6 +224,14 @@ pub enum ConfigureNixDaemonServiceError { #[serde(serialize_with = "crate::serialize_error_to_display")] std::io::Error, ), + #[error("Copying file `{0}` to `{1}`")] + Copy( + std::path::PathBuf, + std::path::PathBuf, + #[source] + #[serde(serialize_with = "crate::serialize_error_to_display")] + std::io::Error, + ), #[error("No supported init system found")] InitNotSupported, } diff --git a/src/actions/base/create_directory.rs b/src/actions/base/create_directory.rs index 575822e..0abdb8f 100644 --- a/src/actions/base/create_directory.rs +++ b/src/actions/base/create_directory.rs @@ -10,9 +10,9 @@ use crate::actions::{Action, ActionDescription, ActionState, Actionable}; #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] pub struct CreateDirectory { path: PathBuf, - user: String, - group: String, - mode: u32, + user: Option, + group: Option, + mode: Option, action_state: ActionState, force_prune_on_revert: bool, } @@ -21,12 +21,15 @@ impl CreateDirectory { #[tracing::instrument(skip_all)] pub async fn plan( path: impl AsRef, - user: String, - group: String, - mode: u32, + user: impl Into>, + group: impl Into>, + mode: impl Into>, force_prune_on_revert: bool, ) -> Result { let path = path.as_ref(); + let user = user.into(); + let group = group.into(); + let mode = mode.into(); let action_state = if path.exists() { let metadata = tokio::fs::metadata(path) @@ -77,10 +80,7 @@ impl Actionable for CreateDirectory { } else { vec![ActionDescription::new( format!("Create the directory `{}`", path.display()), - vec![format!( - "Creating directory `{}` owned by `{user}:{group}` with mode `{mode:#o}`", - path.display() - )], + vec![], )] } } @@ -89,7 +89,7 @@ impl Actionable for CreateDirectory { path = %self.path.display(), user = self.user, group = self.group, - mode = format!("{:#o}", self.mode), + mode = self.mode.map(|v| tracing::field::display(format!("{:#o}", v))), ))] async fn execute(&mut self) -> Result<(), Self::Error> { let Self { @@ -106,23 +106,37 @@ impl Actionable for CreateDirectory { } tracing::debug!("Creating directory"); - let gid = Group::from_name(group.as_str()) - .map_err(|e| Self::Error::GroupId(group.clone(), e))? - .ok_or(Self::Error::NoGroup(group.clone()))? - .gid; - let uid = User::from_name(user.as_str()) - .map_err(|e| Self::Error::UserId(user.clone(), e))? - .ok_or(Self::Error::NoUser(user.clone()))? - .uid; + let gid = if let Some(group) = group { + Some( + Group::from_name(group.as_str()) + .map_err(|e| Self::Error::GroupId(group.clone(), e))? + .ok_or(Self::Error::NoGroup(group.clone()))? + .gid, + ) + } else { + None + }; + let uid = if let Some(user) = user { + Some( + User::from_name(user.as_str()) + .map_err(|e| Self::Error::UserId(user.clone(), e))? + .ok_or(Self::Error::NoUser(user.clone()))? + .uid, + ) + } else { + None + }; create_dir(path.clone()) .await .map_err(|e| Self::Error::Creating(path.clone(), e))?; - chown(path, Some(uid), Some(gid)).map_err(|e| Self::Error::Chown(path.clone(), e))?; + chown(path, uid, gid).map_err(|e| Self::Error::Chown(path.clone(), e))?; - tokio::fs::set_permissions(&path, PermissionsExt::from_mode(*mode)) - .await - .map_err(|e| Self::Error::SetPermissions(*mode, path.to_owned(), e))?; + if let Some(mode) = mode { + tokio::fs::set_permissions(&path, PermissionsExt::from_mode(*mode)) + .await + .map_err(|e| Self::Error::SetPermissions(*mode, path.to_owned(), e))?; + } tracing::trace!("Created directory"); *action_state = ActionState::Completed; @@ -160,7 +174,7 @@ impl Actionable for CreateDirectory { path = %self.path.display(), user = self.user, group = self.group, - mode = format!("{:#o}", self.mode), + mode = self.mode.map(|v| tracing::field::display(format!("{:#o}", v))), ))] async fn revert(&mut self) -> Result<(), Self::Error> { let Self { diff --git a/src/actions/base/create_file.rs b/src/actions/base/create_file.rs index 98e5d95..0d11a96 100644 --- a/src/actions/base/create_file.rs +++ b/src/actions/base/create_file.rs @@ -12,10 +12,10 @@ use crate::actions::{ActionDescription, Actionable}; #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] pub struct CreateFile { - path: PathBuf, - user: String, - group: String, - mode: u32, + pub(crate) path: PathBuf, + user: Option, + group: Option, + mode: Option, buf: String, force: bool, action_state: ActionState, @@ -25,9 +25,9 @@ impl CreateFile { #[tracing::instrument(skip_all)] pub async fn plan( path: impl AsRef, - user: String, - group: String, - mode: u32, + user: impl Into>, + group: impl Into>, + mode: impl Into>, buf: String, force: bool, ) -> Result { @@ -39,9 +39,9 @@ impl CreateFile { Ok(Self { path, - user, - group, - mode, + user: user.into(), + group: group.into(), + mode: mode.into(), buf, force, action_state: ActionState::Uncompleted, @@ -68,9 +68,7 @@ impl Actionable for CreateFile { } else { vec![ActionDescription::new( format!("Create or overwrite file `{}`", path.display()), - vec![format!( - "Create or overwrite `{}` owned by `{user}:{group}` with mode `{mode:#o}` with `{buf}`", path.display() - )], + vec![], )] } } @@ -79,7 +77,7 @@ impl Actionable for CreateFile { path = %self.path.display(), user = self.user, group = self.group, - mode = format!("{:#o}", self.mode), + mode = self.mode.map(|v| tracing::field::display(format!("{:#o}", v))), ))] async fn execute(&mut self) -> Result<(), Self::Error> { let Self { @@ -97,11 +95,14 @@ impl Actionable for CreateFile { } tracing::debug!("Creating file"); - let mut file = OpenOptions::new() - .create_new(true) - .mode(*mode) - .write(true) - .read(true) + let mut options = OpenOptions::new(); + options.create_new(true).write(true).read(true); + + if let Some(mode) = mode { + options.mode(*mode); + } + + let mut file = options .open(&path) .await .map_err(|e| Self::Error::OpenFile(path.to_owned(), e))?; @@ -110,16 +111,27 @@ impl Actionable for CreateFile { .await .map_err(|e| Self::Error::WriteFile(path.to_owned(), e))?; - let gid = Group::from_name(group.as_str()) - .map_err(|e| Self::Error::GroupId(group.clone(), e))? - .ok_or(Self::Error::NoGroup(group.clone()))? - .gid; - let uid = User::from_name(user.as_str()) - .map_err(|e| Self::Error::UserId(user.clone(), e))? - .ok_or(Self::Error::NoUser(user.clone()))? - .uid; - - chown(path, Some(uid), Some(gid)).map_err(|e| Self::Error::Chown(path.clone(), e))?; + let gid = if let Some(group) = group { + Some( + Group::from_name(group.as_str()) + .map_err(|e| Self::Error::GroupId(group.clone(), e))? + .ok_or(Self::Error::NoGroup(group.clone()))? + .gid, + ) + } else { + None + }; + let uid = if let Some(user) = user { + Some( + User::from_name(user.as_str()) + .map_err(|e| Self::Error::UserId(user.clone(), e))? + .ok_or(Self::Error::NoUser(user.clone()))? + .uid, + ) + } else { + None + }; + chown(path, uid, gid).map_err(|e| Self::Error::Chown(path.clone(), e))?; tracing::trace!("Created file"); *action_state = ActionState::Completed; @@ -150,7 +162,7 @@ impl Actionable for CreateFile { path = %self.path.display(), user = self.user, group = self.group, - mode = format!("{:#o}", self.mode), + mode = self.mode.map(|v| tracing::field::display(format!("{:#o}", v))), ))] async fn revert(&mut self) -> Result<(), Self::Error> { let Self { diff --git a/src/actions/base/create_or_append_file.rs b/src/actions/base/create_or_append_file.rs index 471206c..fa225bb 100644 --- a/src/actions/base/create_or_append_file.rs +++ b/src/actions/base/create_or_append_file.rs @@ -17,9 +17,9 @@ use crate::actions::{ActionDescription, Actionable}; #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] pub struct CreateOrAppendFile { path: PathBuf, - user: String, - group: String, - mode: u32, + user: Option, + group: Option, + mode: Option, buf: String, action_state: ActionState, } @@ -28,18 +28,18 @@ impl CreateOrAppendFile { #[tracing::instrument(skip_all)] pub async fn plan( path: impl AsRef, - user: String, - group: String, - mode: u32, + user: impl Into>, + group: impl Into>, + mode: impl Into>, buf: String, ) -> Result { let path = path.as_ref().to_path_buf(); Ok(Self { path, - user, - group, - mode, + user: user.into(), + group: group.into(), + mode: mode.into(), buf, action_state: ActionState::Uncompleted, }) @@ -64,9 +64,7 @@ impl Actionable for CreateOrAppendFile { } else { vec![ActionDescription::new( format!("Create or append file `{}`", path.display()), - vec![format!( - "Create or append `{}` owned by `{user}:{group}` with mode `{mode:#o}` with `{buf}`", path.display() - )], + vec![], )] } } @@ -75,7 +73,7 @@ impl Actionable for CreateOrAppendFile { path = %self.path.display(), user = self.user, group = self.group, - mode = format!("{:#o}", self.mode), + mode = self.mode.map(|v| tracing::field::display(format!("{:#o}", v))), ))] async fn execute(&mut self) -> Result<(), Self::Error> { let Self { @@ -108,20 +106,34 @@ impl Actionable for CreateOrAppendFile { .await .map_err(|e| Self::Error::WriteFile(path.to_owned(), e))?; - let gid = Group::from_name(group.as_str()) - .map_err(|e| Self::Error::GroupId(group.clone(), e))? - .ok_or(Self::Error::NoGroup(group.clone()))? - .gid; - let uid = User::from_name(user.as_str()) - .map_err(|e| Self::Error::UserId(user.clone(), e))? - .ok_or(Self::Error::NoUser(user.clone()))? - .uid; + let gid = if let Some(group) = group { + Some( + Group::from_name(group.as_str()) + .map_err(|e| Self::Error::GroupId(group.clone(), e))? + .ok_or(Self::Error::NoGroup(group.clone()))? + .gid, + ) + } else { + None + }; + let uid = if let Some(user) = user { + Some( + User::from_name(user.as_str()) + .map_err(|e| Self::Error::UserId(user.clone(), e))? + .ok_or(Self::Error::NoUser(user.clone()))? + .uid, + ) + } else { + None + }; - tokio::fs::set_permissions(&path, PermissionsExt::from_mode(*mode)) - .await - .map_err(|e| Self::Error::SetPermissions(*mode, path.to_owned(), e))?; + if let Some(mode) = mode { + tokio::fs::set_permissions(&path, PermissionsExt::from_mode(*mode)) + .await + .map_err(|e| Self::Error::SetPermissions(*mode, path.to_owned(), e))?; + } - chown(path, Some(uid), Some(gid)).map_err(|e| Self::Error::Chown(path.clone(), e))?; + chown(path, uid, gid).map_err(|e| Self::Error::Chown(path.clone(), e))?; tracing::trace!("Created or appended fragment to file"); *action_state = ActionState::Completed; @@ -154,7 +166,7 @@ impl Actionable for CreateOrAppendFile { path = %self.path.display(), user = self.user, group = self.group, - mode = format!("{:#o}", self.mode), + mode = self.mode.map(|v| tracing::field::display(format!("{:#o}", v))), ))] async fn revert(&mut self) -> Result<(), Self::Error> { let Self { diff --git a/src/actions/base/create_user.rs b/src/actions/base/create_user.rs index cb52909..1ebefaf 100644 --- a/src/actions/base/create_user.rs +++ b/src/actions/base/create_user.rs @@ -68,7 +68,7 @@ impl Actionable for CreateUser { tracing::debug!("Creating user"); use target_lexicon::OperatingSystem; - match target_lexicon::OperatingSystem::host() { + match OperatingSystem::host() { OperatingSystem::MacOSX { major: _, minor: _, diff --git a/src/actions/base/darwin/kickstart_launchctl_service.rs b/src/actions/base/darwin/kickstart_launchctl_service.rs new file mode 100644 index 0000000..c4e30ac --- /dev/null +++ b/src/actions/base/darwin/kickstart_launchctl_service.rs @@ -0,0 +1,115 @@ +use serde::Serialize; +use tokio::process::Command; + +use crate::execute_command; + +use crate::actions::{Action, ActionDescription, ActionState, Actionable}; + +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] +pub struct KickstartLaunchctlService { + unit: String, + action_state: ActionState, +} + +impl KickstartLaunchctlService { + #[tracing::instrument(skip_all)] + pub async fn plan(unit: String) -> Result { + Ok(Self { + unit, + action_state: ActionState::Uncompleted, + }) + } +} + +#[async_trait::async_trait] +impl Actionable for KickstartLaunchctlService { + type Error = KickstartLaunchctlServiceError; + + fn describe_execute(&self) -> Vec { + let Self { unit, action_state } = self; + if *action_state == ActionState::Completed { + vec![] + } else { + vec![ActionDescription::new( + format!("Kickstart the launchctl unit `{unit}`"), + vec![ + "The `nix` command line tool communicates with a running Nix daemon managed by your init system".to_string() + ] + )] + } + } + + #[tracing::instrument(skip_all, fields( + unit = %self.unit, + ))] + async fn execute(&mut self) -> Result<(), Self::Error> { + let Self { unit, action_state } = self; + if *action_state == ActionState::Completed { + tracing::trace!("Already completed: Kickstarting launchctl unit"); + return Ok(()); + } + tracing::debug!("Kickstarting launchctl unit"); + + execute_command( + Command::new("launchctl") + .arg("kickstart") + .arg("-k") + .arg(unit), + ) + .await + .map_err(KickstartLaunchctlServiceError::Command)?; + + tracing::trace!("Kickstarted launchctl unit"); + *action_state = ActionState::Completed; + Ok(()) + } + + fn describe_revert(&self) -> Vec { + if self.action_state == ActionState::Uncompleted { + vec![] + } else { + vec![ActionDescription::new( + "Kick".to_string(), + vec![ + "The `nix` command line tool communicates with a running Nix daemon managed by your init system".to_string() + ] + )] + } + } + + #[tracing::instrument(skip_all, fields( + unit = %self.unit, + ))] + async fn revert(&mut self) -> Result<(), Self::Error> { + let Self { unit, action_state } = self; + if *action_state == ActionState::Uncompleted { + tracing::trace!("Already reverted: Stopping launchctl unit"); + return Ok(()); + } + tracing::debug!("Stopping launchctl unit"); + + execute_command(Command::new("launchctl").arg("stop").arg(unit)) + .await + .map_err(KickstartLaunchctlServiceError::Command)?; + + tracing::trace!("Stopped launchctl unit"); + *action_state = ActionState::Completed; + Ok(()) + } +} + +impl From for Action { + fn from(v: KickstartLaunchctlService) -> Self { + Action::DarwinKickStartLaunchctlService(v) + } +} + +#[derive(Debug, thiserror::Error, Serialize)] +pub enum KickstartLaunchctlServiceError { + #[error("Failed to execute command")] + Command( + #[source] + #[serde(serialize_with = "crate::serialize_error_to_display")] + std::io::Error, + ), +} diff --git a/src/actions/base/darwin/mod.rs b/src/actions/base/darwin/mod.rs index 17a5dfc..314e4a5 100644 --- a/src/actions/base/darwin/mod.rs +++ b/src/actions/base/darwin/mod.rs @@ -3,6 +3,7 @@ mod create_synthetic_objects; mod create_volume; mod enable_ownership; mod encrypt_volume; +mod kickstart_launchctl_service; mod unmount_volume; pub use bootstrap_volume::{BootstrapVolume, BootstrapVolumeError}; @@ -10,4 +11,5 @@ pub use create_synthetic_objects::{CreateSyntheticObjects, CreateSyntheticObject pub use create_volume::{CreateVolume, CreateVolumeError}; pub use enable_ownership::{EnableOwnership, EnableOwnershipError}; pub use encrypt_volume::{EncryptVolume, EncryptVolumeError}; +pub use kickstart_launchctl_service::{KickstartLaunchctlService, KickstartLaunchctlServiceError}; pub use unmount_volume::{UnmountVolume, UnmountVolumeError}; diff --git a/src/actions/base/darwin/unmount_volume.rs b/src/actions/base/darwin/unmount_volume.rs index 1e18868..91b8cef 100644 --- a/src/actions/base/darwin/unmount_volume.rs +++ b/src/actions/base/darwin/unmount_volume.rs @@ -60,7 +60,7 @@ impl Actionable for UnmountVolume { tracing::debug!("Unmounting volume"); execute_command( - Command::new(" /usr/sbin/diskutil") + Command::new("/usr/sbin/diskutil") .args(["unmount", "force"]) .arg(name), ) diff --git a/src/actions/meta/configure_shell_profile.rs b/src/actions/meta/configure_shell_profile.rs index 942f0d4..74e0627 100644 --- a/src/actions/meta/configure_shell_profile.rs +++ b/src/actions/meta/configure_shell_profile.rs @@ -41,10 +41,8 @@ impl ConfigureShellProfile { # End Nix\n \n", ); - create_or_append_files.push( - CreateOrAppendFile::plan(path, "root".to_string(), "root".to_string(), 0o0644, buf) - .await?, - ); + create_or_append_files + .push(CreateOrAppendFile::plan(path, None, None, 0o0644, buf).await?); } Ok(Self { diff --git a/src/actions/meta/create_nix_tree.rs b/src/actions/meta/create_nix_tree.rs index 82d9d0c..bbae380 100644 --- a/src/actions/meta/create_nix_tree.rs +++ b/src/actions/meta/create_nix_tree.rs @@ -31,9 +31,7 @@ impl CreateNixTree { let mut create_directories = Vec::default(); for path in PATHS { // We use `create_dir` over `create_dir_all` to ensure we always set permissions right - create_directories.push( - CreateDirectory::plan(path, "root".into(), "root".into(), 0o0755, false).await?, - ) + create_directories.push(CreateDirectory::plan(path, None, None, 0o0755, false).await?) } Ok(Self { diff --git a/src/actions/meta/create_systemd_sysext.rs b/src/actions/meta/create_systemd_sysext.rs index 93d09a7..44c47b8 100644 --- a/src/actions/meta/create_systemd_sysext.rs +++ b/src/actions/meta/create_systemd_sysext.rs @@ -26,19 +26,11 @@ impl CreateSystemdSysext { pub async fn plan(destination: impl AsRef) -> Result { let destination = destination.as_ref(); - let mut create_directories = vec![ - CreateDirectory::plan(destination, "root".into(), "root".into(), 0o0755, true).await?, - ]; + let mut create_directories = + vec![CreateDirectory::plan(destination, None, None, 0o0755, true).await?]; for path in PATHS { create_directories.push( - CreateDirectory::plan( - destination.join(path), - "root".into(), - "root".into(), - 0o0755, - false, - ) - .await?, + CreateDirectory::plan(destination.join(path), None, None, 0o0755, false).await?, ) } @@ -57,8 +49,8 @@ impl CreateSystemdSysext { format!("SYSEXT_LEVEL=1.0\nID=steamos\nVERSION_ID={version_id}"); let create_extension_release = CreateFile::plan( destination.join("usr/lib/extension-release.d/extension-release.nix"), - "root".into(), - "root".into(), + None, + None, 0o0755, extension_release_buf, false, @@ -77,8 +69,8 @@ impl CreateSystemdSysext { ); let create_bind_mount_unit = CreateFile::plan( destination.join("usr/lib/systemd/system/nix.mount"), - "root".into(), - "root".into(), + None, + None, 0o0755, create_bind_mount_buf, false, diff --git a/src/actions/meta/darwin/create_apfs_volume.rs b/src/actions/meta/darwin/create_apfs_volume.rs index 4c4f546..343d6ef 100644 --- a/src/actions/meta/darwin/create_apfs_volume.rs +++ b/src/actions/meta/darwin/create_apfs_volume.rs @@ -39,14 +39,9 @@ impl CreateApfsVolume { encrypt: Option, ) -> Result { let disk = disk.as_ref(); - let create_or_append_synthetic_conf = CreateOrAppendFile::plan( - "/etc/synthetic.conf", - "root".into(), - "wheel".into(), - 0o0655, - "nix".into(), - ) - .await?; + let create_or_append_synthetic_conf = + CreateOrAppendFile::plan("/etc/synthetic.conf", None, None, 0o0655, "nix".into()) + .await?; let create_synthetic_objects = CreateSyntheticObjects::plan().await?; @@ -56,10 +51,10 @@ impl CreateApfsVolume { let create_or_append_fstab = CreateOrAppendFile::plan( "/etc/fstab", - "root".into(), - "root".into(), + None, + None, 0o0655, - "NAME={name} /nix apfs rw,noauto,nobrowse,suid,owners".into(), + format!("NAME=\"{name}\" /nix apfs rw,noauto,nobrowse,suid,owners"), ) .await?; @@ -138,7 +133,7 @@ impl Actionable for CreateApfsVolume { create_or_append_synthetic_conf.execute().await?; create_synthetic_objects.execute().await?; - unmount_volume.execute().await?; + unmount_volume.execute().await.ok(); // We actually expect this may fail. create_volume.execute().await?; create_or_append_fstab.execute().await?; if let Some(encrypt_volume) = encrypt_volume { diff --git a/src/actions/meta/mod.rs b/src/actions/meta/mod.rs index cfda484..e153b8e 100644 --- a/src/actions/meta/mod.rs +++ b/src/actions/meta/mod.rs @@ -9,7 +9,6 @@ pub mod darwin; mod place_channel_configuration; mod place_nix_configuration; mod provision_nix; -mod start_nix_daemon; pub use configure_nix::{ConfigureNix, ConfigureNixError}; pub use configure_shell_profile::{ConfigureShellProfile, ConfigureShellProfileError}; @@ -19,4 +18,3 @@ pub use create_users_and_group::{CreateUsersAndGroup, CreateUsersAndGroupError}; pub use place_channel_configuration::{PlaceChannelConfiguration, PlaceChannelConfigurationError}; pub use place_nix_configuration::{PlaceNixConfiguration, PlaceNixConfigurationError}; pub use provision_nix::{ProvisionNix, ProvisionNixError}; -pub use start_nix_daemon::{StartNixDaemon, StartNixDaemonError}; diff --git a/src/actions/meta/place_channel_configuration.rs b/src/actions/meta/place_channel_configuration.rs index 20c4f3f..0e60a15 100644 --- a/src/actions/meta/place_channel_configuration.rs +++ b/src/actions/meta/place_channel_configuration.rs @@ -5,8 +5,6 @@ use crate::actions::{Action, ActionDescription, ActionState, Actionable}; use crate::actions::base::{CreateFile, CreateFileError}; -const NIX_CHANNELS_PATH: &str = "/root/.nix-channels"; - #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] pub struct PlaceChannelConfiguration { channels: Vec<(String, Url)>, @@ -26,9 +24,11 @@ impl PlaceChannelConfiguration { .collect::>() .join("\n"); let create_file = CreateFile::plan( - NIX_CHANNELS_PATH, - "root".into(), - "root".into(), + dirs::home_dir() + .ok_or(PlaceChannelConfigurationError::NoRootHome)? + .join("/.nix-channels"), + None, + None, 0o0664, buf, force, @@ -49,17 +49,18 @@ impl Actionable for PlaceChannelConfiguration { fn describe_execute(&self) -> Vec { let Self { channels: _, - create_file: _, + create_file, action_state: _, } = self; if self.action_state == ActionState::Completed { vec![] } else { vec![ActionDescription::new( - format!("Place channel configuration at `{NIX_CHANNELS_PATH}`"), - vec![format!( - "Place channel configuration at `{NIX_CHANNELS_PATH}`" - )], + format!( + "Place channel configuration at `{}`", + create_file.path.display() + ), + vec![], )] } } @@ -90,17 +91,18 @@ impl Actionable for PlaceChannelConfiguration { fn describe_revert(&self) -> Vec { let Self { channels: _, - create_file: _, + create_file, action_state: _, } = self; if self.action_state == ActionState::Uncompleted { vec![] } else { vec![ActionDescription::new( - format!("Remove channel configuration at `{NIX_CHANNELS_PATH}`"), - vec![format!( - "Remove channel configuration at `{NIX_CHANNELS_PATH}`" - )], + format!( + "Remove channel configuration at `{}`", + create_file.path.display() + ), + vec![], )] } } @@ -143,4 +145,6 @@ pub enum PlaceChannelConfigurationError { #[from] CreateFileError, ), + #[error("No root home found to place channel configuration in")] + NoRootHome, } diff --git a/src/actions/meta/place_nix_configuration.rs b/src/actions/meta/place_nix_configuration.rs index eac1686..80402a1 100644 --- a/src/actions/meta/place_nix_configuration.rs +++ b/src/actions/meta/place_nix_configuration.rs @@ -33,10 +33,8 @@ impl PlaceNixConfiguration { extra_conf = extra_conf.unwrap_or_else(|| "".into()), ); let create_directory = - CreateDirectory::plan(NIX_CONF_FOLDER, "root".into(), "root".into(), 0o0755, force) - .await?; - let create_file = - CreateFile::plan(NIX_CONF, "root".into(), "root".into(), 0o0664, buf, force).await?; + CreateDirectory::plan(NIX_CONF_FOLDER, None, None, 0o0755, force).await?; + let create_file = CreateFile::plan(NIX_CONF, None, None, 0o0664, buf, force).await?; Ok(Self { create_directory, create_file, diff --git a/src/actions/meta/provision_nix.rs b/src/actions/meta/provision_nix.rs index 8cf074b..034a887 100644 --- a/src/actions/meta/provision_nix.rs +++ b/src/actions/meta/provision_nix.rs @@ -26,8 +26,7 @@ pub struct ProvisionNix { impl ProvisionNix { #[tracing::instrument(skip_all)] pub async fn plan(settings: InstallSettings) -> Result { - let create_nix_dir = - CreateDirectory::plan("/nix", "root".into(), "root".into(), 0o0755, true).await?; + let create_nix_dir = CreateDirectory::plan("/nix", None, None, 0o0755, true).await?; let fetch_nix = FetchNix::plan( settings.nix_package_url.clone(), diff --git a/src/actions/meta/start_nix_daemon.rs b/src/actions/meta/start_nix_daemon.rs deleted file mode 100644 index ab03441..0000000 --- a/src/actions/meta/start_nix_daemon.rs +++ /dev/null @@ -1,101 +0,0 @@ -use serde::Serialize; - -use crate::actions::base::{StartSystemdUnit, StartSystemdUnitError}; - -use crate::actions::{Action, ActionDescription, ActionState, Actionable}; - -/// This is mostly indirection for supporting non-systemd -#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] -pub struct StartNixDaemon { - start_systemd_socket: StartSystemdUnit, - action_state: ActionState, -} - -impl StartNixDaemon { - #[tracing::instrument(skip_all)] - pub async fn plan() -> Result { - let start_systemd_socket = StartSystemdUnit::plan("nix-daemon.socket".into()).await?; - Ok(Self { - start_systemd_socket, - action_state: ActionState::Uncompleted, - }) - } -} - -#[async_trait::async_trait] -impl Actionable for StartNixDaemon { - type Error = StartNixDaemonError; - - fn describe_execute(&self) -> Vec { - if self.action_state == ActionState::Completed { - vec![] - } else { - self.start_systemd_socket.describe_execute() - } - } - - #[tracing::instrument(skip_all)] - async fn execute(&mut self) -> Result<(), Self::Error> { - let Self { - start_systemd_socket, - action_state, - } = self; - if *action_state == ActionState::Completed { - tracing::trace!("Already completed: Starting the nix daemon"); - return Ok(()); - } - *action_state = ActionState::Progress; - tracing::debug!("Starting the nix daemon"); - - start_systemd_socket.execute().await?; - - tracing::trace!("Started the nix daemon"); - *action_state = ActionState::Completed; - Ok(()) - } - - fn describe_revert(&self) -> Vec { - if self.action_state == ActionState::Uncompleted { - vec![] - } else { - self.start_systemd_socket.describe_revert() - } - } - - #[tracing::instrument(skip_all)] - async fn revert(&mut self) -> Result<(), Self::Error> { - let Self { - start_systemd_socket, - action_state, - .. - } = self; - if *action_state == ActionState::Uncompleted { - tracing::trace!("Already reverted: Stop the nix daemon"); - return Ok(()); - } - *action_state = ActionState::Progress; - tracing::debug!("Stop the nix daemon"); - - start_systemd_socket.revert().await?; - - tracing::trace!("Stopped the nix daemon"); - *action_state = ActionState::Uncompleted; - Ok(()) - } -} - -impl From for Action { - fn from(v: StartNixDaemon) -> Self { - Action::StartNixDaemon(v) - } -} - -#[derive(Debug, thiserror::Error, Serialize)] -pub enum StartNixDaemonError { - #[error("Starting systemd unit")] - StartSystemdUnit( - #[source] - #[from] - StartSystemdUnitError, - ), -} diff --git a/src/actions/mod.rs b/src/actions/mod.rs index aa79adb..f117f5d 100644 --- a/src/actions/mod.rs +++ b/src/actions/mod.rs @@ -15,7 +15,7 @@ use meta::{ CreateNixTree, CreateNixTreeError, CreateSystemdSysext, CreateSystemdSysextError, CreateUsersAndGroup, CreateUsersAndGroupError, PlaceChannelConfiguration, PlaceChannelConfigurationError, PlaceNixConfiguration, PlaceNixConfigurationError, - ProvisionNix, ProvisionNixError, StartNixDaemon, StartNixDaemonError, + ProvisionNix, ProvisionNixError, }; use serde::{de::DeserializeOwned, Deserialize, Serialize}; @@ -62,6 +62,7 @@ pub enum Action { DarwinCreateVolume(base::darwin::CreateVolume), DarwinEnableOwnership(base::darwin::EnableOwnership), DarwinEncryptVolume(base::darwin::EncryptVolume), + DarwinKickStartLaunchctlService(base::darwin::KickstartLaunchctlService), DarwinUnmountVolume(base::darwin::UnmountVolume), ConfigureNix(ConfigureNix), ConfigureNixDaemonService(ConfigureNixDaemonService), @@ -81,7 +82,6 @@ pub enum Action { PlaceNixConfiguration(PlaceNixConfiguration), ProvisionNix(ProvisionNix), SetupDefaultProfile(SetupDefaultProfile), - StartNixDaemon(StartNixDaemon), StartSystemdUnit(StartSystemdUnit), SystemdSysextMerge(SystemdSysextMerge), } @@ -99,6 +99,8 @@ pub enum ActionError { #[error(transparent)] DarwinEncryptVolume(#[from] base::darwin::EncryptVolumeError), #[error(transparent)] + DarwinKickStartLaunchctlService(#[from] base::darwin::KickstartLaunchctlServiceError), + #[error(transparent)] DarwinUnmountVolume(#[from] base::darwin::UnmountVolumeError), #[error("Attempted to revert an unexecuted action")] NotExecuted(Action), @@ -141,8 +143,6 @@ pub enum ActionError { #[error(transparent)] SetupDefaultProfile(#[from] SetupDefaultProfileError), #[error(transparent)] - StartNixDaemon(#[from] StartNixDaemonError), - #[error(transparent)] StartSystemdUnit(#[from] StartSystemdUnitError), #[error(transparent)] SystemdSysExtMerge(#[from] SystemdSysextMergeError), @@ -179,7 +179,7 @@ impl Actionable for Action { Action::PlaceNixConfiguration(i) => i.describe_execute(), Action::ProvisionNix(i) => i.describe_execute(), Action::SetupDefaultProfile(i) => i.describe_execute(), - Action::StartNixDaemon(i) => i.describe_execute(), + Action::DarwinKickStartLaunchctlService(i) => i.describe_execute(), Action::StartSystemdUnit(i) => i.describe_execute(), Action::SystemdSysextMerge(i) => i.describe_execute(), } @@ -211,7 +211,7 @@ impl Actionable for Action { Action::PlaceNixConfiguration(i) => i.execute().await?, Action::ProvisionNix(i) => i.execute().await?, Action::SetupDefaultProfile(i) => i.execute().await?, - Action::StartNixDaemon(i) => i.execute().await?, + Action::DarwinKickStartLaunchctlService(i) => i.execute().await?, Action::StartSystemdUnit(i) => i.execute().await?, Action::SystemdSysextMerge(i) => i.execute().await?, }; @@ -244,7 +244,7 @@ impl Actionable for Action { Action::PlaceNixConfiguration(i) => i.describe_revert(), Action::ProvisionNix(i) => i.describe_revert(), Action::SetupDefaultProfile(i) => i.describe_revert(), - Action::StartNixDaemon(i) => i.describe_revert(), + Action::DarwinKickStartLaunchctlService(i) => i.describe_revert(), Action::StartSystemdUnit(i) => i.describe_revert(), Action::SystemdSysextMerge(i) => i.describe_revert(), } @@ -276,7 +276,7 @@ impl Actionable for Action { Action::PlaceNixConfiguration(i) => i.revert().await?, Action::ProvisionNix(i) => i.revert().await?, Action::SetupDefaultProfile(i) => i.revert().await?, - Action::StartNixDaemon(i) => i.revert().await?, + Action::DarwinKickStartLaunchctlService(i) => i.revert().await?, Action::StartSystemdUnit(i) => i.revert().await?, Action::SystemdSysextMerge(i) => i.revert().await?, } diff --git a/src/plan.rs b/src/plan.rs index 31807be..6a0e898 100644 --- a/src/plan.rs +++ b/src/plan.rs @@ -85,7 +85,9 @@ impl InstallPlan { // The plan itself represents the concept of the sequence of stages. for action in actions { if let Err(err) = action.execute().await { - write_receipt(self.clone()).await?; + if let Err(err) = write_receipt(self.clone()).await { + tracing::error!("Error saving receipt: {:?}", err); + } return Err(ActionError::from(err).into()); } } @@ -157,7 +159,9 @@ impl InstallPlan { // The plan itself represents the concept of the sequence of stages. for action in actions { if let Err(err) = action.revert().await { - write_receipt(self.clone()).await?; + if let Err(err) = write_receipt(self.clone()).await { + tracing::error!("Error saving receipt: {:?}", err); + } return Err(ActionError::from(err).into()); } } diff --git a/src/planner/darwin/multi_user.rs b/src/planner/darwin/multi_user.rs index 9af3892..3fcfca9 100644 --- a/src/planner/darwin/multi_user.rs +++ b/src/planner/darwin/multi_user.rs @@ -4,7 +4,8 @@ use tokio::process::Command; use crate::{ actions::{ - meta::{darwin::CreateApfsVolume, ConfigureNix, ProvisionNix, StartNixDaemon}, + base::darwin::KickstartLaunchctlService, + meta::{darwin::CreateApfsVolume, ConfigureNix, ProvisionNix}, Action, ActionError, }, execute_command, @@ -57,7 +58,7 @@ impl Plannable for DarwinMultiUser { .await .map(Action::from) .map_err(ActionError::from)?, - StartNixDaemon::plan() + KickstartLaunchctlService::plan("system/org.nixos.nix-daemon".into()) .await .map(Action::from) .map_err(ActionError::from)?, diff --git a/src/planner/linux/multi_user.rs b/src/planner/linux/multi_user.rs index c3953ba..c2d29cb 100644 --- a/src/planner/linux/multi_user.rs +++ b/src/planner/linux/multi_user.rs @@ -1,6 +1,7 @@ use crate::{ actions::{ - meta::{ConfigureNix, ProvisionNix, StartNixDaemon}, + base::StartSystemdUnit, + meta::{ConfigureNix, ProvisionNix}, Action, ActionError, }, planner::{Plannable, PlannerError}, @@ -28,7 +29,7 @@ impl Plannable for LinuxMultiUser { .await .map(Action::from) .map_err(ActionError::from)?, - StartNixDaemon::plan() + StartSystemdUnit::plan("nix-daemon.socket".into()) .await .map(Action::from) .map_err(ActionError::from)?, diff --git a/src/planner/specific/steam_deck.rs b/src/planner/specific/steam_deck.rs index 9d6a053..e92ad20 100644 --- a/src/planner/specific/steam_deck.rs +++ b/src/planner/specific/steam_deck.rs @@ -1,6 +1,7 @@ use crate::{ actions::{ - meta::{CreateSystemdSysext, ProvisionNix, StartNixDaemon}, + base::StartSystemdUnit, + meta::{CreateSystemdSysext, ProvisionNix}, Action, ActionError, }, planner::Plannable, @@ -30,7 +31,7 @@ impl Plannable for SteamDeck { .await .map(Action::from) .map_err(ActionError::from)?, - StartNixDaemon::plan() + StartSystemdUnit::plan("nix-daemon.socket".into()) .await .map(Action::from) .map_err(ActionError::from)?,