From 3622c7b8ecfe431188e416ac0e4996bd953f3087 Mon Sep 17 00:00:00 2001 From: Vasilito Date: Fri, 8 May 2026 11:17:07 +0100 Subject: [PATCH] feat: yay-style dependency resolution (topological sort, providers, circular detection) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- local/recipes/dev/cubl/recipe.toml | 2 +- .../cub/source/cub-lib/src/depresolve.rs | 204 ++++++++++++++++++ .../system/cub/source/cub-lib/src/lib.rs | 1 + .../system/cub/source/cub-tui/src/lib.rs | 117 +--------- 4 files changed, 215 insertions(+), 109 deletions(-) create mode 100644 local/recipes/system/cub/source/cub-lib/src/depresolve.rs diff --git a/local/recipes/dev/cubl/recipe.toml b/local/recipes/dev/cubl/recipe.toml index 3c575a0b9..ed68c5843 100644 --- a/local/recipes/dev/cubl/recipe.toml +++ b/local/recipes/dev/cubl/recipe.toml @@ -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" """ diff --git a/local/recipes/system/cub/source/cub-lib/src/depresolve.rs b/local/recipes/system/cub/source/cub-lib/src/depresolve.rs new file mode 100644 index 000000000..664fe891f --- /dev/null +++ b/local/recipes/system/cub/source/cub-lib/src/depresolve.rs @@ -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, + pub makedepends: Vec, + pub provides: Vec, +} + +#[derive(Debug, Clone)] +pub struct ResolvedOrder { + pub build_order: Vec, + pub circular: Vec>, +} + +pub fn resolve_build_order(packages: &[DepNode]) -> ResolvedOrder { + let mut deps: HashMap> = HashMap::new(); + let mut providers: HashMap = 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 = 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 = deps.keys().cloned().collect(); + + while !remaining.is_empty() { + let ready: Vec = 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> = 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>, + remaining: &HashSet, +) -> Vec> { + let mut components = Vec::new(); + let mut visited: HashSet = 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 = 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, + seen: &mut HashSet, + depth: usize, +) -> Vec { + 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"); + } +} diff --git a/local/recipes/system/cub/source/cub-lib/src/lib.rs b/local/recipes/system/cub/source/cub-lib/src/lib.rs index 711de9d06..77a4a6e8e 100644 --- a/local/recipes/system/cub/source/cub-lib/src/lib.rs +++ b/local/recipes/system/cub/source/cub-lib/src/lib.rs @@ -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; diff --git a/local/recipes/system/cub/source/cub-tui/src/lib.rs b/local/recipes/system/cub/source/cub-tui/src/lib.rs index 576789f73..77557b188 100644 --- a/local/recipes/system/cub/source/cub-tui/src/lib.rs +++ b/local/recipes/system/cub/source/cub-tui/src/lib.rs @@ -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 ", - "Install a package from the official repo or BUR", - ), - ("-Ss ", "Search the official repo and cached BUR"), - ("-Si ", "Show AUR package information"), - ("-Sy", "Refresh BUR cache and verify AUR metadata access"), - ("-Syu", "Refresh metadata and update installed packages"), - ("-G ", "Import an AUR package into ~/.cub/recipes"), - ("-B ", "Build and install a local RBPKGBUILD directory"), - ("-R ", "Remove an installed package"), - ("-Q", "List installed packages"), - ("-Qi ", "Show installed package details"), - ("-Ql ", "List files installed by a package"), - ("-Sc", "Clean Cub and pkg download caches"), -]; - -const COMMANDS: &[(&str, &str)] = &[ - ("install ", "Install from repo or BUR"), - ("search ", "Search official repo and BUR cache"), - ("info ", "Query AUR metadata"), - ("sync", "Refresh BUR cache and AUR sync stamp"), - ( - "system-upgrade", - "Sync first, then update installed packages", - ), - ("build ", "Build and install a local recipe tree"), - ("get ", "Copy a BUR recipe into the current directory"), - ( - "get-aur ", - "Convert an AUR PKGBUILD into ~/.cub/recipes", - ), - ( - "inspect ", - "Inspect a local RBPKGBUILD or package metadata", - ), - ( - "import-aur ", - "Convert an AUR PKGBUILD into the current directory", - ), - ("update-all", "Update installed packages"), - ("remove ", "Remove an installed package"), - ("query-local", "List installed packages"), - ("query-info ", "Show installed package details"), - ("query-list ", "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() }