use std::borrow::Cow; use std::collections::{BTreeMap, BTreeSet, HashMap}; use std::ffi::OsStr; use std::path::{Path, PathBuf}; use std::sync::LazyLock; use pkg::{Package, PackageError, PackageName}; // This file contains code that caches recipe paths. // TODO: This file is previously resides in `pkg` crate, // and can actually be merged with other logic in this cookbook. static RECIPE_PATHS: LazyLock> = LazyLock::new(|| { let mut recipe_paths = HashMap::::new(); let mut walker = ignore::WalkBuilder::new("recipes"); walker.follow_links(true); for entry_res in walker.build() { let Ok(entry) = entry_res else { continue; }; if entry.file_name() == OsStr::new("recipe.toml") { let recipe_file = entry.path(); let Some(recipe_dir) = recipe_file.parent() else { continue; }; let Some(recipe_name): Option = recipe_dir .file_name() .and_then(|x| x.to_str()?.try_into().ok()) else { continue; }; let existing = recipe_paths.get(&recipe_name); match existing { Some(other_dir) => { let other_dir = other_dir.clone(); let current_is_deeper = recipe_dir.starts_with(&other_dir); let existing_is_deeper = other_dir.starts_with(&recipe_dir); if current_is_deeper { recipe_paths.insert(recipe_name.clone(), other_dir); } else if existing_is_deeper { recipe_paths.insert(recipe_name, recipe_dir.to_path_buf()); } else { recipe_paths.insert(recipe_name.clone(), recipe_dir.to_path_buf()); } } None => { recipe_paths.insert(recipe_name, recipe_dir.to_path_buf()); } } } } recipe_paths }); pub fn find(recipe: &str) -> Option<&'static Path> { RECIPE_PATHS.get(recipe).map(PathBuf::as_path) } pub fn list(prefix: impl AsRef) -> BTreeSet { let prefix = prefix.as_ref(); RECIPE_PATHS .values() .map(|path| prefix.join(path)) .collect() } pub fn new(name: &PackageName) -> Result { let dir = find(name.name()).ok_or_else(|| PackageError::PackageNotFound(name.clone()))?; from_path(dir, name.suffix()) } pub fn from_path(dir: &Path, feature: Option<&str>) -> Result { let target = redoxer::target(); let stage_name = match feature { Some(f) => Cow::Owned(format!("stage.{f}.toml")), None => Cow::Borrowed("stage.toml"), }; let file = dir.join("target").join(target).join(stage_name.as_ref()); if !file.is_file() { return Err(PackageError::FileMissing(file)); } let toml = std::fs::read_to_string(&file) .map_err(|err| PackageError::FileError(err.raw_os_error(), file.clone()))?; toml::from_str(&toml).map_err(|err| PackageError::Parse(err, Some(file))) } pub fn new_recursive( names: &[PackageName], nonstop: bool, recursion: usize, ) -> Result, PackageError> { if names.len() == 0 { return Ok(vec![]); } let (list, map) = new_recursive_nonstop(names, recursion); if nonstop && list.len() > 0 { Ok(list) } else if !nonstop && map.len() == list.len() { Ok(list) } else { let (_, res) = map.into_iter().find(|(_, v)| v.is_err()).unwrap(); Err(res.err().unwrap()) } } /// List ordered success packages and map of failed packages. /// A package can be both success and failed if dependencies aren't satistied. pub fn new_recursive_nonstop( names: &[PackageName], recursion: usize, ) -> ( Vec, BTreeMap>, ) { let mut packages = Vec::new(); let mut packages_map = BTreeMap::new(); for name in names { if packages_map.contains_key(name) { continue; } let package = if recursion == 0 { Err(PackageError::Recursion(Default::default())) } else { new(name) }; match package { Ok(package) => { let mut has_invalid_dependency = false; let (dependencies, dependencies_map) = new_recursive_nonstop(&package.depends, recursion - 1); for dependency in dependencies { if !packages_map.contains_key(&dependency.name) { packages_map.insert(dependency.name.clone(), Ok(())); packages.push(dependency); } } for (dep_name, result) in dependencies_map { if let Err(mut e) = result { if !packages_map.contains_key(&dep_name) { e.append_recursion(name); packages_map.insert(dep_name, Err(e)); } has_invalid_dependency = true; } } // TODO: this check is redundant if !packages_map.contains_key(name) { packages_map.insert( name.clone(), if has_invalid_dependency { Err(PackageError::DependencyInvalid(name.clone())) } else { Ok(()) }, ); packages.push(package); } } Err(e) => { packages_map.insert(name.clone(), Err(e)); } } } (packages, packages_map) } #[cfg(test)] mod tests { use super::*; use std::collections::HashSet; #[test] fn test_find_known_recipe() { let path = find("evdevd"); assert!(path.is_some(), "evdevd recipe should be found"); let path = path.unwrap(); assert!(path.ends_with("evdevd"), "path should end with evdevd"); assert!( path.join("recipe.toml").exists(), "recipe.toml should exist" ); } #[test] fn test_find_nonexistent_recipe() { let path = find("this-package-does-not-exist-xyzzy"); assert!(path.is_none(), "nonexistent recipe should return None"); } #[test] fn test_no_recipe_name_collisions() { let all_names: Vec<&str> = RECIPE_PATHS.keys().map(|n| n.name()).collect(); let unique: HashSet<&&str> = all_names.iter().collect(); assert_eq!( all_names.len(), unique.len(), "duplicate recipe names detected: {:?}", { let mut counts = std::collections::HashMap::new(); for name in &all_names { *counts.entry(name).or_insert(0) += 1; } let mut dups: Vec = Vec::new(); for (name, count) in counts.iter() { if *count > 1 { dups.push(format!("{} (x{})", name, count)); } } dups } ); } #[test] fn test_recipe_count_reasonable() { let count = RECIPE_PATHS.len(); assert!( count > 100, "should have more than 100 recipes (got {})", count ); assert!( count < 10000, "should have fewer than 10000 recipes (got {})", count ); } #[test] fn test_recipe_paths_exist() { for (name, path) in RECIPE_PATHS.iter() { let recipe_file = path.join("recipe.toml"); assert!( recipe_file.exists(), "recipe {} at {}: recipe.toml missing", name.name(), path.display() ); } } }