pacsea/logic/deps/
query.rs

1//! Package querying functions for dependency resolution.
2
3use std::collections::HashSet;
4use std::hash::BuildHasher;
5use std::process::{Command, Stdio};
6
7/// What: Collect names of packages that have upgrades available via pacman.
8///
9/// Inputs:
10/// - (none): Reads upgrade information by invoking `pacman -Qu`.
11///
12/// Output:
13/// - Returns a set containing package names that pacman reports as upgradable.
14///
15/// Details:
16/// - Trims each line from the command output and extracts the leading package token before version metadata.
17/// - Gracefully handles command failures by returning an empty set to avoid blocking dependency checks.
18pub fn get_upgradable_packages() -> HashSet<String> {
19    tracing::debug!("Running: pacman -Qu");
20    let output = Command::new("pacman")
21        .args(["-Qu"])
22        .env("LC_ALL", "C")
23        .env("LANG", "C")
24        .stdin(Stdio::null())
25        .stdout(Stdio::piped())
26        .stderr(Stdio::piped())
27        .output();
28
29    match output {
30        Ok(output) => {
31            if output.status.success() {
32                let text = String::from_utf8_lossy(&output.stdout);
33                // pacman -Qu outputs "name old-version -> new-version" or just "name" for AUR packages
34                let packages: HashSet<String> = text
35                    .lines()
36                    .filter_map(|line| {
37                        let line = line.trim();
38                        if line.is_empty() {
39                            return None;
40                        }
41                        // Extract package name (everything before space or "->")
42                        Some(line.find(' ').map_or_else(
43                            || line.to_string(),
44                            |space_pos| line[..space_pos].trim().to_string(),
45                        ))
46                    })
47                    .collect();
48                tracing::debug!(
49                    "Successfully retrieved {} upgradable packages",
50                    packages.len()
51                );
52                packages
53            } else {
54                // No upgradable packages or error - return empty set
55                HashSet::new()
56            }
57        }
58        Err(e) => {
59            tracing::debug!("Failed to execute pacman -Qu: {} (assuming no upgrades)", e);
60            HashSet::new()
61        }
62    }
63}
64
65/// What: Enumerate all currently installed packages on the system.
66///
67/// Inputs:
68/// - (none): Invokes `pacman -Qq` to query the local database.
69///
70/// Output:
71/// - Returns a set of package names installed on the machine; empty on failure.
72///
73/// Details:
74/// - Uses pacman's quiet format to obtain trimmed names and logs errors where available for diagnostics.
75pub fn get_installed_packages() -> HashSet<String> {
76    tracing::debug!("Running: pacman -Qq");
77    let output = Command::new("pacman")
78        .args(["-Qq"])
79        .env("LC_ALL", "C")
80        .env("LANG", "C")
81        .stdin(Stdio::null())
82        .stdout(Stdio::piped())
83        .stderr(Stdio::piped())
84        .output();
85
86    match output {
87        Ok(output) => {
88            if output.status.success() {
89                let text = String::from_utf8_lossy(&output.stdout);
90                let packages: HashSet<String> =
91                    text.lines().map(|s| s.trim().to_string()).collect();
92                tracing::debug!(
93                    "Successfully retrieved {} installed packages",
94                    packages.len()
95                );
96                packages
97            } else {
98                let stderr = String::from_utf8_lossy(&output.stderr);
99                tracing::error!(
100                    "pacman -Qq failed with status {:?}: {}",
101                    output.status.code(),
102                    stderr
103                );
104                HashSet::new()
105            }
106        }
107        Err(e) => {
108            tracing::error!("Failed to execute pacman -Qq: {}", e);
109            HashSet::new()
110        }
111    }
112}
113
114/// What: Check if a specific package name is provided by any installed package (lazy check).
115///
116/// Inputs:
117/// - `name`: Package name to check.
118/// - `installed`: Set of installed package names (used to optimize search).
119///
120/// Output:
121/// - Returns `Some(package_name)` if the name is provided by an installed package, `None` otherwise.
122///
123/// Details:
124/// - Uses `pacman -Qqo` to efficiently check if any installed package provides the name.
125/// - This is much faster than querying all packages upfront.
126/// - Returns the name of the providing package for debugging purposes.
127fn check_if_provided<S: BuildHasher + Default>(
128    name: &str,
129    _installed: &HashSet<String, S>,
130) -> Option<String> {
131    // Use pacman -Qqo to check which package provides this name
132    // This is efficient - pacman does the lookup internally
133    let output = Command::new("pacman")
134        .args(["-Qqo", name])
135        .env("LC_ALL", "C")
136        .env("LANG", "C")
137        .stdin(Stdio::null())
138        .stdout(Stdio::piped())
139        .stderr(Stdio::piped())
140        .output();
141
142    match output {
143        Ok(output) if output.status.success() => {
144            let text = String::from_utf8_lossy(&output.stdout);
145            let providing_pkg = text.lines().next().map(|s| s.trim().to_string());
146            if let Some(providing_pkg) = &providing_pkg {
147                tracing::debug!("{} is provided by {}", name, providing_pkg);
148            }
149            providing_pkg
150        }
151        _ => None,
152    }
153}
154
155/// What: Build an empty provides set (for API compatibility).
156///
157/// Inputs:
158/// - `installed`: Set of installed package names (unused, kept for API compatibility).
159///
160/// Output:
161/// - Returns an empty set (provides are now checked lazily).
162///
163/// Details:
164/// - This function is kept for API compatibility but no longer builds the full provides set.
165/// - Provides are now checked on-demand using `check_if_provided()` for better performance.
166#[must_use]
167pub fn get_provided_packages<S: BuildHasher + Default>(
168    _installed: &HashSet<String, S>,
169) -> HashSet<String> {
170    // Return empty set - provides are now checked lazily on-demand
171    // This avoids querying all installed packages upfront, which was very slow
172    HashSet::new()
173}
174
175/// What: Check if a package is installed or provided by an installed package.
176///
177/// Inputs:
178/// - `name`: Package name to check.
179/// - `installed`: Set of directly installed package names.
180/// - `provided`: Set of package names provided by installed packages (unused, kept for API compatibility).
181///
182/// Output:
183/// - Returns `true` if the package is directly installed or provided by an installed package.
184///
185/// Details:
186/// - First checks if the package is directly installed.
187/// - Then lazily checks if it's provided by any installed package using `pacman -Qqo`.
188/// - This handles cases like `rustup` providing `rust` efficiently without querying all packages upfront.
189#[must_use]
190pub fn is_package_installed_or_provided<S: BuildHasher + Default>(
191    name: &str,
192    installed: &HashSet<String, S>,
193    _provided: &HashSet<String, S>,
194) -> bool {
195    // First check if directly installed
196    if installed.contains(name) {
197        return true;
198    }
199
200    // Lazy check if provided by any installed package (much faster than building full set upfront)
201    check_if_provided(name, installed).is_some()
202}