Skip to content

Commit

Permalink
Grahamc/fh 560 suggest dn (#1415)
Browse files Browse the repository at this point in the history
  • Loading branch information
grahamc authored Feb 7, 2025
1 parent 78bd674 commit 2542680
Show file tree
Hide file tree
Showing 4 changed files with 199 additions and 101 deletions.
31 changes: 29 additions & 2 deletions src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ use url::Url;

use self::subcommand::NixInstallerSubcommand;

const FAIL_PKG_SUGGEST: &str = "\
The Determinate Nix Installer failed.
Try our macOS-native package instead, which can handle almost anything: https://dtr.mn/determinate-nix\
";

#[async_trait::async_trait]
pub trait CommandExecute {
async fn execute<T>(self, feedback: T) -> eyre::Result<ExitCode>
Expand Down Expand Up @@ -80,14 +86,35 @@ pub struct NixInstallerCli {
#[async_trait::async_trait]
impl CommandExecute for NixInstallerCli {
#[tracing::instrument(level = "trace", skip_all)]
async fn execute<T>(self, feedback: T) -> eyre::Result<ExitCode>
async fn execute<T>(self, mut feedback: T) -> eyre::Result<ExitCode>
where
T: crate::feedback::Feedback,
{
match self.subcommand {
NixInstallerSubcommand::Plan(plan) => plan.execute(feedback).await,
NixInstallerSubcommand::SelfTest(self_test) => self_test.execute(feedback).await,
NixInstallerSubcommand::Install(install) => install.execute(feedback).await,
NixInstallerSubcommand::Install(install) => {
let ret = install.execute(feedback.clone()).await;

if matches!(
target_lexicon::OperatingSystem::host(),
target_lexicon::OperatingSystem::MacOSX { .. }
| target_lexicon::OperatingSystem::Darwin
) {
#[allow(clippy::collapsible_if)]
if ret.is_err() || ret.as_ref().is_ok_and(|code| code == &ExitCode::FAILURE) {
let msg = feedback
.get_feature_ptr_payload::<String>("dni-det-msg-fail-pkg-ptr")
.await
.unwrap_or(FAIL_PKG_SUGGEST.into());
tracing::warn!("{}\n", msg.trim());

return Ok(ExitCode::FAILURE);
}
}

ret
},
NixInstallerSubcommand::Repair(repair) => repair.execute(feedback).await,
NixInstallerSubcommand::Uninstall(revert) => revert.execute(feedback).await,
NixInstallerSubcommand::SplitReceipt(split_receipt) => {
Expand Down
246 changes: 149 additions & 97 deletions src/cli/subcommand/install.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
use std::{
io::IsTerminal as _,
os::unix::prelude::PermissionsExt,
path::{Path, PathBuf},
process::ExitCode,
};

use crate::{
action::ActionState,
cli::{
ensure_root,
interaction::{self, PromptChoice},
Expand All @@ -15,7 +15,6 @@ use crate::{
},
error::HasExpectedErrors,
plan::RECEIPT_LOCATION,
planner::Planner,
settings::CommonSettings,
util::OnMissing,
BuiltinPlanner, InstallPlan, NixInstallerError,
Expand All @@ -33,6 +32,15 @@ const EXISTING_INCOMPATIBLE_PLAN_GUIDANCE: &str = "\
If you are using `nix-installer` in an automated curing process and seeing this message, consider pinning the version you use via https://github.com/DeterminateSystems/nix-installer#accessing-other-versions.\
";

const PRE_PKG_SUGGEST: &str = "For a more robust Nix installation, use the Determinate package for macOS: https://dtr.mn/determinate-nix";

const DETERMINATE_MSG_EXPLAINER: &str = "\
Determinate Nix is Determinate Systems' validated and secure downstream Nix distribution for enterprises. \
It comes bundled with Determinate Nixd, a helpful daemon that automates some otherwise-unpleasant aspects of using Nix, such as garbage collection, and enables you to easily authenticate with FlakeHub.
For more details: https://dtr.mn/determinate-nix\
";

/**
Install Nix using a planner
Expand Down Expand Up @@ -77,14 +85,14 @@ pub struct Install {
#[async_trait::async_trait]
impl CommandExecute for Install {
#[tracing::instrument(level = "trace", skip_all)]
async fn execute<T>(self, feedback: T) -> eyre::Result<ExitCode>
async fn execute<T>(self, mut feedback: T) -> eyre::Result<ExitCode>
where
T: crate::feedback::Feedback,
{
let Self {
no_confirm,
plan,
planner,
planner: maybe_planner,
settings,
explain,
} = self;
Expand All @@ -111,107 +119,147 @@ impl CommandExecute for Install {
false => format!("curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix/tag/v{} | sh -s -- uninstall", env!("CARGO_PKG_VERSION")),
};

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 let Err(e) = existing_receipt.check_compatible() {
eprintln!(
"{}",
format!("\
{e}\n\
\n\
Found existing plan in `{RECEIPT_LOCATION}` which was created by a version incompatible `nix-installer`.\n\
{EXISTING_INCOMPATIBLE_PLAN_GUIDANCE}\n\
").red()
);
return Ok(ExitCode::FAILURE)
}
if existing_receipt.planner.typetag_name() != chosen_planner.typetag_name() {
eprintln!("{}", format!("Found existing plan in `{RECEIPT_LOCATION}` which used a different planner, try uninstalling the existing install with `{uninstall_command}`").red());
return Ok(ExitCode::FAILURE)
}
if existing_receipt.planner.settings().map_err(|e| eyre!(e))? != chosen_planner.settings().map_err(|e| eyre!(e))? {
eprintln!("{}", format!("Found existing plan in `{RECEIPT_LOCATION}` which used different planner settings, try uninstalling the existing install with `{uninstall_command}`").red());
return Ok(ExitCode::FAILURE)
}
eprintln!("{}", format!("Found existing plan in `{RECEIPT_LOCATION}`, with the same settings, already completed. Try uninstalling (`{uninstall_command}`) and reinstalling if Nix isn't working").red());
return Ok(ExitCode::SUCCESS)
},
None => {
let res = planner.plan().await;
match res {
Ok(plan) => plan,
Err(err) => {
if let Some(expected) = err.expected() {
eprintln!("{}", expected.red());
return Ok(ExitCode::FAILURE);
}
return Err(err)?;
}
}
},
}
},
(None, Some(plan_path)) => {
let install_plan_string = tokio::fs::read_to_string(&plan_path)
if plan.is_some() && maybe_planner.is_some() {
return Err(eyre!("`--plan` conflicts with passing a planner, a planner creates plans, so passing an existing plan doesn't make sense"));
}

if matches!(
target_lexicon::OperatingSystem::host(),
target_lexicon::OperatingSystem::MacOSX { .. }
| target_lexicon::OperatingSystem::Darwin
) {
let msg = feedback
.get_feature_ptr_payload::<String>("dni-det-msg-start-pkg-ptr")
.await
.unwrap_or(PRE_PKG_SUGGEST.into());
tracing::info!("{}", msg.trim());
}

let mut post_install_message = None;

let mut install_plan = if let Some(plan_path) = plan {
let install_plan_string = tokio::fs::read_to_string(&plan_path)
.await
.wrap_err("Reading plan")?;
serde_json::from_str(&install_plan_string)?
},
(None, None) => {
let builtin_planner = BuiltinPlanner::from_common_settings(settings.clone())
serde_json::from_str(&install_plan_string)?
} else {
let mut planner = match maybe_planner {
Some(planner) => planner,
None => BuiltinPlanner::from_common_settings(settings.clone())
.await
.map_err(|e| eyre::eyre!(e))?;

match existing_receipt {
Some(existing_receipt) => {
if let Err(e) = existing_receipt.check_compatible() {
eprintln!(
"{}",
format!("\
{e}\n\
\n\
Found existing plan in `{RECEIPT_LOCATION}` which was created by a version incompatible `nix-installer`.\n\
{EXISTING_INCOMPATIBLE_PLAN_GUIDANCE}\n\
").red()
);
return Ok(ExitCode::FAILURE)
}
if existing_receipt.planner.typetag_name() != builtin_planner.typetag_name() {
eprintln!("{}", format!("Found existing plan in `{RECEIPT_LOCATION}` which used a different planner, try uninstalling the existing install with `{uninstall_command}`").red());
return Ok(ExitCode::FAILURE)
}
if existing_receipt.planner.settings().map_err(|e| eyre!(e))? != builtin_planner.settings().map_err(|e| eyre!(e))? {
eprintln!("{}", format!("Found existing plan in `{RECEIPT_LOCATION}` which used different planner settings, try uninstalling the existing install with `{uninstall_command}`").red());
return Ok(ExitCode::FAILURE)
}
if existing_receipt.actions.iter().all(|v| v.state == ActionState::Completed) {
eprintln!("{}", format!("Found existing plan in `{RECEIPT_LOCATION}`, with the same settings, already completed. Try uninstalling (`{uninstall_command}`) and reinstalling if Nix isn't working").yellow());
return Ok(ExitCode::SUCCESS)
}
existing_receipt
},
None => {
let res = builtin_planner.plan().await;
match res {
Ok(plan) => plan,
Err(err) => {
if let Some(expected) = err.expected() {
eprintln!("{}", expected.red());
return Ok(ExitCode::FAILURE);
.map_err(|e| eyre::eyre!(e))?,
};

match existing_receipt {
Some(existing_receipt) => {
if let Err(e) = existing_receipt.check_compatible() {
eprintln!(
"{}",
format!("\
{e}\n\
\n\
Found existing plan in `{RECEIPT_LOCATION}` which was created by a version incompatible `nix-installer`.\n\
{EXISTING_INCOMPATIBLE_PLAN_GUIDANCE}\n\
").red()
);
return Ok(ExitCode::FAILURE);
}

if existing_receipt.planner.typetag_name() != planner.typetag_name() {
eprintln!("{}", format!("Found existing plan in `{RECEIPT_LOCATION}` which used a different planner, try uninstalling the existing install with `{uninstall_command}`").red());
return Ok(ExitCode::FAILURE);
}

if existing_receipt.planner.settings().map_err(|e| eyre!(e))?
!= planner.settings().map_err(|e| eyre!(e))?
{
eprintln!("{}", format!("Found existing plan in `{RECEIPT_LOCATION}` which used different planner settings, try uninstalling the existing install with `{uninstall_command}`").red());
return Ok(ExitCode::FAILURE);
}

eprintln!("{}", format!("Found existing plan in `{RECEIPT_LOCATION}`, with the same settings, already completed. Try uninstalling (`{uninstall_command}`) and reinstalling if Nix isn't working").red());
return Ok(ExitCode::SUCCESS);
},
None => {
let planner_settings = planner.common_settings_mut();

if !planner_settings.determinate_nix {
if !std::io::stdin().is_terminal() || no_confirm {
let msg = feedback
.get_feature_ptr_payload::<String>("dni-det-msg-noninteractive-ptr")
.await
.unwrap_or("Consider using Determinate Nix, for less fuss: https://dtr.mn/determinate-nix\n".into());
post_install_message = Some(msg);
} else {
let base_prompt = feedback
.get_feature_ptr_payload::<String>(
"dni-det-msg-interactive-prompt-ptr",
)
.await
.unwrap_or("Install Determinate Nix?".into());
let explanation = feedback
.get_feature_ptr_payload::<String>(
"dni-det-msg-interactive-explanation-ptr",
)
.await
.unwrap_or(DETERMINATE_MSG_EXPLAINER.into());

let mut currently_explaining = explain;

loop {
let prompt = if currently_explaining {
&format!(
"\n{}\n{}\n",
base_prompt.trim().green(),
explanation.trim()
)
} else {
&format!("\n{}", base_prompt.trim().green())
};

let response = interaction::prompt(
prompt.to_string(),
PromptChoice::Yes,
currently_explaining,
)
.await?;

match response {
PromptChoice::Explain => {
currently_explaining = true;
},
PromptChoice::Yes => {
planner_settings.determinate_nix = true;
break;
},
PromptChoice::No => {
break;
},
}
return Err(err)?;
}
}
},
}
},
(Some(_), Some(_)) => return Err(eyre!("`--plan` conflicts with passing a planner, a planner creates plans, so passing an existing plan doesn't make sense")),
}

feedback.set_planner(&planner).await?;

let res = planner.plan().await;
match res {
Ok(plan) => plan,
Err(err) => {
feedback.planning_failed(&err).await;
if let Some(expected) = err.expected() {
eprintln!("{}", expected.red());
return Ok(ExitCode::FAILURE);
}
return Err(err)?;
},
}
},
}
};

feedback.planning_succeeded().await;

if let Err(err) = install_plan.pre_install_check().await {
if let Some(expected) = err.expected() {
eprintln!("{}", expected.red());
Expand Down Expand Up @@ -358,6 +406,10 @@ impl CommandExecute for Install {
". /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh".bold(),
},
);

if let Some(post_message) = post_install_message {
println!("{}", post_message.trim());
}
},
}

Expand Down
14 changes: 12 additions & 2 deletions src/diagnostics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -221,11 +221,21 @@ impl crate::feedback::Feedback for DiagnosticData {
.add_fact("planner", planner.typetag_name().into())
.await;

if let Ok(settings) = planner.configured_settings().await {
if let Ok(ref settings) = planner.configured_settings().await {
self.ids_client
.add_fact(
"configured_settings",
settings.into_keys().collect::<Vec<_>>().into(),
settings.keys().cloned().collect::<Vec<_>>().into(),
)
.await;

self.ids_client
.add_fact(
"install_determinate_nix",
settings
.get("determinate_nix")
.cloned()
.unwrap_or(serde_json::Value::Bool(false)),
)
.await;
}
Expand Down
9 changes: 9 additions & 0 deletions src/planner/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,15 @@ impl BuiltinPlanner {
Ok(built)
}

pub fn common_settings_mut(&mut self) -> &mut CommonSettings {
match self {
BuiltinPlanner::Linux(inner) => &mut inner.settings,
BuiltinPlanner::SteamDeck(inner) => &mut inner.settings,
BuiltinPlanner::Ostree(inner) => &mut inner.settings,
BuiltinPlanner::Macos(inner) => &mut inner.settings,
}
}

pub async fn configured_settings(
&self,
) -> Result<HashMap<String, serde_json::Value>, PlannerError> {
Expand Down

0 comments on commit 2542680

Please sign in to comment.