From 0e0a1e84f041d1a58a97567d41da24a47feca677 Mon Sep 17 00:00:00 2001 From: Zhaofeng Li Date: Mon, 25 Oct 2021 23:38:10 -0700 Subject: [PATCH] Make flake resolution (slightly) less terrible Instead of using `path:` which always copies the entire directory, we now try to resolve the Flake URI using `nix flake metadata` which may give us a `git+file:`. --- src/command/apply.rs | 2 +- src/command/apply_local.rs | 2 +- src/command/exec.rs | 2 +- src/command/introspect.rs | 2 +- src/nix/flake.rs | 92 ++++++++++++++++++++++++++++++++++++++ src/nix/hive.rs | 48 +++++++------------- src/nix/info.rs | 14 +++++- src/nix/mod.rs | 3 ++ src/nix/tests/mod.rs | 11 +++-- src/util.rs | 9 ++-- 10 files changed, 137 insertions(+), 48 deletions(-) create mode 100644 src/nix/flake.rs diff --git a/src/command/apply.rs b/src/command/apply.rs index 48da56c..d04dbf3 100644 --- a/src/command/apply.rs +++ b/src/command/apply.rs @@ -115,7 +115,7 @@ pub fn subcommand() -> App<'static, 'static> { } pub async fn run(_global_args: &ArgMatches<'_>, local_args: &ArgMatches<'_>) { - let hive = util::hive_from_args(local_args).unwrap(); + let hive = util::hive_from_args(local_args).await.unwrap(); log::info!("Enumerating nodes..."); let all_nodes = hive.deployment_info().await.unwrap(); diff --git a/src/command/apply_local.rs b/src/command/apply_local.rs index d7a3af0..3c28570 100644 --- a/src/command/apply_local.rs +++ b/src/command/apply_local.rs @@ -89,7 +89,7 @@ pub async fn run(_global_args: &ArgMatches<'_>, local_args: &ArgMatches<'_>) { } } - let hive = util::hive_from_args(local_args).unwrap(); + let hive = util::hive_from_args(local_args).await.unwrap(); let hostname = if local_args.is_present("node") { local_args.value_of("node").unwrap().to_owned() } else { diff --git a/src/command/exec.rs b/src/command/exec.rs index a878e6f..eeda5c0 100644 --- a/src/command/exec.rs +++ b/src/command/exec.rs @@ -55,7 +55,7 @@ It's recommended to use -- to separate Colmena options from the command to run. } pub async fn run(_global_args: &ArgMatches<'_>, local_args: &ArgMatches<'_>) { - let hive = util::hive_from_args(local_args).unwrap(); + let hive = util::hive_from_args(local_args).await.unwrap(); log::info!("Enumerating nodes..."); let all_nodes = hive.deployment_info().await.unwrap(); diff --git a/src/command/introspect.rs b/src/command/introspect.rs index cf00d1a..b46587a 100644 --- a/src/command/introspect.rs +++ b/src/command/introspect.rs @@ -28,7 +28,7 @@ For example, to retrieve the configuration of one node, you may write something } pub async fn run(_global_args: &ArgMatches<'_>, local_args: &ArgMatches<'_>) { - let hive = util::hive_from_args(local_args).unwrap(); + let hive = util::hive_from_args(local_args).await.unwrap(); if !(local_args.is_present("expression") ^ local_args.is_present("expression_file")) { log::error!("Either an expression (-E) or a .nix file containing an expression should be specified, not both."); diff --git a/src/nix/flake.rs b/src/nix/flake.rs new file mode 100644 index 0000000..aa47916 --- /dev/null +++ b/src/nix/flake.rs @@ -0,0 +1,92 @@ +//! Nix Flake utilities. + +use std::convert::AsRef; +use std::path::{Path, PathBuf}; +use std::process::Stdio; + +use serde::Deserialize; +use tokio::process::Command; + +use super::{NixCheck, NixError, NixResult}; + +/// A Nix Flake. +#[derive(Debug)] +pub struct Flake { + /// The Flake URI. + uri: String, + + /// The directory the flake lives in, if it's a local flake. + local_dir: Option, +} + +impl Flake { + /// Creates a flake from the given directory. + /// + /// This will try to retrieve the resolved URL of the local flake + /// in the specified directory. + pub async fn from_dir>(dir: P) -> NixResult { + NixCheck::require_flake_support().await?; + + let flake = dir.as_ref().as_os_str().to_str() + .expect("Flake directory path contains non-UTF-8 characters"); + + let info = FlakeMetadata::resolve(flake).await?; + + Ok(Self { + uri: info.resolved_url, + local_dir: Some(dir.as_ref().to_owned()), + }) + } + + /// Creates a flake from a Flake URI. + pub async fn from_uri(uri: String) -> NixResult { + NixCheck::require_flake_support().await?; + + Ok(Self { + uri, + local_dir: None, + }) + } + + /// Returns the URI. + pub fn uri(&self) -> &str { + &self.uri + } + + /// Returns the local directory, if it exists. + pub fn local_dir(&self) -> Option<&Path> { + self.local_dir.as_ref().map(|d| d.as_path()) + } +} + +/// A `nix flake metadata --json` invocation. +#[derive(Deserialize, Debug)] +struct FlakeMetadata { + /// The resolved URL of the flake. + #[serde(rename = "resolvedUrl")] + resolved_url: String, +} + +impl FlakeMetadata { + /// Resolves a flake. + async fn resolve(flake: &str) -> NixResult { + let child = Command::new("nix") + .args(&["flake", "metadata", "--json"]) + .args(&["--experimental-features", "nix-command flakes"]) + .arg(flake) + .stdout(Stdio::piped()) + .spawn()?; + + let output = child.wait_with_output().await?; + + if !output.status.success() { + return Err(output.status.into()); + } + + serde_json::from_slice::(&output.stdout) + .map_err(|_| { + let output = String::from_utf8_lossy(&output.stdout).to_string(); + NixError::BadOutput { output } + }) + } +} diff --git a/src/nix/hive.rs b/src/nix/hive.rs index a1cb734..d4d3aed 100644 --- a/src/nix/hive.rs +++ b/src/nix/hive.rs @@ -10,13 +10,13 @@ use serde::Serialize; use validator::Validate; use super::{ + Flake, StoreDerivation, NixResult, - NixError, NodeConfig, ProfileMap, }; -use super::{NixCommand, NixCheck}; +use super::NixCommand; use crate::util::CommandExecution; use crate::progress::TaskProgress; @@ -24,49 +24,38 @@ const HIVE_EVAL: &'static [u8] = include_bytes!("eval.nix"); #[derive(Debug)] pub enum HivePath { - /// A Nix Flake URI. + /// A Nix Flake. /// /// The flake must contain the `colmena` output. - Flake(String), + Flake(Flake), /// A regular .nix file Legacy(PathBuf), } impl HivePath { - pub fn from_path>(path: P) -> Self { + pub async fn from_path>(path: P) -> NixResult { let path = path.as_ref(); if let Some(osstr) = path.file_name() { if osstr == "flake.nix" { - let parent = path.parent().unwrap().canonicalize().unwrap(); - let parent = parent.to_str().unwrap(); - let uri = format!("path:{}", parent); - - return Self::Flake(uri); + let parent = path.parent().unwrap(); + let flake = Flake::from_dir(parent).await?; + return Ok(Self::Flake(flake)); } } - Self::Legacy(path.to_owned()) + Ok(Self::Legacy(path.to_owned())) } fn context_dir(&self) -> Option { match self { Self::Legacy(p) => { - if let Some(parent) = p.parent() { - return Some(parent.to_owned()); - } - None + p.parent().map(|d| d.to_owned()) + } + Self::Flake(flake) => { + flake.local_dir().map(|d| d.to_owned()) } - _ => None, - } - } - - fn is_flake(&self) -> bool { - if let Self::Flake(_) = self { - true - } else { - false } } } @@ -128,13 +117,6 @@ impl Hive { /// Retrieve deployment info for all nodes. pub async fn deployment_info(&self) -> NixResult> { - let nix_check = NixCheck::detect().await; - - if self.path.is_flake() && !nix_check.flakes_supported() { - nix_check.print_flakes_info(true); - return Err(NixError::NoFlakesSupport); - } - // FIXME: Really ugly :( let s: String = self.nix_instantiate("hive.deploymentConfigJson").eval_with_builders().await? .capture_json().await?; @@ -281,7 +263,7 @@ impl<'hive> NixInstantiate<'hive> { self.expression, )); } - HivePath::Flake(uri) => { + HivePath::Flake(flake) => { command .args(&["--experimental-features", "flakes"]) .arg("--no-gc-warning") @@ -289,7 +271,7 @@ impl<'hive> NixInstantiate<'hive> { .arg(format!( "with builtins; let eval = import {}; hive = eval {{ flakeUri = \"{}\"; }}; in {}", self.hive.eval_nix.to_str().unwrap(), - &uri, + flake.uri(), self.expression, )); } diff --git a/src/nix/info.rs b/src/nix/info.rs index 464c496..a314042 100644 --- a/src/nix/info.rs +++ b/src/nix/info.rs @@ -3,9 +3,10 @@ use std::process::Stdio; use log::Level; use regex::Regex; - use tokio::process::Command; +use super::{NixError, NixResult}; + struct NixVersion { major: usize, minor: usize, @@ -88,6 +89,17 @@ impl NixCheck { } } + pub async fn require_flake_support() -> NixResult<()> { + let check = Self::detect().await; + + if !check.flakes_supported() { + check.print_flakes_info(true); + Err(NixError::NoFlakesSupport) + } else { + Ok(()) + } + } + pub fn print_version_info(&self) { if let Some(v) = &self.version { log::info!("Nix Version: {}", v); diff --git a/src/nix/mod.rs b/src/nix/mod.rs index 8249217..413a6b5 100644 --- a/src/nix/mod.rs +++ b/src/nix/mod.rs @@ -36,6 +36,9 @@ pub use deployment::{Goal, Target, Deployment}; pub mod info; pub use info::NixCheck; +pub mod flake; +pub use flake::Flake; + #[cfg(test)] mod tests; diff --git a/src/nix/tests/mod.rs b/src/nix/tests/mod.rs index 82068c1..5694851 100644 --- a/src/nix/tests/mod.rs +++ b/src/nix/tests/mod.rs @@ -34,7 +34,7 @@ impl TempHive { let mut temp_file = NamedTempFile::new().unwrap(); temp_file.write_all(text.as_bytes()).unwrap(); - let hive_path = HivePath::from_path(temp_file.path()); + let hive_path = block_on(HivePath::from_path(temp_file.path())).unwrap(); let hive = Hive::new(hive_path).unwrap(); Self { @@ -149,11 +149,10 @@ fn test_parse_simple() { #[test] fn test_parse_flake() { - let flake_path = { - let p = PathBuf::from("./src/nix/tests/simple-flake"); - p.canonicalize().unwrap() - }; - let hive_path = HivePath::Flake(format!("path:{}", flake_path.to_str().unwrap())); + let flake_dir = PathBuf::from("./src/nix/tests/simple-flake"); + let flake = block_on(Flake::from_dir(flake_dir)).unwrap(); + + let hive_path = HivePath::Flake(flake); let mut hive = Hive::new(hive_path).unwrap(); hive.set_show_trace(true); diff --git a/src/util.rs b/src/util.rs index 3b5c8ea..33bbce9 100644 --- a/src/util.rs +++ b/src/util.rs @@ -9,7 +9,7 @@ use glob::Pattern as GlobPattern; use tokio::io::{AsyncRead, AsyncBufReadExt, BufReader}; use tokio::process::Command; -use super::nix::{NodeConfig, Hive, HivePath, NixResult}; +use super::nix::{Flake, NodeConfig, Hive, HivePath, NixResult}; use super::progress::TaskProgress; enum NodeFilter { @@ -79,7 +79,7 @@ impl CommandExecution { } } -pub fn hive_from_args(args: &ArgMatches<'_>) -> NixResult { +pub async fn hive_from_args(args: &ArgMatches<'_>) -> NixResult { let path = match args.occurrences_of("config") { 0 => { // traverse upwards until we find hive.nix @@ -142,7 +142,8 @@ pub fn hive_from_args(args: &ArgMatches<'_>) -> NixResult { if !fpath.exists() && path.contains(":") { // Treat as flake URI - let hive_path = HivePath::Flake(path); + let flake = Flake::from_uri(path).await?; + let hive_path = HivePath::Flake(flake); let mut hive = Hive::new(hive_path)?; if args.is_present("show-trace") { @@ -156,7 +157,7 @@ pub fn hive_from_args(args: &ArgMatches<'_>) -> NixResult { } }; - let hive_path = HivePath::from_path(path); + let hive_path = HivePath::from_path(path).await?; let mut hive = Hive::new(hive_path)?; if args.is_present("show-trace") {