From a8fb0845bb566684b7c37abacf88f15117afa48c Mon Sep 17 00:00:00 2001 From: Jakob Meier Date: Fri, 11 Aug 2023 21:36:06 +0200 Subject: [PATCH] Initial dependency tree & package build support Added tree command to show dependency list and build command to build packages --- Cargo.lock | 7 +++ Cargo.toml | 1 + src/cmds/build.rs | 99 +++++++++++++++++++++++++++++ src/cmds/info.rs | 7 +-- src/cmds/mod.rs | 4 ++ src/cmds/tree.rs | 157 ++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 37 ++++++++++- src/types.rs | 18 +++++- 8 files changed, 321 insertions(+), 9 deletions(-) create mode 100644 src/cmds/build.rs create mode 100644 src/cmds/tree.rs diff --git a/Cargo.lock b/Cargo.lock index 54cf412..61bc08a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -74,6 +74,7 @@ dependencies = [ "regex", "serde", "serde_yaml", + "topological-sort", "walkdir", ] @@ -354,6 +355,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "topological-sort" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea68304e134ecd095ac6c3574494fc62b909f416c4fca77e440530221e549d3d" + [[package]] name = "unicode-ident" version = "1.0.11" diff --git a/Cargo.toml b/Cargo.toml index 1fb6483..4b6d36d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,3 +14,4 @@ serde_yaml = "0.9" serde = { version = "1.0", features = [ "derive" ] } walkdir = "2.3" regex = { version = "1.9.3", features = [ "std" ] } +topological-sort = "0.2.2" diff --git a/src/cmds/build.rs b/src/cmds/build.rs new file mode 100644 index 0000000..ebc4ba9 --- /dev/null +++ b/src/cmds/build.rs @@ -0,0 +1,99 @@ +use std::{path::PathBuf, process::Command, collections::HashSet, fs}; + +use crate::types::{Database, PackageName}; + +/// returns true if the given architecture is whitelisted +/// in the working architecture list +fn arch_in(def: &HashSet, arch: &str) -> bool { + if def.contains(&format!("!{}", arch)) { + return false; + } + + if def.contains(arch) { + return true; + } + + if def.contains("all") || def.contains("noarch") { + return true; + } + + return false; +} + +/// builds a single package without checking the dependencies +fn build_package(name: PackageName, db: &Database, arch: &Option, verbose:bool) { + let pkg = db.packages.get(&name).expect("Package not found"); + + let mut child = Command::new("sh"); + + if let Some(arch) = arch { + // check if arch is supported + if ! arch_in(&pkg.arch, &arch) { + panic!("{} not available on {}", pkg.name, arch); + } + // build for target arch + child + .env("CBUILD_ARCH", arch) + .env("APORTSDIR", + &format!( + "{}/../../", + fs::canonicalize( + pkg.path.clone() + ).expect("Unable to get absolute path") + .to_str() + .expect("Unable to read path"))) + .arg("-c") + .arg( + &format!( + "cd {} && echo $CBUILD_ARCH && echo $APORTSDIR && abuild rootbld", + pkg.path.to_str().expect("Unable to read path"), + )); + } else { + // build on host arch + child + .arg("-c") + .arg( + &format!("cd {} && abuild -r", + pkg.path.to_str().expect("Unable to find folder"))); + } + + if verbose { + let mut child = child.spawn() + .expect(&format!("Unable to build {}", pkg.name)); + let ecode = child.wait().expect("failed to wait for builder").success(); + if !ecode { + panic!("Failed to build {}", pkg.name); + } + } else { + let child = child.output() + .expect(&format!("Unable to build {}", pkg.name)); + let ecode = child.status.success(); + if !ecode { + panic!("Failed to build {}", pkg.name); + } + + } +} + +/// builds a package including the dependencies +fn build_group(name: PackageName, db: &Database, arch: &Option, verbose:bool) { + let tasks = super::tree::topological_sort(&name, &db) + .expect("Unable to resolve dependencies, as there is a cycle in the graph"); + + for task in tasks { + build_package(task, db, arch, verbose); + } +} + +/// attempts to build a package and its dependencies +pub fn build(name: PackageName, db: Option, arch: Option, build_deps: bool, verbose:bool) { + let db = Database::from_file(db.unwrap_or(PathBuf::from("db.yml"))) + .expect("Unable to open database"); + + if build_deps { + // dependency resolution + build_group(name, &db, &arch, verbose); + } else { + build_package(name, &db, &arch, verbose); + } +} diff --git a/src/cmds/info.rs b/src/cmds/info.rs index cc9b02c..139932d 100644 --- a/src/cmds/info.rs +++ b/src/cmds/info.rs @@ -35,10 +35,9 @@ pub fn info(name: PackageName, db: Option, show_provides: bool, show_de if show_provides { println!("---Provides---"); - if let Some(prv) = db.provides.get(&name) { - for pr in prv { - println!("{}", pr); - } + let pkg = db.packages.get(&name).unwrap(); + for pr in &pkg.provides { + println!("{}", pr); } } diff --git a/src/cmds/mod.rs b/src/cmds/mod.rs index 9ab54f2..c6572e2 100644 --- a/src/cmds/mod.rs +++ b/src/cmds/mod.rs @@ -1,6 +1,10 @@ mod scan; mod list; mod info; +mod build; +mod tree; pub use scan::scan; pub use list::list; pub use info::info; +pub use build::build; +pub use tree::tree; diff --git a/src/cmds/tree.rs b/src/cmds/tree.rs new file mode 100644 index 0000000..f306f20 --- /dev/null +++ b/src/cmds/tree.rs @@ -0,0 +1,157 @@ +use std::{path::PathBuf, collections::{LinkedList, HashSet}}; + +use topological_sort::TopologicalSort; + +use crate::types::{PackageName, Database}; + +/// returns all packages vaguely associated with the named packed +/// this might be, because the named package is a subpackage, +/// or the named package is provided by multiple packages +fn resolve_package(name: &str, db: &Database) -> HashSet { + let mut can_use = HashSet::new(); + + if let Some(providers) = db.provides.get(name) { + for provider in providers { + if let Some(pkg) = db.packages.get(provider) { + can_use.insert(pkg.name.to_string()); + } + } + } + if let Some(pkg) = db.packages.get(name) { + can_use.insert(pkg.name.to_string()); + } + + can_use +} + +/// creates a list of all dependencies used by the package itself, +/// or by packages required by the start package +fn list_all_deps(start: PackageName, db: &Database, local_only: bool) -> HashSet { + let mut stack = LinkedList::new(); + let mut marker = HashSet::new(); + stack.push_front(start); + + while ! stack.is_empty() { + let current = stack.pop_back().unwrap(); + + if marker.contains(¤t) { + // already processed vertex + continue; + } + + let res = resolve_package(¤t, db); + if res.is_empty() { + // not a local dependency, + // so we can't go any deeper + if !local_only { + marker.insert(current); + } + } else { + for loc in res { + if let Some(deps) = db.depends.get(&loc) { + for dep in deps { + stack.push_front(dep.to_string()); + } + } + + // if the current package is provided by the given package, + // we add the package name of the provider-package + // to our list + if let Some(pkg) = db.packages.get(&loc) { + if pkg.provides.contains(¤t) || pkg.name == current { + marker.insert(pkg.name.to_string()); + } + } + } + } + + } + + return marker; +} + +/// topologically sorts all local dependencies +/// filters unknown dependencies +pub fn topological_sort(start: &PackageName, db: &Database) -> Option> { + let mut ts = { + let mut stack = LinkedList::new(); + let mut marker = HashSet::new(); + stack.push_front(start.to_string()); + + + let mut ts = TopologicalSort::::new(); + ts.insert(start); + + // setup the topological sort dependency graph + while ! stack.is_empty() { + let current = stack.pop_back().unwrap(); + + if marker.contains(¤t) { + // already processed vertex + continue; + } + + let res = resolve_package(¤t, db); + if !res.is_empty() { + for loc in res { + if let Some(deps) = db.depends.get(&loc) { + for dep in deps { + let resolved = resolve_package(dep, db); + for res in resolved { + stack.push_front(res.to_string()); + ts.add_dependency( + res.to_string(), + current.to_string() + ); + } + } + } + + // if the current package is provided by the given package, + // we add the package name of the provider-package + // to our list + if let Some(pkg) = db.packages.get(&loc) { + if pkg.provides.contains(¤t) || pkg.name == current { + marker.insert(pkg.name.to_string()); + } + } + } + } + } + + ts + }; + + let mut v = Vec::new(); + + while !ts.is_empty() { + let mut next = ts.pop_all(); + + if next.is_empty() { + // cycle detected + return None; + } + + v.append(&mut next); + } + + Some(v) +} + +pub fn tree(name: PackageName, db: Option, local_only: bool) { + let db = Database::from_file(db.unwrap_or(PathBuf::from("db.yml"))) + .expect("Unable to open database"); + + if local_only { + let sorted = topological_sort(&name, &db) + .expect("Unable to resolve dependencies, as there is a cycle in the graph"); + for dep in sorted { + println!("{}", dep); + } + } else { + let deps = list_all_deps(name, &db, false); + for dep in deps { + println!("{}", dep); + } + } +} diff --git a/src/main.rs b/src/main.rs index ecbd943..c2a744c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,6 +10,7 @@ enum Commands { /// Lists all known packages List { /// package database path + #[arg(short='d',long)] db: Option }, /// Rescans all folders @@ -22,17 +23,33 @@ enum Commands { Build { /// the packagename package: String, + /// build the package for a target architecture + /// requires abuild-rootbld to be installed, + /// and the .rootbld-repositories file to be configured correctly + /// (even if set to host architecture) + #[arg(short='a',long)] + arch: Option, + /// disables dependency resolution + /// wont compile the dependencies locally + /// however this will still pass -r to abuild + /// and fetch the prebuild packages from upstream + #[arg(short='r', default_value="false")] + no_build_dependencies: bool, /// package database path - db: Option + #[arg(short='d',long)] + db: Option, + #[arg(short='v', default_value="false")] + verbose: bool }, /// Displays package information Info { /// the packagename package: String, /// package database path + #[arg(short='d',long)] db: Option, /// prints the dependencies after the package info - #[arg(short='d',long)] + #[arg(short='r',long)] show_depends: bool, /// prints the packages provided by the package after the package info #[arg(short='p', long)] @@ -44,7 +61,21 @@ enum Commands { /// search query query: String, /// package database path + #[arg(short='d',long)] db: Option + }, + /// show a list of all dependencies + Tree { + /// the packagename + package: String, + /// package database path + #[arg(short='d',long)] + db: Option, + /// shows only local dependencies, + /// that can be found in the local database + /// will also topologically sort the dependencies + #[arg(short='l', default_value="false")] + only_local: bool, } } @@ -55,6 +86,8 @@ fn main() { Commands::Scan { folder, db } => cmds::scan(folder, db), Commands::List { db } => cmds::list(db), Commands::Info { package, db, show_depends, show_provides } => cmds::info(package, db, show_provides, show_depends), + Commands::Build { package, db, arch, no_build_dependencies, verbose } => cmds::build(package, db, arch, !no_build_dependencies, verbose), + Commands::Tree { package, db, only_local } => cmds::tree(package, db, only_local), _ => panic!("Unimplemented") } } diff --git a/src/types.rs b/src/types.rs index f6d9ea8..b8fa6b7 100644 --- a/src/types.rs +++ b/src/types.rs @@ -25,7 +25,9 @@ pub struct Package { /// list? of supported/disabled architectures pub arch: HashSet, /// relative path to APKBUILD - pub path: PathBuf + pub path: PathBuf, + /// list of packages provides by this package + pub provides: HashSet } /// Package information collection @@ -83,12 +85,22 @@ impl Database { description: apk.description, url: apk.url, rel: apk.rel, - path: folder + path: folder, + provides: provides.clone() }; db.packages.insert(apk.name.to_string(), pkg); db.depends.insert(apk.name.to_string(), depends); - db.provides.insert(apk.name.to_string(), provides); + + for provider in provides { + if let Some(table) = db.provides.get_mut(&provider) { + table.insert(apk.name.to_string()); + } else { + let mut empty = HashSet::new(); + empty.insert(apk.name.to_string()); + db.provides.insert(provider, empty); + } + } } db -- 2.38.5