diff --git a/Cargo.lock b/Cargo.lock index 3449868c5..e102c7898 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -546,6 +546,7 @@ dependencies = [ "indoc", "miette", "nix", + "nix-conf-parser", "petgraph", "regex", "reqwest", @@ -973,13 +974,19 @@ dependencies = [ "allocator-api2", ] +[[package]] +name = "hashbrown" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3" + [[package]] name = "hashlink" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" dependencies = [ - "hashbrown", + "hashbrown 0.14.5", ] [[package]] @@ -1135,12 +1142,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.2.3" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233cf39063f058ea2caae4091bf4a3ef70a653afbc026f5c4a4135d114e3c177" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.15.1", "serde", ] @@ -1361,6 +1368,15 @@ dependencies = [ "libc", ] +[[package]] +name = "nix-conf-parser" +version = "0.0.1" +dependencies = [ + "indexmap", + "miette", + "thiserror", +] + [[package]] name = "nom" version = "7.1.3" @@ -2211,7 +2227,7 @@ dependencies = [ "futures-intrusive", "futures-io", "futures-util", - "hashbrown", + "hashbrown 0.14.5", "hashlink", "hex", "indexmap", diff --git a/Cargo.toml b/Cargo.toml index 8d3b48f2f..43be1d6e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "2" -members = ["devenv", "devenv-eval-cache", "devenv-run-tests", "devenv-tasks", "xtask"] +members = ["devenv", "devenv-eval-cache", "devenv-run-tests", "devenv-tasks", "nix-conf-parser", "xtask"] [workspace.package] edition = "2021" @@ -13,6 +13,7 @@ devenv = { path = "devenv" } devenv-eval-cache = { path = "devenv-eval-cache" } devenv-run-tests = { path = "devenv-run-tests" } devenv-tasks = { path = "devenv-tasks" } +nix-conf-parser = { path = "nix-conf-parser" } xtask = { path = "xtask" } ansiterm = "0.12.2" @@ -24,6 +25,7 @@ dotlock = "0.5.0" futures = "0.3.30" hex = "0.4.3" include_dir = "0.7.3" +indexmap = "2.6.0" indoc = "2.0.4" lazy_static = "1.5.0" miette = { version = "7.1.0", features = ["fancy"] } diff --git a/devenv/Cargo.toml b/devenv/Cargo.toml index 4c088f42b..89d4f9755 100644 --- a/devenv/Cargo.toml +++ b/devenv/Cargo.toml @@ -12,6 +12,7 @@ default-run = "devenv" [dependencies] devenv-eval-cache.workspace = true devenv-tasks.workspace = true +nix-conf-parser.workspace = true clap.workspace = true cli-table.workspace = true diff --git a/devenv/src/cnix.rs b/devenv/src/cnix.rs index e3b0fd865..e39f8339d 100644 --- a/devenv/src/cnix.rs +++ b/devenv/src/cnix.rs @@ -1,5 +1,6 @@ use crate::{cli, config, log}; use miette::{bail, IntoDiagnostic, Result, WrapErr}; +use nix_conf_parser::NixConf; use serde::Deserialize; use sqlx::SqlitePool; use std::cell::{Ref, RefCell}; @@ -588,6 +589,16 @@ impl<'a> Nix<'a> { Ok(cmd) } + async fn get_nix_config(&self) -> Result { + let options = Options { + logging: false, + ..self.options + }; + let raw_conf = self.run_nix("nix", &["config", "show"], &options).await?; + let nix_conf = NixConf::parse_stdout(&raw_conf.stdout)?; + Ok(nix_conf) + } + async fn get_cachix_caches(&self) -> Result> { if self.cachix_caches.borrow().is_none() { let no_logging = Options { @@ -646,7 +657,7 @@ impl<'a> Nix<'a> { .trusted; if trusted.is_none() { self.logger.warn( - "You're using very old version of Nix, please upgrade and restart nix-daemon.", + "You're using an outdated version of Nix. Please upgrade and restart the nix-daemon.", ); } let restart_command = if cfg!(target_os = "linux") { @@ -673,68 +684,123 @@ impl<'a> Nix<'a> { ) .expect("Failed to write cachix caches to file"); + // If the user is not a trusted user, we can't set up the caches for them. + // Check if all of the requested caches and their public keys are in the substituters and trusted-public-keys lists. + // If not, suggest actions to remedy the issue. if trusted == Some(0) { - if !Path::new("/etc/NIXOS").exists() { - self.logger.error(&indoc::formatdoc!( - "You're not a trusted user of the Nix store. You have the following options: + let mut missing_caches = Vec::new(); + let mut missing_public_keys = Vec::new(); + + if let Ok(nix_conf) = self.get_nix_config().await { + let substituters = nix_conf + .get("substituters") + .map(|s| s.split_whitespace().collect::>()); + + if let Some(substituters) = substituters { + for cache in caches.caches.pull.iter() { + let cache_url = format!("https://{}.cachix.org", cache); + if !substituters.iter().any(|s| s == &cache_url) { + missing_caches.push(cache_url); + } + } + } + + let trusted_public_keys = nix_conf + .get("trusted-public-keys") + .map(|s| s.split_whitespace().collect::>()); + + if let Some(trusted_public_keys) = trusted_public_keys { + for (_name, key) in caches.known_keys.iter() { + if !trusted_public_keys.iter().any(|p| p == key) { + missing_public_keys.push(key.clone()); + } + } + } + } + + if !missing_caches.is_empty() || !missing_public_keys.is_empty() { + if !Path::new("/etc/NIXOS").exists() { + self.logger.error(&indoc::formatdoc!( + "Failed to set up binary caches: + + {} + + devenv is configured to automatically manage binary caches with `cachix.enable = true`, but cannot do so because you are not a trusted user of the Nix store. - a) Add yourself to the trusted-users list in /etc/nix/nix.conf for devenv to manage caches for you. + You have several options: - trusted-users = root {} + a) To let devenv set up the caches for you, add yourself to the trusted-users list in /etc/nix/nix.conf: - Restart nix-daemon with: + trusted-users = root {} - $ {restart_command} + Then restart the nix-daemon: - b) Add binary caches to /etc/nix/nix.conf yourself: + $ {restart_command} - extra-substituters = {} - extra-trusted-public-keys = {} + b) Add the missing binary caches to /etc/nix/nix.conf yourself: - And disable automatic cache configuration in `devenv.nix`: + extra-substituters = {} + extra-trusted-public-keys = {} - {{ - cachix.enable = false; - }} - ", whoami::username() - , caches.caches.pull.iter().map(|cache| format!("https://{}.cachix.org", cache)).collect::>().join(" ") - , caches.known_keys.values().cloned().collect::>().join(" ") + c) Disable automatic cache management in your devenv configuration: + + {{ + cachix.enable = false; + }} + " + , missing_caches.join(" ") + , whoami::username() + , missing_caches.join(" ") + , missing_public_keys.join(" ") )); - } else { - self.logger.error(&indoc::formatdoc!( - "You're not a trusted user of the Nix store. You have the following options: + } else { + self.logger.error(&indoc::formatdoc!( + "Failed to set up binary caches: + + {} + + devenv is configured to automatically manage binary caches with `cachix.enable = true`, but cannot do so because you are not a trusted user of the Nix store. + + You have several options: - a) Add yourself to the trusted-users list in /etc/nix/nix.conf by editing configuration.nix for devenv to manage caches for you. + a) To let devenv set up the caches for you, add yourself to the trusted-users list in /etc/nix/nix.conf by editing configuration.nix. - {{ - nix.extraOptions = '' - trusted-users = root {} - ''; - }} + {{ + nix.settings.trusted-users = [ \"root\" \"{}\" ]; + }} - b) Add binary caches to /etc/nix/nix.conf yourself by editing configuration.nix: - {{ - nix.extraOptions = '' - extra-substituters = {} - extra-trusted-public-keys = {} - ''; - }} + Rebuild your system: - Disable automatic cache configuration in `devenv.nix`: + $ sudo nixos-rebuild switch - {{ - cachix.enable = false; - }} + b) Add the missing binary caches to /etc/nix/nix.conf yourself by editing configuration.nix: - Lastly, rebuild your system: + {{ + nix.extraOptions = '' + extra-substituters = {} + extra-trusted-public-keys = {} + ''; + }} - $ sudo nixos-rebuild switch - ", whoami::username() - , caches.caches.pull.iter().map(|cache| format!("https://{}.cachix.org", cache)).collect::>().join(" ") - , caches.known_keys.values().cloned().collect::>().join(" ") + Rebuild your system: + + $ sudo nixos-rebuild switch + + c) Disable automatic cache management in your devenv configuration: + + {{ + cachix.enable = false; + }} + " + , missing_caches.join(" ") + , whoami::username() + , missing_caches.join(" ") + , missing_public_keys.join(" ") )); + } + + bail!("You're not a trusted user of the Nix store.") } - bail!("You're not a trusted user of the Nix store.") } } diff --git a/nix-conf-parser/Cargo.toml b/nix-conf-parser/Cargo.toml new file mode 100644 index 000000000..7fc922e8d --- /dev/null +++ b/nix-conf-parser/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "nix-conf-parser" +version = "0.0.1" +edition.workspace = true +license.workspace = true + +[dependencies] +indexmap.workspace = true +miette.workspace = true +thiserror.workspace = true diff --git a/nix-conf-parser/src/lib.rs b/nix-conf-parser/src/lib.rs new file mode 100644 index 000000000..ba155cfca --- /dev/null +++ b/nix-conf-parser/src/lib.rs @@ -0,0 +1,104 @@ +/// Parse a nix.conf into an ordered map of key-value string pairs. +/// +/// Closely follows the upstream implementation: +/// https://github.com/NixOS/nix/blob/acb60fc3594edcc54dae9a10d2a0dc3f3b3be0da/src/libutil/config.cc#L104-L161 +/// +/// Only intended to work on the output of `nix config show`. +/// Therefore, this intentionally leaves out: +/// - includes and !includes +/// - comments +/// - formatting +use indexmap::IndexMap; +use miette::Diagnostic; +use thiserror::Error; + +#[derive(Debug)] +pub struct NixConf { + settings: IndexMap, +} + +impl NixConf { + pub fn parse_stdout(input: &[u8]) -> Result { + let input = String::from_utf8_lossy(input); + Self::parse_str(&input) + } + + /// Parse a string into an ordered map of key-value string pairs. + pub fn parse_str(input: &str) -> Result { + let mut settings = IndexMap::new(); + + for mut line in input.lines() { + // Trim comments + if let Some(pos) = line.find('#') { + line = &line[..pos]; + } + + if line.trim().is_empty() { + continue; + } + + let mut tokens = line.split_whitespace().collect::>(); + tokens.retain(|t| !t.is_empty()); + + if tokens.is_empty() { + continue; + } + + if tokens.len() < 2 { + return Err(ParseError::IllegalConfiguration(line.to_string())); + } + + // Skip includes if they make it into the input + match tokens[0] { + "include" | "!include" => continue, + _ => {} + } + + if tokens[1] != "=" { + return Err(ParseError::IllegalConfiguration(line.to_string())); + } + + let name = tokens[0]; + let value = tokens[2..].join(" "); + + settings.insert(name.to_string(), value); + } + + Ok(Self { settings }) + } + + pub fn get(&self, key: &str) -> Option<&String> { + self.settings.get(key) + } +} + +#[derive(Debug, Diagnostic, Error)] +pub enum ParseError { + #[error("illegal configuration line '{0}'")] + IllegalConfiguration(String), +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_parse() { + let input = r#" + # This is a comment + include /etc/nixos/hardware-configuration.nix + !include /etc/nixos/hardware-configuration.nix + single = foo + space = foo bar + list = foo bar baz + comment = foo # comment + tab = foo + "#; + let nix_conf = NixConf::parse_str(input).unwrap(); + assert_eq!(nix_conf.get("single"), Some(&"foo".into())); + assert_eq!(nix_conf.get("space"), Some(&"foo bar".into())); + assert_eq!(nix_conf.get("list"), Some(&"foo bar baz".into())); + assert_eq!(nix_conf.get("comment"), Some(&"foo".into())); + assert_eq!(nix_conf.get("tab"), Some(&"foo".into())); + } +} diff --git a/package.nix b/package.nix index 2909442a1..7d22c4226 100644 --- a/package.nix +++ b/package.nix @@ -14,6 +14,7 @@ pkgs.rustPlatform.buildRustPackage { ".*devenv-eval-cache(/.*)?" ".*devenv-run-tests(/.*)?" ".*devenv-tasks(/.*)?" + ".*nix-conf-parser(/.*)?" ".*xtask(/.*)?" ];