use std::collections::{HashMap, HashSet};
use std::hash::BuildHasher;
use crate::deps::{
get_foreign_packages, get_installed_versions, is_package_installed_or_provided, parse_dep_spec,
parse_pkgbuild_deps, parse_srcinfo_deps, version_satisfies,
};
use crate::types::sandbox::{DependencyDelta, SandboxInfo};
#[must_use]
pub fn analyze_pkgbuild<S: BuildHasher>(
package_name: &str,
pkgbuild_text: &str,
installed: &HashSet<String, S>,
provided: &HashSet<String, S>,
) -> SandboxInfo {
let (depends, makedepends, checkdepends, optdepends) = parse_pkgbuild_deps(pkgbuild_text);
build_info(
package_name,
&depends,
&makedepends,
&checkdepends,
&optdepends,
installed,
provided,
)
}
#[must_use]
pub fn analyze_srcinfo<S: BuildHasher>(
package_name: &str,
srcinfo_text: &str,
installed: &HashSet<String, S>,
provided: &HashSet<String, S>,
) -> SandboxInfo {
let (depends, makedepends, checkdepends, optdepends) = parse_srcinfo_deps(srcinfo_text);
build_info(
package_name,
&depends,
&makedepends,
&checkdepends,
&optdepends,
installed,
provided,
)
}
fn build_info<S: BuildHasher>(
package_name: &str,
depends: &[String],
makedepends: &[String],
checkdepends: &[String],
optdepends: &[String],
installed: &HashSet<String, S>,
provided: &HashSet<String, S>,
) -> SandboxInfo {
let host = HostState::query();
SandboxInfo {
package_name: package_name.to_string(),
depends: analyze_with_host(depends, installed, provided, &host),
makedepends: analyze_with_host(makedepends, installed, provided, &host),
checkdepends: analyze_with_host(checkdepends, installed, provided, &host),
optdepends: analyze_with_host(optdepends, installed, provided, &host),
}
}
struct HostState {
versions: HashMap<String, String>,
foreign: HashSet<String>,
}
impl HostState {
fn query() -> Self {
Self {
versions: get_installed_versions(),
foreign: get_foreign_packages(),
}
}
}
#[must_use]
pub fn analyze_dependencies<S: BuildHasher>(
deps: &[String],
installed: &HashSet<String, S>,
provided: &HashSet<String, S>,
) -> Vec<DependencyDelta> {
analyze_with_host(deps, installed, provided, &HostState::query())
}
fn analyze_with_host<S: BuildHasher>(
deps: &[String],
installed: &HashSet<String, S>,
provided: &HashSet<String, S>,
host: &HostState,
) -> Vec<DependencyDelta> {
deps.iter()
.filter_map(|dep_spec| {
let pkg_name = extract_package_name(dep_spec);
let is_installed = is_package_installed_or_provided(&pkg_name, installed, provided);
if is_installed && host.foreign.contains(&pkg_name) {
return None;
}
let installed_version = if is_installed {
host.versions.get(&pkg_name).cloned()
} else {
None
};
let spec_only = dep_spec
.split_once(": ")
.map_or(dep_spec.as_str(), |(spec, _desc)| spec);
let version_req = parse_dep_spec(spec_only).version_req;
let version_satisfied = installed_version
.as_ref()
.is_some_and(|version| version_satisfies(version, &version_req));
Some(DependencyDelta {
name: dep_spec.clone(),
is_installed,
installed_version,
version_satisfied,
})
})
.collect()
}
#[must_use]
pub fn extract_package_name(dep_spec: &str) -> String {
let spec_only = dep_spec
.split_once(':')
.map_or(dep_spec, |(before, _)| before);
parse_dep_spec(spec_only.trim()).name
}
#[cfg(test)]
mod tests {
use super::*;
fn sets(installed: &[&str]) -> (HashSet<String>, HashSet<String>) {
(
installed.iter().map(ToString::to_string).collect(),
HashSet::new(),
)
}
#[test]
fn membership() {
let (installed, provided) = sets(&["glibc"]);
let deps = vec![
"glibc".to_string(),
"definitely-not-installed-xyz".to_string(),
];
let deltas = analyze_dependencies(&deps, &installed, &provided);
assert_eq!(deltas.len(), 2);
assert!(deltas[0].is_installed);
assert!(!deltas[1].is_installed);
assert!(deltas[1].installed_version.is_none());
assert!(!deltas[1].version_satisfied);
}
#[test]
fn provided_counts_as_installed() {
let installed: HashSet<String> = HashSet::new();
let provided: HashSet<String> = HashSet::from(["arch-toolkit-virtual-fixture".to_string()]);
let deltas = analyze_dependencies(
&["arch-toolkit-virtual-fixture".to_string()],
&installed,
&provided,
);
assert!(deltas[0].is_installed);
}
#[test]
fn name_extraction() {
assert_eq!(extract_package_name("python>=3.12"), "python");
assert_eq!(extract_package_name("qt6-base<7"), "qt6-base");
assert_eq!(extract_package_name("libfoo=1.0"), "libfoo");
assert_eq!(extract_package_name("cups: printing support"), "cups");
assert_eq!(extract_package_name(" glibc "), "glibc");
}
#[test]
fn pkgbuild_analysis() {
let pkgbuild = r"
depends=('glibc' 'missing-dep-xyz')
makedepends=('cmake')
optdepends=('cups: printing support')
";
let (installed, provided) = sets(&["glibc", "cmake"]);
let info = analyze_pkgbuild("demo", pkgbuild, &installed, &provided);
assert_eq!(info.package_name, "demo");
assert_eq!(info.depends.len(), 2);
assert_eq!(info.makedepends.len(), 1);
assert_eq!(info.optdepends.len(), 1);
assert_eq!(info.missing_packages(), ["missing-dep-xyz"]);
assert!(!info.is_ready_to_build());
}
#[test]
fn srcinfo_analysis() {
let srcinfo = "pkgbase = demo\n\tdepends = glibc\n\tmakedepends = rust\n\toptdepends = cups: printing";
let (installed, provided) = sets(&["glibc", "rust"]);
let info = analyze_srcinfo("demo", srcinfo, &installed, &provided);
assert!(info.is_ready_to_build());
assert!(info.missing_packages().is_empty());
assert_eq!(info.optdepends.len(), 1);
}
#[test]
fn empty_input() {
let (installed, provided) = sets(&[]);
let info = analyze_pkgbuild("empty", "", &installed, &provided);
assert!(info.depends.is_empty());
assert!(info.is_ready_to_build());
}
}