Skip to main content

arch_toolkit/deps/
resolve.rs

1//! Core dependency resolution logic for individual packages.
2//!
3//! This module provides functions to resolve dependencies for packages, determine
4//! dependency status, and handle batch operations for efficient dependency resolution.
5
6use crate::deps::graph::{DependencyMetadataProvider, resolve_dependency_graph};
7use crate::deps::parse::{parse_dep_spec, parse_pacman_si_conflicts, parse_pacman_si_deps};
8use crate::deps::pkgbuild::parse_pkgbuild_deps;
9use crate::deps::query::{
10    get_available_version, get_installed_packages, get_installed_version, get_provided_packages,
11    get_upgradable_packages, is_package_installed_or_provided,
12};
13use crate::deps::source::{determine_dependency_source, is_system_package};
14use crate::deps::version::version_satisfies;
15use crate::error::Result;
16use crate::types::dependency::{
17    Dependency, DependencyGraphConfig, DependencyGraphResolution, DependencySource,
18    DependencyStatus, PackageRef, PackageSource, ResolverConfig,
19};
20use std::collections::{HashMap, HashSet};
21use std::hash::BuildHasher;
22use std::process::{Command, Stdio};
23
24/// Type alias for PKGBUILD cache callback function.
25type PkgbuildCacheFn = dyn Fn(&str) -> Option<String> + Send + Sync;
26
27/// What: Evaluate a dependency's installation status relative to required versions.
28///
29/// Inputs:
30/// - `name`: Dependency package identifier.
31/// - `version_req`: Optional version constraint string (e.g., `>=1.2`).
32/// - `installed`: Set of names currently installed on the system.
33/// - `provided`: Set of package names provided by installed packages.
34/// - `upgradable`: Set of names pacman reports as upgradable.
35///
36/// Output:
37/// - Returns a `DependencyStatus` describing whether installation, upgrade, or no action is needed.
38///
39/// Details:
40/// - Combines local database queries with helper functions to capture upgrade requirements.
41/// - Uses `is_package_installed_or_provided()` to check if package is available.
42/// - Uses `get_installed_version()` and `get_available_version()` for version checking.
43/// - Uses `version_satisfies()` for version requirement validation.
44///
45/// # Example
46///
47/// ```no_run
48/// use arch_toolkit::deps::determine_status;
49/// use std::collections::HashSet;
50///
51/// let installed = HashSet::from(["glibc".to_string()]);
52/// let provided = HashSet::new();
53/// let upgradable = HashSet::new();
54///
55/// let status = determine_status("glibc", "", &installed, &provided, &upgradable);
56/// println!("Status: {:?}", status);
57/// ```
58pub fn determine_status<S: BuildHasher>(
59    name: &str,
60    version_req: &str,
61    installed: &HashSet<String, S>,
62    provided: &HashSet<String, S>,
63    upgradable: &HashSet<String, S>,
64) -> DependencyStatus {
65    // Check if package is installed or provided by an installed package
66    if !is_package_installed_or_provided(name, installed, provided) {
67        return DependencyStatus::ToInstall;
68    }
69
70    // Check if package is upgradable (even without version requirement)
71    let is_upgradable = upgradable.contains(name);
72
73    // If version requirement is specified, check if it matches
74    if !version_req.is_empty() {
75        // Try to get installed version
76        if let Ok(installed_version) = get_installed_version(name) {
77            // Check if version requirement is satisfied
78            if !version_satisfies(&installed_version, version_req) {
79                return DependencyStatus::ToUpgrade {
80                    current: installed_version,
81                    required: version_req.to_string(),
82                };
83            }
84            // Version requirement satisfied, but check if package is upgradable anyway
85            if is_upgradable {
86                // Get available version from pacman -Si if possible
87                let available_version =
88                    get_available_version(name).unwrap_or_else(|| "newer".to_string());
89                return DependencyStatus::ToUpgrade {
90                    current: installed_version,
91                    required: available_version,
92                };
93            }
94            return DependencyStatus::Installed {
95                version: installed_version,
96            };
97        }
98    }
99
100    // Installed but no version check needed - check if upgradable
101    if is_upgradable {
102        match get_installed_version(name) {
103            Ok(current_version) => {
104                let available_version =
105                    get_available_version(name).unwrap_or_else(|| "newer".to_string());
106                return DependencyStatus::ToUpgrade {
107                    current: current_version,
108                    required: available_version,
109                };
110            }
111            Err(_) => {
112                return DependencyStatus::ToUpgrade {
113                    current: "installed".to_string(),
114                    required: "newer".to_string(),
115                };
116            }
117        }
118    }
119
120    // Installed and up-to-date - get actual version
121    get_installed_version(name).map_or_else(
122        |_| DependencyStatus::Installed {
123            version: "installed".to_string(),
124        },
125        |version| DependencyStatus::Installed { version },
126    )
127}
128
129/// What: Batch fetch dependency lists for multiple official packages using `pacman -Si`.
130///
131/// Inputs:
132/// - `names`: Package names to query (must be official packages, not local).
133///
134/// Output:
135/// - `HashMap` mapping package name to its dependency list (`Vec<String>`).
136///
137/// Details:
138/// - Batches queries into chunks of 50 to avoid command-line length limits.
139/// - Parses multi-package `pacman -Si` output (packages separated by blank lines).
140/// - Gracefully handles command failures by returning partial results.
141///
142/// # Example
143///
144/// ```no_run
145/// use arch_toolkit::deps::batch_fetch_official_deps;
146///
147/// let packages = vec!["firefox", "vim"];
148/// let deps = batch_fetch_official_deps(&packages);
149/// println!("Found dependencies for {} packages", deps.len());
150/// ```
151#[must_use]
152pub fn batch_fetch_official_deps(names: &[&str]) -> HashMap<String, Vec<String>> {
153    const BATCH_SIZE: usize = 50;
154    let mut result_map = HashMap::new();
155
156    for chunk in names.chunks(BATCH_SIZE) {
157        let mut args = vec!["-Si"];
158        args.extend(chunk.iter().copied());
159        match Command::new("pacman")
160            .args(&args)
161            .env("LC_ALL", "C")
162            .env("LANG", "C")
163            .stdin(Stdio::null())
164            .stdout(Stdio::piped())
165            .stderr(Stdio::piped())
166            .output()
167        {
168            Ok(output) if output.status.success() => {
169                let text = String::from_utf8_lossy(&output.stdout);
170                // Parse multi-package output: packages are separated by blank lines
171                let mut package_blocks = Vec::new();
172                let mut current_block = String::new();
173                for line in text.lines() {
174                    if line.trim().is_empty() {
175                        if !current_block.is_empty() {
176                            package_blocks.push(current_block.clone());
177                            current_block.clear();
178                        }
179                    } else {
180                        current_block.push_str(line);
181                        current_block.push('\n');
182                    }
183                }
184                if !current_block.is_empty() {
185                    package_blocks.push(current_block);
186                }
187
188                // Parse each block to extract package name and dependencies
189                for block in package_blocks {
190                    let dep_names = parse_pacman_si_deps(&block);
191                    // Extract package name from block
192                    if let Some(name_line) =
193                        block.lines().find(|l| l.trim_start().starts_with("Name"))
194                        && let Some((_, name)) = name_line.split_once(':')
195                    {
196                        let pkg_name = name.trim().to_string();
197                        result_map.insert(pkg_name, dep_names);
198                    }
199                }
200            }
201            _ => {
202                // If batch fails, fall back to individual queries (but don't do it here to avoid recursion)
203                // The caller will handle individual queries
204                break;
205            }
206        }
207    }
208    result_map
209}
210
211/// What: Check if a command is available in PATH.
212///
213/// Inputs:
214/// - `cmd`: Command name to check.
215///
216/// Output:
217/// - Returns true if the command exists and can be executed.
218///
219/// Details:
220/// - Uses a simple version check to verify command availability.
221fn is_command_available(cmd: &str) -> bool {
222    Command::new(cmd)
223        .args(["--version"])
224        .stdin(Stdio::null())
225        .stdout(Stdio::null())
226        .stderr(Stdio::null())
227        .output()
228        .is_ok()
229}
230
231/// What: Check if a package name should be filtered out (virtual package or self-reference).
232///
233/// Inputs:
234/// - `pkg_name`: Package name to check.
235/// - `parent_name`: Name of the parent package (to detect self-references).
236///
237/// Output:
238/// - Returns true if the package should be filtered out.
239///
240/// Details:
241/// - Filters out .so files (virtual packages) and self-references.
242#[allow(clippy::case_sensitive_file_extension_comparisons)]
243fn should_filter_dependency(pkg_name: &str, parent_name: &str) -> bool {
244    let pkg_lower = pkg_name.to_lowercase();
245    pkg_name == parent_name
246        || pkg_lower.ends_with(".so")
247        || pkg_lower.contains(".so.")
248        || pkg_lower.contains(".so=")
249}
250
251/// What: Convert a dependency spec into a `Dependency` record.
252///
253/// Inputs:
254/// - `dep_spec`: Dependency specification string (may include version requirements).
255/// - `parent_name`: Name of the package that requires this dependency.
256/// - `installed`: Set of locally installed packages.
257/// - `provided`: Set of package names provided by installed packages.
258/// - `upgradable`: Set of packages flagged for upgrades.
259///
260/// Output:
261/// - Returns Some(Dependency) if the dependency should be included, None if filtered.
262///
263/// Details:
264/// - Parses the dependency spec, filters out virtual packages and self-references,
265///   and determines status, source, and system package flags.
266fn process_dependency_spec<S: BuildHasher>(
267    dep_spec: &str,
268    parent_name: &str,
269    installed: &HashSet<String, S>,
270    provided: &HashSet<String, S>,
271    upgradable: &HashSet<String, S>,
272) -> Option<Dependency> {
273    let spec = parse_dep_spec(dep_spec);
274    let pkg_name = spec.name;
275    let version_req = spec.version_req;
276
277    if should_filter_dependency(&pkg_name, parent_name) {
278        if pkg_name == parent_name {
279            tracing::debug!("Skipping self-reference: {} == {}", pkg_name, parent_name);
280        } else {
281            tracing::debug!("Filtering out virtual package: {}", pkg_name);
282        }
283        return None;
284    }
285
286    let status = determine_status(&pkg_name, &version_req, installed, provided, upgradable);
287    let (source, is_core) = determine_dependency_source(&pkg_name, installed);
288    let is_system = is_core || is_system_package(&pkg_name);
289
290    Some(Dependency {
291        name: pkg_name,
292        version_req,
293        status,
294        source,
295        required_by: vec![parent_name.to_string()],
296        depends_on: Vec::new(),
297        is_core,
298        is_system,
299    })
300}
301
302/// What: Process a list of dependency specs into `Dependency` records.
303///
304/// Inputs:
305/// - `dep_specs`: Vector of dependency specification strings.
306/// - `parent_name`: Name of the package that requires these dependencies.
307/// - `installed`: Set of locally installed packages.
308/// - `provided`: Set of package names provided by installed packages.
309/// - `upgradable`: Set of packages flagged for upgrades.
310///
311/// Output:
312/// - Returns a vector of `Dependency` records (filtered).
313///
314/// Details:
315/// - Processes each dependency spec and collects valid dependencies.
316fn process_dependency_specs<S: BuildHasher>(
317    dep_specs: Vec<String>,
318    parent_name: &str,
319    installed: &HashSet<String, S>,
320    provided: &HashSet<String, S>,
321    upgradable: &HashSet<String, S>,
322) -> Vec<Dependency> {
323    dep_specs
324        .into_iter()
325        .filter_map(|dep_spec| {
326            process_dependency_spec(&dep_spec, parent_name, installed, provided, upgradable)
327        })
328        .collect()
329}
330
331/// What: Resolve dependencies for a local package using pacman -Qi.
332///
333/// Inputs:
334/// - `name`: Package name.
335/// - `installed`: Set of locally installed packages.
336/// - `provided`: Set of package names provided by installed packages.
337/// - `upgradable`: Set of packages flagged for upgrades.
338///
339/// Output:
340/// - Returns a vector of `Dependency` records or an error string.
341///
342/// Details:
343/// - Uses pacman -Qi to get dependency information for locally installed packages.
344fn resolve_local_package_deps<S: BuildHasher>(
345    name: &str,
346    installed: &HashSet<String, S>,
347    provided: &HashSet<String, S>,
348    upgradable: &HashSet<String, S>,
349) -> Result<Vec<Dependency>> {
350    tracing::debug!("Running: pacman -Qi {} (local package)", name);
351    let output = Command::new("pacman")
352        .args(["-Qi", name])
353        .env("LC_ALL", "C")
354        .env("LANG", "C")
355        .stdin(Stdio::null())
356        .stdout(Stdio::piped())
357        .stderr(Stdio::piped())
358        .output()
359        .map_err(|e| {
360            tracing::error!("Failed to execute pacman -Qi {}: {}", name, e);
361            crate::error::ArchToolkitError::Parse(format!("pacman -Qi failed: {e}"))
362        })?;
363
364    if !output.status.success() {
365        let stderr = String::from_utf8_lossy(&output.stderr);
366        tracing::warn!(
367            "pacman -Qi {} failed with status {:?}: {}",
368            name,
369            output.status.code(),
370            stderr
371        );
372        return Ok(Vec::new());
373    }
374
375    let text = String::from_utf8_lossy(&output.stdout);
376    tracing::debug!("pacman -Qi {} output ({} bytes)", name, text.len());
377
378    let dep_names = parse_pacman_si_deps(&text);
379    tracing::debug!(
380        "Parsed {} dependency names from pacman -Qi output",
381        dep_names.len()
382    );
383
384    Ok(process_dependency_specs(
385        dep_names, name, installed, provided, upgradable,
386    ))
387}
388
389/// What: Resolve dependencies for an official package using pacman -Si.
390///
391/// Inputs:
392/// - `name`: Package name.
393/// - `repo`: Repository name (for logging).
394/// - `installed`: Set of locally installed packages.
395/// - `provided`: Set of package names provided by installed packages.
396/// - `upgradable`: Set of packages flagged for upgrades.
397///
398/// Output:
399/// - Returns a vector of `Dependency` records or an error string.
400///
401/// Details:
402/// - Uses pacman -Si to get dependency information for official packages.
403fn resolve_official_package_deps<S: BuildHasher>(
404    name: &str,
405    repo: &str,
406    installed: &HashSet<String, S>,
407    provided: &HashSet<String, S>,
408    upgradable: &HashSet<String, S>,
409) -> Result<Vec<Dependency>> {
410    tracing::debug!("Running: pacman -Si {} (repo: {})", name, repo);
411    let output = Command::new("pacman")
412        .args(["-Si", name])
413        .env("LC_ALL", "C")
414        .env("LANG", "C")
415        .stdin(Stdio::null())
416        .stdout(Stdio::piped())
417        .stderr(Stdio::piped())
418        .output()
419        .map_err(|e| {
420            tracing::error!("Failed to execute pacman -Si {}: {}", name, e);
421            crate::error::ArchToolkitError::Parse(format!("pacman -Si failed: {e}"))
422        })?;
423
424    if !output.status.success() {
425        let stderr = String::from_utf8_lossy(&output.stderr);
426        tracing::error!(
427            "pacman -Si {} failed with status {:?}: {}",
428            name,
429            output.status.code(),
430            stderr
431        );
432        return Err(crate::error::ArchToolkitError::Parse(format!(
433            "pacman -Si failed for {name}: {stderr}"
434        )));
435    }
436
437    let text = String::from_utf8_lossy(&output.stdout);
438    tracing::debug!("pacman -Si {} output ({} bytes)", name, text.len());
439
440    let dep_names = parse_pacman_si_deps(&text);
441    tracing::debug!(
442        "Parsed {} dependency names from pacman -Si output",
443        dep_names.len()
444    );
445
446    Ok(process_dependency_specs(
447        dep_names, name, installed, provided, upgradable,
448    ))
449}
450
451/// What: Try to resolve dependencies using an AUR helper (paru or yay).
452///
453/// Inputs:
454/// - `helper`: Helper command name ("paru" or "yay").
455/// - `name`: Package name.
456/// - `installed`: Set of locally installed packages.
457/// - `provided`: Set of package names provided by installed packages.
458/// - `upgradable`: Set of packages flagged for upgrades.
459///
460/// Output:
461/// - Returns Some(Vec<Dependency>) if successful, None otherwise.
462///
463/// Details:
464/// - Executes helper -Si command and parses the output for dependencies.
465fn try_helper_resolution<S: BuildHasher>(
466    helper: &str,
467    name: &str,
468    installed: &HashSet<String, S>,
469    provided: &HashSet<String, S>,
470    upgradable: &HashSet<String, S>,
471) -> Option<Vec<Dependency>> {
472    tracing::debug!("Trying {} -Si {} for dependency resolution", helper, name);
473    let output = Command::new(helper)
474        .args(["-Si", name])
475        .env("LC_ALL", "C")
476        .env("LANG", "C")
477        .stdin(Stdio::null())
478        .stdout(Stdio::piped())
479        .stderr(Stdio::piped())
480        .output()
481        .ok()?;
482
483    if !output.status.success() {
484        let stderr = String::from_utf8_lossy(&output.stderr);
485        tracing::debug!(
486            "{} -Si {} failed (will try other methods): {}",
487            helper,
488            name,
489            stderr.trim()
490        );
491        return None;
492    }
493
494    let text = String::from_utf8_lossy(&output.stdout);
495    tracing::debug!("{} -Si {} output ({} bytes)", helper, name, text.len());
496    let dep_names = parse_pacman_si_deps(&text);
497
498    if dep_names.is_empty() {
499        return None;
500    }
501
502    tracing::info!(
503        "Using {} to resolve runtime dependencies for {} (will fetch .SRCINFO for build-time deps)",
504        helper,
505        name
506    );
507
508    let deps = process_dependency_specs(dep_names, name, installed, provided, upgradable);
509    Some(deps)
510}
511
512/// What: Enhance dependency list with .SRCINFO data.
513///
514/// Inputs:
515/// - `name`: Package name.
516/// - `deps`: Existing dependency list to enhance.
517/// - `installed`: Set of locally installed packages.
518/// - `provided`: Set of package names provided by installed packages.
519/// - `upgradable`: Set of packages flagged for upgrades.
520///
521/// Output:
522/// - Returns the enhanced dependency list.
523///
524/// Details:
525/// - Fetches and parses .SRCINFO to add missing depends entries.
526/// - Requires `feature = "aur"` to be enabled.
527#[cfg(feature = "aur")]
528fn enhance_with_srcinfo<S: BuildHasher>(
529    name: &str,
530    deps: Vec<Dependency>,
531    _installed: &HashSet<String, S>,
532    _provided: &HashSet<String, S>,
533    _upgradable: &HashSet<String, S>,
534) -> Vec<Dependency> {
535    // Note: fetch_srcinfo is async and requires a reqwest client, so we can't use it here
536    // in the sync context. This is a limitation - callers should fetch .SRCINFO separately
537    // if needed, or use the async API when available.
538    tracing::debug!(
539        "Skipping .SRCINFO enhancement for {} (requires async context)",
540        name
541    );
542    deps
543}
544
545/// What: Enhance dependency list with .SRCINFO data (no-op when AUR feature is disabled).
546///
547/// Inputs:
548/// - `name`: Package name (unused).
549/// - `deps`: Existing dependency list to return as-is.
550/// - `installed`: Set of locally installed packages (unused).
551/// - `provided`: Set of package names provided by installed packages (unused).
552/// - `upgradable`: Set of packages flagged for upgrades (unused).
553///
554/// Output:
555/// - Returns the dependency list unchanged.
556///
557/// Details:
558/// - This is a no-op when `feature = "aur"` is not enabled.
559#[cfg(not(feature = "aur"))]
560const fn enhance_with_srcinfo<S: BuildHasher>(
561    _name: &str,
562    deps: Vec<Dependency>,
563    _installed: &HashSet<String, S>,
564    _provided: &HashSet<String, S>,
565    _upgradable: &HashSet<String, S>,
566) -> Vec<Dependency> {
567    deps
568}
569
570/// What: Fallback to cached PKGBUILD for dependency resolution.
571///
572/// Inputs:
573/// - `name`: Package name.
574/// - `pkgbuild_cache`: Optional callback to fetch PKGBUILD from cache.
575/// - `installed`: Set of locally installed packages.
576/// - `provided`: Set of package names provided by installed packages.
577/// - `upgradable`: Set of packages flagged for upgrades.
578///
579/// Output:
580/// - Returns a vector of `Dependency` records if `PKGBUILD` is found, empty vector otherwise.
581///
582/// Details:
583/// - Attempts to use cached PKGBUILD when .SRCINFO is unavailable (offline fallback).
584fn fallback_to_pkgbuild<S: BuildHasher>(
585    name: &str,
586    pkgbuild_cache: Option<&PkgbuildCacheFn>,
587    installed: &HashSet<String, S>,
588    provided: &HashSet<String, S>,
589    upgradable: &HashSet<String, S>,
590) -> Vec<Dependency> {
591    let Some(pkgbuild_text) = pkgbuild_cache.and_then(|f| f(name)) else {
592        tracing::debug!(
593            "No cached PKGBUILD available for {} (offline, no dependencies resolved)",
594            name
595        );
596        return Vec::new();
597    };
598
599    tracing::info!(
600        "Using cached PKGBUILD for {} to resolve dependencies (offline fallback)",
601        name
602    );
603    let (pkgbuild_depends, _, _, _) = parse_pkgbuild_deps(&pkgbuild_text);
604
605    let deps = process_dependency_specs(pkgbuild_depends, name, installed, provided, upgradable);
606    tracing::info!(
607        "Resolved {} dependencies from cached PKGBUILD for {}",
608        deps.len(),
609        name
610    );
611    deps
612}
613
614/// What: Resolve dependencies for an AUR package.
615///
616/// Inputs:
617/// - `name`: Package name.
618/// - `installed`: Set of locally installed packages.
619/// - `provided`: Set of package names provided by installed packages.
620/// - `upgradable`: Set of packages flagged for upgrades.
621/// - `pkgbuild_cache`: Optional callback to fetch PKGBUILD from cache.
622///
623/// Output:
624/// - Returns a vector of `Dependency` records.
625///
626/// Details:
627/// - Tries paru/yay first, then falls back to .SRCINFO and cached PKGBUILD.
628fn resolve_aur_package_deps<S: BuildHasher>(
629    name: &str,
630    installed: &HashSet<String, S>,
631    provided: &HashSet<String, S>,
632    upgradable: &HashSet<String, S>,
633    pkgbuild_cache: Option<&PkgbuildCacheFn>,
634) -> Vec<Dependency> {
635    tracing::debug!(
636        "Attempting to resolve AUR package: {} (will skip if not found)",
637        name
638    );
639
640    // Try paru first
641    let (mut deps, mut used_helper) = if is_command_available("paru")
642        && let Some(helper_deps) =
643            try_helper_resolution("paru", name, installed, provided, upgradable)
644    {
645        (helper_deps, true)
646    } else {
647        (Vec::new(), false)
648    };
649
650    // Try yay if paru didn't work
651    if !used_helper
652        && is_command_available("yay")
653        && let Some(helper_deps) =
654            try_helper_resolution("yay", name, installed, provided, upgradable)
655    {
656        deps = helper_deps;
657        used_helper = true;
658    }
659
660    if !used_helper {
661        tracing::debug!(
662            "Skipping AUR API for {} - paru/yay failed or not available (likely not a real package)",
663            name
664        );
665    }
666
667    // Always try to enhance with .SRCINFO
668    deps = enhance_with_srcinfo(name, deps, installed, provided, upgradable);
669
670    // Fallback to PKGBUILD if no dependencies were found
671    if !used_helper && deps.is_empty() {
672        deps = fallback_to_pkgbuild(name, pkgbuild_cache, installed, provided, upgradable);
673    }
674
675    deps
676}
677
678/// What: Resolve direct dependency metadata for a single package.
679///
680/// Inputs:
681/// - `name`: Package identifier whose dependencies should be enumerated.
682/// - `source`: Source enum describing whether the package is official or AUR.
683/// - `installed`: Set of locally installed packages for status determination.
684/// - `provided`: Set of package names provided by installed packages.
685/// - `upgradable`: Set of packages flagged for upgrades, used to detect stale dependencies.
686/// - `pkgbuild_cache`: Optional callback to fetch PKGBUILD from cache.
687///
688/// Output:
689/// - Returns a vector of `Dependency` records or an error string when resolution fails.
690///
691/// Details:
692/// - Invokes pacman or AUR helpers depending on source, filtering out virtual entries and self references.
693fn resolve_package_deps<S: BuildHasher>(
694    name: &str,
695    source: &PackageSource,
696    installed: &HashSet<String, S>,
697    provided: &HashSet<String, S>,
698    upgradable: &HashSet<String, S>,
699    pkgbuild_cache: Option<&PkgbuildCacheFn>,
700) -> Result<Vec<Dependency>> {
701    let deps = match source {
702        PackageSource::Official { repo, .. } => {
703            if repo == "local" {
704                resolve_local_package_deps(name, installed, provided, upgradable)?
705            } else {
706                resolve_official_package_deps(name, repo, installed, provided, upgradable)?
707            }
708        }
709        PackageSource::Aur => {
710            resolve_aur_package_deps(name, installed, provided, upgradable, pkgbuild_cache)
711        }
712    };
713
714    tracing::debug!("Resolved {} dependencies for package {}", deps.len(), name);
715    Ok(deps)
716}
717
718/// What: Fetch conflicts for a package from pacman or AUR sources.
719///
720/// Inputs:
721/// - `name`: Package identifier.
722/// - `source`: Source enum describing whether the package is official or AUR.
723///
724/// Output:
725/// - Returns a vector of conflicting package names, or empty vector on error.
726///
727/// Details:
728/// - For official packages, uses `pacman -Si` to get conflicts.
729/// - For AUR packages, tries paru/yay first, then falls back to .SRCINFO.
730///
731/// # Example
732///
733/// ```no_run
734/// use arch_toolkit::deps::fetch_package_conflicts;
735/// use arch_toolkit::PackageSource;
736///
737/// let conflicts = fetch_package_conflicts(
738///     "firefox",
739///     &PackageSource::Official {
740///         repo: "extra".into(),
741///         arch: "x86_64".into(),
742///     },
743/// );
744/// println!("Found {} conflicts", conflicts.len());
745/// ```
746pub fn fetch_package_conflicts(name: &str, source: &PackageSource) -> Vec<String> {
747    match source {
748        PackageSource::Official { repo, .. } => {
749            // Handle local packages specially - use pacman -Qi instead of -Si
750            if repo == "local" {
751                tracing::debug!("Running: pacman -Qi {} (local package, conflicts)", name);
752                if let Ok(output) = Command::new("pacman")
753                    .args(["-Qi", name])
754                    .env("LC_ALL", "C")
755                    .env("LANG", "C")
756                    .stdin(Stdio::null())
757                    .stdout(Stdio::piped())
758                    .stderr(Stdio::piped())
759                    .output()
760                    && output.status.success()
761                {
762                    let text = String::from_utf8_lossy(&output.stdout);
763                    return parse_pacman_si_conflicts(&text);
764                }
765                return Vec::new();
766            }
767
768            // Use pacman -Si to get conflicts
769            tracing::debug!("Running: pacman -Si {} (conflicts)", name);
770            if let Ok(output) = Command::new("pacman")
771                .args(["-Si", name])
772                .env("LC_ALL", "C")
773                .env("LANG", "C")
774                .stdin(Stdio::null())
775                .stdout(Stdio::piped())
776                .stderr(Stdio::piped())
777                .output()
778                && output.status.success()
779            {
780                let text = String::from_utf8_lossy(&output.stdout);
781                return parse_pacman_si_conflicts(&text);
782            }
783            Vec::new()
784        }
785        PackageSource::Aur => {
786            // Try paru/yay first
787            let has_paru = is_command_available("paru");
788            let has_yay = is_command_available("yay");
789
790            if has_paru {
791                tracing::debug!("Trying paru -Si {} for conflicts", name);
792                if let Ok(output) = Command::new("paru")
793                    .args(["-Si", name])
794                    .env("LC_ALL", "C")
795                    .env("LANG", "C")
796                    .stdin(Stdio::null())
797                    .stdout(Stdio::piped())
798                    .stderr(Stdio::piped())
799                    .output()
800                    && output.status.success()
801                {
802                    let text = String::from_utf8_lossy(&output.stdout);
803                    let conflicts = parse_pacman_si_conflicts(&text);
804                    if !conflicts.is_empty() {
805                        return conflicts;
806                    }
807                }
808            }
809
810            if has_yay {
811                tracing::debug!("Trying yay -Si {} for conflicts", name);
812                if let Ok(output) = Command::new("yay")
813                    .args(["-Si", name])
814                    .env("LC_ALL", "C")
815                    .env("LANG", "C")
816                    .stdin(Stdio::null())
817                    .stdout(Stdio::piped())
818                    .stderr(Stdio::piped())
819                    .output()
820                    && output.status.success()
821                {
822                    let text = String::from_utf8_lossy(&output.stdout);
823                    let conflicts = parse_pacman_si_conflicts(&text);
824                    if !conflicts.is_empty() {
825                        return conflicts;
826                    }
827                }
828            }
829
830            // Fall back to .SRCINFO
831            // Note: fetch_srcinfo is async, so we can't use it here in sync context
832            // This is a limitation - conflicts from .SRCINFO won't be detected in sync mode
833            #[cfg(feature = "aur")]
834            {
835                tracing::debug!(
836                    "Skipping .SRCINFO conflict check for {} (requires async context)",
837                    name
838                );
839            }
840
841            Vec::new()
842        }
843    }
844}
845
846/// What: Get priority value for dependency status (lower = more urgent).
847///
848/// Inputs:
849/// - `status`: Dependency status to get priority for.
850///
851/// Output:
852/// - Returns a numeric priority where lower numbers indicate higher urgency.
853///
854/// Details:
855/// - Priority order: Conflict (0) < Missing (1) < `ToInstall` (2) < `ToUpgrade` (3) < Installed (4).
856const fn dependency_priority(status: &DependencyStatus) -> u8 {
857    status.priority()
858}
859
860/// What: Merge a dependency into the dependency map.
861///
862/// Inputs:
863/// - `dep`: Dependency to merge.
864/// - `parent_name`: Name of the package that requires this dependency.
865/// - `installed`: Set of installed package names.
866/// - `provided`: Set of provided packages.
867/// - `upgradable`: Set of upgradable package names.
868/// - `deps`: Mutable reference to the dependency map to update.
869///
870/// Output:
871/// - Updates the `deps` map with the merged dependency.
872///
873/// Details:
874/// - Merges status (keeps worst), version requirements (keeps more restrictive), and `required_by` lists.
875fn merge_dependency<S: BuildHasher>(
876    dep: &Dependency,
877    parent_name: &str,
878    installed: &HashSet<String, S>,
879    provided: &HashSet<String, S>,
880    upgradable: &HashSet<String, S>,
881    deps: &mut HashMap<String, Dependency>,
882) {
883    let dep_name = dep.name.clone();
884
885    // Check if dependency already exists and get its current state
886    let needs_required_by_update = deps
887        .get(&dep_name)
888        .is_none_or(|e| !e.required_by.contains(&parent_name.to_string()));
889
890    // Update or create dependency entry
891    let entry = deps.entry(dep_name.clone()).or_insert_with(|| Dependency {
892        name: dep_name.clone(),
893        version_req: dep.version_req.clone(),
894        status: dep.status.clone(),
895        source: dep.source.clone(),
896        required_by: vec![parent_name.to_string()],
897        depends_on: Vec::new(),
898        is_core: dep.is_core,
899        is_system: dep.is_system,
900    });
901
902    // Update required_by (add the parent if not already present)
903    if needs_required_by_update {
904        entry.required_by.push(parent_name.to_string());
905    }
906
907    // Merge status (keep worst)
908    // But never overwrite a Conflict status - conflicts take precedence
909    if !matches!(entry.status, DependencyStatus::Conflict { .. }) {
910        let existing_priority = dependency_priority(&entry.status);
911        let new_priority = dependency_priority(&dep.status);
912        if new_priority < existing_priority {
913            entry.status = dep.status.clone();
914        }
915    }
916
917    // Merge version requirements (keep more restrictive)
918    // But never overwrite a Conflict status - conflicts take precedence
919    if !dep.version_req.is_empty() && dep.version_req != entry.version_req {
920        // If entry is already a conflict, don't overwrite it with dependency status
921        if matches!(entry.status, DependencyStatus::Conflict { .. }) {
922            // Still update version if needed, but keep conflict status
923            if entry.version_req.is_empty() {
924                entry.version_req.clone_from(&dep.version_req);
925            }
926            return;
927        }
928
929        if entry.version_req.is_empty() {
930            entry.version_req.clone_from(&dep.version_req);
931        } else {
932            // Check which version requirement is more restrictive
933            let existing_status = determine_status(
934                &entry.name,
935                &entry.version_req,
936                installed,
937                provided,
938                upgradable,
939            );
940            let new_status = determine_status(
941                &entry.name,
942                &dep.version_req,
943                installed,
944                provided,
945                upgradable,
946            );
947            let existing_req_priority = dependency_priority(&existing_status);
948            let new_req_priority = dependency_priority(&new_status);
949
950            if new_req_priority < existing_req_priority {
951                entry.version_req.clone_from(&dep.version_req);
952                entry.status = new_status;
953            }
954        }
955    }
956}
957
958/// Dependency resolver for batch package operations.
959///
960/// Provides a high-level API for resolving dependencies for multiple packages,
961/// handling batch operations, conflict detection, and dependency merging.
962pub struct DependencyResolver {
963    /// Resolver configuration.
964    config: ResolverConfig,
965}
966
967impl DependencyResolver {
968    /// What: Create a new dependency resolver with default configuration.
969    ///
970    /// Inputs:
971    /// - (none)
972    ///
973    /// Output:
974    /// - Returns a new `DependencyResolver` with default configuration.
975    ///
976    /// Details:
977    /// - Uses `ResolverConfig::default()` for configuration.
978    /// - Default config: direct dependencies only, no optional/make/check deps, no AUR checking.
979    ///
980    /// # Example
981    ///
982    /// ```no_run
983    /// use arch_toolkit::deps::DependencyResolver;
984    ///
985    /// let resolver = DependencyResolver::new();
986    /// ```
987    #[must_use]
988    pub fn new() -> Self {
989        Self {
990            config: ResolverConfig::default(),
991        }
992    }
993
994    /// What: Create a resolver with custom configuration.
995    ///
996    /// Inputs:
997    /// - `config`: Custom resolver configuration.
998    ///
999    /// Output:
1000    /// - Returns a new `DependencyResolver` with the provided configuration.
1001    ///
1002    /// Details:
1003    /// - Allows customization of dependency resolution behavior.
1004    ///
1005    /// # Example
1006    ///
1007    /// ```no_run
1008    /// use arch_toolkit::deps::DependencyResolver;
1009    /// use arch_toolkit::types::dependency::ResolverConfig;
1010    ///
1011    /// let config = ResolverConfig {
1012    ///     include_optdepends: true,
1013    ///     include_makedepends: false,
1014    ///     include_checkdepends: false,
1015    ///     max_depth: 0,
1016    ///     pkgbuild_cache: None,
1017    ///     check_aur: false,
1018    /// };
1019    /// let resolver = DependencyResolver::with_config(config);
1020    /// ```
1021    #[must_use]
1022    #[allow(clippy::missing_const_for_fn)] // ResolverConfig contains function pointer, can't be const
1023    pub fn with_config(config: ResolverConfig) -> Self {
1024        Self { config }
1025    }
1026
1027    /// What: Resolve a bounded deterministic dependency graph through injected `.SRCINFO` metadata.
1028    ///
1029    /// Inputs:
1030    /// - `packages`: Root package references to expand.
1031    /// - `provider`: Mockable metadata provider returning verified raw `.SRCINFO` text.
1032    /// - `graph_config`: Explicit graph depth, node, timeout, and batch-concurrency bounds.
1033    ///
1034    /// Output:
1035    /// - Returns a graph with lexical nodes/edges and non-fatal structured diagnostics.
1036    ///
1037    /// # Errors
1038    ///
1039    /// Returns `Err(ArchToolkitError::InvalidInput)` when node, timeout, or provider batch bounds
1040    /// are zero.
1041    ///
1042    /// Details:
1043    /// - This additive path preserves `resolve` as the synchronous direct-only host resolver.
1044    /// - It is available with `deps` alone and does not execute helpers, pacman, or network I/O.
1045    /// - Metadata is cached only for this call; rendering is available separately through
1046    ///   `DependencyGraphResolution::render_tree`.
1047    pub fn resolve_graph<P: DependencyMetadataProvider>(
1048        &self,
1049        packages: &[PackageRef],
1050        provider: &P,
1051        graph_config: DependencyGraphConfig,
1052    ) -> Result<DependencyGraphResolution> {
1053        resolve_dependency_graph(
1054            packages,
1055            provider,
1056            graph_config,
1057            self.config.include_optdepends,
1058            self.config.include_makedepends,
1059            self.config.include_checkdepends,
1060        )
1061    }
1062
1063    /// What: Resolve dependencies for a list of packages.
1064    ///
1065    /// Inputs:
1066    /// - `packages`: Slice of `PackageRef` records to resolve dependencies for.
1067    ///
1068    /// Output:
1069    /// - Returns `Ok(DependencyResolution)` with resolved dependencies, conflicts, and missing packages.
1070    /// - Returns `Err(ArchToolkitError)` if resolution fails.
1071    ///
1072    /// Details:
1073    /// - Resolves ONLY direct dependencies (non-recursive) for each package.
1074    /// - Merges duplicates by name, retaining the most severe status across all requesters.
1075    /// - Detects conflicts between packages being installed and already installed packages.
1076    /// - Sorts dependencies by priority (conflicts first, then missing, then to-install, then installed).
1077    /// - Uses batch fetching for official packages to reduce pacman command overhead.
1078    ///
1079    /// # Errors
1080    ///
1081    /// Returns `Err(ArchToolkitError::Parse)` if pacman commands fail or output cannot be parsed.
1082    /// Returns `Err(ArchToolkitError::PackageNotFound)` if required packages are not found.
1083    ///
1084    /// # Example
1085    ///
1086    /// ```no_run
1087    /// use arch_toolkit::deps::DependencyResolver;
1088    /// use arch_toolkit::{PackageRef, PackageSource};
1089    ///
1090    /// let resolver = DependencyResolver::new();
1091    /// let packages = vec![
1092    ///     PackageRef {
1093    ///         name: "firefox".into(),
1094    ///         version: "121.0".into(),
1095    ///         source: PackageSource::Official {
1096    ///             repo: "extra".into(),
1097    ///             arch: "x86_64".into(),
1098    ///         },
1099    ///     },
1100    /// ];
1101    ///
1102    /// let result = resolver.resolve(&packages)?;
1103    /// println!("Found {} dependencies", result.dependencies.len());
1104    /// # Ok::<(), arch_toolkit::error::ArchToolkitError>(())
1105    /// ```
1106    pub fn resolve(
1107        &self,
1108        packages: &[PackageRef],
1109    ) -> Result<crate::types::dependency::DependencyResolution> {
1110        use crate::types::dependency::DependencyResolution;
1111
1112        if packages.is_empty() {
1113            tracing::warn!("No packages provided for dependency resolution");
1114            return Ok(DependencyResolution::default());
1115        }
1116
1117        let mut deps: HashMap<String, Dependency> = HashMap::new();
1118        let mut conflicts: Vec<String> = Vec::new();
1119        let mut missing: Vec<String> = Vec::new();
1120
1121        // Get installed packages set
1122        tracing::info!("Fetching list of installed packages...");
1123        let installed = get_installed_packages()?;
1124        tracing::info!("Found {} installed packages", installed.len());
1125
1126        // Get all provided packages (e.g., rustup provides rust)
1127        // Note: Provides are checked lazily on-demand for performance, not built upfront
1128        tracing::debug!(
1129            "Provides will be checked lazily on-demand (not building full set for performance)"
1130        );
1131        let provided = get_provided_packages(&installed);
1132
1133        // Get list of upgradable packages to detect if dependencies need upgrades
1134        let upgradable = get_upgradable_packages()?;
1135        tracing::info!("Found {} upgradable packages", upgradable.len());
1136
1137        // Initialize set of root packages (for tracking)
1138        let root_names: HashSet<String> = packages.iter().map(|p| p.name.clone()).collect();
1139
1140        // Check conflicts for packages being installed
1141        tracing::info!("Checking conflicts for {} package(s)", packages.len());
1142        for package in packages {
1143            let package_conflicts = fetch_package_conflicts(&package.name, &package.source);
1144            for conflict_name in package_conflicts {
1145                if installed.contains(&conflict_name) || root_names.contains(&conflict_name) {
1146                    if !conflicts.contains(&conflict_name) {
1147                        conflicts.push(conflict_name.clone());
1148                    }
1149                    // Mark as conflict in dependency map
1150                    let dep = Dependency {
1151                        name: conflict_name,
1152                        version_req: String::new(),
1153                        status: DependencyStatus::Conflict {
1154                            reason: format!("Conflicts with {}", package.name),
1155                        },
1156                        source: DependencySource::Local,
1157                        required_by: vec![package.name.clone()],
1158                        depends_on: Vec::new(),
1159                        is_core: false,
1160                        is_system: false,
1161                    };
1162                    merge_dependency(
1163                        &dep,
1164                        &package.name,
1165                        &installed,
1166                        &provided,
1167                        &upgradable,
1168                        &mut deps,
1169                    );
1170                }
1171            }
1172        }
1173
1174        // Batch fetch official package dependencies to reduce pacman command overhead
1175        let official_packages: Vec<&str> = packages
1176            .iter()
1177            .filter_map(|pkg| {
1178                if let PackageSource::Official { repo, .. } = &pkg.source {
1179                    if repo == "local" {
1180                        None
1181                    } else {
1182                        Some(pkg.name.as_str())
1183                    }
1184                } else {
1185                    None
1186                }
1187            })
1188            .collect();
1189        let batched_deps_cache = if official_packages.is_empty() {
1190            HashMap::new()
1191        } else {
1192            batch_fetch_official_deps(&official_packages)
1193        };
1194
1195        // Resolve ONLY direct dependencies (non-recursive)
1196        // This is faster and avoids resolving transitive dependencies which can be slow and error-prone
1197        for package in packages {
1198            // Check if we have batched results for this official package
1199            let use_batched = matches!(package.source, PackageSource::Official { ref repo, .. } if repo != "local")
1200                && batched_deps_cache.contains_key(package.name.as_str());
1201
1202            let resolved_deps = if use_batched {
1203                // Use batched dependency list
1204                let dep_names = batched_deps_cache
1205                    .get(package.name.as_str())
1206                    .cloned()
1207                    .unwrap_or_default();
1208                process_dependency_specs(
1209                    dep_names,
1210                    &package.name,
1211                    &installed,
1212                    &provided,
1213                    &upgradable,
1214                )
1215            } else {
1216                // Resolve individually
1217                match resolve_package_deps(
1218                    &package.name,
1219                    &package.source,
1220                    &installed,
1221                    &provided,
1222                    &upgradable,
1223                    self.config
1224                        .pkgbuild_cache
1225                        .as_ref()
1226                        .map(|f| f.as_ref() as &(dyn Fn(&str) -> Option<String> + Send + Sync)),
1227                ) {
1228                    Ok(deps) => deps,
1229                    Err(e) => {
1230                        tracing::warn!(
1231                            "  Failed to resolve dependencies for {}: {}",
1232                            package.name,
1233                            e
1234                        );
1235                        // Mark as missing
1236                        if !missing.contains(&package.name) {
1237                            missing.push(package.name.clone());
1238                        }
1239                        continue;
1240                    }
1241                }
1242            };
1243
1244            tracing::debug!(
1245                "  Found {} dependencies for {}",
1246                resolved_deps.len(),
1247                package.name
1248            );
1249
1250            for dep in resolved_deps {
1251                // Check if dependency is missing
1252                if matches!(dep.status, DependencyStatus::Missing) && !missing.contains(&dep.name) {
1253                    missing.push(dep.name.clone());
1254                }
1255
1256                merge_dependency(
1257                    &dep,
1258                    &package.name,
1259                    &installed,
1260                    &provided,
1261                    &upgradable,
1262                    &mut deps,
1263                );
1264
1265                // DON'T recursively resolve dependencies - only show direct dependencies
1266                // This prevents resolving transitive dependencies which can be slow and error-prone
1267            }
1268        }
1269
1270        let mut result: Vec<Dependency> = deps.into_values().collect();
1271        tracing::info!("Total unique dependencies found: {}", result.len());
1272
1273        // Sort dependencies: conflicts first, then missing, then to-install, then installed
1274        result.sort_by(|a, b| {
1275            let priority_a = dependency_priority(&a.status);
1276            let priority_b = dependency_priority(&b.status);
1277            priority_a
1278                .cmp(&priority_b)
1279                .then_with(|| a.name.cmp(&b.name))
1280        });
1281
1282        Ok(DependencyResolution {
1283            dependencies: result,
1284            conflicts,
1285            missing,
1286        })
1287    }
1288}
1289
1290impl Default for DependencyResolver {
1291    fn default() -> Self {
1292        Self::new()
1293    }
1294}
1295
1296#[cfg(test)]
1297mod tests {
1298    use super::*;
1299    use crate::types::dependency::DependencyStatus;
1300
1301    #[test]
1302    fn test_should_filter_dependency() {
1303        assert!(should_filter_dependency("libfoo.so", "package"));
1304        assert!(should_filter_dependency("libfoo.so.1", "package"));
1305        assert!(should_filter_dependency("libfoo.so=1", "package"));
1306        assert!(should_filter_dependency("package", "package")); // self-reference
1307        assert!(!should_filter_dependency("glibc", "package"));
1308        assert!(!should_filter_dependency("firefox", "package"));
1309    }
1310
1311    #[test]
1312    fn test_determine_status_not_installed() {
1313        let installed = HashSet::new();
1314        let provided = HashSet::new();
1315        let upgradable = HashSet::new();
1316
1317        let status = determine_status("nonexistent", "", &installed, &provided, &upgradable);
1318        assert!(matches!(status, DependencyStatus::ToInstall));
1319    }
1320
1321    #[test]
1322    fn test_batch_fetch_official_deps_parsing() {
1323        // Test parsing logic with sample output
1324        let sample_output = "Name            : firefox\nDepends On      : glibc\n\nName            : vim\nDepends On      : glibc\n";
1325        let mut package_blocks = Vec::new();
1326        let mut current_block = String::new();
1327        for line in sample_output.lines() {
1328            if line.trim().is_empty() {
1329                if !current_block.is_empty() {
1330                    package_blocks.push(current_block.clone());
1331                    current_block.clear();
1332                }
1333            } else {
1334                current_block.push_str(line);
1335                current_block.push('\n');
1336            }
1337        }
1338        if !current_block.is_empty() {
1339            package_blocks.push(current_block);
1340        }
1341
1342        assert_eq!(package_blocks.len(), 2);
1343        assert!(package_blocks[0].contains("firefox"));
1344        assert!(package_blocks[1].contains("vim"));
1345    }
1346
1347    #[test]
1348    fn test_dependency_priority() {
1349        assert_eq!(
1350            dependency_priority(&DependencyStatus::Conflict {
1351                reason: "test".to_string(),
1352            }),
1353            0
1354        );
1355        assert_eq!(dependency_priority(&DependencyStatus::Missing), 1);
1356        assert_eq!(dependency_priority(&DependencyStatus::ToInstall), 2);
1357        assert_eq!(
1358            dependency_priority(&DependencyStatus::ToUpgrade {
1359                current: "1.0".to_string(),
1360                required: "2.0".to_string(),
1361            }),
1362            3
1363        );
1364        assert_eq!(
1365            dependency_priority(&DependencyStatus::Installed {
1366                version: "1.0".to_string(),
1367            }),
1368            4
1369        );
1370    }
1371
1372    #[test]
1373    fn test_dependency_resolver_new() {
1374        let resolver = DependencyResolver::new();
1375        // Just verify it can be created
1376        assert!(matches!(resolver.config.max_depth, 0));
1377    }
1378
1379    #[test]
1380    fn test_dependency_resolver_with_config() {
1381        let config = ResolverConfig {
1382            include_optdepends: true,
1383            include_makedepends: true,
1384            include_checkdepends: true,
1385            max_depth: 2,
1386            pkgbuild_cache: None,
1387            check_aur: true,
1388        };
1389        let resolver = DependencyResolver::with_config(config);
1390        assert_eq!(resolver.config.max_depth, 2);
1391        assert!(resolver.config.include_optdepends);
1392        assert!(resolver.config.check_aur);
1393    }
1394
1395    #[test]
1396    fn test_dependency_resolver_resolve_empty() {
1397        let resolver = DependencyResolver::new();
1398        let result = resolver
1399            .resolve(&[])
1400            .expect("resolve should succeed for empty packages");
1401        assert_eq!(result.dependencies.len(), 0);
1402        assert_eq!(result.conflicts.len(), 0);
1403        assert_eq!(result.missing.len(), 0);
1404    }
1405
1406    // Integration tests that require pacman - these are ignored by default
1407    #[test]
1408    #[ignore = "Requires pacman to be available"]
1409    fn test_dependency_resolver_resolve_integration() {
1410        let resolver = DependencyResolver::new();
1411        let packages = vec![PackageRef {
1412            name: "pacman".to_string(),
1413            version: "6.1.0".to_string(),
1414            source: PackageSource::Official {
1415                repo: "core".to_string(),
1416                arch: "x86_64".to_string(),
1417            },
1418        }];
1419
1420        if let Ok(result) = resolver.resolve(&packages) {
1421            // Should find some dependencies for pacman
1422            println!("Found {} dependencies", result.dependencies.len());
1423            assert!(!result.dependencies.is_empty());
1424        }
1425    }
1426}