~comcloudway/cabin

7f1a4e45fca49409ee384806ab293e4c9eb49551 — Jakob Meier 1 year, 3 months ago 5348648
Added APKBUILD parsing and database generator
4 files changed, 284 insertions(+), 0 deletions(-)

A src/cmds/mod.rs
A src/cmds/scan.rs
A src/main.rs
A src/types.rs
A src/cmds/mod.rs => src/cmds/mod.rs +2 -0
@@ 0,0 1,2 @@
mod scan;
pub use scan::scan;

A src/cmds/scan.rs => src/cmds/scan.rs +17 -0
@@ 0,0 1,17 @@
use std::{path::PathBuf, fs};

use crate::types::Database;

pub fn scan(folder: Option<PathBuf>, db: Option<PathBuf>) {
    let data = Database::from_scan(
        folder.unwrap_or(
            PathBuf::from(".")));

    let text = serde_yaml::to_string(&data)
        .expect("Unable to create database");

    let target = db.unwrap_or(PathBuf::from("db.yml"));
    if fs::write(target, text).is_err() {
        println!("Unable to write database to disk");
    }
}

A src/main.rs => src/main.rs +52 -0
@@ 0,0 1,52 @@
use std::{path::PathBuf, collections::HashMap};
use clap::Parser;

mod cmds;
mod types;

#[derive(Parser)]
#[command(author, version, about, long_about = None)]
enum Commands {
    /// Lists all known packages
    List {
        /// package database path
        db: Option<PathBuf>
    },
    /// Rescans all folders
    Scan {
        folder: Option<PathBuf>,
        /// package database path
        db: Option<PathBuf>
    },
    /// Builds the package
    Build {
        /// the packagename
        package: String,
        /// package database path
        db: Option<PathBuf>
    },
    /// Displays package information
    Info {
        /// the packagename
        package: String,
        /// package database path
        db: Option<PathBuf>
    },
    /// Searches the database for a package with the name
    /// or containing the name
    Search {
        /// search query
        query: String,
        /// package database path
        db: Option<PathBuf>
    }
}

fn main() {
    let cli = Commands::parse();

    match cli {
        Commands::Scan { folder, db } => cmds::scan(folder, db),
        _ => panic!("Unimplemented")
    }
}

A src/types.rs => src/types.rs +213 -0
@@ 0,0 1,213 @@
use std::{path::PathBuf, collections::{HashMap, HashSet}, fs::{FileType, DirEntry, self}};

use regex::Regex;
use serde::{Deserialize, Serialize};
use walkdir::WalkDir;

/// make it easy to change the type later
/// and make it easier to understand the types
pub type PackageName = String;

/// Additional package information
#[derive(Serialize, Deserialize, Debug)]
pub struct Package {
    /// the name of the package
    name: String,
    /// the version of the package
    version: String,
    /// license used by the package
    license: String,
    /// project url
    url: String,
    /// package description
    description: String,
    /// package release number (starts at zero)
    rel: usize,
    /// list? of supported/disabled architectures
    arch: HashSet<String>,
    /// relative path to APKBUILD
    path: PathBuf
}

/// Package information collection
#[derive(Serialize, Deserialize, Debug)]
pub struct Database {
    /// dictionary of package names associated with a dictionary
    /// of additional package info
    packages: HashMap<PackageName, Package>,
    /// dictionary of package names associated with a dependency list
    /// the dependencies may not create cycles
    /// all dependencies of a given package have to be build,
    /// before the package can be build
    depends: HashMap<PackageName, HashSet<PackageName>>,
    /// dictionary of provide names associated with a list of packages,
    /// which provide the given package
    /// in case multiple packages provide the same provider,
    /// all of them should be build
    /// (runtime apk should take care of the rest)
    provides: HashMap<PackageName, HashSet<PackageName>>
}
impl Database {
    pub fn from_scan(folder: PathBuf) -> Self {
        let mut db = Self {
            packages: HashMap::new(),
            depends: HashMap::new(),
            provides: HashMap::new(),
        };

        for entry in WalkDir::new(folder).follow_links(true) {
            // ignore broken entries
            if let Ok(entry) = entry {
                if entry.file_type().is_dir() {
                    // ignore folders themselves
                    continue;
                }
                if entry.file_type().is_file() && entry.file_name() != "APKBUILD" {
                    // ignore every file that is not the APKBUILD file itself
                    continue;
                }


                let file = entry.into_path();
                let folder = file.parent().expect("Unable to get APKBUILD dir").to_path_buf();

                let apk = APKBUILD::from_file(file);
                let depends = apk.depends;
                let mut provides = apk.provides;
                for sub in apk.subs {
                    provides.insert(sub);
                }
                let pkg = Package {
                    name: apk.name.to_string(),
                    arch: apk.arch,
                    version: apk.version,
                    license: apk.license,
                    description: apk.description,
                    url: apk.url,
                    rel: apk.rel,
                    path: folder
                };

                db.packages.insert(apk.name.to_string(), pkg);
                db.depends.insert(apk.name.to_string(), depends);
                db.provides.insert(apk.name.to_string(), provides);
            }
        }

        db
    }
}

