fix: PKGBUILD shell variable resolver, TUI auto-fetch before build, AurClient Result fix

- resolve_shell_vars(): handles ${var}, $var, ${var%suffix} patterns
- nushell-git ($_pkgname-git) now resolves correctly to nushell-git
- TUI start_build_selected() auto-fetches AUR PKGBUILD if not cached
- Fix AurClient::new() Result handling in app.rs
- 70 tests pass (1 new: resolves_shell_variables)
This commit is contained in:
2026-05-08 11:40:36 +01:00
parent 2d9ccec10c
commit cd07d32597
2 changed files with 129 additions and 18 deletions
@@ -30,20 +30,34 @@ pub struct AurSrcInfo {
pub fn convert_pkgbuild(content: &str) -> Result<ConversionResult, CubError> {
let pkgnames = extract_array_assignment(content, "pkgname").unwrap_or_default();
let split_packages = extract_split_packages(content);
let pkgname = pkgnames
.first()
.cloned()
.or_else(|| split_packages.first().cloned())
.or_else(|| extract_scalar_assignment(content, "pkgbase"))
.ok_or_else(|| CubError::Conversion("missing pkgname in PKGBUILD".to_string()))?;
let pkgver = extract_scalar_assignment(content, "pkgver")
.ok_or_else(|| CubError::Conversion("missing pkgver in PKGBUILD".to_string()))?;
let pkgname = resolve_shell_vars(
&pkgnames
.first()
.cloned()
.or_else(|| split_packages.first().cloned())
.or_else(|| extract_scalar_assignment(content, "pkgbase"))
.ok_or_else(|| CubError::Conversion("missing pkgname in PKGBUILD".to_string()))?,
content,
);
let pkgver = resolve_shell_vars(
&extract_scalar_assignment(content, "pkgver")
.ok_or_else(|| CubError::Conversion("missing pkgver in PKGBUILD".to_string()))?,
content,
);
let pkgrel_raw =
extract_scalar_assignment(content, "pkgrel").unwrap_or_else(|| "1".to_string());
let pkgrel_raw = resolve_shell_vars(
&extract_scalar_assignment(content, "pkgrel").unwrap_or_else(|| "1".to_string()),
content,
);
let pkgrel = pkgrel_raw.parse::<u32>().unwrap_or(1);
let pkgdesc = extract_scalar_assignment(content, "pkgdesc").unwrap_or_default();
let url = extract_scalar_assignment(content, "url").unwrap_or_default();
let pkgdesc = resolve_shell_vars(
&extract_scalar_assignment(content, "pkgdesc").unwrap_or_default(),
content,
);
let url = resolve_shell_vars(
&extract_scalar_assignment(content, "url").unwrap_or_default(),
content,
);
let licenses = extract_array_assignment(content, "license").unwrap_or_default();
let depends = extract_array_assignment(content, "depends").unwrap_or_default();
let makedepends = extract_array_assignment(content, "makedepends").unwrap_or_default();
@@ -375,6 +389,72 @@ pub fn extract_bash_function(content: &str, name: &str) -> Option<String> {
None
}
pub fn resolve_shell_vars(value: &str, content: &str) -> String {
let mut result = value.to_string();
for _ in 0..10 {
let mut changed = false;
let mut resolved = String::with_capacity(result.len());
let chars: Vec<char> = result.chars().collect();
let mut i = 0;
while i < chars.len() {
if chars[i] == '$' && i + 1 < chars.len() {
if chars[i + 1] == '{' {
if let Some(end) = chars[i + 2..].iter().position(|c| *c == '}') {
let inner: String = chars[i + 2..i + 2 + end].iter().collect();
if let Some(val) = lookup_var(&inner, content) {
resolved.push_str(&val);
changed = true;
} else {
resolved.push_str(&result[i..i + 3 + end]);
}
i += 3 + end;
continue;
}
} else {
let mut j = i + 1;
while j < chars.len() && (chars[j].is_alphanumeric() || chars[j] == '_') {
j += 1;
}
let varname: String = chars[i + 1..j].iter().collect();
if let Some(val) = lookup_var(&varname, content) {
resolved.push_str(&val);
changed = true;
} else {
resolved.push_str(&result[i..j]);
}
i = j;
continue;
}
}
resolved.push(chars[i]);
i += 1;
}
result = resolved;
if !changed {
break;
}
}
result
}
fn lookup_var(name: &str, content: &str) -> Option<String> {
if let Some(suffix) = name.strip_suffix("%-git") {
if let Some(val) = extract_scalar_assignment(content, suffix) {
return Some(val);
}
}
if let Some((var, suffix)) = name.split_once('%') {
if let Some(val) = extract_scalar_assignment(content, var) {
return Some(val.trim_end_matches(suffix).to_string());
}
}
extract_scalar_assignment(content, name)
}
pub fn detect_linuxisms(content: &str) -> Vec<String> {
let lowered = content.to_ascii_lowercase();
let checks = [
@@ -697,6 +777,13 @@ package() {
assert_eq!(parsed, vec!["glibc", "zlib"]);
}
#[test]
fn resolves_shell_variables() {
let content = "_pkgname=nushell\npkgname=$_pkgname-git\npkgver=1.0\npkgrel=1\narch=('x86_64')\nbuild() { cargo build --release }\npackage() { install -Dm755 target/release/nushell \"$pkgdir/usr/bin/nushell\" }\n";
let result = convert_pkgbuild(content).expect("convert");
assert_eq!(result.rbpkg.package.name, "nushell-git");
}
#[test]
fn detects_meson_template() {
let input = "pkgname=demo\npkgver=1\nmeson setup build\n";
@@ -92,9 +92,11 @@ impl CubApp {
});
let _ = store.init();
let aur_client = Some(AurClient::new()).filter(|_| {
env::var("AUR_OFFLINE").is_err()
});
let aur_client = if env::var("AUR_OFFLINE").is_err() {
Some(AurClient::new())
} else {
None
};
let mut app = Self {
search_query: String::new(),
@@ -341,9 +343,31 @@ impl CubApp {
let build_target = if self.current_view == View::Query {
self.selected_query_recipe_dir()
} else {
self.selected_package()
.map(|package| self.store.recipes_dir().join(&package.name))
.filter(|path| path.is_dir())
self.selected_package().map(|package| {
let dir = self.store.recipes_dir().join(&package.name);
if !dir.is_dir() {
let _ = self.store.init();
let _ = std::fs::create_dir_all(&dir);
if env::var("AUR_OFFLINE").is_err() {
let repo_url = format!("https://aur.archlinux.org/{}.git", package.name);
let tmp = std::env::temp_dir().join(format!("cub-tui-aur-{}", package.name));
let _ = std::fs::create_dir_all(&tmp);
if std::process::Command::new("git")
.arg("clone").arg("--depth").arg("1").arg(&repo_url).arg(&tmp)
.status().ok().map_or(false, |s| s.success())
{
if let Ok(pkgbuild) = std::fs::read_to_string(tmp.join("PKGBUILD")) {
if let Ok(conv) = cub::pkgbuild::convert_pkgbuild(&pkgbuild) {
let _ = std::fs::write(dir.join("RBPKGBUILD"), conv.rbpkg.to_toml().unwrap_or_default());
let _ = cub::recipe::save_recipe_to_store(&conv.rbpkg, &self.store);
}
}
}
let _ = std::fs::remove_dir_all(&tmp);
}
}
dir
}).filter(|path| path.is_dir())
};
let Some(recipe_dir) = build_target else {