Files
RedBear-OS/src/staged_pkg.rs
T

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()
);
}
}
}