diff --git a/src/action/base/create_user.rs b/src/action/base/create_user.rs index a1637ad..791aa05 100644 --- a/src/action/base/create_user.rs +++ b/src/action/base/create_user.rs @@ -304,7 +304,7 @@ impl Action for CreateUser { Some(40) if stderr.contains("-14120") => { // The user is on an ephemeral Mac, like detsys uses // These Macs cannot always delete users, as sometimes there is no graphical login - tracing::warn!("Encountered an exit code 40 with -14120 error while removing user, this is likely because the initial executing user did not have a secure token, or that there was no graphical login session. To delete the user, log in graphically, then run `/usr/bin/dscl . -delete /Users/{}", self.name); + tracing::warn!("Encountered an exit code 40 with -14120 error while removing user, this is likely because the initial executing user did not have a secure token, or that there was no graphical login session. To delete the user, log in graphically, then run `/usr/bin/dscl . -delete /Users/{}`", self.name); }, _ => { // Something went wrong diff --git a/src/action/base/delete_user.rs b/src/action/base/delete_user.rs index 4994d77..01d4ed6 100644 --- a/src/action/base/delete_user.rs +++ b/src/action/base/delete_user.rs @@ -100,7 +100,7 @@ impl Action for DeleteUser { Some(40) if stderr.contains("-14120") => { // The user is on an ephemeral Mac, like detsys uses // These Macs cannot always delete users, as sometimes there is no graphical login - tracing::warn!("Encountered an exit code 40 with -14120 error while removing user, this is likely because the initial executing user did not have a secure token, or that there was no graphical login session. To delete the user, log in graphically, then run `/usr/bin/dscl . -delete /Users/{}", self.name); + tracing::warn!("Encountered an exit code 40 with -14120 error while removing user, this is likely because the initial executing user did not have a secure token, or that there was no graphical login session. To delete the user, log in graphically, then run `/usr/bin/dscl . -delete /Users/{}`", self.name); }, _ => { // Something went wrong diff --git a/src/cli/subcommand/install.rs b/src/cli/subcommand/install.rs index 18d04ef..59e2e3f 100644 --- a/src/cli/subcommand/install.rs +++ b/src/cli/subcommand/install.rs @@ -199,6 +199,14 @@ impl CommandExecute for Install { (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 let Err(err) = install_plan.pre_install_check().await { + if let Some(expected) = err.expected() { + eprintln!("{}", expected.red()); + return Ok(ExitCode::FAILURE); + } + Err(err)? + } + if !no_confirm { let mut currently_explaining = explain; loop { diff --git a/src/cli/subcommand/uninstall.rs b/src/cli/subcommand/uninstall.rs index 1ff2e1b..d10367e 100644 --- a/src/cli/subcommand/uninstall.rs +++ b/src/cli/subcommand/uninstall.rs @@ -113,6 +113,14 @@ impl CommandExecute for Uninstall { .wrap_err("Reading receipt")?; let mut plan: InstallPlan = serde_json::from_str(&install_receipt_string)?; + if let Err(err) = plan.pre_uninstall_check().await { + if let Some(expected) = err.expected() { + eprintln!("{}", expected.red()); + return Ok(ExitCode::FAILURE); + } + Err(err)? + } + if !no_confirm { let mut currently_explaining = explain; loop { diff --git a/src/plan.rs b/src/plan.rs index 2140573..14e3d07 100644 --- a/src/plan.rs +++ b/src/plan.rs @@ -53,6 +53,9 @@ impl InstallPlan { #[cfg(feature = "diagnostics")] let diagnostic_data = Some(planner.diagnostic_data().await?); + // Some Action `plan` calls may fail if we don't do these checks + planner.pre_install_check().await?; + let actions = planner.plan().await?; Ok(Self { planner: planner.boxed(), @@ -62,6 +65,17 @@ impl InstallPlan { diagnostic_data, }) } + + pub async fn pre_uninstall_check(&self) -> Result<(), NixInstallerError> { + self.planner.pre_uninstall_check().await?; + Ok(()) + } + + pub async fn pre_install_check(&self) -> Result<(), NixInstallerError> { + self.planner.pre_install_check().await?; + Ok(()) + } + #[tracing::instrument(level = "debug", skip_all)] pub async fn describe_install(&self, explain: bool) -> Result { let Self { @@ -143,6 +157,8 @@ impl InstallPlan { cancel_channel: impl Into>>, ) -> Result<(), NixInstallerError> { self.check_compatible()?; + self.planner.pre_install_check().await?; + let Self { actions, .. } = self; let mut cancel_channel = cancel_channel.into(); @@ -313,6 +329,8 @@ impl InstallPlan { cancel_channel: impl Into>>, ) -> Result<(), NixInstallerError> { self.check_compatible()?; + self.planner.pre_uninstall_check().await?; + let Self { actions, .. } = self; let mut cancel_channel = cancel_channel.into(); let mut errors = vec![]; diff --git a/src/planner/linux.rs b/src/planner/linux.rs index b394a1a..85b8256 100644 --- a/src/planner/linux.rs +++ b/src/planner/linux.rs @@ -38,18 +38,8 @@ impl Planner for Linux { } async fn plan(&self) -> Result>>, PlannerError> { - check_not_nixos()?; - - check_nix_not_already_installed().await?; - - check_not_wsl1()?; - let has_selinux = detect_selinux().await?; - if self.init.init == InitSystem::Systemd && self.init.start_daemon { - check_systemd_active()?; - } - let mut plan = vec![]; plan.push( @@ -141,6 +131,29 @@ impl Planner for Linux { self.settings.ssl_cert_file.clone(), )?) } + async fn pre_uninstall_check(&self) -> Result<(), PlannerError> { + check_not_wsl1()?; + + if self.init.init == InitSystem::Systemd && self.init.start_daemon { + check_systemd_active()?; + } + + Ok(()) + } + + async fn pre_install_check(&self) -> Result<(), PlannerError> { + check_not_nixos()?; + + check_nix_not_already_installed().await?; + + check_not_wsl1()?; + + if self.init.init == InitSystem::Systemd && self.init.start_daemon { + check_systemd_active()?; + } + + Ok(()) + } } impl Into for Linux { @@ -150,7 +163,7 @@ impl Into for Linux { } // If on NixOS, running `nix_installer` is pointless -fn check_not_nixos() -> Result<(), PlannerError> { +pub(crate) fn check_not_nixos() -> Result<(), PlannerError> { // NixOS always sets up this file as part of setting up /etc itself: https://github.com/NixOS/nixpkgs/blob/bdd39e5757d858bd6ea58ed65b4a2e52c8ed11ca/nixos/modules/system/etc/setup-etc.pl#L145 if Path::new("/etc/NIXOS").exists() { return Err(PlannerError::NixOs); @@ -158,7 +171,7 @@ fn check_not_nixos() -> Result<(), PlannerError> { Ok(()) } -fn check_not_wsl1() -> Result<(), PlannerError> { +pub(crate) fn check_not_wsl1() -> Result<(), PlannerError> { // Detection strategies: https://patrickwu.space/wslconf/ if std::env::var("WSL_DISTRO_NAME").is_ok() && std::env::var("WSL_INTEROP").is_err() { return Err(PlannerError::Wsl1); @@ -166,7 +179,7 @@ fn check_not_wsl1() -> Result<(), PlannerError> { Ok(()) } -async fn detect_selinux() -> Result { +pub(crate) async fn detect_selinux() -> Result { if Path::new("/sys/fs/selinux").exists() && which("sestatus").is_ok() { // We expect systems with SELinux to have the normal SELinux tools. let has_semodule = which("semodule").is_ok(); @@ -181,7 +194,7 @@ async fn detect_selinux() -> Result { } } -async fn check_nix_not_already_installed() -> Result<(), PlannerError> { +pub(crate) async fn check_nix_not_already_installed() -> Result<(), PlannerError> { // For now, we don't try to repair the user's Nix install or anything special. if let Ok(_) = Command::new("nix-env") .arg("--version") @@ -195,7 +208,7 @@ async fn check_nix_not_already_installed() -> Result<(), PlannerError> { Ok(()) } -fn check_systemd_active() -> Result<(), PlannerError> { +pub(crate) fn check_systemd_active() -> Result<(), PlannerError> { if !Path::new("/run/systemd/system").exists() { if std::env::var("WSL_DISTRO_NAME").is_ok() { return Err(LinuxErrorKind::Wsl2SystemdNotActive)?; diff --git a/src/planner/macos.rs b/src/planner/macos.rs index d2c89b0..048dd5a 100644 --- a/src/planner/macos.rs +++ b/src/planner/macos.rs @@ -3,8 +3,10 @@ use std::{collections::HashMap, io::Cursor, path::PathBuf}; #[cfg(feature = "cli")] use clap::ArgAction; use tokio::process::Command; +use which::which; use super::ShellProfileLocations; +use crate::planner::HasExpectedErrors; use crate::{ action::{ @@ -89,8 +91,6 @@ impl Planner for Macos { } async fn plan(&self) -> Result>>, PlannerError> { - ensure_not_running_in_rosetta().await?; - let root_disk = match &self.root_disk { root_disk @ Some(_) => root_disk.clone(), None => { @@ -219,6 +219,18 @@ impl Planner for Macos { self.settings.ssl_cert_file.clone(), )?) } + + async fn pre_uninstall_check(&self) -> Result<(), PlannerError> { + check_nix_darwin_not_installed().await?; + + Ok(()) + } + + async fn pre_install_check(&self) -> Result<(), PlannerError> { + check_not_running_in_rosetta()?; + + Ok(()) + } } impl Into for Macos { @@ -227,7 +239,30 @@ impl Into for Macos { } } -async fn ensure_not_running_in_rosetta() -> Result<(), PlannerError> { +async fn check_nix_darwin_not_installed() -> Result<(), PlannerError> { + let has_darwin_rebuild = which("darwin-rebuild").is_ok(); + let has_darwin_option = which("darwin-option").is_ok(); + + let activate_system_present = Command::new("launchctl") + .arg("print") + .arg("system/org.nixos.activate-system") + .process_group(0) + .stdin(std::process::Stdio::null()) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .status() + .await + .map(|v| v.success()) + .unwrap_or(false); + + if activate_system_present || has_darwin_rebuild || has_darwin_option { + return Err(MacosError::UninstallNixDarwin).map_err(|e| PlannerError::Custom(Box::new(e))); + }; + + Ok(()) +} + +fn check_not_running_in_rosetta() -> Result<(), PlannerError> { use sysctl::{Ctl, Sysctl}; const CTLNAME: &str = "sysctl.proc_translated"; @@ -246,3 +281,18 @@ async fn ensure_not_running_in_rosetta() -> Result<(), PlannerError> { Ok(()) } + +#[non_exhaustive] +#[derive(thiserror::Error, Debug)] +pub enum MacosError { + #[error("`nix-darwin` installation detected, it must be removed before uninstalling Nix. Please refer to https://github.com/LnL7/nix-darwin#uninstalling for instructions how to uninstall `nix-darwin`.")] + UninstallNixDarwin, +} + +impl HasExpectedErrors for MacosError { + fn expected<'a>(&'a self) -> Option> { + match self { + this @ MacosError::UninstallNixDarwin => Some(Box::new(this)), + } + } +} diff --git a/src/planner/mod.rs b/src/planner/mod.rs index bc6dc1c..c91bba9 100644 --- a/src/planner/mod.rs +++ b/src/planner/mod.rs @@ -144,6 +144,14 @@ pub trait Planner: std::fmt::Debug + Send + Sync + dyn_clone::DynClone { Box::new(self) } + async fn pre_uninstall_check(&self) -> Result<(), PlannerError> { + Ok(()) + } + + async fn pre_install_check(&self) -> Result<(), PlannerError> { + Ok(()) + } + #[cfg(feature = "diagnostics")] async fn diagnostic_data(&self) -> Result; } @@ -415,6 +423,10 @@ impl HasExpectedErrors for PlannerError { if let Some(err) = _e.downcast_ref::() { return err.expected(); } + #[cfg(target_os = "macos")] + if let Some(err) = _e.downcast_ref::() { + return err.expected(); + } None }, this @ PlannerError::NixOs => Some(Box::new(this)), diff --git a/src/planner/steam_deck.rs b/src/planner/steam_deck.rs index 38f7426..cc69b4a 100644 --- a/src/planner/steam_deck.rs +++ b/src/planner/steam_deck.rs @@ -398,6 +398,28 @@ impl Planner for SteamDeck { self.settings.ssl_cert_file.clone(), )?) } + + async fn pre_uninstall_check(&self) -> Result<(), PlannerError> { + super::linux::check_not_wsl1()?; + + // Unlike the Linux planner, the steam deck planner requires systemd + super::linux::check_systemd_active()?; + + Ok(()) + } + + async fn pre_install_check(&self) -> Result<(), PlannerError> { + super::linux::check_not_nixos()?; + + super::linux::check_nix_not_already_installed().await?; + + super::linux::check_not_wsl1()?; + + // Unlike the Linux planner, the steam deck planner requires systemd + super::linux::check_systemd_active()?; + + Ok(()) + } } impl Into for SteamDeck {