Skip to content

Commit d36ff53

Browse files
authored
Merge pull request #277 from nix-community/environment-checks
nh: validate nix features & version
2 parents 4eb1941 + 968c12b commit d36ff53

File tree

5 files changed

+267
-55
lines changed

5 files changed

+267
-55
lines changed

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,20 @@
22

33
# NH Changelog
44

5+
## Unreleased
6+
7+
### Added
8+
9+
- Nh now checks if the current Nix implementation has necessary experimental
10+
features enabled. In mainline Nix (CppNix, etc.) we check for `nix-command`
11+
and `flakes` being set. In Lix, we also use `repl-flake` as it is still
12+
provided as an experimental feature.
13+
14+
- Nh will now check if you are using the latest stable, or "recommended,"
15+
version of Nix (or Lix.) This check has been placed to make it clear we do not
16+
support legacy/vulnerable versions of Nix, and encourage users to update if
17+
they have not yet done so.
18+
519
## 4.0.3
620

721
### Added

package.nix

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,12 @@ rustPlatform.buildRustPackage {
3939

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

42-
preFixup = ''
42+
postInstall = ''
4343
mkdir completions
44-
$out/bin/nh completions bash > completions/nh.bash
45-
$out/bin/nh completions zsh > completions/nh.zsh
46-
$out/bin/nh completions fish > completions/nh.fish
44+
45+
for shell in bash zsh fish; do
46+
NH_NO_CHECKS=1 $out/bin/nh completions $shell > completions/nh.$shell
47+
done
4748
4849
installShellCompletion completions/*
4950
'';
@@ -55,9 +56,7 @@ rustPlatform.buildRustPackage {
5556

5657
cargoLock.lockFile = ./Cargo.lock;
5758

58-
env = {
59-
NH_REV = rev;
60-
};
59+
env.NH_REV = rev;
6160

6261
meta = {
6362
description = "Yet another nix cli helper";
@@ -66,6 +65,7 @@ rustPlatform.buildRustPackage {
6665
mainProgram = "nh";
6766
maintainers = with lib.maintainers; [
6867
drupol
68+
NotAShelf
6969
viperML
7070
];
7171
};

src/checks.rs

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
use std::{cmp::Ordering, env};
2+
3+
use color_eyre::{eyre, Result};
4+
use semver::Version;
5+
use tracing::warn;
6+
7+
use crate::util;
8+
9+
/// Verifies if the installed Nix version meets requirements
10+
///
11+
/// # Returns
12+
///
13+
/// * `Result<()>` - Ok if version requirements are met, error otherwise
14+
pub fn check_nix_version() -> Result<()> {
15+
if env::var("NH_NO_CHECKS").is_ok() {
16+
return Ok(());
17+
}
18+
19+
let version = util::get_nix_version()?;
20+
let is_lix_binary = util::is_lix()?;
21+
22+
// XXX: Both Nix and Lix follow semantic versioning (semver). Update the
23+
// versions below once latest stable for either of those packages change.
24+
// TODO: Set up a CI to automatically update those in the future.
25+
const MIN_LIX_VERSION: &str = "2.91.1";
26+
const MIN_NIX_VERSION: &str = "2.24.14";
27+
28+
// Minimum supported versions. Those should generally correspond to
29+
// latest package versions in the stable branch.
30+
//
31+
// Q: Why are you doing this?
32+
// A: First of all to make sure we do not make baseless assumptions
33+
// about the user's system; we should only work around APIs that we
34+
// are fully aware of, and not try to work around every edge case.
35+
// Also, nh should be responsible for nudging the user to use the
36+
// relevant versions of the software it wraps, so that we do not have
37+
// to try and support too many versions. NixOS stable and unstable
38+
// will ALWAYS be supported, but outdated versions will not. If your
39+
// Nix fork uses a different versioning scheme, please open an issue.
40+
let min_version = if is_lix_binary {
41+
MIN_LIX_VERSION
42+
} else {
43+
MIN_NIX_VERSION
44+
};
45+
46+
let current = Version::parse(&version)?;
47+
let required = Version::parse(min_version)?;
48+
49+
match current.cmp(&required) {
50+
Ordering::Less => {
51+
let binary_name = if is_lix_binary { "Lix" } else { "Nix" };
52+
warn!(
53+
"Warning: {} version {} is older than the recommended minimum version {}. You may encounter issues.",
54+
binary_name,
55+
version,
56+
min_version
57+
);
58+
Ok(())
59+
}
60+
_ => Ok(()),
61+
}
62+
}
63+
64+
/// Verifies if the required experimental features are enabled
65+
///
66+
/// # Returns
67+
///
68+
/// * `Result<()>` - Ok if all required features are enabled, error otherwise
69+
pub fn check_nix_features() -> Result<()> {
70+
if env::var("NH_NO_CHECKS").is_ok() {
71+
return Ok(());
72+
}
73+
74+
let mut required_features = vec!["nix-command", "flakes"];
75+
76+
// Lix still uses repl-flake, which is removed in the latest version of Nix.
77+
if util::is_lix()? {
78+
required_features.push("repl-flake");
79+
}
80+
81+
tracing::debug!("Required Nix features: {}", required_features.join(", "));
82+
83+
// Get currently enabled features
84+
match util::get_nix_experimental_features() {
85+
Ok(enabled_features) => {
86+
let features_vec: Vec<_> = enabled_features.into_iter().collect();
87+
tracing::debug!("Enabled Nix features: {}", features_vec.join(", "));
88+
}
89+
Err(e) => {
90+
tracing::warn!("Failed to get enabled Nix features: {}", e);
91+
}
92+
}
93+
94+
let missing_features = util::get_missing_experimental_features(&required_features)?;
95+
96+
if !missing_features.is_empty() {
97+
tracing::warn!(
98+
"Missing required Nix features: {}",
99+
missing_features.join(", ")
100+
);
101+
return Err(eyre::eyre!(
102+
"Missing required experimental features. Please enable: {}",
103+
missing_features.join(", ")
104+
));
105+
}
106+
107+
tracing::debug!("All required Nix features are enabled");
108+
Ok(())
109+
}
110+
111+
/// Handles environment variable setup and returns if a warning should be shown
112+
///
113+
/// # Returns
114+
///
115+
/// * `Result<bool>` - True if a warning should be shown about the FLAKE
116+
/// variable, false otherwise
117+
pub fn setup_environment() -> Result<bool> {
118+
let mut do_warn = false;
119+
120+
if let Ok(f) = std::env::var("FLAKE") {
121+
// Set NH_FLAKE if it's not already set
122+
if std::env::var("NH_FLAKE").is_err() {
123+
std::env::set_var("NH_FLAKE", f);
124+
125+
// Only warn if FLAKE is set and we're using it to set NH_FLAKE
126+
// AND none of the command-specific env vars are set
127+
if std::env::var("NH_OS_FLAKE").is_err()
128+
&& std::env::var("NH_HOME_FLAKE").is_err()
129+
&& std::env::var("NH_DARWIN_FLAKE").is_err()
130+
{
131+
do_warn = true;
132+
}
133+
}
134+
}
135+
136+
Ok(do_warn)
137+
}
138+
139+
/// Consolidate all necessary checks for Nix functionality into a single
140+
/// function. This will be executed in the main function, but can be executed
141+
/// before critical commands to double-check if necessary.
142+
///
143+
/// # Returns
144+
///
145+
/// * `Result<()>` - Ok if all checks pass, error otherwise
146+
pub fn verify_nix_environment() -> Result<()> {
147+
if env::var("NH_NO_CHECKS").is_ok() {
148+
return Ok(());
149+
}
150+
151+
check_nix_version()?;
152+
check_nix_features()?;
153+
Ok(())
154+
}

src/main.rs

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
mod checks;
12
mod clean;
23
mod commands;
34
mod completion;
@@ -20,29 +21,20 @@ const NH_VERSION: &str = env!("CARGO_PKG_VERSION");
2021
const NH_REV: Option<&str> = option_env!("NH_REV");
2122

2223
fn main() -> Result<()> {
23-
let mut do_warn = false;
24-
if let Ok(f) = std::env::var("FLAKE") {
25-
// Set NH_FLAKE if it's not already set
26-
if std::env::var("NH_FLAKE").is_err() {
27-
std::env::set_var("NH_FLAKE", f);
28-
29-
// Only warn if FLAKE is set and we're using it to set NH_FLAKE
30-
// AND none of the command-specific env vars are set
31-
if std::env::var("NH_OS_FLAKE").is_err()
32-
&& std::env::var("NH_HOME_FLAKE").is_err()
33-
&& std::env::var("NH_DARWIN_FLAKE").is_err()
34-
{
35-
do_warn = true;
36-
}
37-
}
38-
}
39-
4024
let args = <crate::interface::Main as clap::Parser>::parse();
25+
26+
// Set up logging
4127
crate::logging::setup_logging(args.verbose)?;
4228
tracing::debug!("{args:#?}");
4329
tracing::debug!(%NH_VERSION, ?NH_REV);
4430

45-
if do_warn {
31+
// Verify the Nix environment before running commands
32+
checks::verify_nix_environment()?;
33+
34+
// Once we assert required Nix features, validate NH environment checks
35+
// For now, this is just NH_* variables being set. More checks may be
36+
// added to setup_environment in the future.
37+
if checks::setup_environment()? {
4638
tracing::warn!(
4739
"nh {NH_VERSION} now uses NH_FLAKE instead of FLAKE, please modify your configuration"
4840
);
@@ -51,6 +43,7 @@ fn main() -> Result<()> {
5143
args.command.run()
5244
}
5345

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

0 commit comments

Comments
 (0)