diff --git a/src/cli/subcommand/install.rs b/src/cli/subcommand/install.rs index e80e1cb..8133cab 100644 --- a/src/cli/subcommand/install.rs +++ b/src/cli/subcommand/install.rs @@ -24,6 +24,12 @@ use color_eyre::{ }; use owo_colors::OwoColorize; +const EXISTING_INCOMPATIBLE_PLAN_GUIDANCE: &'static str = "\ + If you are trying to upgrade Nix, try running `sudo -i nix upgrade-nix` instead.\n\ + If you are trying to install Nix over an existing install (from an incompatible `nix-installer` install), try running `/nix/nix-installer uninstall` then try to install again.\n\ + 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.\ +"; + /// Execute an install (possibly using an existing plan) /// /// To pass custom options, select a planner, for example `nix-installer install linux-multi --help` @@ -78,7 +84,11 @@ impl CommandExecute for Install { let install_plan_string = tokio::fs::read_to_string(&RECEIPT_LOCATION) .await .wrap_err("Reading plan")?; - Some(serde_json::from_str(&install_plan_string)?) + Some( + serde_json::from_str(&install_plan_string).wrap_err_with(|| { + format!("Unable to parse existing receipt `{RECEIPT_LOCATION}`, it may be from an incompatible version of `nix-installer`. Try running `/nix/nix-installer uninstall`, then installing again.") + })?, + ) }, false => None, }; @@ -94,6 +104,18 @@ impl CommandExecute for Install { 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) @@ -104,7 +126,7 @@ impl CommandExecute for Install { } 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::FAILURE) - } , + }, None => { let res = planner.plan().await; match res { @@ -133,6 +155,18 @@ impl CommandExecute for Install { 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) diff --git a/src/error.rs b/src/error.rs index 7a926a2..30c714f 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,5 +1,7 @@ use std::{error::Error, path::PathBuf}; +use semver::Version; + use crate::{action::ActionError, planner::PlannerError, settings::InstallSettingsError}; /// An error occurring during a call defined in this crate @@ -68,6 +70,15 @@ pub enum NixInstallerError { #[source] crate::diagnostics::DiagnosticError, ), + /// Could not parse the value as a version requirement in order to ensure it's compatible + #[error("Could not parse `{0}` as a version requirement in order to ensure it's compatible")] + InvalidVersionRequirement(String, semver::Error), + /// Could not parse `nix-installer`'s version as a valid version according to Semantic Versioning, therefore the plan version compatibility cannot be checked + #[error("Could not parse `nix-installer`'s version `{0}` as a valid version according to Semantic Versioning, therefore the plan version compatibility cannot be checked")] + InvalidCurrentVersion(String, semver::Error), + /// This version of `nix-installer` is not compatible with this plan's version + #[error("`nix-installer` version `{}` is not compatible with this plan's version `{}`", .binary, .plan)] + IncompatibleVersion { binary: Version, plan: Version }, } pub(crate) trait HasExpectedErrors: std::error::Error + Sized + Send + Sync { @@ -86,6 +97,11 @@ impl HasExpectedErrors for NixInstallerError { NixInstallerError::SemVer(_) => None, NixInstallerError::Planner(planner_error) => planner_error.expected(), NixInstallerError::InstallSettings(_) => None, + this @ NixInstallerError::InvalidVersionRequirement(_, _) => Some(Box::new(this)), + this @ NixInstallerError::InvalidCurrentVersion(_, _) => Some(Box::new(this)), + this @ NixInstallerError::IncompatibleVersion { binary: _, plan: _ } => { + Some(Box::new(this)) + }, #[cfg(feature = "diagnostics")] NixInstallerError::Diagnostic(_) => None, } diff --git a/src/plan.rs b/src/plan.rs index 26a67c4..5cef0f3 100644 --- a/src/plan.rs +++ b/src/plan.rs @@ -7,7 +7,6 @@ use crate::{ }; use owo_colors::OwoColorize; use semver::{Version, VersionReq}; -use serde::{de::Error, Deserialize, Deserializer}; use tokio::sync::broadcast::Receiver; pub const RECEIPT_LOCATION: &str = "/nix/receipt.json"; @@ -18,7 +17,6 @@ revert */ #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] pub struct InstallPlan { - #[serde(deserialize_with = "ensure_version")] pub(crate) version: Version, pub(crate) actions: Vec>>, @@ -144,6 +142,7 @@ impl InstallPlan { &mut self, cancel_channel: impl Into>>, ) -> Result<(), NixInstallerError> { + self.check_compatible()?; let Self { actions, .. } = self; let mut cancel_channel = cancel_channel.into(); @@ -293,6 +292,7 @@ impl InstallPlan { &mut self, cancel_channel: impl Into>>, ) -> Result<(), NixInstallerError> { + self.check_compatible()?; let Self { actions, .. } = self; let mut cancel_channel = cancel_channel.into(); let mut errors = vec![]; @@ -359,6 +359,21 @@ impl InstallPlan { return Err(error); } } + + pub fn check_compatible(&self) -> Result<(), NixInstallerError> { + let self_version_string = self.version.to_string(); + let req = VersionReq::parse(&self_version_string) + .map_err(|e| NixInstallerError::InvalidVersionRequirement(self_version_string, e))?; + let nix_installer_version = current_version()?; + if req.matches(&nix_installer_version) { + Ok(()) + } else { + Err(NixInstallerError::IncompatibleVersion { + binary: nix_installer_version, + plan: self.version.clone(), + }) + } + } } async fn write_receipt(plan: InstallPlan) -> Result<(), NixInstallerError> { @@ -374,30 +389,11 @@ async fn write_receipt(plan: InstallPlan) -> Result<(), NixInstallerError> { Result::<(), NixInstallerError>::Ok(()) } -fn current_version() -> Result { +fn current_version() -> Result { let nix_installer_version_str = env!("CARGO_PKG_VERSION"); - Version::from_str(nix_installer_version_str) -} - -fn ensure_version<'de, D: Deserializer<'de>>(d: D) -> Result { - let plan_version = Version::deserialize(d)?; - let req = VersionReq::parse(&plan_version.to_string()).map_err(|_e| { - D::Error::custom(&format!( - "Could not parse version `{plan_version}` as a version requirement, please report this", - )) - })?; - let nix_installer_version = current_version().map_err(|_e| { - D::Error::custom(&format!( - "Could not parse `nix-installer`'s version `{}` as a valid version according to Semantic Versioning, therefore the plan version ({plan_version}) compatibility cannot be checked", env!("CARGO_PKG_VERSION") - )) - })?; - if req.matches(&nix_installer_version) { - Ok(plan_version) - } else { - Err(D::Error::custom(&format!( - "This version of `nix-installer` ({nix_installer_version}) is not compatible with this plan's version ({plan_version}), you probably are trying to install with a new version of `nix-installer` which is not compatible with version {plan_version} plans. To upgrade Nix, try `sudo -i nix upgrade-nix`. To reinstall Nix, try `/nix/nix-installer uninstall` then installing again from the instructions on https://github.com/DeterminateSystems/nix-installer. To continue using this plan, download the matching release from https://github.com/DeterminateSystems/nix-installer/releases.", - ))) - } + Version::from_str(nix_installer_version_str).map_err(|e| { + NixInstallerError::InvalidCurrentVersion(nix_installer_version_str.to_string(), e) + }) } #[cfg(test)] @@ -415,8 +411,8 @@ mod test { "version": good_version, "actions": [], }); - let maybe_plan: Result = serde_json::from_value(value); - maybe_plan.unwrap(); + let maybe_plan: InstallPlan = serde_json::from_value(value)?; + maybe_plan.check_compatible()?; Ok(()) } @@ -429,10 +425,8 @@ mod test { "version": bad_version, "actions": [], }); - let maybe_plan: Result = serde_json::from_value(value); - assert!(maybe_plan.is_err()); - let err = maybe_plan.unwrap_err(); - assert!(err.is_data()); + let maybe_plan: InstallPlan = serde_json::from_value(value)?; + assert!(maybe_plan.check_compatible().is_err()); Ok(()) } }