~comcloudway/cabin

a8fb0845bb566684b7c37abacf88f15117afa48c — Jakob Meier 1 year, 1 month ago 1c4af03
Initial dependency tree & package build support

Added tree command to show dependency list
and build command to build packages
8 files changed, 321 insertions(+), 9 deletions(-)

M Cargo.lock
M Cargo.toml
A src/cmds/build.rs
M src/cmds/info.rs
M src/cmds/mod.rs
A src/cmds/tree.rs
M src/main.rs
M src/types.rs
M Cargo.lock => Cargo.lock +7 -0
@@ 74,6 74,7 @@ dependencies = [
 "regex",
 "serde",
 "serde_yaml",
 "topological-sort",
 "walkdir",
]



@@ 355,6 356,12 @@ dependencies = [
]

[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"

M Cargo.toml => Cargo.toml +1 -0
@@ 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"

A src/cmds/build.rs => src/cmds/build.rs +99 -0
@@ 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<String>, 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<String>, 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<String>, 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<PathBuf>, arch: Option<String>, 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);
    }
}

M src/cmds/info.rs => src/cmds/info.rs +3 -4
@@ 35,10 35,9 @@ pub fn info(name: PackageName, db: Option<PathBuf>, 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);
        }

    }

M src/cmds/mod.rs => src/cmds/mod.rs +4 -0
@@ 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;

A src/cmds/tree.rs => src/cmds/tree.rs +157 -0
@@ 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<PackageName> {
    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<PackageName> {
    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(&current) {
            // already processed vertex
            continue;
        }

        let res = resolve_package(&current, 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(&current) || 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<Vec<PackageName>> {
    let mut ts = {
        let mut stack = LinkedList::new();
        let mut marker = HashSet::new();
        stack.push_front(start.to_string());


        let mut ts = TopologicalSort::<String>::new();
        ts.insert(start);

        // setup the topological sort dependency graph
        while ! stack.is_empty() {
            let current = stack.pop_back().unwrap();

            if marker.contains(&current) {
                // already processed vertex
                continue;
            }

            let res = resolve_package(&current, 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(&current) || 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<PathBuf>, 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);
        }
    }
}

M src/main.rs => src/main.rs +35 -2
@@ 10,6 10,7 @@ enum Commands {
    /// Lists all known packages
    List {
        /// package database path
        #[arg(short='d',long)]
        db: Option<PathBuf>
    },
    /// 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<String>,
        /// 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<PathBuf>
        #[arg(short='d',long)]
        db: Option<PathBuf>,
        #[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<PathBuf>,
        /// 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<PathBuf>
    },
    /// show a list of all dependencies
    Tree {
        /// the packagename
        package: String,
        /// package database path
        #[arg(short='d',long)]
        db: Option<PathBuf>,
        /// 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")
    }
}

M src/types.rs => src/types.rs +15 -3
@@ 25,7 25,9 @@ pub struct Package {
    /// list? of supported/disabled architectures
    pub arch: HashSet<String>,
    /// relative path to APKBUILD
    pub path: PathBuf
    pub path: PathBuf,
    /// list of packages provides by this package
    pub provides: HashSet<PackageName>
}

/// 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