Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,20 @@

# NH Changelog

## Unreleased

### Added

- Nh now checks if the current Nix implementation has necessary experimental
features enabled. In mainline Nix (CppNix, etc.) we check for `nix-command`
and `flakes` being set. In Lix, we also use `repl-flake` as it is still
provided as an experimental feature.

- Nh will now check if you are using the latest stable, or "recommended,"
version of Nix (or Lix.) This check has been placed to make it clear we do not
support legacy/vulnerable versions of Nix, and encourage users to update if
they have not yet done so.

## 4.0.3

### Added
Expand Down
14 changes: 7 additions & 7 deletions package.nix
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,12 @@ rustPlatform.buildRustPackage {

buildInputs = lib.optionals stdenv.isDarwin [ darwin.apple_sdk.frameworks.SystemConfiguration ];

preFixup = ''
postInstall = ''
mkdir completions
$out/bin/nh completions bash > completions/nh.bash
$out/bin/nh completions zsh > completions/nh.zsh
$out/bin/nh completions fish > completions/nh.fish

for shell in bash zsh fish; do
NH_NO_CHECKS=1 $out/bin/nh completions $shell > completions/nh.$shell
done

installShellCompletion completions/*
'';
Expand All @@ -55,9 +56,7 @@ rustPlatform.buildRustPackage {

cargoLock.lockFile = ./Cargo.lock;

env = {
NH_REV = rev;
};
env.NH_REV = rev;

meta = {
description = "Yet another nix cli helper";
Expand All @@ -66,6 +65,7 @@ rustPlatform.buildRustPackage {
mainProgram = "nh";
maintainers = with lib.maintainers; [
drupol
NotAShelf
viperML
];
};
Expand Down
154 changes: 154 additions & 0 deletions src/checks.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
use std::{cmp::Ordering, env};

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

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<()> {
if env::var("NH_NO_CHECKS").is_ok() {
return Ok(());
}

let version = util::get_nix_version()?;
let is_lix_binary = util::is_lix()?;

// XXX: Both Nix and Lix follow semantic versioning (semver). Update the
// versions below once latest stable for either of those packages change.
// TODO: Set up a CI to automatically update those in the future.
const MIN_LIX_VERSION: &str = "2.91.1";
const MIN_NIX_VERSION: &str = "2.24.14";

// Minimum supported versions. Those should generally correspond to
// latest package versions in the stable branch.
//
// Q: Why are you doing this?
// A: First of all to make sure we do not make baseless assumptions
// about the user's system; we should only work around APIs that we
// are fully aware of, and not try to work around every edge case.
// Also, nh should be responsible for nudging the user to use the
// relevant versions of the software it wraps, so that we do not have
// to try and support too many versions. NixOS stable and unstable
// will ALWAYS be supported, but outdated versions will not. If your
// Nix fork uses a different versioning scheme, please open an issue.
let min_version = if is_lix_binary {
MIN_LIX_VERSION
} else {
MIN_NIX_VERSION
};

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" };
warn!(
"Warning: {} version {} is older than the recommended minimum version {}. You may encounter issues.",
binary_name,
version,
min_version
);
Ok(())
}
_ => 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<()> {
if env::var("NH_NO_CHECKS").is_ok() {
return Ok(());
}

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");
}

tracing::debug!("Required Nix features: {}", required_features.join(", "));

// Get currently enabled features
match util::get_nix_experimental_features() {
Ok(enabled_features) => {
let features_vec: Vec<_> = enabled_features.into_iter().collect();
tracing::debug!("Enabled Nix features: {}", features_vec.join(", "));
}
Err(e) => {
tracing::warn!("Failed to get enabled Nix features: {}", e);
}
}

let missing_features = util::get_missing_experimental_features(&required_features)?;

if !missing_features.is_empty() {
tracing::warn!(
"Missing required Nix features: {}",
missing_features.join(", ")
);
return Err(eyre::eyre!(
"Missing required experimental features. Please enable: {}",
missing_features.join(", ")
));
}

tracing::debug!("All required Nix features are enabled");
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)
}

/// Consolidate all necessary checks for Nix functionality into a single
/// function. This will be executed in the main function, but can be executed
/// before critical commands to double-check if necessary.
///
/// # Returns
///
/// * `Result<()>` - Ok if all checks pass, error otherwise
pub fn verify_nix_environment() -> Result<()> {
if env::var("NH_NO_CHECKS").is_ok() {
return Ok(());
}

check_nix_version()?;
check_nix_features()?;
Ok(())
}
29 changes: 11 additions & 18 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
mod checks;
mod clean;
mod commands;
mod completion;
Expand All @@ -20,29 +21,20 @@ 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 args = <crate::interface::Main as clap::Parser>::parse();

// Set up logging
crate::logging::setup_logging(args.verbose)?;
tracing::debug!("{args:#?}");
tracing::debug!(%NH_VERSION, ?NH_REV);

if do_warn {
// Verify the Nix environment before running commands
checks::verify_nix_environment()?;

// Once we assert required Nix features, validate NH environment checks
// For now, this is just NH_* variables being set. More checks may be
// added to setup_environment in the future.
if checks::setup_environment()? {
tracing::warn!(
"nh {NH_VERSION} now uses NH_FLAKE instead of FLAKE, please modify your configuration"
);
Expand All @@ -51,6 +43,7 @@ fn main() -> Result<()> {
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
Loading