diff --git a/src/nix/expression.rs b/src/nix/expression.rs new file mode 100644 index 0000000..96f74f3 --- /dev/null +++ b/src/nix/expression.rs @@ -0,0 +1,73 @@ +//! Nix expression serializer. + +use serde::Serialize; + +/// A Nix expression. +pub trait NixExpression: Send + Sync { + /// Returns the full Nix expression to be evaluated. + fn expression(&self) -> String; + + /// Returns whether this expression requires the use of flakes. + fn requires_flakes(&self) -> bool { + false + } +} + +/// A serialized Nix expression. +pub struct SerializedNixExpression(String); + +impl NixExpression for String { + fn expression(&self) -> String { + self.clone() + } +} + +impl SerializedNixExpression { + pub fn new(data: T) -> Self + where + T: Serialize, + { + let json = serde_json::to_string(&data).expect("Could not serialize data"); + let quoted = nix_quote(&json); + + Self(quoted) + } +} + +impl NixExpression for SerializedNixExpression { + fn expression(&self) -> String { + format!("(builtins.fromJSON {})", &self.0) + } +} + +/// Turns a string into a quoted Nix string expression. +fn nix_quote(s: &str) -> String { + let inner = s + .replace('\\', r#"\\"#) + .replace('"', r#"\""#) + .replace("${", r#"\${"#); + + format!("\"{}\"", inner) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_nix_quote() { + let cases = [ + (r#"["a", "b"]"#, r#""[\"a\", \"b\"]""#), + ( + r#"["\"a\"", "\"b\""]"#, + r#""[\"\\\"a\\\"\", \"\\\"b\\\"\"]""#, + ), + (r#"${dontExpandMe}"#, r#""\${dontExpandMe}""#), + (r#"\${dontExpandMe}"#, r#""\\\${dontExpandMe}""#), + ]; + + for (orig, quoted) in cases { + assert_eq!(quoted, nix_quote(orig)); + } + } +} diff --git a/src/nix/hive/mod.rs b/src/nix/hive/mod.rs index 3636989..2f8e85f 100644 --- a/src/nix/hive/mod.rs +++ b/src/nix/hive/mod.rs @@ -5,11 +5,8 @@ mod tests; use std::collections::HashMap; use std::convert::AsRef; -use std::io::Write; use std::path::{Path, PathBuf}; -use serde::Serialize; -use tempfile::{NamedTempFile, TempPath}; use tokio::process::Command; use tokio::sync::OnceCell; use validator::Validate; @@ -17,7 +14,7 @@ use validator::Validate; use super::deployment::TargetNode; use super::{ Flake, MetaConfig, NixExpression, NixOptions, NodeConfig, NodeFilter, NodeName, - ProfileDerivation, StorePath, + ProfileDerivation, SerializedNixExpression, StorePath, }; use crate::error::ColmenaResult; use crate::job::JobHandle; @@ -60,15 +57,6 @@ struct NixInstantiate<'hive> { expression: String, } -/// A serialized Nix expression. -/// -/// Very hacky so should be avoided as much as possible. But I suppose it's -/// more robust than attempting to generate Nix expressions directly or -/// escaping a JSON string to strip off Nix interpolation. -struct SerializedNixExpression { - json_file: TempPath, -} - /// An expression to evaluate the system profiles of selected nodes. struct EvalSelectedExpression<'hive> { hive: &'hive Hive, @@ -290,7 +278,7 @@ impl Hive { &self, nodes: &[NodeName], ) -> ColmenaResult> { - let nodes_expr = SerializedNixExpression::new(nodes)?; + let nodes_expr = SerializedNixExpression::new(nodes); let configs: HashMap = self .nix_instantiate(&format!( @@ -322,7 +310,7 @@ impl Hive { nodes: &[NodeName], job: Option, ) -> ColmenaResult> { - let nodes_expr = SerializedNixExpression::new(nodes)?; + let nodes_expr = SerializedNixExpression::new(nodes); let expr = format!("hive.evalSelectedDrvPaths {}", nodes_expr.expression()); @@ -344,7 +332,7 @@ impl Hive { /// Returns the expression to evaluate selected nodes. pub fn eval_selected_expr(&self, nodes: &[NodeName]) -> ColmenaResult { - let nodes_expr = SerializedNixExpression::new(nodes)?; + let nodes_expr = SerializedNixExpression::new(nodes); Ok(EvalSelectedExpression { hive: self, @@ -446,30 +434,6 @@ impl<'hive> NixInstantiate<'hive> { } } -impl SerializedNixExpression { - pub fn new(data: T) -> ColmenaResult - where - T: Serialize, - { - let mut tmp = NamedTempFile::new()?; - let json = serde_json::to_vec(&data).expect("Could not serialize data"); - tmp.write_all(&json)?; - - Ok(Self { - json_file: tmp.into_temp_path(), - }) - } -} - -impl NixExpression for SerializedNixExpression { - fn expression(&self) -> String { - format!( - "(builtins.fromJSON (builtins.readFile {}))", - self.json_file.to_str().unwrap() - ) - } -} - impl<'hive> NixExpression for EvalSelectedExpression<'hive> { fn expression(&self) -> String { format!( diff --git a/src/nix/mod.rs b/src/nix/mod.rs index ca017e5..6f6018c 100644 --- a/src/nix/mod.rs +++ b/src/nix/mod.rs @@ -39,6 +39,9 @@ pub use node_filter::NodeFilter; pub mod evaluator; +pub mod expression; +pub use expression::{NixExpression, SerializedNixExpression}; + /// Path to the main system profile. pub const SYSTEM_PROFILE: &str = "/nix/var/nix/profiles/system"; @@ -104,17 +107,6 @@ pub struct NixOptions { builders: Option, } -/// A Nix expression. -pub trait NixExpression: Send + Sync { - /// Returns the full Nix expression to be evaluated. - fn expression(&self) -> String; - - /// Returns whether this expression requires the use of flakes. - fn requires_flakes(&self) -> bool { - false - } -} - impl NodeName { /// Returns the string. pub fn as_str(&self) -> &str { @@ -218,12 +210,6 @@ impl NixOptions { } } -impl NixExpression for String { - fn expression(&self) -> String { - self.clone() - } -} - fn validate_keys(keys: &HashMap) -> Result<(), ValidationErrorType> { // Bad secret names: // - /etc/passwd