@@ 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")
+ }
+}
@@ 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
+}