Rework planners to support custom args

This commit is contained in:
Ana Hobden 2022-10-25 11:57:09 -07:00
parent b920b384d3
commit 16acc1fe6c
28 changed files with 289 additions and 325 deletions

View file

@ -69,13 +69,13 @@ impl Actionable for CreateDirectory {
fn describe_execute(&self) -> Vec<ActionDescription> { fn describe_execute(&self) -> Vec<ActionDescription> {
let Self { let Self {
path, path,
user, user: _,
group, group: _,
mode, mode: _,
force_prune_on_revert: _, force_prune_on_revert: _,
action_state: _, action_state,
} = &self; } = &self;
if self.action_state == ActionState::Completed { if *action_state == ActionState::Completed {
vec![] vec![]
} else { } else {
vec![ActionDescription::new( vec![ActionDescription::new(

View file

@ -56,10 +56,10 @@ impl Actionable for CreateFile {
fn describe_execute(&self) -> Vec<ActionDescription> { fn describe_execute(&self) -> Vec<ActionDescription> {
let Self { let Self {
path, path,
user, user: _,
group, group: _,
mode, mode: _,
buf, buf: _,
force: _, force: _,
action_state: _, action_state: _,
} = &self; } = &self;

View file

@ -53,10 +53,10 @@ impl Actionable for CreateOrAppendFile {
fn describe_execute(&self) -> Vec<ActionDescription> { fn describe_execute(&self) -> Vec<ActionDescription> {
let Self { let Self {
path, path,
user, user: _,
group, group: _,
mode, mode: _,
buf, buf: _,
action_state: _, action_state: _,
} = &self; } = &self;
if self.action_state == ActionState::Completed { if self.action_state == ActionState::Completed {

View file

@ -110,7 +110,7 @@ impl Actionable for CreateVolume {
))] ))]
async fn revert(&mut self) -> Result<(), Self::Error> { async fn revert(&mut self) -> Result<(), Self::Error> {
let Self { let Self {
disk, disk: _,
name, name,
case_sensitive: _, case_sensitive: _,
action_state, action_state,

View file

@ -1,8 +1,5 @@
use serde::Serialize; use serde::Serialize;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use tokio::process::Command;
use crate::execute_command;
use crate::actions::{Action, ActionDescription, ActionState, Actionable}; use crate::actions::{Action, ActionDescription, ActionState, Actionable};
@ -47,8 +44,8 @@ impl Actionable for EncryptVolume {
))] ))]
async fn execute(&mut self) -> Result<(), Self::Error> { async fn execute(&mut self) -> Result<(), Self::Error> {
let Self { let Self {
disk, disk: _,
password, password: _,
action_state, action_state,
} = self; } = self;
if *action_state == ActionState::Completed { if *action_state == ActionState::Completed {
@ -77,8 +74,8 @@ impl Actionable for EncryptVolume {
))] ))]
async fn revert(&mut self) -> Result<(), Self::Error> { async fn revert(&mut self) -> Result<(), Self::Error> {
let Self { let Self {
disk, disk: _,
password, password: _,
action_state, action_state,
} = self; } = self;
if *action_state == ActionState::Uncompleted { if *action_state == ActionState::Uncompleted {

View file

@ -49,7 +49,7 @@ impl Actionable for UnmountVolume {
))] ))]
async fn execute(&mut self) -> Result<(), Self::Error> { async fn execute(&mut self) -> Result<(), Self::Error> {
let Self { let Self {
disk, disk: _,
name, name,
action_state, action_state,
} = self; } = self;
@ -91,7 +91,7 @@ impl Actionable for UnmountVolume {
))] ))]
async fn revert(&mut self) -> Result<(), Self::Error> { async fn revert(&mut self) -> Result<(), Self::Error> {
let Self { let Self {
disk, disk: _,
name, name,
action_state, action_state,
} = self; } = self;

View file

@ -1,6 +1,9 @@
use reqwest::Url;
use serde::Serialize; use serde::Serialize;
use crate::actions::{ use crate::InstallSettings;
use crate::{
actions::{
base::{ base::{
ConfigureNixDaemonService, ConfigureNixDaemonServiceError, SetupDefaultProfile, ConfigureNixDaemonService, ConfigureNixDaemonServiceError, SetupDefaultProfile,
SetupDefaultProfileError, SetupDefaultProfileError,
@ -10,8 +13,9 @@ use crate::actions::{
PlaceChannelConfigurationError, PlaceNixConfiguration, PlaceNixConfigurationError, PlaceChannelConfigurationError, PlaceNixConfiguration, PlaceNixConfigurationError,
}, },
Action, ActionState, Action, ActionState,
},
cli::arg::ChannelValue,
}; };
use crate::InstallSettings;
use crate::actions::{ActionDescription, Actionable}; use crate::actions::{ActionDescription, Actionable};
@ -28,13 +32,14 @@ pub struct ConfigureNix {
impl ConfigureNix { impl ConfigureNix {
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
pub async fn plan(settings: InstallSettings) -> Result<Self, ConfigureNixError> { pub async fn plan(settings: InstallSettings) -> Result<Self, ConfigureNixError> {
let channels = settings let channels: Vec<(String, Url)> = settings
.channels .channels
.iter() .iter()
.map(|(channel, _)| channel.to_string()) .map(|ChannelValue(channel, url)| (channel.to_string(), url.clone()))
.collect(); .collect();
let setup_default_profile = SetupDefaultProfile::plan(channels).await?; let setup_default_profile =
SetupDefaultProfile::plan(channels.iter().map(|(v, _k)| v.clone()).collect()).await?;
let configure_shell_profile = if settings.modify_profile { let configure_shell_profile = if settings.modify_profile {
Some(ConfigureShellProfile::plan().await?) Some(ConfigureShellProfile::plan().await?)
@ -42,7 +47,7 @@ impl ConfigureNix {
None None
}; };
let place_channel_configuration = let place_channel_configuration =
PlaceChannelConfiguration::plan(settings.channels, settings.force).await?; PlaceChannelConfiguration::plan(channels, settings.force).await?;
let place_nix_configuration = PlaceNixConfiguration::plan( let place_nix_configuration = PlaceNixConfiguration::plan(
settings.nix_build_group_name, settings.nix_build_group_name,
settings.extra_conf, settings.extra_conf,

View file

@ -5,19 +5,15 @@ use std::{
}; };
use tokio::process::Command; use tokio::process::Command;
use crate::actions::{base::darwin, Action, ActionDescription, ActionState, Actionable}; use crate::actions::base::{
use crate::{
actions::base::{
darwin::{ darwin::{
BootstrapVolume, BootstrapVolumeError, CreateSyntheticObjects, BootstrapVolume, BootstrapVolumeError, CreateSyntheticObjects, CreateSyntheticObjectsError,
CreateSyntheticObjectsError, CreateVolume, CreateVolumeError, EnableOwnership, CreateVolume, CreateVolumeError, EnableOwnership, EnableOwnershipError, EncryptVolume,
EnableOwnershipError, EncryptVolume, EncryptVolumeError, UnmountVolume, EncryptVolumeError, UnmountVolume, UnmountVolumeError,
UnmountVolumeError,
}, },
CreateFile, CreateFileError, CreateOrAppendFile, CreateOrAppendFileError, CreateFile, CreateFileError, CreateOrAppendFile, CreateOrAppendFileError,
},
execute_command,
}; };
use crate::actions::{Action, ActionDescription, ActionState, Actionable};
const NIX_VOLUME_MOUNTD_DEST: &str = "/Library/LaunchDaemons/org.nixos.darwin-store.plist"; const NIX_VOLUME_MOUNTD_DEST: &str = "/Library/LaunchDaemons/org.nixos.darwin-store.plist";

View file

@ -4,8 +4,7 @@ use serde::Serialize;
use tokio::task::JoinError; use tokio::task::JoinError;
use crate::actions::base::{ use crate::actions::base::{
CreateDirectory, CreateDirectoryError, FetchNix, FetchNixError, MoveUnpackedNix, CreateDirectoryError, FetchNix, FetchNixError, MoveUnpackedNix, MoveUnpackedNixError,
MoveUnpackedNixError,
}; };
use crate::InstallSettings; use crate::InstallSettings;

View file

@ -1,6 +1,7 @@
use reqwest::Url; use reqwest::Url;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChannelValue(pub String, pub Url); pub struct ChannelValue(pub String, pub Url);
impl clap::builder::ValueParserFactory for ChannelValue { impl clap::builder::ValueParserFactory for ChannelValue {
@ -10,6 +11,12 @@ impl clap::builder::ValueParserFactory for ChannelValue {
} }
} }
impl From<(String, Url)> for ChannelValue {
fn from((string, url): (String, Url)) -> Self {
Self(string, url)
}
}
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct ChannelValueParser; pub struct ChannelValueParser;
impl clap::builder::TypedValueParser for ChannelValueParser { impl clap::builder::TypedValueParser for ChannelValueParser {

View file

@ -9,11 +9,11 @@ use valuable::Valuable;
pub struct Instrumentation { pub struct Instrumentation {
/// Enable debug logs, -vv for trace /// Enable debug logs, -vv for trace
#[clap(short = 'v', long, action = clap::ArgAction::Count, global = true)] #[clap(short = 'v', long, action = clap::ArgAction::Count, global = true)]
pub(crate) verbose: u8, pub verbose: u8,
} }
impl<'a> Instrumentation { impl<'a> Instrumentation {
pub(crate) fn log_level(&self) -> String { pub fn log_level(&self) -> String {
match self.verbose { match self.verbose {
0 => "info", 0 => "info",
1 => "debug", 1 => "debug",
@ -22,7 +22,7 @@ impl<'a> Instrumentation {
.to_string() .to_string()
} }
pub(crate) fn setup<'b: 'a>(&'b self) -> eyre::Result<()> { pub fn setup<'b: 'a>(&'b self) -> eyre::Result<()> {
let fmt_layer = self.fmt_layer(); let fmt_layer = self.fmt_layer();
let filter_layer = self.filter_layer()?; let filter_layer = self.filter_layer()?;
@ -35,7 +35,7 @@ impl<'a> Instrumentation {
Ok(()) Ok(())
} }
pub(crate) fn fmt_layer<S>(&self) -> impl tracing_subscriber::layer::Layer<S> pub fn fmt_layer<S>(&self) -> impl tracing_subscriber::layer::Layer<S>
where where
S: tracing::Subscriber + for<'span> tracing_subscriber::registry::LookupSpan<'span>, S: tracing::Subscriber + for<'span> tracing_subscriber::registry::LookupSpan<'span>,
{ {
@ -45,7 +45,7 @@ impl<'a> Instrumentation {
.pretty() .pretty()
} }
pub(crate) fn filter_layer(&self) -> eyre::Result<EnvFilter> { pub fn filter_layer(&self) -> eyre::Result<EnvFilter> {
let filter_layer = match EnvFilter::try_from_default_env() { let filter_layer = match EnvFilter::try_from_default_env() {
Ok(layer) => layer, Ok(layer) => layer,
Err(e) => { Err(e) => {

View file

@ -2,5 +2,3 @@ mod instrumentation;
pub(crate) use instrumentation::Instrumentation; pub(crate) use instrumentation::Instrumentation;
mod channel_value; mod channel_value;
pub(crate) use channel_value::ChannelValue; pub(crate) use channel_value::ChannelValue;
mod plan_options;
pub(crate) use plan_options::PlanOptions;

View file

@ -1,45 +0,0 @@
use clap::{ArgAction, Parser};
use harmonic::Planner;
/// Plan an install that can be repeated on an identical host later
#[derive(Debug, Parser)]
pub(crate) struct PlanOptions {
/// Channel(s) to add by default, pass multiple times for multiple channels
#[clap(
long,
value_parser,
action = clap::ArgAction::Append,
env = "HARMONIC_CHANNEL",
default_value = "nixpkgs=https://nixos.org/channels/nixpkgs-unstable",
group = "plan_options"
)]
pub(crate) channel: Vec<crate::cli::arg::ChannelValue>,
/// Don't modify the user profile to automatically load nix
#[clap(
long,
action(ArgAction::SetTrue),
default_value = "false",
global = true,
group = "plan_options"
)]
pub(crate) no_modify_profile: bool,
/// Number of build users to create
#[clap(
long,
default_value = "32",
env = "HARMONIC_NIX_DAEMON_USER_COUNT",
group = "plan_options"
)]
pub(crate) daemon_user_count: usize,
#[clap(
long,
action(ArgAction::SetTrue),
default_value = "false",
global = true,
group = "plan_options"
)]
pub(crate) force: bool,
// Override the default planner for this OS/Architecture
#[clap(long, global = true, group = "plan_options", value_parser = clap::builder::EnumValueParser::<Planner>::new())]
pub(crate) planner: Option<Planner>,
}

View file

@ -7,7 +7,7 @@ use std::process::ExitCode;
use self::subcommand::HarmonicSubcommand; use self::subcommand::HarmonicSubcommand;
#[async_trait::async_trait] #[async_trait::async_trait]
pub(crate) trait CommandExecute { pub trait CommandExecute {
async fn execute(self) -> eyre::Result<ExitCode>; async fn execute(self) -> eyre::Result<ExitCode>;
} }
@ -16,12 +16,12 @@ pub(crate) trait CommandExecute {
/// Plans a Nix install, prompts for confirmation, then executes it /// Plans a Nix install, prompts for confirmation, then executes it
#[derive(Debug, Parser)] #[derive(Debug, Parser)]
#[clap(version)] #[clap(version)]
pub(crate) struct HarmonicCli { pub struct HarmonicCli {
#[clap(flatten)] #[clap(flatten)]
pub(crate) instrumentation: arg::Instrumentation, pub instrumentation: arg::Instrumentation,
#[clap(subcommand)] #[clap(subcommand)]
subcommand: HarmonicSubcommand, pub subcommand: HarmonicSubcommand,
} }
#[async_trait::async_trait] #[async_trait::async_trait]

View file

@ -1,41 +1,35 @@
use std::{path::PathBuf, process::ExitCode}; use std::{path::PathBuf, process::ExitCode};
use crate::BuiltinPlanner;
use clap::{ArgAction, Parser}; use clap::{ArgAction, Parser};
use eyre::{eyre, WrapErr}; use eyre::{eyre, WrapErr};
use harmonic::{InstallPlan, InstallSettings, Planner};
use crate::{ use crate::{cli::CommandExecute, interaction};
cli::{
arg::{ChannelValue, PlanOptions},
CommandExecute,
},
interaction,
};
/// Execute an install (possibly using an existing plan) /// Execute an install (possibly using an existing plan)
#[derive(Debug, Parser)] #[derive(Debug, Parser)]
pub(crate) struct Install { #[command(args_conflicts_with_subcommands = true)]
pub struct Install {
#[clap( #[clap(
long, long,
action(ArgAction::SetTrue), action(ArgAction::SetTrue),
default_value = "false", default_value = "false",
global = true global = true
)] )]
no_confirm: bool, pub no_confirm: bool,
#[clap(flatten)]
plan_options: PlanOptions,
#[clap( #[clap(
long, long,
action(ArgAction::SetTrue), action(ArgAction::SetTrue),
default_value = "false", default_value = "false",
global = true global = true
)] )]
pub(crate) explain: bool, pub explain: bool,
#[clap( #[clap(env = "HARMONIC_PLAN")]
conflicts_with_all = [ "plan_options" ], pub plan: Option<PathBuf>,
env = "HARMONIC_PLAN",
)] #[clap(subcommand)]
plan: Option<PathBuf>, pub planner: BuiltinPlanner,
} }
#[async_trait::async_trait] #[async_trait::async_trait]
@ -45,7 +39,7 @@ impl CommandExecute for Install {
let Self { let Self {
no_confirm, no_confirm,
plan, plan,
plan_options, planner,
explain, explain,
} = self; } = self;
@ -56,26 +50,7 @@ impl CommandExecute for Install {
.wrap_err("Reading plan")?; .wrap_err("Reading plan")?;
serde_json::from_str(&install_plan_string)? serde_json::from_str(&install_plan_string)?
}, },
None => { None => planner.plan().await?,
let mut settings = InstallSettings::default()?;
settings.force(plan_options.force);
settings.daemon_user_count(plan_options.daemon_user_count);
settings.channels(
plan_options
.channel
.into_iter()
.map(|ChannelValue(name, url)| (name, url)),
);
settings.modify_profile(!plan_options.no_modify_profile);
let planner = match plan_options.planner {
Some(planner) => planner,
None => Planner::default()?,
};
InstallPlan::new(planner, settings).await?
},
}; };
if !no_confirm { if !no_confirm {

View file

@ -6,7 +6,7 @@ mod uninstall;
use uninstall::Uninstall; use uninstall::Uninstall;
#[derive(Debug, clap::Subcommand)] #[derive(Debug, clap::Subcommand)]
pub(crate) enum HarmonicSubcommand { pub enum HarmonicSubcommand {
Plan(Plan), Plan(Plan),
Install(Install), Install(Install),
Uninstall(Uninstall), Uninstall(Uninstall),

View file

@ -1,61 +1,34 @@
use std::{path::PathBuf, process::ExitCode}; use std::{path::PathBuf, process::ExitCode};
use crate::BuiltinPlanner;
use clap::Parser; use clap::Parser;
use harmonic::{InstallPlan, InstallSettings, Planner};
use eyre::WrapErr; use eyre::WrapErr;
use crate::cli::{ use crate::cli::CommandExecute;
arg::{ChannelValue, PlanOptions},
CommandExecute,
};
/// Plan an install that can be repeated on an identical host later /// Plan an install that can be repeated on an identical host later
#[derive(Debug, Parser)] #[derive(Debug, Parser)]
pub(crate) struct Plan { #[command(multicall = true)]
#[clap(flatten)] pub struct Plan {
plan_options: PlanOptions, #[clap(subcommand)]
#[clap(default_value = "/dev/stdout")] pub planner: Option<BuiltinPlanner>,
pub(crate) plan: PathBuf, #[clap(env = "HARMONIC_PLAN")]
pub plan: PathBuf,
} }
#[async_trait::async_trait] #[async_trait::async_trait]
impl CommandExecute for Plan { impl CommandExecute for Plan {
#[tracing::instrument(skip_all, fields( #[tracing::instrument(skip_all, fields())]
channels = %self.plan_options.channel.iter().map(|ChannelValue(name, url)| format!("{name} {url}")).collect::<Vec<_>>().join(", "),
daemon_user_count = %self.plan_options.daemon_user_count,
no_modify_profile = %self.plan_options.no_modify_profile,
))]
async fn execute(self) -> eyre::Result<ExitCode> { async fn execute(self) -> eyre::Result<ExitCode> {
let Self { let Self { planner, plan } = self;
plan_options:
PlanOptions {
channel,
no_modify_profile,
daemon_user_count,
force,
planner,
},
plan,
} = self;
let mut settings = InstallSettings::default()?;
settings.force(force);
settings.daemon_user_count(daemon_user_count);
settings.channels(
channel
.into_iter()
.map(|ChannelValue(name, url)| (name, url)),
);
settings.modify_profile(!no_modify_profile);
let planner = match planner { let planner = match planner {
Some(planner) => planner, Some(planner) => planner,
None => Planner::default()?, None => BuiltinPlanner::default()?,
}; };
let install_plan = InstallPlan::new(planner, settings).await?; let install_plan = planner.plan().await?;
let json = serde_json::to_string_pretty(&install_plan)?; let json = serde_json::to_string_pretty(&install_plan)?;
tokio::fs::write(plan, json) tokio::fs::write(plan, json)

View file

@ -1,30 +1,30 @@
use std::{path::PathBuf, process::ExitCode}; use std::{path::PathBuf, process::ExitCode};
use crate::InstallPlan;
use clap::{ArgAction, Parser}; use clap::{ArgAction, Parser};
use eyre::WrapErr; use eyre::WrapErr;
use harmonic::InstallPlan;
use crate::{cli::CommandExecute, interaction}; use crate::{cli::CommandExecute, interaction};
/// 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)]
pub(crate) struct Uninstall { pub struct Uninstall {
#[clap( #[clap(
long, long,
action(ArgAction::SetTrue), action(ArgAction::SetTrue),
default_value = "false", default_value = "false",
global = true global = true
)] )]
no_confirm: bool, pub no_confirm: bool,
#[clap( #[clap(
long, long,
action(ArgAction::SetTrue), action(ArgAction::SetTrue),
default_value = "false", default_value = "false",
global = true global = true
)] )]
explain: bool, pub explain: bool,
#[clap(default_value = "/nix/receipt.json")] #[clap(default_value = "/nix/receipt.json")]
receipt: PathBuf, pub receipt: PathBuf,
} }
#[async_trait::async_trait] #[async_trait::async_trait]

View file

@ -1,19 +1,17 @@
mod actions; mod actions;
pub mod cli;
mod error; mod error;
mod interaction;
mod os; mod os;
mod plan; mod plan;
mod planner; mod planner;
mod settings; mod settings;
use std::{ use std::{ffi::OsStr, fmt::Display, process::Output};
ffi::OsStr,
fmt::Display,
process::{ExitStatus, Output},
};
pub use error::HarmonicError; pub use error::HarmonicError;
pub use plan::InstallPlan; pub use plan::InstallPlan;
pub use planner::Planner; pub use planner::BuiltinPlanner;
use serde::Serializer; use serde::Serializer;
pub use settings::InstallSettings; pub use settings::InstallSettings;

View file

@ -1,11 +1,7 @@
pub(crate) mod cli;
use std::process::ExitCode; use std::process::ExitCode;
pub mod interaction;
use clap::Parser; use clap::Parser;
use cli::CommandExecute; use harmonic::cli::CommandExecute;
#[tokio::main] #[tokio::main]
async fn main() -> color_eyre::Result<ExitCode> { async fn main() -> color_eyre::Result<ExitCode> {
@ -17,7 +13,7 @@ async fn main() -> color_eyre::Result<ExitCode> {
}) })
.install()?; .install()?;
let cli = cli::HarmonicCli::parse(); let cli = harmonic::cli::HarmonicCli::parse();
cli.instrumentation.setup()?; cli.instrumentation.setup()?;

View file

@ -2,32 +2,20 @@ use std::path::PathBuf;
use crate::{ use crate::{
actions::{Action, ActionDescription, ActionError, Actionable}, actions::{Action, ActionDescription, ActionError, Actionable},
planner::PlannerError, BuiltinPlanner, HarmonicError,
settings::InstallSettings,
HarmonicError, Planner,
}; };
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
pub struct InstallPlan { pub struct InstallPlan {
pub(crate) settings: InstallSettings,
pub(crate) actions: Vec<Action>, pub(crate) actions: Vec<Action>,
pub(crate) planner: Planner, pub(crate) planner: BuiltinPlanner,
} }
impl InstallPlan { impl InstallPlan {
pub async fn new(planner: Planner, settings: InstallSettings) -> Result<Self, PlannerError> {
planner.plan(settings).await
}
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
pub fn describe_execute(&self, explain: bool) -> String { pub fn describe_execute(&self, explain: bool) -> String {
let Self { let Self { planner, actions } = self;
planner,
settings,
actions,
} = self;
format!( format!(
"\ "\
This Nix install is for:\n\ This Nix install is for:\n\
@ -42,12 +30,7 @@ impl InstallPlan {
", ",
os_type = "Linux", os_type = "Linux",
init_type = "systemd", init_type = "systemd",
nix_channels = settings nix_channels = "todo",
.channels
.iter()
.map(|(name, url)| format!("{name}={url}"))
.collect::<Vec<_>>()
.join(","),
actions = actions actions = actions
.iter() .iter()
.map(|v| v.describe_execute()) .map(|v| v.describe_execute())
@ -76,7 +59,6 @@ impl InstallPlan {
pub async fn install(&mut self) -> Result<(), HarmonicError> { pub async fn install(&mut self) -> Result<(), HarmonicError> {
let Self { let Self {
actions, actions,
settings: _,
planner: _, planner: _,
} = self; } = self;
@ -97,11 +79,7 @@ impl InstallPlan {
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
pub fn describe_revert(&self, explain: bool) -> String { pub fn describe_revert(&self, explain: bool) -> String {
let Self { let Self { planner, actions } = self;
planner,
settings,
actions,
} = self;
format!( format!(
"\ "\
This Nix uninstall is for:\n\ This Nix uninstall is for:\n\
@ -116,12 +94,7 @@ impl InstallPlan {
", ",
os_type = "Linux", os_type = "Linux",
init_type = "systemd", init_type = "systemd",
nix_channels = settings nix_channels = "todo",
.channels
.iter()
.map(|(name, url)| format!("{name}={url}"))
.collect::<Vec<_>>()
.join(","),
actions = actions actions = actions
.iter() .iter()
.map(|v| v.describe_revert()) .map(|v| v.describe_revert())
@ -150,7 +123,6 @@ impl InstallPlan {
pub async fn revert(&mut self) -> Result<(), HarmonicError> { pub async fn revert(&mut self) -> Result<(), HarmonicError> {
let Self { let Self {
actions, actions,
settings: _,
planner: _, planner: _,
} = self; } = self;

View file

@ -1,3 +1,3 @@
mod multi_user; mod multi;
pub use multi_user::DarwinMultiUser; pub use multi::DarwinMulti;

View file

@ -11,20 +11,27 @@ use crate::{
execute_command, execute_command,
os::darwin::DiskUtilOutput, os::darwin::DiskUtilOutput,
planner::{Plannable, PlannerError}, planner::{Plannable, PlannerError},
InstallPlan, Planner, BuiltinPlanner, InstallPlan, InstallSettings,
}; };
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)] #[derive(Debug, Clone, clap::Parser, serde::Serialize, serde::Deserialize)]
pub struct DarwinMultiUser; pub struct DarwinMulti {
#[clap(flatten)]
settings: InstallSettings,
}
#[async_trait::async_trait] #[async_trait::async_trait]
impl Plannable for DarwinMultiUser { impl Plannable for DarwinMulti {
const DISPLAY_STRING: &'static str = "Darwin Multi-User"; const DISPLAY_STRING: &'static str = "Darwin Multi-User";
const SLUG: &'static str = "darwin-multi"; const SLUG: &'static str = "darwin-multi";
async fn plan( fn default() -> Result<Self, PlannerError> {
settings: crate::InstallSettings, Ok(Self {
) -> Result<crate::InstallPlan, crate::planner::PlannerError> { settings: InstallSettings::default()?,
})
}
async fn plan(self) -> Result<crate::InstallPlan, crate::planner::PlannerError> {
let root_disk = { let root_disk = {
let buf = let buf =
execute_command(Command::new("/usr/sbin/diskutil").args(["info", "-plist", "/"])) execute_command(Command::new("/usr/sbin/diskutil").args(["info", "-plist", "/"]))
@ -39,8 +46,7 @@ impl Plannable for DarwinMultiUser {
let volume_label = "Nix Store".into(); let volume_label = "Nix Store".into();
Ok(InstallPlan { Ok(InstallPlan {
planner: Self.into(), planner: self.clone().into(),
settings: settings.clone(),
actions: vec![ actions: vec![
// Create Volume step: // Create Volume step:
// //
@ -50,11 +56,11 @@ impl Plannable for DarwinMultiUser {
.await .await
.map(Action::from) .map(Action::from)
.map_err(ActionError::from)?, .map_err(ActionError::from)?,
ProvisionNix::plan(settings.clone()) ProvisionNix::plan(self.settings.clone())
.await .await
.map(Action::from) .map(Action::from)
.map_err(ActionError::from)?, .map_err(ActionError::from)?,
ConfigureNix::plan(settings) ConfigureNix::plan(self.settings)
.await .await
.map(Action::from) .map(Action::from)
.map_err(ActionError::from)?, .map_err(ActionError::from)?,
@ -67,8 +73,8 @@ impl Plannable for DarwinMultiUser {
} }
} }
impl Into<Planner> for DarwinMultiUser { impl Into<BuiltinPlanner> for DarwinMulti {
fn into(self) -> Planner { fn into(self) -> BuiltinPlanner {
Planner::DarwinMultiUser BuiltinPlanner::DarwinMulti(self)
} }
} }

View file

@ -1,3 +1,3 @@
mod multi_user; mod multi;
pub use multi_user::LinuxMultiUser; pub use multi::LinuxMulti;

View file

@ -5,31 +5,39 @@ use crate::{
Action, ActionError, Action, ActionError,
}, },
planner::{Plannable, PlannerError}, planner::{Plannable, PlannerError},
InstallPlan, InstallSettings, Planner, BuiltinPlanner, InstallPlan, InstallSettings,
}; };
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)] #[derive(Debug, Clone, clap::Parser, serde::Serialize, serde::Deserialize)]
pub struct LinuxMultiUser; pub struct LinuxMulti {
#[clap(flatten)]
settings: InstallSettings,
}
#[async_trait::async_trait] #[async_trait::async_trait]
impl Plannable for LinuxMultiUser { impl Plannable for LinuxMulti {
const DISPLAY_STRING: &'static str = "Linux Multi-User"; const DISPLAY_STRING: &'static str = "Linux Multi-User";
const SLUG: &'static str = "linux-multi"; const SLUG: &'static str = "linux-multi";
async fn plan(settings: InstallSettings) -> Result<InstallPlan, PlannerError> { fn default() -> Result<Self, PlannerError> {
Ok(Self {
settings: InstallSettings::default()?,
})
}
async fn plan(self) -> Result<InstallPlan, PlannerError> {
Ok(InstallPlan { Ok(InstallPlan {
planner: Self.into(), planner: self.clone().into(),
settings: settings.clone(),
actions: vec![ actions: vec![
CreateDirectory::plan("/nix", None, None, 0o0755, true) CreateDirectory::plan("/nix", None, None, 0o0755, true)
.await .await
.map(Action::from) .map(Action::from)
.map_err(ActionError::from)?, .map_err(ActionError::from)?,
ProvisionNix::plan(settings.clone()) ProvisionNix::plan(self.settings.clone())
.await .await
.map(Action::from) .map(Action::from)
.map_err(ActionError::from)?, .map_err(ActionError::from)?,
ConfigureNix::plan(settings) ConfigureNix::plan(self.settings)
.await .await
.map(Action::from) .map(Action::from)
.map_err(ActionError::from)?, .map_err(ActionError::from)?,
@ -42,8 +50,8 @@ impl Plannable for LinuxMultiUser {
} }
} }
impl Into<Planner> for LinuxMultiUser { impl Into<BuiltinPlanner> for LinuxMulti {
fn into(self) -> Planner { fn into(self) -> BuiltinPlanner {
Planner::LinuxMultiUser BuiltinPlanner::LinuxMulti(self)
} }
} }

View file

@ -1,51 +1,57 @@
mod darwin; pub mod darwin;
mod linux; pub mod linux;
mod specific; pub mod specific;
use crate::{actions::ActionError, InstallPlan, InstallSettings}; use crate::{actions::ActionError, settings::InstallSettingsError, InstallPlan};
#[derive(Debug, Clone, clap::ValueEnum, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, clap::Subcommand, serde::Serialize, serde::Deserialize)]
pub enum Planner { pub enum BuiltinPlanner {
LinuxMultiUser, LinuxMulti(linux::LinuxMulti),
DarwinMultiUser, DarwinMulti(darwin::DarwinMulti),
SteamDeck, SteamDeck(specific::SteamDeck),
} }
impl Planner { impl BuiltinPlanner {
pub fn possible_values() -> &'static [Planner] {
&[Self::LinuxMultiUser, Self::DarwinMultiUser, Self::SteamDeck]
}
pub fn default() -> Result<Self, PlannerError> { pub 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) => Ok(Self::LinuxMultiUser), (Architecture::X86_64, OperatingSystem::Linux) => {
(Architecture::Aarch64(_), OperatingSystem::Linux) => Ok(Self::LinuxMultiUser), Ok(Self::LinuxMulti(linux::LinuxMulti::default()?))
},
(Architecture::Aarch64(_), OperatingSystem::Linux) => {
Ok(Self::LinuxMulti(linux::LinuxMulti::default()?))
},
(Architecture::X86_64, OperatingSystem::MacOSX { .. }) (Architecture::X86_64, OperatingSystem::MacOSX { .. })
| (Architecture::X86_64, OperatingSystem::Darwin) => Ok(Self::DarwinMultiUser), | (Architecture::X86_64, OperatingSystem::Darwin) => {
Ok(Self::DarwinMulti(darwin::DarwinMulti::default()?))
},
(Architecture::Aarch64(_), OperatingSystem::MacOSX { .. }) (Architecture::Aarch64(_), OperatingSystem::MacOSX { .. })
| (Architecture::Aarch64(_), OperatingSystem::Darwin) => Ok(Self::DarwinMultiUser), | (Architecture::Aarch64(_), OperatingSystem::Darwin) => {
Ok(Self::DarwinMulti(darwin::DarwinMulti::default()?))
},
_ => Err(PlannerError::UnsupportedArchitecture(target_lexicon::HOST)), _ => Err(PlannerError::UnsupportedArchitecture(target_lexicon::HOST)),
} }
} }
pub async fn plan(self, settings: InstallSettings) -> Result<InstallPlan, PlannerError> { pub async fn plan(self) -> Result<InstallPlan, PlannerError> {
match self { match self {
Planner::LinuxMultiUser => linux::LinuxMultiUser::plan(settings).await, BuiltinPlanner::LinuxMulti(planner) => planner.plan().await,
Planner::DarwinMultiUser => darwin::DarwinMultiUser::plan(settings).await, BuiltinPlanner::DarwinMulti(planner) => planner.plan().await,
Planner::SteamDeck => specific::SteamDeck::plan(settings).await, BuiltinPlanner::SteamDeck(planner) => planner.plan().await,
} }
} }
} }
#[async_trait::async_trait] #[async_trait::async_trait]
trait Plannable: Into<Planner> trait Plannable: Into<BuiltinPlanner>
where where
Self: Sized, Self: Sized,
{ {
const DISPLAY_STRING: &'static str; const DISPLAY_STRING: &'static str;
const SLUG: &'static str; const SLUG: &'static str;
async fn plan(settings: InstallSettings) -> Result<InstallPlan, PlannerError>; fn default() -> Result<Self, PlannerError>;
async fn plan(self) -> Result<InstallPlan, PlannerError>;
} }
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
@ -58,4 +64,6 @@ pub enum PlannerError {
#[from] #[from]
ActionError, ActionError,
), ),
#[error(transparent)]
InstallSettings(#[from] InstallSettingsError),
} }

View file

@ -4,24 +4,30 @@ use crate::{
meta::{CreateSystemdSysext, ProvisionNix}, meta::{CreateSystemdSysext, ProvisionNix},
Action, ActionError, Action, ActionError,
}, },
planner::Plannable, planner::{Plannable, PlannerError},
InstallPlan, Planner, BuiltinPlanner, InstallPlan, InstallSettings,
}; };
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)] #[derive(Debug, Clone, clap::Parser, serde::Serialize, serde::Deserialize)]
pub struct SteamDeck; pub struct SteamDeck {
#[clap(flatten)]
settings: InstallSettings,
}
#[async_trait::async_trait] #[async_trait::async_trait]
impl Plannable for SteamDeck { impl Plannable for SteamDeck {
const DISPLAY_STRING: &'static str = "Steam Deck (x86_64 Linux Multi-User)"; const DISPLAY_STRING: &'static str = "Steam Deck (x86_64 Linux Multi-User)";
const SLUG: &'static str = "steam-deck"; const SLUG: &'static str = "steam-deck";
async fn plan( fn default() -> Result<Self, PlannerError> {
settings: crate::InstallSettings, Ok(Self {
) -> Result<crate::InstallPlan, crate::planner::PlannerError> { settings: InstallSettings::default()?,
})
}
async fn plan(self) -> Result<crate::InstallPlan, PlannerError> {
Ok(InstallPlan { Ok(InstallPlan {
planner: Self.into(), planner: self.clone().into(),
settings: settings.clone(),
actions: vec![ actions: vec![
CreateSystemdSysext::plan("/var/lib/extensions") CreateSystemdSysext::plan("/var/lib/extensions")
.await .await
@ -31,7 +37,7 @@ impl Plannable for SteamDeck {
.await .await
.map(Action::from) .map(Action::from)
.map_err(ActionError::from)?, .map_err(ActionError::from)?,
ProvisionNix::plan(settings.clone()) ProvisionNix::plan(self.settings.clone())
.await .await
.map(Action::from) .map(Action::from)
.map_err(ActionError::from)?, .map_err(ActionError::from)?,
@ -44,8 +50,8 @@ impl Plannable for SteamDeck {
} }
} }
impl Into<Planner> for SteamDeck { impl Into<BuiltinPlanner> for SteamDeck {
fn into(self) -> Planner { fn into(self) -> BuiltinPlanner {
Planner::SteamDeck BuiltinPlanner::SteamDeck(self)
} }
} }

View file

@ -1,22 +1,90 @@
use crate::planner; use clap::ArgAction;
use target_lexicon::Triple; use derivative::Derivative;
use url::Url; use url::Url;
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";
pub const NIX_AARCH64_LINUX_URL: &str =
"https://releases.nixos.org/nix/nix-2.11.0/nix-2.11.0-aarch64-linux.tar.xz";
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";
pub const NIX_AARCH64_DARWIN_URL: &str =
"https://releases.nixos.org/nix/nix-2.11.0/nix-2.11.0-aarch64-darwin.tar.xz";
#[serde_with::serde_as] #[serde_with::serde_as]
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] #[derive(Debug, serde::Deserialize, serde::Serialize, Clone, clap::Parser)]
pub struct InstallSettings { pub struct InstallSettings {
pub(crate) daemon_user_count: usize, /// Channel(s) to add by default, pass multiple times for multiple channels
pub(crate) channels: Vec<(String, Url)>, #[clap(
long,
value_parser,
name = "channel",
action = clap::ArgAction::Append,
env = "HARMONIC_CHANNEL",
default_value = "nixpkgs=https://nixos.org/channels/nixpkgs-unstable",
)]
pub(crate) channels: Vec<crate::cli::arg::ChannelValue>,
/// Modify the user profile to automatically load nix
#[clap(
long,
action(ArgAction::SetFalse),
default_value = "true",
global = true,
env = "HARMONIC_NO_MODIFY_PROFILE",
name = "no-modify-profile"
)]
pub(crate) modify_profile: bool, pub(crate) modify_profile: bool,
/// Number of build users to create
#[clap(long, default_value = "32", env = "HARMONIC_DAEMON_USER_COUNT")]
pub(crate) daemon_user_count: usize,
#[clap(long, default_value = "nixbld", env = "HARMONIC_NIX_BUILD_GROUP_NAME")]
pub(crate) nix_build_group_name: String, pub(crate) nix_build_group_name: String,
#[clap(long, default_value_t = 3000, env = "HARMONIC_NIX_BUILD_GROUP_ID")]
pub(crate) nix_build_group_id: usize, pub(crate) 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(crate) nix_build_user_prefix: String, pub(crate) 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(crate) nix_build_user_id_base: usize, pub(crate) nix_build_user_id_base: usize,
#[clap(long, env = "HARMONIC_NIX_PACKAGE_URL")]
#[cfg_attr(
all(target_os = "macos", target_arch = "x86_64"),
clap(
default_value = NIX_X64_64_DARWIN_URL,
)
)]
#[cfg_attr(
all(target_os = "macos", target_arch = "aarch64"),
clap(
default_value = NIX_AARCH64_DARWIN_URL,
)
)]
#[cfg_attr(
all(target_os = "linux", target_arch = "x86_64"),
clap(
default_value = NIX_X64_64_LINUX_URL,
)
)]
#[cfg_attr(
all(target_os = "linux", target_arch = "aarch64"),
clap(
default_value = NIX_AARCH64_LINUX_URL,
)
)]
pub(crate) nix_package_url: Url, pub(crate) nix_package_url: Url,
#[clap(long, env = "HARMONIC_EXTRA_CONF")]
pub(crate) extra_conf: Option<String>, pub(crate) extra_conf: Option<String>,
#[clap(
long,
action(ArgAction::SetTrue),
default_value = "false",
global = true,
env = "HARMONIC_FORCE"
)]
pub(crate) force: bool, pub(crate) force: bool,
#[serde_as(as = "serde_with::DisplayFromStr")]
pub(crate) triple: Triple,
} }
impl InstallSettings { impl InstallSettings {
@ -28,24 +96,24 @@ impl InstallSettings {
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) => {
url = "https://releases.nixos.org/nix/nix-2.11.0/nix-2.11.0-x86_64-linux.tar.xz"; url = NIX_X64_64_LINUX_URL;
nix_build_user_prefix = "nixbld"; nix_build_user_prefix = "nixbld";
nix_build_user_id_base = 3000; nix_build_user_id_base = 3000;
}, },
(Architecture::Aarch64(_), OperatingSystem::Linux) => { (Architecture::Aarch64(_), OperatingSystem::Linux) => {
url = "https://releases.nixos.org/nix/nix-2.11.0/nix-2.11.0-aarch64-linux.tar.xz"; url = NIX_AARCH64_LINUX_URL;
nix_build_user_prefix = "nixbld"; nix_build_user_prefix = "nixbld";
nix_build_user_id_base = 3000; nix_build_user_id_base = 3000;
}, },
(Architecture::X86_64, OperatingSystem::MacOSX { .. }) (Architecture::X86_64, OperatingSystem::MacOSX { .. })
| (Architecture::X86_64, OperatingSystem::Darwin) => { | (Architecture::X86_64, OperatingSystem::Darwin) => {
url = "https://releases.nixos.org/nix/nix-2.11.0/nix-2.11.0-x86_64-darwin.tar.xz"; url = NIX_X64_64_DARWIN_URL;
nix_build_user_prefix = "_nixbld"; nix_build_user_prefix = "_nixbld";
nix_build_user_id_base = 300; nix_build_user_id_base = 300;
}, },
(Architecture::Aarch64(_), OperatingSystem::MacOSX { .. }) (Architecture::Aarch64(_), OperatingSystem::MacOSX { .. })
| (Architecture::Aarch64(_), OperatingSystem::Darwin) => { | (Architecture::Aarch64(_), OperatingSystem::Darwin) => {
url = "https://releases.nixos.org/nix/nix-2.11.0/nix-2.11.0-aarch64-darwin.tar.xz"; url = NIX_AARCH64_DARWIN_URL;
nix_build_user_prefix = "_nixbld"; nix_build_user_prefix = "_nixbld";
nix_build_user_id_base = 300; nix_build_user_id_base = 300;
}, },
@ -57,17 +125,14 @@ impl InstallSettings {
}; };
Ok(Self { Ok(Self {
triple: target_lexicon::HOST,
daemon_user_count: Default::default(), daemon_user_count: Default::default(),
channels: Default::default(), channels: Vec::default(),
modify_profile: Default::default(), modify_profile: true,
nix_build_group_name: String::from("nixbld"), nix_build_group_name: String::from("nixbld"),
nix_build_group_id: 3000, nix_build_group_id: 3000,
nix_build_user_prefix: nix_build_user_prefix.to_string(), nix_build_user_prefix: nix_build_user_prefix.to_string(),
nix_build_user_id_base, nix_build_user_id_base,
nix_package_url: url nix_package_url: url.parse()?,
.parse()
.expect("Could not parse default Nix archive url, please report this issue"),
extra_conf: Default::default(), extra_conf: Default::default(),
force: false, force: false,
}) })
@ -82,7 +147,7 @@ impl InstallSettings {
} }
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().collect(); self.channels = channels.into_iter().map(Into::into).collect();
self self
} }
@ -128,10 +193,10 @@ impl InstallSettings {
pub enum InstallSettingsError { pub enum InstallSettingsError {
#[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),
#[error("Planner error")] #[error("Parsing URL")]
Planner( Parse(
#[source] #[source]
#[from] #[from]
planner::PlannerError, url::ParseError,
), ),
} }