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)
- 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)).

View file

@ -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(())
}

View file

@ -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<Self> {
pub async fn new(hive_path: HivePath) -> ColmenaResult<Self> {
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(),
)
}
}

View file

@ -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 <nixpkgs>
, rawFlake ? null # Nix Flake attrset with `outputs.colmena`
, hermetic ? rawFlake != null # Whether we are allowed to use <nixpkgs>
, 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

View file

@ -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;
};

View file

@ -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<Self> {
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.

View file

@ -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 <https://nixos.org/manual/nix/stable/advanced-topics/distributed-builds.html>.
@ -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<String>) {
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
}
}