1use std::collections::HashSet;
2
3use super::explicit_lock;
4use crate::state::InstalledPackagesMode;
5
6pub async fn refresh_explicit_cache(mode: InstalledPackagesMode) {
19 let args: &[&str] = match mode {
20 InstalledPackagesMode::LeafOnly => &["-Qetq"], InstalledPackagesMode::AllExplicit => &["-Qeq"], };
23 if let Ok(Ok(body)) =
24 tokio::task::spawn_blocking(move || crate::util::pacman::run_pacman(args)).await
25 {
26 let set: HashSet<String> = body.lines().map(|s| s.trim().to_string()).collect();
27 if let Ok(mut g) = explicit_lock().write() {
28 *g = set;
29 }
30 }
31}
32
33#[must_use]
44pub fn explicit_names() -> HashSet<String> {
45 explicit_lock()
46 .read()
47 .map(|s| s.clone())
48 .unwrap_or_default()
49}
50
51#[must_use]
65pub fn query_explicit_packages_sync(mode: InstalledPackagesMode) -> Vec<String> {
66 let args: &[&str] = match mode {
67 InstalledPackagesMode::LeafOnly => &["-Qetq"], InstalledPackagesMode::AllExplicit => &["-Qeq"], };
70 match crate::util::pacman::run_pacman(args) {
71 Ok(body) => {
72 let mut names: Vec<String> = body
73 .lines()
74 .map(|s| s.trim().to_string())
75 .filter(|s| !s.is_empty())
76 .collect();
77 names.sort();
78 names
79 }
80 Err(e) => {
81 tracing::warn!(
82 mode = ?mode,
83 error = %e,
84 "Failed to query explicit packages from pacman"
85 );
86 Vec::new()
87 }
88 }
89}
90
91#[cfg(test)]
92mod tests {
93 #[test]
104 fn explicit_names_returns_empty_when_uninitialized() {
105 let _guard = crate::global_test_mutex_lock();
106 if let Ok(mut g) = super::explicit_lock().write() {
108 g.clear();
109 }
110 let set = super::explicit_names();
111 assert!(set.is_empty());
112 }
113
114 #[test]
125 fn explicit_names_returns_cloned_set() {
126 let _guard = crate::global_test_mutex_lock();
127 if let Ok(mut g) = super::explicit_lock().write() {
128 g.clear();
129 g.insert("a".to_string());
130 g.insert("b".to_string());
131 }
132 let mut set = super::explicit_names();
133 assert_eq!(set.len(), 2);
134 let mut v: Vec<String> = set.drain().collect();
135 v.sort();
136 assert_eq!(v, vec!["a", "b"]);
137 }
138
139 #[cfg(not(target_os = "windows"))]
140 #[allow(clippy::await_holding_lock)]
141 #[tokio::test]
142 async fn refresh_explicit_cache_populates_cache_from_pacman_output() {
153 struct PathGuard {
154 original: String,
155 }
156 impl Drop for PathGuard {
157 fn drop(&mut self) {
158 unsafe {
159 std::env::set_var("PATH", &self.original);
160 }
161 }
162 }
163 let _guard = crate::global_test_mutex_lock();
164
165 if let Ok(mut g) = super::explicit_lock().write() {
166 g.clear();
167 }
168
169 let old_path = std::env::var("PATH").unwrap_or_default();
170 let _path_guard = PathGuard {
171 original: old_path.clone(),
172 };
173
174 let mut root = std::env::temp_dir();
175 root.push(format!(
176 "pacsea_fake_pacman_qetq_{}_{}",
177 std::process::id(),
178 std::time::SystemTime::now()
179 .duration_since(std::time::UNIX_EPOCH)
180 .expect("System time is before UNIX epoch")
181 .as_nanos()
182 ));
183 std::fs::create_dir_all(&root).expect("failed to create test root directory");
184 let mut bin = root.clone();
185 bin.push("bin");
186 std::fs::create_dir_all(&bin).expect("failed to create test bin directory");
187 let mut script = bin.clone();
188 script.push("pacman");
189 let body = r#"#!/usr/bin/env bash
190set -e
191if [[ "$1" == "-Qetq" ]]; then
192 echo "alpha"
193 echo "beta"
194 exit 0
195fi
196exit 1
197"#;
198 std::fs::write(&script, body).expect("failed to write test pacman script");
199 #[cfg(unix)]
200 {
201 use std::os::unix::fs::PermissionsExt;
202 let mut perm = std::fs::metadata(&script)
203 .expect("failed to read test pacman script metadata")
204 .permissions();
205 perm.set_mode(0o755);
206 std::fs::set_permissions(&script, perm)
207 .expect("failed to set test pacman script permissions");
208 }
209 let new_path = format!("{}:{old_path}", bin.to_string_lossy());
210 unsafe {
211 std::env::set_var("PATH", &new_path);
212 }
213
214 super::refresh_explicit_cache(crate::state::InstalledPackagesMode::LeafOnly).await;
215
216 let _ = std::fs::remove_dir_all(&root);
217
218 let set = super::explicit_names();
219 assert_eq!(set.len(), 2);
220 assert!(set.contains("alpha"));
221 assert!(set.contains("beta"));
222 }
223
224 #[cfg(not(target_os = "windows"))]
225 #[allow(clippy::await_holding_lock)]
226 #[tokio::test]
227 async fn refresh_explicit_cache_populates_cache_with_all_explicit_mode() {
239 struct PathGuard {
240 original: String,
241 }
242 impl Drop for PathGuard {
243 fn drop(&mut self) {
244 unsafe {
245 std::env::set_var("PATH", &self.original);
246 }
247 }
248 }
249 let _guard = crate::global_test_mutex_lock();
250
251 if let Ok(mut g) = super::explicit_lock().write() {
252 g.clear();
253 }
254
255 let old_path = std::env::var("PATH").unwrap_or_default();
256 let _path_guard = PathGuard {
257 original: old_path.clone(),
258 };
259
260 let mut root = std::env::temp_dir();
261 root.push(format!(
262 "pacsea_fake_pacman_qeq_{}_{}",
263 std::process::id(),
264 std::time::SystemTime::now()
265 .duration_since(std::time::UNIX_EPOCH)
266 .expect("System time is before UNIX epoch")
267 .as_nanos()
268 ));
269 std::fs::create_dir_all(&root).expect("failed to create test root directory");
270 let mut bin = root.clone();
271 bin.push("bin");
272 std::fs::create_dir_all(&bin).expect("failed to create test bin directory");
273 let mut script = bin.clone();
274 script.push("pacman");
275 let body = r#"#!/usr/bin/env bash
276set -e
277if [[ "$1" == "-Qeq" ]]; then
278 echo "git"
279 echo "python"
280 echo "wget"
281 exit 0
282fi
283exit 1
284"#;
285 std::fs::write(&script, body).expect("failed to write test pacman script");
286 #[cfg(unix)]
287 {
288 use std::os::unix::fs::PermissionsExt;
289 let mut perm = std::fs::metadata(&script)
290 .expect("failed to read test pacman script metadata")
291 .permissions();
292 perm.set_mode(0o755);
293 std::fs::set_permissions(&script, perm)
294 .expect("failed to set test pacman script permissions");
295 }
296 let new_path = format!("{}:{old_path}", bin.to_string_lossy());
297 unsafe {
298 std::env::set_var("PATH", &new_path);
299 }
300
301 super::refresh_explicit_cache(crate::state::InstalledPackagesMode::AllExplicit).await;
302
303 let _ = std::fs::remove_dir_all(&root);
304
305 let set = super::explicit_names();
306 assert_eq!(set.len(), 3);
307 assert!(set.contains("git"));
308 assert!(set.contains("python"));
309 assert!(set.contains("wget"));
310 }
311}