use serde::{Deserialize, Serialize};
use tatara_lisp::DeriveTataraDomain;
use crate::SpecError;
#[derive(DeriveTataraDomain, Serialize, Deserialize, Debug, Clone)]
#[tatara(keyword = "defstore-recipe")]
pub struct StoreRecipe {
pub name: String,
pub description: String,
pub slice: String,
pub transforms: Vec<String>,
#[serde(rename = "destSuffix")]
pub dest_suffix: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RecipeEntryOutcome {
pub source: std::path::PathBuf,
pub dest: std::path::PathBuf,
pub total_rewrites: usize,
pub noop: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RecipeOutcome {
pub recipe: String,
pub slice: String,
pub transforms: Vec<String>,
pub dest_root: std::path::PathBuf,
pub entries: Vec<RecipeEntryOutcome>,
}
impl RecipeOutcome {
#[must_use]
pub fn modified_count(&self) -> usize {
self.entries.iter().filter(|e| !e.noop).count()
}
#[must_use]
pub fn total_rewrites(&self) -> usize {
self.entries.iter().map(|e| e.total_rewrites).sum()
}
}
pub fn run(
recipe: &StoreRecipe,
dest_root_base: &std::path::Path,
) -> Result<RecipeOutcome, SpecError> {
use crate::store_ops::{self, ParsedNar};
use crate::store_transform;
use crate::nar;
let slices = store_ops::load_canonical_slices()?;
let slice = slices.iter().find(|s| s.name == recipe.slice)
.ok_or_else(|| SpecError::Interp {
phase: "recipe-unknown-slice".into(),
message: format!("recipe `{}` references unknown slice `{}`",
recipe.name, recipe.slice),
})?;
let all_transforms = store_transform::load_canonical()?;
let resolved: Vec<_> = recipe.transforms.iter().map(|name| {
all_transforms.iter().find(|t| &t.name == name)
.cloned()
.ok_or_else(|| SpecError::Interp {
phase: "recipe-unknown-transform".into(),
message: format!("recipe `{}` references unknown transform `{}`",
recipe.name, name),
})
}).collect::<Result<Vec<_>, _>>()?;
let dest_root = dest_root_base.join(&recipe.dest_suffix);
std::fs::create_dir_all(&dest_root).map_err(|e| SpecError::Interp {
phase: "recipe-mkdir".into(),
message: format!("mkdir {}: {e}", dest_root.display()),
})?;
let plans = store_ops::build_materialization_plan(slice, &dest_root)?;
let mut entries: Vec<RecipeEntryOutcome> = Vec::with_capacity(plans.len());
for plan in &plans {
let nar_bytes = nar::encode(&plan.source).map_err(|e| SpecError::Interp {
phase: "recipe-encode".into(),
message: format!("encode {}: {e}", plan.source.display()),
})?;
let mut parsed = ParsedNar::parse(&nar_bytes).map_err(|e| SpecError::Interp {
phase: "recipe-parse".into(),
message: format!("parse {}: {e}", plan.source.display()),
})?;
let outcomes = store_transform::apply_all(&mut parsed, &resolved)?;
let total = outcomes.iter().map(|o|
o.file_rewrites + o.ref_rewrites + o.entries_renamed
).sum::<usize>();
let new_nar = parsed.serialize();
if plan.dest.exists() {
std::fs::remove_dir_all(&plan.dest).ok();
}
nar::decode(&new_nar, &plan.dest).map_err(|e| SpecError::Interp {
phase: "recipe-decode".into(),
message: format!("decode → {}: {e}", plan.dest.display()),
})?;
entries.push(RecipeEntryOutcome {
source: plan.source.clone(),
dest: plan.dest.clone(),
total_rewrites: total,
noop: total == 0,
});
}
Ok(RecipeOutcome {
recipe: recipe.name.clone(),
slice: recipe.slice.clone(),
transforms: recipe.transforms.clone(),
dest_root,
entries,
})
}
pub const CANONICAL_STORE_RECIPES_LISP: &str =
include_str!("../specs/store_recipes.lisp");
pub fn load_canonical() -> Result<Vec<StoreRecipe>, SpecError> {
crate::loader::load_all::<StoreRecipe>(CANONICAL_STORE_RECIPES_LISP)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn canonical_recipes_parse() {
let rs = load_canonical().expect("recipes must compile");
assert!(!rs.is_empty());
}
#[test]
fn recipe_references_known_slice() {
let recipes = load_canonical().unwrap();
let slices = crate::store_ops::load_canonical_slices().unwrap();
let slice_names: std::collections::HashSet<String> =
slices.iter().map(|s| s.name.clone()).collect();
for r in &recipes {
assert!(slice_names.contains(&r.slice),
"recipe `{}` references unknown slice `{}`", r.name, r.slice);
}
}
#[test]
fn recipe_references_known_transforms() {
let recipes = load_canonical().unwrap();
let xforms = crate::store_transform::load_canonical().unwrap();
let xform_names: std::collections::HashSet<String> =
xforms.iter().map(|t| t.name.clone()).collect();
for r in &recipes {
for t in &r.transforms {
assert!(xform_names.contains(t),
"recipe `{}` references unknown transform `{}`", r.name, t);
}
}
}
}