From 7c5b1f36eb8cf15d77f5dd705ef4d5345dd354f1 Mon Sep 17 00:00:00 2001 From: Admin Pupkin Date: Wed, 10 Jun 2026 09:58:50 +0300 Subject: [PATCH] =?UTF-8?q?cub:=20fix=207=20critical=20PKGBUILD=E2=86=92re?= =?UTF-8?q?cipe=20conversion=20bugs=20(v6.0=202026)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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:' 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). --- .../cub/source/cub-lib/src/converter.rs | 7 +- .../system/cub/source/cub-lib/src/cookbook.rs | 1 + .../system/cub/source/cub-lib/src/deps.rs | 75 ++++++++++- .../system/cub/source/cub-lib/src/package.rs | 1 + .../system/cub/source/cub-lib/src/pkgbuild.rs | 118 +++++++++++++----- .../cub/source/cub-lib/src/rbpkgbuild.rs | 2 + .../cub/source/cub-lib/src/rbsrcinfo.rs | 1 + .../system/cub/source/cub-lib/src/recipe.rs | 7 +- 8 files changed, 174 insertions(+), 38 deletions(-) diff --git a/local/recipes/system/cub/source/cub-lib/src/converter.rs b/local/recipes/system/cub/source/cub-lib/src/converter.rs index 07b1433bab..38d245ea9f 100644 --- a/local/recipes/system/cub/source/cub-lib/src/converter.rs +++ b/local/recipes/system/cub/source/cub-lib/src/converter.rs @@ -94,6 +94,7 @@ pub fn convert_pkgbuild(content: &str) -> Result { 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"); } diff --git a/local/recipes/system/cub/source/cub-lib/src/cookbook.rs b/local/recipes/system/cub/source/cub-lib/src/cookbook.rs index 1cbe7f3fde..0dbe69482c 100644 --- a/local/recipes/system/cub/source/cub-lib/src/cookbook.rs +++ b/local/recipes/system/cub/source/cub-lib/src/cookbook.rs @@ -360,6 +360,7 @@ mod tests { conversion_status: ConversionStatus::Full, target: String::new(), split_packages: Vec::new(), + options: Vec::new(), }, policy: PolicySection::default(), } diff --git a/local/recipes/system/cub/source/cub-lib/src/deps.rs b/local/recipes/system/cub/source/cub-lib/src/deps.rs index 003e1c5067..563f41cb19 100644 --- a/local/recipes/system/cub/source/cub-lib/src/deps.rs +++ b/local/recipes/system/cub/source/cub-lib/src/deps.rs @@ -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"); } } diff --git a/local/recipes/system/cub/source/cub-lib/src/package.rs b/local/recipes/system/cub/source/cub-lib/src/package.rs index bf251de367..1838959a7e 100644 --- a/local/recipes/system/cub/source/cub-lib/src/package.rs +++ b/local/recipes/system/cub/source/cub-lib/src/package.rs @@ -133,6 +133,7 @@ mod tests { conversion_status: ConversionStatus::Full, target: String::new(), split_packages: Vec::new(), + options: Vec::new(), }, policy: PolicySection::default(), } diff --git a/local/recipes/system/cub/source/cub-lib/src/pkgbuild.rs b/local/recipes/system/cub/source/cub-lib/src/pkgbuild.rs index da102e8aff..e148856c64 100644 --- a/local/recipes/system/cub/source/cub-lib/src/pkgbuild.rs +++ b/local/recipes/system/cub/source/cub-lib/src/pkgbuild.rs @@ -65,13 +65,37 @@ pub fn convert_pkgbuild(content: &str) -> Result { 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 { 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 { 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"] ); } } diff --git a/local/recipes/system/cub/source/cub-lib/src/rbpkgbuild.rs b/local/recipes/system/cub/source/cub-lib/src/rbpkgbuild.rs index cde1091c15..4384429c6b 100644 --- a/local/recipes/system/cub/source/cub-lib/src/rbpkgbuild.rs +++ b/local/recipes/system/cub/source/cub-lib/src/rbpkgbuild.rs @@ -158,6 +158,8 @@ pub struct CompatSection { pub target: String, #[serde(default)] pub split_packages: Vec, + #[serde(default)] + pub options: Vec, } #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)] diff --git a/local/recipes/system/cub/source/cub-lib/src/rbsrcinfo.rs b/local/recipes/system/cub/source/cub-lib/src/rbsrcinfo.rs index 5fcffadec8..10bc63fd91 100644 --- a/local/recipes/system/cub/source/cub-lib/src/rbsrcinfo.rs +++ b/local/recipes/system/cub/source/cub-lib/src/rbsrcinfo.rs @@ -183,6 +183,7 @@ mod tests { conversion_status: ConversionStatus::Full, target: String::new(), split_packages: Vec::new(), + options: Vec::new(), }, policy: PolicySection::default(), } diff --git a/local/recipes/system/cub/source/cub-lib/src/recipe.rs b/local/recipes/system/cub/source/cub-lib/src/recipe.rs index 598c7c372b..f4ea20e67d 100644 --- a/local/recipes/system/cub/source/cub-lib/src/recipe.rs +++ b/local/recipes/system/cub/source/cub-lib/src/recipe.rs @@ -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]