/// unprocessed Data directly extracted from APKBUILD
#[derive(Debug)]
struct APKBUILD {
    /// the name of the package
    name: String,
    /// the version of the package
    version: String,
    /// license used by the package
    license: String,
    /// project url
    url: String,
    /// package description
    description: String,
    /// package release number (starts at zero)
    rel: usize,
    /// list? of supported/disabled architectures
    arch: HashSet<String>,
    /// list of all required dependencies
    depends: HashSet<String>,
    /// list of all providers
    provides: HashSet<String>,
    /// list of subpackages
    subs: Vec<String>
}
impl APKBUILD {
    fn from_file(file: PathBuf) -> Self{
        let cont = fs::read_to_string(file)
            .expect("Failed to open APKBUILD");

        let mut apk = APKBUILD {
            name: easy_match("pkgname", &cont),
            description: easy_match("pkgdesc", &cont),
            rel: easy_match("pkgrel", &cont).parse()
                                            .expect("pkgrel should be a number"),
            arch: HashSet::new(),
            version: easy_match("pkgver", &cont),
            license: easy_match("license", &cont),
            url: easy_match("url", &cont),
            depends: HashSet::new(),
            provides: HashSet::new(),
            subs: Vec::new()
        };

        for dep in easy_match_multi("depends", &cont) {
            apk.depends.insert(dep);
        }
        for provider in easy_match_multi("provides", &cont) {
            apk.provides.insert(provider);
        }
        for arch in easy_match_multi("arch", &cont) {
            apk.arch.insert(arch);
        }
        for subpkg in easy_match_multi("subpackages", &cont) {
            let subpkg = subpkg
                .replace("$pkgname", "${pkgname}")
                .replace("${pkgname}", &apk.name);
            apk.subs.push(subpkg);
        }

        apk
    }
}

/// extracts the first occurence of the given variable from the text
/// automatically removes whitespaces and quotes
/// panics if the variable is not defined
fn easy_match(var: &str, text: &str) -> String {
    let re = Regex::new(&format!("{}=\"[^\"]+\"|{}=[^\"\n]+\n", var,var)).unwrap();
    let extr = re.find(text)
                 .expect(&format!("Invalid APKBUILD, expected to find {}", var)).as_str();

    // tidy up the value, by removing unneccesarry characters
    // var="<want>"
    let value = extr.split_at(var.len())
                    .1
                    .trim()
                    .trim_start_matches('=')
                    .trim_start_matches('"')
                    .trim_end_matches('"')
                    .trim();

    String::from(value)
}

/// extracts every occurence of the given variable from the text
/// automatically removes whitespaces and quotes
fn easy_match_multi(var: &str, text: &str) -> Vec<String> {
    let re = Regex::new(&format!("{}=\"[^\"]+\"|{}=[^\"\n]+\n", var,var)).unwrap();

    let mut v = Vec::new();

    for extr in re.find_iter(text) {
        let extr = extr.as_str();
        // prepare value for line & space loop
        let value = extr.split_at(var.len())
                        .1
                        .trim()
                        .trim_start_matches('=')
                        .trim_start_matches('"')
                        .trim_end_matches('"')
                        .trim();

        for line in value.lines() {
            let line = line.trim();
            for dep in line.split(' ') {
                // values can be seperated by lines and/or spaces
                v.push(dep.to_string());
            }
        }
    }

    v
}