From 7f1a4e45fca49409ee384806ab293e4c9eb49551 Mon Sep 17 00:00:00 2001 From: Jakob Meier Date: Fri, 11 Aug 2023 09:25:13 +0200 Subject: [PATCH] Added APKBUILD parsing and database generator --- src/cmds/mod.rs | 2 + src/cmds/scan.rs | 17 ++++ src/main.rs | 52 ++++++++++++ src/types.rs | 213 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 284 insertions(+) create mode 100644 src/cmds/mod.rs create mode 100644 src/cmds/scan.rs create mode 100644 src/main.rs create mode 100644 src/types.rs diff --git a/src/cmds/mod.rs b/src/cmds/mod.rs new file mode 100644 index 0000000..2f310ab --- /dev/null +++ b/src/cmds/mod.rs @@ -0,0 +1,2 @@ +mod scan; +pub use scan::scan; diff --git a/src/cmds/scan.rs b/src/cmds/scan.rs new file mode 100644 index 0000000..b9c3293 --- /dev/null +++ b/src/cmds/scan.rs @@ -0,0 +1,17 @@ +use std::{path::PathBuf, fs}; + +use crate::types::Database; + +pub fn scan(folder: Option, db: Option) { + 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"); + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..95dcb2d --- /dev/null +++ b/src/main.rs @@ -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 + }, + /// Rescans all folders + Scan { + folder: Option, + /// package database path + db: Option + }, + /// Builds the package + Build { + /// the packagename + package: String, + /// package database path + db: Option + }, + /// Displays package information + Info { + /// the packagename + package: String, + /// package database path + db: Option + }, + /// Searches the database for a package with the name + /// or containing the name + Search { + /// search query + query: String, + /// package database path + db: Option + } +} + +fn main() { + let cli = Commands::parse(); + + match cli { + Commands::Scan { folder, db } => cmds::scan(folder, db), + _ => panic!("Unimplemented") + } +} diff --git a/src/types.rs b/src/types.rs new file mode 100644 index 0000000..37f8e77 --- /dev/null +++ b/src/types.rs @@ -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, + /// 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, + /// 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>, + /// 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> +} +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, + /// list of all required dependencies + depends: HashSet, + /// list of all providers + provides: HashSet, + /// list of subpackages + subs: Vec +} +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="" + 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 { + 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 +} -- 2.38.5