1use std::sync::Arc;
4
5use sim_cookbook::{
6 EmbeddedDir, RecipeCard, RecipeRun, RecipeSource, RecipeStore, recipes_from_embedded,
7};
8use sim_kernel::{Cx, Error, Lib, LibId, Result, Symbol};
9
10use crate::catalog::LibCatalog;
11
12pub type LibFactory = Arc<dyn Fn() -> Box<dyn Lib + Send + Sync> + Send + Sync>;
14
15#[derive(Clone, Copy, Debug, PartialEq, Eq)]
17pub enum LifecycleAction {
18 Load,
20 Unload,
22}
23
24impl LifecycleAction {
25 pub fn as_str(self) -> &'static str {
27 match self {
28 Self::Load => "load",
29 Self::Unload => "unload",
30 }
31 }
32}
33
34pub struct LoadableLibEntry {
36 pub id: String,
38 pub source: String,
40 pub title: String,
42 pub order: i64,
44 pub recipes: Option<EmbeddedDir>,
46 pub catalog_lib: Box<dyn Lib + Send + Sync>,
48 pub factory: LibFactory,
50}
51
52pub struct LoadableLibList {
54 entries: Vec<LoadableLibEntry>,
55}
56
57impl LoadableLibList {
58 pub fn new(entries: Vec<LoadableLibEntry>) -> Self {
60 Self { entries }
61 }
62
63 pub fn entries(&self) -> &[LoadableLibEntry] {
65 &self.entries
66 }
67
68 pub fn entry(&self, id: &str) -> Option<&LoadableLibEntry> {
70 self.entries.iter().find(|entry| entry.id == id)
71 }
72
73 pub fn is_loaded(cx: &Cx, id: &str) -> bool {
75 Self::loaded_id(cx, id).is_some()
76 }
77
78 pub fn load(&self, cx: &mut Cx, id: &str) -> Result<String> {
83 let entry = self
84 .entry(id)
85 .ok_or_else(|| Error::Eval(format!("unknown loadable lib `{id}`")))?;
86 if Self::loaded_entry_id(cx, entry).is_some() {
87 return Ok(format!("already loaded {id}"));
88 }
89 let lib = (entry.factory)();
90 cx.load_lib(lib.as_ref())?;
91 Ok(format!("loaded {id}"))
92 }
93
94 pub fn unload(&self, cx: &mut Cx, id: &str) -> Result<String> {
99 let loaded_id = self
100 .entry(id)
101 .and_then(|entry| Self::loaded_entry_id(cx, entry))
102 .or_else(|| Self::loaded_id(cx, id))
103 .ok_or_else(|| Error::Eval(format!("lib `{id}` is not loaded")))?;
104 cx.unload_lib(loaded_id)?;
105 Ok(format!("unloaded {id}"))
106 }
107
108 fn loaded_id(cx: &Cx, id: &str) -> Option<LibId> {
109 cx.registry()
110 .libs()
111 .iter()
112 .find(|loaded| lib_id_matches(&loaded.manifest.id, id))
113 .map(|loaded| loaded.id)
114 }
115
116 fn loaded_entry_id(cx: &Cx, entry: &LoadableLibEntry) -> Option<LibId> {
117 let manifest_id = entry.catalog_lib.manifest().id;
118 cx.registry()
119 .libs()
120 .iter()
121 .find(|loaded| {
122 loaded.manifest.id == manifest_id
123 || lib_id_matches(&loaded.manifest.id, &entry.id)
124 || lib_id_matches(&loaded.manifest.id, &manifest_id.as_qualified_str())
125 })
126 .map(|loaded| loaded.id)
127 }
128}
129
130impl LibCatalog for LoadableLibList {
131 fn resolve(&self, name: &str) -> Option<&dyn Lib> {
132 self.entries
133 .iter()
134 .find(|entry| {
135 entry.id == name
136 || entry.id.rsplit('/').next() == Some(name)
137 || lib_id_matches(&entry.catalog_lib.manifest().id, name)
138 })
139 .map(|entry| entry.catalog_lib.as_ref() as &dyn Lib)
140 }
141}
142
143fn lib_id_matches(symbol: &Symbol, id: &str) -> bool {
144 let tail = id.rsplit('/').next().unwrap_or(id);
145 let qualified = symbol.as_qualified_str();
146 qualified == id
147 || qualified.replace('/', "-") == id
148 || symbol.name.as_ref() == id
149 || symbol.name.as_ref() == tail
150}
151
152pub fn lifecycle_action(card: &RecipeCard) -> Option<(LifecycleAction, String)> {
154 let action = card
155 .tags
156 .iter()
157 .find_map(|tag| tag.strip_prefix("cookbook-action:"))?;
158 let lib = card
159 .tags
160 .iter()
161 .find_map(|tag| tag.strip_prefix("cookbook-lib:"))?;
162 let action = match action {
163 "load" => LifecycleAction::Load,
164 "unload" => LifecycleAction::Unload,
165 _ => return None,
166 };
167 Some((action, lib.to_owned()))
168}
169
170pub fn run_recipe_with_loadable_libs(
176 cx: &mut Cx,
177 directory: &LoadableLibList,
178 card: &RecipeCard,
179) -> Result<RecipeRun> {
180 if let Some((action, lib)) = lifecycle_action(card) {
181 return Ok(run_lifecycle_action(cx, directory, action, &lib, &card.id));
182 }
183 crate::run::run_recipe_with_catalog(cx, directory, card)
184}
185
186pub fn run_lifecycle_action(
188 cx: &mut Cx,
189 directory: &LoadableLibList,
190 action: LifecycleAction,
191 lib: &str,
192 recipe: &str,
193) -> RecipeRun {
194 let result = match action {
195 LifecycleAction::Load => directory.load(cx, lib),
196 LifecycleAction::Unload => directory.unload(cx, lib),
197 };
198 lifecycle_run(recipe, result)
199}
200
201fn lifecycle_run(recipe: &str, result: Result<String>) -> RecipeRun {
202 match result {
203 Ok(message) => RecipeRun {
204 recipe: recipe.to_owned(),
205 forms: 1,
206 results: vec![message],
207 checks: Vec::new(),
208 ok: true,
209 },
210 Err(err) => RecipeRun {
211 recipe: recipe.to_owned(),
212 forms: 1,
213 results: vec![err.to_string()],
214 checks: Vec::new(),
215 ok: false,
216 },
217 }
218}
219
220pub fn projected_recipe_store(cx: &Cx, directory: &LoadableLibList) -> Result<RecipeStore> {
226 let mut store = RecipeStore::new();
227 for entry in directory.entries() {
228 if LoadableLibList::loaded_entry_id(cx, entry).is_some() {
229 if let Some(recipes) = entry.recipes {
230 for mut card in recipes_from_embedded(recipes)
231 .map_err(|err| Error::Eval(format!("{} recipes: {err}", entry.id)))?
232 {
233 card.id = effective_recipe_id(&card.book, &card.id, &entry.id);
234 card.book = entry.id.clone();
235 card.book_title = entry.title.clone();
236 card.book_order = entry.order;
237 store.insert_card(card).map_err(Error::Eval)?;
238 }
239 } else {
240 store
241 .insert_card(setup_debt_card(entry))
242 .map_err(Error::Eval)?;
243 }
244 store.insert_card(unload_card(entry)).map_err(Error::Eval)?;
245 } else {
246 store.insert_card(load_card(entry)).map_err(Error::Eval)?;
247 }
248 }
249 Ok(store)
250}
251
252fn load_card(entry: &LoadableLibEntry) -> RecipeCard {
253 lifecycle_card(
254 format!("cookbook/load/{}", entry.id),
255 "cookbook/loadable".to_owned(),
256 entry,
257 LifecycleAction::Load,
258 format!("Load {}", entry.id),
259 0,
260 0,
261 )
262}
263
264fn effective_recipe_id(original_book: &str, original_id: &str, entry_id: &str) -> String {
265 if original_id == original_book {
266 return entry_id.to_owned();
267 }
268 if let Some(suffix) = original_id.strip_prefix(&format!("{original_book}/")) {
269 return format!("{entry_id}/{suffix}");
270 }
271 if original_id == entry_id || original_id.starts_with(&format!("{entry_id}/")) {
272 return original_id.to_owned();
273 }
274 format!("{entry_id}/{original_id}")
275}
276
277fn unload_card(entry: &LoadableLibEntry) -> RecipeCard {
278 lifecycle_card(
279 format!("{}/cookbook-lifecycle/unload", entry.id),
280 entry.id.clone(),
281 entry,
282 LifecycleAction::Unload,
283 format!("Unload {}", entry.id),
284 i64::MAX,
285 i64::MAX,
286 )
287}
288
289fn setup_debt_card(entry: &LoadableLibEntry) -> RecipeCard {
290 RecipeCard {
291 id: format!("{}/cookbook-lifecycle/setup-debt", entry.id),
292 book: entry.id.clone(),
293 chapter: "cookbook-lifecycle".to_owned(),
294 chapter_title: "Lifecycle".to_owned(),
295 chapter_summary: String::new(),
296 title: format!("Setup debt for {}", entry.id),
297 codec: "lisp".to_owned(),
298 setup: format!(
299 "(cookbook/setup-debt {:?} {:?})",
300 "missing-recipes", entry.id
301 )
302 .into_bytes(),
303 purpose: format!(
304 "`{}` is loadable in this product build and exposes no embedded cookbook directory; this descriptor keeps the gap visible.",
305 entry.id
306 ),
307 order: i64::MAX - 1,
308 chapter_order: i64::MAX - 1,
309 book_order: entry.order,
310 book_title: entry.title.clone(),
311 book_summary: String::new(),
312 tags: vec![
313 "sandbox-descriptor".to_owned(),
314 "setup-debt:missing-recipes".to_owned(),
315 format!("cookbook-lib:{}", entry.id),
316 format!("cookbook-source:{}", entry.source),
317 ],
318 requires: Vec::new(),
319 expect: Vec::new(),
320 source: RecipeSource::Crate {
321 lib: "sim/cookbook".to_owned(),
322 },
323 }
324}
325
326fn lifecycle_card(
327 id: String,
328 book: String,
329 entry: &LoadableLibEntry,
330 action: LifecycleAction,
331 title: String,
332 chapter_order: i64,
333 order: i64,
334) -> RecipeCard {
335 RecipeCard {
336 id,
337 book,
338 chapter: "cookbook-lifecycle".to_owned(),
339 chapter_title: "Lifecycle".to_owned(),
340 chapter_summary: String::new(),
341 title,
342 codec: "lisp".to_owned(),
343 setup: format!("(cookbook/{}-lib {:?})", action.as_str(), entry.id).into_bytes(),
344 purpose: format!("{} the loadable lib `{}`.", action.as_str(), entry.id),
345 order,
346 chapter_order,
347 book_order: entry.order,
348 book_title: entry.title.clone(),
349 book_summary: String::new(),
350 tags: vec![
351 format!("cookbook-action:{}", action.as_str()),
352 format!("cookbook-lib:{}", entry.id),
353 format!("cookbook-source:{}", entry.source),
354 ],
355 requires: Vec::new(),
356 expect: Vec::new(),
357 source: RecipeSource::Crate {
358 lib: "sim/cookbook".to_owned(),
359 },
360 }
361}