diff --git a/manual/src/release-notes.md b/manual/src/release-notes.md index e3c628c..26ddafc 100644 --- a/manual/src/release-notes.md +++ b/manual/src/release-notes.md @@ -2,6 +2,7 @@ ## Release 0.4.0 (Unreleased) +- Flake evaluation is now actually pure by default. To enable impure expressions, pass `--impure`. - `--reboot` is added to trigger a reboot and wait for the node to come back up. - The target user is no longer explicitly set when `deployment.targetUser` is null ([#91](https://github.com/zhaofengli/colmena/pull/91)). - In `apply-local`, we now only escalate privileges during activation ([#85](https://github.com/zhaofengli/colmena/issues/85)). diff --git a/src/nix/flake.rs b/src/nix/flake.rs index 79e7232..adcf23a 100644 --- a/src/nix/flake.rs +++ b/src/nix/flake.rs @@ -10,7 +10,7 @@ use tokio::process::Command; use super::{ColmenaError, ColmenaResult, NixCheck}; /// A Nix Flake. -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Flake { /// The flake metadata. metadata: FlakeMetadata, @@ -20,7 +20,7 @@ pub struct Flake { } /// A `nix flake metadata --json` invocation. -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Debug, Clone)] struct FlakeMetadata { /// The resolved URL of the flake. #[serde(rename = "resolvedUrl")] @@ -105,3 +105,20 @@ impl FlakeMetadata { }) } } + +/// Quietly locks the dependencies of a flake. +pub async fn lock_flake_quiet(uri: &str) -> ColmenaResult<()> { + let status = Command::new("nix") + .args(&["flake", "lock"]) + .args(&["--experimental-features", "nix-command flakes"]) + .arg(uri) + .stderr(Stdio::null()) + .status() + .await?; + + if !status.success() { + return Err(status.into()); + } + + Ok(()) +} diff --git a/src/nix/hive/assets.rs b/src/nix/hive/assets.rs index 2d4fb52..57ef7c9 100644 --- a/src/nix/hive/assets.rs +++ b/src/nix/hive/assets.rs @@ -13,8 +13,9 @@ use tempfile::TempDir; use super::{Flake, HivePath}; use crate::error::ColmenaResult; +use crate::nix::flake::lock_flake_quiet; -const FLAKE_NIX: &[u8] = include_bytes!("flake.nix"); +const FLAKE_NIX: &str = include_str!("flake.nix"); const EVAL_NIX: &[u8] = include_bytes!("eval.nix"); const OPTIONS_NIX: &[u8] = include_bytes!("options.nix"); const MODULES_NIX: &[u8] = include_bytes!("modules.nix"); @@ -22,6 +23,9 @@ const MODULES_NIX: &[u8] = include_bytes!("modules.nix"); /// Static files required to evaluate a Hive configuration. #[derive(Debug)] pub(super) struct Assets { + /// Path to the hive being evaluated. + hive_path: HivePath, + /// Temporary directory holding the files. temp_dir: TempDir, @@ -30,7 +34,7 @@ pub(super) struct Assets { } impl Assets { - pub async fn new(flake: bool) -> ColmenaResult { + pub async fn new(hive_path: HivePath) -> ColmenaResult { let temp_dir = TempDir::new().unwrap(); create_file(&temp_dir, "eval.nix", false, EVAL_NIX)?; @@ -39,27 +43,30 @@ impl Assets { let mut assets_flake_uri = None; - if flake { + if let HivePath::Flake(hive_flake) = &hive_path { // Emit a temporary flake, then resolve the locked URI - create_file(&temp_dir, "flake.nix", false, FLAKE_NIX)?; + let flake_nix = FLAKE_NIX.replace("%hive%", hive_flake.locked_uri()); + create_file(&temp_dir, "flake.nix", false, flake_nix.as_bytes())?; // We explicitly specify `path:` instead of letting Nix resolve // automatically, which would involve checking parent directories // for a git repository. let uri = format!("path:{}", temp_dir.path().to_str().unwrap()); + let _ = lock_flake_quiet(&uri).await; let assets_flake = Flake::from_uri(uri).await?; assets_flake_uri = Some(assets_flake.locked_uri().to_owned()); } Ok(Self { + hive_path, temp_dir, assets_flake_uri, }) } /// Returns the base expression from which the evaluated Hive can be used. - pub fn get_base_expression(&self, hive_path: &HivePath) -> String { - match hive_path { + pub fn get_base_expression(&self) -> String { + match &self.hive_path { HivePath::Legacy(path) => { format!( "with builtins; let eval = import {eval_nix}; hive = eval {{ rawHive = import {path}; colmenaOptions = import {options_nix}; colmenaModules = import {modules_nix}; }}; in ", @@ -69,11 +76,10 @@ impl Assets { modules_nix = self.get_path("modules.nix"), ) } - HivePath::Flake(flake) => { + HivePath::Flake(_) => { format!( - "with builtins; let assets = getFlake \"{assets_flake_uri}\"; hive = assets.lib.colmenaEval {{ flakeUri = \"{flake_uri}\"; }}; in ", + "with builtins; let assets = getFlake \"{assets_flake_uri}\"; hive = assets.colmenaEval; in ", assets_flake_uri = self.assets_flake_uri.as_ref().expect("The assets flake must have been initialized"), - flake_uri = flake.locked_uri(), ) } } diff --git a/src/nix/hive/eval.nix b/src/nix/hive/eval.nix index 408b5c9..e1621fb 100644 --- a/src/nix/hive/eval.nix +++ b/src/nix/hive/eval.nix @@ -1,6 +1,6 @@ { rawHive ? null # Colmena Hive attrset -, flakeUri ? null # Nix Flake URI with `outputs.colmena` -, hermetic ? flakeUri != null # Whether we are allowed to use +, rawFlake ? null # Nix Flake attrset with `outputs.colmena` +, hermetic ? rawFlake != null # Whether we are allowed to use , colmenaOptions , colmenaModules }: @@ -19,10 +19,8 @@ let uncheckedHive = let - flakeToHive = flakeUri: let - flake = builtins.getFlake flakeUri; - hive = if flake.outputs ? colmena then flake.outputs.colmena else throw "Flake must define outputs.colmena."; - in hive; + flakeToHive = rawFlake: + if rawFlake.outputs ? colmena then rawFlake.outputs.colmena else throw "Flake must define outputs.colmena."; rawToHive = rawHive: if typeOf rawHive == "lambda" || rawHive ? __functor then rawHive {} @@ -30,8 +28,8 @@ let else throw "The config must evaluate to an attribute set."; in if rawHive != null then rawToHive rawHive - else if flakeUri != null then flakeToHive flakeUri - else throw "Either an attribute set or a flake URI must be specified."; + else if rawFlake != null then flakeToHive rawFlake + else throw "Either a plain Hive attribute set or a Nix Flake attribute set must be specified."; uncheckedUserMeta = if uncheckedHive ? meta && uncheckedHive ? network then diff --git a/src/nix/hive/flake.nix b/src/nix/hive/flake.nix index f56f37e..4acb727 100644 --- a/src/nix/hive/flake.nix +++ b/src/nix/hive/flake.nix @@ -1,13 +1,13 @@ { description = "Internal Colmena expressions"; - outputs = { ... }: { - lib.colmenaEval = { - rawHive ? null, - flakeUri ? null, - hermetic ? flakeUri != null, - }: import ./eval.nix { - inherit rawHive flakeUri hermetic; + inputs = { + hive.url = "%hive%"; + }; + + outputs = { self, hive }: { + colmenaEval = import ./eval.nix { + rawFlake = hive; colmenaOptions = import ./options.nix; colmenaModules = import ./modules.nix; }; diff --git a/src/nix/hive/mod.rs b/src/nix/hive/mod.rs index 2f8e85f..b8141b2 100644 --- a/src/nix/hive/mod.rs +++ b/src/nix/hive/mod.rs @@ -21,7 +21,7 @@ use crate::job::JobHandle; use crate::util::{CommandExecution, CommandExt}; use assets::Assets; -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum HivePath { /// A Nix Flake. /// @@ -93,7 +93,7 @@ impl HivePath { impl Hive { pub async fn new(path: HivePath) -> ColmenaResult { let context_dir = path.context_dir(); - let assets = Assets::new(path.is_flake()).await?; + let assets = Assets::new(path.clone()).await?; Ok(Self { path, @@ -127,6 +127,7 @@ impl Hive { pub fn nix_options(&self) -> NixOptions { let mut options = NixOptions::default(); options.set_show_trace(self.show_trace); + options.set_pure_eval(self.path.is_flake()); options } @@ -361,7 +362,7 @@ impl Hive { /// Returns the base expression from which the evaluated Hive can be used. fn get_base_expression(&self) -> String { - self.assets.get_base_expression(self.path()) + self.assets.get_base_expression() } /// Returns whether this Hive is a flake. diff --git a/src/nix/mod.rs b/src/nix/mod.rs index 6f6018c..284ab87 100644 --- a/src/nix/mod.rs +++ b/src/nix/mod.rs @@ -97,6 +97,12 @@ pub struct NixOptions { /// Whether to pass --show-trace. show_trace: bool, + /// Whether to pass --pure-eval. + pure_eval: bool, + + /// Whether to pass --impure. + impure: bool, + /// Designated builders. /// /// See . @@ -187,6 +193,10 @@ impl NixOptions { self.show_trace = show_trace; } + pub fn set_pure_eval(&mut self, pure_eval: bool) { + self.pure_eval = pure_eval; + } + pub fn set_builders(&mut self, builders: Option) { self.builders = builders; } @@ -206,6 +216,14 @@ impl NixOptions { options.push("--show-trace".to_string()); } + if self.pure_eval { + options.push("--pure-eval".to_string()); + } + + if self.impure { + options.push("--impure".to_string()); + } + options } }