Make flake evaluation pure

This seems to be the easiest way to get pure-eval working with
existing evaluation mechinery (nix-instantiate, nix-eval-jobs).

Now `--pure-eval` is forced for flakes with user being able to
add `--impure` as needed.
This commit is contained in:
Zhaofeng Li 2022-08-16 20:15:43 -06:00
parent 092e5848ab
commit 8aca525788
7 changed files with 70 additions and 29 deletions

View file

@ -2,6 +2,7 @@
## Release 0.4.0 (Unreleased) ## 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. - `--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)). - 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)). - In `apply-local`, we now only escalate privileges during activation ([#85](https://github.com/zhaofengli/colmena/issues/85)).

View file

@ -10,7 +10,7 @@ use tokio::process::Command;
use super::{ColmenaError, ColmenaResult, NixCheck}; use super::{ColmenaError, ColmenaResult, NixCheck};
/// A Nix Flake. /// A Nix Flake.
#[derive(Debug)] #[derive(Debug, Clone)]
pub struct Flake { pub struct Flake {
/// The flake metadata. /// The flake metadata.
metadata: FlakeMetadata, metadata: FlakeMetadata,
@ -20,7 +20,7 @@ pub struct Flake {
} }
/// A `nix flake metadata --json` invocation. /// A `nix flake metadata --json` invocation.
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug, Clone)]
struct FlakeMetadata { struct FlakeMetadata {
/// The resolved URL of the flake. /// The resolved URL of the flake.
#[serde(rename = "resolvedUrl")] #[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(())
}

View file

@ -13,8 +13,9 @@ use tempfile::TempDir;
use super::{Flake, HivePath}; use super::{Flake, HivePath};
use crate::error::ColmenaResult; 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 EVAL_NIX: &[u8] = include_bytes!("eval.nix");
const OPTIONS_NIX: &[u8] = include_bytes!("options.nix"); const OPTIONS_NIX: &[u8] = include_bytes!("options.nix");
const MODULES_NIX: &[u8] = include_bytes!("modules.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. /// Static files required to evaluate a Hive configuration.
#[derive(Debug)] #[derive(Debug)]
pub(super) struct Assets { pub(super) struct Assets {
/// Path to the hive being evaluated.
hive_path: HivePath,
/// Temporary directory holding the files. /// Temporary directory holding the files.
temp_dir: TempDir, temp_dir: TempDir,
@ -30,7 +34,7 @@ pub(super) struct Assets {
} }
impl Assets { impl Assets {
pub async fn new(flake: bool) -> ColmenaResult<Self> { pub async fn new(hive_path: HivePath) -> ColmenaResult<Self> {
let temp_dir = TempDir::new().unwrap(); let temp_dir = TempDir::new().unwrap();
create_file(&temp_dir, "eval.nix", false, EVAL_NIX)?; create_file(&temp_dir, "eval.nix", false, EVAL_NIX)?;
@ -39,27 +43,30 @@ impl Assets {
let mut assets_flake_uri = None; 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 // 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 // We explicitly specify `path:` instead of letting Nix resolve
// automatically, which would involve checking parent directories // automatically, which would involve checking parent directories
// for a git repository. // for a git repository.
let uri = format!("path:{}", temp_dir.path().to_str().unwrap()); let uri = format!("path:{}", temp_dir.path().to_str().unwrap());
let _ = lock_flake_quiet(&uri).await;
let assets_flake = Flake::from_uri(uri).await?; let assets_flake = Flake::from_uri(uri).await?;
assets_flake_uri = Some(assets_flake.locked_uri().to_owned()); assets_flake_uri = Some(assets_flake.locked_uri().to_owned());
} }
Ok(Self { Ok(Self {
hive_path,
temp_dir, temp_dir,
assets_flake_uri, assets_flake_uri,
}) })
} }
/// Returns the base expression from which the evaluated Hive can be used. /// Returns the base expression from which the evaluated Hive can be used.
pub fn get_base_expression(&self, hive_path: &HivePath) -> String { pub fn get_base_expression(&self) -> String {
match hive_path { match &self.hive_path {
HivePath::Legacy(path) => { HivePath::Legacy(path) => {
format!( format!(
"with builtins; let eval = import {eval_nix}; hive = eval {{ rawHive = import {path}; colmenaOptions = import {options_nix}; colmenaModules = import {modules_nix}; }}; in ", "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"), modules_nix = self.get_path("modules.nix"),
) )
} }
HivePath::Flake(flake) => { HivePath::Flake(_) => {
format!( 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"), assets_flake_uri = self.assets_flake_uri.as_ref().expect("The assets flake must have been initialized"),
flake_uri = flake.locked_uri(),
) )
} }
} }

View file

@ -1,6 +1,6 @@
{ rawHive ? null # Colmena Hive attrset { rawHive ? null # Colmena Hive attrset
, flakeUri ? null # Nix Flake URI with `outputs.colmena` , rawFlake ? null # Nix Flake attrset with `outputs.colmena`
, hermetic ? flakeUri != null # Whether we are allowed to use <nixpkgs> , hermetic ? rawFlake != null # Whether we are allowed to use <nixpkgs>
, colmenaOptions , colmenaOptions
, colmenaModules , colmenaModules
}: }:
@ -19,10 +19,8 @@ let
uncheckedHive = let uncheckedHive = let
flakeToHive = flakeUri: let flakeToHive = rawFlake:
flake = builtins.getFlake flakeUri; if rawFlake.outputs ? colmena then rawFlake.outputs.colmena else throw "Flake must define outputs.colmena.";
hive = if flake.outputs ? colmena then flake.outputs.colmena else throw "Flake must define outputs.colmena.";
in hive;
rawToHive = rawHive: rawToHive = rawHive:
if typeOf rawHive == "lambda" || rawHive ? __functor then rawHive {} if typeOf rawHive == "lambda" || rawHive ? __functor then rawHive {}
@ -30,8 +28,8 @@ let
else throw "The config must evaluate to an attribute set."; else throw "The config must evaluate to an attribute set.";
in in
if rawHive != null then rawToHive rawHive if rawHive != null then rawToHive rawHive
else if flakeUri != null then flakeToHive flakeUri else if rawFlake != null then flakeToHive rawFlake
else throw "Either an attribute set or a flake URI must be specified."; else throw "Either a plain Hive attribute set or a Nix Flake attribute set must be specified.";
uncheckedUserMeta = uncheckedUserMeta =
if uncheckedHive ? meta && uncheckedHive ? network then if uncheckedHive ? meta && uncheckedHive ? network then

View file

@ -1,13 +1,13 @@
{ {
description = "Internal Colmena expressions"; description = "Internal Colmena expressions";
outputs = { ... }: { inputs = {
lib.colmenaEval = { hive.url = "%hive%";
rawHive ? null, };
flakeUri ? null,
hermetic ? flakeUri != null, outputs = { self, hive }: {
}: import ./eval.nix { colmenaEval = import ./eval.nix {
inherit rawHive flakeUri hermetic; rawFlake = hive;
colmenaOptions = import ./options.nix; colmenaOptions = import ./options.nix;
colmenaModules = import ./modules.nix; colmenaModules = import ./modules.nix;
}; };

View file

@ -21,7 +21,7 @@ use crate::job::JobHandle;
use crate::util::{CommandExecution, CommandExt}; use crate::util::{CommandExecution, CommandExt};
use assets::Assets; use assets::Assets;
#[derive(Debug)] #[derive(Debug, Clone)]
pub enum HivePath { pub enum HivePath {
/// A Nix Flake. /// A Nix Flake.
/// ///
@ -93,7 +93,7 @@ impl HivePath {
impl Hive { impl Hive {
pub async fn new(path: HivePath) -> ColmenaResult<Self> { pub async fn new(path: HivePath) -> ColmenaResult<Self> {
let context_dir = path.context_dir(); let context_dir = path.context_dir();
let assets = Assets::new(path.is_flake()).await?; let assets = Assets::new(path.clone()).await?;
Ok(Self { Ok(Self {
path, path,
@ -127,6 +127,7 @@ impl Hive {
pub fn nix_options(&self) -> NixOptions { pub fn nix_options(&self) -> NixOptions {
let mut options = NixOptions::default(); let mut options = NixOptions::default();
options.set_show_trace(self.show_trace); options.set_show_trace(self.show_trace);
options.set_pure_eval(self.path.is_flake());
options options
} }
@ -361,7 +362,7 @@ impl Hive {
/// Returns the base expression from which the evaluated Hive can be used. /// Returns the base expression from which the evaluated Hive can be used.
fn get_base_expression(&self) -> String { 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. /// Returns whether this Hive is a flake.

View file

@ -97,6 +97,12 @@ pub struct NixOptions {
/// Whether to pass --show-trace. /// Whether to pass --show-trace.
show_trace: bool, show_trace: bool,
/// Whether to pass --pure-eval.
pure_eval: bool,
/// Whether to pass --impure.
impure: bool,
/// Designated builders. /// Designated builders.
/// ///
/// See <https://nixos.org/manual/nix/stable/advanced-topics/distributed-builds.html>. /// See <https://nixos.org/manual/nix/stable/advanced-topics/distributed-builds.html>.
@ -187,6 +193,10 @@ impl NixOptions {
self.show_trace = show_trace; 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<String>) { pub fn set_builders(&mut self, builders: Option<String>) {
self.builders = builders; self.builders = builders;
} }
@ -206,6 +216,14 @@ impl NixOptions {
options.push("--show-trace".to_string()); 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 options
} }
} }