249 lines
8.0 KiB
Rust
249 lines
8.0 KiB
Rust
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<HashMap<PackageName, PathBuf>> = LazyLock::new(|| {
|
|
let mut recipe_paths = HashMap::<PackageName, PathBuf>::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<PackageName> = 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<Path>) -> BTreeSet<PathBuf> {
|
|
let prefix = prefix.as_ref();
|
|
RECIPE_PATHS
|
|
.values()
|
|
.map(|path| prefix.join(path))
|
|
.collect()
|
|
}
|
|
|
|
pub fn new(name: &PackageName) -> Result<Package, PackageError> {
|
|
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<Package, PackageError> {
|
|
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<Vec<Package>, 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<Package>,
|
|
BTreeMap<PackageName, Result<(), PackageError>>,
|
|
) {
|
|
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<String> = 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()
|
|
);
|
|
}
|
|
}
|
|
}
|