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
This commit is contained in:
Ana Hobden 2022-11-28 14:57:35 -08:00 committed by GitHub
parent f1d12149de
commit c39bf0a510
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
53 changed files with 1577 additions and 1304 deletions

69
Cargo.lock generated
View file

@ -594,12 +594,6 @@ dependencies = [
"percent-encoding", "percent-encoding",
] ]
[[package]]
name = "fuchsia-cprng"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
[[package]] [[package]]
name = "futures" name = "futures"
version = "0.1.31" version = "0.1.31"
@ -795,7 +789,7 @@ dependencies = [
"nix", "nix",
"owo-colors", "owo-colors",
"plist", "plist",
"rand 0.8.5", "rand",
"reqwest", "reqwest",
"semver", "semver",
"serde", "serde",
@ -805,7 +799,6 @@ dependencies = [
"sxd-xpath", "sxd-xpath",
"tar", "tar",
"target-lexicon", "target-lexicon",
"tempdir",
"thiserror", "thiserror",
"tokio", "tokio",
"tokio-util", "tokio-util",
@ -1389,19 +1382,6 @@ dependencies = [
"proc-macro2", "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]] [[package]]
name = "rand" name = "rand"
version = "0.8.5" version = "0.8.5"
@ -1410,7 +1390,7 @@ checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [ dependencies = [
"libc", "libc",
"rand_chacha", "rand_chacha",
"rand_core 0.6.4", "rand_core",
] ]
[[package]] [[package]]
@ -1420,24 +1400,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [ dependencies = [
"ppv-lite86", "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]] [[package]]
name = "rand_core" name = "rand_core"
version = "0.6.4" version = "0.6.4"
@ -1447,15 +1412,6 @@ dependencies = [
"getrandom", "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]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.2.16" version = "0.2.16"
@ -1500,15 +1456,6 @@ version = "0.6.28"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" 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]] [[package]]
name = "reqwest" name = "reqwest"
version = "0.11.13" version = "0.11.13"
@ -1849,16 +1796,6 @@ version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9410d0f6853b1d94f0e519fb95df60f29d2c1eff2d921ffdf01a4c8a3b54f12d" 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]] [[package]]
name = "termcolor" name = "termcolor"
version = "1.1.3" version = "1.1.3"

View file

@ -11,15 +11,23 @@ build-inputs = ["darwin.apple_sdk.frameworks.Security"]
[package.metadata.riff.targets.x86_64-apple-darwin] [package.metadata.riff.targets.x86_64-apple-darwin]
build-inputs = ["darwin.apple_sdk.frameworks.Security"] 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] [dependencies]
async-tar = "0.4.2" async-tar = "0.4.2"
async-trait = "0.1.57" async-trait = "0.1.57"
atty = "0.2.14" atty = { version = "0.2.14", optional = true }
bytes = "1.2.1" bytes = "1.2.1"
clap = { version = "4", features = ["derive", "env"] } clap = { version = "4", features = ["derive", "env"], optional = true }
color-eyre = "0.6.2" color-eyre = { version = "0.6.2", optional = true }
crossterm = { version = "0.25.0", features = ["event-stream"] } crossterm = { version = "0.25.0", features = ["event-stream"], optional = true }
eyre = "0.6.8" eyre = { version = "0.6.8", optional = true }
futures = "0.3.24" futures = "0.3.24"
glob = "0.3.0" glob = "0.3.0"
nix = { version = "0.25.0", features = ["user", "fs", "process", "term"], default-features = false } 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" serde_with = "2.0.1"
tar = "0.4.38" tar = "0.4.38"
target-lexicon = "0.12.4" target-lexicon = "0.12.4"
tempdir = { version = "0.3.7"}
thiserror = "1.0.33" 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 = { 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"] } tokio-util = { version = "0.7", features = ["io"] }
tracing = { version = "0.1.36", features = [ "valuable" ] } tracing = { version = "0.1.36", features = [ "valuable" ] }
tracing-error = "0.2.0" tracing-error = { version = "0.2.0", optional = true }
tracing-subscriber = { version = "0.3.15", features = [ "env-filter", "valuable" ] } tracing-subscriber = { version = "0.3.15", features = [ "env-filter", "valuable" ], optional = true }
url = { version = "2.3.1", features = ["serde"] } url = { version = "2.3.1", features = ["serde"] }
valuable = { version = "0.1.0", features = ["derive"] } valuable = { version = "0.1.0", features = ["derive"] }
walkdir = "2.3.2" walkdir = "2.3.2"

View file

@ -121,14 +121,16 @@
pkg-config pkg-config
]; ];
buildInputs = with pkgs; [ buildInputs = with pkgs; [
openssl openssl
] ++ lib.optionals (pkgs.stdenv.isDarwin) (with pkgs.darwin.apple_sdk.frameworks; [ ] ++ lib.optionals (pkgs.stdenv.isDarwin) (with pkgs.darwin.apple_sdk.frameworks; [
SystemConfiguration SystemConfiguration
]); ]);
doCheck = true; doCheck = true;
doDoc = true;
doDocFail = true;
RUSTFLAGS = "--cfg tracing_unstable --cfg tokio_unstable"; RUSTFLAGS = "--cfg tracing_unstable --cfg tokio_unstable";
cargoTestOptions = f: f ++ ["--all"];
override = { preBuild ? "", ... }: { override = { preBuild ? "", ... }: {
preBuild = preBuild + '' preBuild = preBuild + ''

View file

@ -5,18 +5,23 @@ use nix::unistd::{chown, Group, User};
use tokio::fs::{create_dir, remove_dir_all}; use tokio::fs::{create_dir, remove_dir_all};
use crate::action::StatefulAction;
use crate::{ use crate::{
action::{Action, ActionDescription, ActionState}, action::{Action, ActionDescription, ActionState},
BoxableError, 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)] #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
pub struct CreateDirectory { pub struct CreateDirectory {
path: PathBuf, path: PathBuf,
user: Option<String>, user: Option<String>,
group: Option<String>, group: Option<String>,
mode: Option<u32>, mode: Option<u32>,
action_state: ActionState,
force_prune_on_revert: bool, force_prune_on_revert: bool,
} }
@ -28,7 +33,7 @@ impl CreateDirectory {
group: impl Into<Option<String>>, group: impl Into<Option<String>>,
mode: impl Into<Option<u32>>, mode: impl Into<Option<u32>>,
force_prune_on_revert: bool, force_prune_on_revert: bool,
) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> { ) -> Result<StatefulAction<Self>, Box<dyn std::error::Error + Send + Sync>> {
let path = path.as_ref(); let path = path.as_ref();
let user = user.into(); let user = user.into();
let group = group.into(); let group = group.into();
@ -59,13 +64,15 @@ impl CreateDirectory {
ActionState::Uncompleted ActionState::Uncompleted
}; };
Ok(Self { Ok(StatefulAction {
path: path.to_path_buf(), action: Self {
user, path: path.to_path_buf(),
group, user,
mode, group,
force_prune_on_revert, mode,
action_state, force_prune_on_revert,
},
state: action_state,
}) })
} }
} }
@ -94,7 +101,6 @@ impl Action for CreateDirectory {
group, group,
mode, mode,
force_prune_on_revert: _, force_prune_on_revert: _,
action_state: _,
} = self; } = self;
let gid = if let Some(group) = group { let gid = if let Some(group) = group {
@ -141,7 +147,6 @@ impl Action for CreateDirectory {
group: _, group: _,
mode: _, mode: _,
force_prune_on_revert, force_prune_on_revert,
action_state: _,
} = &self; } = &self;
vec![ActionDescription::new( vec![ActionDescription::new(
format!( format!(
@ -170,7 +175,6 @@ impl Action for CreateDirectory {
group: _, group: _,
mode: _, mode: _,
force_prune_on_revert, force_prune_on_revert,
action_state: _,
} = self; } = self;
let is_empty = path let is_empty = path
@ -189,14 +193,6 @@ impl Action for CreateDirectory {
Ok(()) 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)] #[derive(Debug, thiserror::Error)]

View file

@ -7,10 +7,16 @@ use tokio::{
}; };
use crate::{ use crate::{
action::{Action, ActionDescription, ActionState}, action::{Action, ActionDescription, StatefulAction},
BoxableError, 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)] #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
pub struct CreateFile { pub struct CreateFile {
pub(crate) path: PathBuf, pub(crate) path: PathBuf,
@ -19,7 +25,6 @@ pub struct CreateFile {
mode: Option<u32>, mode: Option<u32>,
buf: String, buf: String,
force: bool, force: bool,
action_state: ActionState,
} }
impl CreateFile { impl CreateFile {
@ -31,7 +36,7 @@ impl CreateFile {
mode: impl Into<Option<u32>>, mode: impl Into<Option<u32>>,
buf: String, buf: String,
force: bool, force: bool,
) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> { ) -> Result<StatefulAction<Self>, Box<dyn std::error::Error + Send + Sync>> {
let path = path.as_ref().to_path_buf(); let path = path.as_ref().to_path_buf();
if path.exists() && !force { if path.exists() && !force {
@ -45,8 +50,8 @@ impl CreateFile {
mode: mode.into(), mode: mode.into(),
buf, buf,
force, force,
action_state: ActionState::Uncompleted, }
}) .into())
} }
} }
@ -74,7 +79,6 @@ impl Action for CreateFile {
mode, mode,
buf, buf,
force: _, force: _,
action_state: _,
} = self; } = self;
let mut options = OpenOptions::new(); let mut options = OpenOptions::new();
@ -126,7 +130,6 @@ impl Action for CreateFile {
mode: _, mode: _,
buf: _, buf: _,
force: _, force: _,
action_state: _,
} = &self; } = &self;
vec![ActionDescription::new( vec![ActionDescription::new(
@ -149,7 +152,6 @@ impl Action for CreateFile {
mode: _, mode: _,
buf: _, buf: _,
force: _, force: _,
action_state: _,
} = self; } = self;
remove_file(&path) remove_file(&path)
@ -158,14 +160,6 @@ impl Action for CreateFile {
Ok(()) 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)] #[derive(Debug, thiserror::Error)]

View file

@ -3,25 +3,23 @@ use tokio::process::Command;
use crate::execute_command; use crate::execute_command;
use crate::{ use crate::{
action::{Action, ActionDescription, ActionState}, action::{Action, ActionDescription, StatefulAction},
BoxableError, BoxableError,
}; };
/**
Create an operating system level user group
*/
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
pub struct CreateGroup { pub struct CreateGroup {
name: String, name: String,
gid: usize, gid: usize,
action_state: ActionState,
} }
impl CreateGroup { impl CreateGroup {
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
pub fn plan(name: String, gid: usize) -> Self { pub fn plan(name: String, gid: usize) -> StatefulAction<Self> {
Self { Self { name, gid }.into()
name,
gid,
action_state: ActionState::Uncompleted,
}
} }
} }
@ -32,11 +30,7 @@ impl Action for CreateGroup {
format!("Create group `{}` (GID {})", self.name, self.gid) format!("Create group `{}` (GID {})", self.name, self.gid)
} }
fn execute_description(&self) -> Vec<ActionDescription> { fn execute_description(&self) -> Vec<ActionDescription> {
let Self { let Self { name: _, gid: _ } = &self;
name: _,
gid: _,
action_state: _,
} = &self;
vec![ActionDescription::new( vec![ActionDescription::new(
self.tracing_synopsis(), self.tracing_synopsis(),
vec![format!( vec![format!(
@ -50,11 +44,7 @@ impl Action for CreateGroup {
gid = self.gid, gid = self.gid,
))] ))]
async fn execute(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { async fn execute(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let Self { let Self { name, gid } = self;
name,
gid,
action_state: _,
} = self;
use target_lexicon::OperatingSystem; use target_lexicon::OperatingSystem;
match target_lexicon::OperatingSystem::host() { match target_lexicon::OperatingSystem::host() {
@ -109,11 +99,7 @@ impl Action for CreateGroup {
} }
fn revert_description(&self) -> Vec<ActionDescription> { fn revert_description(&self) -> Vec<ActionDescription> {
let Self { let Self { name, gid } = &self;
name,
gid,
action_state: _,
} = &self;
vec![ActionDescription::new( vec![ActionDescription::new(
format!("Delete group `{name}` (GID {gid})"), format!("Delete group `{name}` (GID {gid})"),
vec![format!( vec![format!(
@ -127,11 +113,7 @@ impl Action for CreateGroup {
gid = self.gid, gid = self.gid,
))] ))]
async fn revert(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { async fn revert(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let Self { let Self { name, gid: _ } = self;
name,
gid: _,
action_state: _,
} = self;
use target_lexicon::OperatingSystem; use target_lexicon::OperatingSystem;
match target_lexicon::OperatingSystem::host() { match target_lexicon::OperatingSystem::host() {
@ -166,14 +148,6 @@ impl Action for CreateGroup {
Ok(()) 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)] #[derive(Debug, thiserror::Error)]

View file

@ -11,10 +11,18 @@ use tokio::{
}; };
use crate::{ use crate::{
action::{Action, ActionDescription, ActionState}, action::{Action, ActionDescription, StatefulAction},
BoxableError, 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)] #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
pub struct CreateOrAppendFile { pub struct CreateOrAppendFile {
path: PathBuf, path: PathBuf,
@ -22,7 +30,6 @@ pub struct CreateOrAppendFile {
group: Option<String>, group: Option<String>,
mode: Option<u32>, mode: Option<u32>,
buf: String, buf: String,
action_state: ActionState,
} }
impl CreateOrAppendFile { impl CreateOrAppendFile {
@ -33,7 +40,7 @@ impl CreateOrAppendFile {
group: impl Into<Option<String>>, group: impl Into<Option<String>>,
mode: impl Into<Option<u32>>, mode: impl Into<Option<u32>>,
buf: String, buf: String,
) -> Result<Self, CreateOrAppendFileError> { ) -> Result<StatefulAction<Self>, CreateOrAppendFileError> {
let path = path.as_ref().to_path_buf(); let path = path.as_ref().to_path_buf();
Ok(Self { Ok(Self {
@ -42,8 +49,8 @@ impl CreateOrAppendFile {
group: group.into(), group: group.into(),
mode: mode.into(), mode: mode.into(),
buf, buf,
action_state: ActionState::Uncompleted, }
}) .into())
} }
} }
@ -71,7 +78,6 @@ impl Action for CreateOrAppendFile {
group, group,
mode, mode,
buf, buf,
action_state: _,
} = self; } = self;
let mut file = OpenOptions::new() let mut file = OpenOptions::new()
@ -132,7 +138,6 @@ impl Action for CreateOrAppendFile {
group: _, group: _,
mode: _, mode: _,
buf, buf,
action_state: _,
} = &self; } = &self;
vec![ActionDescription::new( vec![ActionDescription::new(
format!("Delete Nix related fragment from file `{}`", path.display()), format!("Delete Nix related fragment from file `{}`", path.display()),
@ -156,7 +161,6 @@ impl Action for CreateOrAppendFile {
group: _, group: _,
mode: _, mode: _,
buf, buf,
action_state: _,
} = self; } = self;
let mut file = OpenOptions::new() let mut file = OpenOptions::new()
.create(false) .create(false)
@ -190,14 +194,6 @@ impl Action for CreateOrAppendFile {
} }
Ok(()) 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)] #[derive(Debug, thiserror::Error)]

View file

@ -3,29 +3,31 @@ use tokio::process::Command;
use crate::execute_command; use crate::execute_command;
use crate::{ use crate::{
action::{Action, ActionDescription, ActionState}, action::{Action, ActionDescription, StatefulAction},
BoxableError, BoxableError,
}; };
/**
Create an operating system level user in the given group
*/
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
pub struct CreateUser { pub struct CreateUser {
name: String, name: String,
uid: usize, uid: usize,
groupname: String, groupname: String,
gid: usize, gid: usize,
action_state: ActionState,
} }
impl CreateUser { impl CreateUser {
#[tracing::instrument(skip_all)] #[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> {
Self { Self {
name, name,
uid, uid,
groupname, groupname,
gid, gid,
action_state: ActionState::Uncompleted,
} }
.into()
} }
} }
@ -59,7 +61,6 @@ impl Action for CreateUser {
uid, uid,
groupname, groupname,
gid, gid,
action_state: _,
} = self; } = self;
use target_lexicon::OperatingSystem; use target_lexicon::OperatingSystem;
@ -241,7 +242,6 @@ impl Action for CreateUser {
uid: _, uid: _,
groupname: _, groupname: _,
gid: _, gid: _,
action_state: _,
} = self; } = self;
use target_lexicon::OperatingSystem; use target_lexicon::OperatingSystem;
@ -277,14 +277,6 @@ impl Action for CreateUser {
Ok(()) 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)] #[derive(Debug, thiserror::Error)]

View file

@ -6,46 +6,38 @@ use reqwest::Url;
use tokio::task::JoinError; use tokio::task::JoinError;
use crate::{ use crate::{
action::{Action, ActionDescription, ActionState}, action::{Action, ActionDescription, StatefulAction},
BoxableError, BoxableError,
}; };
/**
Fetch a URL to the given path
*/
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
pub struct FetchNix { pub struct FetchAndUnpackNix {
url: Url, url: Url,
dest: PathBuf, dest: PathBuf,
action_state: ActionState,
} }
impl FetchNix { impl FetchAndUnpackNix {
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
pub async fn plan(url: Url, dest: PathBuf) -> Result<Self, FetchNixError> { pub async fn plan(url: Url, dest: PathBuf) -> Result<StatefulAction<Self>, FetchUrlError> {
// TODO(@hoverbear): Check URL exists? // TODO(@hoverbear): Check URL exists?
// TODO(@hoverbear): Check tempdir exists // TODO(@hoverbear): Check tempdir exists
Ok(Self { Ok(Self { url, dest }.into())
url,
dest,
action_state: ActionState::Uncompleted,
})
} }
} }
#[async_trait::async_trait] #[async_trait::async_trait]
#[typetag::serde(name = "fetch_nix")] #[typetag::serde(name = "fetch_and_unpack_nix")]
impl Action for FetchNix { impl Action for FetchAndUnpackNix {
fn tracing_synopsis(&self) -> String { fn tracing_synopsis(&self) -> String {
format!("Fetch Nix from `{}`", self.url) format!("Fetch `{}` to `{}`", self.url, self.dest.display())
} }
fn execute_description(&self) -> Vec<ActionDescription> { fn execute_description(&self) -> Vec<ActionDescription> {
vec![ActionDescription::new( vec![ActionDescription::new(self.tracing_synopsis(), vec![])]
self.tracing_synopsis(),
vec![format!(
"Unpack it to `{}` (moved later)",
self.dest.display()
)],
)]
} }
#[tracing::instrument(skip_all, fields( #[tracing::instrument(skip_all, fields(
@ -53,19 +45,15 @@ impl Action for FetchNix {
dest = %self.dest.display(), dest = %self.dest.display(),
))] ))]
async fn execute(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { async fn execute(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let Self { let Self { url, dest } = self;
url,
dest,
action_state: _,
} = self;
let res = reqwest::get(url.clone()) let res = reqwest::get(url.clone())
.await .await
.map_err(|e| FetchNixError::Reqwest(e).boxed())?; .map_err(|e| FetchUrlError::Reqwest(e).boxed())?;
let bytes = res let bytes = res
.bytes() .bytes()
.await .await
.map_err(|e| FetchNixError::Reqwest(e).boxed())?; .map_err(|e| FetchUrlError::Reqwest(e).boxed())?;
// TODO(@Hoverbear): Pick directory // TODO(@Hoverbear): Pick directory
tracing::trace!("Unpacking tar.xz"); tracing::trace!("Unpacking tar.xz");
let dest_clone = dest.clone(); let dest_clone = dest.clone();
@ -74,7 +62,7 @@ impl Action for FetchNix {
let mut archive = tar::Archive::new(decoder); let mut archive = tar::Archive::new(decoder);
archive archive
.unpack(&dest_clone) .unpack(&dest_clone)
.map_err(|e| FetchNixError::Unarchive(e).boxed())?; .map_err(|e| FetchUrlError::Unarchive(e).boxed())?;
Ok(()) Ok(())
} }
@ -88,26 +76,14 @@ impl Action for FetchNix {
dest = %self.dest.display(), dest = %self.dest.display(),
))] ))]
async fn revert(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { async fn revert(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let Self { let Self { url: _, dest: _ } = self;
url: _,
dest: _,
action_state: _,
} = self;
Ok(()) 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)] #[derive(Debug, thiserror::Error)]
pub enum FetchNixError { pub enum FetchUrlError {
#[error("Joining spawned async task")] #[error("Joining spawned async task")]
Join( Join(
#[source] #[source]

View file

@ -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_directory;
mod create_file; mod create_file;
mod create_group; mod create_group;
mod create_or_append_file; mod create_or_append_file;
mod create_user; mod create_user;
mod fetch_nix; mod fetch_and_unpack_nix;
mod move_unpacked_nix; mod move_unpacked_nix;
mod setup_default_profile; mod setup_default_profile;
pub use configure_nix_daemon_service::{ConfigureNixDaemonService, ConfigureNixDaemonServiceError};
pub use create_directory::{CreateDirectory, CreateDirectoryError}; pub use create_directory::{CreateDirectory, CreateDirectoryError};
pub use create_file::{CreateFile, CreateFileError}; pub use create_file::{CreateFile, CreateFileError};
pub use create_group::{CreateGroup, CreateGroupError}; pub use create_group::{CreateGroup, CreateGroupError};
pub use create_or_append_file::{CreateOrAppendFile, CreateOrAppendFileError}; pub use create_or_append_file::{CreateOrAppendFile, CreateOrAppendFileError};
pub use create_user::{CreateUser, CreateUserError}; 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 move_unpacked_nix::{MoveUnpackedNix, MoveUnpackedNixError};
pub use setup_default_profile::{SetupDefaultProfile, SetupDefaultProfileError}; pub use setup_default_profile::{SetupDefaultProfile, SetupDefaultProfileError};

View file

@ -1,26 +1,25 @@
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use crate::{ use crate::{
action::{Action, ActionDescription, ActionState}, action::{Action, ActionDescription, StatefulAction},
BoxableError, BoxableError,
}; };
const DEST: &str = "/nix/store"; const DEST: &str = "/nix/store";
/**
Move an unpacked Nix at `src` to `/nix`
*/
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
pub struct MoveUnpackedNix { pub struct MoveUnpackedNix {
src: PathBuf, src: PathBuf,
action_state: ActionState,
} }
impl MoveUnpackedNix { impl MoveUnpackedNix {
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
pub async fn plan(src: PathBuf) -> Result<Self, MoveUnpackedNixError> { pub async fn plan(src: PathBuf) -> Result<StatefulAction<Self>, MoveUnpackedNixError> {
// Note: Do NOT try to check for the src/dest since the installer creates those // Note: Do NOT try to check for the src/dest since the installer creates those
Ok(Self { Ok(Self { src }.into())
src,
action_state: ActionState::Uncompleted,
})
} }
} }
@ -46,12 +45,7 @@ impl Action for MoveUnpackedNix {
dest = DEST, dest = DEST,
))] ))]
async fn execute(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { async fn execute(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let Self { src, action_state } = self; let Self { src } = self;
if *action_state == ActionState::Completed {
tracing::trace!("Already completed: Moving Nix");
return Ok(());
}
tracing::debug!("Moving Nix");
// TODO(@Hoverbear): I would like to make this less awful // TODO(@Hoverbear): I would like to make this less awful
let found_nix_paths = glob::glob(&format!("{}/nix-*", src.display())) let found_nix_paths = glob::glob(&format!("{}/nix-*", src.display()))
@ -76,8 +70,7 @@ impl Action for MoveUnpackedNix {
tokio::fs::remove_dir_all(src) tokio::fs::remove_dir_all(src)
.await .await
.map_err(|e| MoveUnpackedNixError::Rename(src_store, dest.to_owned(), e).boxed())?; .map_err(|e| MoveUnpackedNixError::Rename(src_store, dest.to_owned(), e).boxed())?;
tracing::trace!("Moved Nix");
*action_state = ActionState::Completed;
Ok(()) Ok(())
} }
@ -93,14 +86,6 @@ impl Action for MoveUnpackedNix {
// Noop // Noop
Ok(()) 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)] #[derive(Debug, thiserror::Error)]

View file

@ -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; use glob::glob;
@ -6,19 +6,20 @@ use tokio::process::Command;
use crate::action::{Action, ActionDescription}; use crate::action::{Action, ActionDescription};
/**
Setup the default Nix profile with `nss-cacert` and `nix` itself.
*/
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
pub struct SetupDefaultProfile { pub struct SetupDefaultProfile {
channels: Vec<String>, channels: Vec<String>,
action_state: ActionState,
} }
impl SetupDefaultProfile { impl SetupDefaultProfile {
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
pub async fn plan(channels: Vec<String>) -> Result<Self, SetupDefaultProfileError> { pub async fn plan(
Ok(Self { channels: Vec<String>,
channels, ) -> Result<StatefulAction<Self>, SetupDefaultProfileError> {
action_state: ActionState::Uncompleted, Ok(Self { channels }.into())
})
} }
} }
@ -37,10 +38,7 @@ impl Action for SetupDefaultProfile {
channels = %self.channels.join(","), channels = %self.channels.join(","),
))] ))]
async fn execute(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { async fn execute(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let Self { let Self { channels } = self;
channels,
action_state: _,
} = self;
// Find an `nix` package // Find an `nix` package
let nix_pkg_glob = "/nix/store/*-nix-*"; let nix_pkg_glob = "/nix/store/*-nix-*";
@ -159,14 +157,6 @@ impl Action for SetupDefaultProfile {
Ok(()) 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)] #[derive(Debug, thiserror::Error)]

View file

@ -1,30 +1,34 @@
use crate::{ use crate::{
action::{ action::{
base::{ConfigureNixDaemonService, SetupDefaultProfile}, base::SetupDefaultProfile,
common::{ConfigureShellProfile, PlaceChannelConfiguration, PlaceNixConfiguration}, common::{ConfigureShellProfile, PlaceChannelConfiguration, PlaceNixConfiguration},
Action, ActionDescription, ActionImplementation, ActionState, linux::ConfigureNixDaemonService,
Action, ActionDescription, StatefulAction,
}, },
channel_value::ChannelValue, channel_value::ChannelValue,
BoxableError, CommonSettings, settings::CommonSettings,
BoxableError,
}; };
use reqwest::Url; use reqwest::Url;
/**
Configure Nix and start it
*/
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
pub struct ConfigureNix { pub struct ConfigureNix {
setup_default_profile: SetupDefaultProfile, setup_default_profile: StatefulAction<SetupDefaultProfile>,
configure_shell_profile: Option<ConfigureShellProfile>, configure_shell_profile: Option<StatefulAction<ConfigureShellProfile>>,
place_channel_configuration: PlaceChannelConfiguration, place_channel_configuration: StatefulAction<PlaceChannelConfiguration>,
place_nix_configuration: PlaceNixConfiguration, place_nix_configuration: StatefulAction<PlaceNixConfiguration>,
configure_nix_daemon_service: ConfigureNixDaemonService, configure_nix_daemon_service: StatefulAction<ConfigureNixDaemonService>,
action_state: ActionState,
} }
impl ConfigureNix { impl ConfigureNix {
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
pub async fn plan( pub async fn plan(
settings: &CommonSettings, settings: &CommonSettings,
) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> { ) -> Result<StatefulAction<Self>, Box<dyn std::error::Error + Send + Sync>> {
let channels: Vec<(String, Url)> = settings let channels: Vec<(String, Url)> = settings
.channels .channels
.iter() .iter()
@ -57,8 +61,8 @@ impl ConfigureNix {
setup_default_profile, setup_default_profile,
configure_nix_daemon_service, configure_nix_daemon_service,
configure_shell_profile, configure_shell_profile,
action_state: ActionState::Uncompleted, }
}) .into())
} }
} }
@ -76,15 +80,14 @@ impl Action for ConfigureNix {
place_nix_configuration, place_nix_configuration,
place_channel_configuration, place_channel_configuration,
configure_shell_profile, configure_shell_profile,
action_state: _,
} = &self; } = &self;
let mut buf = setup_default_profile.execute_description(); let mut buf = setup_default_profile.describe_execute();
buf.append(&mut configure_nix_daemon_service.execute_description()); buf.append(&mut configure_nix_daemon_service.describe_execute());
buf.append(&mut place_nix_configuration.execute_description()); buf.append(&mut place_nix_configuration.describe_execute());
buf.append(&mut place_channel_configuration.execute_description()); buf.append(&mut place_channel_configuration.describe_execute());
if let Some(configure_shell_profile) = configure_shell_profile { 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 buf
} }
@ -97,7 +100,6 @@ impl Action for ConfigureNix {
place_nix_configuration, place_nix_configuration,
place_channel_configuration, place_channel_configuration,
configure_shell_profile, configure_shell_profile,
action_state: _,
} = self; } = self;
if let Some(configure_shell_profile) = configure_shell_profile { if let Some(configure_shell_profile) = configure_shell_profile {
@ -126,17 +128,16 @@ impl Action for ConfigureNix {
place_nix_configuration, place_nix_configuration,
place_channel_configuration, place_channel_configuration,
configure_shell_profile, configure_shell_profile,
action_state: _,
} = &self; } = &self;
let mut buf = Vec::default(); let mut buf = Vec::default();
if let Some(configure_shell_profile) = configure_shell_profile { 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_channel_configuration.describe_revert());
buf.append(&mut place_nix_configuration.revert_description()); buf.append(&mut place_nix_configuration.describe_revert());
buf.append(&mut configure_nix_daemon_service.revert_description()); buf.append(&mut configure_nix_daemon_service.describe_revert());
buf.append(&mut setup_default_profile.revert_description()); buf.append(&mut setup_default_profile.describe_revert());
buf buf
} }
@ -149,7 +150,6 @@ impl Action for ConfigureNix {
place_nix_configuration, place_nix_configuration,
place_channel_configuration, place_channel_configuration,
configure_shell_profile, configure_shell_profile,
action_state: _,
} = self; } = self;
configure_nix_daemon_service.try_revert().await?; configure_nix_daemon_service.try_revert().await?;
@ -162,12 +162,4 @@ impl Action for ConfigureNix {
Ok(()) Ok(())
} }
fn action_state(&self) -> ActionState {
self.action_state
}
fn set_action_state(&mut self, action_state: ActionState) {
self.action_state = action_state;
}
} }

