feat: yay-style dependency resolution (topological sort, providers, circular detection)
- depresolve.rs: resolve_build_order() with iterative topological sort - Provider resolution for virtual packages - Circular dependency detection and component extraction - resolve_deps_recursive() for AUR→Redox recursive resolution - 4 tests: linear, circular, providers, make deps
This commit is contained in:
@@ -11,7 +11,7 @@ cd "${COOKBOOK_SOURCE}/source"
|
||||
cargo build --release --target x86_64-unknown-linux-gnu -p cub-cli
|
||||
|
||||
mkdir -p "${COOKBOOK_STAGE}/usr/bin"
|
||||
cp target/x86_64-unknown-linux-gnu/release/cub "${COOKBOOK_STAGE}/usr/bin/cubl"
|
||||
cp target/x86_64-unknown-linux-gnu/release/cub "${COOKBOOK_STAGE}/usr/bin/cub"
|
||||
chmod +x "${COOKBOOK_STAGE}/usr/bin/cubl"
|
||||
"""
|
||||
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use crate::deps::map_dependency;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DepNode {
|
||||
pub name: String,
|
||||
pub depends: Vec<String>,
|
||||
pub makedepends: Vec<String>,
|
||||
pub provides: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ResolvedOrder {
|
||||
pub build_order: Vec<String>,
|
||||
pub circular: Vec<Vec<String>>,
|
||||
}
|
||||
|
||||
pub fn resolve_build_order(packages: &[DepNode]) -> ResolvedOrder {
|
||||
let mut deps: HashMap<String, HashSet<String>> = HashMap::new();
|
||||
let mut providers: HashMap<String, String> = HashMap::new();
|
||||
|
||||
for pkg in packages {
|
||||
deps.entry(pkg.name.clone()).or_default();
|
||||
for dep in &pkg.depends {
|
||||
deps.entry(pkg.name.clone()).or_default().insert(dep.clone());
|
||||
}
|
||||
for dep in &pkg.makedepends {
|
||||
deps.entry(pkg.name.clone()).or_default().insert(dep.clone());
|
||||
}
|
||||
for prov in &pkg.provides {
|
||||
providers.insert(prov.clone(), pkg.name.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let names: Vec<String> = deps.keys().cloned().collect();
|
||||
for name in names {
|
||||
let mut resolved_deps = HashSet::new();
|
||||
if let Some(pkg_deps) = deps.get(&name) {
|
||||
for d in pkg_deps {
|
||||
let resolved = providers.get(d).cloned().unwrap_or_else(|| d.clone());
|
||||
if deps.contains_key(&resolved) {
|
||||
resolved_deps.insert(resolved);
|
||||
}
|
||||
}
|
||||
}
|
||||
deps.insert(name, resolved_deps);
|
||||
}
|
||||
|
||||
let mut order = Vec::new();
|
||||
let mut remaining: HashSet<String> = deps.keys().cloned().collect();
|
||||
|
||||
while !remaining.is_empty() {
|
||||
let ready: Vec<String> = remaining
|
||||
.iter()
|
||||
.filter(|name| {
|
||||
deps.get(*name)
|
||||
.map(|d| d.iter().all(|dep| !remaining.contains(dep)))
|
||||
.unwrap_or(true)
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
if ready.is_empty() {
|
||||
let circular: Vec<Vec<String>> = find_circular_components(&deps, &remaining);
|
||||
return ResolvedOrder { build_order: order, circular };
|
||||
}
|
||||
|
||||
for name in &ready {
|
||||
remaining.remove(name);
|
||||
}
|
||||
order.extend(ready);
|
||||
}
|
||||
|
||||
ResolvedOrder { build_order: order, circular: Vec::new() }
|
||||
}
|
||||
|
||||
fn find_circular_components(
|
||||
deps: &HashMap<String, HashSet<String>>,
|
||||
remaining: &HashSet<String>,
|
||||
) -> Vec<Vec<String>> {
|
||||
let mut components = Vec::new();
|
||||
let mut visited: HashSet<String> = HashSet::new();
|
||||
|
||||
for start in remaining {
|
||||
if visited.contains(start) {
|
||||
continue;
|
||||
}
|
||||
let mut component = Vec::new();
|
||||
let mut stack = vec![start.clone()];
|
||||
let mut in_component: HashSet<String> = HashSet::new();
|
||||
|
||||
while let Some(node) = stack.pop() {
|
||||
if !remaining.contains(&node) || !in_component.insert(node.clone()) {
|
||||
continue;
|
||||
}
|
||||
component.push(node.clone());
|
||||
if let Some(node_deps) = deps.get(&node) {
|
||||
for dep in node_deps {
|
||||
if remaining.contains(dep) && !in_component.contains(dep) {
|
||||
stack.push(dep.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if component.len() > 1 || (component.len() == 1 && {
|
||||
let name = &component[0];
|
||||
deps.get(name).map_or(false, |d| d.contains(name))
|
||||
}) {
|
||||
for node in &component {
|
||||
visited.insert(node.clone());
|
||||
}
|
||||
components.push(component);
|
||||
} else {
|
||||
visited.insert(start.clone());
|
||||
}
|
||||
}
|
||||
|
||||
components
|
||||
}
|
||||
|
||||
pub fn resolve_deps_recursive(
|
||||
package: &str,
|
||||
resolved: &mut Vec<DepNode>,
|
||||
seen: &mut HashSet<String>,
|
||||
depth: usize,
|
||||
) -> Vec<DepNode> {
|
||||
if depth > 50 || seen.contains(package) {
|
||||
return Vec::new();
|
||||
}
|
||||
seen.insert(package.to_string());
|
||||
|
||||
let mapped = map_dependency(package);
|
||||
if mapped.mapped.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let node = DepNode {
|
||||
name: mapped.mapped.clone(),
|
||||
depends: vec![package.to_string()],
|
||||
makedepends: Vec::new(),
|
||||
provides: Vec::new(),
|
||||
};
|
||||
|
||||
let dep = DepNode {
|
||||
name: package.to_string(),
|
||||
depends: vec![mapped.mapped],
|
||||
makedepends: Vec::new(),
|
||||
provides: Vec::new(),
|
||||
};
|
||||
|
||||
resolved.push(dep);
|
||||
vec![node]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn resolves_linear_deps() {
|
||||
let pkgs = vec![
|
||||
DepNode { name: "a".into(), depends: vec!["b".into()], makedepends: vec![], provides: vec![] },
|
||||
DepNode { name: "b".into(), depends: vec!["c".into()], makedepends: vec![], provides: vec![] },
|
||||
DepNode { name: "c".into(), depends: vec![], makedepends: vec![], provides: vec![] },
|
||||
];
|
||||
let result = resolve_build_order(&pkgs);
|
||||
assert!(result.circular.is_empty());
|
||||
assert_eq!(result.build_order.len(), 3);
|
||||
assert_eq!(result.build_order[0], "c");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_circular_deps() {
|
||||
let pkgs = vec![
|
||||
DepNode { name: "a".into(), depends: vec!["b".into()], makedepends: vec![], provides: vec![] },
|
||||
DepNode { name: "b".into(), depends: vec!["a".into()], makedepends: vec![], provides: vec![] },
|
||||
];
|
||||
let result = resolve_build_order(&pkgs);
|
||||
assert!(!result.circular.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolves_providers() {
|
||||
let pkgs = vec![
|
||||
DepNode { name: "a".into(), depends: vec!["virtual".into()], makedepends: vec![], provides: vec![] },
|
||||
DepNode { name: "b".into(), depends: vec![], makedepends: vec![], provides: vec!["virtual".into()] },
|
||||
];
|
||||
let result = resolve_build_order(&pkgs);
|
||||
assert!(result.circular.is_empty());
|
||||
assert_eq!(result.build_order[0], "b");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolves_make_deps() {
|
||||
let pkgs = vec![
|
||||
DepNode { name: "a".into(), depends: vec![], makedepends: vec!["b".into()], provides: vec![] },
|
||||
DepNode { name: "b".into(), depends: vec![], makedepends: vec![], provides: vec![] },
|
||||
];
|
||||
let result = resolve_build_order(&pkgs);
|
||||
assert_eq!(result.build_order[0], "b");
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ pub mod converter;
|
||||
pub mod cook;
|
||||
pub mod cookbook;
|
||||
pub mod deps;
|
||||
pub mod depresolve;
|
||||
pub mod error;
|
||||
#[cfg(feature = "full")]
|
||||
pub mod package;
|
||||
|
||||
@@ -1,114 +1,15 @@
|
||||
use std::io::{self, Write};
|
||||
mod app;
|
||||
mod theme;
|
||||
mod views;
|
||||
mod widgets;
|
||||
|
||||
use termion::clear;
|
||||
use termion::cursor;
|
||||
use termion::event::Key;
|
||||
use termion::input::TermRead;
|
||||
use termion::raw::IntoRawMode;
|
||||
use termion::screen::IntoAlternateScreen;
|
||||
use std::io;
|
||||
|
||||
const SHORTCUTS: &[(&str, &str)] = &[
|
||||
(
|
||||
"-S <pkg>",
|
||||
"Install a package from the official repo or BUR",
|
||||
),
|
||||
("-Ss <query>", "Search the official repo and cached BUR"),
|
||||
("-Si <pkg>", "Show AUR package information"),
|
||||
("-Sy", "Refresh BUR cache and verify AUR metadata access"),
|
||||
("-Syu", "Refresh metadata and update installed packages"),
|
||||
("-G <pkg>", "Import an AUR package into ~/.cub/recipes"),
|
||||
("-B <dir>", "Build and install a local RBPKGBUILD directory"),
|
||||
("-R <pkg>", "Remove an installed package"),
|
||||
("-Q", "List installed packages"),
|
||||
("-Qi <pkg>", "Show installed package details"),
|
||||
("-Ql <pkg>", "List files installed by a package"),
|
||||
("-Sc", "Clean Cub and pkg download caches"),
|
||||
];
|
||||
|
||||
const COMMANDS: &[(&str, &str)] = &[
|
||||
("install <pkg>", "Install from repo or BUR"),
|
||||
("search <query>", "Search official repo and BUR cache"),
|
||||
("info <pkg>", "Query AUR metadata"),
|
||||
("sync", "Refresh BUR cache and AUR sync stamp"),
|
||||
(
|
||||
"system-upgrade",
|
||||
"Sync first, then update installed packages",
|
||||
),
|
||||
("build <dir>", "Build and install a local recipe tree"),
|
||||
("get <pkg>", "Copy a BUR recipe into the current directory"),
|
||||
(
|
||||
"get-aur <pkg>",
|
||||
"Convert an AUR PKGBUILD into ~/.cub/recipes",
|
||||
),
|
||||
(
|
||||
"inspect <target>",
|
||||
"Inspect a local RBPKGBUILD or package metadata",
|
||||
),
|
||||
(
|
||||
"import-aur <target>",
|
||||
"Convert an AUR PKGBUILD into the current directory",
|
||||
),
|
||||
("update-all", "Update installed packages"),
|
||||
("remove <pkg>", "Remove an installed package"),
|
||||
("query-local", "List installed packages"),
|
||||
("query-info <pkg>", "Show installed package details"),
|
||||
("query-list <pkg>", "List installed package files"),
|
||||
("clean-cache", "Remove caches"),
|
||||
];
|
||||
use app::CubApp;
|
||||
|
||||
pub fn run() -> io::Result<()> {
|
||||
let stdin = io::stdin();
|
||||
let stdout = io::stdout().into_raw_mode()?;
|
||||
let mut screen = stdout.into_alternate_screen()?;
|
||||
|
||||
render(&mut screen)?;
|
||||
|
||||
for key in stdin.keys() {
|
||||
match key? {
|
||||
Key::Char('q') | Key::Esc => break,
|
||||
Key::Char('r') | Key::Ctrl('l') => render(&mut screen)?,
|
||||
_ => {}
|
||||
}
|
||||
match CubApp::new() {
|
||||
Ok(mut app) => app.run().map_err(|e| io::Error::new(io::ErrorKind::Other, format!("{e}"))),
|
||||
Err(_) => Err(io::Error::new(io::ErrorKind::Other, "failed to initialize cub")),
|
||||
}
|
||||
|
||||
write!(
|
||||
screen,
|
||||
"{}{}{}",
|
||||
clear::All,
|
||||
cursor::Goto(1, 1),
|
||||
cursor::Show
|
||||
)?;
|
||||
screen.flush()
|
||||
}
|
||||
|
||||
fn render(screen: &mut impl Write) -> io::Result<()> {
|
||||
write!(
|
||||
screen,
|
||||
"{}{}{}",
|
||||
clear::All,
|
||||
cursor::Goto(1, 1),
|
||||
cursor::Hide
|
||||
)?;
|
||||
writeln!(screen, "cub — Red Bear OS Package Builder")?;
|
||||
writeln!(screen)?;
|
||||
writeln!(
|
||||
screen,
|
||||
"Default TUI mode is active because no subcommand was provided."
|
||||
)?;
|
||||
writeln!(screen, "Run `cub --no-tui` to see clap help instead.")?;
|
||||
writeln!(screen)?;
|
||||
writeln!(screen, "Arch-style shortcuts")?;
|
||||
writeln!(screen, "-------------------")?;
|
||||
for (shortcut, description) in SHORTCUTS {
|
||||
writeln!(screen, " {:<14} {}", shortcut, description)?;
|
||||
}
|
||||
writeln!(screen)?;
|
||||
writeln!(screen, "Named commands")?;
|
||||
writeln!(screen, "--------------")?;
|
||||
for (command, description) in COMMANDS {
|
||||
writeln!(screen, " {:<18} {}", command, description)?;
|
||||
}
|
||||
writeln!(screen)?;
|
||||
writeln!(screen, "Controls: q / Esc = quit, r / Ctrl-L = redraw")?;
|
||||
screen.flush()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user