use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::convert::Infallible;
use crate::derivation::Derivation;
use crate::store::{StoreHash, StorePath};
pub trait Evaluator {
type Error;
fn evaluate(&self, deriv: &Derivation) -> Result<EvaluationResult, Self::Error>;
fn evaluate_many(&self, derivs: &[Derivation]) -> Result<Vec<EvaluationResult>, Self::Error> {
derivs.iter().map(|d| self.evaluate(d)).collect()
}
fn plan(&self, deriv: &Derivation) -> Result<Plan, Self::Error>;
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EvaluationResult {
pub store_path: StorePath,
pub outputs: BTreeMap<String, StorePath>,
pub log: Vec<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Plan {
pub root: StorePath,
pub edges: Vec<(StorePath, StorePath)>,
pub order: Vec<StorePath>,
}
#[derive(Clone, Copy, Debug, Default)]
pub struct DryRun;
impl Evaluator for DryRun {
type Error = Infallible;
fn evaluate(&self, d: &Derivation) -> Result<EvaluationResult, Self::Error> {
let primary = d.store_path();
let mut outputs = BTreeMap::new();
outputs.insert(d.outputs.primary.clone(), primary.clone());
for extra in &d.outputs.extra {
let extra_hash = StoreHash::of(&(&primary.hash.0, extra));
let extra_path =
StorePath::new(extra_hash, format!("{}-{extra}", d.name), d.version.clone());
outputs.insert(extra.clone(), extra_path);
}
Ok(EvaluationResult {
store_path: primary,
outputs,
log: vec![format!(
"[DryRun] would evaluate {} ({} inputs, {} phases)",
d.name,
d.inputs.len(),
d.builder.phases.len()
)],
})
}
fn plan(&self, d: &Derivation) -> Result<Plan, Self::Error> {
let root = d.store_path();
let mut edges = Vec::new();
let mut order = Vec::new();
for input in &d.inputs {
if let Some(pinned) = &input.pinned {
edges.push((root.clone(), pinned.clone()));
if !order.contains(pinned) {
order.push(pinned.clone());
}
}
}
order.push(root.clone());
Ok(Plan { root, edges, order })
}
}
pub mod adapter_notes {}
#[cfg(test)]
mod tests {
use super::*;
use crate::derivation::{BuilderPhase, BuilderPhases, InputRef};
fn mk(name: &str, version: Option<&str>) -> Derivation {
Derivation {
name: name.into(),
version: version.map(String::from),
inputs: vec![],
source: Default::default(),
builder: Default::default(),
outputs: Default::default(),
env: vec![],
sandbox: Default::default(),
bridge: None,
nix_expr: None,
}
}
#[test]
fn dry_run_produces_deterministic_store_path() {
let d = mk("hello", Some("2.12.1"));
let r1 = DryRun.evaluate(&d).unwrap();
let r2 = DryRun.evaluate(&d).unwrap();
assert_eq!(r1.store_path, r2.store_path);
assert_eq!(r1.store_path, d.store_path());
}
#[test]
fn dry_run_expands_all_outputs() {
let mut d = mk("hello", Some("2.12.1"));
d.outputs.extra = vec!["doc".into(), "dev".into()];
let r = DryRun.evaluate(&d).unwrap();
assert_eq!(r.outputs.len(), 3);
assert!(r.outputs.contains_key("out"));
assert!(r.outputs.contains_key("doc"));
assert!(r.outputs.contains_key("dev"));
let out = &r.outputs["out"];
let doc = &r.outputs["doc"];
let dev = &r.outputs["dev"];
assert_ne!(out.hash, doc.hash);
assert_ne!(out.hash, dev.hash);
assert_ne!(doc.hash, dev.hash);
}
#[test]
fn dry_run_log_includes_input_and_phase_counts() {
let mut d = mk("hello", None);
d.inputs.push(InputRef {
name: "gcc".into(),
version: None,
pinned: None,
});
d.builder = BuilderPhases {
phases: vec![
BuilderPhase::Unpack,
BuilderPhase::Build,
BuilderPhase::Install,
],
commands: Default::default(),
};
let r = DryRun.evaluate(&d).unwrap();
assert_eq!(r.log.len(), 1);
assert!(r.log[0].contains("hello"));
assert!(r.log[0].contains("1 inputs"));
assert!(r.log[0].contains("3 phases"));
}
#[test]
fn plan_emits_edges_only_for_pinned_inputs() {
let dep = mk("libc", Some("2.38"));
let dep_path = dep.store_path();
let mut d = mk("hello", Some("2.12.1"));
d.inputs.push(InputRef {
name: "libc".into(),
version: Some("2.38".into()),
pinned: Some(dep_path.clone()),
});
d.inputs.push(InputRef {
name: "gcc".into(), version: None,
pinned: None,
});
let plan = DryRun.plan(&d).unwrap();
assert_eq!(plan.root, d.store_path());
assert_eq!(plan.edges.len(), 1);
assert_eq!(plan.edges[0].0, d.store_path());
assert_eq!(plan.edges[0].1, dep_path);
assert_eq!(plan.order.len(), 2);
assert_eq!(plan.order[0], dep_path);
assert_eq!(plan.order[1], d.store_path());
}
#[test]
fn evaluate_many_preserves_order() {
let a = mk("a", None);
let b = mk("b", None);
let c = mk("c", None);
let results = DryRun
.evaluate_many(&[a.clone(), b.clone(), c.clone()])
.unwrap();
assert_eq!(results.len(), 3);
assert_eq!(results[0].store_path, a.store_path());
assert_eq!(results[1].store_path, b.store_path());
assert_eq!(results[2].store_path, c.store_path());
}
#[test]
fn identical_derivations_across_evaluators_agree_on_store_path() {
let a = DryRun;
let b = DryRun;
let d = mk("hello", Some("2.12.1"));
assert_eq!(
a.evaluate(&d).unwrap().store_path,
b.evaluate(&d).unwrap().store_path
);
}
#[test]
fn plan_handles_no_inputs() {
let d = mk("hello", None);
let plan = DryRun.plan(&d).unwrap();
assert_eq!(plan.edges.len(), 0);
assert_eq!(plan.order, vec![d.store_path()]);
}
#[test]
fn plan_deduplicates_repeated_pinned_inputs() {
let dep = mk("libc", Some("2.38"));
let dep_path = dep.store_path();
let mut d = mk("hello", None);
d.inputs.push(InputRef {
name: "libc".into(),
version: Some("2.38".into()),
pinned: Some(dep_path.clone()),
});
d.inputs.push(InputRef {
name: "libc-duplicate-alias".into(),
version: Some("2.38".into()),
pinned: Some(dep_path.clone()),
});
let plan = DryRun.plan(&d).unwrap();
assert_eq!(plan.edges.len(), 2); assert_eq!(plan.order.len(), 2); }
}