View file

@ -1,5 +1,5 @@
use crate::action::base::{CreateOrAppendFile, CreateOrAppendFileError}; use crate::action::base::{CreateOrAppendFile, CreateOrAppendFileError};
use crate::action::{Action, ActionDescription, ActionImplementation, ActionState}; use crate::action::{Action, ActionDescription, StatefulAction};
use crate::BoxableError; use crate::BoxableError;
use std::path::Path; 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"; 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)] #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
pub struct ConfigureShellProfile { pub struct ConfigureShellProfile {
create_or_append_files: Vec<CreateOrAppendFile>, create_or_append_files: Vec<StatefulAction<CreateOrAppendFile>>,
action_state: ActionState,
} }
impl ConfigureShellProfile { impl ConfigureShellProfile {
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
pub async fn plan() -> Result<Self, Box<dyn std::error::Error + Send + Sync>> { pub async fn plan() -> Result<StatefulAction<Self>, Box<dyn std::error::Error + Send + Sync>> {
let mut create_or_append_files = Vec::default(); let mut create_or_append_files = Vec::default();
for profile_target in PROFILE_TARGETS { for profile_target in PROFILE_TARGETS {
let path = Path::new(profile_target); let path = Path::new(profile_target);
@ -49,8 +51,8 @@ impl ConfigureShellProfile {
Ok(Self { Ok(Self {
create_or_append_files, create_or_append_files,
action_state: ActionState::Uncompleted, }
}) .into())
} }
} }
@ -72,7 +74,6 @@ impl Action for ConfigureShellProfile {
async fn execute(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { async fn execute(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let Self { let Self {
create_or_append_files, create_or_append_files,
action_state: _,
} = self; } = self;
let mut set = JoinSet::new(); let mut set = JoinSet::new();
@ -121,7 +122,6 @@ impl Action for ConfigureShellProfile {
async fn revert(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { async fn revert(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let Self { let Self {
create_or_append_files, create_or_append_files,
action_state: _,
} = self; } = self;
let mut set = JoinSet::new(); 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() { 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 mut create_or_append_file_clone = create_or_append_file.clone();
let _abort_handle = set.spawn(async move { let _abort_handle = set.spawn(async move {
create_or_append_file_clone.revert().await?; create_or_append_file_clone.try_revert().await?;
Result::<_, Box<dyn std::error::Error + Send + Sync>>::Ok(( Result::<_, Box<dyn std::error::Error + Send + Sync>>::Ok((
idx, idx,
create_or_append_file_clone, create_or_append_file_clone,
@ -158,14 +158,6 @@ impl Action for ConfigureShellProfile {
Ok(()) 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)] #[derive(Debug, thiserror::Error)]

View file

@ -1,5 +1,5 @@
use crate::action::base::{CreateDirectory, CreateDirectoryError}; use crate::action::base::{CreateDirectory, CreateDirectoryError};
use crate::action::{Action, ActionDescription, ActionImplementation, ActionState}; use crate::action::{Action, ActionDescription, StatefulAction};
const PATHS: &[&str] = &[ const PATHS: &[&str] = &[
"/nix/var", "/nix/var",
@ -17,25 +17,24 @@ const PATHS: &[&str] = &[
"/nix/var/nix/daemon-socket", "/nix/var/nix/daemon-socket",
]; ];
/**
Create the `/nix` tree
*/
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
pub struct CreateNixTree { pub struct CreateNixTree {
create_directories: Vec<CreateDirectory>, create_directories: Vec<StatefulAction<CreateDirectory>>,
action_state: ActionState,
} }
impl CreateNixTree { impl CreateNixTree {
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
pub async fn plan() -> Result<Self, Box<dyn std::error::Error + Send + Sync>> { pub async fn plan() -> Result<StatefulAction<Self>, Box<dyn std::error::Error + Send + Sync>> {
let mut create_directories = Vec::default(); let mut create_directories = Vec::default();
for path in PATHS { for path in PATHS {
// We use `create_dir` over `create_dir_all` to ensure we always set permissions right // 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?) create_directories.push(CreateDirectory::plan(path, None, None, 0o0755, false).await?)
} }
Ok(Self { Ok(Self { create_directories }.into())
create_directories,
action_state: ActionState::Uncompleted,
})
} }
} }
@ -67,10 +66,7 @@ impl Action for CreateNixTree {
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
async fn execute(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { async fn execute(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let Self { let Self { create_directories } = self;
create_directories,
action_state: _,
} = self;
// Just do sequential since parallelizing this will have little benefit // Just do sequential since parallelizing this will have little benefit
for create_directory in create_directories { for create_directory in create_directories {
@ -102,26 +98,15 @@ impl Action for CreateNixTree {
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
async fn revert(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { async fn revert(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let Self { let Self { create_directories } = self;
create_directories,
action_state: _,
} = self;
// Just do sequential since parallelizing this will have little benefit // Just do sequential since parallelizing this will have little benefit
for create_directory in create_directories.iter_mut().rev() { for create_directory in create_directories.iter_mut().rev() {
create_directory.revert().await? create_directory.try_revert().await?
} }
Ok(()) 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)] #[derive(Debug, thiserror::Error)]

View file

@ -1,28 +1,29 @@
use crate::CommonSettings;
use crate::{ use crate::{
action::{ action::{
base::{CreateGroup, CreateGroupError, CreateUser, CreateUserError}, base::{CreateGroup, CreateGroupError, CreateUser, CreateUserError},
Action, ActionDescription, ActionImplementation, ActionState, Action, ActionDescription, StatefulAction,
}, },
settings::CommonSettings,
BoxableError, BoxableError,
}; };
use tokio::task::{JoinError, JoinSet}; use tokio::task::{JoinError, JoinSet};
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
pub struct CreateUsersAndGroup { pub struct CreateUsersAndGroups {
daemon_user_count: usize, daemon_user_count: usize,
nix_build_group_name: String, nix_build_group_name: String,
nix_build_group_id: usize, nix_build_group_id: usize,
nix_build_user_prefix: String, nix_build_user_prefix: String,
nix_build_user_id_base: usize, nix_build_user_id_base: usize,
create_group: CreateGroup, create_group: StatefulAction<CreateGroup>,
create_users: Vec<CreateUser>, create_users: Vec<StatefulAction<CreateUser>>,
action_state: ActionState,
} }
impl CreateUsersAndGroup { impl CreateUsersAndGroups {
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
pub async fn plan(settings: CommonSettings) -> Result<Self, CreateUsersAndGroupError> { pub async fn plan(
settings: CommonSettings,
) -> Result<StatefulAction<Self>, CreateUsersAndGroupsError> {
// TODO(@hoverbear): CHeck if it exist, error if so // TODO(@hoverbear): CHeck if it exist, error if so
let create_group = CreateGroup::plan( let create_group = CreateGroup::plan(
settings.nix_build_group_name.clone(), settings.nix_build_group_name.clone(),
@ -47,14 +48,14 @@ impl CreateUsersAndGroup {
nix_build_user_id_base: settings.nix_build_user_id_base, nix_build_user_id_base: settings.nix_build_user_id_base,
create_group, create_group,
create_users, create_users,
action_state: ActionState::Uncompleted, }
}) .into())
} }
} }
#[async_trait::async_trait] #[async_trait::async_trait]
#[typetag::serde(name = "create_users_and_group")] #[typetag::serde(name = "create_users_and_group")]
impl Action for CreateUsersAndGroup { impl Action for CreateUsersAndGroups {
fn tracing_synopsis(&self) -> String { fn tracing_synopsis(&self) -> String {
format!( format!(
"Create build users (UID {}-{}) and group (GID {})", "Create build users (UID {}-{}) and group (GID {})",
@ -73,7 +74,6 @@ impl Action for CreateUsersAndGroup {
nix_build_user_id_base: _, nix_build_user_id_base: _,
create_group, create_group,
create_users, create_users,
action_state: _,
} = &self; } = &self;
let mut create_users_descriptions = Vec::new(); let mut create_users_descriptions = Vec::new();
@ -110,7 +110,6 @@ impl Action for CreateUsersAndGroup {
nix_build_group_id: _, nix_build_group_id: _,
nix_build_user_prefix: _, nix_build_user_prefix: _,
nix_build_user_id_base: _, nix_build_user_id_base: _,
action_state: _,
} = self; } = self;
// Create group // Create group
@ -152,7 +151,7 @@ impl Action for CreateUsersAndGroup {
if errors.len() == 1 { if errors.len() == 1 {
return Err(errors.into_iter().next().unwrap().into()); return Err(errors.into_iter().next().unwrap().into());
} else { } 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: _, nix_build_user_id_base: _,
create_group, create_group,
create_users, create_users,
action_state: _,
} = &self; } = &self;
if self.action_state == ActionState::Uncompleted { let mut create_users_descriptions = Vec::new();
vec![] for create_user in create_users {
} else { if let Some(val) = create_user.describe_revert().iter().next() {
let mut create_users_descriptions = Vec::new(); create_users_descriptions.push(val.description.clone())
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( #[tracing::instrument(skip_all, fields(
@ -213,7 +207,6 @@ impl Action for CreateUsersAndGroup {
nix_build_group_id: _, nix_build_group_id: _,
nix_build_user_prefix: _, nix_build_user_prefix: _,
nix_build_user_id_base: _, nix_build_user_id_base: _,
action_state: _,
} = self; } = self;
let mut set = JoinSet::new(); let mut set = JoinSet::new();
@ -222,7 +215,7 @@ impl Action for CreateUsersAndGroup {
for (idx, create_user) in create_users.iter().enumerate() { for (idx, create_user) in create_users.iter().enumerate() {
let mut create_user_clone = create_user.clone(); let mut create_user_clone = create_user.clone();
let _abort_handle = set.spawn(async move { let _abort_handle = set.spawn(async move {
create_user_clone.revert().await?; create_user_clone.try_revert().await?;
Result::<_, Box<dyn std::error::Error + Send + Sync>>::Ok((idx, create_user_clone)) Result::<_, Box<dyn std::error::Error + Send + Sync>>::Ok((idx, create_user_clone))
}); });
} }
@ -239,27 +232,19 @@ impl Action for CreateUsersAndGroup {
if errors.len() == 1 { if errors.len() == 1 {
return Err(errors.into_iter().next().unwrap().into()); return Err(errors.into_iter().next().unwrap().into());
} else { } else {
return Err(CreateUsersAndGroupError::CreateUsers(errors).boxed()); return Err(CreateUsersAndGroupsError::CreateUsers(errors).boxed());
} }
} }
// Create group // Create group
create_group.revert().await?; create_group.try_revert().await?;
Ok(()) 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)] #[derive(Debug, thiserror::Error)]
pub enum CreateUsersAndGroupError { pub enum CreateUsersAndGroupsError {
#[error("Creating user")] #[error("Creating user")]
CreateUser( CreateUser(
#[source] #[source]

View file

@ -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_nix;
mod configure_shell_profile; mod configure_shell_profile;
mod create_nix_tree; mod create_nix_tree;
mod create_users_and_group; mod create_users_and_groups;
mod place_channel_configuration; mod place_channel_configuration;
mod place_nix_configuration; mod place_nix_configuration;
mod provision_nix; mod provision_nix;
@ -11,7 +11,7 @@ mod provision_nix;
pub use configure_nix::ConfigureNix; pub use configure_nix::ConfigureNix;
pub use configure_shell_profile::ConfigureShellProfile; pub use configure_shell_profile::ConfigureShellProfile;
pub use create_nix_tree::{CreateNixTree, CreateNixTreeError}; 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_channel_configuration::{PlaceChannelConfiguration, PlaceChannelConfigurationError};
pub use place_nix_configuration::{PlaceNixConfiguration, PlaceNixConfigurationError}; pub use place_nix_configuration::{PlaceNixConfiguration, PlaceNixConfigurationError};
pub use provision_nix::{ProvisionNix, ProvisionNixError}; pub use provision_nix::{ProvisionNix, ProvisionNixError};

View file

@ -1,15 +1,17 @@
use crate::action::base::{CreateFile, CreateFileError}; use crate::action::base::{CreateFile, CreateFileError};
use crate::{ use crate::{
action::{Action, ActionDescription, ActionImplementation, ActionState}, action::{Action, ActionDescription, StatefulAction},
BoxableError, BoxableError,
}; };
use reqwest::Url; use reqwest::Url;
/**
Place a channel configuration containing `channels` to the `$ROOT_HOME/.nix-channels` file
*/
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
pub struct PlaceChannelConfiguration { pub struct PlaceChannelConfiguration {
channels: Vec<(String, Url)>, channels: Vec<(String, Url)>,
create_file: CreateFile, create_file: StatefulAction<CreateFile>,
action_state: ActionState,
} }
impl PlaceChannelConfiguration { impl PlaceChannelConfiguration {
@ -17,7 +19,7 @@ impl PlaceChannelConfiguration {
pub async fn plan( pub async fn plan(
channels: Vec<(String, Url)>, channels: Vec<(String, Url)>,
force: bool, force: bool,
) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> { ) -> Result<StatefulAction<Self>, Box<dyn std::error::Error + Send + Sync>> {
let buf = channels let buf = channels
.iter() .iter()
.map(|(name, url)| format!("{} {}", url, name)) .map(|(name, url)| format!("{} {}", url, name))
@ -37,8 +39,8 @@ impl PlaceChannelConfiguration {
Ok(Self { Ok(Self {
create_file, create_file,
channels, channels,
action_state: ActionState::Uncompleted, }
}) .into())
} }
} }
@ -48,7 +50,7 @@ impl Action for PlaceChannelConfiguration {
fn tracing_synopsis(&self) -> String { fn tracing_synopsis(&self) -> String {
format!( format!(
"Place channel configuration at `{}`", "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 { let Self {
create_file, create_file,
channels: _, channels: _,
action_state: _,
} = self; } = self;
create_file.try_execute().await?; create_file.try_execute().await?;
@ -75,7 +76,7 @@ impl Action for PlaceChannelConfiguration {
vec![ActionDescription::new( vec![ActionDescription::new(
format!( format!(
"Remove channel configuration at `{}`", "Remove channel configuration at `{}`",
self.create_file.path.display() self.create_file.inner().path.display()
), ),
vec![], vec![],
)] )]
@ -88,21 +89,12 @@ impl Action for PlaceChannelConfiguration {
let Self { let Self {
create_file, create_file,
channels: _, channels: _,
action_state: _,
} = self; } = self;
create_file.revert().await?; create_file.try_revert().await?;
Ok(()) 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)] #[derive(Debug, thiserror::Error)]

View file

@ -1,14 +1,16 @@
use crate::action::base::{CreateDirectory, CreateDirectoryError, CreateFile, CreateFileError}; 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_FOLDER: &str = "/etc/nix";
const NIX_CONF: &str = "/etc/nix/nix.conf"; const NIX_CONF: &str = "/etc/nix/nix.conf";
/**
Place the `/etc/nix.conf` file
*/
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
pub struct PlaceNixConfiguration { pub struct PlaceNixConfiguration {
create_directory: CreateDirectory, create_directory: StatefulAction<CreateDirectory>,
create_file: CreateFile, create_file: StatefulAction<CreateFile>,
action_state: ActionState,
} }
impl PlaceNixConfiguration { impl PlaceNixConfiguration {
@ -17,7 +19,7 @@ impl PlaceNixConfiguration {
nix_build_group_name: String, nix_build_group_name: String,
extra_conf: Option<String>, extra_conf: Option<String>,
force: bool, force: bool,
) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> { ) -> Result<StatefulAction<Self>, Box<dyn std::error::Error + Send + Sync>> {
let buf = format!( let buf = format!(
"\ "\
{extra_conf}\n\ {extra_conf}\n\
@ -36,8 +38,8 @@ impl PlaceNixConfiguration {
Ok(Self { Ok(Self {
create_directory, create_directory,
create_file, create_file,
action_state: ActionState::Uncompleted, }
}) .into())
} }
} }
@ -63,7 +65,6 @@ impl Action for PlaceNixConfiguration {
let Self { let Self {
create_file, create_file,
create_directory, create_directory,
action_state: _,
} = self; } = self;
create_directory.try_execute().await?; create_directory.try_execute().await?;
@ -87,22 +88,13 @@ impl Action for PlaceNixConfiguration {
let Self { let Self {
create_file, create_file,
create_directory, create_directory,
action_state: _,
} = self; } = self;
create_file.revert().await?; create_file.try_revert().await?;
create_directory.revert().await?; create_directory.try_revert().await?;
Ok(()) 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)] #[derive(Debug, thiserror::Error)]

View file

@ -1,37 +1,42 @@
use crate::action::base::{
CreateDirectoryError, FetchNix, FetchNixError, MoveUnpackedNix, MoveUnpackedNixError,
};
use crate::CommonSettings;
use crate::{ use crate::{
action::{Action, ActionDescription, ActionImplementation, ActionState}, action::{
base::{
CreateDirectoryError, FetchAndUnpackNix, FetchUrlError, MoveUnpackedNix,
MoveUnpackedNixError,
},
Action, ActionDescription, StatefulAction,
},
settings::CommonSettings,
BoxableError, BoxableError,
}; };
use std::path::PathBuf; use std::path::PathBuf;
use tokio::task::JoinError; 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)] #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
pub struct ProvisionNix { pub struct ProvisionNix {
fetch_nix: FetchNix, fetch_nix: StatefulAction<FetchAndUnpackNix>,
create_users_and_group: CreateUsersAndGroup, create_users_and_group: StatefulAction<CreateUsersAndGroups>,
create_nix_tree: CreateNixTree, create_nix_tree: StatefulAction<CreateNixTree>,
move_unpacked_nix: MoveUnpackedNix, move_unpacked_nix: StatefulAction<MoveUnpackedNix>,
action_state: ActionState,
} }
impl ProvisionNix { impl ProvisionNix {
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
pub async fn plan( pub async fn plan(
settings: &CommonSettings, settings: &CommonSettings,
) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> { ) -> Result<StatefulAction<Self>, Box<dyn std::error::Error + Send + Sync>> {
let fetch_nix = FetchNix::plan( let fetch_nix = FetchAndUnpackNix::plan(
settings.nix_package_url.clone(), settings.nix_package_url.clone(),
PathBuf::from("/nix/temp-install-dir"), PathBuf::from("/nix/temp-install-dir"),
) )
.await .await
.map_err(|e| e.boxed())?; .map_err(|e| e.boxed())?;
let create_users_and_group = CreateUsersAndGroup::plan(settings.clone()) let create_users_and_group = CreateUsersAndGroups::plan(settings.clone())
.await .await
.map_err(|e| e.boxed())?; .map_err(|e| e.boxed())?;
let create_nix_tree = CreateNixTree::plan().await?; let create_nix_tree = CreateNixTree::plan().await?;
@ -43,8 +48,8 @@ impl ProvisionNix {
create_users_and_group, create_users_and_group,
create_nix_tree, create_nix_tree,
move_unpacked_nix, move_unpacked_nix,
action_state: ActionState::Uncompleted, }
}) .into())
} }
} }
@ -61,14 +66,13 @@ impl Action for ProvisionNix {
create_users_and_group, create_users_and_group,
create_nix_tree, create_nix_tree,
move_unpacked_nix, move_unpacked_nix,
action_state: _,
} = &self; } = &self;
let mut buf = Vec::default(); let mut buf = Vec::default();
buf.append(&mut fetch_nix.execute_description()); buf.append(&mut fetch_nix.describe_execute());
buf.append(&mut create_users_and_group.execute_description()); buf.append(&mut create_users_and_group.describe_execute());
buf.append(&mut create_nix_tree.execute_description()); buf.append(&mut create_nix_tree.describe_execute());
buf.append(&mut move_unpacked_nix.execute_description()); buf.append(&mut move_unpacked_nix.describe_execute());
buf buf
} }
@ -80,7 +84,6 @@ impl Action for ProvisionNix {
create_nix_tree, create_nix_tree,
create_users_and_group, create_users_and_group,
move_unpacked_nix, move_unpacked_nix,
action_state: _,
} = self; } = self;
// We fetch nix while doing the rest, then move it over. // We fetch nix while doing the rest, then move it over.
@ -105,14 +108,13 @@ impl Action for ProvisionNix {
create_users_and_group, create_users_and_group,
create_nix_tree, create_nix_tree,
move_unpacked_nix, move_unpacked_nix,
action_state: _,
} = &self; } = &self;
let mut buf = Vec::default(); let mut buf = Vec::default();
buf.append(&mut move_unpacked_nix.revert_description()); buf.append(&mut move_unpacked_nix.describe_revert());
buf.append(&mut create_nix_tree.revert_description()); buf.append(&mut create_nix_tree.describe_revert());
buf.append(&mut create_users_and_group.revert_description()); buf.append(&mut create_users_and_group.describe_revert());
buf.append(&mut fetch_nix.revert_description()); buf.append(&mut fetch_nix.describe_revert());
buf buf
} }
@ -123,7 +125,6 @@ impl Action for ProvisionNix {
create_nix_tree, create_nix_tree,
create_users_and_group, create_users_and_group,
move_unpacked_nix, move_unpacked_nix,
action_state: _,
} = self; } = self;
// We fetch nix while doing the rest, then move it over. // 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())??; *fetch_nix = fetch_nix_handle.await.map_err(|e| e.boxed())??;
move_unpacked_nix.revert().await?; move_unpacked_nix.try_revert().await?;
Ok(()) 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)] #[derive(Debug, thiserror::Error)]
@ -163,7 +156,7 @@ pub enum ProvisionNixError {
FetchNix( FetchNix(
#[source] #[source]
#[from] #[from]
FetchNixError, FetchUrlError,
), ),
#[error("Joining spawned async task")] #[error("Joining spawned async task")]
Join( Join(
@ -181,7 +174,7 @@ pub enum ProvisionNixError {
CreateUsersAndGroup( CreateUsersAndGroup(
#[source] #[source]
#[from] #[from]
CreateUsersAndGroupError, CreateUsersAndGroupsError,
), ),
#[error("Creating nix tree")] #[error("Creating nix tree")]
CreateNixTree( CreateNixTree(

View file

@ -2,34 +2,37 @@ use std::path::{Path, PathBuf};
use tokio::process::Command; use tokio::process::Command;
use crate::action::StatefulAction;
use crate::execute_command; use crate::execute_command;
use crate::{ use crate::{
action::{Action, ActionDescription, ActionState}, action::{Action, ActionDescription},
BoxableError, BoxableError,
}; };
/**
Bootstrap and kickstart an APFS volume
*/
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
pub struct BootstrapVolume { pub struct BootstrapApfsVolume {
path: PathBuf, path: PathBuf,
action_state: ActionState,
} }
impl BootstrapVolume { impl BootstrapApfsVolume {
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
pub async fn plan( pub async fn plan(
path: impl AsRef<Path>, path: impl AsRef<Path>,
) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> { ) -> Result<StatefulAction<Self>, Box<dyn std::error::Error + Send + Sync>> {
Ok(Self { Ok(Self {
path: path.as_ref().to_path_buf(), path: path.as_ref().to_path_buf(),
action_state: ActionState::Uncompleted, }
}) .into())
} }
} }
#[async_trait::async_trait] #[async_trait::async_trait]
#[typetag::serde(name = "bootstrap_volume")] #[typetag::serde(name = "bootstrap_volume")]
impl Action for BootstrapVolume { impl Action for BootstrapApfsVolume {
fn tracing_synopsis(&self) -> String { fn tracing_synopsis(&self) -> String {
format!("Bootstrap and kickstart `{}`", self.path.display()) format!("Bootstrap and kickstart `{}`", self.path.display())
} }
@ -42,10 +45,7 @@ impl Action for BootstrapVolume {
path = %self.path.display(), path = %self.path.display(),
))] ))]
async fn execute(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { async fn execute(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let Self { let Self { path } = self;
path,
action_state: _,
} = self;
execute_command( execute_command(
Command::new("launchctl") Command::new("launchctl")
@ -79,10 +79,7 @@ impl Action for BootstrapVolume {
path = %self.path.display(), path = %self.path.display(),
))] ))]
async fn revert(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { async fn revert(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let Self { let Self { path } = self;
path,
action_state: _,
} = self;
execute_command( execute_command(
Command::new("launchctl") Command::new("launchctl")
@ -96,14 +93,6 @@ impl Action for BootstrapVolume {
Ok(()) 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)] #[derive(Debug, thiserror::Error)]

View file

@ -1,39 +1,20 @@
use crate::{ use std::path::{Path, PathBuf};
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 tokio::process::Command; 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)] #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
pub struct CreateApfsVolume { pub struct CreateApfsVolume {
disk: PathBuf, disk: PathBuf,
name: String, name: String,
case_sensitive: bool, 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<EncryptVolume>,
setup_volume_daemon: CreateFile,
bootstrap_volume: BootstrapVolume,
enable_ownership: EnableOwnership,
action_state: ActionState, action_state: ActionState,
} }
@ -43,246 +24,107 @@ impl CreateApfsVolume {
disk: impl AsRef<Path>, disk: impl AsRef<Path>,
name: String, name: String,
case_sensitive: bool, case_sensitive: bool,
encrypt: bool, ) -> Result<StatefulAction<Self>, Box<dyn std::error::Error + Send + Sync>> {
) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
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!(
"\
<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\
<!DOCTYPE plist PUBLIC \"-//Apple Computer//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n\
<plist version=\"1.0\">\n\
<dict>\n\
<key>RunAtLoad</key>\n\
<true/>\n\
<key>Label</key>\n\
<string>org.nixos.darwin-store</string>\n\
<key>ProgramArguments</key>\n\
<array>\n\
{}\
</array>\n\
</dict>\n\
</plist>\n\
\
", mount_command.iter().map(|v| format!("<string>{v}</string>\n")).collect::<Vec<_>>().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?;
Ok(Self { Ok(Self {
disk: disk.to_path_buf(), disk: disk.as_ref().to_path_buf(),
name, name,
case_sensitive, 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, action_state: ActionState::Uncompleted,
}) }
.into())
} }
} }
#[async_trait::async_trait] #[async_trait::async_trait]
#[typetag::serde(name = "create_apfs_volume")] #[typetag::serde(name = "create_volume")]
impl Action for CreateApfsVolume { impl Action for CreateApfsVolume {
fn tracing_synopsis(&self) -> String { fn tracing_synopsis(&self) -> String {
format!( format!(
"Create an APFS volume `{}` on `{}`", "Create an APFS volume on `{}` named `{}`",
self.name, self.disk.display(),
self.disk.display() self.name
) )
} }
fn execute_description(&self) -> Vec<ActionDescription> { fn execute_description(&self) -> Vec<ActionDescription> {
let Self {
disk: _, name: _, ..
} = &self;
vec![ActionDescription::new(self.tracing_synopsis(), vec![])] 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<dyn std::error::Error + Send + Sync>> { async fn execute(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let Self { let Self {
disk: _, disk,
name: _, name,
case_sensitive: _, 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: _, action_state: _,
} = self; } = self;
create_or_append_synthetic_conf.try_execute().await?; execute_command(
create_synthetic_objects.try_execute().await?; Command::new("/usr/sbin/diskutil")
unmount_volume.try_execute().await.ok(); // We actually expect this may fail. .process_group(0)
create_volume.try_execute().await?; .args([
create_or_append_fstab.try_execute().await?; "apfs",
if let Some(encrypt_volume) = encrypt_volume { "addVolume",
encrypt_volume.try_execute().await?; &format!("{}", disk.display()),
} if !*case_sensitive {
setup_volume_daemon.try_execute().await?; "APFS"
} else {
bootstrap_volume.try_execute().await?; "Case-sensitive APFS"
},
let mut retry_tokens: usize = 50; name,
loop { "-nomount",
tracing::trace!(%retry_tokens, "Checking for Nix Store existence"); ])
let status = Command::new("/usr/sbin/diskutil") .stdin(std::process::Stdio::null()),
.args(["info", "/nix"]) )
.stderr(std::process::Stdio::null()) .await
.stdout(std::process::Stdio::null()) .map_err(|e| CreateVolumeError::Command(e).boxed())?;
.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(()) Ok(())
} }
fn revert_description(&self) -> Vec<ActionDescription> { fn revert_description(&self) -> Vec<ActionDescription> {
let Self { disk, name, .. } = &self;
vec![ActionDescription::new( vec![ActionDescription::new(
format!("Remove the APFS volume `{name}` on `{}`", disk.display()), format!(
vec![format!( "Remove the volume on `{}` named `{}`",
"Create a writable, persistent systemd system extension.", 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<dyn std::error::Error + Send + Sync>> { async fn revert(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let Self { let Self {
disk: _, disk: _,
name: _, name,
case_sensitive: _, 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: _, action_state: _,
} = self; } = self;
enable_ownership.try_revert().await?; execute_command(
bootstrap_volume.try_revert().await?; Command::new("/usr/sbin/diskutil")
setup_volume_daemon.try_revert().await?; .process_group(0)
if let Some(encrypt_volume) = encrypt_volume { .args(["apfs", "deleteVolume", name])
encrypt_volume.try_revert().await?; .stdin(std::process::Stdio::null()),
} )
create_or_append_fstab.try_revert().await?; .await
.map_err(|e| CreateVolumeError::Command(e).boxed())?;
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(()) 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)] #[derive(Debug, thiserror::Error)]
pub enum CreateApfsVolumeError { pub enum CreateVolumeError {
#[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")] #[error("Failed to execute command")]
Command(#[source] std::io::Error), Command(#[source] std::io::Error),
} }

View file

@ -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<CreateOrAppendFile>,
create_synthetic_objects: StatefulAction<CreateSyntheticObjects>,
unmount_volume: StatefulAction<UnmountApfsVolume>,
create_volume: StatefulAction<CreateApfsVolume>,
create_or_append_fstab: StatefulAction<CreateOrAppendFile>,
encrypt_volume: Option<StatefulAction<EncryptApfsVolume>>,
setup_volume_daemon: StatefulAction<CreateFile>,
bootstrap_volume: StatefulAction<BootstrapApfsVolume>,
enable_ownership: StatefulAction<EnableOwnership>,
}
impl CreateNixVolume {
#[tracing::instrument(skip_all)]
pub async fn plan(
disk: impl AsRef<Path>,
name: String,
case_sensitive: bool,
encrypt: bool,
) -> Result<StatefulAction<Self>, Box<dyn std::error::Error + Send + Sync>> {
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!(
"\
<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\
<!DOCTYPE plist PUBLIC \"-//Apple Computer//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n\
<plist version=\"1.0\">\n\
<dict>\n\
<key>RunAtLoad</key>\n\
<true/>\n\
<key>Label</key>\n\
<string>org.nixos.darwin-store</string>\n\
<key>ProgramArguments</key>\n\
<array>\n\
{}\
</array>\n\
</dict>\n\
</plist>\n\
\
", mount_command.iter().map(|v| format!("<string>{v}</string>\n")).collect::<Vec<_>>().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<ActionDescription> {
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<dyn std::error::Error + Send + Sync>> {
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<ActionDescription> {
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<dyn std::error::Error + Send + Sync>> {
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),
}

View file

@ -2,19 +2,16 @@ use tokio::process::Command;
use crate::execute_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)] #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
pub struct CreateSyntheticObjects { pub struct CreateSyntheticObjects;
action_state: ActionState,
}
impl CreateSyntheticObjects { impl CreateSyntheticObjects {
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
pub async fn plan() -> Result<Self, Box<dyn std::error::Error + Send + Sync>> { pub async fn plan() -> Result<StatefulAction<Self>, Box<dyn std::error::Error + Send + Sync>> {
Ok(Self { Ok(Self.into())
action_state: ActionState::Uncompleted,
})
} }
} }
@ -84,14 +81,6 @@ impl Action for CreateSyntheticObjects {
Ok(()) 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)] #[derive(Debug, thiserror::Error)]

View file

@ -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<Path>,
name: String,
case_sensitive: bool,
) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
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<ActionDescription> {
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<dyn std::error::Error + Send + Sync>> {
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<ActionDescription> {
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<dyn std::error::Error + Send + Sync>> {
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),
}

View file

@ -3,29 +3,32 @@ use std::path::{Path, PathBuf};
use tokio::process::Command; use tokio::process::Command;
use crate::action::StatefulAction;
use crate::execute_command; use crate::execute_command;
use crate::os::darwin::DiskUtilOutput; use crate::os::darwin::DiskUtilOutput;
use crate::{ use crate::{
action::{Action, ActionDescription, ActionState}, action::{Action, ActionDescription},
BoxableError, BoxableError,
}; };
/**
Enable ownership on a volume
*/
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
pub struct EnableOwnership { pub struct EnableOwnership {
path: PathBuf, path: PathBuf,
action_state: ActionState,
} }
impl EnableOwnership { impl EnableOwnership {
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
pub async fn plan( pub async fn plan(
path: impl AsRef<Path>, path: impl AsRef<Path>,
) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> { ) -> Result<StatefulAction<Self>, Box<dyn std::error::Error + Send + Sync>> {
Ok(Self { Ok(Self {
path: path.as_ref().to_path_buf(), path: path.as_ref().to_path_buf(),
action_state: ActionState::Uncompleted, }
}) .into())
} }
} }
@ -44,10 +47,7 @@ impl Action for EnableOwnership {
path = %self.path.display(), path = %self.path.display(),
))] ))]
async fn execute(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { async fn execute(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let Self { let Self { path } = self;
path,
action_state: _,
} = self;
let should_enable_ownership = { let should_enable_ownership = {
let buf = execute_command( let buf = execute_command(
@ -90,14 +90,6 @@ impl Action for EnableOwnership {
// noop // noop
Ok(()) 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)] #[derive(Debug, thiserror::Error)]

View file

@ -1,36 +1,38 @@
use crate::{ use crate::{
action::{darwin::NIX_VOLUME_MOUNTD_DEST, Action, ActionDescription, ActionState}, action::{darwin::NIX_VOLUME_MOUNTD_DEST, Action, ActionDescription, StatefulAction},
execute_command, execute_command,
}; };
use rand::Rng; use rand::Rng;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use tokio::process::Command; use tokio::process::Command;
/**
Encrypt an APFS volume
*/
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
pub struct EncryptVolume { pub struct EncryptApfsVolume {
disk: PathBuf, disk: PathBuf,
name: String, name: String,
action_state: ActionState,
} }
impl EncryptVolume { impl EncryptApfsVolume {
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
pub async fn plan( pub async fn plan(
disk: impl AsRef<Path>, disk: impl AsRef<Path>,
name: impl AsRef<str>, name: impl AsRef<str>,
) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> { ) -> Result<StatefulAction<Self>, Box<dyn std::error::Error + Send + Sync>> {
let name = name.as_ref().to_owned(); let name = name.as_ref().to_owned();
Ok(Self { Ok(Self {
name, name,
disk: disk.as_ref().to_path_buf(), disk: disk.as_ref().to_path_buf(),
action_state: ActionState::Uncompleted, }
}) .into())
} }
} }
#[async_trait::async_trait] #[async_trait::async_trait]
#[typetag::serde(name = "encrypt_volume")] #[typetag::serde(name = "encrypt_volume")]
impl Action for EncryptVolume { impl Action for EncryptApfsVolume {
fn tracing_synopsis(&self) -> String { fn tracing_synopsis(&self) -> String {
format!( format!(
"Encrypt volume `{}` on disk `{}`", "Encrypt volume `{}` on disk `{}`",
@ -47,11 +49,7 @@ impl Action for EncryptVolume {
disk = %self.disk.display(), disk = %self.disk.display(),
))] ))]
async fn execute(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { async fn execute(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let Self { let Self { disk, name } = self;
disk,
name,
action_state: _,
} = self;
// Generate a random password. // Generate a random password.
let password: String = { let password: String = {
@ -141,11 +139,7 @@ impl Action for EncryptVolume {
disk = %self.disk.display(), disk = %self.disk.display(),
))] ))]
async fn revert(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { async fn revert(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let Self { let Self { disk, name } = self;
disk,
name,
action_state: _,
} = self;
let disk_str = disk.to_str().expect("Could not turn disk into string"); /* Should not reasonably ever fail */ 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(()) 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)] #[derive(Debug, thiserror::Error)]

View file

@ -1,25 +1,27 @@
use tokio::process::Command; use tokio::process::Command;
use crate::action::StatefulAction;
use crate::execute_command; use crate::execute_command;
use crate::{ use crate::{
action::{Action, ActionDescription, ActionState}, action::{Action, ActionDescription},
BoxableError, BoxableError,
}; };
/**
Kickstart a `launchctl` service
*/
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
pub struct KickstartLaunchctlService { pub struct KickstartLaunchctlService {
unit: String, unit: String,
action_state: ActionState,
} }
impl KickstartLaunchctlService { impl KickstartLaunchctlService {
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
pub async fn plan(unit: String) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> { pub async fn plan(
Ok(Self { unit: String,
unit, ) -> Result<StatefulAction<Self>, Box<dyn std::error::Error + Send + Sync>> {
action_state: ActionState::Uncompleted, Ok(Self { unit }.into())
})
} }
} }
@ -39,10 +41,7 @@ impl Action for KickstartLaunchctlService {
unit = %self.unit, unit = %self.unit,
))] ))]
async fn execute(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { async fn execute(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let Self { let Self { unit } = self;
unit,
action_state: _,
} = self;
execute_command( execute_command(
Command::new("launchctl") Command::new("launchctl")
@ -69,14 +68,6 @@ impl Action for KickstartLaunchctlService {
// noop // noop
Ok(()) 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)] #[derive(Debug, thiserror::Error)]

View file

@ -1,17 +1,20 @@
mod bootstrap_volume; /*! [`Action`](crate::action::Action)s for Darwin based systems
mod create_apfs_volume; */
mod create_synthetic_objects;
mod create_volume;
mod enable_ownership;
mod encrypt_volume;
mod kickstart_launchctl_service;
mod unmount_volume;
pub use bootstrap_volume::{BootstrapVolume, BootstrapVolumeError}; mod bootstrap_apfs_volume;
pub use create_apfs_volume::{CreateApfsVolume, CreateApfsVolumeError, NIX_VOLUME_MOUNTD_DEST}; 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_synthetic_objects::{CreateSyntheticObjects, CreateSyntheticObjectsError};
pub use create_volume::{CreateVolume, CreateVolumeError};
pub use enable_ownership::{EnableOwnership, EnableOwnershipError}; 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 kickstart_launchctl_service::{KickstartLaunchctlService, KickstartLaunchctlServiceError};
pub use unmount_volume::{UnmountVolume, UnmountVolumeError}; pub use unmount_apfs_volume::{UnmountApfsVolume, UnmountVolumeError};

View file

@ -2,40 +2,39 @@ use std::path::{Path, PathBuf};
use tokio::process::Command; use tokio::process::Command;
use crate::action::StatefulAction;
use crate::execute_command; use crate::execute_command;
use crate::{ use crate::{
action::{Action, ActionDescription, ActionState}, action::{Action, ActionDescription},
BoxableError, BoxableError,
}; };
/**
Unmount an APFS volume
*/
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
pub struct UnmountVolume { pub struct UnmountApfsVolume {
disk: PathBuf, disk: PathBuf,
name: String, name: String,
action_state: ActionState,
} }
impl UnmountVolume { impl UnmountApfsVolume {
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
pub async fn plan( pub async fn plan(
disk: impl AsRef<Path>, disk: impl AsRef<Path>,
name: String, name: String,
) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> { ) -> Result<StatefulAction<Self>, Box<dyn std::error::Error + Send + Sync>> {
let disk = disk.as_ref().to_owned(); let disk = disk.as_ref().to_owned();
Ok(Self { Ok(Self { disk, name }.into())
disk,
name,
action_state: ActionState::Uncompleted,
})
} }
} }
#[async_trait::async_trait] #[async_trait::async_trait]
#[typetag::serde(name = "unmount_volume")] #[typetag::serde(name = "unmount_volume")]
impl Action for UnmountVolume { impl Action for UnmountApfsVolume {
fn tracing_synopsis(&self) -> String { fn tracing_synopsis(&self) -> String {
format!("Unmount the `{}` volume", self.name) format!("Unmount the `{}` APFS volume", self.name)
} }
fn execute_description(&self) -> Vec<ActionDescription> { fn execute_description(&self) -> Vec<ActionDescription> {
@ -47,11 +46,7 @@ impl Action for UnmountVolume {
name = %self.name, name = %self.name,
))] ))]
async fn execute(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { async fn execute(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let Self { let Self { disk: _, name } = self;
disk: _,
name,
action_state: _,
} = self;
execute_command( execute_command(
Command::new("/usr/sbin/diskutil") Command::new("/usr/sbin/diskutil")
@ -75,11 +70,7 @@ impl Action for UnmountVolume {
name = %self.name, name = %self.name,
))] ))]
async fn revert(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { async fn revert(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let Self { let Self { disk: _, name } = self;
disk: _,
name,
action_state: _,
} = self;
execute_command( execute_command(
Command::new("/usr/sbin/diskutil") Command::new("/usr/sbin/diskutil")
@ -93,14 +84,6 @@ impl Action for UnmountVolume {
Ok(()) 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)] #[derive(Debug, thiserror::Error)]

View file

@ -4,10 +4,11 @@ use target_lexicon::OperatingSystem;
use tokio::fs::remove_file; use tokio::fs::remove_file;
use tokio::process::Command; use tokio::process::Command;
use crate::action::StatefulAction;
use crate::execute_command; use crate::execute_command;
use crate::{ use crate::{
action::{Action, ActionDescription, ActionState}, action::{Action, ActionDescription},
BoxableError, 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 TMPFILES_DEST: &str = "/etc/tmpfiles.d/nix-daemon.conf";
const DARWIN_NIX_DAEMON_DEST: &str = "/Library/LaunchDaemons/org.nixos.nix-daemon.plist"; 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)] #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
pub struct ConfigureNixDaemonService { pub struct ConfigureNixDaemonService {}
action_state: ActionState,
}
impl ConfigureNixDaemonService { impl ConfigureNixDaemonService {
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
pub async fn plan() -> Result<Self, Box<dyn std::error::Error + Send + Sync>> { pub async fn plan() -> Result<StatefulAction<Self>, Box<dyn std::error::Error + Send + Sync>> {
match OperatingSystem::host() { match OperatingSystem::host() {
OperatingSystem::MacOSX { OperatingSystem::MacOSX {
major: _, major: _,
@ -39,9 +41,7 @@ impl ConfigureNixDaemonService {
}, },
}; };
Ok(Self { Ok(Self {}.into())
action_state: ActionState::Uncompleted,
})
} }
} }
@ -65,7 +65,7 @@ impl Action for ConfigureNixDaemonService {
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
async fn execute(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { async fn execute(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let Self { action_state: _ } = self; let Self {} = self;
match OperatingSystem::host() { match OperatingSystem::host() {
OperatingSystem::MacOSX { OperatingSystem::MacOSX {
@ -274,14 +274,6 @@ impl Action for ConfigureNixDaemonService {
Ok(()) 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)] #[derive(Debug, thiserror::Error)]

View file

@ -1,6 +1,7 @@
use crate::action::base::{CreateDirectory, CreateDirectoryError, CreateFile, CreateFileError}; use crate::action::base::{CreateDirectory, CreateDirectoryError, CreateFile, CreateFileError};
use crate::action::StatefulAction;
use crate::{ use crate::{
action::{Action, ActionDescription, ActionImplementation, ActionState}, action::{Action, ActionDescription},
BoxableError, BoxableError,
}; };
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@ -16,17 +17,16 @@ const PATHS: &[&str] = &[
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
pub struct CreateSystemdSysext { pub struct CreateSystemdSysext {
destination: PathBuf, destination: PathBuf,
create_directories: Vec<CreateDirectory>, create_directories: Vec<StatefulAction<CreateDirectory>>,
create_extension_release: CreateFile, create_extension_release: StatefulAction<CreateFile>,
create_bind_mount_unit: CreateFile, create_bind_mount_unit: StatefulAction<CreateFile>,
action_state: ActionState,
} }
impl CreateSystemdSysext { impl CreateSystemdSysext {
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
pub async fn plan( pub async fn plan(
destination: impl AsRef<Path>, destination: impl AsRef<Path>,
) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> { ) -> Result<StatefulAction<Self>, Box<dyn std::error::Error + Send + Sync>> {
let destination = destination.as_ref(); let destination = destination.as_ref();
let mut create_directories = let mut create_directories =
@ -85,8 +85,8 @@ impl CreateSystemdSysext {
create_directories, create_directories,
create_extension_release, create_extension_release,
create_bind_mount_unit, create_bind_mount_unit,
action_state: ActionState::Uncompleted, }
}) .into())
} }
} }
@ -113,7 +113,6 @@ impl Action for CreateSystemdSysext {
async fn execute(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { async fn execute(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let Self { let Self {
destination: _, destination: _,
action_state: _,
create_directories, create_directories,
create_extension_release, create_extension_release,
create_bind_mount_unit, create_bind_mount_unit,
@ -142,7 +141,6 @@ impl Action for CreateSystemdSysext {
async fn revert(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { async fn revert(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let Self { let Self {
destination: _, destination: _,
action_state: _,
create_directories, create_directories,
create_extension_release, create_extension_release,
create_bind_mount_unit, create_bind_mount_unit,
@ -158,14 +156,6 @@ impl Action for CreateSystemdSysext {
Ok(()) 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)] #[derive(Debug, thiserror::Error)]

View file

@ -1,5 +1,9 @@
//! [`Action`](crate::action::Action)s for Linux based systems
mod configure_nix_daemon_service;
mod create_systemd_sysext; mod create_systemd_sysext;
mod start_systemd_unit; mod start_systemd_unit;
pub use configure_nix_daemon_service::{ConfigureNixDaemonService, ConfigureNixDaemonServiceError};
pub use create_systemd_sysext::{CreateSystemdSysext, CreateSystemdSysextError}; pub use create_systemd_sysext::{CreateSystemdSysext, CreateSystemdSysextError};
pub use start_systemd_unit::{StartSystemdUnit, StartSystemdUnitError}; pub use start_systemd_unit::{StartSystemdUnit, StartSystemdUnitError};

View file

@ -1,25 +1,27 @@
use tokio::process::Command; use tokio::process::Command;
use crate::action::StatefulAction;
use crate::execute_command; use crate::execute_command;
use crate::{ use crate::{
action::{Action, ActionDescription, ActionState}, action::{Action, ActionDescription},
BoxableError, BoxableError,
}; };
/**
Start a given systemd unit
*/
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
pub struct StartSystemdUnit { pub struct StartSystemdUnit {
unit: String, unit: String,
action_state: ActionState,
} }
impl StartSystemdUnit { impl StartSystemdUnit {
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
pub async fn plan(unit: String) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> { pub async fn plan(
Ok(Self { unit: String,
unit, ) -> Result<StatefulAction<Self>, Box<dyn std::error::Error + Send + Sync>> {
action_state: ActionState::Uncompleted, Ok(Self { unit }.into())
})
} }
} }
@ -91,14 +93,6 @@ impl Action for StartSystemdUnit {
Ok(()) 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)] #[derive(Debug, thiserror::Error)]

View file

@ -9,16 +9,12 @@ use tokio::process::Command;
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
pub struct SystemdSysextMerge { pub struct SystemdSysextMerge {
device: PathBuf, device: PathBuf,
action_state: ActionState,
} }
impl SystemdSysextMerge { impl SystemdSysextMerge {
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
pub async fn plan(device: PathBuf) -> Result<Self, SystemdSysextMergeError> { pub async fn plan(device: PathBuf) -> Result<Self, SystemdSysextMergeError> {
Ok(Self { Ok(Self { device })
device,
action_state: ActionState::Uncompleted,
})
} }
} }
@ -68,10 +64,7 @@ impl Action for SystemdSysextMerge {
device = %self.device.display(), device = %self.device.display(),
))] ))]
async fn revert(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { async fn revert(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let Self { let Self { device } = self;
device,
action_state,
} = self;
// TODO(@Hoverbear): Handle proxy vars // TODO(@Hoverbear): Handle proxy vars
execute_command( execute_command(
@ -86,14 +79,6 @@ impl Action for SystemdSysextMerge {
Ok(()) 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)] #[derive(Debug, thiserror::Error)]

View file

@ -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<StatefulAction<Self>, Box<dyn std::error::Error + Send + Sync>> {
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<ActionDescription> {
vec![ActionDescription::new(self.tracing_synopsis(), vec![])]
}
#[tracing::instrument(skip_all, fields(
// Tracing fields...
))]
async fn execute(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// Execute steps ...
Ok(())
}
fn revert_description(&self) -> Vec<ActionDescription> {
vec![ActionDescription::new(self.tracing_synopsis(), vec![])]
}
#[tracing::instrument(skip_all, fields(
// Tracing fields...
))]
async fn revert(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// 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<Self, PlannerError> {
Ok(Self {
common: CommonSettings::default()?,
})
}
async fn plan(&self) -> Result<Vec<StatefulAction<Box<dyn Action>>>, PlannerError> {
Ok(vec![
// ...
MyAction::plan()
.await
.map_err(PlannerError::Action)?.boxed(),
])
}
fn settings(&self) -> Result<HashMap<String, serde_json::Value>, 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 base;
pub mod common; pub mod common;
pub mod darwin; pub mod darwin;
pub mod linux; 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 /// 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] #[async_trait::async_trait]
#[typetag::serde(tag = "action")] #[typetag::serde(tag = "action")]
pub trait Action: Send + Sync + std::fmt::Debug + dyn_clone::DynClone { pub trait Action: Send + Sync + std::fmt::Debug + dyn_clone::DynClone {
/// A synopsis of the action for tracing purposes
fn tracing_synopsis(&self) -> String; 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<ActionDescription>; fn execute_description(&self) -> Vec<ActionDescription>;
/// 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<ActionDescription>; fn revert_description(&self) -> Vec<ActionDescription>;
/// 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<dyn std::error::Error + Send + Sync>>; async fn execute(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;
/// 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<dyn std::error::Error + Send + Sync>>; async fn revert(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;
fn action_state(&self) -> ActionState;
fn set_action_state(&mut self, new_state: ActionState);
// They should also have an `async fn plan(args...) -> Result<ActionState<Self>, Box<dyn std::error::Error + Send + Sync>>;` fn stateful(self) -> StatefulAction<Self>
where
Self: Sized,
{
StatefulAction {
action: self,
state: ActionState::Uncompleted,
}
}
// They should also have an `async fn plan(args...) -> Result<StatefulAction<Self>, Box<dyn std::error::Error + Send + Sync>>;`
} }
/// The main wrapper around [`Action`], handling [`ActionState`] and tracing.
#[async_trait::async_trait]
pub trait ActionImplementation: Action {
fn describe_execute(&self) -> Vec<ActionDescription> {
if self.action_state() == ActionState::Completed {
return vec![];
}
return self.execute_description();
}
fn describe_revert(&self) -> Vec<ActionDescription> {
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<dyn std::error::Error + Send + Sync>> {
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<dyn std::error::Error + Send + Sync>> {
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<A> ActionImplementation for A where A: Action {}
dyn_clone::clone_trait_object!(Action); dyn_clone::clone_trait_object!(Action);
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Copy)] /**
pub enum ActionState { A description of an [`Action`](crate::action::Action), intended for humans to review
Completed, */
// Only applicable to meta-actions that start multiple sub-actions.
Progress,
Uncompleted,
}
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
pub struct ActionDescription { pub struct ActionDescription {
pub description: String, pub description: String,
pub explanation: Vec<String>, pub explanation: Vec<String>,
} }
impl ActionDescription { impl ActionDescription {
fn new(description: String, explanation: Vec<String>) -> Self { pub fn new(description: String, explanation: Vec<String>) -> Self {
Self { Self {
description, description,
explanation, explanation,

168
src/action/stateful.rs Normal file
View file

@ -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<A> {
pub(crate) action: A,
pub(crate) state: ActionState,
}
impl<A> From<A> for StatefulAction<A>
where
A: Action,
{
fn from(action: A) -> Self {
Self {
action,
state: ActionState::Uncompleted,
}
}
}
impl StatefulAction<Box<dyn Action>> {
/// A description of what this action would do during execution
pub fn describe_execute(&self) -> Vec<ActionDescription> {
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<ActionDescription> {
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<dyn std::error::Error + Send + Sync>> {
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<dyn std::error::Error + Send + Sync>> {
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<A> StatefulAction<A>
where
A: Action,
{
pub fn inner(&self) -> &A {
&self.action
}
pub fn boxed(self) -> StatefulAction<Box<dyn Action>>
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<ActionDescription> {
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<ActionDescription> {
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<dyn std::error::Error + Send + Sync>> {
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<dyn std::error::Error + Send + Sync>> {
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,
}

View file

@ -1,9 +1,13 @@
use reqwest::Url; use reqwest::Url;
use serde::{Deserialize, Serialize}; 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)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChannelValue(pub String, pub Url); pub struct ChannelValue(pub String, pub Url);
#[cfg(feature = "cli")]
impl clap::builder::ValueParserFactory for ChannelValue { impl clap::builder::ValueParserFactory for ChannelValue {
type Parser = ChannelValueParser; type Parser = ChannelValueParser;
fn value_parser() -> Self::Parser { fn value_parser() -> Self::Parser {
@ -19,6 +23,8 @@ impl From<(String, Url)> for ChannelValue {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct ChannelValueParser; pub struct ChannelValueParser;
#[cfg(feature = "cli")]
impl clap::builder::TypedValueParser for ChannelValueParser { impl clap::builder::TypedValueParser for ChannelValueParser {
type Value = ChannelValue; type Value = ChannelValue;

View file

@ -1,4 +1,9 @@
/*! CLI argument structures and utilities
*/
pub(crate) mod arg; pub(crate) mod arg;
mod interaction;
pub(crate) mod subcommand; pub(crate) mod subcommand;
use clap::Parser; use clap::Parser;

View file

@ -4,8 +4,11 @@ use std::{
}; };
use crate::{ use crate::{
action::ActionState, cli::is_root, cli::signal_channel, cli::CommandExecute, interaction, action::ActionState,
plan::RECEIPT_LOCATION, BuiltinPlanner, InstallPlan, Planner, cli::{interaction, is_root, signal_channel, CommandExecute},
plan::RECEIPT_LOCATION,
planner::Planner,
BuiltinPlanner, InstallPlan,
}; };
use clap::{ArgAction, Parser}; use clap::{ArgAction, Parser};
use eyre::{eyre, WrapErr}; 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))? { 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")) 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")) 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 existing_receipt
} , } ,
None => { 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() let builtin_planner = BuiltinPlanner::default()
.await .await
.map_err(|e| eyre::eyre!(e))?; .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")), (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 !no_confirm {
if !interaction::confirm( if !interaction::confirm(
install_plan install_plan
.describe_execute(explain) .describe_install(explain)
.map_err(|e| eyre!(e))?, .map_err(|e| eyre!(e))?,
) )
.await? .await?
@ -122,7 +125,7 @@ impl CommandExecute for Install {
tracing::error!("{:?}", error); tracing::error!("{:?}", error);
if !interaction::confirm( if !interaction::confirm(
install_plan install_plan
.describe_revert(explain) .describe_uninstall(explain)
.map_err(|e| eyre!(e))?, .map_err(|e| eyre!(e))?,
) )
.await? .await?
@ -130,7 +133,7 @@ impl CommandExecute for Install {
interaction::clean_exit_with_message("Okay, didn't do anything! Bye!").await; interaction::clean_exit_with_message("Okay, didn't do anything! Bye!").await;
} }
let rx2 = tx.subscribe(); let rx2 = tx.subscribe();
install_plan.revert(rx2).await? install_plan.uninstall(rx2).await?
} else { } else {
return Err(error); return Err(error);
} }

View file

@ -8,7 +8,7 @@ use crate::{
use clap::{ArgAction, Parser}; use clap::{ArgAction, Parser};
use eyre::{eyre, WrapErr}; use eyre::{eyre, WrapErr};
use crate::{cli::CommandExecute, interaction}; use crate::cli::{interaction, CommandExecute};
/// Uninstall a previously installed Nix (only Harmonic done installs supported) /// Uninstall a previously installed Nix (only Harmonic done installs supported)
#[derive(Debug, Parser)] #[derive(Debug, Parser)]
@ -53,14 +53,16 @@ impl CommandExecute for Uninstall {
let mut plan: InstallPlan = serde_json::from_str(&install_receipt_string)?; let mut plan: InstallPlan = serde_json::from_str(&install_receipt_string)?;
if !no_confirm { 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; interaction::clean_exit_with_message("Okay, didn't do anything! Bye!").await;
} }
} }
let (_tx, rx) = signal_channel().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... // 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`. // However that will require being able to link error -> step and manually setting that step as `Uncompleted`.

View file

@ -1,17 +1,49 @@
use std::path::PathBuf; use std::path::PathBuf;
use crate::{planner::PlannerError, settings::InstallSettingsError};
/// An error occurring during a call defined in this crate
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
pub enum HarmonicError { pub enum HarmonicError {
/// An error originating from an [`Action`](crate::action::Action)
#[error("Error executing action")] #[error("Error executing action")]
Action( Action(
#[source] #[source]
#[from] #[from]
Box<dyn std::error::Error + Send + Sync>, Box<dyn std::error::Error + Send + Sync>,
), ),
/// An error while writing the [`InstallPlan`](crate::InstallPlan)
#[error("Recording install receipt")] #[error("Recording install receipt")]
RecordingReceipt(PathBuf, #[source] std::io::Error), RecordingReceipt(PathBuf, #[source] std::io::Error),
#[error(transparent)] /// An error while serializing the [`InstallPlan`](crate::InstallPlan)
SerializingReceipt(serde_json::Error), #[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")] #[error("Cancelled by user")]
Cancelled, 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,
),
} }

View file

@ -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 action;
pub mod channel_value; mod channel_value;
#[cfg(feature = "cli")]
pub mod cli; pub mod cli;
mod error; mod error;
mod interaction;
mod os; mod os;
mod plan; mod plan;
pub mod planner; pub mod planner;
mod settings; pub mod settings;
use std::{ffi::OsStr, process::Output}; use std::{ffi::OsStr, process::Output};
pub use action::Action; use action::Action;
pub use planner::Planner;
pub use channel_value::ChannelValue;
pub use error::HarmonicError; pub use error::HarmonicError;
pub use plan::InstallPlan; pub use plan::InstallPlan;
use planner::BuiltinPlanner; use planner::BuiltinPlanner;
pub use settings::CommonSettings;
use tokio::process::Command; use tokio::process::Command;
#[tracing::instrument(skip_all, fields(command = %format!("{:?}", command.as_std())))] #[tracing::instrument(skip_all, fields(command = %format!("{:?}", command.as_std())))]

View file

@ -1,43 +1,56 @@
use std::{path::PathBuf, str::FromStr}; use std::{path::PathBuf, str::FromStr};
use crate::{ use crate::{
action::{Action, ActionDescription, ActionImplementation}, action::{Action, ActionDescription, StatefulAction},
planner::Planner, planner::{BuiltinPlanner, Planner},
HarmonicError, HarmonicError,
}; };
use crossterm::style::Stylize; use owo_colors::OwoColorize;
use semver::{Version, VersionReq}; use semver::{Version, VersionReq};
use serde::{de::Error, Deserialize, Deserializer}; use serde::{de::Error, Deserialize, Deserializer};
use tokio::sync::broadcast::Receiver; use tokio::sync::broadcast::Receiver;
pub const RECEIPT_LOCATION: &str = "/nix/receipt.json"; 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)] #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
pub struct InstallPlan { pub struct InstallPlan {
#[serde(deserialize_with = "ensure_version")] #[serde(deserialize_with = "ensure_version")]
pub(crate) version: Version, pub(crate) version: Version,
pub(crate) actions: Vec<Box<dyn Action>>, pub(crate) actions: Vec<StatefulAction<Box<dyn Action>>>,
pub(crate) planner: Box<dyn Planner>, pub(crate) planner: Box<dyn Planner>,
} }
impl InstallPlan { impl InstallPlan {
pub async fn plan( pub async fn default() -> Result<Self, HarmonicError> {
planner: Box<dyn Planner>, let planner = BuiltinPlanner::default().await?.boxed();
) -> Result<Self, Box<dyn std::error::Error + Sync + Send>> {
let actions = planner.plan().await?; let actions = planner.plan().await?;
Ok(Self { Ok(Self {
planner, planner,
actions, actions,
version: current_version()?, version: current_version()?,
}) })
} }
pub async fn plan<P>(planner: P) -> Result<Self, HarmonicError>
where
P: Planner + 'static,
{
let actions = planner.plan().await?;
Ok(Self {
planner: planner.boxed(),
actions,
version: current_version()?,
})
}
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
pub fn describe_execute( pub fn describe_install(&self, explain: bool) -> Result<String, HarmonicError> {
&self,
explain: bool,
) -> Result<String, Box<dyn std::error::Error + Sync + Send>> {
let Self { let Self {
planner, planner,
actions, actions,
@ -134,10 +147,7 @@ impl InstallPlan {
} }
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
pub fn describe_revert( pub fn describe_uninstall(&self, explain: bool) -> Result<String, HarmonicError> {
&self,
explain: bool,
) -> Result<String, Box<dyn std::error::Error + Sync + Send>> {
let Self { let Self {
version: _, version: _,
planner, planner,
@ -196,7 +206,7 @@ impl InstallPlan {
} }
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
pub async fn revert( pub async fn uninstall(
&mut self, &mut self,
cancel_channel: impl Into<Option<Receiver<()>>>, cancel_channel: impl Into<Option<Receiver<()>>>,
) -> Result<(), HarmonicError> { ) -> Result<(), HarmonicError> {
@ -277,13 +287,11 @@ fn ensure_version<'de, D: Deserializer<'de>>(d: D) -> Result<Version, D::Error>
mod test { mod test {
use semver::Version; use semver::Version;
use crate::{planner::BuiltinPlanner, InstallPlan}; use crate::{planner::BuiltinPlanner, HarmonicError, InstallPlan};
#[tokio::test] #[tokio::test]
async fn ensure_version_allows_compatible() -> eyre::Result<()> { async fn ensure_version_allows_compatible() -> Result<(), HarmonicError> {
let planner = BuiltinPlanner::default() let planner = BuiltinPlanner::default().await?;
.await
.map_err(|e| eyre::eyre!(e))?;
let good_version = Version::parse(env!("CARGO_PKG_VERSION"))?; let good_version = Version::parse(env!("CARGO_PKG_VERSION"))?;
let value = serde_json::json!({ let value = serde_json::json!({
"planner": planner.boxed(), "planner": planner.boxed(),
@ -296,10 +304,8 @@ mod test {
} }
#[tokio::test] #[tokio::test]
async fn ensure_version_denies_incompatible() -> eyre::Result<()> { async fn ensure_version_denies_incompatible() -> Result<(), HarmonicError> {
let planner = BuiltinPlanner::default() let planner = BuiltinPlanner::default().await?;
.await
.map_err(|e| eyre::eyre!(e))?;
let bad_version = Version::parse("9999999999999.9999999999.99999999")?; let bad_version = Version::parse("9999999999999.9999999999.99999999")?;
let value = serde_json::json!({ let value = serde_json::json!({
"planner": planner.boxed(), "planner": planner.boxed(),

View file

@ -1,3 +1,5 @@
//! Planners for Darwin based systems
mod multi; mod multi;
pub use multi::DarwinMulti; pub use multi::DarwinMulti;

View file

@ -1,46 +1,63 @@
use std::{collections::HashMap, io::Cursor}; use std::{collections::HashMap, io::Cursor};
#[cfg(feature = "cli")]
use clap::ArgAction; use clap::ArgAction;
use tokio::process::Command; use tokio::process::Command;
use crate::{ use crate::{
action::{ action::{
common::{ConfigureNix, ProvisionNix}, common::{ConfigureNix, ProvisionNix},
darwin::{CreateApfsVolume, KickstartLaunchctlService}, darwin::{CreateNixVolume, KickstartLaunchctlService},
StatefulAction,
}, },
execute_command, execute_command,
os::darwin::DiskUtilOutput, os::darwin::DiskUtilOutput,
planner::{BuiltinPlannerError, Planner}, planner::{Planner, PlannerError},
Action, BuiltinPlanner, CommonSettings, 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 { pub struct DarwinMulti {
#[clap(flatten)] #[cfg_attr(feature = "cli", clap(flatten))]
pub settings: CommonSettings, pub settings: CommonSettings,
/// Force encryption on the volume /// Force encryption on the volume
#[clap( #[cfg_attr(
long, feature = "cli",
action(ArgAction::Set), clap(
default_value = "false", long,
env = "HARMONIC_ENCRYPT" action(ArgAction::Set),
default_value = "false",
env = "HARMONIC_ENCRYPT"
)
)] )]
pub encrypt: Option<bool>, pub encrypt: Option<bool>,
/// Use a case sensitive volume /// Use a case sensitive volume
#[clap( #[cfg_attr(
long, feature = "cli",
action(ArgAction::SetTrue), clap(
default_value = "false", long,
env = "HARMONIC_CASE_SENSITIVE" action(ArgAction::SetTrue),
default_value = "false",
env = "HARMONIC_CASE_SENSITIVE"
)
)] )]
pub case_sensitive: bool, 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, 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<String>, pub root_disk: Option<String>,
} }
async fn default_root_disk() -> Result<String, BuiltinPlannerError> { async fn default_root_disk() -> Result<String, PlannerError> {
let buf = execute_command( let buf = execute_command(
Command::new("/usr/sbin/diskutil") Command::new("/usr/sbin/diskutil")
.args(["info", "-plist", "/"]) .args(["info", "-plist", "/"])
@ -57,7 +74,7 @@ async fn default_root_disk() -> Result<String, BuiltinPlannerError> {
#[async_trait::async_trait] #[async_trait::async_trait]
#[typetag::serde(name = "darwin-multi")] #[typetag::serde(name = "darwin-multi")]
impl Planner for DarwinMulti { impl Planner for DarwinMulti {
async fn default() -> Result<Self, Box<dyn std::error::Error + Sync + Send>> { async fn default() -> Result<Self, PlannerError> {
Ok(Self { Ok(Self {
settings: CommonSettings::default()?, settings: CommonSettings::default()?,
root_disk: Some(default_root_disk().await?), root_disk: Some(default_root_disk().await?),
@ -67,7 +84,7 @@ impl Planner for DarwinMulti {
}) })
} }
async fn plan(&self) -> Result<Vec<Box<dyn Action>>, Box<dyn std::error::Error + Sync + Send>> { async fn plan(&self) -> Result<Vec<StatefulAction<Box<dyn Action>>>, PlannerError> {
let root_disk = match &self.root_disk { let root_disk = match &self.root_disk {
root_disk @ Some(_) => root_disk.clone(), root_disk @ Some(_) => root_disk.clone(),
None => { None => {
@ -89,7 +106,8 @@ impl Planner for DarwinMulti {
Command::new("/usr/bin/fdesetup") Command::new("/usr/bin/fdesetup")
.arg("isactive") .arg("isactive")
.status() .status()
.await? .await
.map_err(|e| PlannerError::Custom(Box::new(e)))?
.code() .code()
.map(|v| if v == 0 { false } else { true }) .map(|v| if v == 0 { false } else { true })
.unwrap_or(false) .unwrap_or(false)
@ -102,24 +120,31 @@ impl Planner for DarwinMulti {
// //
// setup_Synthetic -> create_synthetic_objects // setup_Synthetic -> create_synthetic_objects
// Unmount -> create_volume -> Setup_fstab -> maybe encrypt_volume -> launchctl bootstrap -> launchctl kickstart -> await_volume -> maybe enableOwnership // Unmount -> create_volume -> Setup_fstab -> maybe encrypt_volume -> launchctl bootstrap -> launchctl kickstart -> await_volume -> maybe enableOwnership
Box::new( CreateNixVolume::plan(
CreateApfsVolume::plan( root_disk.unwrap(), /* We just ensured it was populated */
root_disk.unwrap(), /* We just ensured it was populated */ self.volume_label.clone(),
self.volume_label.clone(), false,
false, encrypt,
encrypt, )
) .await
.await?, .map_err(PlannerError::Action)?
), .boxed(),
Box::new(ProvisionNix::plan(&self.settings).await?), ProvisionNix::plan(&self.settings)
Box::new(ConfigureNix::plan(&self.settings).await?), .await
Box::new(KickstartLaunchctlService::plan("system/org.nixos.nix-daemon".into()).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( fn settings(&self) -> Result<HashMap<String, serde_json::Value>, InstallSettingsError> {
&self,
) -> Result<HashMap<String, serde_json::Value>, Box<dyn std::error::Error + Sync + Send>> {
let Self { let Self {
settings, settings,
encrypt, encrypt,
@ -129,7 +154,7 @@ impl Planner for DarwinMulti {
} = self; } = self;
let mut map = HashMap::default(); 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_encrypt".into(), serde_json::to_value(encrypt)?);
map.insert("volume_label".into(), serde_json::to_value(volume_label)?); map.insert("volume_label".into(), serde_json::to_value(volume_label)?);
map.insert("root_disk".into(), serde_json::to_value(root_disk)?); map.insert("root_disk".into(), serde_json::to_value(root_disk)?);

View file

@ -1,3 +1,5 @@
//! Planners for Linux based systems
mod multi; mod multi;
pub use multi::LinuxMulti; pub use multi::LinuxMulti;

View file

@ -2,32 +2,37 @@ use crate::{
action::{ action::{
base::CreateDirectory, base::CreateDirectory,
common::{ConfigureNix, ProvisionNix}, common::{ConfigureNix, ProvisionNix},
StatefulAction,
}, },
planner::Planner, planner::{Planner, PlannerError},
Action, BuiltinPlanner, CommonSettings, settings::CommonSettings,
settings::InstallSettingsError,
Action, BuiltinPlanner,
}; };
use std::{collections::HashMap, path::Path}; 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 { pub struct LinuxMulti {
#[clap(flatten)] #[cfg_attr(feature = "cli", clap(flatten))]
pub settings: CommonSettings, pub settings: CommonSettings,
} }
#[async_trait::async_trait] #[async_trait::async_trait]
#[typetag::serde(name = "linux-multi")] #[typetag::serde(name = "linux-multi")]
impl Planner for LinuxMulti { impl Planner for LinuxMulti {
async fn default() -> Result<Self, Box<dyn std::error::Error + Sync + Send>> { async fn default() -> Result<Self, PlannerError> {
Ok(Self { Ok(Self {
settings: CommonSettings::default()?, settings: CommonSettings::default()?,
}) })
} }
async fn plan(&self) -> Result<Vec<Box<dyn Action>>, Box<dyn std::error::Error + Sync + Send>> { async fn plan(&self) -> Result<Vec<StatefulAction<Box<dyn Action>>>, PlannerError> {
// If on NixOS, running `harmonic` is pointless // 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 // 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() { 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. // 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() .status()
.await .await
{ {
return Err(Error::NixExists.into()); return Err(PlannerError::Custom(Box::new(LinuxMultiError::NixExists)));
} }
Ok(vec![ Ok(vec![
Box::new( CreateDirectory::plan("/nix", None, None, 0o0755, true)
CreateDirectory::plan("/nix", None, None, 0o0755, true) .await
.await .map_err(PlannerError::Action)?
.map_err(|v| Error::Action(v.into()))?, .boxed(),
), ProvisionNix::plan(&self.settings.clone())
Box::new( .await
ProvisionNix::plan(&self.settings.clone()) .map_err(PlannerError::Action)?
.await .boxed(),
.map_err(|v| Error::Action(v.into()))?, ConfigureNix::plan(&self.settings)
), .await
Box::new( .map_err(PlannerError::Action)?
ConfigureNix::plan(&self.settings) .boxed(),
.await
.map_err(|v| Error::Action(v.into()))?,
),
]) ])
} }
fn settings( fn settings(&self) -> Result<HashMap<String, serde_json::Value>, InstallSettingsError> {
&self,
) -> Result<HashMap<String, serde_json::Value>, Box<dyn std::error::Error + Sync + Send>> {
let Self { settings } = self; let Self { settings } = self;
let mut map = HashMap::default(); let mut map = HashMap::default();
map.extend(settings.describe()?.into_iter()); map.extend(settings.settings()?.into_iter());
Ok(map) Ok(map)
} }
@ -78,7 +78,7 @@ impl Into<BuiltinPlanner> for LinuxMulti {
} }
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
enum Error { enum LinuxMultiError {
#[error("NixOS already has Nix installed")] #[error("NixOS already has Nix installed")]
NixOs, NixOs,
#[error("`nix` is already a valid command, so it is installed")] #[error("`nix` is already a valid command, so it is installed")]

View file

@ -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<Self, PlannerError> {
Ok(Self {
common: CommonSettings::default()?,
})
}
async fn plan(&self) -> Result<Vec<StatefulAction<Box<dyn Action>>>, PlannerError> {
Ok(vec![
// ...
StartSystemdUnit::plan("nix-daemon.socket".into())
.await
.map_err(PlannerError::Action)?.boxed(),
])
}
fn settings(&self) -> Result<HashMap<String, serde_json::Value>, 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 darwin;
pub mod linux; pub mod linux;
pub mod specific; pub mod specific;
use std::collections::HashMap; 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] #[async_trait::async_trait]
#[typetag::serde(tag = "planner")] #[typetag::serde(tag = "planner")]
pub trait Planner: std::fmt::Debug + Send + Sync + dyn_clone::DynClone { pub trait Planner: std::fmt::Debug + Send + Sync + dyn_clone::DynClone {
async fn default() -> Result<Self, Box<dyn std::error::Error + Sync + Send>> /// Instantiate the planner with default settings, if possible
async fn default() -> Result<Self, PlannerError>
where where
Self: Sized; Self: Sized;
async fn plan(&self) -> Result<Vec<Box<dyn Action>>, Box<dyn std::error::Error + Sync + Send>>; /// Plan out the [`Action`]s for an [`InstallPlan`]
fn settings( async fn plan(&self) -> Result<Vec<StatefulAction<Box<dyn Action>>>, PlannerError>;
&self, /// The settings being used by the planner
) -> Result<HashMap<String, serde_json::Value>, Box<dyn std::error::Error + Sync + Send>>; fn settings(&self) -> Result<HashMap<String, serde_json::Value>, InstallSettingsError>;
/// A boxed, type erased planner
fn boxed(self) -> Box<dyn Planner> fn boxed(self) -> Box<dyn Planner>
where where
Self: Sized + 'static, Self: Sized + 'static,
@ -26,15 +107,21 @@ pub trait Planner: std::fmt::Debug + Send + Sync + dyn_clone::DynClone {
dyn_clone::clone_trait_object!(Planner); 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 { pub enum BuiltinPlanner {
/// A standard Linux multi-user install
LinuxMulti(linux::LinuxMulti), LinuxMulti(linux::LinuxMulti),
/// A standard MacOS (Darwin) multi-user install
DarwinMulti(darwin::DarwinMulti), DarwinMulti(darwin::DarwinMulti),
/// An install suitable for the Valve Steam Deck console
SteamDeck(specific::SteamDeck), SteamDeck(specific::SteamDeck),
} }
impl BuiltinPlanner { impl BuiltinPlanner {
pub async fn default() -> Result<Self, Box<dyn std::error::Error + Sync + Send>> { /// Heuristically determine the default planner for the target system
pub async fn default() -> Result<Self, PlannerError> {
use target_lexicon::{Architecture, OperatingSystem}; use target_lexicon::{Architecture, OperatingSystem};
match (Architecture::host(), OperatingSystem::host()) { match (Architecture::host(), OperatingSystem::host()) {
(Architecture::X86_64, OperatingSystem::Linux) => { (Architecture::X86_64, OperatingSystem::Linux) => {
@ -51,17 +138,15 @@ impl BuiltinPlanner {
| (Architecture::Aarch64(_), OperatingSystem::Darwin) => { | (Architecture::Aarch64(_), OperatingSystem::Darwin) => {
Ok(Self::DarwinMulti(darwin::DarwinMulti::default().await?)) Ok(Self::DarwinMulti(darwin::DarwinMulti::default().await?))
}, },
_ => Err(BuiltinPlannerError::UnsupportedArchitecture(target_lexicon::HOST).boxed()), _ => Err(PlannerError::UnsupportedArchitecture(target_lexicon::HOST)),
} }
} }
pub async fn plan( pub async fn plan(self) -> Result<InstallPlan, HarmonicError> {
self,
) -> Result<Vec<Box<dyn Action>>, Box<dyn std::error::Error + Sync + Send>> {
match self { match self {
BuiltinPlanner::LinuxMulti(planner) => planner.plan().await, BuiltinPlanner::LinuxMulti(planner) => InstallPlan::plan(planner).await,
BuiltinPlanner::DarwinMulti(planner) => planner.plan().await, BuiltinPlanner::DarwinMulti(planner) => InstallPlan::plan(planner).await,
BuiltinPlanner::SteamDeck(planner) => planner.plan().await, BuiltinPlanner::SteamDeck(planner) => InstallPlan::plan(planner).await,
} }
} }
pub fn boxed(self) -> Box<dyn Planner> { pub fn boxed(self) -> Box<dyn Planner> {
@ -73,18 +158,22 @@ impl BuiltinPlanner {
} }
} }
/// An error originating from a [`Planner`]
#[derive(thiserror::Error, Debug)] #[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")] #[error("Harmonic does not have a default planner for the `{0}` architecture right now, pass a specific archetype")]
UnsupportedArchitecture(target_lexicon::Triple), UnsupportedArchitecture(target_lexicon::Triple),
/// Error executing action
#[error("Error executing action")] #[error("Error executing action")]
ActionError( Action(#[source] Box<dyn std::error::Error + Send + Sync>),
#[source] /// An [`InstallSettingsError`]
#[from]
Box<dyn std::error::Error + Send + Sync>,
),
#[error(transparent)] #[error(transparent)]
InstallSettings(#[from] InstallSettingsError), InstallSettings(#[from] InstallSettingsError),
/// A MacOS (Darwin) plist related error
#[error(transparent)] #[error(transparent)]
Plist(#[from] plist::Error), Plist(#[from] plist::Error),
/// Custom planner error
#[error("Custom planner error")]
Custom(#[source] Box<dyn std::error::Error + Send + Sync>),
} }

View file

@ -5,42 +5,57 @@ use crate::{
base::CreateDirectory, base::CreateDirectory,
common::ProvisionNix, common::ProvisionNix,
linux::{CreateSystemdSysext, StartSystemdUnit}, linux::{CreateSystemdSysext, StartSystemdUnit},
StatefulAction,
}, },
planner::Planner, planner::{Planner, PlannerError},
Action, BuiltinPlanner, CommonSettings, 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 { pub struct SteamDeck {
#[clap(flatten)] #[cfg_attr(feature = "cli", clap(flatten))]
pub settings: CommonSettings, pub settings: CommonSettings,
} }
#[async_trait::async_trait] #[async_trait::async_trait]
#[typetag::serde(name = "steam-deck")] #[typetag::serde(name = "steam-deck")]
impl Planner for SteamDeck { impl Planner for SteamDeck {
async fn default() -> Result<Self, Box<dyn std::error::Error + Sync + Send>> { async fn default() -> Result<Self, PlannerError> {
Ok(Self { Ok(Self {
settings: CommonSettings::default()?, settings: CommonSettings::default()?,
}) })
} }
async fn plan(&self) -> Result<Vec<Box<dyn Action>>, Box<dyn std::error::Error + Sync + Send>> { async fn plan(&self) -> Result<Vec<StatefulAction<Box<dyn Action>>>, PlannerError> {
Ok(vec![ Ok(vec![
Box::new(CreateSystemdSysext::plan("/var/lib/extensions/nix").await?), CreateSystemdSysext::plan("/var/lib/extensions/nix")
Box::new(CreateDirectory::plan("/nix", None, None, 0o0755, true).await?), .await
Box::new(ProvisionNix::plan(&self.settings.clone()).await?), .map_err(PlannerError::Action)?
Box::new(StartSystemdUnit::plan("nix-daemon.socket".into()).await?), .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( fn settings(&self) -> Result<HashMap<String, serde_json::Value>, InstallSettingsError> {
&self,
) -> Result<HashMap<String, serde_json::Value>, Box<dyn std::error::Error + Sync + Send>> {
let Self { settings } = self; let Self { settings } = self;
let mut map = HashMap::default(); let mut map = HashMap::default();
map.extend(settings.describe()?.into_iter()); map.extend(settings.settings()?.into_iter());
Ok(map) Ok(map)
} }

View file

@ -1,105 +1,151 @@
/*! Configurable knobs and their related errors
*/
use std::collections::HashMap; use std::collections::HashMap;
#[cfg(feature = "cli")]
use clap::ArgAction; use clap::ArgAction;
use url::Url; use url::Url;
use crate::channel_value::ChannelValue; 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 = 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"; "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 = pub const NIX_AARCH64_LINUX_URL: &str =
"https://releases.nixos.org/nix/nix-2.11.0/nix-2.11.0-aarch64-linux.tar.xz"; "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 = 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"; "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 = pub const NIX_AARCH64_DARWIN_URL: &str =
"https://releases.nixos.org/nix/nix-2.11.0/nix-2.11.0-aarch64-darwin.tar.xz"; "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] #[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 { pub struct CommonSettings {
/// Channel(s) to add by default, pass multiple times for multiple channels /// Channel(s) to add
#[clap( #[cfg_attr(
feature = "cli",clap(
long, long,
value_parser, value_parser,
name = "channel", name = "channel",
action = clap::ArgAction::Append, action = clap::ArgAction::Append,
env = "HARMONIC_CHANNEL", env = "HARMONIC_CHANNEL",
default_value = "nixpkgs=https://nixos.org/channels/nixpkgs-unstable", default_value = "nixpkgs=https://nixos.org/channels/nixpkgs-unstable",
)] ))]
pub channels: Vec<ChannelValue>, pub(crate) channels: Vec<ChannelValue>,
/// Modify the user profile to automatically load nix /// Modify the user profile to automatically load nix
#[clap( #[cfg_attr(
long, feature = "cli",
action(ArgAction::SetFalse), clap(
default_value = "true", long,
global = true, action(ArgAction::SetFalse),
env = "HARMONIC_NO_MODIFY_PROFILE", default_value = "true",
name = "no-modify-profile" 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 /// 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( #[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( clap(
default_value = NIX_X64_64_DARWIN_URL, default_value = NIX_X64_64_DARWIN_URL,
) )
)] )]
#[cfg_attr( #[cfg_attr(
all(target_os = "macos", target_arch = "aarch64"), all(target_os = "macos", target_arch = "aarch64", feature = "cli"),
clap( clap(
default_value = NIX_AARCH64_DARWIN_URL, default_value = NIX_AARCH64_DARWIN_URL,
) )
)] )]
#[cfg_attr( #[cfg_attr(
all(target_os = "linux", target_arch = "x86_64"), all(target_os = "linux", target_arch = "x86_64", feature = "cli"),
clap( clap(
default_value = NIX_X64_64_LINUX_URL, default_value = NIX_X64_64_LINUX_URL,
) )
)] )]
#[cfg_attr( #[cfg_attr(
all(target_os = "linux", target_arch = "aarch64"), all(target_os = "linux", target_arch = "aarch64", feature = "cli"),
clap( clap(
default_value = NIX_AARCH64_LINUX_URL, default_value = NIX_AARCH64_LINUX_URL,
) )
)] )]
pub nix_package_url: Url, pub(crate) nix_package_url: Url,
#[clap(long, env = "HARMONIC_EXTRA_CONF")] /// Extra configuration lines for `/etc/nix.conf`
pub extra_conf: Option<String>, #[cfg_attr(feature = "cli", clap(long, env = "HARMONIC_EXTRA_CONF"))]
pub(crate) extra_conf: Option<String>,
#[clap( /// If Harmonic should forcibly recreate files it finds existing
long, #[cfg_attr(
action(ArgAction::SetTrue), feature = "cli",
default_value = "false", clap(
global = true, long,
env = "HARMONIC_FORCE" action(ArgAction::SetTrue),
default_value = "false",
global = true,
env = "HARMONIC_FORCE"
)
)] )]
pub force: bool, pub(crate) force: bool,
} }
impl CommonSettings { impl CommonSettings {
/// The default settings for the given Architecture & Operating System
pub fn default() -> Result<Self, InstallSettingsError> { pub fn default() -> Result<Self, InstallSettingsError> {
let url; let url;
let nix_build_user_prefix; let nix_build_user_prefix;
@ -154,9 +200,8 @@ impl CommonSettings {
}) })
} }
pub fn describe( /// A listing of the settings, suitable for [`Planner::settings`](crate::planner::Planner::settings)
&self, pub fn settings(&self) -> Result<HashMap<String, serde_json::Value>, InstallSettingsError> {
) -> Result<HashMap<String, serde_json::Value>, Box<dyn std::error::Error + Sync + Send>> {
let Self { let Self {
channels, channels,
modify_profile, modify_profile,
@ -217,62 +262,85 @@ impl CommonSettings {
// Builder Pattern // Builder Pattern
impl CommonSettings { impl CommonSettings {
/// Number of build users to create
pub fn daemon_user_count(&mut self, count: usize) -> &mut Self { pub fn daemon_user_count(&mut self, count: usize) -> &mut Self {
self.daemon_user_count = count; self.daemon_user_count = count;
self self
} }
/// Channel(s) to add
pub fn channels(&mut self, channels: impl IntoIterator<Item = (String, Url)>) -> &mut Self { pub fn channels(&mut self, channels: impl IntoIterator<Item = (String, Url)>) -> &mut Self {
self.channels = channels.into_iter().map(Into::into).collect(); self.channels = channels.into_iter().map(Into::into).collect();
self self
} }
/// Modify the user profile to automatically load nix
pub fn modify_profile(&mut self, toggle: bool) -> &mut Self { pub fn modify_profile(&mut self, toggle: bool) -> &mut Self {
self.modify_profile = toggle; self.modify_profile = toggle;
self self
} }
/// The Nix build group name
pub fn nix_build_group_name(&mut self, val: String) -> &mut Self { pub fn nix_build_group_name(&mut self, val: String) -> &mut Self {
self.nix_build_group_name = val; self.nix_build_group_name = val;
self self
} }
/// The Nix build group GID
pub fn nix_build_group_id(&mut self, count: usize) -> &mut Self { pub fn nix_build_group_id(&mut self, count: usize) -> &mut Self {
self.nix_build_group_id = count; self.nix_build_group_id = count;
self self
} }
/// The Nix build user prefix (user numbers will be postfixed)
pub fn nix_build_user_prefix(&mut self, val: String) -> &mut Self { pub fn nix_build_user_prefix(&mut self, val: String) -> &mut Self {
self.nix_build_user_prefix = val; self.nix_build_user_prefix = val;
self self
} }
/// The Nix build user base UID (ascending)
pub fn nix_build_user_id_base(&mut self, count: usize) -> &mut Self { pub fn nix_build_user_id_base(&mut self, count: usize) -> &mut Self {
self.nix_build_user_id_base = count; self.nix_build_user_id_base = count;
self self
} }
/// The Nix package URL
pub fn nix_package_url(&mut self, url: Url) -> &mut Self { pub fn nix_package_url(&mut self, url: Url) -> &mut Self {
self.nix_package_url = url; self.nix_package_url = url;
self self
} }
/// Extra configuration lines for `/etc/nix.conf`
pub fn extra_conf(&mut self, extra_conf: Option<String>) -> &mut Self { pub fn extra_conf(&mut self, extra_conf: Option<String>) -> &mut Self {
self.extra_conf = extra_conf; self.extra_conf = extra_conf;
self self
} }
/// If Harmonic should forcibly recreate files it finds existing
pub fn force(&mut self, force: bool) -> &mut Self { pub fn force(&mut self, force: bool) -> &mut Self {
self.force = force; self.force = force;
self self
} }
} }
/// An error originating from a [`Planner::settings`](crate::planner::Planner::settings)
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
pub enum InstallSettingsError { pub enum InstallSettingsError {
/// Harmonic does not support the architecture right now
#[error("Harmonic does not support the `{0}` architecture right now")] #[error("Harmonic does not support the `{0}` architecture right now")]
UnsupportedArchitecture(target_lexicon::Triple), UnsupportedArchitecture(target_lexicon::Triple),
/// Parsing URL
#[error("Parsing URL")] #[error("Parsing URL")]
Parse( Parse(
#[source] #[source]
#[from] #[from]
url::ParseError, url::ParseError,
), ),
/// JSON serialization or deserialization error
#[error("JSON serialization or deserialization error")]
SerdeJson(
#[source]
#[from]
serde_json::Error,
),
} }