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}