1use serde::{Deserialize, Serialize};
27use tatara_lisp::DeriveTataraDomain;
28
29use crate::SpecError;
30
31#[derive(DeriveTataraDomain, Serialize, Deserialize, Debug, Clone)]
33#[tatara(keyword = "defstore-recipe")]
34pub struct StoreRecipe {
35 pub name: String,
37 pub description: String,
39 pub slice: String,
41 pub transforms: Vec<String>,
44 #[serde(rename = "destSuffix")]
47 pub dest_suffix: String,
48}
49
50#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct RecipeEntryOutcome {
53 pub source: std::path::PathBuf,
54 pub dest: std::path::PathBuf,
55 pub total_rewrites: usize,
57 pub noop: bool,
59}
60
61#[derive(Debug, Clone, PartialEq, Eq)]
63pub struct RecipeOutcome {
64 pub recipe: String,
65 pub slice: String,
66 pub transforms: Vec<String>,
67 pub dest_root: std::path::PathBuf,
68 pub entries: Vec<RecipeEntryOutcome>,
69}
70
71impl RecipeOutcome {
72 #[must_use]
75 pub fn modified_count(&self) -> usize {
76 self.entries.iter().filter(|e| !e.noop).count()
77 }
78
79 #[must_use]
81 pub fn total_rewrites(&self) -> usize {
82 self.entries.iter().map(|e| e.total_rewrites).sum()
83 }
84}
85
86pub fn run(
97 recipe: &StoreRecipe,
98 dest_root_base: &std::path::Path,
99) -> Result<RecipeOutcome, SpecError> {
100 use crate::store_ops::{self, ParsedNar};
101 use crate::store_transform;
102 use crate::nar;
103
104 let slices = store_ops::load_canonical_slices()?;
105 let slice = slices.iter().find(|s| s.name == recipe.slice)
106 .ok_or_else(|| SpecError::Interp {
107 phase: "recipe-unknown-slice".into(),
108 message: format!("recipe `{}` references unknown slice `{}`",
109 recipe.name, recipe.slice),
110 })?;
111
112 let all_transforms = store_transform::load_canonical()?;
113 let resolved: Vec<_> = recipe.transforms.iter().map(|name| {
114 all_transforms.iter().find(|t| &t.name == name)
115 .cloned()
116 .ok_or_else(|| SpecError::Interp {
117 phase: "recipe-unknown-transform".into(),
118 message: format!("recipe `{}` references unknown transform `{}`",
119 recipe.name, name),
120 })
121 }).collect::<Result<Vec<_>, _>>()?;
122
123 let dest_root = dest_root_base.join(&recipe.dest_suffix);
124 std::fs::create_dir_all(&dest_root).map_err(|e| SpecError::Interp {
125 phase: "recipe-mkdir".into(),
126 message: format!("mkdir {}: {e}", dest_root.display()),
127 })?;
128
129 let plans = store_ops::build_materialization_plan(slice, &dest_root)?;
130 let mut entries: Vec<RecipeEntryOutcome> = Vec::with_capacity(plans.len());
131
132 for plan in &plans {
133 let nar_bytes = nar::encode(&plan.source).map_err(|e| SpecError::Interp {
135 phase: "recipe-encode".into(),
136 message: format!("encode {}: {e}", plan.source.display()),
137 })?;
138 let mut parsed = ParsedNar::parse(&nar_bytes).map_err(|e| SpecError::Interp {
140 phase: "recipe-parse".into(),
141 message: format!("parse {}: {e}", plan.source.display()),
142 })?;
143 let outcomes = store_transform::apply_all(&mut parsed, &resolved)?;
145 let total = outcomes.iter().map(|o|
146 o.file_rewrites + o.ref_rewrites + o.entries_renamed
147 ).sum::<usize>();
148 let new_nar = parsed.serialize();
150 if plan.dest.exists() {
151 std::fs::remove_dir_all(&plan.dest).ok();
152 }
153 nar::decode(&new_nar, &plan.dest).map_err(|e| SpecError::Interp {
154 phase: "recipe-decode".into(),
155 message: format!("decode → {}: {e}", plan.dest.display()),
156 })?;
157 entries.push(RecipeEntryOutcome {
158 source: plan.source.clone(),
159 dest: plan.dest.clone(),
160 total_rewrites: total,
161 noop: total == 0,
162 });
163 }
164
165 Ok(RecipeOutcome {
166 recipe: recipe.name.clone(),
167 slice: recipe.slice.clone(),
168 transforms: recipe.transforms.clone(),
169 dest_root,
170 entries,
171 })
172}
173
174pub const CANONICAL_STORE_RECIPES_LISP: &str =
177 include_str!("../specs/store_recipes.lisp");
178
179pub fn load_canonical() -> Result<Vec<StoreRecipe>, SpecError> {
185 crate::loader::load_all::<StoreRecipe>(CANONICAL_STORE_RECIPES_LISP)
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191
192 #[test]
193 fn canonical_recipes_parse() {
194 let rs = load_canonical().expect("recipes must compile");
195 assert!(!rs.is_empty());
196 }
197
198 #[test]
199 fn recipe_references_known_slice() {
200 let recipes = load_canonical().unwrap();
203 let slices = crate::store_ops::load_canonical_slices().unwrap();
204 let slice_names: std::collections::HashSet<String> =
205 slices.iter().map(|s| s.name.clone()).collect();
206 for r in &recipes {
207 assert!(slice_names.contains(&r.slice),
208 "recipe `{}` references unknown slice `{}`", r.name, r.slice);
209 }
210 }
211
212 #[test]
213 fn recipe_references_known_transforms() {
214 let recipes = load_canonical().unwrap();
215 let xforms = crate::store_transform::load_canonical().unwrap();
216 let xform_names: std::collections::HashSet<String> =
217 xforms.iter().map(|t| t.name.clone()).collect();
218 for r in &recipes {
219 for t in &r.transforms {
220 assert!(xform_names.contains(t),
221 "recipe `{}` references unknown transform `{}`", r.name, t);
222 }
223 }
224 }
225}