use std::{collections::HashMap, ffi::OsStr, fs::File, path::Path}; use serde::{de::DeserializeOwned, Deserialize}; use walkdir::WalkDir; use rebel_common::error::*; use rebel_resolve::task::{TaskDef, TaskMeta}; #[derive(Clone, Debug, Deserialize, Default)] pub struct RecipeMeta { pub name: Option, pub version: Option, } #[derive(Debug, Deserialize)] struct Recipe { #[serde(default)] pub meta: RecipeMeta, pub tasks: HashMap, } #[derive(Debug, Deserialize)] struct Subrecipe { pub tasks: HashMap, } fn read_yaml(path: &Path) -> Result { let f = File::open(path).context("IO error")?; let value: T = serde_yaml::from_reader(f) .map_err(Error::new) .context("YAML error")?; Ok(value) } const RECIPE_NAME: &str = "build"; const RECIPE_PREFIX: &str = "build."; fn recipe_name(path: &Path) -> Option<&str> { if path.extension() != Some("yml".as_ref()) { return None; } let stem = path.file_stem()?.to_str()?; if stem == RECIPE_NAME { return Some(""); } stem.strip_prefix(RECIPE_PREFIX) } fn handle_recipe_tasks( tasks: &mut HashMap>>, recipe_tasks: HashMap, meta: &TaskMeta, ) { let task_map = match tasks.get_mut(&meta.recipe) { Some(task_map) => task_map, None => tasks.entry(meta.recipe.clone()).or_default(), }; for (label, mut task) in recipe_tasks { task.meta = meta.clone(); task_map.entry(label).or_default().push(task); } } fn read_recipe_tasks( path: &Path, basename: &str, tasks: &mut HashMap>>, ) -> Result { let recipe_def = read_yaml::(path)?; let name = recipe_def .meta .name .as_deref() .unwrap_or(basename) .to_string(); let meta = TaskMeta { basename: basename.to_string(), recipename: "".to_string(), recipe: basename.to_string(), name, version: recipe_def.meta.version.clone(), }; handle_recipe_tasks(tasks, recipe_def.tasks, &meta); Ok(recipe_def.meta) } fn read_subrecipe_tasks( path: &Path, basename: &str, recipename: &str, recipe_meta: &RecipeMeta, tasks: &mut HashMap>>, ) -> Result<()> { let recipe = format!("{basename}/{recipename}"); let recipe_def = read_yaml::(path)?; let name = recipe_meta.name.as_deref().unwrap_or(basename).to_string(); let meta = TaskMeta { basename: basename.to_string(), recipename: recipename.to_string(), recipe: recipe.clone(), name, version: recipe_meta.version.clone(), }; handle_recipe_tasks(tasks, recipe_def.tasks, &meta); Ok(()) } pub fn read_recipes>( path: P, ) -> Result>>> { let mut tasks = HashMap::>>::new(); let mut recipe_metas = HashMap::::new(); for entry in WalkDir::new(path) .sort_by(|a, b| { // Files are sorted first by stem, then by extension, so that // recipe.yml will always be read before recipe.NAME.yml let stem_cmp = a.path().file_stem().cmp(&b.path().file_stem()); let ext_cmp = a.path().extension().cmp(&b.path().extension()); stem_cmp.then(ext_cmp) }) .into_iter() .filter_map(|e| e.ok()) { let path = entry.path(); if !path.is_file() { continue; } let Some(recipename) = recipe_name(path) else { continue; }; let Some(basename) = path .parent() .and_then(Path::file_name) .and_then(OsStr::to_str) else { continue; }; if recipename.is_empty() { recipe_metas.insert( basename.to_string(), read_recipe_tasks(path, basename, &mut tasks)?, ); } else { let Some(recipe_meta) = recipe_metas.get(basename) else { continue; }; read_subrecipe_tasks(path, basename, recipename, recipe_meta, &mut tasks)?; } } Ok(tasks) }