pacsea/logic/files/
pkgbuild_fetch.rs1use crate::util::{curl_args, percent_encode};
4use std::process::Command;
5use std::sync::Mutex;
6use std::time::{Duration, Instant};
7
8static PKGBUILD_RATE_LIMITER: Mutex<Option<Instant>> = Mutex::new(None);
12const PKGBUILD_MIN_INTERVAL_MS: u64 = 500;
16
17fn find_pkgbuild_in_dir(
30 base_dir: &std::path::Path,
31 name: &str,
32 helper_name: &str,
33) -> Option<String> {
34 let pkgbuild_path = base_dir.join("PKGBUILD");
36 if let Ok(text) = std::fs::read_to_string(&pkgbuild_path)
37 && text.contains("pkgname")
38 {
39 tracing::debug!("Found PKGBUILD for {} via {} -G", name, helper_name);
40 return Some(text);
41 }
42
43 let Ok(entries) = std::fs::read_dir(base_dir) else {
45 return None;
46 };
47
48 for entry in entries.flatten() {
49 if !entry.path().is_dir() {
50 continue;
51 }
52
53 let pkgbuild_path = entry.path().join("PKGBUILD");
54 if let Ok(text) = std::fs::read_to_string(&pkgbuild_path)
55 && text.contains("pkgname")
56 {
57 tracing::debug!(
58 "Found PKGBUILD for {} via {} -G (in subdir)",
59 name,
60 helper_name
61 );
62 return Some(text);
63 }
64 }
65
66 None
67}
68
69fn try_helper_command(helper: &str, name: &str) -> Option<String> {
81 let temp_dir = std::env::temp_dir().join(format!("pacsea_pkgbuild_{name}"));
82 let _ = std::fs::create_dir_all(&temp_dir);
83
84 let output = Command::new(helper)
85 .args(["-G", name])
86 .current_dir(&temp_dir)
87 .output()
88 .ok()?;
89
90 if !output.status.success() {
91 let _ = std::fs::remove_dir_all(&temp_dir);
92 return None;
93 }
94
95 let result = find_pkgbuild_in_dir(&temp_dir.join(name), name, helper);
96 let _ = std::fs::remove_dir_all(&temp_dir);
97 result
98}
99
100fn try_direct_cache_paths(name: &str, home: &str) -> Option<String> {
112 let cache_paths = [
113 format!("{home}/.cache/paru/clone/{name}/PKGBUILD"),
114 format!("{home}/.cache/yay/{name}/PKGBUILD"),
115 ];
116
117 for path_str in cache_paths {
118 if let Ok(text) = std::fs::read_to_string(&path_str)
119 && text.contains("pkgname")
120 {
121 tracing::debug!("Found PKGBUILD for {} in cache: {}", name, path_str);
122 return Some(text);
123 }
124 }
125
126 None
127}
128
129fn try_cache_subdirectories(name: &str, home: &str) -> Option<String> {
141 let cache_bases = [
142 format!("{home}/.cache/paru/clone"),
143 format!("{home}/.cache/yay"),
144 ];
145
146 for cache_base in cache_bases {
147 let Ok(entries) = std::fs::read_dir(&cache_base) else {
148 continue;
149 };
150
151 for entry in entries.flatten() {
152 let path = entry.path();
153 if !path.is_dir() {
154 continue;
155 }
156
157 let matches_name = path
158 .file_name()
159 .and_then(|n| n.to_str())
160 .is_some_and(|n| n.contains(name));
161
162 if !matches_name {
163 continue;
164 }
165
166 let pkgbuild_path = path.join("PKGBUILD");
168 if let Ok(text) = std::fs::read_to_string(&pkgbuild_path)
169 && text.contains("pkgname")
170 {
171 tracing::debug!(
172 "Found PKGBUILD for {} in cache subdirectory: {:?}",
173 name,
174 pkgbuild_path
175 );
176 return Some(text);
177 }
178
179 let Ok(sub_entries) = std::fs::read_dir(&path) else {
181 continue;
182 };
183
184 for sub_entry in sub_entries.flatten() {
185 if !sub_entry.path().is_dir() {
186 continue;
187 }
188
189 let pkgbuild_path = sub_entry.path().join("PKGBUILD");
190 if let Ok(text) = std::fs::read_to_string(&pkgbuild_path)
191 && text.contains("pkgname")
192 {
193 tracing::debug!(
194 "Found PKGBUILD for {} in cache subdirectory: {:?}",
195 name,
196 pkgbuild_path
197 );
198 return Some(text);
199 }
200 }
201 }
202 }
203
204 None
205}
206
207#[must_use]
219pub fn get_pkgbuild_from_cache(name: &str) -> Option<String> {
220 if let Some(text) = try_helper_command("paru", name) {
222 return Some(text);
223 }
224 if let Some(text) = try_helper_command("yay", name) {
225 return Some(text);
226 }
227
228 let home = std::env::var("HOME").ok()?;
230 if let Some(text) = try_direct_cache_paths(name, &home) {
231 return Some(text);
232 }
233
234 try_cache_subdirectories(name, &home)
236}
237
238pub fn fetch_pkgbuild_sync(name: &str) -> Result<String, String> {
260 if let Some(cached) = get_pkgbuild_from_cache(name) {
262 tracing::debug!("Using cached PKGBUILD for {} (offline)", name);
263 return Ok(cached);
264 }
265
266 {
268 let mut last_request = PKGBUILD_RATE_LIMITER
269 .lock()
270 .expect("PKGBUILD rate limiter mutex poisoned");
271 if let Some(last) = *last_request {
272 let elapsed = last.elapsed();
273 if elapsed < Duration::from_millis(PKGBUILD_MIN_INTERVAL_MS) {
274 let delay = Duration::from_millis(PKGBUILD_MIN_INTERVAL_MS)
275 .checked_sub(elapsed)
276 .expect("elapsed should be less than PKGBUILD_MIN_INTERVAL_MS");
277 tracing::debug!(
278 "Rate limiting PKGBUILD request for {}: waiting {:?}",
279 name,
280 delay
281 );
282 std::thread::sleep(delay);
283 }
284 }
285 *last_request = Some(Instant::now());
286 }
287
288 let url_aur = format!(
290 "https://aur.archlinux.org/cgit/aur.git/plain/PKGBUILD?h={}",
291 percent_encode(name)
292 );
293 tracing::debug!("Fetching PKGBUILD from AUR: {}", url_aur);
294
295 let args = curl_args(&url_aur, &[]);
296 let output = Command::new("curl").args(&args).output();
297
298 let aur_failed_http_error = match &output {
299 Ok(output) if output.status.success() => {
300 let text = String::from_utf8_lossy(&output.stdout).to_string();
301 if !text.trim().is_empty() && text.contains("pkgname") {
302 return Ok(text);
303 }
304 false
305 }
306 Ok(output) => {
307 output.status.code().is_some_and(|code| code == 22)
312 }
313 _ => false,
314 };
315
316 if aur_failed_http_error {
317 tracing::debug!(
318 "AUR returned HTTP error (likely 502) for {} - skipping GitLab fallback (likely AUR package or temporary AUR issue)",
319 name
320 );
321 return Err("AUR returned HTTP error (likely 502 Bad Gateway)".to_string());
322 }
323
324 let url_main = format!(
326 "https://gitlab.archlinux.org/archlinux/packaging/packages/{}/-/raw/main/PKGBUILD",
327 percent_encode(name)
328 );
329 tracing::debug!("Fetching PKGBUILD from GitLab main: {}", url_main);
330
331 let args = curl_args(&url_main, &[]);
332 let output = Command::new("curl").args(&args).output();
333
334 match output {
335 Ok(output) if output.status.success() => {
336 let text = String::from_utf8_lossy(&output.stdout).to_string();
337 if !text.trim().is_empty()
339 && (text.contains("pkgname") || text.contains("pkgver") || text.contains("pkgdesc"))
340 && !text.trim_start().starts_with("<!DOCTYPE")
341 && !text.trim_start().starts_with("<html")
342 {
343 return Ok(text);
344 }
345 tracing::warn!(
346 "GitLab main returned invalid PKGBUILD (likely HTML): first 200 chars: {:?}",
347 text.chars().take(200).collect::<String>()
348 );
349 }
350 _ => {}
351 }
352
353 let url_master = format!(
355 "https://gitlab.archlinux.org/archlinux/packaging/packages/{}/-/raw/master/PKGBUILD",
356 percent_encode(name)
357 );
358 tracing::debug!("Fetching PKGBUILD from GitLab master: {}", url_master);
359
360 let args = curl_args(&url_master, &[]);
361 let output = Command::new("curl")
362 .args(&args)
363 .output()
364 .map_err(|e| format!("curl failed: {e}"))?;
365
366 if !output.status.success() {
367 return Err(format!(
368 "curl failed with status: {:?}",
369 output.status.code()
370 ));
371 }
372
373 let text = String::from_utf8_lossy(&output.stdout).to_string();
374 if text.trim().is_empty() {
375 return Err("Empty PKGBUILD content".to_string());
376 }
377
378 if text.trim_start().starts_with("<!DOCTYPE") || text.trim_start().starts_with("<html") {
380 tracing::warn!(
381 "GitLab master returned HTML instead of PKGBUILD: first 200 chars: {:?}",
382 text.chars().take(200).collect::<String>()
383 );
384 return Err("GitLab returned HTML page instead of PKGBUILD".to_string());
385 }
386
387 if !text.contains("pkgname") && !text.contains("pkgver") && !text.contains("pkgdesc") {
388 tracing::warn!(
389 "GitLab master returned content that doesn't look like PKGBUILD: first 200 chars: {:?}",
390 text.chars().take(200).collect::<String>()
391 );
392 return Err("Response doesn't appear to be a valid PKGBUILD".to_string());
393 }
394
395 Ok(text)
396}
397
398pub fn fetch_srcinfo_sync(name: &str) -> Result<String, String> {
413 crate::util::srcinfo::fetch_srcinfo(name, None)
414}