diff --git a/crates/nix_rs/src/config.rs b/crates/nix_rs/src/config.rs index 7f1f1784..d0cadebf 100644 --- a/crates/nix_rs/src/config.rs +++ b/crates/nix_rs/src/config.rs @@ -10,7 +10,7 @@ use url::Url; use crate::{ command::{NixCmd, NixCmdError}, - version::NixVersion, + version::{NixVersion, VersionSpec}, }; use super::flake::system::System; @@ -51,11 +51,11 @@ pub struct ConfigVal { static NIX_CONFIG: OnceCell> = OnceCell::const_new(); -static NIX_2_20_0: NixVersion = NixVersion { +static NIX_2_20_0: NixVersion = NixVersion::Official(VersionSpec { major: 2, minor: 20, patch: 0, -}; +}); impl NixConfig { /// Get the once version of `NixConfig`. diff --git a/crates/nix_rs/src/info.rs b/crates/nix_rs/src/info.rs index 2ecefab0..63bb4c7c 100644 --- a/crates/nix_rs/src/info.rs +++ b/crates/nix_rs/src/info.rs @@ -2,7 +2,11 @@ use serde::{Deserialize, Serialize}; use tokio::sync::OnceCell; -use crate::{config::NixConfig, env::NixEnv, version::NixVersion}; +use crate::{ + config::NixConfig, + env::NixEnv, + version::{NixInstallationType, NixVersion}, +}; /// All the information about the user's Nix installation #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -15,6 +19,13 @@ pub struct NixInfo { pub nix_env: NixEnv, } +impl NixInfo { + /// Get the installation type (derived from nix_version) + pub fn installation_type(&self) -> NixInstallationType { + self.nix_version.installation_type() + } +} + static NIX_INFO: OnceCell> = OnceCell::const_new(); impl NixInfo { diff --git a/crates/nix_rs/src/version.rs b/crates/nix_rs/src/version.rs index b0e74ac8..9cd6be2a 100644 --- a/crates/nix_rs/src/version.rs +++ b/crates/nix_rs/src/version.rs @@ -1,7 +1,7 @@ //! Rust module for `nix --version` use regex::Regex; use serde_with::{DeserializeFromStr, SerializeDisplay}; -use std::{fmt, str::FromStr}; +use std::{fmt, str::FromStr, sync::LazyLock}; use thiserror::Error; use tokio::sync::OnceCell; @@ -9,9 +9,9 @@ use tracing::instrument; use crate::command::{NixCmd, NixCmdError}; -/// Nix version as parsed from `nix --version` -#[derive(Clone, Copy, PartialOrd, PartialEq, Eq, Debug, SerializeDisplay, DeserializeFromStr)] -pub struct NixVersion { +/// Simple version triple (major.minor.patch) +#[derive(Clone, Copy, PartialOrd, PartialEq, Eq, Ord, Debug)] +pub struct VersionSpec { /// Major version pub major: u32, /// Minor version @@ -20,6 +20,82 @@ pub struct NixVersion { pub patch: u32, } +/// Nix version as parsed from `nix --version`, capturing both version and installation type +#[derive(Clone, Copy, Debug, SerializeDisplay, DeserializeFromStr)] +pub enum NixVersion { + /// Official Nix installation: "nix (Nix) 2.28.4" or "2.28.4" + Official(VersionSpec), + /// Determinate Systems Nix: "nix (Determinate Nix 3.8.5) 2.30.2" + DeterminateSystems { + /// The Determinate Systems version (e.g., 3.8.5) + det_sys_version: VersionSpec, + /// The underlying Nix version (e.g., 2.30.2) + nix_version: VersionSpec, + }, +} + +impl NixVersion { + /// Get the effective Nix version (the actual Nix version being used) + pub fn nix_version(&self) -> VersionSpec { + match self { + NixVersion::Official(version) => *version, + NixVersion::DeterminateSystems { nix_version, .. } => *nix_version, + } + } + + /// Check if this is a Determinate Systems installation + pub fn is_determinate_systems(&self) -> bool { + matches!(self, NixVersion::DeterminateSystems { .. }) + } + + /// Get the installation type + pub fn installation_type(&self) -> NixInstallationType { + match self { + NixVersion::Official(_) => NixInstallationType::Official, + NixVersion::DeterminateSystems { .. } => NixInstallationType::DeterminateSystems, + } + } +} + +impl PartialEq for NixVersion { + fn eq(&self, other: &Self) -> bool { + self.nix_version() == other.nix_version() + } +} + +impl Eq for NixVersion {} + +#[allow(clippy::non_canonical_partial_ord_impl)] +impl PartialOrd for NixVersion { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for NixVersion { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.nix_version().cmp(&other.nix_version()) + } +} + +/// Type of Nix installation (derived from NixVersion) +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum NixInstallationType { + /// Official Nix installation + Official, + /// Determinate Systems Nix + DeterminateSystems, +} + +impl std::fmt::Display for NixInstallationType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + NixInstallationType::Official => write!(f, "official"), + NixInstallationType::DeterminateSystems => write!(f, "determinate-systems"), + } + } +} + /// Error type for parsing `nix --version` #[derive(Error, Debug, Clone, PartialEq)] pub enum BadNixVersion { @@ -36,25 +112,58 @@ pub enum BadNixVersion { Command, } +impl VersionSpec { + /// Parse a version string like "2.28.4" into a VersionSpec + fn parse_version_string(s: &str) -> Result { + let parts: Vec<&str> = s.split('.').collect(); + if parts.len() != 3 { + return Err(BadNixVersion::Command); + } + + let major = parts[0].parse::()?; + let minor = parts[1].parse::()?; + let patch = parts[2].parse::()?; + + Ok(VersionSpec { + major, + minor, + patch, + }) + } +} + +impl std::fmt::Display for VersionSpec { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}.{}.{}", self.major, self.minor, self.patch) + } +} + +static DET_SYS_REGEX: LazyLock = + LazyLock::new(|| Regex::new(r"^nix \(Determinate Nix ([\d.]+)\) ([\d.]+)$").unwrap()); +static OFFICIAL_REGEX: LazyLock = + LazyLock::new(|| Regex::new(r"^(?:nix \(Nix\) )?([\d.]+)$").unwrap()); + impl FromStr for NixVersion { type Err = BadNixVersion; /// Parse the string output of `nix --version` into a [NixVersion] fn from_str(s: &str) -> Result { - // NOTE: The parser is lenient in allowing pure nix version (produced - // by [Display] instance), so as to work with serde_with instances. - let re = Regex::new(r"(?:nix \(Nix\) )?(\d+)\.(\d+)\.(\d+)$")?; + // Try to match Determinate Systems format: "nix (Determinate Nix 3.8.5) 2.30.2" + if let Some(captures) = DET_SYS_REGEX.captures(s) { + return Ok(NixVersion::DeterminateSystems { + det_sys_version: VersionSpec::parse_version_string(&captures[1])?, + nix_version: VersionSpec::parse_version_string(&captures[2])?, + }); + } - let captures = re.captures(s).ok_or(BadNixVersion::Command)?; - let major = captures[1].parse::()?; - let minor = captures[2].parse::()?; - let patch = captures[3].parse::()?; + // Try to match official format: "nix (Nix) 2.28.4" or plain format: "2.28.4" + if let Some(captures) = OFFICIAL_REGEX.captures(s) { + return Ok(NixVersion::Official(VersionSpec::parse_version_string( + &captures[1], + )?)); + } - Ok(NixVersion { - major, - minor, - patch, - }) + Err(BadNixVersion::Command) } } @@ -84,7 +193,10 @@ impl NixVersion { /// The String view for [NixVersion] impl fmt::Display for NixVersion { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}.{}.{}", self.major, self.minor, self.patch) + match self { + NixVersion::Official(version) => write!(f, "{}", version), + NixVersion::DeterminateSystems { nix_version, .. } => write!(f, "{}", nix_version), + } } } @@ -96,32 +208,64 @@ async fn test_run_nix_version() { #[tokio::test] async fn test_parse_nix_version() { + // Test official Nix format assert_eq!( NixVersion::from_str("nix (Nix) 2.13.0"), - Ok(NixVersion { + Ok(NixVersion::Official(VersionSpec { major: 2, minor: 13, patch: 0 - }) + })) ); - // Parse simple nix version + // Test simple version format (treated as official) assert_eq!( NixVersion::from_str("2.13.0"), - Ok(NixVersion { + Ok(NixVersion::Official(VersionSpec { major: 2, minor: 13, patch: 0 - }) + })) ); - // Parse Determinate Nix Version + // Test Determinate Systems format assert_eq!( NixVersion::from_str("nix (Determinate Nix 3.6.6) 2.29.0"), - Ok(NixVersion { - major: 2, - minor: 29, - patch: 0 + Ok(NixVersion::DeterminateSystems { + det_sys_version: VersionSpec { + major: 3, + minor: 6, + patch: 6 + }, + nix_version: VersionSpec { + major: 2, + minor: 29, + patch: 0 + } }) ); + + // Test installation type detection + let official = NixVersion::from_str("nix (Nix) 2.28.4").unwrap(); + assert_eq!(official.installation_type(), NixInstallationType::Official); + assert!(!official.is_determinate_systems()); + + let det_sys = NixVersion::from_str("nix (Determinate Nix 3.8.5) 2.30.2").unwrap(); + assert_eq!( + det_sys.installation_type(), + NixInstallationType::DeterminateSystems + ); + assert!(det_sys.is_determinate_systems()); + + // Test version comparison between Official and DeterminateSystems + let official_v2_30 = NixVersion::from_str("2.30.0").unwrap(); + let det_sys_v2_30 = NixVersion::from_str("nix (Determinate Nix 3.8.5) 2.30.0").unwrap(); + let official_v2_28 = NixVersion::from_str("2.28.0").unwrap(); + + // Same underlying Nix version should be equal + assert_eq!(official_v2_30, det_sys_v2_30); + + // Both should be greater than older version + assert!(official_v2_30 > official_v2_28); + assert!(det_sys_v2_30 > official_v2_28); } diff --git a/crates/nix_rs/src/version_spec.rs b/crates/nix_rs/src/version_spec.rs index 5a15cff0..ce15a332 100644 --- a/crates/nix_rs/src/version_spec.rs +++ b/crates/nix_rs/src/version_spec.rs @@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize}; use serde_with::{DeserializeFromStr, SerializeDisplay}; use thiserror::Error; -use crate::version::NixVersion; +use crate::version::{NixVersion, VersionSpec}; /// An individual component of [NixVersionReq] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -119,11 +119,11 @@ impl FromStr for NixVersionSpec { .name("patch") .map_or(Ok(0), |m| m.as_str().parse::())?; - let nix_version = NixVersion { + let nix_version = NixVersion::Official(VersionSpec { major, minor, patch, - }; + }); match op { ">=" => Ok(Gteq(nix_version)), @@ -170,19 +170,19 @@ mod tests { fn test_parse() { assert_eq!( NixVersionSpec::from_str(">2.8").unwrap(), - NixVersionSpec::Gt(NixVersion { + NixVersionSpec::Gt(NixVersion::Official(VersionSpec { major: 2, minor: 8, patch: 0 - }) + })) ); assert_eq!( NixVersionSpec::from_str(">2").unwrap(), - NixVersionSpec::Gt(NixVersion { + NixVersionSpec::Gt(NixVersion::Official(VersionSpec { major: 2, minor: 0, patch: 0 - }) + })) ); } diff --git a/crates/omnix-health/src/check/flake_enabled.rs b/crates/omnix-health/src/check/flake_enabled.rs index 884a1730..d9c316f5 100644 --- a/crates/omnix-health/src/check/flake_enabled.rs +++ b/crates/omnix-health/src/check/flake_enabled.rs @@ -1,4 +1,4 @@ -use nix_rs::info; +use nix_rs::{info, version::NixInstallationType}; use serde::{Deserialize, Serialize}; use crate::traits::*; @@ -15,12 +15,32 @@ impl Checkable for FlakeEnabled { _: Option<&nix_rs::flake::url::FlakeUrl>, ) -> Vec<(&'static str, Check)> { let val = &nix_info.nix_config.experimental_features.value; + + // Check if flakes are enabled either through configuration or installation type + let flakes_enabled = match nix_info.installation_type() { + NixInstallationType::DeterminateSystems => { + // Determinate Systems Nix has flakes enabled by default + true + } + NixInstallationType::Official => { + // Official Nix requires explicit configuration + val.contains(&"flakes".to_string()) && val.contains(&"nix-command".to_string()) + } + }; + + let info_msg = match nix_info.installation_type() { + NixInstallationType::DeterminateSystems => { + "Determinate Systems Nix (flakes enabled by default)".to_string() + } + NixInstallationType::Official => { + format!("experimental-features = {}", val.join(" ")) + } + }; + let check = Check { title: "Flakes Enabled".to_string(), - info: format!("experimental-features = {}", val.join(" ")), - result: if val.contains(&"flakes".to_string()) - && val.contains(&"nix-command".to_string()) - { + info: info_msg, + result: if flakes_enabled { CheckResult::Green } else { CheckResult::Red {