use crate::logic::files::get_pkgbuild_from_cache;
use crate::state::{PackageItem, Source};
use crate::util::percent_encode;
use std::sync::Mutex;
use std::time::{Duration, Instant};
type Result<T> = super::Result<T>;
static PKGBUILD_RATE_LIMITER: Mutex<Option<Instant>> = Mutex::new(None);
const PKGBUILD_MIN_INTERVAL_MS: u64 = 200;
const PKGBUILD_CURL_EXTRA: &[&str] = &["--connect-timeout", "8", "--max-time", "10"];
pub async fn fetch_pkgbuild_fast(item: &PackageItem) -> Result<String> {
let name = item.name.clone();
if let Some(cached) = tokio::task::spawn_blocking({
let name = name.clone();
move || get_pkgbuild_from_cache(&name)
})
.await?
{
tracing::debug!("Using cached PKGBUILD for {} (offline)", name);
return Ok(cached);
}
let delay = {
let mut last_request = PKGBUILD_RATE_LIMITER
.lock()
.expect("PKGBUILD rate limiter mutex poisoned");
if let Some(last) = *last_request {
let elapsed = last.elapsed();
if elapsed < Duration::from_millis(PKGBUILD_MIN_INTERVAL_MS) {
let delay = Duration::from_millis(PKGBUILD_MIN_INTERVAL_MS)
.checked_sub(elapsed)
.expect("elapsed should be less than PKGBUILD_MIN_INTERVAL_MS");
tracing::debug!(
"Rate limiting PKGBUILD request for {}: waiting {:?}",
name,
delay
);
*last_request = Some(Instant::now());
Some(delay)
} else {
*last_request = Some(Instant::now());
None
}
} else {
*last_request = Some(Instant::now());
None
}
};
if let Some(delay) = delay {
tokio::time::sleep(delay).await;
}
match &item.source {
Source::Aur => {
let url = format!(
"https://aur.archlinux.org/cgit/aur.git/plain/PKGBUILD?h={}",
percent_encode(&name)
);
let res = tokio::task::spawn_blocking({
let url = url.clone();
move || crate::util::curl::curl_text_with_args(&url, PKGBUILD_CURL_EXTRA)
})
.await??;
Ok(res)
}
Source::Official { .. } => {
let url_main = format!(
"https://gitlab.archlinux.org/archlinux/packaging/packages/{}/-/raw/main/PKGBUILD",
percent_encode(&name)
);
let main_result = tokio::task::spawn_blocking({
let u = url_main.clone();
move || crate::util::curl::curl_text_with_args(&u, PKGBUILD_CURL_EXTRA)
})
.await;
if let Ok(Ok(txt)) = main_result {
return Ok(txt);
}
let url_master = format!(
"https://gitlab.archlinux.org/archlinux/packaging/packages/{}/-/raw/master/PKGBUILD",
percent_encode(&name)
);
let txt = tokio::task::spawn_blocking({
let u = url_master;
move || crate::util::curl::curl_text_with_args(&u, PKGBUILD_CURL_EXTRA)
})
.await??;
Ok(txt)
}
}
}
#[cfg(not(target_os = "windows"))]
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
#[ignore = "Only run when explicitly mentioned"]
#[allow(clippy::await_holding_lock)]
async fn pkgbuild_fetches_aur_via_curl_text() {
let _guard = crate::sources::test_mutex()
.lock()
.expect("Test mutex poisoned");
let old_path = std::env::var("PATH").unwrap_or_default();
let mut root = std::env::temp_dir();
root.push(format!(
"pacsea_fake_curl_pkgbuild_{}_{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("System time is before UNIX epoch")
.as_nanos()
));
std::fs::create_dir_all(&root).expect("Failed to create test root directory");
let mut bin = root.clone();
bin.push("bin");
std::fs::create_dir_all(&bin).expect("Failed to create test bin directory");
let mut curl = bin.clone();
curl.push("curl");
let script = "#!/bin/sh\necho 'pkgver=1'\n";
std::fs::write(&curl, script.as_bytes()).expect("Failed to write test curl script");
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perm = std::fs::metadata(&curl)
.expect("Failed to read test curl script metadata")
.permissions();
perm.set_mode(0o755);
std::fs::set_permissions(&curl, perm)
.expect("Failed to set test curl script permissions");
}
let new_path = format!("{}:{old_path}", bin.to_string_lossy());
unsafe { std::env::set_var("PATH", &new_path) };
let item = PackageItem {
name: "yay-bin".into(),
version: String::new(),
description: String::new(),
source: Source::Aur,
popularity: None,
out_of_date: None,
orphaned: false,
};
let txt = super::fetch_pkgbuild_fast(&item)
.await
.expect("Failed to fetch PKGBUILD in test");
assert!(txt.contains("pkgver=1"));
unsafe { std::env::set_var("PATH", &old_path) };
let _ = std::fs::remove_dir_all(&root);
}
#[test]
#[allow(clippy::await_holding_lock)]
fn pkgbuild_fetches_official_main_then_master() {
let _guard = crate::global_test_mutex_lock();
let old_path = std::env::var("PATH").unwrap_or_default();
let mut root = std::env::temp_dir();
root.push(format!(
"pacsea_fake_curl_pkgbuild_official_{}_{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("System time is before UNIX epoch")
.as_nanos()
));
std::fs::create_dir_all(&root).expect("Failed to create test root directory");
let mut bin = root.clone();
bin.push("bin");
std::fs::create_dir_all(&bin).expect("Failed to create test bin directory");
let mut curl = bin.clone();
curl.push("curl");
let script = "#!/bin/sh\nfor arg; do :; done\nurl=\"$arg\"\nif echo \"$url\" | grep -q '/-/raw/main/'; then exit 22; fi\nprintf 'pkgrel=2'\n";
std::fs::write(&curl, script.as_bytes()).expect("Failed to write test curl script");
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perm = std::fs::metadata(&curl)
.expect("Failed to read test curl script metadata")
.permissions();
perm.set_mode(0o755);
std::fs::set_permissions(&curl, perm)
.expect("Failed to set test curl script permissions");
}
let mut paru = bin.clone();
paru.push("paru");
std::fs::write(&paru, b"#!/bin/sh\nexit 1\n").expect("Failed to write test paru script");
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perm = std::fs::metadata(&paru)
.expect("Failed to read test paru script metadata")
.permissions();
perm.set_mode(0o755);
std::fs::set_permissions(&paru, perm)
.expect("Failed to set test paru script permissions");
}
let mut yay = bin.clone();
yay.push("yay");
std::fs::write(&yay, b"#!/bin/sh\nexit 1\n").expect("Failed to write test yay script");
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perm = std::fs::metadata(&yay)
.expect("Failed to read test yay script metadata")
.permissions();
perm.set_mode(0o755);
std::fs::set_permissions(&yay, perm)
.expect("Failed to set test yay script permissions");
}
let new_path = format!("{}:{old_path}", bin.to_string_lossy());
unsafe { std::env::set_var("PATH", &new_path) };
unsafe { std::env::set_var("PACSEA_CURL_PATH", "1") };
let old_home = std::env::var("HOME").unwrap_or_default();
unsafe { std::env::set_var("HOME", root.to_string_lossy().as_ref()) };
let rt = tokio::runtime::Runtime::new().expect("Failed to create tokio runtime for test");
let txt = rt.block_on(async {
let item = PackageItem {
name: "ripgrep".into(),
version: String::new(),
description: String::new(),
source: Source::Official {
repo: "extra".into(),
arch: "x86_64".into(),
},
popularity: None,
out_of_date: None,
orphaned: false,
};
super::fetch_pkgbuild_fast(&item)
.await
.expect("Failed to fetch PKGBUILD in test")
});
assert!(txt.contains("pkgrel=2"));
unsafe { std::env::set_var("PATH", &old_path) };
unsafe { std::env::set_var("HOME", &old_home) };
unsafe { std::env::remove_var("PACSEA_CURL_PATH") };
let _ = std::fs::remove_dir_all(&root);
}
}