Files
RedBear-OS/local/recipes/system/cub/source/cub-lib/src/pkgbuild.rs
T
vasilito cd07d32597 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)
2026-05-08 11:40:36 +01:00

938 lines
28 KiB
Rust

pub use crate::converter::{ConversionReport, ConversionResult};
use crate::deps::map_dependency;
use crate::error::CubError;
use crate::rbpkgbuild::{
BuildSection, BuildTemplate, CompatSection, ConversionStatus, DependenciesSection,
InstallSection, PackageSection, PatchesSection, PolicySection, RbPkgBuild, SourceEntry,
SourceSection, SourceType,
};
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct AurSrcInfo {
pub pkgbase: String,
pub pkgname: Vec<String>,
pub pkgver: String,
pub pkgrel: String,
pub pkgdesc: String,
pub url: String,
pub arch: Vec<String>,
pub license: Vec<String>,
pub depends: Vec<String>,
pub makedepends: Vec<String>,
pub checkdepends: Vec<String>,
pub optdepends: Vec<(String, Option<String>)>,
pub provides: Vec<String>,
pub conflicts: Vec<String>,
pub source: Vec<String>,
pub sha256sums: Vec<String>,
}
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 = 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 = 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 = 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();
let checkdepends = extract_array_assignment(content, "checkdepends").unwrap_or_default();
let optdepends_raw = extract_array_assignment(content, "optdepends").unwrap_or_default();
let provides = extract_array_assignment(content, "provides").unwrap_or_default();
let conflicts = extract_array_assignment(content, "conflicts").unwrap_or_default();
let sources = extract_array_assignment(content, "source").unwrap_or_default();
let sha256sums = extract_array_assignment(content, "sha256sums").unwrap_or_default();
let template = detect_build_template(content);
let mut warnings = detect_linuxisms(content);
let mut actions_required = Vec::new();
let build_body = if matches!(template, BuildTemplate::Custom) {
extract_bash_function(content, "build")
} else {
None
};
let package_body = if matches!(template, BuildTemplate::Custom) {
extract_bash_function(content, "package")
} else {
None
};
if template == BuildTemplate::Custom && build_body.is_none() && package_body.is_none() {
warnings.push("Custom build detected but could not extract build() or package() function body".to_string());
actions_required.push("review the PKGBUILD build() and package() functions manually".to_string());
}
let mapped_runtime = map_dep_list(&depends, &mut warnings, &mut actions_required);
let mapped_build = map_dep_list(&makedepends, &mut warnings, &mut actions_required);
let mapped_check = map_dep_list(&checkdepends, &mut warnings, &mut actions_required);
let optdepends = optdepends_raw
.iter()
.flat_map(|raw| parse_optdepends(raw))
.map(|(name, _)| name)
.collect::<Vec<_>>();
let mapped_optional = map_dep_list(&optdepends, &mut warnings, &mut actions_required);
if sources.is_empty() {
warnings.push("PKGBUILD does not define any source entries".to_string());
}
if pkgnames.len() > 1 || !split_packages.is_empty() {
warnings.push(
"split package PKGBUILD detected; converting the primary package only".to_string(),
);
actions_required
.push("review split package metadata and package_* install logic manually".to_string());
}
let status = if warnings.is_empty() && actions_required.is_empty() {
ConversionStatus::Full
} else {
ConversionStatus::Partial
};
let rbpkg = RbPkgBuild {
format: 1,
package: PackageSection {
name: sanitize_pkgname(&pkgname),
version: pkgver,
release: pkgrel,
description: pkgdesc,
homepage: url,
license: licenses,
architectures: vec!["x86_64-unknown-redox".to_string()],
maintainers: Vec::new(),
},
source: SourceSection {
sources: sources
.into_iter()
.enumerate()
.map(|(index, source)| {
source_from_arch(source, sha256sums.get(index).map(String::as_str))
})
.collect(),
},
dependencies: DependenciesSection {
build: mapped_build,
runtime: mapped_runtime,
check: mapped_check,
optional: mapped_optional,
provides,
conflicts,
},
build: BuildSection {
template,
build_script: build_body.into_iter().collect(),
install_script: package_body.into_iter().collect(),
..BuildSection::default()
},
install: InstallSection::default(),
patches: PatchesSection::default(),
compat: CompatSection {
imported_from: "aur".to_string(),
original_pkgbuild: content.to_string(),
conversion_status: status.clone(),
target: "x86_64-unknown-redox".to_string(),
split_packages: split_packages,
},
policy: PolicySection::default(),
};
rbpkg.validate()?;
let _ = rbpkg.to_srcinfo();
Ok(ConversionResult {
rbpkg,
report: ConversionReport {
status,
warnings,
actions_required,
},
})
}
pub fn parse_optdepends(raw: &str) -> Vec<(String, Option<String>)> {
let binding = strip_unquoted_comment(raw);
let trimmed = binding.trim();
if trimmed.is_empty() {
return Vec::new();
}
let items = if trimmed.starts_with('(') || trimmed.contains('"') || trimmed.contains('\'') {
parse_array(trimmed)
} else if trimmed.contains(':') && trimmed.chars().any(char::is_whitespace) {
vec![trimmed.to_string()]
} else {
shell_split(trimmed)
};
items
.into_iter()
.map(|value| parse_optdepend_entry(&value))
.filter(|(name, _)| !name.is_empty())
.collect()
}
pub fn parse_srcinfo(content: &str) -> Result<AurSrcInfo, CubError> {
let mut info = AurSrcInfo::default();
for raw_line in content.lines() {
let line = raw_line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let Some((key, value)) = line.split_once('=') else {
return Err(CubError::Conversion(format!(
"invalid .SRCINFO line: {raw_line}"
)));
};
let key = key.trim();
let value = parse_scalar(value.trim());
if value.is_empty() {
continue;
}
match key {
"pkgbase" => info.pkgbase = value,
"pkgname" => info.pkgname.push(value),
"pkgver" => info.pkgver = value,
"pkgrel" => info.pkgrel = value,
"pkgdesc" => {
if info.pkgdesc.is_empty() {
info.pkgdesc = value;
}
}
"url" => {
if info.url.is_empty() {
info.url = value;
}
}
"arch" => info.arch.push(value),
"license" => info.license.push(value),
"depends" => info.depends.push(value),
"makedepends" => info.makedepends.push(value),
"checkdepends" => info.checkdepends.push(value),
"optdepends" => {
let optdepend = parse_optdepend_entry(&value);
if !optdepend.0.is_empty() {
info.optdepends.push(optdepend);
}
}
"provides" => info.provides.push(value),
"conflicts" => info.conflicts.push(value),
"source" => info.source.push(value),
"sha256sums" => info.sha256sums.push(value),
_ => {}
}
}
if info.pkgname.is_empty() {
return Err(CubError::Conversion(
"missing pkgname in .SRCINFO".to_string(),
));
}
if info.pkgver.is_empty() {
return Err(CubError::Conversion(
"missing pkgver in .SRCINFO".to_string(),
));
}
if info.pkgrel.is_empty() {
return Err(CubError::Conversion(
"missing pkgrel in .SRCINFO".to_string(),
));
}
Ok(info)
}
pub fn extract_split_packages(content: &str) -> Vec<String> {
let mut packages = Vec::new();
for raw_line in content.lines() {
let binding = strip_unquoted_comment(raw_line);
let mut line = binding.trim_start();
if let Some(rest) = line.strip_prefix("function ") {
line = rest.trim_start();
}
let Some(rest) = line.strip_prefix("package_") else {
continue;
};
let Some((name, tail)) = rest.split_once('(') else {
continue;
};
if name.is_empty() || !tail.trim_start().starts_with(')') {
continue;
}
let name = name.trim().to_string();
if !packages.contains(&name) {
packages.push(name);
}
}
packages
}
fn map_dep_list(
deps: &[String],
warnings: &mut Vec<String>,
actions_required: &mut Vec<String>,
) -> Vec<String> {
let mut mapped = Vec::new();
for dep in deps {
let mapping = map_dependency(dep);
if mapping.mapped.is_empty() {
warnings.push(format!(
"dependency '{}' has no Redox mapping and was omitted",
mapping.original
));
actions_required.push(format!(
"port or replace dependency '{}' manually",
mapping.original
));
continue;
}
if !mapping.is_exact {
warnings.push(format!(
"dependency '{}' mapped to '{}'",
mapping.original, mapping.mapped
));
}
if !mapped.contains(&mapping.mapped) {
mapped.push(mapping.mapped);
}
}
mapped
}
pub fn detect_build_template(content: &str) -> BuildTemplate {
let lowered = content.to_ascii_lowercase();
if lowered.contains("cargo build") || lowered.contains("cargo install") {
BuildTemplate::Cargo
} else if lowered.contains("meson setup") || lowered.contains(" meson ") {
BuildTemplate::Meson
} else if lowered.contains("cmake") {
BuildTemplate::Cmake
} else if lowered.contains("./configure") || lowered.contains(" configure ") {
BuildTemplate::Configure
} else {
BuildTemplate::Custom
}
}
pub fn extract_bash_function(content: &str, name: &str) -> Option<String> {
for pattern in &[format!("{name}() {{"), format!("function {name} () {{"), format!("{name} () {{")] {
if let Some(pos) = content.find(pattern) {
let start = pos + pattern.len();
let rest = &content[start..];
let mut depth = 1u32;
let mut end = 0usize;
for (i, ch) in rest.char_indices() {
match ch {
'{' => depth += 1,
'}' => {
depth -= 1;
if depth == 0 {
end = i;
break;
}
}
_ => {}
}
}
if end > 0 {
let body = rest[..end].trim().to_string();
if !body.is_empty() {
return Some(body);
}
}
}
}
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 = [
("systemctl", "uses systemctl, which is not available on Redox"),
("/usr/lib/systemd", "references /usr/lib/systemd, which is Linux-specific"),
("systemd", "references systemd, which is unavailable on Redox"),
("/proc", "references /proc, which may require Redox-specific adaptation"),
("dbus-daemon", "uses dbus-daemon, verify D-Bus service compatibility"),
("dbus-launch", "uses dbus-launch, verify D-Bus session compatibility"),
("systemd-udevd", "references systemd-udevd, unavailable on Redox (use udev-shim)"),
("udevadm", "uses udevadm, unavailable on Redox"),
("/sys/class", "references /sys/class, may require Redox-specific adaptation"),
("/sys/devices", "references /sys/devices, may require Redox-specific adaptation"),
("/run/", "references /run/, may require Redox-specific adaptation"),
("systemd-tmpfiles", "uses systemd-tmpfiles, unavailable on Redox"),
("systemd-sysusers", "uses systemd-sysusers, unavailable on Redox"),
("libsystemd", "links against libsystemd, unavailable on Redox"),
("libudev", "links against libudev, may need udev-shim"),
("polkit", "references polkit, verify PolicyKit compatibility"),
("pam_systemd", "references pam_systemd, unavailable on Redox"),
("elogind", "references elogind, unavailable on Redox"),
("logind", "references logind, unavailable on Redox (use redbear-sessiond)"),
];
let mut warnings = Vec::new();
for (needle, warning) in checks {
if lowered.contains(needle) {
warnings.push(warning.to_string());
}
}
warnings
}
pub fn sanitize_pkgname(name: &str) -> String {
name.trim_matches('"')
.to_ascii_lowercase()
.replace('_', "-")
}
pub fn source_from_arch(entry: String, sha256: Option<&str>) -> SourceEntry {
let is_git_source = is_git_source_entry(&entry);
let normalized = normalize_source_entry(&entry);
let source_type =
if is_git_source || normalized.starts_with("git://") || normalized.ends_with(".git") {
SourceType::Git
} else {
SourceType::Tar
};
SourceEntry {
sha256: if matches!(source_type, SourceType::Tar) {
sha256.unwrap_or_default().to_string()
} else {
String::new()
},
url: normalized,
source_type,
rev: String::new(),
branch: String::new(),
}
}
fn is_git_source_entry(entry: &str) -> bool {
let stripped = entry
.split_once("::")
.map(|(_, value)| value)
.unwrap_or(entry)
.trim();
stripped.starts_with("git+")
}
fn normalize_source_entry(entry: &str) -> String {
let stripped = entry
.split_once("::")
.map(|(_, value)| value)
.unwrap_or(entry)
.trim();
stripped
.strip_prefix("git+")
.unwrap_or(stripped)
.to_string()
}
pub fn extract_scalar_assignment(content: &str, name: &str) -> Option<String> {
extract_assignment(content, name).map(|raw| parse_scalar(&raw))
}
pub fn extract_array_assignment(content: &str, name: &str) -> Option<Vec<String>> {
extract_assignment(content, name).map(|raw| parse_array(&raw))
}
fn extract_assignment(content: &str, name: &str) -> Option<String> {
let prefix = format!("{name}=");
let mut lines = content.lines();
while let Some(line) = lines.next() {
let trimmed = line.trim_start();
if !trimmed.starts_with(&prefix) {
continue;
}
let mut value = trimmed[prefix.len()..].trim().to_string();
if value.starts_with('(') {
let mut depth = paren_balance(&value);
while depth > 0 {
let Some(next) = lines.next() else {
break;
};
value.push('\n');
value.push_str(next.trim());
depth += paren_balance(next);
}
} else {
while value.ends_with('\\') {
value.pop();
let Some(next) = lines.next() else {
break;
};
value.push(' ');
value.push_str(next.trim());
}
}
return Some(value);
}
None
}
fn paren_balance(input: &str) -> i32 {
let opens = input.chars().filter(|ch| *ch == '(').count() as i32;
let closes = input.chars().filter(|ch| *ch == ')').count() as i32;
opens - closes
}
fn parse_scalar(raw: &str) -> String {
let binding = strip_unquoted_comment(raw);
let stripped = binding.trim();
if let Some(unquoted) = unquote(stripped) {
unquoted
} else {
stripped.to_string()
}
}
fn parse_array(raw: &str) -> Vec<String> {
let binding = strip_unquoted_comment(raw);
let trimmed = binding.trim();
let inner = trimmed
.strip_prefix('(')
.and_then(|value| value.strip_suffix(')'))
.unwrap_or(trimmed);
shell_split(inner)
}
fn strip_unquoted_comment(input: &str) -> String {
let mut single = false;
let mut double = false;
let mut result = String::new();
for ch in input.chars() {
match ch {
'\'' if !double => {
single = !single;
result.push(ch);
}
'"' if !single => {
double = !double;
result.push(ch);
}
'#' if !single && !double => break,
_ => result.push(ch),
}
}
result
}
fn parse_optdepend_entry(raw: &str) -> (String, Option<String>) {
let value = parse_scalar(raw);
let Some((name, description)) = value.split_once(':') else {
return (value.trim().to_string(), None);
};
let name = name.trim().to_string();
let description = description.trim();
let description = if description.is_empty() {
None
} else {
Some(description.to_string())
};
(name, description)
}
fn unquote(value: &str) -> Option<String> {
if value.len() >= 2 {
let bytes = value.as_bytes();
let first = bytes[0] as char;
let last = bytes[value.len() - 1] as char;
if (first == '\'' && last == '\'') || (first == '"' && last == '"') {
return Some(value[1..value.len() - 1].to_string());
}
}
None
}
pub fn shell_split(input: &str) -> Vec<String> {
let mut items = Vec::new();
let mut current = String::new();
let mut quote: Option<char> = None;
let mut escape = false;
for ch in input.chars() {
if escape {
current.push(ch);
escape = false;
continue;
}
match ch {
'\\' => escape = true,
'\'' | '"' => {
if quote == Some(ch) {
quote = None;
} else if quote.is_none() {
quote = Some(ch);
} else {
current.push(ch);
}
}
'#' if quote.is_none() => break,
ch if ch.is_whitespace() && quote.is_none() => {
if !current.is_empty() {
items.push(current.clone());
current.clear();
}
}
_ => current.push(ch),
}
}
if !current.is_empty() {
items.push(current);
}
items
}
#[cfg(test)]
mod tests {
use super::*;
const PKGBUILD: &str = r#"
pkgname=demo_pkg
pkgver=1.2.3
pkgrel=4
pkgdesc="Demo application"
url="https://example.com/demo"
license=('MIT')
depends=('glibc' 'openssl>=1.1' 'systemd')
makedepends=('cargo' 'pkg-config')
checkdepends=('python')
source=('https://example.com/demo-1.2.3.tar.xz')
sha256sums=('abc123deadbeef')
build() {
cargo build --release
}
package() {
install -Dm755 target/release/demo "$pkgdir/usr/bin/demo"
systemctl --version >/dev/null
}
"#;
#[test]
fn converts_pkgbuild_to_rbpkgbuild() {
let result = convert_pkgbuild(PKGBUILD).expect("convert PKGBUILD");
assert_eq!(result.rbpkg.package.name, "demo-pkg");
assert_eq!(result.rbpkg.package.version, "1.2.3");
assert_eq!(result.rbpkg.package.release, 4);
assert_eq!(result.rbpkg.build.template, BuildTemplate::Cargo);
assert_eq!(
result.rbpkg.dependencies.runtime,
vec!["relibc", "openssl3"]
);
assert_eq!(result.rbpkg.dependencies.build, vec!["cargo", "pkg-config"]);
assert_eq!(result.rbpkg.dependencies.check, vec!["python"]);
assert_eq!(result.rbpkg.source.sources.len(), 1);
assert_eq!(result.rbpkg.source.sources[0].sha256, "abc123deadbeef");
}
#[test]
fn reports_linuxisms_and_unmapped_deps() {
let result = convert_pkgbuild(PKGBUILD).expect("convert PKGBUILD");
assert!(matches!(result.report.status, ConversionStatus::Partial));
assert!(result
.report
.warnings
.iter()
.any(|w| w.contains("systemctl")));
assert!(result
.report
.actions_required
.iter()
.any(|w| w.contains("systemd")));
}
#[test]
fn parses_multiline_arrays() {
let input = "depends=(\n 'glibc'\n 'zlib'\n)\n";
let parsed = extract_array_assignment(input, "depends").expect("depends array");
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";
assert_eq!(detect_build_template(input), BuildTemplate::Meson);
}
#[test]
fn parses_optdepends_with_descriptions() {
let parsed = parse_optdepends("'libnotify: desktop notifications' 'bash-completion'");
assert_eq!(
parsed,
vec![
(
"libnotify".to_string(),
Some("desktop notifications".to_string())
),
("bash-completion".to_string(), None),
]
);
}
#[test]
fn parses_srcinfo_metadata() {
let srcinfo = parse_srcinfo(
r#"
pkgbase = demo
pkgver = 1.2.3
pkgrel = 4
pkgdesc = Demo application
url = https://example.com/demo
arch = x86_64
license = MIT
depends = openssl>=1.1
makedepends = cargo
checkdepends = python
optdepends = libnotify: desktop notifications
provides = demo-virtual
conflicts = demo-old
source = https://example.com/demo-1.2.3.tar.xz
sha256sums = abc123deadbeef
pkgname = demo
pkgname = demo-docs
"#,
)
.expect("parse .SRCINFO");
assert_eq!(srcinfo.pkgbase, "demo");
assert_eq!(srcinfo.pkgname, vec!["demo", "demo-docs"]);
assert_eq!(srcinfo.pkgver, "1.2.3");
assert_eq!(srcinfo.pkgrel, "4");
assert_eq!(srcinfo.depends, vec!["openssl>=1.1"]);
assert_eq!(srcinfo.makedepends, vec!["cargo"]);
assert_eq!(srcinfo.checkdepends, vec!["python"]);
assert_eq!(
srcinfo.optdepends,
vec![(
"libnotify".to_string(),
Some("desktop notifications".to_string())
)]
);
assert_eq!(srcinfo.provides, vec!["demo-virtual"]);
assert_eq!(srcinfo.conflicts, vec!["demo-old"]);
}
#[test]
fn extracts_split_package_functions() {
let pkbuild = r#"
pkgname=('demo' 'demo-docs')
package_demo() {
:
}
function package_demo_docs() {
:
}
"#;
assert_eq!(
extract_split_packages(pkbuild),
vec!["demo".to_string(), "demo_docs".to_string()]
);
}
#[test]
fn warns_when_converting_split_package_pkgbuild() {
let input = r#"
pkgname=('demo' 'demo-docs')
pkgver=1.0.0
pkgrel=1
source=('https://example.com/demo.tar.xz')
sha256sums=('abc')
build() {
cargo build --release
}
package_demo() {
:
}
function package_demo_docs() {
:
}
"#;
let result = convert_pkgbuild(input).expect("convert split-package PKGBUILD");
assert!(result
.report
.warnings
.iter()
.any(|warning| warning.contains("split package PKGBUILD detected")));
assert!(result
.report
.actions_required
.iter()
.any(|action| action.contains("package_* install logic manually")));
}
#[test]
fn treats_git_plus_sources_as_git() {
let source = source_from_arch("git+https://example.com/repo".to_string(), None);
assert_eq!(source.source_type, SourceType::Git);
assert_eq!(source.url, "https://example.com/repo");
assert!(source.sha256.is_empty());
}
#[test]
fn converts_optdepends_into_optional_dependencies() {
let input = r#"
pkgname=demo
pkgver=1.0.0
pkgrel=1
source=('https://example.com/demo.tar.xz')
sha256sums=('abc')
optdepends=('glibc: C runtime' 'bash-completion')
build() {
cargo build --release
}
"#;
let result = convert_pkgbuild(input).expect("convert PKGBUILD with optdepends");
assert_eq!(
result.rbpkg.dependencies.optional,
vec!["relibc", "bash-completion"]
);
}
}