diff --git a/src/cli.rs b/src/cli.rs index 7acc241..9068da6 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -10,7 +10,7 @@ use env_logger::fmt::WriteStyle; use crate::{ command::{self, apply::DeployOpts}, error::ColmenaResult, - nix::{Hive, HivePath}, + nix::{hive::EvaluationMethod, Hive, HivePath}, }; /// Base URL of the manual, without the trailing slash. @@ -137,6 +137,21 @@ This only works when building locally. value_names = ["NAME", "VALUE"], )] nix_option: Vec, + #[arg( + long, + default_value_t, + help = "Use direct flake evaluation (experimental)", + long_help = r#"If enabled, flakes will be evaluated using `nix eval`. This requires the flake to depend on Colmena as an input and expose a compatible `colmenaHive` output: + + outputs = { self, colmena, ... }: { + colmenaHive = colmena.lib.makeHive self.outputs.colmena; + colmena = ...; + }; + +This is an experimental feature."#, + global = true + )] + experimental_flake_eval: bool, #[arg( long, value_name = "WHEN", @@ -262,6 +277,11 @@ async fn get_hive(opts: &Opts) -> ColmenaResult { hive.set_impure(true); } + if opts.experimental_flake_eval { + log::warn!("Using direct flake evaluation (experimental)"); + hive.set_evaluation_method(EvaluationMethod::DirectFlakeEval); + } + for chunks in opts.nix_option.chunks_exact(2) { let [name, value] = chunks else { unreachable!() diff --git a/src/nix/hive/mod.rs b/src/nix/hive/mod.rs index f9d9227..2faa88e 100644 --- a/src/nix/hive/mod.rs +++ b/src/nix/hive/mod.rs @@ -8,6 +8,7 @@ use std::convert::AsRef; use std::path::{Path, PathBuf}; use std::str::FromStr; +use const_format::formatcp; use tokio::process::Command; use tokio::sync::OnceCell; use validator::Validate; @@ -22,6 +23,21 @@ use crate::job::JobHandle; use crate::util::{CommandExecution, CommandExt}; use assets::Assets; +/// The version of the Hive schema we are compatible with. +/// +/// Currently we are tied to one specific version. +const HIVE_SCHEMA: &str = "v0.20241006"; + +/// The snippet to be used for `nix eval --apply`. +const FLAKE_APPLY_SNIPPET: &str = formatcp!( + r#"with builtins; hive: assert (hive.__schema == "{}" || throw '' + The colmenaHive output (schema ${{hive.__schema}}) isn't compatible with this version of Colmena. + + Hint: Use the same version of Colmena as in the Flake input. +''); "#, + HIVE_SCHEMA +); + #[derive(Debug, Clone)] pub enum HivePath { /// A Nix Flake. @@ -63,11 +79,33 @@ impl FromStr for HivePath { } } +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum EvaluationMethod { + /// Use nix-instantiate and specify the entire Nix expression. + /// + /// This is the default method. + /// + /// For flakes, we use `builtins.getFlakes`. Pure evaluation no longer works + /// with this method in Nix 2.21+. + NixInstantiate, + + /// Use `nix eval --apply` on top of a flake. + /// + /// This can be activated with --experimental-flake-eval. + /// + /// In this method, we can no longer pull in our bundled assets and + /// the flake must expose a compatible `colmenaHive` output. + DirectFlakeEval, +} + #[derive(Debug)] pub struct Hive { /// Path to the hive. path: HivePath, + /// Method to evaluate the hive with. + evaluation_method: EvaluationMethod, + /// Path to the context directory. /// /// Normally this is directory containing the "hive.nix" @@ -134,6 +172,7 @@ impl Hive { Ok(Self { path, + evaluation_method: EvaluationMethod::NixInstantiate, context_dir, assets, show_trace: false, @@ -158,6 +197,14 @@ impl Hive { .await } + pub fn set_evaluation_method(&mut self, method: EvaluationMethod) { + if !self.is_flake() && method == EvaluationMethod::DirectFlakeEval { + return; + } + + self.evaluation_method = method; + } + pub fn set_show_trace(&mut self, value: bool) { self.show_trace = value; } @@ -421,7 +468,10 @@ 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() + match self.evaluation_method { + EvaluationMethod::NixInstantiate => self.assets.get_base_expression(), + EvaluationMethod::DirectFlakeEval => FLAKE_APPLY_SNIPPET.to_string(), + } } /// Returns whether this Hive is a flake. @@ -444,6 +494,11 @@ impl<'hive> NixInstantiate<'hive> { } fn instantiate(&self) -> Command { + // TODO: Better error handling + if self.hive.evaluation_method == EvaluationMethod::DirectFlakeEval { + panic!("Instantiation is not supported with DirectFlakeEval"); + } + let mut command = Command::new("nix-instantiate"); if self.hive.is_flake() { @@ -462,17 +517,48 @@ impl<'hive> NixInstantiate<'hive> { } fn eval(self) -> Command { - let mut command = self.instantiate(); let flags = self.hive.nix_flags(); - command - .arg("--eval") - .arg("--json") - .arg("--strict") - // Ensures the derivations are instantiated - // Required for system profile evaluation and IFD - .arg("--read-write-mode") - .args(flags.to_args()); - command + + match self.hive.evaluation_method { + EvaluationMethod::NixInstantiate => { + let mut command = self.instantiate(); + + command + .arg("--eval") + .arg("--json") + .arg("--strict") + // Ensures the derivations are instantiated + // Required for system profile evaluation and IFD + .arg("--read-write-mode") + .args(flags.to_args()); + + command + } + EvaluationMethod::DirectFlakeEval => { + let mut command = Command::new("nix"); + let flake = if let HivePath::Flake(flake) = self.hive.path() { + flake + } else { + panic!("The DirectFlakeEval evaluation method only support flakes"); + }; + + let hive_installable = format!("{}#colmenaHive", flake.uri()); + + let mut full_expression = self.hive.get_base_expression(); + full_expression += &self.expression; + + command + .arg("eval") // nix eval + .args(["--extra-experimental-features", "flakes nix-command"]) + .arg(hive_installable) + .arg("--json") + .arg("--apply") + .arg(&full_expression) + .args(flags.to_args()); + + command + } + } } async fn instantiate_with_builders(self) -> ColmenaResult {