pacsea/logic/
deps.rs

1//! Dependency resolution and analysis for preflight checks.
2
3mod aur;
4mod parse;
5mod query;
6mod resolve;
7mod reverse;
8mod source;
9mod srcinfo;
10mod status;
11mod utils;
12
13use crate::state::modal::{DependencyInfo, DependencyStatus};
14use crate::state::types::{PackageItem, Source};
15use parse::parse_dep_spec;
16use resolve::{batch_fetch_official_deps, fetch_package_conflicts, resolve_package_deps};
17use source::{determine_dependency_source, is_system_package};
18use status::determine_status;
19use std::collections::{HashMap, HashSet};
20use utils::dependency_priority;
21
22pub use query::{
23    get_installed_packages, get_provided_packages, get_upgradable_packages,
24    is_package_installed_or_provided,
25};
26pub use reverse::{
27    ReverseDependencyReport, get_installed_required_by, has_installed_required_by,
28    resolve_reverse_dependencies,
29};
30pub use status::{get_installed_version, version_satisfies};
31
32/// What: Check and process conflicts for a package.
33///
34/// Inputs:
35/// - `item`: Package item to check conflicts for.
36/// - `root_names`: Set of root package names in the install list.
37/// - `installed`: Set of installed package names.
38/// - `provided`: Map of provided packages.
39/// - `deps`: Mutable reference to the dependency map to update.
40///
41/// Output:
42/// - Updates the `deps` map with conflict entries.
43///
44/// Details:
45/// - Checks conflicts against installed packages and packages in the install list.
46/// - Creates conflict entries for both the conflicting package and the current package if needed.
47fn process_conflicts(
48    item: &PackageItem,
49    root_names: &HashSet<String>,
50    installed: &HashSet<String>,
51    provided: &HashSet<String>,
52    deps: &mut HashMap<String, DependencyInfo>,
53) {
54    let conflicts = fetch_package_conflicts(&item.name, &item.source);
55    if conflicts.is_empty() {
56        return;
57    }
58
59    tracing::debug!("Package {} conflicts with: {:?}", item.name, conflicts);
60
61    for conflict_name in conflicts {
62        // Skip self-conflicts (package conflicting with itself)
63        if conflict_name.eq_ignore_ascii_case(&item.name) {
64            tracing::debug!(
65                "Skipping self-conflict: {} conflicts with itself",
66                item.name
67            );
68            continue;
69        }
70
71        // Check if conflict is installed or provided by any installed package
72        let is_installed = crate::logic::deps::query::is_package_installed_or_provided(
73            &conflict_name,
74            installed,
75            provided,
76        );
77
78        // Check if conflict is in the install list
79        let is_in_install_list = root_names.contains(&conflict_name);
80
81        if !is_installed && !is_in_install_list {
82            continue;
83        }
84
85        let reason = if is_installed && is_in_install_list {
86            format!("conflicts with {conflict_name} (installed and in install list)")
87        } else if is_installed {
88            format!("conflicts with installed package {conflict_name}")
89        } else {
90            format!("conflicts with package {conflict_name} in install list")
91        };
92
93        // Add or update conflict entry for the conflicting package
94        let entry = deps.entry(conflict_name.clone()).or_insert_with(|| {
95            // Determine source for conflicting package
96            let (source, is_core) =
97                crate::logic::deps::source::determine_dependency_source(&conflict_name, installed);
98            let is_system =
99                is_core || crate::logic::deps::source::is_system_package(&conflict_name);
100
101            DependencyInfo {
102                name: conflict_name.clone(),
103                version: String::new(),
104                status: DependencyStatus::Conflict {
105                    reason: reason.clone(),
106                },
107                source,
108                required_by: vec![item.name.clone()],
109                depends_on: Vec::new(),
110                is_core,
111                is_system,
112            }
113        });
114
115        // Update status to Conflict if not already
116        if !matches!(entry.status, DependencyStatus::Conflict { .. }) {
117            entry.status = DependencyStatus::Conflict { reason };
118        }
119
120        // Add to required_by if not present
121        if !entry.required_by.contains(&item.name) {
122            entry.required_by.push(item.name.clone());
123        }
124
125        // If the conflict is with another package in the install list, also create
126        // a conflict entry for the current package being checked, so it shows up
127        // in the UI as having a conflict
128        if is_in_install_list {
129            let reverse_reason = format!("conflicts with package {conflict_name} in install list");
130            let current_entry = deps.entry(item.name.clone()).or_insert_with(|| {
131                // Determine source for current package
132                let (dep_source, is_core) =
133                    crate::logic::deps::source::determine_dependency_source(&item.name, installed);
134                let is_system =
135                    is_core || crate::logic::deps::source::is_system_package(&item.name);
136
137                DependencyInfo {
138                    name: item.name.clone(),
139                    version: String::new(),
140                    status: DependencyStatus::Conflict {
141                        reason: reverse_reason.clone(),
142                    },
143                    source: dep_source,
144                    required_by: vec![conflict_name.clone()],
145                    depends_on: Vec::new(),
146                    is_core,
147                    is_system,
148                }
149            });
150
151            // Update status to Conflict if not already
152            if !matches!(current_entry.status, DependencyStatus::Conflict { .. }) {
153                current_entry.status = DependencyStatus::Conflict {
154                    reason: reverse_reason,
155                };
156            }
157
158            // Add to required_by if not present
159            if !current_entry.required_by.contains(&conflict_name) {
160                current_entry.required_by.push(conflict_name.clone());
161            }
162        }
163    }
164}
165
166/// What: Process batched dependencies for an official package.
167///
168/// Inputs:
169/// - `name`: Package name.
170/// - `dep_names`: Vector of dependency specification strings.
171/// - `installed`: Set of installed package names.
172/// - `provided`: Map of provided packages.
173/// - `upgradable`: Set of upgradable package names.
174///
175/// Output:
176/// - Returns a vector of `DependencyInfo` records.
177///
178/// Details:
179/// - Parses dependency specifications and filters out self-references and .so files.
180#[allow(clippy::case_sensitive_file_extension_comparisons)]
181fn process_batched_dependencies(
182    name: &str,
183    dep_names: Vec<String>,
184    installed: &HashSet<String>,
185    provided: &HashSet<String>,
186    upgradable: &HashSet<String>,
187) -> Vec<DependencyInfo> {
188    let mut deps = Vec::new();
189    for dep_spec in dep_names {
190        let (pkg_name, version_req) = parse_dep_spec(&dep_spec);
191        if pkg_name == name {
192            continue;
193        }
194        let pkg_lower = pkg_name.to_lowercase();
195        if pkg_lower.ends_with(".so") || pkg_lower.contains(".so.") || pkg_lower.contains(".so=") {
196            continue;
197        }
198        let status = determine_status(&pkg_name, &version_req, installed, provided, upgradable);
199        let (dep_source, is_core) = determine_dependency_source(&pkg_name, installed);
200        let is_system = is_core || is_system_package(&pkg_name);
201        deps.push(DependencyInfo {
202            name: pkg_name,
203            version: version_req,
204            status,
205            source: dep_source,
206            required_by: vec![name.to_string()],
207            depends_on: Vec::new(),
208            is_core,
209            is_system,
210        });
211    }
212    deps
213}
214
215/// What: Merge a dependency into the dependency map.
216///
217/// Inputs:
218/// - `dep`: Dependency to merge.
219/// - `parent_name`: Name of the package that requires this dependency.
220/// - `installed`: Set of installed package names.
221/// - `provided`: Map of provided packages.
222/// - `upgradable`: Set of upgradable package names.
223/// - `deps`: Mutable reference to the dependency map to update.
224///
225/// Output:
226/// - Updates the `deps` map with the merged dependency.
227///
228/// Details:
229/// - Merges status (keeps worst), version requirements (keeps more restrictive), and `required_by` lists.
230fn merge_dependency(
231    dep: &DependencyInfo,
232    parent_name: &str,
233    installed: &HashSet<String>,
234    provided: &HashSet<String>,
235    upgradable: &HashSet<String>,
236    deps: &mut HashMap<String, DependencyInfo>,
237) {
238    let dep_name = dep.name.clone();
239
240    // Check if dependency already exists and get its current state
241    let existing_dep = deps.get(&dep_name).cloned();
242    let needs_required_by_update = existing_dep
243        .as_ref()
244        .is_none_or(|e| !e.required_by.contains(&parent_name.to_string()));
245
246    // Update or create dependency entry
247    let entry = deps
248        .entry(dep_name.clone())
249        .or_insert_with(|| DependencyInfo {
250            name: dep_name.clone(),
251            version: dep.version.clone(),
252            status: dep.status.clone(),
253            source: dep.source.clone(),
254            required_by: vec![parent_name.to_string()],
255            depends_on: Vec::new(),
256            is_core: dep.is_core,
257            is_system: dep.is_system,
258        });
259
260    // Update required_by (add the parent if not already present)
261    if needs_required_by_update {
262        entry.required_by.push(parent_name.to_string());
263    }
264
265    // Merge status (keep worst)
266    // But never overwrite a Conflict status - conflicts take precedence
267    if !matches!(entry.status, DependencyStatus::Conflict { .. }) {
268        let existing_priority = dependency_priority(&entry.status);
269        let new_priority = dependency_priority(&dep.status);
270        if new_priority < existing_priority {
271            entry.status = dep.status.clone();
272        }
273    }
274
275    // Merge version requirements (keep more restrictive)
276    // But never overwrite a Conflict status - conflicts take precedence
277    if !dep.version.is_empty() && dep.version != entry.version {
278        // If entry is already a conflict, don't overwrite it with dependency status
279        if matches!(entry.status, DependencyStatus::Conflict { .. }) {
280            // Still update version if needed, but keep conflict status
281            if entry.version.is_empty() {
282                entry.version.clone_from(&dep.version);
283            }
284            return;
285        }
286
287        if entry.version.is_empty() {
288            entry.version.clone_from(&dep.version);
289        } else {
290            // Check which version requirement is more restrictive
291            let existing_status =
292                determine_status(&entry.name, &entry.version, installed, provided, upgradable);
293            let new_status =
294                determine_status(&entry.name, &dep.version, installed, provided, upgradable);
295            let existing_req_priority = dependency_priority(&existing_status);
296            let new_req_priority = dependency_priority(&new_status);
297
298            if new_req_priority < existing_req_priority {
299                entry.version.clone_from(&dep.version);
300                entry.status = new_status;
301            }
302        }
303    }
304}
305
306/// What: Resolve dependencies for a single package.
307///
308/// Inputs:
309/// - `item`: Package item to resolve dependencies for.
310/// - `batched_deps_cache`: Optional cache of batched dependencies for official packages.
311/// - `installed`: Set of installed package names.
312/// - `provided`: Map of provided packages.
313/// - `upgradable`: Set of upgradable package names.
314///
315/// Output:
316/// - Returns a result containing a vector of `DependencyInfo` records or an error.
317///
318/// Details:
319/// - Uses batched cache if available for official packages, otherwise calls `resolve_package_deps`.
320fn resolve_single_package_deps(
321    item: &PackageItem,
322    batched_deps_cache: &HashMap<String, Vec<String>>,
323    installed: &HashSet<String>,
324    provided: &HashSet<String>,
325    upgradable: &HashSet<String>,
326) -> Result<Vec<DependencyInfo>, String> {
327    let name = &item.name;
328    let source = &item.source;
329
330    tracing::debug!(
331        "Resolving direct dependencies for {} (source: {:?})",
332        name,
333        source
334    );
335
336    // Check if we have batched results for this official package
337    let use_batched = matches!(source, Source::Official { repo, .. } if repo != "local")
338        && batched_deps_cache.contains_key(name.as_str());
339
340    if use_batched {
341        // Use batched dependency list
342        let dep_names = batched_deps_cache
343            .get(name.as_str())
344            .cloned()
345            .unwrap_or_default();
346        let deps = process_batched_dependencies(name, dep_names, installed, provided, upgradable);
347        Ok(deps)
348    } else {
349        resolve_package_deps(name, source, installed, provided, upgradable)
350    }
351}
352
353/// What: Resolve dependencies for the requested install set while consolidating duplicates.
354///
355/// Inputs:
356/// - `items`: Ordered slice of packages that should be analysed for dependency coverage.
357///
358/// Output:
359/// - Returns a vector of `DependencyInfo` records summarising dependency status and provenance.
360///
361/// Details:
362/// - Resolves ONLY direct dependencies (non-recursive) for each package in the list.
363/// - Merges duplicates by name, retaining the most severe status across all requesters.
364/// - Populates `depends_on` and `required_by` relationships to reflect dependency relationships.
365pub fn resolve_dependencies(items: &[PackageItem]) -> Vec<DependencyInfo> {
366    let _span = tracing::info_span!(
367        "resolve_dependencies",
368        stage = "dependencies",
369        item_count = items.len()
370    )
371    .entered();
372    let start_time = std::time::Instant::now();
373    // Only warn if called from UI thread (not from background workers)
374    // Background workers use spawn_blocking which is fine and expected
375    let backtrace = std::backtrace::Backtrace::force_capture();
376    let backtrace_str = format!("{backtrace:?}");
377    // Only warn if NOT in a blocking task (i.e., called from UI thread/event handlers)
378    // Check for various indicators that we're in a blocking thread pool
379    let is_blocking_task = backtrace_str.contains("blocking::task")
380        || backtrace_str.contains("blocking::pool")
381        || backtrace_str.contains("spawn_blocking");
382    if !is_blocking_task {
383        tracing::warn!(
384            "[Deps] resolve_dependencies called synchronously from UI thread! This will block! Backtrace:\n{}",
385            backtrace_str
386        );
387    }
388
389    if items.is_empty() {
390        tracing::warn!("No packages provided for dependency resolution");
391        return Vec::new();
392    }
393
394    let mut deps: HashMap<String, DependencyInfo> = HashMap::new();
395
396    // Get installed packages set
397    tracing::info!("Fetching list of installed packages...");
398    let installed = get_installed_packages();
399    tracing::info!("Found {} installed packages", installed.len());
400
401    // Get all provided packages (e.g., rustup provides rust)
402    // Note: Provides are checked lazily on-demand for performance, not built upfront
403    tracing::debug!(
404        "Provides will be checked lazily on-demand (not building full set for performance)"
405    );
406    let provided = get_provided_packages(&installed);
407
408    // Get list of upgradable packages to detect if dependencies need upgrades
409    let upgradable = get_upgradable_packages();
410    tracing::info!("Found {} upgradable packages", upgradable.len());
411
412    // Initialize set of root packages (for tracking)
413    let root_names: HashSet<String> = items.iter().map(|i| i.name.clone()).collect();
414
415    // Check conflicts for packages being installed
416    // 1. Check conflicts against installed packages
417    // 2. Check conflicts between packages in the install list
418    tracing::info!("Checking conflicts for {} package(s)", items.len());
419    for item in items {
420        process_conflicts(item, &root_names, &installed, &provided, &mut deps);
421    }
422
423    // Note: Reverse conflict checking (checking all installed packages for conflicts with install list)
424    // has been removed for performance reasons. Checking 2000+ installed packages would require
425    // 2000+ calls to pacman -Si / yay -Si, which is extremely slow.
426    //
427    // The forward check above is sufficient and fast:
428    // - For each package in install list, fetch its conflicts once (1-10 calls total)
429    // - Check if those conflict names are in the installed package set (O(1) HashSet lookup)
430    // - This catches all conflicts where install list packages conflict with installed packages
431    //
432    // Conflicts are typically symmetric (if A conflicts with B, then B conflicts with A),
433    // so the forward check should catch most cases. If an installed package declares a conflict
434    // with a package in the install list, it will be detected when we check the install list
435    // package's conflicts against the installed package set.
436
437    // Batch fetch official package dependencies to reduce pacman command overhead
438    let official_packages: Vec<&str> = items
439        .iter()
440        .filter_map(|item| {
441            if let Source::Official { repo, .. } = &item.source {
442                if *repo == "local" {
443                    None
444                } else {
445                    Some(item.name.as_str())
446                }
447            } else {
448                None
449            }
450        })
451        .collect();
452    let batched_deps_cache = if official_packages.is_empty() {
453        std::collections::HashMap::new()
454    } else {
455        batch_fetch_official_deps(&official_packages)
456    };
457
458    // Resolve ONLY direct dependencies (non-recursive)
459    // This is faster and avoids resolving transitive dependencies which can be slow and error-prone
460    for item in items {
461        match resolve_single_package_deps(
462            item,
463            &batched_deps_cache,
464            &installed,
465            &provided,
466            &upgradable,
467        ) {
468            Ok(resolved_deps) => {
469                tracing::debug!(
470                    "  Found {} dependencies for {}",
471                    resolved_deps.len(),
472                    item.name
473                );
474
475                for dep in resolved_deps {
476                    merge_dependency(
477                        &dep,
478                        &item.name,
479                        &installed,
480                        &provided,
481                        &upgradable,
482                        &mut deps,
483                    );
484
485                    // DON'T recursively resolve dependencies - only show direct dependencies
486                    // This prevents resolving transitive dependencies which can be slow and error-prone
487                }
488            }
489            Err(e) => {
490                tracing::warn!("  Failed to resolve dependencies for {}: {}", item.name, e);
491            }
492        }
493    }
494
495    let mut result: Vec<DependencyInfo> = deps.into_values().collect();
496    tracing::info!("Total unique dependencies found: {}", result.len());
497
498    // Sort dependencies: conflicts first, then missing, then to-install, then installed
499    result.sort_by(|a, b| {
500        let priority_a = dependency_priority(&a.status);
501        let priority_b = dependency_priority(&b.status);
502        priority_a
503            .cmp(&priority_b)
504            .then_with(|| a.name.cmp(&b.name))
505    });
506
507    let elapsed = start_time.elapsed();
508    let duration_ms = u64::try_from(elapsed.as_millis()).unwrap_or(u64::MAX);
509    tracing::info!(
510        stage = "dependencies",
511        item_count = items.len(),
512        result_count = result.len(),
513        duration_ms = duration_ms,
514        "Dependency resolution complete"
515    );
516    result
517}