summaryrefslogtreecommitdiffstats
path: root/crates/rebel/src/recipe.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/rebel/src/recipe.rs')
-rw-r--r--crates/rebel/src/recipe.rs167
1 files changed, 167 insertions, 0 deletions
diff --git a/crates/rebel/src/recipe.rs b/crates/rebel/src/recipe.rs
new file mode 100644
index 0000000..28cc84c
--- /dev/null
+++ b/crates/rebel/src/recipe.rs
@@ -0,0 +1,167 @@
+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<String>,
+ pub version: Option<String>,
+}
+
+#[derive(Debug, Deserialize)]
+struct Recipe {
+ #[serde(default)]
+ pub meta: RecipeMeta,
+ pub tasks: HashMap<String, TaskDef>,
+}
+
+#[derive(Debug, Deserialize)]
+struct Subrecipe {
+ pub tasks: HashMap<String, TaskDef>,
+}
+
+fn read_yaml<T: DeserializeOwned>(path: &Path) -> Result<T> {
+ 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<String, HashMap<String, Vec<TaskDef>>>,
+ recipe_tasks: HashMap<String, TaskDef>,
+ 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<String, HashMap<String, Vec<TaskDef>>>,
+) -> Result<RecipeMeta> {
+ let recipe_def = read_yaml::<Recipe>(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<String, HashMap<String, Vec<TaskDef>>>,
+) -> Result<()> {
+ let recipe = format!("{basename}/{recipename}");
+ let recipe_def = read_yaml::<Subrecipe>(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<P: AsRef<Path>>(
+ path: P,
+) -> Result<HashMap<String, HashMap<String, Vec<TaskDef>>>> {
+ let mut tasks = HashMap::<String, HashMap<String, Vec<TaskDef>>>::new();
+ let mut recipe_metas = HashMap::<String, RecipeMeta>::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)
+}