mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-14 06:22:24 +00:00
* feat(update): add craft-update crate for release checking, install, and self-updating * fix(update): provide non-windows stub for finish * fix(update): address CodeRabbit review feedback on craft-update * feat(dialog,self_update): enhance prompt styling, support standalone self-updates * fix(update): make startup prompt modal with Escape, skip non-file tar entries
162 lines
7.3 KiB
Rust
162 lines
7.3 KiB
Rust
//! Reading system package lists (dpkg, rpm, pacman, Flatpak) and matching them to Craft Apps.
|
|
//!
|
|
//! Pure functions over the tools' text output, so they're tested on every platform; only Linux
|
|
//! runs the tools (`detect::linux`).
|
|
|
|
use crate::{App, InstallSource, version};
|
|
|
|
/// One installed package that belongs to a Craft App.
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct Package {
|
|
pub manager: InstallSource,
|
|
/// The package's name (`photocraft-bin`) or Flatpak app id.
|
|
pub name: String,
|
|
/// As the package manager reports it (`0.5.0-1`); empty when it doesn't say.
|
|
pub version: String,
|
|
}
|
|
|
|
/// `dpkg-query -W -f '${Package}|${db:Status-Status}|${Version}\n'`: installed packages only.
|
|
pub fn parse_dpkg(text: &str) -> Vec<(String, String)> {
|
|
text.lines()
|
|
.filter_map(|line| {
|
|
let mut f = line.split('|');
|
|
match (f.next(), f.next(), f.next()) {
|
|
(Some(name), Some("installed"), Some(ver)) => {
|
|
let name = name.split_once(':').map(|(n, _)| n).unwrap_or(name);
|
|
Some((name.to_string(), ver.to_string()))
|
|
}
|
|
_ => None,
|
|
}
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// `rpm -qa --qf '%{NAME}|%{VERSION}\n'`.
|
|
pub fn parse_rpm(text: &str) -> Vec<(String, String)> {
|
|
text.lines().filter_map(|line| line.split_once('|').map(|(n, v)| (n.to_string(), v.to_string()))).collect()
|
|
}
|
|
|
|
/// `pacman -Q`: `name version` per line.
|
|
pub fn parse_pacman(text: &str) -> Vec<(String, String)> {
|
|
text.lines()
|
|
.filter_map(|line| {
|
|
let mut f = line.split_whitespace();
|
|
Some((f.next()?.to_string(), f.next()?.to_string()))
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// `flatpak list --app --columns=application,version`: the version column is often empty for
|
|
/// bundles built from a release tarball.
|
|
pub fn parse_flatpak_list(text: &str) -> Vec<(String, String)> {
|
|
text.lines()
|
|
.filter_map(|line| {
|
|
let mut f = line.split('\t');
|
|
let id = f.next()?.trim();
|
|
(!id.is_empty()).then(|| (id.to_string(), f.next().unwrap_or("").trim().to_string()))
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// The `Version:` line of `flatpak info <id>`.
|
|
pub fn parse_flatpak_info(text: &str) -> Option<String> {
|
|
text.lines().find_map(|l| l.trim().strip_prefix("Version:").map(|v| v.trim().to_string())).filter(|v| !v.is_empty())
|
|
}
|
|
|
|
/// The package names a Craft App may be installed under with `manager`. On Arch the AUR carries
|
|
/// each app three ways: built from source (`photocraft`), the release binary (`photocraft-bin`)
|
|
/// and the latest commit (`photocraft-git`). Debian and RPM packages may use either the short stem
|
|
/// (`photocraft`) or the reverse-DNS bundle id (`ai.storyteller.photocraft`).
|
|
pub fn candidate_names(app: &App, manager: InstallSource) -> Vec<String> {
|
|
match manager {
|
|
InstallSource::Flatpak => vec![app.bundle_id()],
|
|
InstallSource::Pacman => {
|
|
app.stems().flat_map(|s| [s.to_string(), format!("{s}-bin"), format!("{s}-git")]).chain(std::iter::once(app.bundle_id())).collect()
|
|
}
|
|
InstallSource::Deb | InstallSource::Rpm => app.stems().map(str::to_string).chain(std::iter::once(app.bundle_id())).collect(),
|
|
_ => app.stems().map(str::to_string).collect(),
|
|
}
|
|
}
|
|
|
|
/// The package of `app` among `installed`, preferring the manager listed first in `order`.
|
|
pub fn find<'a>(app: &App, installed: &'a [Package], order: &[InstallSource]) -> Option<&'a Package> {
|
|
order.iter().find_map(|manager| {
|
|
let names = candidate_names(app, *manager);
|
|
names.iter().find_map(|n| installed.iter().find(|p| p.manager == *manager && p.name == *n))
|
|
})
|
|
}
|
|
|
|
/// A package's version, read the package-manager way (`0.5.0-1` is 0.5.0).
|
|
pub fn version_of(p: &Package) -> Option<version::Version> {
|
|
version::parse_package(&p.version)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn pkgs(manager: InstallSource, list: Vec<(String, String)>) -> Vec<Package> {
|
|
list.into_iter().map(|(name, version)| Package { manager, name, version }).collect()
|
|
}
|
|
|
|
#[test]
|
|
fn reads_each_package_managers_listing() {
|
|
assert_eq!(
|
|
parse_dpkg("photocraft|installed|0.5.0-1\nphotocraft:amd64|installed|0.6.0-1\nwordcraft|config-files|0.3.0-1\n"),
|
|
[("photocraft".into(), "0.5.0-1".into()), ("photocraft".into(), "0.6.0-1".into())]
|
|
);
|
|
assert_eq!(parse_rpm("gridcraft|0.3.0\n"), [("gridcraft".into(), "0.3.0".into())]);
|
|
assert_eq!(parse_pacman("photocraft-bin 0.5.0-1\nlinux 6.11.1.arch1-1\n").len(), 2);
|
|
assert_eq!(
|
|
parse_flatpak_list("ai.storyteller.wordcraft\t\nai.storyteller.cadcraft\t0.3.0\n"),
|
|
[("ai.storyteller.wordcraft".into(), String::new()), ("ai.storyteller.cadcraft".into(), "0.3.0".into())]
|
|
);
|
|
let info = "\nWordCraft - Word processor\n\n ID: ai.storyteller.wordcraft\n Version: 0.3.0\n Branch: master\n";
|
|
assert_eq!(parse_flatpak_info(info).as_deref(), Some("0.3.0"));
|
|
assert_eq!(parse_flatpak_info("Version: \n"), None);
|
|
}
|
|
|
|
#[test]
|
|
fn candidate_names_cover_bundle_ids_for_deb_rpm_and_pacman() {
|
|
let photo = App::by_id("photocraft").unwrap();
|
|
let deb_names = candidate_names(photo, InstallSource::Deb);
|
|
assert!(deb_names.contains(&"photocraft".to_string()));
|
|
assert!(deb_names.contains(&"ai.storyteller.photocraft".to_string()));
|
|
|
|
let rpm_names = candidate_names(photo, InstallSource::Rpm);
|
|
assert!(rpm_names.contains(&"photocraft".to_string()));
|
|
assert!(rpm_names.contains(&"ai.storyteller.photocraft".to_string()));
|
|
|
|
let pacman_names = candidate_names(photo, InstallSource::Pacman);
|
|
assert!(pacman_names.contains(&"photocraft".to_string()));
|
|
assert!(pacman_names.contains(&"photocraft-bin".to_string()));
|
|
assert!(pacman_names.contains(&"ai.storyteller.photocraft".to_string()));
|
|
}
|
|
|
|
#[test]
|
|
fn finds_aur_packages_in_every_flavour() {
|
|
let photo = App::by_id("photocraft").unwrap();
|
|
let pdf = App::by_id("pdfcraft").unwrap();
|
|
let installed =
|
|
pkgs(InstallSource::Pacman, parse_pacman("linux 6.11.1.arch1-1\nphotocraft-git 0.3.0.r47.g47f9306-1\nprintcraft-bin 0.2.1-1\n"));
|
|
let hit = find(photo, &installed, &[InstallSource::Pacman]).unwrap();
|
|
assert_eq!(hit.name, "photocraft-git");
|
|
assert_eq!(version_of(hit).unwrap().to_string(), "0.3.0");
|
|
// PdfCraft under its old name.
|
|
assert_eq!(find(pdf, &installed, &[InstallSource::Pacman]).unwrap().name, "printcraft-bin");
|
|
assert!(find(App::by_id("wordcraft").unwrap(), &installed, &[InstallSource::Pacman]).is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn prefers_managers_in_order_and_reads_revisions() {
|
|
let photo = App::by_id("photocraft").unwrap();
|
|
let mut installed = pkgs(InstallSource::Deb, parse_dpkg("photocraft|installed|0.5.0-1\n"));
|
|
installed.extend(pkgs(InstallSource::Flatpak, parse_flatpak_list("ai.storyteller.photocraft\t0.4.0\n")));
|
|
let hit = find(photo, &installed, &[InstallSource::Deb, InstallSource::Flatpak]).unwrap();
|
|
assert_eq!(hit.manager, InstallSource::Deb);
|
|
// 0.5.0-1 is the 0.5.0 release, not a pre-release of it.
|
|
assert_eq!(version_of(hit).unwrap().to_string(), "0.5.0");
|
|
assert_eq!(find(photo, &installed, &[InstallSource::Flatpak]).unwrap().version, "0.4.0");
|
|
}
|
|
}
|