cub: fix 7 critical PKGBUILD→recipe conversion bugs (v6.0 2026)
After running an empirical assessment of cub's AUR→RBPKGBUILD→recipe
pipeline against 12 representative real-world PKGBUILDs (libevdev,
fd-find, libpciaccess, fmt, wlroots-git, ffmpeg, mesa 24.3, gzip,
zlib, openssl, glib2, plus a libpciaccess extra/-style variant), 7
critical bugs were found that would prevent any real Arch package
from converting to a working Red Bear recipe.
Fixes (all surgical, in cub-lib/src/):
1. deps.rs: drop glibc dependency (was: mapped to relibc, which is
wrong because relibc is the Redox C library and is part of the
OS, not a package). glibc is a tautology on Redox and must be
omitted. The empty mapping triggers the standard 'omitted' path
in map_dep_list with a clear 'has no Redox mapping' warning.
2. deps.rs: drop gcc-libs dependency (was: mapped to gcc, which
conflates the runtime libgcc/libstdc++ with the compiler).
gcc-libs is provided by relibc on Redox and must be omitted.
3. deps.rs: prefix build tools (meson, ninja, cmake, make,
pkg-config, autoconf, automake, libtool, git, perl, python,
rust, cargo, llvm, clang, swig, bison, flex, doxygen, and ~50
more) with 'host:' so the Redox cookbook knows they're host-only
and not part of the cross-compiled target. The new BUILD_TOOLS
constant lists all known build tools; map_dependency returns
'host:<name>' for entries in this set.
4. pkgbuild.rs: parse AUR-style 'git+url#tag=branch' source syntax.
The new split_source_fragment function strips the 'git+' prefix,
extracts the '#tag=...' or '#branch=...' or '#commit=...'
fragment, and maps to the Redox cookbook's [source].branch or
[source].rev field. The previous behavior kept the literal
'git+...#tag=...' URL in the recipe, which is invalid Redox
cookbook format.
5. pkgbuild.rs: support multi-source PKGBUILDs. Real packages like
mesa have 2+ sources (git repo + extra file). The previous
behavior errored on multi-source with 'multiple sources not yet
supported'. Now: keep the first source as primary, warn about
the rest, and continue conversion. Auxiliary sources are listed
in the warning message so the user can re-add them.
6. pkgbuild.rs: preserve options=() flags (e.g., '!lto', '!strip',
'!emptydirs') in the new RBPKGBUILD compat.options field.
Previously dropped silently.
7. pkgbuild.rs: substitute \${pkgver}, \${pkgname}, etc. in source
URLs by piping each entry through resolve_shell_vars before
converting. The previous behavior kept the literal '\${pkgver}'
in the URL, making the recipe's [source].tar URL invalid.
All fixes verified by:
- cub-assessment: 12 PKGBUILDs all convert and produce valid TOML.
The mesa 24.3 case (which previously errored on multi-source) now
produces a valid recipe with a warning. ffmpeg's glibc is now
correctly dropped. All build tools (meson, ninja, etc.) are
correctly host: prefixed. All AUR git+url sources parse
correctly into branch/rev fields.
- cargo test --workspace: 72 passing (up from 70 — added 2 new
tests for the build-tool prefixing and gcc-libs dropping).
The 8th known issue (custom-template recipes lack DYNAMIC_INIT and
cookbook_apply_patches boilerplate) is deferred — it's a separate
cookbook-integration concern tracked in the cub assessment plan
(local/docs/cub-assessment-and-improvement-plan.md).
This commit is contained in:
@@ -94,6 +94,7 @@ pub fn convert_pkgbuild(content: &str) -> Result<ConversionResult, CubError> {
|
||||
conversion_status: status.clone(),
|
||||
target: "x86_64-unknown-redox".to_string(),
|
||||
split_packages: Vec::new(),
|
||||
options: Vec::new(),
|
||||
},
|
||||
policy: PolicySection::default(),
|
||||
};
|
||||
@@ -423,10 +424,10 @@ package() {
|
||||
assert_eq!(result.rbpkg.build.template, BuildTemplate::Cargo);
|
||||
assert_eq!(
|
||||
result.rbpkg.dependencies.runtime,
|
||||
vec!["relibc", "openssl3"]
|
||||
vec!["openssl3"]
|
||||
);
|
||||
assert_eq!(result.rbpkg.dependencies.build, vec!["cargo", "pkg-config"]);
|
||||
assert_eq!(result.rbpkg.dependencies.check, vec!["python"]);
|
||||
assert_eq!(result.rbpkg.dependencies.build, vec!["host:cargo", "host:pkg-config"]);
|
||||
assert_eq!(result.rbpkg.dependencies.check, vec!["host:python"]);
|
||||
assert_eq!(result.rbpkg.source.sources.len(), 1);
|
||||
assert_eq!(result.rbpkg.source.sources[0].sha256, "abc123deadbeef");
|
||||
}
|
||||
|
||||
@@ -360,6 +360,7 @@ mod tests {
|
||||
conversion_status: ConversionStatus::Full,
|
||||
target: String::new(),
|
||||
split_packages: Vec::new(),
|
||||
options: Vec::new(),
|
||||
},
|
||||
policy: PolicySection::default(),
|
||||
}
|
||||
|
||||
@@ -5,14 +5,50 @@ pub struct MappedDep {
|
||||
pub is_exact: bool,
|
||||
}
|
||||
|
||||
/// Build tools that run on the host during cross-compilation, not on
|
||||
/// the Redox target. The Redox cookbook's `[build].dependencies`
|
||||
/// expects these to be prefixed with `host:` so the cookbook knows
|
||||
/// not to try to cook them for the target.
|
||||
const BUILD_TOOLS: &[&str] = &[
|
||||
"make", "gmake", "bmake",
|
||||
"cmake", "ninja", "meson", "scons",
|
||||
"pkg-config", "pkgconf", "pkgconfig",
|
||||
"autoconf", "automake", "libtool", "m4", "autoconf-archive",
|
||||
"git", "svn", "mercurial", "cvs",
|
||||
"perl", "python", "python2", "python3", "ruby", "go", "golang",
|
||||
"rust", "cargo", "rustc",
|
||||
"bison", "flex", "yacc", "gperf", "ragel",
|
||||
"gettext", "intltool", "msgmerge",
|
||||
"help2man", "gengetopt", "xmlto", "asciidoc", "doxygen", "graphviz",
|
||||
"swig", "pandoc", "markdown",
|
||||
"lua", "luajit",
|
||||
"go-md2man", "go-bindata", "go-tools",
|
||||
"nodejs", "npm", "yarn",
|
||||
"bzip2", "xz", "zstd", "lz4",
|
||||
"tar", "patch", "diffutils", "findutils", "sed", "gawk", "grep",
|
||||
"coreutils", "binutils", "file", "which", "rsync",
|
||||
"llvm", "clang",
|
||||
];
|
||||
|
||||
pub fn map_dependency(arch_name: &str) -> MappedDep {
|
||||
let cleaned = arch_name.trim();
|
||||
let base = dependency_base_name(cleaned);
|
||||
|
||||
// Build tools (host-only) get the `host:` prefix and a `build-base`
|
||||
// mapping so the cookbook treats them as host tools, not as target
|
||||
// packages. They never need to be cross-compiled.
|
||||
if BUILD_TOOLS.contains(&base.as_str()) {
|
||||
return MappedDep {
|
||||
original: cleaned.to_string(),
|
||||
mapped: format!("host:{base}"),
|
||||
is_exact: true,
|
||||
};
|
||||
}
|
||||
|
||||
let (mapped, is_exact) = match base.as_str() {
|
||||
"glibc" => ("relibc".to_string(), false),
|
||||
"glibc" => (String::new(), false),
|
||||
"gcc" | "make" => ("build-base".to_string(), false),
|
||||
"gcc-libs" => ("gcc".to_string(), false),
|
||||
"gcc-libs" => (String::new(), false),
|
||||
"pkg-config" => ("pkg-config".to_string(), true),
|
||||
"glib2" => ("glib".to_string(), true),
|
||||
"gtk3" => ("gtk".to_string(), false),
|
||||
@@ -154,10 +190,34 @@ mod tests {
|
||||
let mapped = map_dependency("glibc");
|
||||
|
||||
assert_eq!(mapped.original, "glibc");
|
||||
assert_eq!(mapped.mapped, "relibc");
|
||||
// glibc is dropped: relibc is the Redox C library, which is the
|
||||
// OS, not a package — depending on it as a runtime dep is a
|
||||
// tautology. The mapping is empty so the converter omits it.
|
||||
assert!(mapped.mapped.is_empty());
|
||||
assert!(!mapped.is_exact);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drops_gcc_libs_runtime() {
|
||||
let mapped = map_dependency("gcc-libs");
|
||||
// gcc-libs is the runtime libgcc/libstdc++ — provided by relibc
|
||||
// on Redox. The mapping is empty so the converter omits it.
|
||||
assert!(mapped.mapped.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prefixes_build_tools_with_host() {
|
||||
for tool in ["meson", "ninja", "cmake", "make", "git", "perl", "python"] {
|
||||
let mapped = map_dependency(tool);
|
||||
assert_eq!(
|
||||
mapped.mapped,
|
||||
format!("host:{tool}"),
|
||||
"build tool '{tool}' should be host: prefixed"
|
||||
);
|
||||
assert!(mapped.is_exact);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keeps_unknown_dependency_name() {
|
||||
let mapped = map_dependency("expat");
|
||||
@@ -187,8 +247,11 @@ mod tests {
|
||||
let deps = vec!["glibc".to_string(), "cmake".to_string()];
|
||||
let mapped = map_dependencies(&deps);
|
||||
|
||||
assert_eq!(mapped.len(), 2);
|
||||
assert_eq!(mapped[0].mapped, "relibc");
|
||||
assert_eq!(mapped[1].mapped, "cmake");
|
||||
// cmake is a build tool — host: prefixed; glibc is dropped.
|
||||
// The first non-empty entry in the mapped Vec is cmake.
|
||||
assert_eq!(mapped[0].original, "glibc");
|
||||
assert!(mapped[0].mapped.is_empty());
|
||||
assert_eq!(mapped[1].original, "cmake");
|
||||
assert_eq!(mapped[1].mapped, "host:cmake");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,6 +133,7 @@ mod tests {
|
||||
conversion_status: ConversionStatus::Full,
|
||||
target: String::new(),
|
||||
split_packages: Vec::new(),
|
||||
options: Vec::new(),
|
||||
},
|
||||
policy: PolicySection::default(),
|
||||
}
|
||||
|
||||
@@ -65,13 +65,37 @@ pub fn convert_pkgbuild(content: &str) -> Result<ConversionResult, CubError> {
|
||||
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 mut sources = extract_array_assignment(content, "source").unwrap_or_default();
|
||||
let sha256sums = extract_array_assignment(content, "sha256sums").unwrap_or_default();
|
||||
let options = extract_array_assignment(content, "options").unwrap_or_default();
|
||||
|
||||
let template = detect_build_template(content);
|
||||
let mut warnings = detect_linuxisms(content);
|
||||
let mut actions_required = Vec::new();
|
||||
|
||||
// Substitute ${pkgver}, ${pkgname}, etc. in source URLs so the
|
||||
// generated recipe has actual URLs (not literal `${pkgver}`).
|
||||
sources = sources
|
||||
.into_iter()
|
||||
.map(|s| resolve_shell_vars(&s, content))
|
||||
.collect();
|
||||
|
||||
// Truncate to the first source for the recipe (the cookbook's
|
||||
// generate_recipe only consumes one source). Auxiliary sources
|
||||
// are warned about; the user can re-add them to the recipe.
|
||||
let original_sources_len = sources.len();
|
||||
if original_sources_len > 1 {
|
||||
warnings.push(format!(
|
||||
"PKGBUILD has {original_sources_len} sources; using the first as primary ('{}'). Auxiliary sources dropped: [{}]",
|
||||
sources[0],
|
||||
sources[1..].join(", "),
|
||||
));
|
||||
actions_required.push(
|
||||
"review multi-source PKGBUILD and re-add auxiliary sources manually".to_string(),
|
||||
);
|
||||
sources.truncate(1);
|
||||
}
|
||||
|
||||
let build_body = if matches!(template, BuildTemplate::Custom) {
|
||||
extract_bash_function(content, "build")
|
||||
} else {
|
||||
@@ -159,6 +183,7 @@ pub fn convert_pkgbuild(content: &str) -> Result<ConversionResult, CubError> {
|
||||
conversion_status: status.clone(),
|
||||
target: "x86_64-unknown-redox".to_string(),
|
||||
split_packages: split_packages,
|
||||
options,
|
||||
},
|
||||
policy: PolicySection::default(),
|
||||
};
|
||||
@@ -494,29 +519,6 @@ pub fn sanitize_pkgname(name: &str) -> String {
|
||||
.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("::")
|
||||
@@ -540,6 +542,66 @@ fn normalize_source_entry(entry: &str) -> String {
|
||||
.to_string()
|
||||
}
|
||||
|
||||
/// Split an AUR-style source URL into (url, branch, rev).
|
||||
///
|
||||
/// AUR git sources use the syntax `git+https://...git#tag=v1.0` or
|
||||
/// `git+https://...git#branch=main` or `git+https://...git#commit=abc123`.
|
||||
/// The Redox cookbook's git source format is:
|
||||
/// `[source] git = "url" branch = "v1.0" | rev = "v1.0"`
|
||||
///
|
||||
/// Returns (url, branch, rev) where at most one of branch/rev is set.
|
||||
fn split_source_fragment(url: &str) -> (String, String, String) {
|
||||
let (base, fragment) = match url.split_once('#') {
|
||||
Some((b, f)) => (b, f),
|
||||
None => return (url.to_string(), String::new(), String::new()),
|
||||
};
|
||||
let value = fragment.trim_start_matches('=');
|
||||
let mut branch = String::new();
|
||||
let mut rev = String::new();
|
||||
if let Some(v) = value.strip_prefix("tag=") {
|
||||
branch = v.to_string();
|
||||
} else if let Some(v) = value.strip_prefix("branch=") {
|
||||
branch = v.to_string();
|
||||
} else if let Some(v) = value.strip_prefix("commit=") {
|
||||
rev = v.to_string();
|
||||
} else if let Some(v) = value.strip_prefix("revision=") {
|
||||
rev = v.to_string();
|
||||
} else {
|
||||
// Bare fragment (no key=value) — treat as a tag/branch
|
||||
branch = value.to_string();
|
||||
}
|
||||
(base.to_string(), branch, rev)
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
|
||||
let (url, branch, rev) = if matches!(source_type, SourceType::Git) {
|
||||
split_source_fragment(&normalized)
|
||||
} else {
|
||||
(normalized, String::new(), String::new())
|
||||
};
|
||||
|
||||
SourceEntry {
|
||||
sha256: if matches!(source_type, SourceType::Tar) {
|
||||
sha256.unwrap_or_default().to_string()
|
||||
} else {
|
||||
String::new()
|
||||
},
|
||||
url,
|
||||
source_type,
|
||||
rev,
|
||||
branch,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn extract_scalar_assignment(content: &str, name: &str) -> Option<String> {
|
||||
extract_assignment(content, name).map(|raw| parse_scalar(&raw))
|
||||
}
|
||||
@@ -744,10 +806,10 @@ package() {
|
||||
assert_eq!(result.rbpkg.build.template, BuildTemplate::Cargo);
|
||||
assert_eq!(
|
||||
result.rbpkg.dependencies.runtime,
|
||||
vec!["relibc", "openssl3"]
|
||||
vec!["openssl3"]
|
||||
);
|
||||
assert_eq!(result.rbpkg.dependencies.build, vec!["cargo", "pkg-config"]);
|
||||
assert_eq!(result.rbpkg.dependencies.check, vec!["python"]);
|
||||
assert_eq!(result.rbpkg.dependencies.build, vec!["host:cargo", "host:pkg-config"]);
|
||||
assert_eq!(result.rbpkg.dependencies.check, vec!["host:python"]);
|
||||
assert_eq!(result.rbpkg.source.sources.len(), 1);
|
||||
assert_eq!(result.rbpkg.source.sources[0].sha256, "abc123deadbeef");
|
||||
}
|
||||
@@ -931,7 +993,7 @@ build() {
|
||||
|
||||
assert_eq!(
|
||||
result.rbpkg.dependencies.optional,
|
||||
vec!["relibc", "bash-completion"]
|
||||
vec!["bash-completion"]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,6 +158,8 @@ pub struct CompatSection {
|
||||
pub target: String,
|
||||
#[serde(default)]
|
||||
pub split_packages: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub options: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
|
||||
|
||||
@@ -183,6 +183,7 @@ mod tests {
|
||||
conversion_status: ConversionStatus::Full,
|
||||
target: String::new(),
|
||||
split_packages: Vec::new(),
|
||||
options: Vec::new(),
|
||||
},
|
||||
policy: PolicySection::default(),
|
||||
}
|
||||
|
||||
@@ -79,7 +79,12 @@ build() {
|
||||
|
||||
let value: toml::Value = toml::from_str(&recipe_toml).expect("parse generated recipe");
|
||||
assert_eq!(value["build"]["template"].as_str(), Some("cargo"));
|
||||
assert_eq!(value["package"]["dependencies"][0].as_str(), Some("relibc"));
|
||||
// glibc is dropped (relibc is the OS, not a runtime dep). openssl
|
||||
// maps to openssl3, so that's the only runtime dep.
|
||||
assert_eq!(
|
||||
value["package"]["dependencies"][0].as_str(),
|
||||
Some("openssl3")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user