aboutsummaryrefslogtreecommitdiff
path: root/src/gentoo
diff options
context:
space:
mode:
authorpablu <me@pablu.de>2026-05-19 20:51:10 +0200
committerpablu <me@pablu.de>2026-05-19 20:51:10 +0200
commitafe649e0efc31fcd33a828e54bd6313cd37f1784 (patch)
treec2cec9450cc1778f62c6339dad9c8f7d43c46b16 /src/gentoo
parenta6b4b29aaa222fa5c2f30cad233e97cb8125f96f (diff)
Add available package loading, update README.md, use eyre_color::Result
Diffstat (limited to 'src/gentoo')
-rw-r--r--src/gentoo/mod.rs2
-rw-r--r--src/gentoo/package.rs92
-rw-r--r--src/gentoo/portage.rs281
-rw-r--r--src/gentoo/useflags.rs11
4 files changed, 290 insertions, 96 deletions
diff --git a/src/gentoo/mod.rs b/src/gentoo/mod.rs
index ff343bc..e2cc3df 100644
--- a/src/gentoo/mod.rs
+++ b/src/gentoo/mod.rs
@@ -2,6 +2,6 @@ pub mod package;
pub mod portage;
pub mod useflags;
-pub use package::Package;
+pub use package::InstalledPackage;
pub use portage::Portage;
pub use useflags::UseFlag;
diff --git a/src/gentoo/package.rs b/src/gentoo/package.rs
index a74a09c..d1596f7 100644
--- a/src/gentoo/package.rs
+++ b/src/gentoo/package.rs
@@ -1,44 +1,70 @@
use std::collections::HashSet;
+use chrono::{DateTime, Utc};
+
use crate::gentoo::UseFlag;
-#[derive(Debug, Clone)]
-#[allow(dead_code)]
-pub struct Package {
+#[derive(Debug, Clone, Hash, Eq, PartialEq)]
+pub struct PackageKey {
+ pub category: String,
pub name: String,
- pub use_flags: HashSet<UseFlag>,
- pub version: String,
- pub repository: String,
+}
+
+impl PackageKey {
+ pub fn from_atom(s: &str) -> Option<Self> {
+ let (category, name) = s.split_once('/')?;
+ Some(Self {
+ category: category.to_string(),
+ name: name.to_string(),
+ })
+ }
+
+ pub fn qualified_name(&self) -> String {
+ format!("{}/{}", self.category, self.name)
+ }
+}
+
+#[derive(Debug, Clone)]
+pub struct Metadata {
pub maintainer: Option<String>,
pub description: Option<String>,
- pub homepage: Option<Vec<String>>,
+ pub homepage: Vec<String>,
pub license: Option<String>,
+ pub repository: String,
+}
+
+#[derive(Debug, Clone)]
+#[allow(dead_code)]
+pub struct InstalledPackage {
+ pub atom: PackageKey,
+
+ pub version: String,
+
+ pub build_time: DateTime<Utc>,
+
+ pub slot: i32,
+
+ pub metadata: Metadata,
+
+ pub enabled_use_flags: HashSet<String>,
+ pub iuse: Vec<UseFlag>,
+
pub size: usize,
}
-// #[allow(dead_code)]
-// impl Package {
-// pub fn new(
-// name: String,
-// version: String,
-// repository: String,
-// size: usize,
-// homepage: Option<Vec<String>>,
-// license: Option<String>,
-// description: Option<String>,
-// maintainer: Option<String>,
-// use_flags: HashSet<UseFlag>,
-// ) -> Self {
-// Package {
-// name,
-// use_flags,
-// version,
-// repository,
-// maintainer,
-// description,
-// homepage,
-// license,
-// size,
-// }
-// }
-// }
+#[derive(Debug)]
+pub struct Package {
+ pub atom: PackageKey,
+
+ pub versions: Vec<Version>,
+}
+
+#[derive(Debug, Clone)]
+pub struct Version {
+ pub version: String,
+
+ pub metadata: Metadata,
+
+ pub keywords: Vec<String>,
+ pub iuse: Vec<UseFlag>,
+}
diff --git a/src/gentoo/portage.rs b/src/gentoo/portage.rs
index bc9bb32..51dc60a 100644
--- a/src/gentoo/portage.rs
+++ b/src/gentoo/portage.rs
@@ -1,20 +1,26 @@
use std::{
- collections::HashSet,
- fs, io,
+ collections::{HashMap, HashSet},
+ fs,
path::{Path, PathBuf},
};
+use chrono::DateTime;
+use color_eyre::eyre::Context;
use quick_xml::{Reader, events::Event};
-use crate::gentoo::{Package, UseFlag};
+use crate::gentoo::{
+ InstalledPackage, UseFlag,
+ package::{Metadata, Package, PackageKey, Version},
+};
#[derive(Debug, Default)]
pub struct Portage {
- pub installed_packages: Vec<Package>,
- pub world_packages: Vec<String>,
+ pub installed_packages: Vec<InstalledPackage>,
+ pub world_packages: HashSet<PackageKey>,
+ pub available_packages: Vec<Package>,
}
-fn split_pkg(input: &str) -> (&str, Option<&str>) {
+fn split_pkg(input: &str) -> (&str, &str) {
let bytes = input.as_bytes();
for i in (0..bytes.len()).rev() {
@@ -24,14 +30,14 @@ fn split_pkg(input: &str) -> (&str, Option<&str>) {
{
let name = &input[..i];
let version = &input[i + 1..];
- return (name, Some(version));
+ return (name, version);
}
}
- (input, None)
+ (input, "")
}
-fn extract_maintainer(path: &Path) -> io::Result<Option<String>> {
+fn extract_maintainer(path: &Path) -> color_eyre::Result<Option<String>> {
let s = fs::read_to_string(path)?;
let mut reader = Reader::from_str(s.as_str());
reader.config_mut().trim_text(true);
@@ -69,32 +75,189 @@ fn extract_maintainer(path: &Path) -> io::Result<Option<String>> {
Ok(None)
}
+fn parse_cache_file(content: String) -> HashMap<String, String> {
+ content
+ .lines()
+ .filter_map(|line| {
+ line.split_once('=')
+ .map(|(k, v)| (k.to_string(), v.to_string()))
+ })
+ .collect()
+}
+
impl Portage {
pub fn new() -> Self {
Self {
installed_packages: vec![],
- world_packages: vec![],
+ world_packages: HashSet::new(),
+ available_packages: vec![],
}
}
- pub fn load_world_packages(&mut self) -> io::Result<()> {
+ pub fn load_world_packages() -> color_eyre::Result<HashSet<PackageKey>> {
let world = fs::read_to_string("/var/lib/portage/world")?;
- self.world_packages = world
+ Ok(world
.split_whitespace()
- .map(std::string::ToString::to_string)
- .collect();
+ .filter_map(PackageKey::from_atom)
+ .collect())
+ }
+
+ pub fn load_available_packages(&mut self) -> color_eyre::Result<()> {
+ let repos = fs::read_dir("/var/db/repos/").wrap_err("failed to read /var/db/repos")?;
+
+ let mut packages: HashMap<PackageKey, Package> = HashMap::new();
+
+ for repo in repos {
+ let Ok(repo) = repo else {
+ continue;
+ };
+ let repo_name = repo.file_name();
+ let Some(repo_name) = repo_name.to_str() else {
+ continue;
+ };
+
+ let Ok(categories) = fs::read_dir(repo.path().join("metadata/md5-cache")) else {
+ // TODO: Someday parse actual ebuilds instead of md5-cache
+ continue;
+ };
+
+ for category in categories {
+ let Ok(category) = category else {
+ continue;
+ };
+
+ let Ok(cat_name) = category.file_name().into_string() else {
+ continue;
+ };
+
+ if cat_name == "Manifest.gz" {
+ continue;
+ }
+
+ let pkgs = fs::read_dir(category.path())?;
+
+ for pkg in pkgs {
+ let Ok(pkg) = pkg else {
+ continue;
+ };
+
+ let Ok(pkg_name) = pkg.file_name().into_string() else {
+ continue;
+ };
+
+ if pkg_name == "Manifest.gz" {
+ continue;
+ }
+
+ let (pkg_name, pkg_version) = split_pkg(&pkg_name);
+
+ let version_file = fs::read_to_string(pkg.path()).wrap_err_with(|| {
+ format!(
+ "failed to read version_file for {}/{}-{} in repo {}",
+ cat_name, pkg_name, pkg_version, repo_name
+ )
+ })?;
+ let data = parse_cache_file(version_file);
+
+ let homepages: Vec<String> = data
+ .get("HOMEPAGE")
+ .and_then(|s| {
+ Some(
+ s.split_whitespace()
+ .map(std::string::ToString::to_string)
+ .collect(),
+ )
+ })
+ .unwrap_or(vec![]);
+
+ let keywords: Vec<String> = data
+ .get("KEYWORDS")
+ .and_then(|s| {
+ Some(
+ s.split_whitespace()
+ .map(std::string::ToString::to_string)
+ .collect(),
+ )
+ })
+ .unwrap_or(vec![]);
+
+ let iuse: Vec<UseFlag> = data
+ .get("IUSE")
+ .and_then(|s| {
+ Some(
+ s.split_whitespace()
+ .map(|original_iuse| {
+ let iuse = original_iuse
+ .strip_prefix("+")
+ .unwrap_or(original_iuse);
+
+ UseFlag {
+ name: iuse.to_string(),
+ default: iuse != original_iuse,
+ }
+ })
+ .collect(),
+ )
+ })
+ .unwrap_or(vec![]);
+
+ let repo_path = PathBuf::from("/var/db/repos/")
+ .join(&repo_name)
+ .join(&cat_name)
+ .join(pkg_name)
+ .join("metadata.xml");
+
+ let maintainer = extract_maintainer(&repo_path).unwrap_or(None);
+
+ let version = Version {
+ version: pkg_version.to_string(),
+ metadata: Metadata {
+ maintainer: maintainer,
+ description: data.get("DESCRIPTION").cloned(),
+ homepage: homepages,
+ license: data.get("LICENSE").cloned(),
+ repository: repo_name.to_string(),
+ },
+ keywords: keywords,
+ iuse: iuse,
+ };
+
+ let pkg_key = PackageKey {
+ category: cat_name.clone(),
+ name: pkg_name.to_string(),
+ };
+
+ if let Some(package) = packages.get_mut(&pkg_key) {
+ package.versions.push(version);
+ } else {
+ let pkg = Package {
+ atom: pkg_key.clone(),
+ versions: vec![version],
+ };
+ packages.insert(pkg_key, pkg);
+ }
+ }
+ }
+ }
+
+ self.available_packages = packages.into_values().collect();
Ok(())
}
- pub fn load_installed_packages(&mut self) -> io::Result<()> {
+ pub fn load_installed_packages() -> color_eyre::Result<Vec<InstalledPackage>> {
let categories = fs::read_dir("/var/db/pkg")?;
+ let mut v = vec![];
+
for category in categories {
let Ok(category) = category else {
continue;
};
- let cat_name = category.file_name();
+ let Ok(cat_name) = category.file_name().into_string() else {
+ continue;
+ };
+
let pkgs = fs::read_dir(category.path())?;
for pkg in pkgs {
let Ok(pkg) = pkg else {
@@ -109,37 +272,36 @@ impl Portage {
let (pkg_name, pkg_version) = split_pkg(pkg_name);
let use_flags_file = fs::read_to_string(pkg.path().join("USE"))?;
- let use_flags: HashSet<&str> = use_flags_file.trim().split(' ').collect();
+ let enabled_use_flags: HashSet<String> = use_flags_file
+ .split_whitespace()
+ .map(|s| s.to_string())
+ .collect();
let iuse_flags_file = fs::read_to_string(pkg.path().join("IUSE"))?;
- let iuse_flags: Vec<&str> = iuse_flags_file.trim().split(' ').collect();
- let use_flags: HashSet<UseFlag> = iuse_flags
- .iter()
- .filter(|x| !x.trim().is_empty())
- .map(|&original_iuse| {
+ let use_flags: Vec<UseFlag> = iuse_flags_file
+ .split_whitespace()
+ .map(|original_iuse| {
let iuse = original_iuse.strip_prefix("+").unwrap_or(original_iuse);
- let is_default = iuse != original_iuse;
-
- let is_active = use_flags.contains(&iuse);
- UseFlag::new(iuse.into(), is_active, is_default)
+ UseFlag {
+ name: iuse.to_string(),
+ default: iuse != original_iuse,
+ }
})
.collect();
let repository = fs::read_to_string(pkg.path().join("repository"))?
.trim()
.to_string();
- let homepage: Option<Vec<String>> =
- match fs::read_to_string(pkg.path().join("HOMEPAGE")) {
- Ok(homepage) => Some(
- homepage
- .split_whitespace()
- .map(std::string::ToString::to_string)
- .collect(),
- ),
- Err(_) => None,
- };
+
+ let homepage: Vec<String> = match fs::read_to_string(pkg.path().join("HOMEPAGE")) {
+ Ok(homepage) => homepage
+ .split_whitespace()
+ .map(std::string::ToString::to_string)
+ .collect(),
+ Err(_) => vec![],
+ };
let license: Option<String> = match fs::read_to_string(pkg.path().join("LICENSE")) {
Ok(license) => Some(license.trim().to_string()),
@@ -152,33 +314,50 @@ impl Portage {
Err(_) => None,
};
- let size: usize = match fs::read_to_string(pkg.path().join("SIZE")) {
- Ok(size) => size.trim().parse().unwrap_or(0),
- Err(_) => 0,
- };
+ let size: usize = fs::read_to_string(pkg.path().join("SIZE"))?
+ .trim()
+ .parse()
+ .unwrap_or(0);
let repo_path = PathBuf::from("/var/db/repos/")
.join(&repository)
- .join(cat_name.to_str().unwrap_or_default())
+ .join(&cat_name)
.join(pkg_name)
.join("metadata.xml");
+ let build_time = fs::read_to_string(pkg.path().join("BUILD_TIME"))?
+ .trim()
+ .parse::<i64>()
+ .ok()
+ .and_then(DateTime::from_timestamp_secs)
+ .unwrap_or(DateTime::from_timestamp_nanos(0));
+
let maintainer = extract_maintainer(&repo_path)?;
- self.installed_packages.push(Package {
- name: format!("{}/{}", cat_name.to_str().unwrap_or_default(), pkg_name),
- use_flags,
- version: pkg_version.unwrap_or_default().into(),
- repository,
- maintainer,
- description,
- homepage,
- license,
+ let atom = PackageKey {
+ category: cat_name.clone(),
+ name: pkg_name.to_string(),
+ };
+
+ v.push(InstalledPackage {
+ atom: atom,
+ version: pkg_version.to_string(),
+ build_time,
+ slot: 0,
+ metadata: Metadata {
+ maintainer,
+ description,
+ homepage: homepage,
+ license,
+ repository: repository,
+ },
+ enabled_use_flags: enabled_use_flags,
+ iuse: use_flags,
size,
});
}
}
- Ok(())
+ Ok(v)
}
}
diff --git a/src/gentoo/useflags.rs b/src/gentoo/useflags.rs
index 1e83b32..2bcd678 100644
--- a/src/gentoo/useflags.rs
+++ b/src/gentoo/useflags.rs
@@ -1,16 +1,5 @@
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct UseFlag {
pub name: String,
- pub enabled: bool,
pub default: bool,
}
-
-impl UseFlag {
- pub fn new(name: String, enabled: bool, default: bool) -> Self {
- UseFlag {
- name,
- enabled,
- default,
- }
- }
-}