Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions src/check.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
use std::cmp::Ordering;

use color_eyre::{eyre, Result};
use semver::Version;

use crate::util;

/// Verifies if the installed Nix version meets requirements
///
/// # Returns
///
/// * `Result<()>` - Ok if version requirements are met, error otherwise
pub fn check_nix_version() -> Result<()> {
let version = util::get_nix_version()?;
let is_lix_binary = util::is_lix()?;

let min_version = if is_lix_binary { "2.91.0" } else { "2.26.1" };

let current = Version::parse(&version)?;
let required = Version::parse(min_version)?;

match current.cmp(&required) {
Ordering::Less => {
let binary_name = if is_lix_binary { "Lix" } else { "Nix" };
Err(eyre::eyre!(
"{} version {} is too old. Minimum required version is {}",
binary_name,
version,
min_version
))
}
_ => Ok(()),
}
}

/// Verifies if the required experimental features are enabled
///
/// # Returns
///
/// * `Result<()>` - Ok if all required features are enabled, error otherwise
pub fn check_nix_features() -> Result<()> {
let mut required_features = vec!["nix-command", "flakes"];

// Lix still uses repl-flake, which is removed in the latest version of Nix.
if util::is_lix()? {
required_features.push("repl-flake");
}

if !util::has_all_experimental_features(&required_features)? {
return Err(eyre::eyre!(
"Missing required experimental features. Please enable: {}",
required_features.join(", ")
));
}

Ok(())
}

/// Handles environment variable setup and returns if a warning should be shown
///
/// # Returns
///
/// * `Result<bool>` - True if a warning should be shown about the FLAKE variable, false otherwise
pub fn setup_environment() -> Result<bool> {
let mut do_warn = false;

if let Ok(f) = std::env::var("FLAKE") {
// Set NH_FLAKE if it's not already set
if std::env::var("NH_FLAKE").is_err() {
std::env::set_var("NH_FLAKE", f);

// Only warn if FLAKE is set and we're using it to set NH_FLAKE
// AND none of the command-specific env vars are set
if std::env::var("NH_OS_FLAKE").is_err()
&& std::env::var("NH_HOME_FLAKE").is_err()
&& std::env::var("NH_DARWIN_FLAKE").is_err()
{
do_warn = true;
}
}
}

Ok(do_warn)
}

/// Runs all necessary checks for Nix functionality
///
/// # Returns
///
/// * `Result<()>` - Ok if all checks pass, error otherwise
pub fn verify_nix_environment() -> Result<()> {
check_nix_version()?;
check_nix_features()?;
Ok(())
}
22 changes: 6 additions & 16 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
mod check;
mod clean;
mod commands;
mod completion;
Expand All @@ -20,22 +21,7 @@ const NH_VERSION: &str = env!("CARGO_PKG_VERSION");
const NH_REV: Option<&str> = option_env!("NH_REV");

fn main() -> Result<()> {
let mut do_warn = false;
if let Ok(f) = std::env::var("FLAKE") {
// Set NH_FLAKE if it's not already set
if std::env::var("NH_FLAKE").is_err() {
std::env::set_var("NH_FLAKE", f);

// Only warn if FLAKE is set and we're using it to set NH_FLAKE
// AND none of the command-specific env vars are set
if std::env::var("NH_OS_FLAKE").is_err()
&& std::env::var("NH_HOME_FLAKE").is_err()
&& std::env::var("NH_DARWIN_FLAKE").is_err()
{
do_warn = true;
}
}
}
let do_warn = check::setup_environment()?;

let args = <crate::interface::Main as clap::Parser>::parse();
crate::logging::setup_logging(args.verbose)?;
Expand All @@ -48,9 +34,13 @@ fn main() -> Result<()> {
);
}

// Verify the Nix environment before running commands
check::verify_nix_environment()?;

args.command.run()
}

/// Self-elevates the current process by re-executing it with sudo
fn self_elevate() -> ! {
use std::os::unix::process::CommandExt;

Expand Down
87 changes: 63 additions & 24 deletions src/util.rs
Original file line number Diff line number Diff line change
@@ -1,34 +1,11 @@
extern crate semver;

use std::collections::HashSet;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::str;

use color_eyre::{eyre, Result};
use semver::Version;
use tempfile::TempDir;

/// Compares two semantic versions and returns their order.
///
/// This function takes two version strings, parses them into `semver::Version` objects, and compares them.
/// It returns an `Ordering` indicating whether the current version is less than, equal to, or
/// greater than the target version.
///
/// # Arguments
///
/// * `current` - A string slice representing the current version.
/// * `target` - A string slice representing the target version to compare against.
///
/// # Returns
///
/// * `Result<std::cmp::Ordering>` - The comparison result.
pub fn compare_semver(current: &str, target: &str) -> Result<std::cmp::Ordering> {
let current = Version::parse(current)?;
let target = Version::parse(target)?;

Ok(current.cmp(&target))
}

/// Retrieves the installed Nix version as a string.
///
/// This function executes the `nix --version` command, parses the output to extract the version string,
Expand Down Expand Up @@ -59,6 +36,19 @@ pub fn get_nix_version() -> Result<String> {
Err(eyre::eyre!("Failed to extract version"))
}

/// Determines if the Nix binary is actually Lix
///
/// # Returns
///
/// * `Result<bool>` - True if the binary is Lix, false if it's standard Nix
pub fn is_lix() -> Result<bool> {
let output = Command::new("nix").arg("--version").output()?;
let output_str = str::from_utf8(&output.stdout)?.to_lowercase();

Ok(output_str.contains("lix"))
}

/// Represents an object that may be a temporary path
pub trait MaybeTempPath: std::fmt::Debug {
fn get_path(&self) -> &Path;
}
Expand All @@ -75,6 +65,11 @@ impl MaybeTempPath for (PathBuf, TempDir) {
}
}

/// Gets the hostname of the current system
///
/// # Returns
///
/// * `Result<String>` - The hostname as a string or an error
pub fn get_hostname() -> Result<String> {
#[cfg(not(target_os = "macos"))]
{
Expand Down Expand Up @@ -102,3 +97,47 @@ pub fn get_hostname() -> Result<String> {
Ok(name.to_string())
}
}

/// Retrieves all enabled experimental features in Nix.
///
/// This function executes the `nix config show experimental-features` command and returns
/// a HashSet of the enabled features.
///
/// # Returns
///
/// * `Result<HashSet<String>>` - A HashSet of enabled experimental features or an error.
pub fn get_nix_experimental_features() -> Result<HashSet<String>> {
let output = Command::new("nix")
.args(["config", "show", "experimental-features"])
.output()?;

if !output.status.success() {
return Err(eyre::eyre!(
"Failed to get experimental features: {}",
String::from_utf8_lossy(&output.stderr)
));
}

let output_str = str::from_utf8(&output.stdout)?;
let enabled_features: HashSet<String> =
output_str.split_whitespace().map(String::from).collect();

Ok(enabled_features)
}

/// Checks if all specified experimental features are enabled in Nix.
///
/// # Arguments
///
/// * `features` - A slice of string slices representing the features to check for.
///
/// # Returns
///
/// * `Result<bool>` - True if all specified features are enabled, false otherwise.
pub fn has_all_experimental_features(features: &[&str]) -> Result<bool> {
let enabled_features = get_nix_experimental_features()?;
let features_set: HashSet<String> = features.iter().map(|&s| s.to_string()).collect();

// Check if features_set is a subset of enabled_features
Ok(features_set.is_subset(&enabled_features))
}