use crate::deps::parse::{parse_dep_spec, parse_pacman_si_conflicts, parse_pacman_si_deps};
use crate::deps::pkgbuild::parse_pkgbuild_deps;
use crate::deps::query::{
get_available_version, get_installed_packages, get_installed_version, get_provided_packages,
get_upgradable_packages, is_package_installed_or_provided,
};
use crate::deps::source::{determine_dependency_source, is_system_package};
use crate::deps::version::version_satisfies;
use crate::error::Result;
use crate::types::dependency::{
Dependency, DependencySource, DependencyStatus, PackageRef, PackageSource, ResolverConfig,
};
use std::collections::{HashMap, HashSet};
use std::hash::BuildHasher;
use std::process::{Command, Stdio};
type PkgbuildCacheFn = dyn Fn(&str) -> Option<String> + Send + Sync;
pub fn determine_status<S: BuildHasher>(
name: &str,
version_req: &str,
installed: &HashSet<String, S>,
provided: &HashSet<String, S>,
upgradable: &HashSet<String, S>,
) -> DependencyStatus {
if !is_package_installed_or_provided(name, installed, provided) {
return DependencyStatus::ToInstall;
}
let is_upgradable = upgradable.contains(name);
if !version_req.is_empty() {
if let Ok(installed_version) = get_installed_version(name) {
if !version_satisfies(&installed_version, version_req) {
return DependencyStatus::ToUpgrade {
current: installed_version,
required: version_req.to_string(),
};
}
if is_upgradable {
let available_version =
get_available_version(name).unwrap_or_else(|| "newer".to_string());
return DependencyStatus::ToUpgrade {
current: installed_version,
required: available_version,
};
}
return DependencyStatus::Installed {
version: installed_version,
};
}
}
if is_upgradable {
match get_installed_version(name) {
Ok(current_version) => {
let available_version =
get_available_version(name).unwrap_or_else(|| "newer".to_string());
return DependencyStatus::ToUpgrade {
current: current_version,
required: available_version,
};
}
Err(_) => {
return DependencyStatus::ToUpgrade {
current: "installed".to_string(),
required: "newer".to_string(),
};
}
}
}
get_installed_version(name).map_or_else(
|_| DependencyStatus::Installed {
version: "installed".to_string(),
},
|version| DependencyStatus::Installed { version },
)
}
#[must_use]
pub fn batch_fetch_official_deps(names: &[&str]) -> HashMap<String, Vec<String>> {
const BATCH_SIZE: usize = 50;
let mut result_map = HashMap::new();
for chunk in names.chunks(BATCH_SIZE) {
let mut args = vec!["-Si"];
args.extend(chunk.iter().copied());
match Command::new("pacman")
.args(&args)
.env("LC_ALL", "C")
.env("LANG", "C")
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
{
Ok(output) if output.status.success() => {
let text = String::from_utf8_lossy(&output.stdout);
let mut package_blocks = Vec::new();
let mut current_block = String::new();
for line in text.lines() {
if line.trim().is_empty() {
if !current_block.is_empty() {
package_blocks.push(current_block.clone());
current_block.clear();
}
} else {
current_block.push_str(line);
current_block.push('\n');
}
}
if !current_block.is_empty() {
package_blocks.push(current_block);
}
for block in package_blocks {
let dep_names = parse_pacman_si_deps(&block);
if let Some(name_line) =
block.lines().find(|l| l.trim_start().starts_with("Name"))
&& let Some((_, name)) = name_line.split_once(':')
{
let pkg_name = name.trim().to_string();
result_map.insert(pkg_name, dep_names);
}
}
}
_ => {
break;
}
}
}
result_map
}
fn is_command_available(cmd: &str) -> bool {
Command::new(cmd)
.args(["--version"])
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.output()
.is_ok()
}
#[allow(clippy::case_sensitive_file_extension_comparisons)]
fn should_filter_dependency(pkg_name: &str, parent_name: &str) -> bool {
let pkg_lower = pkg_name.to_lowercase();
pkg_name == parent_name
|| pkg_lower.ends_with(".so")
|| pkg_lower.contains(".so.")
|| pkg_lower.contains(".so=")
}
fn process_dependency_spec<S: BuildHasher>(
dep_spec: &str,
parent_name: &str,
installed: &HashSet<String, S>,
provided: &HashSet<String, S>,
upgradable: &HashSet<String, S>,
) -> Option<Dependency> {
let spec = parse_dep_spec(dep_spec);
let pkg_name = spec.name;
let version_req = spec.version_req;
if should_filter_dependency(&pkg_name, parent_name) {
if pkg_name == parent_name {
tracing::debug!("Skipping self-reference: {} == {}", pkg_name, parent_name);
} else {
tracing::debug!("Filtering out virtual package: {}", pkg_name);
}
return None;
}
let status = determine_status(&pkg_name, &version_req, installed, provided, upgradable);
let (source, is_core) = determine_dependency_source(&pkg_name, installed);
let is_system = is_core || is_system_package(&pkg_name);
Some(Dependency {
name: pkg_name,
version_req,
status,
source,
required_by: vec![parent_name.to_string()],
depends_on: Vec::new(),
is_core,
is_system,
})
}
fn process_dependency_specs<S: BuildHasher>(
dep_specs: Vec<String>,
parent_name: &str,
installed: &HashSet<String, S>,
provided: &HashSet<String, S>,
upgradable: &HashSet<String, S>,
) -> Vec<Dependency> {
dep_specs
.into_iter()
.filter_map(|dep_spec| {
process_dependency_spec(&dep_spec, parent_name, installed, provided, upgradable)
})
.collect()
}
fn resolve_local_package_deps<S: BuildHasher>(
name: &str,
installed: &HashSet<String, S>,
provided: &HashSet<String, S>,
upgradable: &HashSet<String, S>,
) -> Result<Vec<Dependency>> {
tracing::debug!("Running: pacman -Qi {} (local package)", name);
let output = Command::new("pacman")
.args(["-Qi", name])
.env("LC_ALL", "C")
.env("LANG", "C")
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.map_err(|e| {
tracing::error!("Failed to execute pacman -Qi {}: {}", name, e);
crate::error::ArchToolkitError::Parse(format!("pacman -Qi failed: {e}"))
})?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
tracing::warn!(
"pacman -Qi {} failed with status {:?}: {}",
name,
output.status.code(),
stderr
);
return Ok(Vec::new());
}
let text = String::from_utf8_lossy(&output.stdout);
tracing::debug!("pacman -Qi {} output ({} bytes)", name, text.len());
let dep_names = parse_pacman_si_deps(&text);
tracing::debug!(
"Parsed {} dependency names from pacman -Qi output",
dep_names.len()
);
Ok(process_dependency_specs(
dep_names, name, installed, provided, upgradable,
))
}
fn resolve_official_package_deps<S: BuildHasher>(
name: &str,
repo: &str,
installed: &HashSet<String, S>,
provided: &HashSet<String, S>,
upgradable: &HashSet<String, S>,
) -> Result<Vec<Dependency>> {
tracing::debug!("Running: pacman -Si {} (repo: {})", name, repo);
let output = Command::new("pacman")
.args(["-Si", name])
.env("LC_ALL", "C")
.env("LANG", "C")
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.map_err(|e| {
tracing::error!("Failed to execute pacman -Si {}: {}", name, e);
crate::error::ArchToolkitError::Parse(format!("pacman -Si failed: {e}"))
})?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
tracing::error!(
"pacman -Si {} failed with status {:?}: {}",
name,
output.status.code(),
stderr
);
return Err(crate::error::ArchToolkitError::Parse(format!(
"pacman -Si failed for {name}: {stderr}"
)));
}
let text = String::from_utf8_lossy(&output.stdout);
tracing::debug!("pacman -Si {} output ({} bytes)", name, text.len());
let dep_names = parse_pacman_si_deps(&text);
tracing::debug!(
"Parsed {} dependency names from pacman -Si output",
dep_names.len()
);
Ok(process_dependency_specs(
dep_names, name, installed, provided, upgradable,
))
}
fn try_helper_resolution<S: BuildHasher>(
helper: &str,
name: &str,
installed: &HashSet<String, S>,
provided: &HashSet<String, S>,
upgradable: &HashSet<String, S>,
) -> Option<Vec<Dependency>> {
tracing::debug!("Trying {} -Si {} for dependency resolution", helper, name);
let output = Command::new(helper)
.args(["-Si", name])
.env("LC_ALL", "C")
.env("LANG", "C")
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.ok()?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
tracing::debug!(
"{} -Si {} failed (will try other methods): {}",
helper,
name,
stderr.trim()
);
return None;
}
let text = String::from_utf8_lossy(&output.stdout);
tracing::debug!("{} -Si {} output ({} bytes)", helper, name, text.len());
let dep_names = parse_pacman_si_deps(&text);
if dep_names.is_empty() {
return None;
}
tracing::info!(
"Using {} to resolve runtime dependencies for {} (will fetch .SRCINFO for build-time deps)",
helper,
name
);
let deps = process_dependency_specs(dep_names, name, installed, provided, upgradable);
Some(deps)
}
#[cfg(feature = "aur")]
fn enhance_with_srcinfo<S: BuildHasher>(
name: &str,
deps: Vec<Dependency>,
_installed: &HashSet<String, S>,
_provided: &HashSet<String, S>,
_upgradable: &HashSet<String, S>,
) -> Vec<Dependency> {
tracing::debug!(
"Skipping .SRCINFO enhancement for {} (requires async context)",
name
);
deps
}
#[cfg(not(feature = "aur"))]
#[allow(clippy::unused_parameters)]
fn enhance_with_srcinfo<S: BuildHasher>(
_name: &str,
deps: Vec<Dependency>,
_installed: &HashSet<String, S>,
_provided: &HashSet<String, S>,
_upgradable: &HashSet<String, S>,
) -> Vec<Dependency> {
deps
}
fn fallback_to_pkgbuild<S: BuildHasher>(
name: &str,
pkgbuild_cache: Option<&PkgbuildCacheFn>,
installed: &HashSet<String, S>,
provided: &HashSet<String, S>,
upgradable: &HashSet<String, S>,
) -> Vec<Dependency> {
let Some(pkgbuild_text) = pkgbuild_cache.and_then(|f| f(name)) else {
tracing::debug!(
"No cached PKGBUILD available for {} (offline, no dependencies resolved)",
name
);
return Vec::new();
};
tracing::info!(
"Using cached PKGBUILD for {} to resolve dependencies (offline fallback)",
name
);
let (pkgbuild_depends, _, _, _) = parse_pkgbuild_deps(&pkgbuild_text);
let deps = process_dependency_specs(pkgbuild_depends, name, installed, provided, upgradable);
tracing::info!(
"Resolved {} dependencies from cached PKGBUILD for {}",
deps.len(),
name
);
deps
}
fn resolve_aur_package_deps<S: BuildHasher>(
name: &str,
installed: &HashSet<String, S>,
provided: &HashSet<String, S>,
upgradable: &HashSet<String, S>,
pkgbuild_cache: Option<&PkgbuildCacheFn>,
) -> Vec<Dependency> {
tracing::debug!(
"Attempting to resolve AUR package: {} (will skip if not found)",
name
);
let (mut deps, mut used_helper) = if is_command_available("paru")
&& let Some(helper_deps) =
try_helper_resolution("paru", name, installed, provided, upgradable)
{
(helper_deps, true)
} else {
(Vec::new(), false)
};
if !used_helper
&& is_command_available("yay")
&& let Some(helper_deps) =
try_helper_resolution("yay", name, installed, provided, upgradable)
{
deps = helper_deps;
used_helper = true;
}
if !used_helper {
tracing::debug!(
"Skipping AUR API for {} - paru/yay failed or not available (likely not a real package)",
name
);
}
deps = enhance_with_srcinfo(name, deps, installed, provided, upgradable);
if !used_helper && deps.is_empty() {
deps = fallback_to_pkgbuild(name, pkgbuild_cache, installed, provided, upgradable);
}
deps
}
fn resolve_package_deps<S: BuildHasher>(
name: &str,
source: &PackageSource,
installed: &HashSet<String, S>,
provided: &HashSet<String, S>,
upgradable: &HashSet<String, S>,
pkgbuild_cache: Option<&PkgbuildCacheFn>,
) -> Result<Vec<Dependency>> {
let deps = match source {
PackageSource::Official { repo, .. } => {
if repo == "local" {
resolve_local_package_deps(name, installed, provided, upgradable)?
} else {
resolve_official_package_deps(name, repo, installed, provided, upgradable)?
}
}
PackageSource::Aur => {
resolve_aur_package_deps(name, installed, provided, upgradable, pkgbuild_cache)
}
};
tracing::debug!("Resolved {} dependencies for package {}", deps.len(), name);
Ok(deps)
}
pub fn fetch_package_conflicts(name: &str, source: &PackageSource) -> Vec<String> {
match source {
PackageSource::Official { repo, .. } => {
if repo == "local" {
tracing::debug!("Running: pacman -Qi {} (local package, conflicts)", name);
if let Ok(output) = Command::new("pacman")
.args(["-Qi", name])
.env("LC_ALL", "C")
.env("LANG", "C")
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
&& output.status.success()
{
let text = String::from_utf8_lossy(&output.stdout);
return parse_pacman_si_conflicts(&text);
}
return Vec::new();
}
tracing::debug!("Running: pacman -Si {} (conflicts)", name);
if let Ok(output) = Command::new("pacman")
.args(["-Si", name])
.env("LC_ALL", "C")
.env("LANG", "C")
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
&& output.status.success()
{
let text = String::from_utf8_lossy(&output.stdout);
return parse_pacman_si_conflicts(&text);
}
Vec::new()
}
PackageSource::Aur => {
let has_paru = is_command_available("paru");
let has_yay = is_command_available("yay");
if has_paru {
tracing::debug!("Trying paru -Si {} for conflicts", name);
if let Ok(output) = Command::new("paru")
.args(["-Si", name])
.env("LC_ALL", "C")
.env("LANG", "C")
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
&& output.status.success()
{
let text = String::from_utf8_lossy(&output.stdout);
let conflicts = parse_pacman_si_conflicts(&text);
if !conflicts.is_empty() {
return conflicts;
}
}
}
if has_yay {
tracing::debug!("Trying yay -Si {} for conflicts", name);
if let Ok(output) = Command::new("yay")
.args(["-Si", name])
.env("LC_ALL", "C")
.env("LANG", "C")
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
&& output.status.success()
{
let text = String::from_utf8_lossy(&output.stdout);
let conflicts = parse_pacman_si_conflicts(&text);
if !conflicts.is_empty() {
return conflicts;
}
}
}
#[cfg(feature = "aur")]
{
tracing::debug!(
"Skipping .SRCINFO conflict check for {} (requires async context)",
name
);
}
Vec::new()
}
}
}
const fn dependency_priority(status: &DependencyStatus) -> u8 {
status.priority()
}
fn merge_dependency<S: BuildHasher>(
dep: &Dependency,
parent_name: &str,
installed: &HashSet<String, S>,
provided: &HashSet<String, S>,
upgradable: &HashSet<String, S>,
deps: &mut HashMap<String, Dependency>,
) {
let dep_name = dep.name.clone();
let needs_required_by_update = deps
.get(&dep_name)
.is_none_or(|e| !e.required_by.contains(&parent_name.to_string()));
let entry = deps.entry(dep_name.clone()).or_insert_with(|| Dependency {
name: dep_name.clone(),
version_req: dep.version_req.clone(),
status: dep.status.clone(),
source: dep.source.clone(),
required_by: vec![parent_name.to_string()],
depends_on: Vec::new(),
is_core: dep.is_core,
is_system: dep.is_system,
});
if needs_required_by_update {
entry.required_by.push(parent_name.to_string());
}
if !matches!(entry.status, DependencyStatus::Conflict { .. }) {
let existing_priority = dependency_priority(&entry.status);
let new_priority = dependency_priority(&dep.status);
if new_priority < existing_priority {
entry.status = dep.status.clone();
}
}
if !dep.version_req.is_empty() && dep.version_req != entry.version_req {
if matches!(entry.status, DependencyStatus::Conflict { .. }) {
if entry.version_req.is_empty() {
entry.version_req.clone_from(&dep.version_req);
}
return;
}
if entry.version_req.is_empty() {
entry.version_req.clone_from(&dep.version_req);
} else {
let existing_status = determine_status(
&entry.name,
&entry.version_req,
installed,
provided,
upgradable,
);
let new_status = determine_status(
&entry.name,
&dep.version_req,
installed,
provided,
upgradable,
);
let existing_req_priority = dependency_priority(&existing_status);
let new_req_priority = dependency_priority(&new_status);
if new_req_priority < existing_req_priority {
entry.version_req.clone_from(&dep.version_req);
entry.status = new_status;
}
}
}
}
pub struct DependencyResolver {
config: ResolverConfig,
}
impl DependencyResolver {
#[must_use]
pub fn new() -> Self {
Self {
config: ResolverConfig::default(),
}
}
#[must_use]
#[allow(clippy::missing_const_for_fn)] pub fn with_config(config: ResolverConfig) -> Self {
Self { config }
}
pub fn resolve(
&self,
packages: &[PackageRef],
) -> Result<crate::types::dependency::DependencyResolution> {
use crate::types::dependency::DependencyResolution;
if packages.is_empty() {
tracing::warn!("No packages provided for dependency resolution");
return Ok(DependencyResolution::default());
}
let mut deps: HashMap<String, Dependency> = HashMap::new();
let mut conflicts: Vec<String> = Vec::new();
let mut missing: Vec<String> = Vec::new();
tracing::info!("Fetching list of installed packages...");
let installed = get_installed_packages()?;
tracing::info!("Found {} installed packages", installed.len());
tracing::debug!(
"Provides will be checked lazily on-demand (not building full set for performance)"
);
let provided = get_provided_packages(&installed);
let upgradable = get_upgradable_packages()?;
tracing::info!("Found {} upgradable packages", upgradable.len());
let root_names: HashSet<String> = packages.iter().map(|p| p.name.clone()).collect();
tracing::info!("Checking conflicts for {} package(s)", packages.len());
for package in packages {
let package_conflicts = fetch_package_conflicts(&package.name, &package.source);
for conflict_name in package_conflicts {
if installed.contains(&conflict_name) || root_names.contains(&conflict_name) {
if !conflicts.contains(&conflict_name) {
conflicts.push(conflict_name.clone());
}
let dep = Dependency {
name: conflict_name,
version_req: String::new(),
status: DependencyStatus::Conflict {
reason: format!("Conflicts with {}", package.name),
},
source: DependencySource::Local,
required_by: vec![package.name.clone()],
depends_on: Vec::new(),
is_core: false,
is_system: false,
};
merge_dependency(
&dep,
&package.name,
&installed,
&provided,
&upgradable,
&mut deps,
);
}
}
}
let official_packages: Vec<&str> = packages
.iter()
.filter_map(|pkg| {
if let PackageSource::Official { repo, .. } = &pkg.source {
if repo == "local" {
None
} else {
Some(pkg.name.as_str())
}
} else {
None
}
})
.collect();
let batched_deps_cache = if official_packages.is_empty() {
HashMap::new()
} else {
batch_fetch_official_deps(&official_packages)
};
for package in packages {
let use_batched = matches!(package.source, PackageSource::Official { ref repo, .. } if repo != "local")
&& batched_deps_cache.contains_key(package.name.as_str());
let resolved_deps = if use_batched {
let dep_names = batched_deps_cache
.get(package.name.as_str())
.cloned()
.unwrap_or_default();
process_dependency_specs(
dep_names,
&package.name,
&installed,
&provided,
&upgradable,
)
} else {
match resolve_package_deps(
&package.name,
&package.source,
&installed,
&provided,
&upgradable,
self.config
.pkgbuild_cache
.as_ref()
.map(|f| f.as_ref() as &(dyn Fn(&str) -> Option<String> + Send + Sync)),
) {
Ok(deps) => deps,
Err(e) => {
tracing::warn!(
" Failed to resolve dependencies for {}: {}",
package.name,
e
);
if !missing.contains(&package.name) {
missing.push(package.name.clone());
}
continue;
}
}
};
tracing::debug!(
" Found {} dependencies for {}",
resolved_deps.len(),
package.name
);
for dep in resolved_deps {
if matches!(dep.status, DependencyStatus::Missing) && !missing.contains(&dep.name) {
missing.push(dep.name.clone());
}
merge_dependency(
&dep,
&package.name,
&installed,
&provided,
&upgradable,
&mut deps,
);
}
}
let mut result: Vec<Dependency> = deps.into_values().collect();
tracing::info!("Total unique dependencies found: {}", result.len());
result.sort_by(|a, b| {
let priority_a = dependency_priority(&a.status);
let priority_b = dependency_priority(&b.status);
priority_a
.cmp(&priority_b)
.then_with(|| a.name.cmp(&b.name))
});
Ok(DependencyResolution {
dependencies: result,
conflicts,
missing,
})
}
}
impl Default for DependencyResolver {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::dependency::DependencyStatus;
#[test]
fn test_should_filter_dependency() {
assert!(should_filter_dependency("libfoo.so", "package"));
assert!(should_filter_dependency("libfoo.so.1", "package"));
assert!(should_filter_dependency("libfoo.so=1", "package"));
assert!(should_filter_dependency("package", "package")); assert!(!should_filter_dependency("glibc", "package"));
assert!(!should_filter_dependency("firefox", "package"));
}
#[test]
fn test_determine_status_not_installed() {
let installed = HashSet::new();
let provided = HashSet::new();
let upgradable = HashSet::new();
let status = determine_status("nonexistent", "", &installed, &provided, &upgradable);
assert!(matches!(status, DependencyStatus::ToInstall));
}
#[test]
fn test_batch_fetch_official_deps_parsing() {
let sample_output = "Name : firefox\nDepends On : glibc\n\nName : vim\nDepends On : glibc\n";
let mut package_blocks = Vec::new();
let mut current_block = String::new();
for line in sample_output.lines() {
if line.trim().is_empty() {
if !current_block.is_empty() {
package_blocks.push(current_block.clone());
current_block.clear();
}
} else {
current_block.push_str(line);
current_block.push('\n');
}
}
if !current_block.is_empty() {
package_blocks.push(current_block);
}
assert_eq!(package_blocks.len(), 2);
assert!(package_blocks[0].contains("firefox"));
assert!(package_blocks[1].contains("vim"));
}
#[test]
fn test_dependency_priority() {
assert_eq!(
dependency_priority(&DependencyStatus::Conflict {
reason: "test".to_string(),
}),
0
);
assert_eq!(dependency_priority(&DependencyStatus::Missing), 1);
assert_eq!(dependency_priority(&DependencyStatus::ToInstall), 2);
assert_eq!(
dependency_priority(&DependencyStatus::ToUpgrade {
current: "1.0".to_string(),
required: "2.0".to_string(),
}),
3
);
assert_eq!(
dependency_priority(&DependencyStatus::Installed {
version: "1.0".to_string(),
}),
4
);
}
#[test]
fn test_dependency_resolver_new() {
let resolver = DependencyResolver::new();
assert!(matches!(resolver.config.max_depth, 0));
}
#[test]
fn test_dependency_resolver_with_config() {
let config = ResolverConfig {
include_optdepends: true,
include_makedepends: true,
include_checkdepends: true,
max_depth: 2,
pkgbuild_cache: None,
check_aur: true,
};
let resolver = DependencyResolver::with_config(config);
assert_eq!(resolver.config.max_depth, 2);
assert!(resolver.config.include_optdepends);
assert!(resolver.config.check_aur);
}
#[test]
fn test_dependency_resolver_resolve_empty() {
let resolver = DependencyResolver::new();
let result = resolver
.resolve(&[])
.expect("resolve should succeed for empty packages");
assert_eq!(result.dependencies.len(), 0);
assert_eq!(result.conflicts.len(), 0);
assert_eq!(result.missing.len(), 0);
}
#[test]
#[ignore = "Requires pacman to be available"]
fn test_dependency_resolver_resolve_integration() {
let resolver = DependencyResolver::new();
let packages = vec![PackageRef {
name: "pacman".to_string(),
version: "6.1.0".to_string(),
source: PackageSource::Official {
repo: "core".to_string(),
arch: "x86_64".to_string(),
},
}];
if let Ok(result) = resolver.resolve(&packages) {
println!("Found {} dependencies", result.dependencies.len());
assert!(!result.dependencies.is_empty());
}
}
}