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:`.
This commit is contained in:
Zhaofeng Li 2021-10-25 23:38:10 -07:00
parent b48753239a
commit 0e0a1e84f0
10 changed files with 137 additions and 48 deletions

View file

@ -115,7 +115,7 @@ pub fn subcommand() -> App<'static, 'static> {
} }
pub async fn run(_global_args: &ArgMatches<'_>, local_args: &ArgMatches<'_>) { 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..."); log::info!("Enumerating nodes...");
let all_nodes = hive.deployment_info().await.unwrap(); let all_nodes = hive.deployment_info().await.unwrap();

View file

@ -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") { let hostname = if local_args.is_present("node") {
local_args.value_of("node").unwrap().to_owned() local_args.value_of("node").unwrap().to_owned()
} else { } else {

View file

@ -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<'_>) { 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..."); log::info!("Enumerating nodes...");
let all_nodes = hive.deployment_info().await.unwrap(); let all_nodes = hive.deployment_info().await.unwrap();

View file

@ -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<'_>) { 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")) { 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."); log::error!("Either an expression (-E) or a .nix file containing an expression should be specified, not both.");

92
src/nix/flake.rs Normal file
View file

@ -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<PathBuf>,
}
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<P: AsRef<Path>>(dir: P) -> NixResult<Self> {
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<Self> {
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<Self> {
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::<FlakeMetadata>(&output.stdout)
.map_err(|_| {
let output = String::from_utf8_lossy(&output.stdout).to_string();
NixError::BadOutput { output }
})
}
}

View file

@ -10,13 +10,13 @@ use serde::Serialize;
use validator::Validate; use validator::Validate;
use super::{ use super::{
Flake,
StoreDerivation, StoreDerivation,
NixResult, NixResult,
NixError,
NodeConfig, NodeConfig,
ProfileMap, ProfileMap,
}; };
use super::{NixCommand, NixCheck}; use super::NixCommand;
use crate::util::CommandExecution; use crate::util::CommandExecution;
use crate::progress::TaskProgress; use crate::progress::TaskProgress;
@ -24,49 +24,38 @@ const HIVE_EVAL: &'static [u8] = include_bytes!("eval.nix");
#[derive(Debug)] #[derive(Debug)]
pub enum HivePath { pub enum HivePath {
/// A Nix Flake URI. /// A Nix Flake.
/// ///
/// The flake must contain the `colmena` output. /// The flake must contain the `colmena` output.
Flake(String), Flake(Flake),
/// A regular .nix file /// A regular .nix file
Legacy(PathBuf), Legacy(PathBuf),
} }
impl HivePath { impl HivePath {
pub fn from_path<P: AsRef<Path>>(path: P) -> Self { pub async fn from_path<P: AsRef<Path>>(path: P) -> NixResult<Self> {
let path = path.as_ref(); let path = path.as_ref();
if let Some(osstr) = path.file_name() { if let Some(osstr) = path.file_name() {
if osstr == "flake.nix" { if osstr == "flake.nix" {
let parent = path.parent().unwrap().canonicalize().unwrap(); let parent = path.parent().unwrap();
let parent = parent.to_str().unwrap(); let flake = Flake::from_dir(parent).await?;
let uri = format!("path:{}", parent); return Ok(Self::Flake(flake));
return Self::Flake(uri);
} }
} }
Self::Legacy(path.to_owned()) Ok(Self::Legacy(path.to_owned()))
} }
fn context_dir(&self) -> Option<PathBuf> { fn context_dir(&self) -> Option<PathBuf> {
match self { match self {
Self::Legacy(p) => { Self::Legacy(p) => {
if let Some(parent) = p.parent() { p.parent().map(|d| d.to_owned())
return Some(parent.to_owned()); }
} Self::Flake(flake) => {
None 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. /// Retrieve deployment info for all nodes.
pub async fn deployment_info(&self) -> NixResult<HashMap<String, NodeConfig>> { pub async fn deployment_info(&self) -> NixResult<HashMap<String, NodeConfig>> {
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 :( // FIXME: Really ugly :(
let s: String = self.nix_instantiate("hive.deploymentConfigJson").eval_with_builders().await? let s: String = self.nix_instantiate("hive.deploymentConfigJson").eval_with_builders().await?
.capture_json().await?; .capture_json().await?;
@ -281,7 +263,7 @@ impl<'hive> NixInstantiate<'hive> {
self.expression, self.expression,
)); ));
} }
HivePath::Flake(uri) => { HivePath::Flake(flake) => {
command command
.args(&["--experimental-features", "flakes"]) .args(&["--experimental-features", "flakes"])
.arg("--no-gc-warning") .arg("--no-gc-warning")
@ -289,7 +271,7 @@ impl<'hive> NixInstantiate<'hive> {
.arg(format!( .arg(format!(
"with builtins; let eval = import {}; hive = eval {{ flakeUri = \"{}\"; }}; in {}", "with builtins; let eval = import {}; hive = eval {{ flakeUri = \"{}\"; }}; in {}",
self.hive.eval_nix.to_str().unwrap(), self.hive.eval_nix.to_str().unwrap(),
&uri, flake.uri(),
self.expression, self.expression,
)); ));
} }

View file

@ -3,9 +3,10 @@ use std::process::Stdio;
use log::Level; use log::Level;
use regex::Regex; use regex::Regex;
use tokio::process::Command; use tokio::process::Command;
use super::{NixError, NixResult};
struct NixVersion { struct NixVersion {
major: usize, major: usize,
minor: 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) { pub fn print_version_info(&self) {
if let Some(v) = &self.version { if let Some(v) = &self.version {
log::info!("Nix Version: {}", v); log::info!("Nix Version: {}", v);

View file

@ -36,6 +36,9 @@ pub use deployment::{Goal, Target, Deployment};
pub mod info; pub mod info;
pub use info::NixCheck; pub use info::NixCheck;
pub mod flake;
pub use flake::Flake;
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;

View file

@ -34,7 +34,7 @@ impl TempHive {
let mut temp_file = NamedTempFile::new().unwrap(); let mut temp_file = NamedTempFile::new().unwrap();
temp_file.write_all(text.as_bytes()).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(); let hive = Hive::new(hive_path).unwrap();
Self { Self {
@ -149,11 +149,10 @@ fn test_parse_simple() {
#[test] #[test]
fn test_parse_flake() { fn test_parse_flake() {
let flake_path = { let flake_dir = PathBuf::from("./src/nix/tests/simple-flake");
let p = PathBuf::from("./src/nix/tests/simple-flake"); let flake = block_on(Flake::from_dir(flake_dir)).unwrap();
p.canonicalize().unwrap()
}; let hive_path = HivePath::Flake(flake);
let hive_path = HivePath::Flake(format!("path:{}", flake_path.to_str().unwrap()));
let mut hive = Hive::new(hive_path).unwrap(); let mut hive = Hive::new(hive_path).unwrap();
hive.set_show_trace(true); hive.set_show_trace(true);

View file

@ -9,7 +9,7 @@ use glob::Pattern as GlobPattern;
use tokio::io::{AsyncRead, AsyncBufReadExt, BufReader}; use tokio::io::{AsyncRead, AsyncBufReadExt, BufReader};
use tokio::process::Command; use tokio::process::Command;
use super::nix::{NodeConfig, Hive, HivePath, NixResult}; use super::nix::{Flake, NodeConfig, Hive, HivePath, NixResult};
use super::progress::TaskProgress; use super::progress::TaskProgress;
enum NodeFilter { enum NodeFilter {
@ -79,7 +79,7 @@ impl CommandExecution {
} }
} }
pub fn hive_from_args(args: &ArgMatches<'_>) -> NixResult<Hive> { pub async fn hive_from_args(args: &ArgMatches<'_>) -> NixResult<Hive> {
let path = match args.occurrences_of("config") { let path = match args.occurrences_of("config") {
0 => { 0 => {
// traverse upwards until we find hive.nix // traverse upwards until we find hive.nix
@ -142,7 +142,8 @@ pub fn hive_from_args(args: &ArgMatches<'_>) -> NixResult<Hive> {
if !fpath.exists() && path.contains(":") { if !fpath.exists() && path.contains(":") {
// Treat as flake URI // 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)?; let mut hive = Hive::new(hive_path)?;
if args.is_present("show-trace") { if args.is_present("show-trace") {
@ -156,7 +157,7 @@ pub fn hive_from_args(args: &ArgMatches<'_>) -> NixResult<Hive> {
} }
}; };
let hive_path = HivePath::from_path(path); let hive_path = HivePath::from_path(path).await?;
let mut hive = Hive::new(hive_path)?; let mut hive = Hive::new(hive_path)?;
if args.is_present("show-trace") { if args.is_present("show-trace") {