From c39bf0a51013e80ae1e2ad0b93d404af5bf30242 Mon Sep 17 00:00:00 2001 From: Ana Hobden Date: Mon, 28 Nov 2022 14:57:35 -0800 Subject: [PATCH] Flesh out docs and tidy up public API substantially (#67) * Make plans versioned * Delint * speeeeeeeeling * remove file that was dead * Flesh out docs and improve public API * Speeling * Fixups * Fix doctests * Do a better job with actionstate * Add some more docs * Fix doctest * Make CLI stuff optional * Touchup * Speeling --- Cargo.lock | 69 +---- Cargo.toml | 23 +- flake.nix | 4 +- src/action/base/create_directory.rs | 36 +-- src/action/base/create_file.rs | 26 +- src/action/base/create_group.rs | 46 +-- src/action/base/create_or_append_file.rs | 28 +- src/action/base/create_user.rs | 20 +- .../{fetch_nix.rs => fetch_and_unpack_nix.rs} | 60 ++-- src/action/base/mod.rs | 8 +- src/action/base/move_unpacked_nix.rs | 31 +- src/action/base/setup_default_profile.rs | 28 +- src/action/common/configure_nix.rs | 60 ++-- src/action/common/configure_shell_profile.rs | 26 +- src/action/common/create_nix_tree.rs | 35 +-- ...nd_group.rs => create_users_and_groups.rs} | 83 +++-- src/action/common/mod.rs | 6 +- .../common/place_channel_configuration.rs | 30 +- src/action/common/place_nix_configuration.rs | 30 +- src/action/common/provision_nix.rs | 71 ++--- ...rap_volume.rs => bootstrap_apfs_volume.rs} | 37 +-- src/action/darwin/create_apfs_volume.rs | 288 ++++-------------- src/action/darwin/create_nix_volume.rs | 277 +++++++++++++++++ src/action/darwin/create_synthetic_objects.rs | 21 +- src/action/darwin/create_volume.rs | 136 --------- src/action/darwin/enable_ownership.rs | 26 +- ...crypt_volume.rs => encrypt_apfs_volume.rs} | 38 +-- .../darwin/kickstart_launchctl_service.rs | 29 +- src/action/darwin/mod.rs | 29 +- ...mount_volume.rs => unmount_apfs_volume.rs} | 43 +-- .../configure_nix_daemon_service.rs | 26 +- src/action/linux/create_systemd_sysext.rs | 26 +- src/action/linux/mod.rs | 4 + src/action/linux/start_systemd_unit.rs | 24 +- src/action/linux/systemd_sysext_merge.rs | 19 +- src/action/mod.rs | 259 ++++++++++++---- src/action/stateful.rs | 168 ++++++++++ src/{main.rs => bin/harmonic.rs} | 0 src/channel_value.rs | 6 + src/{ => cli}/interaction.rs | 0 src/cli/mod.rs | 5 + src/cli/subcommand/install.rs | 19 +- src/cli/subcommand/uninstall.rs | 8 +- src/error.rs | 36 ++- src/lib.rs | 80 ++++- src/plan.rs | 56 ++-- src/planner/darwin/mod.rs | 2 + src/planner/darwin/multi.rs | 99 +++--- src/planner/linux/mod.rs | 2 + src/planner/linux/multi.rs | 56 ++-- src/planner/mod.rs | 131 ++++++-- src/planner/specific/steam_deck.rs | 43 ++- src/settings.rs | 168 +++++++--- 53 files changed, 1577 insertions(+), 1304 deletions(-) rename src/action/base/{fetch_nix.rs => fetch_and_unpack_nix.rs} (59%) rename src/action/common/{create_users_and_group.rs => create_users_and_groups.rs} (80%) rename src/action/darwin/{bootstrap_volume.rs => bootstrap_apfs_volume.rs} (78%) create mode 100644 src/action/darwin/create_nix_volume.rs delete mode 100644 src/action/darwin/create_volume.rs rename src/action/darwin/{encrypt_volume.rs => encrypt_apfs_volume.rs} (88%) rename src/action/darwin/{unmount_volume.rs => unmount_apfs_volume.rs} (71%) rename src/action/{base => linux}/configure_nix_daemon_service.rs (95%) create mode 100644 src/action/stateful.rs rename src/{main.rs => bin/harmonic.rs} (100%) rename src/{ => cli}/interaction.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index 94ce175..3ea9271 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -594,12 +594,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "fuchsia-cprng" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" - [[package]] name = "futures" version = "0.1.31" @@ -795,7 +789,7 @@ dependencies = [ "nix", "owo-colors", "plist", - "rand 0.8.5", + "rand", "reqwest", "semver", "serde", @@ -805,7 +799,6 @@ dependencies = [ "sxd-xpath", "tar", "target-lexicon", - "tempdir", "thiserror", "tokio", "tokio-util", @@ -1389,19 +1382,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "rand" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" -dependencies = [ - "fuchsia-cprng", - "libc", - "rand_core 0.3.1", - "rdrand", - "winapi", -] - [[package]] name = "rand" version = "0.8.5" @@ -1410,7 +1390,7 @@ checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha", - "rand_core 0.6.4", + "rand_core", ] [[package]] @@ -1420,24 +1400,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core 0.6.4", + "rand_core", ] -[[package]] -name = "rand_core" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" -dependencies = [ - "rand_core 0.4.2", -] - -[[package]] -name = "rand_core" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" - [[package]] name = "rand_core" version = "0.6.4" @@ -1447,15 +1412,6 @@ dependencies = [ "getrandom", ] -[[package]] -name = "rdrand" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" -dependencies = [ - "rand_core 0.3.1", -] - [[package]] name = "redox_syscall" version = "0.2.16" @@ -1500,15 +1456,6 @@ version = "0.6.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" -[[package]] -name = "remove_dir_all" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" -dependencies = [ - "winapi", -] - [[package]] name = "reqwest" version = "0.11.13" @@ -1849,16 +1796,6 @@ version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9410d0f6853b1d94f0e519fb95df60f29d2c1eff2d921ffdf01a4c8a3b54f12d" -[[package]] -name = "tempdir" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" -dependencies = [ - "rand 0.4.6", - "remove_dir_all", -] - [[package]] name = "termcolor" version = "1.1.3" diff --git a/Cargo.toml b/Cargo.toml index cfafde4..4c28752 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,15 +11,23 @@ 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"] +cli = [ "eyre", "color-eyre", "crossterm", "clap", "tracing-subscriber", "tracing-error", "atty" ] + +[[bin]] +name = "harmonic" +required-features = [ "cli" ] + [dependencies] async-tar = "0.4.2" async-trait = "0.1.57" -atty = "0.2.14" +atty = { version = "0.2.14", optional = true } bytes = "1.2.1" -clap = { version = "4", features = ["derive", "env"] } -color-eyre = "0.6.2" -crossterm = { version = "0.25.0", features = ["event-stream"] } -eyre = "0.6.8" +clap = { version = "4", features = ["derive", "env"], optional = true } +color-eyre = { version = "0.6.2", optional = true } +crossterm = { version = "0.25.0", features = ["event-stream"], optional = true } +eyre = { version = "0.6.8", optional = true } futures = "0.3.24" glob = "0.3.0" nix = { version = "0.25.0", features = ["user", "fs", "process", "term"], default-features = false } @@ -30,13 +38,12 @@ serde_json = "1.0.85" serde_with = "2.0.1" tar = "0.4.38" target-lexicon = "0.12.4" -tempdir = { version = "0.3.7"} thiserror = "1.0.33" tokio = { version = "1.21.0", features = ["time", "io-std", "process", "fs", "signal", "tracing", "rt-multi-thread", "macros", "io-util", "parking_lot" ] } tokio-util = { version = "0.7", features = ["io"] } tracing = { version = "0.1.36", features = [ "valuable" ] } -tracing-error = "0.2.0" -tracing-subscriber = { version = "0.3.15", features = [ "env-filter", "valuable" ] } +tracing-error = { version = "0.2.0", optional = true } +tracing-subscriber = { version = "0.3.15", features = [ "env-filter", "valuable" ], optional = true } url = { version = "2.3.1", features = ["serde"] } valuable = { version = "0.1.0", features = ["derive"] } walkdir = "2.3.2" diff --git a/flake.nix b/flake.nix index 06bbce3..990e01c 100644 --- a/flake.nix +++ b/flake.nix @@ -121,14 +121,16 @@ pkg-config ]; buildInputs = with pkgs; [ - openssl ] ++ lib.optionals (pkgs.stdenv.isDarwin) (with pkgs.darwin.apple_sdk.frameworks; [ SystemConfiguration ]); doCheck = true; + doDoc = true; + doDocFail = true; RUSTFLAGS = "--cfg tracing_unstable --cfg tokio_unstable"; + cargoTestOptions = f: f ++ ["--all"]; override = { preBuild ? "", ... }: { preBuild = preBuild + '' diff --git a/src/action/base/create_directory.rs b/src/action/base/create_directory.rs index dd92dd8..ef5bd03 100644 --- a/src/action/base/create_directory.rs +++ b/src/action/base/create_directory.rs @@ -5,18 +5,23 @@ use nix::unistd::{chown, Group, User}; use tokio::fs::{create_dir, remove_dir_all}; +use crate::action::StatefulAction; use crate::{ action::{Action, ActionDescription, ActionState}, BoxableError, }; +/** Create a directory at the given location, optionally with an owning user, group, and mode. + +If `force_prune_on_revert` is set, the folder will always be deleted on +[`revert`](CreateDirectory::revert). +*/ #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] pub struct CreateDirectory { path: PathBuf, user: Option, group: Option, mode: Option, - action_state: ActionState, force_prune_on_revert: bool, } @@ -28,7 +33,7 @@ impl CreateDirectory { group: impl Into>, mode: impl Into>, force_prune_on_revert: bool, - ) -> Result> { + ) -> Result, Box> { let path = path.as_ref(); let user = user.into(); let group = group.into(); @@ -59,13 +64,15 @@ impl CreateDirectory { ActionState::Uncompleted }; - Ok(Self { - path: path.to_path_buf(), - user, - group, - mode, - force_prune_on_revert, - action_state, + Ok(StatefulAction { + action: Self { + path: path.to_path_buf(), + user, + group, + mode, + force_prune_on_revert, + }, + state: action_state, }) } } @@ -94,7 +101,6 @@ impl Action for CreateDirectory { group, mode, force_prune_on_revert: _, - action_state: _, } = self; let gid = if let Some(group) = group { @@ -141,7 +147,6 @@ impl Action for CreateDirectory { group: _, mode: _, force_prune_on_revert, - action_state: _, } = &self; vec![ActionDescription::new( format!( @@ -170,7 +175,6 @@ impl Action for CreateDirectory { group: _, mode: _, force_prune_on_revert, - action_state: _, } = self; let is_empty = path @@ -189,14 +193,6 @@ impl Action for CreateDirectory { Ok(()) } - - fn action_state(&self) -> ActionState { - self.action_state - } - - fn set_action_state(&mut self, action_state: ActionState) { - self.action_state = action_state; - } } #[derive(Debug, thiserror::Error)] diff --git a/src/action/base/create_file.rs b/src/action/base/create_file.rs index 573d678..328cc11 100644 --- a/src/action/base/create_file.rs +++ b/src/action/base/create_file.rs @@ -7,10 +7,16 @@ use tokio::{ }; use crate::{ - action::{Action, ActionDescription, ActionState}, + action::{Action, ActionDescription, StatefulAction}, BoxableError, }; +/** Create a file at the given location with the provided `buf`, +optionally with an owning user, group, and mode. + +If `force` is set, the file will always be overwritten (and deleted) +regardless of its presence prior to install. + */ #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] pub struct CreateFile { pub(crate) path: PathBuf, @@ -19,7 +25,6 @@ pub struct CreateFile { mode: Option, buf: String, force: bool, - action_state: ActionState, } impl CreateFile { @@ -31,7 +36,7 @@ impl CreateFile { mode: impl Into>, buf: String, force: bool, - ) -> Result> { + ) -> Result, Box> { let path = path.as_ref().to_path_buf(); if path.exists() && !force { @@ -45,8 +50,8 @@ impl CreateFile { mode: mode.into(), buf, force, - action_state: ActionState::Uncompleted, - }) + } + .into()) } } @@ -74,7 +79,6 @@ impl Action for CreateFile { mode, buf, force: _, - action_state: _, } = self; let mut options = OpenOptions::new(); @@ -126,7 +130,6 @@ impl Action for CreateFile { mode: _, buf: _, force: _, - action_state: _, } = &self; vec![ActionDescription::new( @@ -149,7 +152,6 @@ impl Action for CreateFile { mode: _, buf: _, force: _, - action_state: _, } = self; remove_file(&path) @@ -158,14 +160,6 @@ impl Action for CreateFile { Ok(()) } - - fn action_state(&self) -> ActionState { - self.action_state - } - - fn set_action_state(&mut self, action_state: ActionState) { - self.action_state = action_state; - } } #[derive(Debug, thiserror::Error)] diff --git a/src/action/base/create_group.rs b/src/action/base/create_group.rs index 1c641b2..61064ae 100644 --- a/src/action/base/create_group.rs +++ b/src/action/base/create_group.rs @@ -3,25 +3,23 @@ use tokio::process::Command; use crate::execute_command; use crate::{ - action::{Action, ActionDescription, ActionState}, + action::{Action, ActionDescription, StatefulAction}, BoxableError, }; +/** +Create an operating system level user group +*/ #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] pub struct CreateGroup { name: String, gid: usize, - action_state: ActionState, } impl CreateGroup { #[tracing::instrument(skip_all)] - pub fn plan(name: String, gid: usize) -> Self { - Self { - name, - gid, - action_state: ActionState::Uncompleted, - } + pub fn plan(name: String, gid: usize) -> StatefulAction { + Self { name, gid }.into() } } @@ -32,11 +30,7 @@ impl Action for CreateGroup { format!("Create group `{}` (GID {})", self.name, self.gid) } fn execute_description(&self) -> Vec { - let Self { - name: _, - gid: _, - action_state: _, - } = &self; + let Self { name: _, gid: _ } = &self; vec![ActionDescription::new( self.tracing_synopsis(), vec![format!( @@ -50,11 +44,7 @@ impl Action for CreateGroup { gid = self.gid, ))] async fn execute(&mut self) -> Result<(), Box> { - let Self { - name, - gid, - action_state: _, - } = self; + let Self { name, gid } = self; use target_lexicon::OperatingSystem; match target_lexicon::OperatingSystem::host() { @@ -109,11 +99,7 @@ impl Action for CreateGroup { } fn revert_description(&self) -> Vec { - let Self { - name, - gid, - action_state: _, - } = &self; + let Self { name, gid } = &self; vec![ActionDescription::new( format!("Delete group `{name}` (GID {gid})"), vec![format!( @@ -127,11 +113,7 @@ impl Action for CreateGroup { gid = self.gid, ))] async fn revert(&mut self) -> Result<(), Box> { - let Self { - name, - gid: _, - action_state: _, - } = self; + let Self { name, gid: _ } = self; use target_lexicon::OperatingSystem; match target_lexicon::OperatingSystem::host() { @@ -166,14 +148,6 @@ impl Action for CreateGroup { Ok(()) } - - fn action_state(&self) -> ActionState { - self.action_state - } - - fn set_action_state(&mut self, action_state: ActionState) { - self.action_state = action_state; - } } #[derive(Debug, thiserror::Error)] diff --git a/src/action/base/create_or_append_file.rs b/src/action/base/create_or_append_file.rs index 1df96fe..d533dc6 100644 --- a/src/action/base/create_or_append_file.rs +++ b/src/action/base/create_or_append_file.rs @@ -11,10 +11,18 @@ use tokio::{ }; use crate::{ - action::{Action, ActionDescription, ActionState}, + action::{Action, ActionDescription, StatefulAction}, BoxableError, }; +/** Create a file at the given location with the provided `buf`, +optionally with an owning user, group, and mode. + +If the file exists, the provided `buf` will be appended. + +If `force` is set, the file will always be overwritten (and deleted) +regardless of its presence prior to install. + */ #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] pub struct CreateOrAppendFile { path: PathBuf, @@ -22,7 +30,6 @@ pub struct CreateOrAppendFile { group: Option, mode: Option, buf: String, - action_state: ActionState, } impl CreateOrAppendFile { @@ -33,7 +40,7 @@ impl CreateOrAppendFile { group: impl Into>, mode: impl Into>, buf: String, - ) -> Result { + ) -> Result, CreateOrAppendFileError> { let path = path.as_ref().to_path_buf(); Ok(Self { @@ -42,8 +49,8 @@ impl CreateOrAppendFile { group: group.into(), mode: mode.into(), buf, - action_state: ActionState::Uncompleted, - }) + } + .into()) } } @@ -71,7 +78,6 @@ impl Action for CreateOrAppendFile { group, mode, buf, - action_state: _, } = self; let mut file = OpenOptions::new() @@ -132,7 +138,6 @@ impl Action for CreateOrAppendFile { group: _, mode: _, buf, - action_state: _, } = &self; vec![ActionDescription::new( format!("Delete Nix related fragment from file `{}`", path.display()), @@ -156,7 +161,6 @@ impl Action for CreateOrAppendFile { group: _, mode: _, buf, - action_state: _, } = self; let mut file = OpenOptions::new() .create(false) @@ -190,14 +194,6 @@ impl Action for CreateOrAppendFile { } Ok(()) } - - fn action_state(&self) -> ActionState { - self.action_state - } - - fn set_action_state(&mut self, action_state: ActionState) { - self.action_state = action_state; - } } #[derive(Debug, thiserror::Error)] diff --git a/src/action/base/create_user.rs b/src/action/base/create_user.rs index d288531..8ece74d 100644 --- a/src/action/base/create_user.rs +++ b/src/action/base/create_user.rs @@ -3,29 +3,31 @@ use tokio::process::Command; use crate::execute_command; use crate::{ - action::{Action, ActionDescription, ActionState}, + action::{Action, ActionDescription, StatefulAction}, BoxableError, }; +/** +Create an operating system level user in the given group +*/ #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] pub struct CreateUser { name: String, uid: usize, groupname: String, gid: usize, - action_state: ActionState, } impl CreateUser { #[tracing::instrument(skip_all)] - pub fn plan(name: String, uid: usize, groupname: String, gid: usize) -> Self { + pub fn plan(name: String, uid: usize, groupname: String, gid: usize) -> StatefulAction { Self { name, uid, groupname, gid, - action_state: ActionState::Uncompleted, } + .into() } } @@ -59,7 +61,6 @@ impl Action for CreateUser { uid, groupname, gid, - action_state: _, } = self; use target_lexicon::OperatingSystem; @@ -241,7 +242,6 @@ impl Action for CreateUser { uid: _, groupname: _, gid: _, - action_state: _, } = self; use target_lexicon::OperatingSystem; @@ -277,14 +277,6 @@ impl Action for CreateUser { Ok(()) } - - fn action_state(&self) -> ActionState { - self.action_state - } - - fn set_action_state(&mut self, action_state: ActionState) { - self.action_state = action_state; - } } #[derive(Debug, thiserror::Error)] diff --git a/src/action/base/fetch_nix.rs b/src/action/base/fetch_and_unpack_nix.rs similarity index 59% rename from src/action/base/fetch_nix.rs rename to src/action/base/fetch_and_unpack_nix.rs index 0991df1..47a8bd2 100644 --- a/src/action/base/fetch_nix.rs +++ b/src/action/base/fetch_and_unpack_nix.rs @@ -6,46 +6,38 @@ use reqwest::Url; use tokio::task::JoinError; use crate::{ - action::{Action, ActionDescription, ActionState}, + action::{Action, ActionDescription, StatefulAction}, BoxableError, }; +/** +Fetch a URL to the given path +*/ #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] -pub struct FetchNix { +pub struct FetchAndUnpackNix { url: Url, dest: PathBuf, - action_state: ActionState, } -impl FetchNix { +impl FetchAndUnpackNix { #[tracing::instrument(skip_all)] - pub async fn plan(url: Url, dest: PathBuf) -> Result { + pub async fn plan(url: Url, dest: PathBuf) -> Result, FetchUrlError> { // TODO(@hoverbear): Check URL exists? // TODO(@hoverbear): Check tempdir exists - Ok(Self { - url, - dest, - action_state: ActionState::Uncompleted, - }) + Ok(Self { url, dest }.into()) } } #[async_trait::async_trait] -#[typetag::serde(name = "fetch_nix")] -impl Action for FetchNix { +#[typetag::serde(name = "fetch_and_unpack_nix")] +impl Action for FetchAndUnpackNix { fn tracing_synopsis(&self) -> String { - format!("Fetch Nix from `{}`", self.url) + format!("Fetch `{}` to `{}`", self.url, self.dest.display()) } fn execute_description(&self) -> Vec { - vec![ActionDescription::new( - self.tracing_synopsis(), - vec![format!( - "Unpack it to `{}` (moved later)", - self.dest.display() - )], - )] + vec![ActionDescription::new(self.tracing_synopsis(), vec![])] } #[tracing::instrument(skip_all, fields( @@ -53,19 +45,15 @@ impl Action for FetchNix { dest = %self.dest.display(), ))] async fn execute(&mut self) -> Result<(), Box> { - let Self { - url, - dest, - action_state: _, - } = self; + let Self { url, dest } = self; let res = reqwest::get(url.clone()) .await - .map_err(|e| FetchNixError::Reqwest(e).boxed())?; + .map_err(|e| FetchUrlError::Reqwest(e).boxed())?; let bytes = res .bytes() .await - .map_err(|e| FetchNixError::Reqwest(e).boxed())?; + .map_err(|e| FetchUrlError::Reqwest(e).boxed())?; // TODO(@Hoverbear): Pick directory tracing::trace!("Unpacking tar.xz"); let dest_clone = dest.clone(); @@ -74,7 +62,7 @@ impl Action for FetchNix { let mut archive = tar::Archive::new(decoder); archive .unpack(&dest_clone) - .map_err(|e| FetchNixError::Unarchive(e).boxed())?; + .map_err(|e| FetchUrlError::Unarchive(e).boxed())?; Ok(()) } @@ -88,26 +76,14 @@ impl Action for FetchNix { dest = %self.dest.display(), ))] async fn revert(&mut self) -> Result<(), Box> { - let Self { - url: _, - dest: _, - action_state: _, - } = self; + let Self { url: _, dest: _ } = self; Ok(()) } - - fn action_state(&self) -> ActionState { - self.action_state - } - - fn set_action_state(&mut self, action_state: ActionState) { - self.action_state = action_state; - } } #[derive(Debug, thiserror::Error)] -pub enum FetchNixError { +pub enum FetchUrlError { #[error("Joining spawned async task")] Join( #[source] diff --git a/src/action/base/mod.rs b/src/action/base/mod.rs index f956868..2e8dca0 100644 --- a/src/action/base/mod.rs +++ b/src/action/base/mod.rs @@ -1,21 +1,19 @@ -//! Base actions that themselves have no other actions as dependencies +//! Base [`Action`](crate::action::Action)s that themselves have no other actions as dependencies -mod configure_nix_daemon_service; mod create_directory; mod create_file; mod create_group; mod create_or_append_file; mod create_user; -mod fetch_nix; +mod fetch_and_unpack_nix; mod move_unpacked_nix; mod setup_default_profile; -pub use configure_nix_daemon_service::{ConfigureNixDaemonService, ConfigureNixDaemonServiceError}; pub use create_directory::{CreateDirectory, CreateDirectoryError}; pub use create_file::{CreateFile, CreateFileError}; pub use create_group::{CreateGroup, CreateGroupError}; pub use create_or_append_file::{CreateOrAppendFile, CreateOrAppendFileError}; pub use create_user::{CreateUser, CreateUserError}; -pub use fetch_nix::{FetchNix, FetchNixError}; +pub use fetch_and_unpack_nix::{FetchAndUnpackNix, FetchUrlError}; pub use move_unpacked_nix::{MoveUnpackedNix, MoveUnpackedNixError}; pub use setup_default_profile::{SetupDefaultProfile, SetupDefaultProfileError}; diff --git a/src/action/base/move_unpacked_nix.rs b/src/action/base/move_unpacked_nix.rs index 68f857e..d855590 100644 --- a/src/action/base/move_unpacked_nix.rs +++ b/src/action/base/move_unpacked_nix.rs @@ -1,26 +1,25 @@ use std::path::{Path, PathBuf}; use crate::{ - action::{Action, ActionDescription, ActionState}, + action::{Action, ActionDescription, StatefulAction}, BoxableError, }; const DEST: &str = "/nix/store"; +/** +Move an unpacked Nix at `src` to `/nix` +*/ #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] pub struct MoveUnpackedNix { src: PathBuf, - action_state: ActionState, } impl MoveUnpackedNix { #[tracing::instrument(skip_all)] - pub async fn plan(src: PathBuf) -> Result { + pub async fn plan(src: PathBuf) -> Result, MoveUnpackedNixError> { // Note: Do NOT try to check for the src/dest since the installer creates those - Ok(Self { - src, - action_state: ActionState::Uncompleted, - }) + Ok(Self { src }.into()) } } @@ -46,12 +45,7 @@ impl Action for MoveUnpackedNix { dest = DEST, ))] async fn execute(&mut self) -> Result<(), Box> { - let Self { src, action_state } = self; - if *action_state == ActionState::Completed { - tracing::trace!("Already completed: Moving Nix"); - return Ok(()); - } - tracing::debug!("Moving Nix"); + let Self { src } = self; // TODO(@Hoverbear): I would like to make this less awful let found_nix_paths = glob::glob(&format!("{}/nix-*", src.display())) @@ -76,8 +70,7 @@ impl Action for MoveUnpackedNix { tokio::fs::remove_dir_all(src) .await .map_err(|e| MoveUnpackedNixError::Rename(src_store, dest.to_owned(), e).boxed())?; - tracing::trace!("Moved Nix"); - *action_state = ActionState::Completed; + Ok(()) } @@ -93,14 +86,6 @@ impl Action for MoveUnpackedNix { // Noop Ok(()) } - - fn action_state(&self) -> ActionState { - self.action_state - } - - fn set_action_state(&mut self, action_state: ActionState) { - self.action_state = action_state; - } } #[derive(Debug, thiserror::Error)] diff --git a/src/action/base/setup_default_profile.rs b/src/action/base/setup_default_profile.rs index 6aa094d..6ca8900 100644 --- a/src/action/base/setup_default_profile.rs +++ b/src/action/base/setup_default_profile.rs @@ -1,4 +1,4 @@ -use crate::{action::ActionState, execute_command, set_env, BoxableError}; +use crate::{action::StatefulAction, execute_command, set_env, BoxableError}; use glob::glob; @@ -6,19 +6,20 @@ use tokio::process::Command; use crate::action::{Action, ActionDescription}; +/** +Setup the default Nix profile with `nss-cacert` and `nix` itself. + */ #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] pub struct SetupDefaultProfile { channels: Vec, - action_state: ActionState, } impl SetupDefaultProfile { #[tracing::instrument(skip_all)] - pub async fn plan(channels: Vec) -> Result { - Ok(Self { - channels, - action_state: ActionState::Uncompleted, - }) + pub async fn plan( + channels: Vec, + ) -> Result, SetupDefaultProfileError> { + Ok(Self { channels }.into()) } } @@ -37,10 +38,7 @@ impl Action for SetupDefaultProfile { channels = %self.channels.join(","), ))] async fn execute(&mut self) -> Result<(), Box> { - let Self { - channels, - action_state: _, - } = self; + let Self { channels } = self; // Find an `nix` package let nix_pkg_glob = "/nix/store/*-nix-*"; @@ -159,14 +157,6 @@ impl Action for SetupDefaultProfile { Ok(()) } - - fn action_state(&self) -> ActionState { - self.action_state - } - - fn set_action_state(&mut self, action_state: ActionState) { - self.action_state = action_state; - } } #[derive(Debug, thiserror::Error)] diff --git a/src/action/common/configure_nix.rs b/src/action/common/configure_nix.rs index a03062c..ff04b81 100644 --- a/src/action/common/configure_nix.rs +++ b/src/action/common/configure_nix.rs @@ -1,30 +1,34 @@ use crate::{ action::{ - base::{ConfigureNixDaemonService, SetupDefaultProfile}, + base::SetupDefaultProfile, common::{ConfigureShellProfile, PlaceChannelConfiguration, PlaceNixConfiguration}, - Action, ActionDescription, ActionImplementation, ActionState, + linux::ConfigureNixDaemonService, + Action, ActionDescription, StatefulAction, }, channel_value::ChannelValue, - BoxableError, CommonSettings, + settings::CommonSettings, + BoxableError, }; use reqwest::Url; +/** +Configure Nix and start it + */ #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] pub struct ConfigureNix { - setup_default_profile: SetupDefaultProfile, - configure_shell_profile: Option, - place_channel_configuration: PlaceChannelConfiguration, - place_nix_configuration: PlaceNixConfiguration, - configure_nix_daemon_service: ConfigureNixDaemonService, - action_state: ActionState, + setup_default_profile: StatefulAction, + configure_shell_profile: Option>, + place_channel_configuration: StatefulAction, + place_nix_configuration: StatefulAction, + configure_nix_daemon_service: StatefulAction, } impl ConfigureNix { #[tracing::instrument(skip_all)] pub async fn plan( settings: &CommonSettings, - ) -> Result> { + ) -> Result, Box> { let channels: Vec<(String, Url)> = settings .channels .iter() @@ -57,8 +61,8 @@ impl ConfigureNix { setup_default_profile, configure_nix_daemon_service, configure_shell_profile, - action_state: ActionState::Uncompleted, - }) + } + .into()) } } @@ -76,15 +80,14 @@ impl Action for ConfigureNix { place_nix_configuration, place_channel_configuration, configure_shell_profile, - action_state: _, } = &self; - let mut buf = setup_default_profile.execute_description(); - buf.append(&mut configure_nix_daemon_service.execute_description()); - buf.append(&mut place_nix_configuration.execute_description()); - buf.append(&mut place_channel_configuration.execute_description()); + let mut buf = setup_default_profile.describe_execute(); + buf.append(&mut configure_nix_daemon_service.describe_execute()); + buf.append(&mut place_nix_configuration.describe_execute()); + buf.append(&mut place_channel_configuration.describe_execute()); if let Some(configure_shell_profile) = configure_shell_profile { - buf.append(&mut configure_shell_profile.execute_description()); + buf.append(&mut configure_shell_profile.describe_execute()); } buf } @@ -97,7 +100,6 @@ impl Action for ConfigureNix { place_nix_configuration, place_channel_configuration, configure_shell_profile, - action_state: _, } = self; if let Some(configure_shell_profile) = configure_shell_profile { @@ -126,17 +128,16 @@ impl Action for ConfigureNix { place_nix_configuration, place_channel_configuration, configure_shell_profile, - action_state: _, } = &self; let mut buf = Vec::default(); if let Some(configure_shell_profile) = configure_shell_profile { - buf.append(&mut configure_shell_profile.revert_description()); + buf.append(&mut configure_shell_profile.describe_revert()); } - buf.append(&mut place_channel_configuration.revert_description()); - buf.append(&mut place_nix_configuration.revert_description()); - buf.append(&mut configure_nix_daemon_service.revert_description()); - buf.append(&mut setup_default_profile.revert_description()); + buf.append(&mut place_channel_configuration.describe_revert()); + buf.append(&mut place_nix_configuration.describe_revert()); + buf.append(&mut configure_nix_daemon_service.describe_revert()); + buf.append(&mut setup_default_profile.describe_revert()); buf } @@ -149,7 +150,6 @@ impl Action for ConfigureNix { place_nix_configuration, place_channel_configuration, configure_shell_profile, - action_state: _, } = self; configure_nix_daemon_service.try_revert().await?; @@ -162,12 +162,4 @@ impl Action for ConfigureNix { Ok(()) } - - fn action_state(&self) -> ActionState { - self.action_state - } - - fn set_action_state(&mut self, action_state: ActionState) { - self.action_state = action_state; - } } diff --git a/src/action/common/configure_shell_profile.rs b/src/action/common/configure_shell_profile.rs index fd7079d..edd54cd 100644 --- a/src/action/common/configure_shell_profile.rs +++ b/src/action/common/configure_shell_profile.rs @@ -1,5 +1,5 @@ use crate::action::base::{CreateOrAppendFile, CreateOrAppendFileError}; -use crate::action::{Action, ActionDescription, ActionImplementation, ActionState}; +use crate::action::{Action, ActionDescription, StatefulAction}; use crate::BoxableError; use std::path::Path; @@ -15,15 +15,17 @@ const PROFILE_TARGETS: &[&str] = &[ ]; const PROFILE_NIX_FILE: &str = "/nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh"; +/** +Configure any detected shell profiles to include Nix support + */ #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] pub struct ConfigureShellProfile { - create_or_append_files: Vec, - action_state: ActionState, + create_or_append_files: Vec>, } impl ConfigureShellProfile { #[tracing::instrument(skip_all)] - pub async fn plan() -> Result> { + pub async fn plan() -> Result, Box> { let mut create_or_append_files = Vec::default(); for profile_target in PROFILE_TARGETS { let path = Path::new(profile_target); @@ -49,8 +51,8 @@ impl ConfigureShellProfile { Ok(Self { create_or_append_files, - action_state: ActionState::Uncompleted, - }) + } + .into()) } } @@ -72,7 +74,6 @@ impl Action for ConfigureShellProfile { async fn execute(&mut self) -> Result<(), Box> { let Self { create_or_append_files, - action_state: _, } = self; let mut set = JoinSet::new(); @@ -121,7 +122,6 @@ impl Action for ConfigureShellProfile { async fn revert(&mut self) -> Result<(), Box> { let Self { create_or_append_files, - action_state: _, } = self; let mut set = JoinSet::new(); @@ -130,7 +130,7 @@ impl Action for ConfigureShellProfile { for (idx, create_or_append_file) in create_or_append_files.iter().enumerate() { let mut create_or_append_file_clone = create_or_append_file.clone(); let _abort_handle = set.spawn(async move { - create_or_append_file_clone.revert().await?; + create_or_append_file_clone.try_revert().await?; Result::<_, Box>::Ok(( idx, create_or_append_file_clone, @@ -158,14 +158,6 @@ impl Action for ConfigureShellProfile { Ok(()) } - - fn action_state(&self) -> ActionState { - self.action_state - } - - fn set_action_state(&mut self, action_state: ActionState) { - self.action_state = action_state; - } } #[derive(Debug, thiserror::Error)] diff --git a/src/action/common/create_nix_tree.rs b/src/action/common/create_nix_tree.rs index e39724a..57e73d4 100644 --- a/src/action/common/create_nix_tree.rs +++ b/src/action/common/create_nix_tree.rs @@ -1,5 +1,5 @@ use crate::action::base::{CreateDirectory, CreateDirectoryError}; -use crate::action::{Action, ActionDescription, ActionImplementation, ActionState}; +use crate::action::{Action, ActionDescription, StatefulAction}; const PATHS: &[&str] = &[ "/nix/var", @@ -17,25 +17,24 @@ const PATHS: &[&str] = &[ "/nix/var/nix/daemon-socket", ]; +/** +Create the `/nix` tree + */ #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] pub struct CreateNixTree { - create_directories: Vec, - action_state: ActionState, + create_directories: Vec>, } impl CreateNixTree { #[tracing::instrument(skip_all)] - pub async fn plan() -> Result> { + pub async fn plan() -> Result, Box> { 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, None, None, 0o0755, false).await?) } - Ok(Self { - create_directories, - action_state: ActionState::Uncompleted, - }) + Ok(Self { create_directories }.into()) } } @@ -67,10 +66,7 @@ impl Action for CreateNixTree { #[tracing::instrument(skip_all)] async fn execute(&mut self) -> Result<(), Box> { - let Self { - create_directories, - action_state: _, - } = self; + let Self { create_directories } = self; // Just do sequential since parallelizing this will have little benefit for create_directory in create_directories { @@ -102,26 +98,15 @@ impl Action for CreateNixTree { #[tracing::instrument(skip_all)] async fn revert(&mut self) -> Result<(), Box> { - let Self { - create_directories, - action_state: _, - } = self; + let Self { create_directories } = self; // Just do sequential since parallelizing this will have little benefit for create_directory in create_directories.iter_mut().rev() { - create_directory.revert().await? + create_directory.try_revert().await? } Ok(()) } - - fn action_state(&self) -> ActionState { - self.action_state - } - - fn set_action_state(&mut self, action_state: ActionState) { - self.action_state = action_state; - } } #[derive(Debug, thiserror::Error)] diff --git a/src/action/common/create_users_and_group.rs b/src/action/common/create_users_and_groups.rs similarity index 80% rename from src/action/common/create_users_and_group.rs rename to src/action/common/create_users_and_groups.rs index a3ea62e..e75489d 100644 --- a/src/action/common/create_users_and_group.rs +++ b/src/action/common/create_users_and_groups.rs @@ -1,28 +1,29 @@ -use crate::CommonSettings; use crate::{ action::{ base::{CreateGroup, CreateGroupError, CreateUser, CreateUserError}, - Action, ActionDescription, ActionImplementation, ActionState, + Action, ActionDescription, StatefulAction, }, + settings::CommonSettings, BoxableError, }; use tokio::task::{JoinError, JoinSet}; #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] -pub struct CreateUsersAndGroup { +pub struct CreateUsersAndGroups { daemon_user_count: usize, nix_build_group_name: String, nix_build_group_id: usize, nix_build_user_prefix: String, nix_build_user_id_base: usize, - create_group: CreateGroup, - create_users: Vec, - action_state: ActionState, + create_group: StatefulAction, + create_users: Vec>, } -impl CreateUsersAndGroup { +impl CreateUsersAndGroups { #[tracing::instrument(skip_all)] - pub async fn plan(settings: CommonSettings) -> Result { + pub async fn plan( + settings: CommonSettings, + ) -> Result, CreateUsersAndGroupsError> { // TODO(@hoverbear): CHeck if it exist, error if so let create_group = CreateGroup::plan( settings.nix_build_group_name.clone(), @@ -47,14 +48,14 @@ impl CreateUsersAndGroup { nix_build_user_id_base: settings.nix_build_user_id_base, create_group, create_users, - action_state: ActionState::Uncompleted, - }) + } + .into()) } } #[async_trait::async_trait] #[typetag::serde(name = "create_users_and_group")] -impl Action for CreateUsersAndGroup { +impl Action for CreateUsersAndGroups { fn tracing_synopsis(&self) -> String { format!( "Create build users (UID {}-{}) and group (GID {})", @@ -73,7 +74,6 @@ impl Action for CreateUsersAndGroup { nix_build_user_id_base: _, create_group, create_users, - action_state: _, } = &self; let mut create_users_descriptions = Vec::new(); @@ -110,7 +110,6 @@ impl Action for CreateUsersAndGroup { nix_build_group_id: _, nix_build_user_prefix: _, nix_build_user_id_base: _, - action_state: _, } = self; // Create group @@ -152,7 +151,7 @@ impl Action for CreateUsersAndGroup { if errors.len() == 1 { return Err(errors.into_iter().next().unwrap().into()); } else { - return Err(CreateUsersAndGroupError::CreateUsers(errors).boxed()); + return Err(CreateUsersAndGroupsError::CreateUsers(errors).boxed()); } } }, @@ -170,31 +169,26 @@ impl Action for CreateUsersAndGroup { nix_build_user_id_base: _, create_group, create_users, - action_state: _, } = &self; - if self.action_state == ActionState::Uncompleted { - vec![] - } else { - let mut create_users_descriptions = Vec::new(); - for create_user in create_users { - if let Some(val) = create_user.describe_revert().iter().next() { - create_users_descriptions.push(val.description.clone()) - } + let mut create_users_descriptions = Vec::new(); + for create_user in create_users { + if let Some(val) = create_user.describe_revert().iter().next() { + create_users_descriptions.push(val.description.clone()) } - - let mut explanation = vec![ - format!("The nix daemon requires system users (and a group they share) which it can act as in order to build"), - ]; - if let Some(val) = create_group.describe_revert().iter().next() { - explanation.push(val.description.clone()) - } - explanation.append(&mut create_users_descriptions); - - vec![ActionDescription::new( - format!("Remove Nix users and group"), - explanation, - )] } + + let mut explanation = vec![ + format!("The nix daemon requires system users (and a group they share) which it can act as in order to build"), + ]; + if let Some(val) = create_group.describe_revert().iter().next() { + explanation.push(val.description.clone()) + } + explanation.append(&mut create_users_descriptions); + + vec![ActionDescription::new( + format!("Remove Nix users and group"), + explanation, + )] } #[tracing::instrument(skip_all, fields( @@ -213,7 +207,6 @@ impl Action for CreateUsersAndGroup { nix_build_group_id: _, nix_build_user_prefix: _, nix_build_user_id_base: _, - action_state: _, } = self; let mut set = JoinSet::new(); @@ -222,7 +215,7 @@ impl Action for CreateUsersAndGroup { for (idx, create_user) in create_users.iter().enumerate() { let mut create_user_clone = create_user.clone(); let _abort_handle = set.spawn(async move { - create_user_clone.revert().await?; + create_user_clone.try_revert().await?; Result::<_, Box>::Ok((idx, create_user_clone)) }); } @@ -239,27 +232,19 @@ impl Action for CreateUsersAndGroup { if errors.len() == 1 { return Err(errors.into_iter().next().unwrap().into()); } else { - return Err(CreateUsersAndGroupError::CreateUsers(errors).boxed()); + return Err(CreateUsersAndGroupsError::CreateUsers(errors).boxed()); } } // Create group - create_group.revert().await?; + create_group.try_revert().await?; Ok(()) } - - fn action_state(&self) -> ActionState { - self.action_state - } - - fn set_action_state(&mut self, action_state: ActionState) { - self.action_state = action_state; - } } #[derive(Debug, thiserror::Error)] -pub enum CreateUsersAndGroupError { +pub enum CreateUsersAndGroupsError { #[error("Creating user")] CreateUser( #[source] diff --git a/src/action/common/mod.rs b/src/action/common/mod.rs index baa2d29..4d25986 100644 --- a/src/action/common/mod.rs +++ b/src/action/common/mod.rs @@ -1,9 +1,9 @@ -/*! Actions which only call other base plugins. */ +//! [`Action`](crate::action::Action)s which only call other base plugins mod configure_nix; mod configure_shell_profile; mod create_nix_tree; -mod create_users_and_group; +mod create_users_and_groups; mod place_channel_configuration; mod place_nix_configuration; mod provision_nix; @@ -11,7 +11,7 @@ mod provision_nix; pub use configure_nix::ConfigureNix; pub use configure_shell_profile::ConfigureShellProfile; pub use create_nix_tree::{CreateNixTree, CreateNixTreeError}; -pub use create_users_and_group::{CreateUsersAndGroup, CreateUsersAndGroupError}; +pub use create_users_and_groups::{CreateUsersAndGroups, CreateUsersAndGroupsError}; pub use place_channel_configuration::{PlaceChannelConfiguration, PlaceChannelConfigurationError}; pub use place_nix_configuration::{PlaceNixConfiguration, PlaceNixConfigurationError}; pub use provision_nix::{ProvisionNix, ProvisionNixError}; diff --git a/src/action/common/place_channel_configuration.rs b/src/action/common/place_channel_configuration.rs index 28d786f..9671e65 100644 --- a/src/action/common/place_channel_configuration.rs +++ b/src/action/common/place_channel_configuration.rs @@ -1,15 +1,17 @@ use crate::action::base::{CreateFile, CreateFileError}; use crate::{ - action::{Action, ActionDescription, ActionImplementation, ActionState}, + action::{Action, ActionDescription, StatefulAction}, BoxableError, }; use reqwest::Url; +/** +Place a channel configuration containing `channels` to the `$ROOT_HOME/.nix-channels` file + */ #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] pub struct PlaceChannelConfiguration { channels: Vec<(String, Url)>, - create_file: CreateFile, - action_state: ActionState, + create_file: StatefulAction, } impl PlaceChannelConfiguration { @@ -17,7 +19,7 @@ impl PlaceChannelConfiguration { pub async fn plan( channels: Vec<(String, Url)>, force: bool, - ) -> Result> { + ) -> Result, Box> { let buf = channels .iter() .map(|(name, url)| format!("{} {}", url, name)) @@ -37,8 +39,8 @@ impl PlaceChannelConfiguration { Ok(Self { create_file, channels, - action_state: ActionState::Uncompleted, - }) + } + .into()) } } @@ -48,7 +50,7 @@ impl Action for PlaceChannelConfiguration { fn tracing_synopsis(&self) -> String { format!( "Place channel configuration at `{}`", - self.create_file.path.display() + self.create_file.inner().path.display() ) } @@ -63,7 +65,6 @@ impl Action for PlaceChannelConfiguration { let Self { create_file, channels: _, - action_state: _, } = self; create_file.try_execute().await?; @@ -75,7 +76,7 @@ impl Action for PlaceChannelConfiguration { vec![ActionDescription::new( format!( "Remove channel configuration at `{}`", - self.create_file.path.display() + self.create_file.inner().path.display() ), vec![], )] @@ -88,21 +89,12 @@ impl Action for PlaceChannelConfiguration { let Self { create_file, channels: _, - action_state: _, } = self; - create_file.revert().await?; + create_file.try_revert().await?; Ok(()) } - - fn action_state(&self) -> ActionState { - self.action_state - } - - fn set_action_state(&mut self, action_state: ActionState) { - self.action_state = action_state; - } } #[derive(Debug, thiserror::Error)] diff --git a/src/action/common/place_nix_configuration.rs b/src/action/common/place_nix_configuration.rs index bad6e6f..6116f4c 100644 --- a/src/action/common/place_nix_configuration.rs +++ b/src/action/common/place_nix_configuration.rs @@ -1,14 +1,16 @@ use crate::action::base::{CreateDirectory, CreateDirectoryError, CreateFile, CreateFileError}; -use crate::action::{Action, ActionDescription, ActionImplementation, ActionState}; +use crate::action::{Action, ActionDescription, StatefulAction}; const NIX_CONF_FOLDER: &str = "/etc/nix"; const NIX_CONF: &str = "/etc/nix/nix.conf"; +/** +Place the `/etc/nix.conf` file + */ #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] pub struct PlaceNixConfiguration { - create_directory: CreateDirectory, - create_file: CreateFile, - action_state: ActionState, + create_directory: StatefulAction, + create_file: StatefulAction, } impl PlaceNixConfiguration { @@ -17,7 +19,7 @@ impl PlaceNixConfiguration { nix_build_group_name: String, extra_conf: Option, force: bool, - ) -> Result> { + ) -> Result, Box> { let buf = format!( "\ {extra_conf}\n\ @@ -36,8 +38,8 @@ impl PlaceNixConfiguration { Ok(Self { create_directory, create_file, - action_state: ActionState::Uncompleted, - }) + } + .into()) } } @@ -63,7 +65,6 @@ impl Action for PlaceNixConfiguration { let Self { create_file, create_directory, - action_state: _, } = self; create_directory.try_execute().await?; @@ -87,22 +88,13 @@ impl Action for PlaceNixConfiguration { let Self { create_file, create_directory, - action_state: _, } = self; - create_file.revert().await?; - create_directory.revert().await?; + create_file.try_revert().await?; + create_directory.try_revert().await?; Ok(()) } - - fn action_state(&self) -> ActionState { - self.action_state - } - - fn set_action_state(&mut self, action_state: ActionState) { - self.action_state = action_state; - } } #[derive(Debug, thiserror::Error)] diff --git a/src/action/common/provision_nix.rs b/src/action/common/provision_nix.rs index 6d18fc6..f7b23ee 100644 --- a/src/action/common/provision_nix.rs +++ b/src/action/common/provision_nix.rs @@ -1,37 +1,42 @@ -use crate::action::base::{ - CreateDirectoryError, FetchNix, FetchNixError, MoveUnpackedNix, MoveUnpackedNixError, -}; -use crate::CommonSettings; use crate::{ - action::{Action, ActionDescription, ActionImplementation, ActionState}, + action::{ + base::{ + CreateDirectoryError, FetchAndUnpackNix, FetchUrlError, MoveUnpackedNix, + MoveUnpackedNixError, + }, + Action, ActionDescription, StatefulAction, + }, + settings::CommonSettings, BoxableError, }; use std::path::PathBuf; use tokio::task::JoinError; -use super::{CreateNixTree, CreateNixTreeError, CreateUsersAndGroup, CreateUsersAndGroupError}; +use super::{CreateNixTree, CreateNixTreeError, CreateUsersAndGroups, CreateUsersAndGroupsError}; +/** +Place Nix and it's requirements onto the target + */ #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] pub struct ProvisionNix { - fetch_nix: FetchNix, - create_users_and_group: CreateUsersAndGroup, - create_nix_tree: CreateNixTree, - move_unpacked_nix: MoveUnpackedNix, - action_state: ActionState, + fetch_nix: StatefulAction, + create_users_and_group: StatefulAction, + create_nix_tree: StatefulAction, + move_unpacked_nix: StatefulAction, } impl ProvisionNix { #[tracing::instrument(skip_all)] pub async fn plan( settings: &CommonSettings, - ) -> Result> { - let fetch_nix = FetchNix::plan( + ) -> Result, Box> { + let fetch_nix = FetchAndUnpackNix::plan( settings.nix_package_url.clone(), PathBuf::from("/nix/temp-install-dir"), ) .await .map_err(|e| e.boxed())?; - let create_users_and_group = CreateUsersAndGroup::plan(settings.clone()) + let create_users_and_group = CreateUsersAndGroups::plan(settings.clone()) .await .map_err(|e| e.boxed())?; let create_nix_tree = CreateNixTree::plan().await?; @@ -43,8 +48,8 @@ impl ProvisionNix { create_users_and_group, create_nix_tree, move_unpacked_nix, - action_state: ActionState::Uncompleted, - }) + } + .into()) } } @@ -61,14 +66,13 @@ impl Action for ProvisionNix { create_users_and_group, create_nix_tree, move_unpacked_nix, - action_state: _, } = &self; let mut buf = Vec::default(); - buf.append(&mut fetch_nix.execute_description()); - buf.append(&mut create_users_and_group.execute_description()); - buf.append(&mut create_nix_tree.execute_description()); - buf.append(&mut move_unpacked_nix.execute_description()); + buf.append(&mut fetch_nix.describe_execute()); + buf.append(&mut create_users_and_group.describe_execute()); + buf.append(&mut create_nix_tree.describe_execute()); + buf.append(&mut move_unpacked_nix.describe_execute()); buf } @@ -80,7 +84,6 @@ impl Action for ProvisionNix { create_nix_tree, create_users_and_group, move_unpacked_nix, - action_state: _, } = self; // We fetch nix while doing the rest, then move it over. @@ -105,14 +108,13 @@ impl Action for ProvisionNix { create_users_and_group, create_nix_tree, move_unpacked_nix, - action_state: _, } = &self; let mut buf = Vec::default(); - buf.append(&mut move_unpacked_nix.revert_description()); - buf.append(&mut create_nix_tree.revert_description()); - buf.append(&mut create_users_and_group.revert_description()); - buf.append(&mut fetch_nix.revert_description()); + buf.append(&mut move_unpacked_nix.describe_revert()); + buf.append(&mut create_nix_tree.describe_revert()); + buf.append(&mut create_users_and_group.describe_revert()); + buf.append(&mut fetch_nix.describe_revert()); buf } @@ -123,7 +125,6 @@ impl Action for ProvisionNix { create_nix_tree, create_users_and_group, move_unpacked_nix, - action_state: _, } = self; // We fetch nix while doing the rest, then move it over. @@ -143,18 +144,10 @@ impl Action for ProvisionNix { } *fetch_nix = fetch_nix_handle.await.map_err(|e| e.boxed())??; - move_unpacked_nix.revert().await?; + move_unpacked_nix.try_revert().await?; Ok(()) } - - fn action_state(&self) -> ActionState { - self.action_state - } - - fn set_action_state(&mut self, action_state: ActionState) { - self.action_state = action_state; - } } #[derive(Debug, thiserror::Error)] @@ -163,7 +156,7 @@ pub enum ProvisionNixError { FetchNix( #[source] #[from] - FetchNixError, + FetchUrlError, ), #[error("Joining spawned async task")] Join( @@ -181,7 +174,7 @@ pub enum ProvisionNixError { CreateUsersAndGroup( #[source] #[from] - CreateUsersAndGroupError, + CreateUsersAndGroupsError, ), #[error("Creating nix tree")] CreateNixTree( diff --git a/src/action/darwin/bootstrap_volume.rs b/src/action/darwin/bootstrap_apfs_volume.rs similarity index 78% rename from src/action/darwin/bootstrap_volume.rs rename to src/action/darwin/bootstrap_apfs_volume.rs index 2ec3027..691b9ef 100644 --- a/src/action/darwin/bootstrap_volume.rs +++ b/src/action/darwin/bootstrap_apfs_volume.rs @@ -2,34 +2,37 @@ use std::path::{Path, PathBuf}; use tokio::process::Command; +use crate::action::StatefulAction; use crate::execute_command; use crate::{ - action::{Action, ActionDescription, ActionState}, + action::{Action, ActionDescription}, BoxableError, }; +/** +Bootstrap and kickstart an APFS volume +*/ #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] -pub struct BootstrapVolume { +pub struct BootstrapApfsVolume { path: PathBuf, - action_state: ActionState, } -impl BootstrapVolume { +impl BootstrapApfsVolume { #[tracing::instrument(skip_all)] pub async fn plan( path: impl AsRef, - ) -> Result> { + ) -> Result, Box> { Ok(Self { path: path.as_ref().to_path_buf(), - action_state: ActionState::Uncompleted, - }) + } + .into()) } } #[async_trait::async_trait] #[typetag::serde(name = "bootstrap_volume")] -impl Action for BootstrapVolume { +impl Action for BootstrapApfsVolume { fn tracing_synopsis(&self) -> String { format!("Bootstrap and kickstart `{}`", self.path.display()) } @@ -42,10 +45,7 @@ impl Action for BootstrapVolume { path = %self.path.display(), ))] async fn execute(&mut self) -> Result<(), Box> { - let Self { - path, - action_state: _, - } = self; + let Self { path } = self; execute_command( Command::new("launchctl") @@ -79,10 +79,7 @@ impl Action for BootstrapVolume { path = %self.path.display(), ))] async fn revert(&mut self) -> Result<(), Box> { - let Self { - path, - action_state: _, - } = self; + let Self { path } = self; execute_command( Command::new("launchctl") @@ -96,14 +93,6 @@ impl Action for BootstrapVolume { Ok(()) } - - fn action_state(&self) -> ActionState { - self.action_state - } - - fn set_action_state(&mut self, action_state: ActionState) { - self.action_state = action_state; - } } #[derive(Debug, thiserror::Error)] diff --git a/src/action/darwin/create_apfs_volume.rs b/src/action/darwin/create_apfs_volume.rs index c662382..e4b5fd6 100644 --- a/src/action/darwin/create_apfs_volume.rs +++ b/src/action/darwin/create_apfs_volume.rs @@ -1,39 +1,20 @@ -use crate::{ - action::{ - base::{CreateFile, CreateFileError, CreateOrAppendFile, CreateOrAppendFileError}, - darwin::{ - BootstrapVolume, BootstrapVolumeError, CreateSyntheticObjects, - CreateSyntheticObjectsError, CreateVolume, CreateVolumeError, EnableOwnership, - EnableOwnershipError, EncryptVolume, EncryptVolumeError, UnmountVolume, - UnmountVolumeError, - }, - Action, ActionDescription, ActionImplementation, ActionState, - }, - BoxableError, -}; -use std::{ - path::{Path, PathBuf}, - time::Duration, -}; +use std::path::{Path, PathBuf}; + use tokio::process::Command; -pub const NIX_VOLUME_MOUNTD_DEST: &str = "/Library/LaunchDaemons/org.nixos.darwin-store.plist"; +use crate::action::StatefulAction; +use crate::execute_command; + +use crate::{ + action::{Action, ActionDescription, ActionState}, + BoxableError, +}; #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] pub struct CreateApfsVolume { disk: PathBuf, name: String, case_sensitive: bool, - encrypt: bool, - create_or_append_synthetic_conf: CreateOrAppendFile, - create_synthetic_objects: CreateSyntheticObjects, - unmount_volume: UnmountVolume, - create_volume: CreateVolume, - create_or_append_fstab: CreateOrAppendFile, - encrypt_volume: Option, - setup_volume_daemon: CreateFile, - bootstrap_volume: BootstrapVolume, - enable_ownership: EnableOwnership, action_state: ActionState, } @@ -43,246 +24,107 @@ impl CreateApfsVolume { disk: impl AsRef, name: String, case_sensitive: bool, - encrypt: bool, - ) -> Result> { - let disk = disk.as_ref(); - let create_or_append_synthetic_conf = CreateOrAppendFile::plan( - "/etc/synthetic.conf", - None, - None, - 0o0655, - "nix\n".into(), /* The newline is required otherwise it segfaults */ - ) - .await - .map_err(|e| e.boxed())?; - - let create_synthetic_objects = CreateSyntheticObjects::plan().await?; - - let unmount_volume = UnmountVolume::plan(disk, name.clone()).await?; - - let create_volume = CreateVolume::plan(disk, name.clone(), case_sensitive).await?; - - let create_or_append_fstab = CreateOrAppendFile::plan( - "/etc/fstab", - None, - None, - 0o0655, - format!("NAME=\"{name}\" /nix apfs rw,noauto,nobrowse,suid,owners"), - ) - .await - .map_err(|e| e.boxed())?; - - let encrypt_volume = if encrypt { - Some(EncryptVolume::plan(disk, &name).await?) - } else { - None - }; - - let name_with_qoutes = format!("\"{name}\""); - let encrypted_command; - let mount_command = if encrypt { - encrypted_command = format!("/usr/bin/security find-generic-password -s {name_with_qoutes} -w | /usr/sbin/diskutil apfs unlockVolume {name_with_qoutes} -mountpoint /nix -stdinpassphrase"); - vec!["/bin/sh", "-c", encrypted_command.as_str()] - } else { - vec![ - "/usr/sbin/diskutil", - "mount", - "-mountPoint", - "/nix", - name.as_str(), - ] - }; - // TODO(@hoverbear): Use plist lib we have in tree... - let mount_plist = format!( - "\ - \n\ - \n\ - \n\ - \n\ - RunAtLoad\n\ - \n\ - Label\n\ - org.nixos.darwin-store\n\ - ProgramArguments\n\ - \n\ - {}\ - \n\ - \n\ - \n\ - \ - ", mount_command.iter().map(|v| format!("{v}\n")).collect::>().join("\n") - ); - let setup_volume_daemon = - CreateFile::plan(NIX_VOLUME_MOUNTD_DEST, None, None, None, mount_plist, false).await?; - - let bootstrap_volume = BootstrapVolume::plan(NIX_VOLUME_MOUNTD_DEST).await?; - let enable_ownership = EnableOwnership::plan("/nix").await?; - + ) -> Result, Box> { Ok(Self { - disk: disk.to_path_buf(), + disk: disk.as_ref().to_path_buf(), name, case_sensitive, - encrypt, - create_or_append_synthetic_conf, - create_synthetic_objects, - unmount_volume, - create_volume, - create_or_append_fstab, - encrypt_volume, - setup_volume_daemon, - bootstrap_volume, - enable_ownership, action_state: ActionState::Uncompleted, - }) + } + .into()) } } #[async_trait::async_trait] -#[typetag::serde(name = "create_apfs_volume")] +#[typetag::serde(name = "create_volume")] impl Action for CreateApfsVolume { fn tracing_synopsis(&self) -> String { format!( - "Create an APFS volume `{}` on `{}`", - self.name, - self.disk.display() + "Create an APFS volume on `{}` named `{}`", + self.disk.display(), + self.name ) } fn execute_description(&self) -> Vec { - let Self { - disk: _, name: _, .. - } = &self; vec![ActionDescription::new(self.tracing_synopsis(), vec![])] } - #[tracing::instrument(skip_all, fields(destination,))] + #[tracing::instrument(skip_all, fields( + disk = %self.disk.display(), + name = %self.name, + case_sensitive = %self.case_sensitive, + ))] async fn execute(&mut self) -> Result<(), Box> { let Self { - disk: _, - name: _, - case_sensitive: _, - encrypt: _, - create_or_append_synthetic_conf, - create_synthetic_objects, - unmount_volume, - create_volume, - create_or_append_fstab, - encrypt_volume, - setup_volume_daemon, - bootstrap_volume, - enable_ownership, + disk, + name, + case_sensitive, action_state: _, } = self; - create_or_append_synthetic_conf.try_execute().await?; - create_synthetic_objects.try_execute().await?; - unmount_volume.try_execute().await.ok(); // We actually expect this may fail. - create_volume.try_execute().await?; - create_or_append_fstab.try_execute().await?; - if let Some(encrypt_volume) = encrypt_volume { - encrypt_volume.try_execute().await?; - } - setup_volume_daemon.try_execute().await?; - - bootstrap_volume.try_execute().await?; - - let mut retry_tokens: usize = 50; - loop { - tracing::trace!(%retry_tokens, "Checking for Nix Store existence"); - let status = Command::new("/usr/sbin/diskutil") - .args(["info", "/nix"]) - .stderr(std::process::Stdio::null()) - .stdout(std::process::Stdio::null()) - .status() - .await - .map_err(|e| CreateApfsVolumeError::Command(e).boxed())?; - if status.success() || retry_tokens == 0 { - break; - } else { - retry_tokens = retry_tokens.saturating_sub(1); - } - tokio::time::sleep(Duration::from_millis(100)).await; - } - - enable_ownership.try_execute().await?; + execute_command( + Command::new("/usr/sbin/diskutil") + .process_group(0) + .args([ + "apfs", + "addVolume", + &format!("{}", disk.display()), + if !*case_sensitive { + "APFS" + } else { + "Case-sensitive APFS" + }, + name, + "-nomount", + ]) + .stdin(std::process::Stdio::null()), + ) + .await + .map_err(|e| CreateVolumeError::Command(e).boxed())?; Ok(()) } fn revert_description(&self) -> Vec { - let Self { disk, name, .. } = &self; vec![ActionDescription::new( - format!("Remove the APFS volume `{name}` on `{}`", disk.display()), - vec![format!( - "Create a writable, persistent systemd system extension.", - )], + format!( + "Remove the volume on `{}` named `{}`", + self.disk.display(), + self.name + ), + vec![], )] } - #[tracing::instrument(skip_all, fields(disk, name))] + #[tracing::instrument(skip_all, fields( + disk = %self.disk.display(), + name = %self.name, + case_sensitive = %self.case_sensitive, + ))] async fn revert(&mut self) -> Result<(), Box> { let Self { disk: _, - name: _, + name, case_sensitive: _, - encrypt: _, - create_or_append_synthetic_conf, - create_synthetic_objects, - unmount_volume, - create_volume, - create_or_append_fstab, - encrypt_volume, - setup_volume_daemon, - bootstrap_volume, - enable_ownership, action_state: _, } = self; - enable_ownership.try_revert().await?; - bootstrap_volume.try_revert().await?; - setup_volume_daemon.try_revert().await?; - if let Some(encrypt_volume) = encrypt_volume { - encrypt_volume.try_revert().await?; - } - create_or_append_fstab.try_revert().await?; - - unmount_volume.try_revert().await?; - create_volume.try_revert().await?; - - // Purposefully not reversed - create_or_append_synthetic_conf.try_revert().await?; - create_synthetic_objects.try_revert().await?; + execute_command( + Command::new("/usr/sbin/diskutil") + .process_group(0) + .args(["apfs", "deleteVolume", name]) + .stdin(std::process::Stdio::null()), + ) + .await + .map_err(|e| CreateVolumeError::Command(e).boxed())?; Ok(()) } - - fn action_state(&self) -> ActionState { - self.action_state - } - - fn set_action_state(&mut self, action_state: ActionState) { - self.action_state = action_state; - } } #[derive(Debug, thiserror::Error)] -pub enum CreateApfsVolumeError { - #[error(transparent)] - CreateFile(#[from] CreateFileError), - #[error(transparent)] - DarwinBootstrapVolume(#[from] BootstrapVolumeError), - #[error(transparent)] - DarwinCreateSyntheticObjects(#[from] CreateSyntheticObjectsError), - #[error(transparent)] - DarwinCreateVolume(#[from] CreateVolumeError), - #[error(transparent)] - DarwinEnableOwnership(#[from] EnableOwnershipError), - #[error(transparent)] - DarwinEncryptVolume(#[from] EncryptVolumeError), - #[error(transparent)] - DarwinUnmountVolume(#[from] UnmountVolumeError), - #[error(transparent)] - CreateOrAppendFile(#[from] CreateOrAppendFileError), +pub enum CreateVolumeError { #[error("Failed to execute command")] Command(#[source] std::io::Error), } diff --git a/src/action/darwin/create_nix_volume.rs b/src/action/darwin/create_nix_volume.rs new file mode 100644 index 0000000..7e102c4 --- /dev/null +++ b/src/action/darwin/create_nix_volume.rs @@ -0,0 +1,277 @@ +use crate::{ + action::{ + base::{CreateFile, CreateFileError, CreateOrAppendFile, CreateOrAppendFileError}, + darwin::{ + BootstrapApfsVolume, BootstrapVolumeError, CreateApfsVolume, CreateSyntheticObjects, + CreateSyntheticObjectsError, CreateVolumeError, EnableOwnership, EnableOwnershipError, + EncryptApfsVolume, EncryptVolumeError, UnmountApfsVolume, UnmountVolumeError, + }, + Action, ActionDescription, StatefulAction, + }, + BoxableError, +}; +use std::{ + path::{Path, PathBuf}, + time::Duration, +}; +use tokio::process::Command; + +pub const NIX_VOLUME_MOUNTD_DEST: &str = "/Library/LaunchDaemons/org.nixos.darwin-store.plist"; + +/// Create an APFS volume +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] +pub struct CreateNixVolume { + disk: PathBuf, + name: String, + case_sensitive: bool, + encrypt: bool, + create_or_append_synthetic_conf: StatefulAction, + create_synthetic_objects: StatefulAction, + unmount_volume: StatefulAction, + create_volume: StatefulAction, + create_or_append_fstab: StatefulAction, + encrypt_volume: Option>, + setup_volume_daemon: StatefulAction, + bootstrap_volume: StatefulAction, + enable_ownership: StatefulAction, +} + +impl CreateNixVolume { + #[tracing::instrument(skip_all)] + pub async fn plan( + disk: impl AsRef, + name: String, + case_sensitive: bool, + encrypt: bool, + ) -> Result, Box> { + let disk = disk.as_ref(); + let create_or_append_synthetic_conf = CreateOrAppendFile::plan( + "/etc/synthetic.conf", + None, + None, + 0o0655, + "nix\n".into(), /* The newline is required otherwise it segfaults */ + ) + .await + .map_err(|e| e.boxed())?; + + let create_synthetic_objects = CreateSyntheticObjects::plan().await?; + + let unmount_volume = UnmountApfsVolume::plan(disk, name.clone()).await?; + + let create_volume = CreateApfsVolume::plan(disk, name.clone(), case_sensitive).await?; + + let create_or_append_fstab = CreateOrAppendFile::plan( + "/etc/fstab", + None, + None, + 0o0655, + format!("NAME=\"{name}\" /nix apfs rw,noauto,nobrowse,suid,owners"), + ) + .await + .map_err(|e| e.boxed())?; + + let encrypt_volume = if encrypt { + Some(EncryptApfsVolume::plan(disk, &name).await?) + } else { + None + }; + + let name_with_qoutes = format!("\"{name}\""); + let encrypted_command; + let mount_command = if encrypt { + encrypted_command = format!("/usr/bin/security find-generic-password -s {name_with_qoutes} -w | /usr/sbin/diskutil apfs unlockVolume {name_with_qoutes} -mountpoint /nix -stdinpassphrase"); + vec!["/bin/sh", "-c", encrypted_command.as_str()] + } else { + vec![ + "/usr/sbin/diskutil", + "mount", + "-mountPoint", + "/nix", + name.as_str(), + ] + }; + // TODO(@hoverbear): Use plist lib we have in tree... + let mount_plist = format!( + "\ + \n\ + \n\ + \n\ + \n\ + RunAtLoad\n\ + \n\ + Label\n\ + org.nixos.darwin-store\n\ + ProgramArguments\n\ + \n\ + {}\ + \n\ + \n\ + \n\ + \ + ", mount_command.iter().map(|v| format!("{v}\n")).collect::>().join("\n") + ); + let setup_volume_daemon = + CreateFile::plan(NIX_VOLUME_MOUNTD_DEST, None, None, None, mount_plist, false).await?; + + let bootstrap_volume = BootstrapApfsVolume::plan(NIX_VOLUME_MOUNTD_DEST).await?; + let enable_ownership = EnableOwnership::plan("/nix").await?; + + Ok(Self { + disk: disk.to_path_buf(), + name, + case_sensitive, + encrypt, + create_or_append_synthetic_conf, + create_synthetic_objects, + unmount_volume, + create_volume, + create_or_append_fstab, + encrypt_volume, + setup_volume_daemon, + bootstrap_volume, + enable_ownership, + } + .into()) + } +} + +#[async_trait::async_trait] +#[typetag::serde(name = "create_apfs_volume")] +impl Action for CreateNixVolume { + fn tracing_synopsis(&self) -> String { + format!( + "Create an APFS volume `{}` for Nix on `{}`", + self.name, + self.disk.display() + ) + } + + fn execute_description(&self) -> Vec { + let Self { + disk: _, name: _, .. + } = &self; + vec![ActionDescription::new(self.tracing_synopsis(), vec![])] + } + + #[tracing::instrument(skip_all, fields(destination,))] + async fn execute(&mut self) -> Result<(), Box> { + let Self { + disk: _, + name: _, + case_sensitive: _, + encrypt: _, + create_or_append_synthetic_conf, + create_synthetic_objects, + unmount_volume, + create_volume, + create_or_append_fstab, + encrypt_volume, + setup_volume_daemon, + bootstrap_volume, + enable_ownership, + } = self; + + create_or_append_synthetic_conf.try_execute().await?; + create_synthetic_objects.try_execute().await?; + unmount_volume.try_execute().await.ok(); // We actually expect this may fail. + create_volume.try_execute().await?; + create_or_append_fstab.try_execute().await?; + if let Some(encrypt_volume) = encrypt_volume { + encrypt_volume.try_execute().await?; + } + setup_volume_daemon.try_execute().await?; + + bootstrap_volume.try_execute().await?; + + let mut retry_tokens: usize = 50; + loop { + tracing::trace!(%retry_tokens, "Checking for Nix Store existence"); + let status = Command::new("/usr/sbin/diskutil") + .args(["info", "/nix"]) + .stderr(std::process::Stdio::null()) + .stdout(std::process::Stdio::null()) + .status() + .await + .map_err(|e| CreateApfsVolumeError::Command(e).boxed())?; + if status.success() || retry_tokens == 0 { + break; + } else { + retry_tokens = retry_tokens.saturating_sub(1); + } + tokio::time::sleep(Duration::from_millis(100)).await; + } + + enable_ownership.try_execute().await?; + + Ok(()) + } + + fn revert_description(&self) -> Vec { + let Self { disk, name, .. } = &self; + vec![ActionDescription::new( + format!("Remove the APFS volume `{name}` on `{}`", disk.display()), + vec![format!( + "Create a writable, persistent systemd system extension.", + )], + )] + } + + #[tracing::instrument(skip_all, fields(disk, name))] + async fn revert(&mut self) -> Result<(), Box> { + let Self { + disk: _, + name: _, + case_sensitive: _, + encrypt: _, + create_or_append_synthetic_conf, + create_synthetic_objects, + unmount_volume, + create_volume, + create_or_append_fstab, + encrypt_volume, + setup_volume_daemon, + bootstrap_volume, + enable_ownership, + } = self; + + enable_ownership.try_revert().await?; + bootstrap_volume.try_revert().await?; + setup_volume_daemon.try_revert().await?; + if let Some(encrypt_volume) = encrypt_volume { + encrypt_volume.try_revert().await?; + } + create_or_append_fstab.try_revert().await?; + + unmount_volume.try_revert().await?; + create_volume.try_revert().await?; + + // Purposefully not reversed + create_or_append_synthetic_conf.try_revert().await?; + create_synthetic_objects.try_revert().await?; + + Ok(()) + } +} + +#[derive(Debug, thiserror::Error)] +pub enum CreateApfsVolumeError { + #[error(transparent)] + CreateFile(#[from] CreateFileError), + #[error(transparent)] + DarwinBootstrapVolume(#[from] BootstrapVolumeError), + #[error(transparent)] + DarwinCreateSyntheticObjects(#[from] CreateSyntheticObjectsError), + #[error(transparent)] + DarwinCreateVolume(#[from] CreateVolumeError), + #[error(transparent)] + DarwinEnableOwnership(#[from] EnableOwnershipError), + #[error(transparent)] + DarwinEncryptVolume(#[from] EncryptVolumeError), + #[error(transparent)] + DarwinUnmountVolume(#[from] UnmountVolumeError), + #[error(transparent)] + CreateOrAppendFile(#[from] CreateOrAppendFileError), + #[error("Failed to execute command")] + Command(#[source] std::io::Error), +} diff --git a/src/action/darwin/create_synthetic_objects.rs b/src/action/darwin/create_synthetic_objects.rs index ed72f91..a34503c 100644 --- a/src/action/darwin/create_synthetic_objects.rs +++ b/src/action/darwin/create_synthetic_objects.rs @@ -2,19 +2,16 @@ use tokio::process::Command; use crate::execute_command; -use crate::action::{Action, ActionDescription, ActionState}; +use crate::action::{Action, ActionDescription, StatefulAction}; +/// Create the synthetic objects defined in `/etc/syntethic.conf` #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] -pub struct CreateSyntheticObjects { - action_state: ActionState, -} +pub struct CreateSyntheticObjects; impl CreateSyntheticObjects { #[tracing::instrument(skip_all)] - pub async fn plan() -> Result> { - Ok(Self { - action_state: ActionState::Uncompleted, - }) + pub async fn plan() -> Result, Box> { + Ok(Self.into()) } } @@ -84,14 +81,6 @@ impl Action for CreateSyntheticObjects { Ok(()) } - - fn action_state(&self) -> ActionState { - self.action_state - } - - fn set_action_state(&mut self, action_state: ActionState) { - self.action_state = action_state; - } } #[derive(Debug, thiserror::Error)] diff --git a/src/action/darwin/create_volume.rs b/src/action/darwin/create_volume.rs deleted file mode 100644 index 01d29e0..0000000 --- a/src/action/darwin/create_volume.rs +++ /dev/null @@ -1,136 +0,0 @@ -use std::path::{Path, PathBuf}; - -use tokio::process::Command; - -use crate::execute_command; - -use crate::{ - action::{Action, ActionDescription, ActionState}, - BoxableError, -}; - -#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] -pub struct CreateVolume { - disk: PathBuf, - name: String, - case_sensitive: bool, - action_state: ActionState, -} - -impl CreateVolume { - #[tracing::instrument(skip_all)] - pub async fn plan( - disk: impl AsRef, - name: String, - case_sensitive: bool, - ) -> Result> { - Ok(Self { - disk: disk.as_ref().to_path_buf(), - name, - case_sensitive, - action_state: ActionState::Uncompleted, - }) - } -} - -#[async_trait::async_trait] -#[typetag::serde(name = "create_volume")] -impl Action for CreateVolume { - fn tracing_synopsis(&self) -> String { - format!( - "Create a volume on `{}` named `{}`", - self.disk.display(), - self.name - ) - } - - fn execute_description(&self) -> Vec { - vec![ActionDescription::new(self.tracing_synopsis(), vec![])] - } - - #[tracing::instrument(skip_all, fields( - disk = %self.disk.display(), - name = %self.name, - case_sensitive = %self.case_sensitive, - ))] - async fn execute(&mut self) -> Result<(), Box> { - let Self { - disk, - name, - case_sensitive, - action_state: _, - } = self; - - execute_command( - Command::new("/usr/sbin/diskutil") - .process_group(0) - .args([ - "apfs", - "addVolume", - &format!("{}", disk.display()), - if !*case_sensitive { - "APFS" - } else { - "Case-sensitive APFS" - }, - name, - "-nomount", - ]) - .stdin(std::process::Stdio::null()), - ) - .await - .map_err(|e| CreateVolumeError::Command(e).boxed())?; - - Ok(()) - } - - fn revert_description(&self) -> Vec { - vec![ActionDescription::new( - format!( - "Remove the volume on `{}` named `{}`", - self.disk.display(), - self.name - ), - vec![], - )] - } - - #[tracing::instrument(skip_all, fields( - disk = %self.disk.display(), - name = %self.name, - case_sensitive = %self.case_sensitive, - ))] - async fn revert(&mut self) -> Result<(), Box> { - let Self { - disk: _, - name, - case_sensitive: _, - action_state: _, - } = self; - - execute_command( - Command::new("/usr/sbin/diskutil") - .process_group(0) - .args(["apfs", "deleteVolume", name]) - .stdin(std::process::Stdio::null()), - ) - .await - .map_err(|e| CreateVolumeError::Command(e).boxed())?; - - Ok(()) - } - - fn action_state(&self) -> ActionState { - self.action_state - } - - fn set_action_state(&mut self, action_state: ActionState) { - self.action_state = action_state; - } -} - -#[derive(Debug, thiserror::Error)] -pub enum CreateVolumeError { - #[error("Failed to execute command")] - Command(#[source] std::io::Error), -} diff --git a/src/action/darwin/enable_ownership.rs b/src/action/darwin/enable_ownership.rs index 07fab29..02c7874 100644 --- a/src/action/darwin/enable_ownership.rs +++ b/src/action/darwin/enable_ownership.rs @@ -3,29 +3,32 @@ use std::path::{Path, PathBuf}; use tokio::process::Command; +use crate::action::StatefulAction; use crate::execute_command; use crate::os::darwin::DiskUtilOutput; use crate::{ - action::{Action, ActionDescription, ActionState}, + action::{Action, ActionDescription}, BoxableError, }; +/** +Enable ownership on a volume + */ #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] pub struct EnableOwnership { path: PathBuf, - action_state: ActionState, } impl EnableOwnership { #[tracing::instrument(skip_all)] pub async fn plan( path: impl AsRef, - ) -> Result> { + ) -> Result, Box> { Ok(Self { path: path.as_ref().to_path_buf(), - action_state: ActionState::Uncompleted, - }) + } + .into()) } } @@ -44,10 +47,7 @@ impl Action for EnableOwnership { path = %self.path.display(), ))] async fn execute(&mut self) -> Result<(), Box> { - let Self { - path, - action_state: _, - } = self; + let Self { path } = self; let should_enable_ownership = { let buf = execute_command( @@ -90,14 +90,6 @@ impl Action for EnableOwnership { // noop Ok(()) } - - fn action_state(&self) -> ActionState { - self.action_state - } - - fn set_action_state(&mut self, action_state: ActionState) { - self.action_state = action_state; - } } #[derive(Debug, thiserror::Error)] diff --git a/src/action/darwin/encrypt_volume.rs b/src/action/darwin/encrypt_apfs_volume.rs similarity index 88% rename from src/action/darwin/encrypt_volume.rs rename to src/action/darwin/encrypt_apfs_volume.rs index 618f970..6b1fe07 100644 --- a/src/action/darwin/encrypt_volume.rs +++ b/src/action/darwin/encrypt_apfs_volume.rs @@ -1,36 +1,38 @@ use crate::{ - action::{darwin::NIX_VOLUME_MOUNTD_DEST, Action, ActionDescription, ActionState}, + action::{darwin::NIX_VOLUME_MOUNTD_DEST, Action, ActionDescription, StatefulAction}, execute_command, }; use rand::Rng; use std::path::{Path, PathBuf}; use tokio::process::Command; +/** +Encrypt an APFS volume + */ #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] -pub struct EncryptVolume { +pub struct EncryptApfsVolume { disk: PathBuf, name: String, - action_state: ActionState, } -impl EncryptVolume { +impl EncryptApfsVolume { #[tracing::instrument(skip_all)] pub async fn plan( disk: impl AsRef, name: impl AsRef, - ) -> Result> { + ) -> Result, Box> { let name = name.as_ref().to_owned(); Ok(Self { name, disk: disk.as_ref().to_path_buf(), - action_state: ActionState::Uncompleted, - }) + } + .into()) } } #[async_trait::async_trait] #[typetag::serde(name = "encrypt_volume")] -impl Action for EncryptVolume { +impl Action for EncryptApfsVolume { fn tracing_synopsis(&self) -> String { format!( "Encrypt volume `{}` on disk `{}`", @@ -47,11 +49,7 @@ impl Action for EncryptVolume { disk = %self.disk.display(), ))] async fn execute(&mut self) -> Result<(), Box> { - let Self { - disk, - name, - action_state: _, - } = self; + let Self { disk, name } = self; // Generate a random password. let password: String = { @@ -141,11 +139,7 @@ impl Action for EncryptVolume { disk = %self.disk.display(), ))] async fn revert(&mut self) -> Result<(), Box> { - let Self { - disk, - name, - action_state: _, - } = self; + let Self { disk, name } = self; let disk_str = disk.to_str().expect("Could not turn disk into string"); /* Should not reasonably ever fail */ @@ -172,14 +166,6 @@ impl Action for EncryptVolume { Ok(()) } - - fn action_state(&self) -> ActionState { - self.action_state - } - - fn set_action_state(&mut self, action_state: ActionState) { - self.action_state = action_state; - } } #[derive(Debug, thiserror::Error)] diff --git a/src/action/darwin/kickstart_launchctl_service.rs b/src/action/darwin/kickstart_launchctl_service.rs index 18a960c..8d8cf9c 100644 --- a/src/action/darwin/kickstart_launchctl_service.rs +++ b/src/action/darwin/kickstart_launchctl_service.rs @@ -1,25 +1,27 @@ use tokio::process::Command; +use crate::action::StatefulAction; use crate::execute_command; use crate::{ - action::{Action, ActionDescription, ActionState}, + action::{Action, ActionDescription}, BoxableError, }; +/** +Kickstart a `launchctl` service + */ #[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, - }) + pub async fn plan( + unit: String, + ) -> Result, Box> { + Ok(Self { unit }.into()) } } @@ -39,10 +41,7 @@ impl Action for KickstartLaunchctlService { unit = %self.unit, ))] async fn execute(&mut self) -> Result<(), Box> { - let Self { - unit, - action_state: _, - } = self; + let Self { unit } = self; execute_command( Command::new("launchctl") @@ -69,14 +68,6 @@ impl Action for KickstartLaunchctlService { // noop Ok(()) } - - fn action_state(&self) -> ActionState { - self.action_state - } - - fn set_action_state(&mut self, action_state: ActionState) { - self.action_state = action_state; - } } #[derive(Debug, thiserror::Error)] diff --git a/src/action/darwin/mod.rs b/src/action/darwin/mod.rs index bc3a525..fbfeee7 100644 --- a/src/action/darwin/mod.rs +++ b/src/action/darwin/mod.rs @@ -1,17 +1,20 @@ -mod bootstrap_volume; -mod create_apfs_volume; -mod create_synthetic_objects; -mod create_volume; -mod enable_ownership; -mod encrypt_volume; -mod kickstart_launchctl_service; -mod unmount_volume; +/*! [`Action`](crate::action::Action)s for Darwin based systems +*/ -pub use bootstrap_volume::{BootstrapVolume, BootstrapVolumeError}; -pub use create_apfs_volume::{CreateApfsVolume, CreateApfsVolumeError, NIX_VOLUME_MOUNTD_DEST}; +mod bootstrap_apfs_volume; +mod create_apfs_volume; +mod create_nix_volume; +mod create_synthetic_objects; +mod enable_ownership; +mod encrypt_apfs_volume; +mod kickstart_launchctl_service; +mod unmount_apfs_volume; + +pub use bootstrap_apfs_volume::{BootstrapApfsVolume, BootstrapVolumeError}; +pub use create_apfs_volume::{CreateApfsVolume, CreateVolumeError}; +pub use create_nix_volume::{CreateApfsVolumeError, CreateNixVolume, NIX_VOLUME_MOUNTD_DEST}; pub use create_synthetic_objects::{CreateSyntheticObjects, CreateSyntheticObjectsError}; -pub use create_volume::{CreateVolume, CreateVolumeError}; pub use enable_ownership::{EnableOwnership, EnableOwnershipError}; -pub use encrypt_volume::{EncryptVolume, EncryptVolumeError}; +pub use encrypt_apfs_volume::{EncryptApfsVolume, EncryptVolumeError}; pub use kickstart_launchctl_service::{KickstartLaunchctlService, KickstartLaunchctlServiceError}; -pub use unmount_volume::{UnmountVolume, UnmountVolumeError}; +pub use unmount_apfs_volume::{UnmountApfsVolume, UnmountVolumeError}; diff --git a/src/action/darwin/unmount_volume.rs b/src/action/darwin/unmount_apfs_volume.rs similarity index 71% rename from src/action/darwin/unmount_volume.rs rename to src/action/darwin/unmount_apfs_volume.rs index a3ddc9f..c7b9ed2 100644 --- a/src/action/darwin/unmount_volume.rs +++ b/src/action/darwin/unmount_apfs_volume.rs @@ -2,40 +2,39 @@ use std::path::{Path, PathBuf}; use tokio::process::Command; +use crate::action::StatefulAction; use crate::execute_command; use crate::{ - action::{Action, ActionDescription, ActionState}, + action::{Action, ActionDescription}, BoxableError, }; +/** +Unmount an APFS volume + */ #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] -pub struct UnmountVolume { +pub struct UnmountApfsVolume { disk: PathBuf, name: String, - action_state: ActionState, } -impl UnmountVolume { +impl UnmountApfsVolume { #[tracing::instrument(skip_all)] pub async fn plan( disk: impl AsRef, name: String, - ) -> Result> { + ) -> Result, Box> { let disk = disk.as_ref().to_owned(); - Ok(Self { - disk, - name, - action_state: ActionState::Uncompleted, - }) + Ok(Self { disk, name }.into()) } } #[async_trait::async_trait] #[typetag::serde(name = "unmount_volume")] -impl Action for UnmountVolume { +impl Action for UnmountApfsVolume { fn tracing_synopsis(&self) -> String { - format!("Unmount the `{}` volume", self.name) + format!("Unmount the `{}` APFS volume", self.name) } fn execute_description(&self) -> Vec { @@ -47,11 +46,7 @@ impl Action for UnmountVolume { name = %self.name, ))] async fn execute(&mut self) -> Result<(), Box> { - let Self { - disk: _, - name, - action_state: _, - } = self; + let Self { disk: _, name } = self; execute_command( Command::new("/usr/sbin/diskutil") @@ -75,11 +70,7 @@ impl Action for UnmountVolume { name = %self.name, ))] async fn revert(&mut self) -> Result<(), Box> { - let Self { - disk: _, - name, - action_state: _, - } = self; + let Self { disk: _, name } = self; execute_command( Command::new("/usr/sbin/diskutil") @@ -93,14 +84,6 @@ impl Action for UnmountVolume { Ok(()) } - - fn action_state(&self) -> ActionState { - self.action_state - } - - fn set_action_state(&mut self, action_state: ActionState) { - self.action_state = action_state; - } } #[derive(Debug, thiserror::Error)] diff --git a/src/action/base/configure_nix_daemon_service.rs b/src/action/linux/configure_nix_daemon_service.rs similarity index 95% rename from src/action/base/configure_nix_daemon_service.rs rename to src/action/linux/configure_nix_daemon_service.rs index dd9c0da..362cb02 100644 --- a/src/action/base/configure_nix_daemon_service.rs +++ b/src/action/linux/configure_nix_daemon_service.rs @@ -4,10 +4,11 @@ use target_lexicon::OperatingSystem; use tokio::fs::remove_file; use tokio::process::Command; +use crate::action::StatefulAction; use crate::execute_command; use crate::{ - action::{Action, ActionDescription, ActionState}, + action::{Action, ActionDescription}, BoxableError, }; @@ -17,14 +18,15 @@ const TMPFILES_SRC: &str = "/nix/var/nix/profiles/default//lib/tmpfiles.d/nix-da const TMPFILES_DEST: &str = "/etc/tmpfiles.d/nix-daemon.conf"; const DARWIN_NIX_DAEMON_DEST: &str = "/Library/LaunchDaemons/org.nixos.nix-daemon.plist"; +/** +Run systemd utilities to configure the Nix daemon +*/ #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] -pub struct ConfigureNixDaemonService { - action_state: ActionState, -} +pub struct ConfigureNixDaemonService {} impl ConfigureNixDaemonService { #[tracing::instrument(skip_all)] - pub async fn plan() -> Result> { + pub async fn plan() -> Result, Box> { match OperatingSystem::host() { OperatingSystem::MacOSX { major: _, @@ -39,9 +41,7 @@ impl ConfigureNixDaemonService { }, }; - Ok(Self { - action_state: ActionState::Uncompleted, - }) + Ok(Self {}.into()) } } @@ -65,7 +65,7 @@ impl Action for ConfigureNixDaemonService { #[tracing::instrument(skip_all)] async fn execute(&mut self) -> Result<(), Box> { - let Self { action_state: _ } = self; + let Self {} = self; match OperatingSystem::host() { OperatingSystem::MacOSX { @@ -274,14 +274,6 @@ impl Action for ConfigureNixDaemonService { Ok(()) } - - fn action_state(&self) -> ActionState { - self.action_state - } - - fn set_action_state(&mut self, action_state: ActionState) { - self.action_state = action_state; - } } #[derive(Debug, thiserror::Error)] diff --git a/src/action/linux/create_systemd_sysext.rs b/src/action/linux/create_systemd_sysext.rs index 4d9a634..740ec14 100644 --- a/src/action/linux/create_systemd_sysext.rs +++ b/src/action/linux/create_systemd_sysext.rs @@ -1,6 +1,7 @@ use crate::action::base::{CreateDirectory, CreateDirectoryError, CreateFile, CreateFileError}; +use crate::action::StatefulAction; use crate::{ - action::{Action, ActionDescription, ActionImplementation, ActionState}, + action::{Action, ActionDescription}, BoxableError, }; use std::path::{Path, PathBuf}; @@ -16,17 +17,16 @@ const PATHS: &[&str] = &[ #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] pub struct CreateSystemdSysext { destination: PathBuf, - create_directories: Vec, - create_extension_release: CreateFile, - create_bind_mount_unit: CreateFile, - action_state: ActionState, + create_directories: Vec>, + create_extension_release: StatefulAction, + create_bind_mount_unit: StatefulAction, } impl CreateSystemdSysext { #[tracing::instrument(skip_all)] pub async fn plan( destination: impl AsRef, - ) -> Result> { + ) -> Result, Box> { let destination = destination.as_ref(); let mut create_directories = @@ -85,8 +85,8 @@ impl CreateSystemdSysext { create_directories, create_extension_release, create_bind_mount_unit, - action_state: ActionState::Uncompleted, - }) + } + .into()) } } @@ -113,7 +113,6 @@ impl Action for CreateSystemdSysext { async fn execute(&mut self) -> Result<(), Box> { let Self { destination: _, - action_state: _, create_directories, create_extension_release, create_bind_mount_unit, @@ -142,7 +141,6 @@ impl Action for CreateSystemdSysext { async fn revert(&mut self) -> Result<(), Box> { let Self { destination: _, - action_state: _, create_directories, create_extension_release, create_bind_mount_unit, @@ -158,14 +156,6 @@ impl Action for CreateSystemdSysext { Ok(()) } - - fn action_state(&self) -> ActionState { - self.action_state - } - - fn set_action_state(&mut self, action_state: ActionState) { - self.action_state = action_state; - } } #[derive(Debug, thiserror::Error)] diff --git a/src/action/linux/mod.rs b/src/action/linux/mod.rs index 0211b9d..dfab38f 100644 --- a/src/action/linux/mod.rs +++ b/src/action/linux/mod.rs @@ -1,5 +1,9 @@ +//! [`Action`](crate::action::Action)s for Linux based systems + +mod configure_nix_daemon_service; mod create_systemd_sysext; mod start_systemd_unit; +pub use configure_nix_daemon_service::{ConfigureNixDaemonService, ConfigureNixDaemonServiceError}; pub use create_systemd_sysext::{CreateSystemdSysext, CreateSystemdSysextError}; pub use start_systemd_unit::{StartSystemdUnit, StartSystemdUnitError}; diff --git a/src/action/linux/start_systemd_unit.rs b/src/action/linux/start_systemd_unit.rs index b3e9155..6687d77 100644 --- a/src/action/linux/start_systemd_unit.rs +++ b/src/action/linux/start_systemd_unit.rs @@ -1,25 +1,27 @@ use tokio::process::Command; +use crate::action::StatefulAction; use crate::execute_command; use crate::{ - action::{Action, ActionDescription, ActionState}, + action::{Action, ActionDescription}, BoxableError, }; +/** +Start a given systemd unit + */ #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] pub struct StartSystemdUnit { unit: String, - action_state: ActionState, } impl StartSystemdUnit { #[tracing::instrument(skip_all)] - pub async fn plan(unit: String) -> Result> { - Ok(Self { - unit, - action_state: ActionState::Uncompleted, - }) + pub async fn plan( + unit: String, + ) -> Result, Box> { + Ok(Self { unit }.into()) } } @@ -91,14 +93,6 @@ impl Action for StartSystemdUnit { Ok(()) } - - fn action_state(&self) -> ActionState { - self.action_state - } - - fn set_action_state(&mut self, action_state: ActionState) { - self.action_state = action_state; - } } #[derive(Debug, thiserror::Error)] diff --git a/src/action/linux/systemd_sysext_merge.rs b/src/action/linux/systemd_sysext_merge.rs index 7be2869..9511725 100644 --- a/src/action/linux/systemd_sysext_merge.rs +++ b/src/action/linux/systemd_sysext_merge.rs @@ -9,16 +9,12 @@ use tokio::process::Command; #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] pub struct SystemdSysextMerge { device: PathBuf, - action_state: ActionState, } impl SystemdSysextMerge { #[tracing::instrument(skip_all)] pub async fn plan(device: PathBuf) -> Result { - Ok(Self { - device, - action_state: ActionState::Uncompleted, - }) + Ok(Self { device }) } } @@ -68,10 +64,7 @@ impl Action for SystemdSysextMerge { device = %self.device.display(), ))] async fn revert(&mut self) -> Result<(), Box> { - let Self { - device, - action_state, - } = self; + let Self { device } = self; // TODO(@Hoverbear): Handle proxy vars execute_command( @@ -86,14 +79,6 @@ impl Action for SystemdSysextMerge { Ok(()) } - - fn action_state(&self) -> ActionState { - self.action_state - } - - fn set_action_state(&mut self, action_state: ActionState) { - self.action_state = action_state; - } } #[derive(Debug, thiserror::Error)] diff --git a/src/action/mod.rs b/src/action/mod.rs index 8276f32..a4afa06 100644 --- a/src/action/mod.rs +++ b/src/action/mod.rs @@ -1,99 +1,224 @@ +/*! An executable or revertable step, possibly orcestrating sub-[`Action`]s using things like + [`JoinSet`](tokio::task::JoinSet)s + + +[`Action`]s should be considered an 'atom' of change. Typically they are either a 'base' or +a 'composite' [`Action`]. + +Base actions are things like: + +* [`CreateDirectory`](base::CreateDirectory) +* [`CreateFile`](base::CreateFile) +* [`CreateUser`](base::CreateUser) + +Composite actions are things like: + +* [`CreateNixTree`](common::CreateNixTree) +* [`CreateUsersAndGroups`](common::CreateUsersAndGroups) + +During their `plan` phase, [`Planner`](crate::planner::Planner)s call an [`Action`]s `plan` function, which may accept any +arguments. For example, several 'composite' actions accept a [`CommonSettings`](crate::settings::CommonSettings). Later, the +[`InstallPlan`](crate::InstallPlan) will call [`try_execute`](StatefulAction::try_execute) on the [`StatefulAction`]. + +You can manually plan, execute, then revert an [`Action`] like so: + +```rust,no_run +# async fn wrapper() -> Result<(), harmonic::HarmonicError> { +use harmonic::action::base::CreateDirectory; +let mut action = CreateDirectory::plan("/nix", None, None, 0o0755, true).await?; +action.try_execute().await?; +action.try_revert().await?; +# Ok(()) +# } +``` + +A general guidance for what determines how fine-grained an [`Action`] should be is the unit of +reversion. The [`ConfigureNixDaemonService`](linux::ConfigureNixDaemonService) action is a good +example of this,it takes several steps, such as running `systemd-tmpfiles`, and calling +`systemctl link` on some systemd units. + +Where possible, tasks which could break during execution should be broken up, as uninstalling/installing +step detection is determined by the wrapping [`StatefulAction`]. If an [`Action`] is a 'composite' +its sub-[`Action`]s can be reverted piece-by-piece. So breaking up actions into faillable units is +ideal. + +A custom [`Action`] can be created then used in a custom [`Planner`](crate::planner::Planner): + +```rust,no_run +use std::{error::Error, collections::HashMap}; +use harmonic::{ + InstallPlan, + settings::{CommonSettings, InstallSettingsError}, + planner::{Planner, PlannerError, specific::SteamDeck}, + action::{Action, StatefulAction, ActionDescription}, +}; + +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] +pub struct MyAction {} + + +impl MyAction { + #[tracing::instrument(skip_all)] + pub async fn plan() -> Result, Box> { + Ok(Self {}.into()) + } +} + + +#[async_trait::async_trait] +#[typetag::serde(name = "my_action")] +impl Action for MyAction { + fn tracing_synopsis(&self) -> String { + "My action".to_string() + } + + fn execute_description(&self) -> Vec { + vec![ActionDescription::new(self.tracing_synopsis(), vec![])] + } + + #[tracing::instrument(skip_all, fields( + // Tracing fields... + ))] + async fn execute(&mut self) -> Result<(), Box> { + // Execute steps ... + Ok(()) + } + + fn revert_description(&self) -> Vec { + vec![ActionDescription::new(self.tracing_synopsis(), vec![])] + } + + #[tracing::instrument(skip_all, fields( + // Tracing fields... + ))] + async fn revert(&mut self) -> Result<(), Box> { + // Revert steps... + Ok(()) + } +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct MyPlanner { + pub common: CommonSettings, +} + + +#[async_trait::async_trait] +#[typetag::serde(name = "my-planner")] +impl Planner for MyPlanner { + async fn default() -> Result { + Ok(Self { + common: CommonSettings::default()?, + }) + } + + async fn plan(&self) -> Result>>, PlannerError> { + Ok(vec![ + // ... + MyAction::plan() + .await + .map_err(PlannerError::Action)?.boxed(), + ]) + } + + fn settings(&self) -> Result, InstallSettingsError> { + let Self { common } = self; + let mut map = std::collections::HashMap::default(); + + map.extend(common.settings()?.into_iter()); + + Ok(map) + } +} + +# async fn custom_planner_install() -> color_eyre::Result<()> { +let planner = MyPlanner::default().await?; +let mut plan = InstallPlan::plan(planner).await?; +match plan.install(None).await { + Ok(()) => tracing::info!("Done"), + Err(e) => { + match e.source() { + Some(source) => tracing::error!("{e}: {}", source), + None => tracing::error!("{e}"), + }; + plan.uninstall(None).await?; + }, +}; + +# Ok(()) +# } +``` + +*/ + pub mod base; pub mod common; pub mod darwin; pub mod linux; +mod stateful; -use serde::{Deserialize, Serialize}; +pub use stateful::{ActionState, StatefulAction}; /// An action which can be reverted or completed, with an action state /// -/// This trait interacts with [`ActionImplementation`] which does the [`ActionState`] manipulation and provides some tracing facilities. +/// This trait interacts with [`StatefulAction`] which does the [`ActionState`] manipulation and provides some tracing facilities. /// -/// Instead of calling [`execute`][Action::execute] or [`revert`][Action::revert], you should prefer [`try_execute`][ActionImplementation::try_execute] and [`try_revert`][ActionImplementation::try_revert] +/// Instead of calling [`execute`][Action::execute] or [`revert`][Action::revert], you should prefer [`try_execute`][StatefulAction::try_execute] and [`try_revert`][StatefulAction::try_revert] #[async_trait::async_trait] #[typetag::serde(tag = "action")] pub trait Action: Send + Sync + std::fmt::Debug + dyn_clone::DynClone { + /// A synopsis of the action for tracing purposes fn tracing_synopsis(&self) -> String; + /// A description of what this action would do during execution + /// + /// If this action calls sub-[`Action`]s, care should be taken to use [`StatefulAction::describe_execute`] on those actions, not [`execute_description`][Action::execute_description]. + /// + /// This is called by [`InstallPlan::describe_install`](crate::InstallPlan::describe_install) through [`StatefulAction::describe_execute`] which will skip output if the action is completed. fn execute_description(&self) -> Vec; + /// A description of what this action would do during revert + /// + /// If this action calls sub-[`Action`]s, care should be taken to use [`StatefulAction::describe_revert`] on those actions, not [`revert_description`][Action::revert_description]. + /// + /// This is called by [`InstallPlan::describe_uninstall`](crate::InstallPlan::describe_uninstall) through [`StatefulAction::describe_revert`] which will skip output if the action is completed. fn revert_description(&self) -> Vec; - /// Instead of calling [`execute`][Action::execute], you should prefer [`try_execute`][ActionImplementation::try_execute], so [`ActionState`] is handled correctly and tracing is done. + /// Perform any execution steps + /// + /// If this action calls sub-[`Action`]s, care should be taken to call [`try_execute`][StatefulAction::try_execute], not [`execute`][Action::execute], so that [`ActionState`] is handled correctly and tracing is done. + /// + /// This is called by [`InstallPlan::install`](crate::InstallPlan::install) through [`StatefulAction::try_execute`] which handles tracing as well as if the action needs to execute based on its `action_state`. async fn execute(&mut self) -> Result<(), Box>; - /// Instead of calling [`revert`][Action::revert], you should prefer [`try_revert`][ActionImplementation::try_revert], so [`ActionState`] is handled correctly and tracing is done. + /// Perform any revert steps + /// + /// If this action calls sub-[`Action`]s, care should be taken to call [`try_revert`][StatefulAction::try_revert], not [`revert`][Action::revert], so that [`ActionState`] is handled correctly and tracing is done. + /// + /// /// This is called by [`InstallPlan::uninstall`](crate::InstallPlan::uninstall) through [`StatefulAction::try_revert`] which handles tracing as well as if the action needs to revert based on its `action_state`. async fn revert(&mut self) -> Result<(), Box>; - fn action_state(&self) -> ActionState; - fn set_action_state(&mut self, new_state: ActionState); - // They should also have an `async fn plan(args...) -> Result, Box>;` + fn stateful(self) -> StatefulAction + where + Self: Sized, + { + StatefulAction { + action: self, + state: ActionState::Uncompleted, + } + } + // They should also have an `async fn plan(args...) -> Result, Box>;` } -/// The main wrapper around [`Action`], handling [`ActionState`] and tracing. -#[async_trait::async_trait] -pub trait ActionImplementation: Action { - fn describe_execute(&self) -> Vec { - if self.action_state() == ActionState::Completed { - return vec![]; - } - return self.execute_description(); - } - fn describe_revert(&self) -> Vec { - if self.action_state() == ActionState::Uncompleted { - return vec![]; - } - return self.revert_description(); - } - - /// You should prefer this ([`try_execute`][ActionImplementation::try_execute]) over [`execute`][Action::execute] as it handles [`ActionState`] and does tracing. - async fn try_execute(&mut self) -> Result<(), Box> { - if self.action_state() == ActionState::Completed { - tracing::trace!("Completed: (Already done) {}", self.tracing_synopsis()); - return Ok(()); - } - self.set_action_state(ActionState::Progress); - tracing::debug!("Executing: {}", self.tracing_synopsis()); - self.execute().await?; - self.set_action_state(ActionState::Completed); - tracing::debug!("Completed: {}", self.tracing_synopsis()); - Ok(()) - } - - /// You should prefer this ([`try_revert`][ActionImplementation::try_revert]) over [`revert`][Action::revert] as it handles [`ActionState`] and does tracing. - async fn try_revert(&mut self) -> Result<(), Box> { - if self.action_state() == ActionState::Uncompleted { - tracing::trace!("Reverted: (Already done) {}", self.tracing_synopsis()); - return Ok(()); - } - self.set_action_state(ActionState::Progress); - tracing::debug!("Reverting: {}", self.tracing_synopsis()); - self.revert().await?; - tracing::debug!("Reverted: {}", self.tracing_synopsis()); - self.set_action_state(ActionState::Uncompleted); - Ok(()) - } -} - -impl ActionImplementation for dyn Action {} - -impl ActionImplementation for A where A: Action {} - dyn_clone::clone_trait_object!(Action); -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Copy)] -pub enum ActionState { - Completed, - // Only applicable to meta-actions that start multiple sub-actions. - Progress, - Uncompleted, -} - +/** +A description of an [`Action`](crate::action::Action), intended for humans to review +*/ #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] - pub struct ActionDescription { pub description: String, pub explanation: Vec, } impl ActionDescription { - fn new(description: String, explanation: Vec) -> Self { + pub fn new(description: String, explanation: Vec) -> Self { Self { description, explanation, diff --git a/src/action/stateful.rs b/src/action/stateful.rs new file mode 100644 index 0000000..384f9c7 --- /dev/null +++ b/src/action/stateful.rs @@ -0,0 +1,168 @@ +use serde::{Deserialize, Serialize}; + +use super::{Action, ActionDescription}; + +/// A wrapper around an [`Action`](crate::action::Action) which tracks the [`ActionState`] and +/// handles some tracing output +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] +pub struct StatefulAction { + pub(crate) action: A, + pub(crate) state: ActionState, +} + +impl From for StatefulAction +where + A: Action, +{ + fn from(action: A) -> Self { + Self { + action, + state: ActionState::Uncompleted, + } + } +} + +impl StatefulAction> { + /// A description of what this action would do during execution + pub fn describe_execute(&self) -> Vec { + if self.state == ActionState::Completed { + return vec![]; + } + return self.action.execute_description(); + } + /// A description of what this action would do during revert + pub fn describe_revert(&self) -> Vec { + if self.state == ActionState::Uncompleted { + return vec![]; + } + return self.action.revert_description(); + } + /// Perform any execution steps + /// + /// You should prefer this ([`try_execute`][StatefulAction::try_execute]) over [`execute`][Action::execute] as it handles [`ActionState`] and does tracing + pub async fn try_execute(&mut self) -> Result<(), Box> { + if self.state == ActionState::Completed { + tracing::trace!( + "Completed: (Already done) {}", + self.action.tracing_synopsis() + ); + return Ok(()); + } + self.state = ActionState::Progress; + tracing::debug!("Executing: {}", self.action.tracing_synopsis()); + self.action.execute().await?; + self.state = ActionState::Completed; + tracing::debug!("Completed: {}", self.action.tracing_synopsis()); + Ok(()) + } + /// Perform any revert steps + /// + /// You should prefer this ([`try_revert`][StatefulAction::try_revert]) over [`revert`][Action::revert] as it handles [`ActionState`] and does tracing + pub async fn try_revert(&mut self) -> Result<(), Box> { + if self.state == ActionState::Uncompleted { + tracing::trace!( + "Reverted: (Already done) {}", + self.action.tracing_synopsis() + ); + return Ok(()); + } + self.state = ActionState::Progress; + tracing::debug!("Reverting: {}", self.action.tracing_synopsis()); + self.action.revert().await?; + tracing::debug!("Reverted: {}", self.action.tracing_synopsis()); + self.state = ActionState::Uncompleted; + Ok(()) + } +} + +impl StatefulAction +where + A: Action, +{ + pub fn inner(&self) -> &A { + &self.action + } + + pub fn boxed(self) -> StatefulAction> + where + Self: 'static, + { + StatefulAction { + action: Box::new(self.action), + state: self.state, + } + } + /// A description of what this action would do during execution + pub fn describe_execute(&self) -> Vec { + if self.state == ActionState::Completed { + return vec![]; + } + return self.action.execute_description(); + } + /// A description of what this action would do during revert + pub fn describe_revert(&self) -> Vec { + if self.state == ActionState::Uncompleted { + return vec![]; + } + return self.action.revert_description(); + } + /// Perform any execution steps + /// + /// You should prefer this ([`try_execute`][StatefulAction::try_execute]) over [`execute`][Action::execute] as it handles [`ActionState`] and does tracing + pub async fn try_execute(&mut self) -> Result<(), Box> { + if self.state == ActionState::Completed { + tracing::trace!( + "Completed: (Already done) {}", + self.action.tracing_synopsis() + ); + return Ok(()); + } + self.state = ActionState::Progress; + tracing::debug!("Executing: {}", self.action.tracing_synopsis()); + self.action.execute().await?; + self.state = ActionState::Completed; + tracing::debug!("Completed: {}", self.action.tracing_synopsis()); + Ok(()) + } + /// Perform any revert steps + /// + /// You should prefer this ([`try_revert`][StatefulAction::try_revert]) over [`revert`][Action::revert] as it handles [`ActionState`] and does tracing + pub async fn try_revert(&mut self) -> Result<(), Box> { + if self.state == ActionState::Uncompleted { + tracing::trace!( + "Reverted: (Already done) {}", + self.action.tracing_synopsis() + ); + return Ok(()); + } + self.state = ActionState::Progress; + tracing::debug!("Reverting: {}", self.action.tracing_synopsis()); + self.action.revert().await?; + tracing::debug!("Reverted: {}", self.action.tracing_synopsis()); + self.state = ActionState::Uncompleted; + Ok(()) + } +} + +/** The state of an [`Action`](crate::action::Action) +*/ +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Copy)] +pub enum ActionState { + /** + If [`Completed`](ActionState::Completed) an [`Action`](crate::action::Action) will be skipped + on [`InstallPlan::install`](crate::InstallPlan::install), and reverted on [`InstallPlan::uninstall`](crate::InstallPlan::uninstall) + */ + Completed, + /** + If [`Progress`](ActionState::Progress) an [`Action`](crate::action::Action) will be run on + [`InstallPlan::install`](crate::InstallPlan::install) and [`InstallPlan::uninstall`](crate::InstallPlan::uninstall) + + Only applicable to meta-actions that contain other multiple sub-actions. + */ + Progress, + /** + If [`Completed`](ActionState::Completed) an [`Action`](crate::action::Action) will be skipped + on [`InstallPlan::uninstall`](crate::InstallPlan::uninstall) and executed on [`InstallPlan::install`](crate::InstallPlan::install) + */ + Uncompleted, +} diff --git a/src/main.rs b/src/bin/harmonic.rs similarity index 100% rename from src/main.rs rename to src/bin/harmonic.rs diff --git a/src/channel_value.rs b/src/channel_value.rs index ba4d94c..addc85d 100644 --- a/src/channel_value.rs +++ b/src/channel_value.rs @@ -1,9 +1,13 @@ use reqwest::Url; use serde::{Deserialize, Serialize}; +/** +A pair of [`String`] and [`Url`] destined for the list of subscribed channels for [`nix-channel`](https://nixos.org/manual/nix/stable/command-ref/nix-channel.html) +*/ #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ChannelValue(pub String, pub Url); +#[cfg(feature = "cli")] impl clap::builder::ValueParserFactory for ChannelValue { type Parser = ChannelValueParser; fn value_parser() -> Self::Parser { @@ -19,6 +23,8 @@ impl From<(String, Url)> for ChannelValue { #[derive(Clone, Debug)] pub struct ChannelValueParser; + +#[cfg(feature = "cli")] impl clap::builder::TypedValueParser for ChannelValueParser { type Value = ChannelValue; diff --git a/src/interaction.rs b/src/cli/interaction.rs similarity index 100% rename from src/interaction.rs rename to src/cli/interaction.rs diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 18d5b76..3ae2f2f 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -1,4 +1,9 @@ +/*! CLI argument structures and utilities + +*/ + pub(crate) mod arg; +mod interaction; pub(crate) mod subcommand; use clap::Parser; diff --git a/src/cli/subcommand/install.rs b/src/cli/subcommand/install.rs index 2d58388..1aceeda 100644 --- a/src/cli/subcommand/install.rs +++ b/src/cli/subcommand/install.rs @@ -4,8 +4,11 @@ use std::{ }; use crate::{ - action::ActionState, cli::is_root, cli::signal_channel, cli::CommandExecute, interaction, - plan::RECEIPT_LOCATION, BuiltinPlanner, InstallPlan, Planner, + action::ActionState, + cli::{interaction, is_root, signal_channel, CommandExecute}, + plan::RECEIPT_LOCATION, + planner::Planner, + BuiltinPlanner, InstallPlan, }; use clap::{ArgAction, Parser}; use eyre::{eyre, WrapErr}; @@ -77,13 +80,13 @@ impl CommandExecute for Install { if existing_receipt.planner.settings().map_err(|e| eyre!(e))? != chosen_planner.settings().map_err(|e| eyre!(e))? { return Err(eyre!("Found existing plan in `{RECEIPT_LOCATION}` which used different planner settings, try uninstalling the existing install")) } - if existing_receipt.actions.iter().all(|v| v.action_state() == ActionState::Completed) { + if existing_receipt.actions.iter().all(|v| v.state == ActionState::Completed) { return Err(eyre!("Found existing plan in `{RECEIPT_LOCATION}`, with the same settings, already completed, try uninstalling and reinstalling if Nix isn't working")) } existing_receipt } , None => { - InstallPlan::plan(planner.boxed()).await.map_err(|e| eyre!(e))? + planner.plan().await.map_err(|e| eyre!(e))? }, } }, @@ -97,7 +100,7 @@ impl CommandExecute for Install { let builtin_planner = BuiltinPlanner::default() .await .map_err(|e| eyre::eyre!(e))?; - InstallPlan::plan(builtin_planner.boxed()).await.map_err(|e| eyre!(e))? + builtin_planner.plan().await.map_err(|e| eyre!(e))? }, (Some(_), Some(_)) => return Err(eyre!("`--plan` conflicts with passing a planner, a planner creates plans, so passing an existing plan doesn't make sense")), }; @@ -105,7 +108,7 @@ impl CommandExecute for Install { if !no_confirm { if !interaction::confirm( install_plan - .describe_execute(explain) + .describe_install(explain) .map_err(|e| eyre!(e))?, ) .await? @@ -122,7 +125,7 @@ impl CommandExecute for Install { tracing::error!("{:?}", error); if !interaction::confirm( install_plan - .describe_revert(explain) + .describe_uninstall(explain) .map_err(|e| eyre!(e))?, ) .await? @@ -130,7 +133,7 @@ impl CommandExecute for Install { interaction::clean_exit_with_message("Okay, didn't do anything! Bye!").await; } let rx2 = tx.subscribe(); - install_plan.revert(rx2).await? + install_plan.uninstall(rx2).await? } else { return Err(error); } diff --git a/src/cli/subcommand/uninstall.rs b/src/cli/subcommand/uninstall.rs index 02fc5f2..548e355 100644 --- a/src/cli/subcommand/uninstall.rs +++ b/src/cli/subcommand/uninstall.rs @@ -8,7 +8,7 @@ use crate::{ use clap::{ArgAction, Parser}; use eyre::{eyre, WrapErr}; -use crate::{cli::CommandExecute, interaction}; +use crate::cli::{interaction, CommandExecute}; /// Uninstall a previously installed Nix (only Harmonic done installs supported) #[derive(Debug, Parser)] @@ -53,14 +53,16 @@ impl CommandExecute for Uninstall { let mut plan: InstallPlan = serde_json::from_str(&install_receipt_string)?; if !no_confirm { - if !interaction::confirm(plan.describe_revert(explain).map_err(|e| eyre!(e))?).await? { + if !interaction::confirm(plan.describe_uninstall(explain).map_err(|e| eyre!(e))?) + .await? + { interaction::clean_exit_with_message("Okay, didn't do anything! Bye!").await; } } let (_tx, rx) = signal_channel().await?; - plan.revert(rx).await?; + plan.uninstall(rx).await?; // TODO(@hoverbear): It would be so nice to catch errors and offer the user a way to keep going... // However that will require being able to link error -> step and manually setting that step as `Uncompleted`. diff --git a/src/error.rs b/src/error.rs index 44b810c..86a4f1d 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,17 +1,49 @@ use std::path::PathBuf; +use crate::{planner::PlannerError, settings::InstallSettingsError}; + +/// An error occurring during a call defined in this crate #[derive(thiserror::Error, Debug)] pub enum HarmonicError { + /// An error originating from an [`Action`](crate::action::Action) #[error("Error executing action")] Action( #[source] #[from] Box, ), + /// An error while writing the [`InstallPlan`](crate::InstallPlan) #[error("Recording install receipt")] RecordingReceipt(PathBuf, #[source] std::io::Error), - #[error(transparent)] - SerializingReceipt(serde_json::Error), + /// An error while serializing the [`InstallPlan`](crate::InstallPlan) + #[error("Serializing receipt")] + SerializingReceipt( + #[from] + #[source] + serde_json::Error, + ), + /// An error ocurring when a signal is issued along [`InstallPlan::install`](crate::InstallPlan::install)'s `cancel_channel` argument #[error("Cancelled by user")] Cancelled, + /// Semver error + #[error("Semantic Versioning error")] + SemVer( + #[from] + #[source] + semver::Error, + ), + /// Planner error + #[error("Planner error")] + Planner( + #[from] + #[source] + PlannerError, + ), + /// Install setting error + #[error("Install setting error")] + InstallSettings( + #[from] + #[source] + InstallSettingsError, + ), } diff --git a/src/lib.rs b/src/lib.rs index 9263503..bb27088 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,24 +1,90 @@ +/*! A [Nix](https://github.com/NixOS/nix) installer and uninstaller. + +Harmonic breaks down into three main concepts: + +* [`Action`]: An executable or revertable step, possibly orcestrating sub-[`Action`]s using things + like [`JoinSet`](tokio::task::JoinSet)s. +* [`InstallPlan`]: A set of [`Action`]s, along with some metadata, which can be carried out to + drive an install or revert. +* [`Planner`](planner::Planner): Something which can be used to plan out an [`InstallPlan`]. + +It is possible to create custom [`Action`]s and [`Planner`](planner::Planner)s to suit the needs of your project, team, or organization. + +In the simplest case, Harmonic can be asked to determine a default plan for the platform and install +it, uninstalling if anything goes wrong: + +```rust,no_run +use std::error::Error; +use harmonic::InstallPlan; + +# async fn default_install() -> color_eyre::Result<()> { +let mut plan = InstallPlan::default().await?; +match plan.install(None).await { + Ok(()) => tracing::info!("Done"), + Err(e) => { + match e.source() { + Some(source) => tracing::error!("{e}: {}", source), + None => tracing::error!("{e}"), + }; + plan.uninstall(None).await?; + }, +}; +# +# Ok(()) +# } +``` + +Sometimes choosing a specific plan is desired: + +```rust,no_run +use std::error::Error; +use harmonic::{InstallPlan, planner::{Planner, specific::SteamDeck}}; + +# async fn chosen_planner_install() -> color_eyre::Result<()> { +let planner = SteamDeck::default().await?; + +// Or call `crate::planner::BuiltinPlanner::default()` +// Match on the result to customize. + +// Customize any settings... + +let mut plan = InstallPlan::plan(planner).await?; +match plan.install(None).await { + Ok(()) => tracing::info!("Done"), + Err(e) => { + match e.source() { + Some(source) => tracing::error!("{e}: {}", source), + None => tracing::error!("{e}"), + }; + plan.uninstall(None).await?; + }, +}; +# +# Ok(()) +# } +``` + +*/ + pub mod action; -pub mod channel_value; +mod channel_value; +#[cfg(feature = "cli")] pub mod cli; mod error; -mod interaction; mod os; mod plan; pub mod planner; -mod settings; +pub mod settings; use std::{ffi::OsStr, process::Output}; -pub use action::Action; -pub use planner::Planner; +use action::Action; +pub use channel_value::ChannelValue; pub use error::HarmonicError; pub use plan::InstallPlan; use planner::BuiltinPlanner; -pub use settings::CommonSettings; - use tokio::process::Command; #[tracing::instrument(skip_all, fields(command = %format!("{:?}", command.as_std())))] diff --git a/src/plan.rs b/src/plan.rs index 26e4609..daf748d 100644 --- a/src/plan.rs +++ b/src/plan.rs @@ -1,43 +1,56 @@ use std::{path::PathBuf, str::FromStr}; use crate::{ - action::{Action, ActionDescription, ActionImplementation}, - planner::Planner, + action::{Action, ActionDescription, StatefulAction}, + planner::{BuiltinPlanner, Planner}, HarmonicError, }; -use crossterm::style::Stylize; +use owo_colors::OwoColorize; use semver::{Version, VersionReq}; use serde::{de::Error, Deserialize, Deserializer}; use tokio::sync::broadcast::Receiver; pub const RECEIPT_LOCATION: &str = "/nix/receipt.json"; +/** +A set of [`Action`]s, along with some metadata, which can be carried out to drive an install or +revert +*/ #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] pub struct InstallPlan { #[serde(deserialize_with = "ensure_version")] pub(crate) version: Version, - pub(crate) actions: Vec>, + pub(crate) actions: Vec>>, pub(crate) planner: Box, } impl InstallPlan { - pub async fn plan( - planner: Box, - ) -> Result> { + pub async fn default() -> Result { + let planner = BuiltinPlanner::default().await?.boxed(); let actions = planner.plan().await?; + Ok(Self { planner, actions, version: current_version()?, }) } + + pub async fn plan

(planner: P) -> Result + where + P: Planner + 'static, + { + let actions = planner.plan().await?; + Ok(Self { + planner: planner.boxed(), + actions, + version: current_version()?, + }) + } #[tracing::instrument(skip_all)] - pub fn describe_execute( - &self, - explain: bool, - ) -> Result> { + pub fn describe_install(&self, explain: bool) -> Result { let Self { planner, actions, @@ -134,10 +147,7 @@ impl InstallPlan { } #[tracing::instrument(skip_all)] - pub fn describe_revert( - &self, - explain: bool, - ) -> Result> { + pub fn describe_uninstall(&self, explain: bool) -> Result { let Self { version: _, planner, @@ -196,7 +206,7 @@ impl InstallPlan { } #[tracing::instrument(skip_all)] - pub async fn revert( + pub async fn uninstall( &mut self, cancel_channel: impl Into>>, ) -> Result<(), HarmonicError> { @@ -277,13 +287,11 @@ fn ensure_version<'de, D: Deserializer<'de>>(d: D) -> Result mod test { use semver::Version; - use crate::{planner::BuiltinPlanner, InstallPlan}; + use crate::{planner::BuiltinPlanner, HarmonicError, InstallPlan}; #[tokio::test] - async fn ensure_version_allows_compatible() -> eyre::Result<()> { - let planner = BuiltinPlanner::default() - .await - .map_err(|e| eyre::eyre!(e))?; + async fn ensure_version_allows_compatible() -> Result<(), HarmonicError> { + let planner = BuiltinPlanner::default().await?; let good_version = Version::parse(env!("CARGO_PKG_VERSION"))?; let value = serde_json::json!({ "planner": planner.boxed(), @@ -296,10 +304,8 @@ mod test { } #[tokio::test] - async fn ensure_version_denies_incompatible() -> eyre::Result<()> { - let planner = BuiltinPlanner::default() - .await - .map_err(|e| eyre::eyre!(e))?; + async fn ensure_version_denies_incompatible() -> Result<(), HarmonicError> { + let planner = BuiltinPlanner::default().await?; let bad_version = Version::parse("9999999999999.9999999999.99999999")?; let value = serde_json::json!({ "planner": planner.boxed(), diff --git a/src/planner/darwin/mod.rs b/src/planner/darwin/mod.rs index 32a934a..18747e0 100644 --- a/src/planner/darwin/mod.rs +++ b/src/planner/darwin/mod.rs @@ -1,3 +1,5 @@ +//! Planners for Darwin based systems + mod multi; pub use multi::DarwinMulti; diff --git a/src/planner/darwin/multi.rs b/src/planner/darwin/multi.rs index 8aa2dac..dbe5166 100644 --- a/src/planner/darwin/multi.rs +++ b/src/planner/darwin/multi.rs @@ -1,46 +1,63 @@ use std::{collections::HashMap, io::Cursor}; +#[cfg(feature = "cli")] use clap::ArgAction; use tokio::process::Command; use crate::{ action::{ common::{ConfigureNix, ProvisionNix}, - darwin::{CreateApfsVolume, KickstartLaunchctlService}, + darwin::{CreateNixVolume, KickstartLaunchctlService}, + StatefulAction, }, execute_command, os::darwin::DiskUtilOutput, - planner::{BuiltinPlannerError, Planner}, - Action, BuiltinPlanner, CommonSettings, + planner::{Planner, PlannerError}, + settings::CommonSettings, + settings::InstallSettingsError, + Action, BuiltinPlanner, }; -#[derive(Debug, Clone, clap::Parser, serde::Serialize, serde::Deserialize)] +/// A planner for MacOS (Darwin) multi-user installs +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[cfg_attr(feature = "cli", derive(clap::Parser))] pub struct DarwinMulti { - #[clap(flatten)] + #[cfg_attr(feature = "cli", clap(flatten))] pub settings: CommonSettings, /// Force encryption on the volume - #[clap( - long, - action(ArgAction::Set), - default_value = "false", - env = "HARMONIC_ENCRYPT" + #[cfg_attr( + feature = "cli", + clap( + long, + action(ArgAction::Set), + default_value = "false", + env = "HARMONIC_ENCRYPT" + ) )] pub encrypt: Option, /// Use a case sensitive volume - #[clap( - long, - action(ArgAction::SetTrue), - default_value = "false", - env = "HARMONIC_CASE_SENSITIVE" + #[cfg_attr( + feature = "cli", + clap( + long, + action(ArgAction::SetTrue), + default_value = "false", + env = "HARMONIC_CASE_SENSITIVE" + ) )] pub case_sensitive: bool, - #[clap(long, default_value = "Nix Store", env = "HARMONIC_VOLUME_LABEL")] + /// The label for the created APFS volume + #[cfg_attr( + feature = "cli", + clap(long, default_value = "Nix Store", env = "HARMONIC_VOLUME_LABEL") + )] pub volume_label: String, - #[clap(long, env = "HARMONIC_ROOT_DISK")] + /// The root disk of the target + #[cfg_attr(feature = "cli", clap(long, env = "HARMONIC_ROOT_DISK"))] pub root_disk: Option, } -async fn default_root_disk() -> Result { +async fn default_root_disk() -> Result { let buf = execute_command( Command::new("/usr/sbin/diskutil") .args(["info", "-plist", "/"]) @@ -57,7 +74,7 @@ async fn default_root_disk() -> Result { #[async_trait::async_trait] #[typetag::serde(name = "darwin-multi")] impl Planner for DarwinMulti { - async fn default() -> Result> { + async fn default() -> Result { Ok(Self { settings: CommonSettings::default()?, root_disk: Some(default_root_disk().await?), @@ -67,7 +84,7 @@ impl Planner for DarwinMulti { }) } - async fn plan(&self) -> Result>, Box> { + async fn plan(&self) -> Result>>, PlannerError> { let root_disk = match &self.root_disk { root_disk @ Some(_) => root_disk.clone(), None => { @@ -89,7 +106,8 @@ impl Planner for DarwinMulti { Command::new("/usr/bin/fdesetup") .arg("isactive") .status() - .await? + .await + .map_err(|e| PlannerError::Custom(Box::new(e)))? .code() .map(|v| if v == 0 { false } else { true }) .unwrap_or(false) @@ -102,24 +120,31 @@ impl Planner for DarwinMulti { // // setup_Synthetic -> create_synthetic_objects // Unmount -> create_volume -> Setup_fstab -> maybe encrypt_volume -> launchctl bootstrap -> launchctl kickstart -> await_volume -> maybe enableOwnership - Box::new( - CreateApfsVolume::plan( - root_disk.unwrap(), /* We just ensured it was populated */ - self.volume_label.clone(), - false, - encrypt, - ) - .await?, - ), - Box::new(ProvisionNix::plan(&self.settings).await?), - Box::new(ConfigureNix::plan(&self.settings).await?), - Box::new(KickstartLaunchctlService::plan("system/org.nixos.nix-daemon".into()).await?), + CreateNixVolume::plan( + root_disk.unwrap(), /* We just ensured it was populated */ + self.volume_label.clone(), + false, + encrypt, + ) + .await + .map_err(PlannerError::Action)? + .boxed(), + ProvisionNix::plan(&self.settings) + .await + .map_err(PlannerError::Action)? + .boxed(), + ConfigureNix::plan(&self.settings) + .await + .map_err(PlannerError::Action)? + .boxed(), + KickstartLaunchctlService::plan("system/org.nixos.nix-daemon".into()) + .await + .map_err(PlannerError::Action)? + .boxed(), ]) } - fn settings( - &self, - ) -> Result, Box> { + fn settings(&self) -> Result, InstallSettingsError> { let Self { settings, encrypt, @@ -129,7 +154,7 @@ impl Planner for DarwinMulti { } = self; let mut map = HashMap::default(); - map.extend(settings.describe()?.into_iter()); + map.extend(settings.settings()?.into_iter()); 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)?); diff --git a/src/planner/linux/mod.rs b/src/planner/linux/mod.rs index 0cb2e17..6f81f90 100644 --- a/src/planner/linux/mod.rs +++ b/src/planner/linux/mod.rs @@ -1,3 +1,5 @@ +//! Planners for Linux based systems + mod multi; pub use multi::LinuxMulti; diff --git a/src/planner/linux/multi.rs b/src/planner/linux/multi.rs index 8611df9..a39dca9 100644 --- a/src/planner/linux/multi.rs +++ b/src/planner/linux/multi.rs @@ -2,32 +2,37 @@ use crate::{ action::{ base::CreateDirectory, common::{ConfigureNix, ProvisionNix}, + StatefulAction, }, - planner::Planner, - Action, BuiltinPlanner, CommonSettings, + planner::{Planner, PlannerError}, + settings::CommonSettings, + settings::InstallSettingsError, + Action, BuiltinPlanner, }; use std::{collections::HashMap, path::Path}; -#[derive(Debug, Clone, clap::Parser, serde::Serialize, serde::Deserialize)] +/// A planner for Linux multi-user installs +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[cfg_attr(feature = "cli", derive(clap::Parser))] pub struct LinuxMulti { - #[clap(flatten)] + #[cfg_attr(feature = "cli", clap(flatten))] pub settings: CommonSettings, } #[async_trait::async_trait] #[typetag::serde(name = "linux-multi")] impl Planner for LinuxMulti { - async fn default() -> Result> { + async fn default() -> Result { Ok(Self { settings: CommonSettings::default()?, }) } - async fn plan(&self) -> Result>, Box> { + async fn plan(&self) -> Result>>, PlannerError> { // If on NixOS, running `harmonic` is pointless // NixOS always sets up this file as part of setting up /etc itself: https://github.com/NixOS/nixpkgs/blob/bdd39e5757d858bd6ea58ed65b4a2e52c8ed11ca/nixos/modules/system/etc/setup-etc.pl#L145 if Path::new("/etc/NIXOS").exists() { - return Err(Error::NixOs.into()); + return Err(PlannerError::Custom(Box::new(LinuxMultiError::NixOs))); } // For now, we don't try to repair the user's Nix install or anything special. @@ -37,35 +42,30 @@ impl Planner for LinuxMulti { .status() .await { - return Err(Error::NixExists.into()); + return Err(PlannerError::Custom(Box::new(LinuxMultiError::NixExists))); } Ok(vec![ - Box::new( - CreateDirectory::plan("/nix", None, None, 0o0755, true) - .await - .map_err(|v| Error::Action(v.into()))?, - ), - Box::new( - ProvisionNix::plan(&self.settings.clone()) - .await - .map_err(|v| Error::Action(v.into()))?, - ), - Box::new( - ConfigureNix::plan(&self.settings) - .await - .map_err(|v| Error::Action(v.into()))?, - ), + CreateDirectory::plan("/nix", None, None, 0o0755, true) + .await + .map_err(PlannerError::Action)? + .boxed(), + ProvisionNix::plan(&self.settings.clone()) + .await + .map_err(PlannerError::Action)? + .boxed(), + ConfigureNix::plan(&self.settings) + .await + .map_err(PlannerError::Action)? + .boxed(), ]) } - fn settings( - &self, - ) -> Result, Box> { + fn settings(&self) -> Result, InstallSettingsError> { let Self { settings } = self; let mut map = HashMap::default(); - map.extend(settings.describe()?.into_iter()); + map.extend(settings.settings()?.into_iter()); Ok(map) } @@ -78,7 +78,7 @@ impl Into for LinuxMulti { } #[derive(thiserror::Error, Debug)] -enum Error { +enum LinuxMultiError { #[error("NixOS already has Nix installed")] NixOs, #[error("`nix` is already a valid command, so it is installed")] diff --git a/src/planner/mod.rs b/src/planner/mod.rs index 90b009a..628084b 100644 --- a/src/planner/mod.rs +++ b/src/planner/mod.rs @@ -1,21 +1,102 @@ +/*! [`BuiltinPlanner`]s and traits to create new types which can be used to plan out an [`InstallPlan`] + +It's a [`Planner`]s job to construct (if possible) a valid [`InstallPlan`] for the host. Some planners, +like [`LinuxMulti`](linux::LinuxMulti), are operating system specific. Others, like [`SteamDeck`](specific::SteamDeck), are device specific. + +[`Planner`]s contain their planner specific settings, typically alongside a [`CommonSettings`][crate::settings::CommonSettings]. + +[`BuiltinPlanner::default()`] offers a way to get the default builtin planner for a given host. + +Custom Planners can also be used to create a platform, project, or organization specific install. + +A custom [`Planner`] can be created: + +```rust,no_run +use std::{error::Error, collections::HashMap}; +use harmonic::{ + InstallPlan, + settings::{CommonSettings, InstallSettingsError}, + planner::{Planner, PlannerError, specific::SteamDeck}, + action::{Action, StatefulAction, linux::StartSystemdUnit}, +}; + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct MyPlanner { + pub common: CommonSettings, +} + + +#[async_trait::async_trait] +#[typetag::serde(name = "my-planner")] +impl Planner for MyPlanner { + async fn default() -> Result { + Ok(Self { + common: CommonSettings::default()?, + }) + } + + async fn plan(&self) -> Result>>, PlannerError> { + Ok(vec![ + // ... + + StartSystemdUnit::plan("nix-daemon.socket".into()) + .await + .map_err(PlannerError::Action)?.boxed(), + ]) + } + + fn settings(&self) -> Result, InstallSettingsError> { + let Self { common } = self; + let mut map = std::collections::HashMap::default(); + + map.extend(common.settings()?.into_iter()); + + Ok(map) + } +} + +# async fn custom_planner_install() -> color_eyre::Result<()> { +let planner = MyPlanner::default().await?; +let mut plan = InstallPlan::plan(planner).await?; +match plan.install(None).await { + Ok(()) => tracing::info!("Done"), + Err(e) => { + match e.source() { + Some(source) => tracing::error!("{e}: {}", source), + None => tracing::error!("{e}"), + }; + plan.uninstall(None).await?; + }, +}; + +# Ok(()) +# } +``` + +*/ pub mod darwin; pub mod linux; pub mod specific; use std::collections::HashMap; -use crate::{settings::InstallSettingsError, Action, BoxableError}; +use crate::{ + action::StatefulAction, settings::InstallSettingsError, Action, HarmonicError, InstallPlan, +}; +/// Something which can be used to plan out an [`InstallPlan`] #[async_trait::async_trait] #[typetag::serde(tag = "planner")] pub trait Planner: std::fmt::Debug + Send + Sync + dyn_clone::DynClone { - async fn default() -> Result> + /// Instantiate the planner with default settings, if possible + async fn default() -> Result where Self: Sized; - async fn plan(&self) -> Result>, Box>; - fn settings( - &self, - ) -> Result, Box>; + /// Plan out the [`Action`]s for an [`InstallPlan`] + async fn plan(&self) -> Result>>, PlannerError>; + /// The settings being used by the planner + fn settings(&self) -> Result, InstallSettingsError>; + /// A boxed, type erased planner fn boxed(self) -> Box where Self: Sized + 'static, @@ -26,15 +107,21 @@ pub trait Planner: std::fmt::Debug + Send + Sync + dyn_clone::DynClone { dyn_clone::clone_trait_object!(Planner); -#[derive(Debug, Clone, clap::Subcommand, serde::Serialize, serde::Deserialize)] +/// Planners built into this crate +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[cfg_attr(feature = "cli", derive(clap::Subcommand))] pub enum BuiltinPlanner { + /// A standard Linux multi-user install LinuxMulti(linux::LinuxMulti), + /// A standard MacOS (Darwin) multi-user install DarwinMulti(darwin::DarwinMulti), + /// An install suitable for the Valve Steam Deck console SteamDeck(specific::SteamDeck), } impl BuiltinPlanner { - pub async fn default() -> Result> { + /// Heuristically determine the default planner for the target system + pub async fn default() -> Result { use target_lexicon::{Architecture, OperatingSystem}; match (Architecture::host(), OperatingSystem::host()) { (Architecture::X86_64, OperatingSystem::Linux) => { @@ -51,17 +138,15 @@ impl BuiltinPlanner { | (Architecture::Aarch64(_), OperatingSystem::Darwin) => { Ok(Self::DarwinMulti(darwin::DarwinMulti::default().await?)) }, - _ => Err(BuiltinPlannerError::UnsupportedArchitecture(target_lexicon::HOST).boxed()), + _ => Err(PlannerError::UnsupportedArchitecture(target_lexicon::HOST)), } } - pub async fn plan( - self, - ) -> Result>, Box> { + pub async fn plan(self) -> Result { match self { - BuiltinPlanner::LinuxMulti(planner) => planner.plan().await, - BuiltinPlanner::DarwinMulti(planner) => planner.plan().await, - BuiltinPlanner::SteamDeck(planner) => planner.plan().await, + BuiltinPlanner::LinuxMulti(planner) => InstallPlan::plan(planner).await, + BuiltinPlanner::DarwinMulti(planner) => InstallPlan::plan(planner).await, + BuiltinPlanner::SteamDeck(planner) => InstallPlan::plan(planner).await, } } pub fn boxed(self) -> Box { @@ -73,18 +158,22 @@ impl BuiltinPlanner { } } +/// An error originating from a [`Planner`] #[derive(thiserror::Error, Debug)] -pub enum BuiltinPlannerError { +pub enum PlannerError { + /// Harmonic does not have a default planner for the target architecture right now #[error("Harmonic does not have a default planner for the `{0}` architecture right now, pass a specific archetype")] UnsupportedArchitecture(target_lexicon::Triple), + /// Error executing action #[error("Error executing action")] - ActionError( - #[source] - #[from] - Box, - ), + Action(#[source] Box), + /// An [`InstallSettingsError`] #[error(transparent)] InstallSettings(#[from] InstallSettingsError), + /// A MacOS (Darwin) plist related error #[error(transparent)] Plist(#[from] plist::Error), + /// Custom planner error + #[error("Custom planner error")] + Custom(#[source] Box), } diff --git a/src/planner/specific/steam_deck.rs b/src/planner/specific/steam_deck.rs index a0f6ccb..ab64370 100644 --- a/src/planner/specific/steam_deck.rs +++ b/src/planner/specific/steam_deck.rs @@ -5,42 +5,57 @@ use crate::{ base::CreateDirectory, common::ProvisionNix, linux::{CreateSystemdSysext, StartSystemdUnit}, + StatefulAction, }, - planner::Planner, - Action, BuiltinPlanner, CommonSettings, + planner::{Planner, PlannerError}, + settings::CommonSettings, + settings::InstallSettingsError, + Action, BuiltinPlanner, }; -#[derive(Debug, Clone, clap::Parser, serde::Serialize, serde::Deserialize)] +/// A planner suitable for Valve Steam Deck consoles +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[cfg_attr(feature = "cli", derive(clap::Parser))] pub struct SteamDeck { - #[clap(flatten)] + #[cfg_attr(feature = "cli", clap(flatten))] pub settings: CommonSettings, } #[async_trait::async_trait] #[typetag::serde(name = "steam-deck")] impl Planner for SteamDeck { - async fn default() -> Result> { + async fn default() -> Result { Ok(Self { settings: CommonSettings::default()?, }) } - async fn plan(&self) -> Result>, Box> { + async fn plan(&self) -> Result>>, PlannerError> { Ok(vec![ - Box::new(CreateSystemdSysext::plan("/var/lib/extensions/nix").await?), - Box::new(CreateDirectory::plan("/nix", None, None, 0o0755, true).await?), - Box::new(ProvisionNix::plan(&self.settings.clone()).await?), - Box::new(StartSystemdUnit::plan("nix-daemon.socket".into()).await?), + CreateSystemdSysext::plan("/var/lib/extensions/nix") + .await + .map_err(PlannerError::Action)? + .boxed(), + CreateDirectory::plan("/nix", None, None, 0o0755, true) + .await + .map_err(PlannerError::Action)? + .boxed(), + ProvisionNix::plan(&self.settings.clone()) + .await + .map_err(PlannerError::Action)? + .boxed(), + StartSystemdUnit::plan("nix-daemon.socket".into()) + .await + .map_err(PlannerError::Action)? + .boxed(), ]) } - fn settings( - &self, - ) -> Result, Box> { + fn settings(&self) -> Result, InstallSettingsError> { let Self { settings } = self; let mut map = HashMap::default(); - map.extend(settings.describe()?.into_iter()); + map.extend(settings.settings()?.into_iter()); Ok(map) } diff --git a/src/settings.rs b/src/settings.rs index d671c69..dcc4f11 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -1,105 +1,151 @@ +/*! Configurable knobs and their related errors +*/ use std::collections::HashMap; +#[cfg(feature = "cli")] use clap::ArgAction; use url::Url; use crate::channel_value::ChannelValue; +/// Default [`nix_package_url`](CommonSettings::nix_package_url) for Linux x86_64 pub const NIX_X64_64_LINUX_URL: &str = "https://releases.nixos.org/nix/nix-2.11.0/nix-2.11.0-x86_64-linux.tar.xz"; +/// Default [`nix_package_url`](CommonSettings::nix_package_url) for Linux aarch64 pub const NIX_AARCH64_LINUX_URL: &str = "https://releases.nixos.org/nix/nix-2.11.0/nix-2.11.0-aarch64-linux.tar.xz"; +/// Default [`nix_package_url`](CommonSettings::nix_package_url) for Darwin x86_64 pub const NIX_X64_64_DARWIN_URL: &str = "https://releases.nixos.org/nix/nix-2.11.0/nix-2.11.0-x86_64-darwin.tar.xz"; +/// Default [`nix_package_url`](CommonSettings::nix_package_url) for Darwin aarch64 pub const NIX_AARCH64_DARWIN_URL: &str = "https://releases.nixos.org/nix/nix-2.11.0/nix-2.11.0-aarch64-darwin.tar.xz"; +/** Common settings used by all [`BuiltinPlanner`](crate::planner::BuiltinPlanner)s + +Settings which only apply to certain [`Planner`](crate::planner::Planner)s should be located in the planner. + +*/ #[serde_with::serde_as] -#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, clap::Parser)] +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] +#[cfg_attr(feature = "cli", derive(clap::Parser))] pub struct CommonSettings { - /// Channel(s) to add by default, pass multiple times for multiple channels - #[clap( + /// Channel(s) to add + #[cfg_attr( + feature = "cli",clap( long, value_parser, name = "channel", action = clap::ArgAction::Append, env = "HARMONIC_CHANNEL", default_value = "nixpkgs=https://nixos.org/channels/nixpkgs-unstable", - )] - pub channels: Vec, + ))] + pub(crate) channels: Vec, /// Modify the user profile to automatically load nix - #[clap( - long, - action(ArgAction::SetFalse), - default_value = "true", - global = true, - env = "HARMONIC_NO_MODIFY_PROFILE", - name = "no-modify-profile" + #[cfg_attr( + feature = "cli", + clap( + long, + action(ArgAction::SetFalse), + default_value = "true", + global = true, + env = "HARMONIC_NO_MODIFY_PROFILE", + name = "no-modify-profile" + ) )] - pub modify_profile: bool, + pub(crate) modify_profile: bool, /// Number of build users to create - #[clap(long, default_value = "32", env = "HARMONIC_DAEMON_USER_COUNT")] - pub daemon_user_count: usize, - - #[clap(long, default_value = "nixbld", env = "HARMONIC_NIX_BUILD_GROUP_NAME")] - pub nix_build_group_name: String, - - #[clap(long, default_value_t = 3000, env = "HARMONIC_NIX_BUILD_GROUP_ID")] - pub nix_build_group_id: usize, - - #[clap(long, env = "HARMONIC_NIX_BUILD_USER_PREFIX")] - #[cfg_attr(target_os = "macos", clap(default_value = "_nixbld"))] - #[cfg_attr(target_os = "linux", clap(default_value = "nixbld"))] - pub nix_build_user_prefix: String, - - #[clap(long, env = "HARMONIC_NIX_BUILD_USER_ID_BASE")] - #[cfg_attr(target_os = "macos", clap(default_value_t = 300))] - #[cfg_attr(target_os = "linux", clap(default_value_t = 3000))] - pub nix_build_user_id_base: usize, - - #[clap(long, env = "HARMONIC_NIX_PACKAGE_URL")] #[cfg_attr( - all(target_os = "macos", target_arch = "x86_64"), + feature = "cli", + clap(long, default_value = "32", env = "HARMONIC_DAEMON_USER_COUNT") + )] + pub(crate) daemon_user_count: usize, + + /// The Nix build group name + #[cfg_attr( + feature = "cli", + clap(long, default_value = "nixbld", env = "HARMONIC_NIX_BUILD_GROUP_NAME") + )] + pub(crate) nix_build_group_name: String, + + /// The Nix build group GID + #[cfg_attr( + feature = "cli", + clap(long, default_value_t = 3000, env = "HARMONIC_NIX_BUILD_GROUP_ID") + )] + pub(crate) nix_build_group_id: usize, + + /// The Nix build user prefix (user numbers will be postfixed) + #[cfg_attr(feature = "cli", clap(long, env = "HARMONIC_NIX_BUILD_USER_PREFIX"))] + #[cfg_attr( + all(target_os = "macos", feature = "cli"), + clap(default_value = "_nixbld") + )] + #[cfg_attr( + all(target_os = "linux", feature = "cli"), + clap(default_value = "nixbld") + )] + pub(crate) nix_build_user_prefix: String, + + /// The Nix build user base UID (ascending) + #[cfg_attr(feature = "cli", clap(long, env = "HARMONIC_NIX_BUILD_USER_ID_BASE"))] + #[cfg_attr(all(target_os = "macos", feature = "cli"), clap(default_value_t = 300))] + #[cfg_attr( + all(target_os = "linux", feature = "cli"), + clap(default_value_t = 3000) + )] + pub(crate) nix_build_user_id_base: usize, + + /// The Nix package URL + #[cfg_attr(feature = "cli", clap(long, env = "HARMONIC_NIX_PACKAGE_URL"))] + #[cfg_attr( + all(target_os = "macos", target_arch = "x86_64", feature = "cli"), clap( default_value = NIX_X64_64_DARWIN_URL, ) )] #[cfg_attr( - all(target_os = "macos", target_arch = "aarch64"), + all(target_os = "macos", target_arch = "aarch64", feature = "cli"), clap( default_value = NIX_AARCH64_DARWIN_URL, ) )] #[cfg_attr( - all(target_os = "linux", target_arch = "x86_64"), + all(target_os = "linux", target_arch = "x86_64", feature = "cli"), clap( default_value = NIX_X64_64_LINUX_URL, ) )] #[cfg_attr( - all(target_os = "linux", target_arch = "aarch64"), + all(target_os = "linux", target_arch = "aarch64", feature = "cli"), clap( default_value = NIX_AARCH64_LINUX_URL, ) )] - pub nix_package_url: Url, + pub(crate) nix_package_url: Url, - #[clap(long, env = "HARMONIC_EXTRA_CONF")] - pub extra_conf: Option, + /// Extra configuration lines for `/etc/nix.conf` + #[cfg_attr(feature = "cli", clap(long, env = "HARMONIC_EXTRA_CONF"))] + pub(crate) extra_conf: Option, - #[clap( - long, - action(ArgAction::SetTrue), - default_value = "false", - global = true, - env = "HARMONIC_FORCE" + /// If Harmonic should forcibly recreate files it finds existing + #[cfg_attr( + feature = "cli", + clap( + long, + action(ArgAction::SetTrue), + default_value = "false", + global = true, + env = "HARMONIC_FORCE" + ) )] - pub force: bool, + pub(crate) force: bool, } impl CommonSettings { + /// The default settings for the given Architecture & Operating System pub fn default() -> Result { let url; let nix_build_user_prefix; @@ -154,9 +200,8 @@ impl CommonSettings { }) } - pub fn describe( - &self, - ) -> Result, Box> { + /// A listing of the settings, suitable for [`Planner::settings`](crate::planner::Planner::settings) + pub fn settings(&self) -> Result, InstallSettingsError> { let Self { channels, modify_profile, @@ -217,62 +262,85 @@ impl CommonSettings { // Builder Pattern impl CommonSettings { + /// Number of build users to create pub fn daemon_user_count(&mut self, count: usize) -> &mut Self { self.daemon_user_count = count; self } + /// Channel(s) to add pub fn channels(&mut self, channels: impl IntoIterator) -> &mut Self { self.channels = channels.into_iter().map(Into::into).collect(); self } + /// Modify the user profile to automatically load nix pub fn modify_profile(&mut self, toggle: bool) -> &mut Self { self.modify_profile = toggle; self } + /// The Nix build group name pub fn nix_build_group_name(&mut self, val: String) -> &mut Self { self.nix_build_group_name = val; self } + /// The Nix build group GID pub fn nix_build_group_id(&mut self, count: usize) -> &mut Self { self.nix_build_group_id = count; self } + /// The Nix build user prefix (user numbers will be postfixed) pub fn nix_build_user_prefix(&mut self, val: String) -> &mut Self { self.nix_build_user_prefix = val; self } + /// The Nix build user base UID (ascending) pub fn nix_build_user_id_base(&mut self, count: usize) -> &mut Self { self.nix_build_user_id_base = count; self } + + /// The Nix package URL pub fn nix_package_url(&mut self, url: Url) -> &mut Self { self.nix_package_url = url; self } + + /// Extra configuration lines for `/etc/nix.conf` pub fn extra_conf(&mut self, extra_conf: Option) -> &mut Self { self.extra_conf = extra_conf; self } + + /// If Harmonic should forcibly recreate files it finds existing pub fn force(&mut self, force: bool) -> &mut Self { self.force = force; self } } +/// An error originating from a [`Planner::settings`](crate::planner::Planner::settings) #[derive(thiserror::Error, Debug)] pub enum InstallSettingsError { + /// Harmonic does not support the architecture right now #[error("Harmonic does not support the `{0}` architecture right now")] UnsupportedArchitecture(target_lexicon::Triple), + /// Parsing URL #[error("Parsing URL")] Parse( #[source] #[from] url::ParseError, ), + /// JSON serialization or deserialization error + #[error("JSON serialization or deserialization error")] + SerdeJson( + #[source] + #[from] + serde_json::Error, + ), }