nix_dev_env/
nix_version_check.rs

1use std::ffi::OsStr;
2
3use once_cell::sync::Lazy;
4use regex::Regex;
5use semver::{Comparator, Op, Prerelease, Version, VersionReq};
6
7use crate::nix_command;
8
9static REQUIRED_NIX_VERSION: Lazy<VersionReq> = Lazy::new(|| VersionReq {
10    comparators: vec![Comparator {
11        op: Op::GreaterEq,
12        major: 2,
13        minor: Some(10),
14        patch: Some(0),
15        pre: Prerelease::EMPTY,
16    }],
17});
18
19static SEMVER_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"([0-9]+\.[0-9]+\.[0-9]+)").unwrap());
20
21pub fn check_nix_version() -> anyhow::Result<()> {
22    check_nix_program_version(OsStr::new("nix"))
23}
24
25fn check_nix_program_version(nix_executable_path: impl AsRef<OsStr>) -> anyhow::Result<()> {
26    let stdout_content = nix_command::nix_program(nix_executable_path.as_ref(), ["--version"])?;
27
28    if stdout_content.is_empty() {
29        return Err(anyhow::format_err!("`nix --version` failed to execute."));
30    }
31
32    let nix_version_match = SEMVER_RE
33        .find(&stdout_content)
34        .ok_or_else(|| anyhow::format_err!("SemVer from `nix --version` could not be found."))?;
35    let nix_version = Version::parse(nix_version_match.as_str())?;
36
37    if REQUIRED_NIX_VERSION.matches(&nix_version) {
38        Ok(())
39    } else {
40        Err(anyhow::format_err!("`nix` version too old for flakes."))
41    }
42}
43
44#[cfg(test)]
45mod tests {
46    use std::{env, fs, os::unix::fs::PermissionsExt, path::PathBuf};
47
48    use super::check_nix_program_version;
49
50    #[derive(Debug)]
51    struct NixExecutable {
52        // NB: `_dir` needed to prevent tempfile cleanup
53        pub _dir: tempfile::TempDir,
54        pub file_path: PathBuf,
55    }
56
57    impl NixExecutable {
58        fn new(file_contents: &str) -> Self {
59            // NB: Use a temp dir instead of a temp file since executing a file requires the file is
60            // not open for writing / deleting
61            let dir = tempfile::tempdir().unwrap();
62            let file_path = dir.path().join("nix");
63            let bash_path = env::var("NIX_BIN_BASH").unwrap_or_else(|_| String::from("/bin/bash"));
64            fs::write(&file_path, format!("#! {bash_path}\n{file_contents}")).unwrap();
65            fs::set_permissions(&file_path, fs::Permissions::from_mode(0o777)).unwrap();
66            Self {
67                _dir: dir,
68                file_path,
69            }
70        }
71    }
72
73    #[test]
74    fn test_error_on_exit_failure() {
75        let nix_executable = NixExecutable::new(r#"exit 1;"#);
76        assert_eq!(
77            check_nix_program_version(&nix_executable.file_path)
78                .unwrap_err()
79                .to_string(),
80            format!(
81                "`{} --extra-experimental-features nix-command' flakes' --version` failed with error:\nprocess exited unsuccessfully: exit status: 1",
82                nix_executable.file_path.to_string_lossy()
83            )
84        );
85    }
86
87    #[test]
88    fn test_error_on_empty_stdout() {
89        let nix_executable = NixExecutable::new(r#"printf "";"#);
90        assert_eq!(
91            check_nix_program_version(nix_executable.file_path)
92                .unwrap_err()
93                .to_string(),
94            "`nix --version` failed to execute."
95        );
96    }
97
98    #[test]
99    fn test_error_on_missing_semver() {
100        let nix_executable = NixExecutable::new(r#"echo "hello";"#);
101        assert_eq!(
102            check_nix_program_version(nix_executable.file_path)
103                .unwrap_err()
104                .to_string(),
105            "SemVer from `nix --version` could not be found."
106        );
107    }
108
109    #[test]
110    fn test_error_on_too_old_version() {
111        let nix_executable = NixExecutable::new(r#"echo "nix (Nix) 0.0.0";"#);
112        assert_eq!(
113            check_nix_program_version(nix_executable.file_path)
114                .unwrap_err()
115                .to_string(),
116            "`nix` version too old for flakes."
117        );
118    }
119
120    #[test]
121    fn test_version_matches_minimum() {
122        let nix_executable = NixExecutable::new(r#"echo "nix (Nix) 2.10.0";"#);
123        check_nix_program_version(nix_executable.file_path).unwrap();
124    }
125
126    #[test]
127    fn test_version_matches_newer() {
128        let nix_executable = NixExecutable::new(r#"echo "nix (Nix) 2.30.0";"#);
129        check_nix_program_version(nix_executable.file_path).unwrap();
130    }
131}