forked from lix-project/lix-installer
Merge pull request #39 from DeterminateSystems/hoverbear/ds-411-install-should-detect-existing
Install can detect existing /nix/receipt.json
This commit is contained in:
commit
460c0ba8c2
|
@ -184,4 +184,8 @@ impl Action for ConfigureNix {
|
|||
*action_state = ActionState::Uncompleted;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn action_state(&self) -> ActionState {
|
||||
self.action_state
|
||||
}
|
||||
}
|
||||
|
|
|
@ -221,6 +221,10 @@ impl Action for ConfigureNixDaemonService {
|
|||
*action_state = ActionState::Uncompleted;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn action_state(&self) -> ActionState {
|
||||
self.action_state
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
|
|
|
@ -181,6 +181,10 @@ impl Action for ConfigureShellProfile {
|
|||
*action_state = ActionState::Uncompleted;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn action_state(&self) -> ActionState {
|
||||
self.action_state
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
|
|
|
@ -214,6 +214,10 @@ impl Action for CreateDirectory {
|
|||
*action_state = ActionState::Uncompleted;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn action_state(&self) -> ActionState {
|
||||
self.action_state
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
|
|
|
@ -188,6 +188,10 @@ impl Action for CreateFile {
|
|||
*action_state = ActionState::Uncompleted;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn action_state(&self) -> ActionState {
|
||||
self.action_state
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
|
|
|
@ -160,6 +160,10 @@ impl Action for CreateGroup {
|
|||
*action_state = ActionState::Uncompleted;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn action_state(&self) -> ActionState {
|
||||
self.action_state
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
|
|
|
@ -134,6 +134,10 @@ impl Action for CreateNixTree {
|
|||
*action_state = ActionState::Uncompleted;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn action_state(&self) -> ActionState {
|
||||
self.action_state
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
|
|
|
@ -223,6 +223,10 @@ impl Action for CreateOrAppendFile {
|
|||
*action_state = ActionState::Uncompleted;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn action_state(&self) -> ActionState {
|
||||
self.action_state
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
|
|
|
@ -254,6 +254,10 @@ impl Action for CreateUser {
|
|||
*action_state = ActionState::Uncompleted;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn action_state(&self) -> ActionState {
|
||||
self.action_state
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
|
|
|
@ -229,6 +229,10 @@ impl Action for CreateUsersAndGroup {
|
|||
*action_state = ActionState::Uncompleted;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn action_state(&self) -> ActionState {
|
||||
self.action_state
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
|
|
|
@ -115,6 +115,10 @@ impl Action for FetchNix {
|
|||
*action_state = ActionState::Uncompleted;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn action_state(&self) -> ActionState {
|
||||
self.action_state
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
|
|
|
@ -106,6 +106,10 @@ impl Action for MoveUnpackedNix {
|
|||
*action_state = ActionState::Uncompleted;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn action_state(&self) -> ActionState {
|
||||
self.action_state
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
|
|
|
@ -130,6 +130,10 @@ impl Action for PlaceChannelConfiguration {
|
|||
*action_state = ActionState::Uncompleted;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn action_state(&self) -> ActionState {
|
||||
self.action_state
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
|
|
|
@ -115,6 +115,10 @@ impl Action for PlaceNixConfiguration {
|
|||
*action_state = ActionState::Uncompleted;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn action_state(&self) -> ActionState {
|
||||
self.action_state
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
|
|
|
@ -168,6 +168,10 @@ impl Action for ProvisionNix {
|
|||
*action_state = ActionState::Uncompleted;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn action_state(&self) -> ActionState {
|
||||
self.action_state
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
|
|
|
@ -181,6 +181,10 @@ impl Action for SetupDefaultProfile {
|
|||
*action_state = ActionState::Completed;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn action_state(&self) -> ActionState {
|
||||
self.action_state
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
|
|
|
@ -106,6 +106,10 @@ impl Action for BootstrapVolume {
|
|||
*action_state = ActionState::Completed;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn action_state(&self) -> ActionState {
|
||||
self.action_state
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
|
|
|
@ -289,6 +289,10 @@ impl Action for CreateApfsVolume {
|
|||
*action_state = ActionState::Uncompleted;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn action_state(&self) -> ActionState {
|
||||
self.action_state
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
|
|
|
@ -98,6 +98,10 @@ impl Action for CreateSyntheticObjects {
|
|||
*action_state = ActionState::Completed;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn action_state(&self) -> ActionState {
|
||||
self.action_state
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
|
|
|
@ -130,6 +130,10 @@ impl Action for CreateVolume {
|
|||
*action_state = ActionState::Completed;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn action_state(&self) -> ActionState {
|
||||
self.action_state
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
|
|
|
@ -109,6 +109,10 @@ impl Action for EnableOwnership {
|
|||
*action_state = ActionState::Completed;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn action_state(&self) -> ActionState {
|
||||
self.action_state
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
|
|
|
@ -86,6 +86,10 @@ impl Action for EncryptVolume {
|
|||
*action_state = ActionState::Completed;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn action_state(&self) -> ActionState {
|
||||
self.action_state
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
|
|
|
@ -96,6 +96,10 @@ impl Action for KickstartLaunchctlService {
|
|||
*action_state = ActionState::Completed;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn action_state(&self) -> ActionState {
|
||||
self.action_state
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
|
|
|
@ -117,6 +117,10 @@ impl Action for UnmountVolume {
|
|||
*action_state = ActionState::Completed;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn action_state(&self) -> ActionState {
|
||||
self.action_state
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
|
|
|
@ -185,6 +185,10 @@ impl Action for CreateSystemdSysext {
|
|||
*action_state = ActionState::Uncompleted;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn action_state(&self) -> ActionState {
|
||||
self.action_state
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
|
|
|
@ -102,6 +102,10 @@ impl Action for StartSystemdUnit {
|
|||
*action_state = ActionState::Completed;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn action_state(&self) -> ActionState {
|
||||
self.action_state
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
|
|
|
@ -102,6 +102,10 @@ impl Action for SystemdSysextMerge {
|
|||
*action_state = ActionState::Completed;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn action_state(&self) -> ActionState {
|
||||
self.action_state
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
|
|
|
@ -13,11 +13,12 @@ pub trait Action: Send + Sync + std::fmt::Debug + dyn_clone::DynClone {
|
|||
// They should also have an `async fn plan(args...) -> Result<ActionState<Self>, Box<dyn std::error::Error + Send + Sync>>;`
|
||||
async fn execute(&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;
|
||||
}
|
||||
|
||||
dyn_clone::clone_trait_object!(Action);
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Copy)]
|
||||
pub enum ActionState {
|
||||
Completed,
|
||||
// Only applicable to meta-actions that start multiple sub-actions.
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
use std::{path::PathBuf, process::ExitCode};
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
process::ExitCode,
|
||||
};
|
||||
|
||||
use crate::BuiltinPlanner;
|
||||
use crate::{action::ActionState, plan::RECEIPT_LOCATION, BuiltinPlanner, InstallPlan, Planner};
|
||||
use clap::{ArgAction, Parser};
|
||||
use eyre::{eyre, WrapErr};
|
||||
|
||||
|
@ -8,7 +11,7 @@ use crate::{cli::CommandExecute, interaction};
|
|||
|
||||
/// Execute an install (possibly using an existing plan)
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(args_conflicts_with_subcommands = true)]
|
||||
#[command(args_conflicts_with_subcommands = true, arg_required_else_help = true)]
|
||||
pub struct Install {
|
||||
#[clap(
|
||||
long,
|
||||
|
@ -29,7 +32,7 @@ pub struct Install {
|
|||
pub plan: Option<PathBuf>,
|
||||
|
||||
#[clap(subcommand)]
|
||||
pub planner: BuiltinPlanner,
|
||||
pub planner: Option<BuiltinPlanner>,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
|
@ -43,30 +46,68 @@ impl CommandExecute for Install {
|
|||
explain,
|
||||
} = self;
|
||||
|
||||
let mut plan = match &plan {
|
||||
Some(plan_path) => {
|
||||
let install_plan_string = tokio::fs::read_to_string(&plan_path)
|
||||
let existing_receipt: Option<InstallPlan> = match Path::new(RECEIPT_LOCATION).exists() {
|
||||
true => {
|
||||
let install_plan_string = tokio::fs::read_to_string(&RECEIPT_LOCATION)
|
||||
.await
|
||||
.wrap_err("Reading plan")?;
|
||||
Some(serde_json::from_str(&install_plan_string)?)
|
||||
},
|
||||
false => None,
|
||||
};
|
||||
|
||||
let mut install_plan = match (planner, plan) {
|
||||
(Some(planner), None) => {
|
||||
let chosen_planner: Box<dyn Planner> = planner.clone().boxed();
|
||||
|
||||
match existing_receipt {
|
||||
Some(existing_receipt) => {
|
||||
if existing_receipt.planner.typetag_name() != chosen_planner.typetag_name() {
|
||||
return Err(eyre!("Found existing plan in `{RECEIPT_LOCATION}` which used a different planner, try uninstalling the existing install"))
|
||||
}
|
||||
if existing_receipt.planner.settings().map_err(|e| eyre!(e))? != chosen_planner.settings().map_err(|e| eyre!(e))? {
|
||||
return Err(eyre!("Found existing plan in `{RECEIPT_LOCATION}` which used different planner settings, try uninstalling the existing install"))
|
||||
}
|
||||
if existing_receipt.actions.iter().all(|v| v.action_state() == ActionState::Completed) {
|
||||
return Err(eyre!("Found existing plan in `{RECEIPT_LOCATION}`, with the same settings, already completed, try uninstalling and reinstalling if Nix isn't working"))
|
||||
}
|
||||
existing_receipt
|
||||
} ,
|
||||
None => {
|
||||
planner.plan().await.map_err(|e| eyre!(e))?
|
||||
},
|
||||
}
|
||||
},
|
||||
(None, Some(plan_path)) => {
|
||||
let install_plan_string = tokio::fs::read_to_string(&plan_path)
|
||||
.await
|
||||
.wrap_err("Reading plan")?;
|
||||
serde_json::from_str(&install_plan_string)?
|
||||
},
|
||||
None => planner.plan().await.map_err(|e| eyre!(e))?,
|
||||
(None, None) => return Err(eyre!("`--plan` or a planner is required")),
|
||||
(Some(_), Some(_)) => return Err(eyre!("`--plan` conflicts with passing a planner, a planner creates plans, so passing an existing plan doesn't make sense")),
|
||||
};
|
||||
|
||||
if !no_confirm {
|
||||
if !interaction::confirm(plan.describe_execute(explain).map_err(|e| eyre!(e))?).await? {
|
||||
if !interaction::confirm(
|
||||
install_plan
|
||||
.describe_execute(explain)
|
||||
.map_err(|e| eyre!(e))?,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
interaction::clean_exit_with_message("Okay, didn't do anything! Bye!").await;
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(err) = plan.install().await {
|
||||
if let Err(err) = install_plan.install().await {
|
||||
let error = eyre!(err).wrap_err("Install failure");
|
||||
if !no_confirm {
|
||||
tracing::error!("{:?}", error);
|
||||
if !interaction::confirm(plan.describe_revert(explain)).await? {
|
||||
if !interaction::confirm(install_plan.describe_revert(explain)).await? {
|
||||
interaction::clean_exit_with_message("Okay, didn't do anything! Bye!").await;
|
||||
}
|
||||
plan.revert().await?
|
||||
install_plan.revert().await?
|
||||
} else {
|
||||
return Err(error);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use std::{path::PathBuf, process::ExitCode};
|
||||
|
||||
use crate::InstallPlan;
|
||||
use crate::{plan::RECEIPT_LOCATION, InstallPlan};
|
||||
use clap::{ArgAction, Parser};
|
||||
use eyre::WrapErr;
|
||||
|
||||
|
@ -23,7 +23,7 @@ pub struct Uninstall {
|
|||
global = true
|
||||
)]
|
||||
pub explain: bool,
|
||||
#[clap(default_value = "/nix/receipt.json")]
|
||||
#[clap(default_value = RECEIPT_LOCATION)]
|
||||
pub receipt: PathBuf,
|
||||
}
|
||||
|
||||
|
|
|
@ -8,6 +8,8 @@ use crate::{
|
|||
HarmonicError,
|
||||
};
|
||||
|
||||
pub const RECEIPT_LOCATION: &str = "/nix/receipt.json";
|
||||
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
|
||||
pub struct InstallPlan {
|
||||
pub(crate) actions: Vec<Box<dyn Action>>,
|
||||
|
@ -43,7 +45,7 @@ impl InstallPlan {
|
|||
},
|
||||
planner = planner.typetag_name(),
|
||||
plan_settings = planner
|
||||
.describe()?
|
||||
.settings()?
|
||||
.into_iter()
|
||||
.map(|(k, v)| format!("* {k}: {v}", k = k.bold().white()))
|
||||
.collect::<Vec<_>>()
|
||||
|
@ -166,7 +168,7 @@ async fn write_receipt(plan: InstallPlan) -> Result<(), HarmonicError> {
|
|||
tokio::fs::create_dir_all("/nix")
|
||||
.await
|
||||
.map_err(|e| HarmonicError::RecordingReceipt(PathBuf::from("/nix"), e))?;
|
||||
let install_receipt_path = PathBuf::from("/nix/receipt.json");
|
||||
let install_receipt_path = PathBuf::from(RECEIPT_LOCATION);
|
||||
let self_json =
|
||||
serde_json::to_string_pretty(&plan).map_err(HarmonicError::SerializingReceipt)?;
|
||||
tokio::fs::write(&install_receipt_path, self_json)
|
||||
|
|
|
@ -98,7 +98,7 @@ impl Planner for DarwinMulti {
|
|||
})
|
||||
}
|
||||
|
||||
fn describe(
|
||||
fn settings(
|
||||
&self,
|
||||
) -> Result<HashMap<String, serde_json::Value>, Box<dyn std::error::Error + Sync + Send>> {
|
||||
let Self {
|
||||
|
|
|
@ -35,7 +35,7 @@ impl Planner for LinuxMulti {
|
|||
})
|
||||
}
|
||||
|
||||
fn describe(
|
||||
fn settings(
|
||||
&self,
|
||||
) -> Result<HashMap<String, serde_json::Value>, Box<dyn std::error::Error + Sync + Send>> {
|
||||
let Self { settings } = self;
|
||||
|
|
|
@ -13,9 +13,15 @@ pub trait Planner: std::fmt::Debug + Send + Sync + dyn_clone::DynClone {
|
|||
where
|
||||
Self: Sized;
|
||||
async fn plan(self) -> Result<InstallPlan, Box<dyn std::error::Error + Sync + Send>>;
|
||||
fn describe(
|
||||
fn settings(
|
||||
&self,
|
||||
) -> Result<HashMap<String, serde_json::Value>, Box<dyn std::error::Error + Sync + Send>>;
|
||||
fn boxed(self) -> Box<dyn Planner>
|
||||
where
|
||||
Self: Sized + 'static,
|
||||
{
|
||||
Box::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
dyn_clone::clone_trait_object!(Planner);
|
||||
|
@ -56,6 +62,13 @@ impl BuiltinPlanner {
|
|||
BuiltinPlanner::SteamDeck(planner) => planner.plan().await,
|
||||
}
|
||||
}
|
||||
pub fn boxed(self) -> Box<dyn Planner> {
|
||||
match self {
|
||||
BuiltinPlanner::LinuxMulti(i) => i.boxed(),
|
||||
BuiltinPlanner::DarwinMulti(i) => i.boxed(),
|
||||
BuiltinPlanner::SteamDeck(i) => i.boxed(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
|
|
|
@ -36,7 +36,7 @@ impl Planner for SteamDeck {
|
|||
})
|
||||
}
|
||||
|
||||
fn describe(
|
||||
fn settings(
|
||||
&self,
|
||||
) -> Result<HashMap<String, serde_json::Value>, Box<dyn std::error::Error + Sync + Send>> {
|
||||
let Self { settings } = self;
|
||||
|
|
Loading…
Reference in a new issue