cub: support multi-source PKGBUILDs in recipe generation (v6.0 2026)

This commit is contained in:
2026-06-10 15:32:28 +03:00
parent 6cd5534426
commit 21783ba474
7 changed files with 128 additions and 61 deletions
@@ -212,6 +212,7 @@ fn source_from_arch(entry: String, sha256: Option<&str>) -> SourceEntry {
};
SourceEntry {
name: None,
sha256: if matches!(source_type, SourceType::Tar) {
sha256.unwrap_or_default().to_string()
} else {
@@ -77,52 +77,76 @@ struct OptionalPackage {
pub fn generate_recipe(rbpkg: &RbPkgBuild) -> Result<String, CubError> {
rbpkg.validate()?;
let source_count = rbpkg.source.sources.len();
if source_count > 1 {
let names: Vec<String> = rbpkg
.source
.sources
.iter()
.map(|s| {
let url = &s.url;
if url.len() > 60 {
format!("{}...", &url[..57])
} else {
url.clone()
}
})
.collect();
return Err(CubError::Conversion(format!(
"multiple sources not yet supported (found {}: {}). Use single-source packages or manually create recipe.toml",
source_count,
names.join(", ")
)));
if rbpkg.source.sources.is_empty() {
return Err(CubError::InvalidPkgbuild(
"recipe has no source entries".to_string(),
));
}
let source = rbpkg
let primary_source = rbpkg.source.sources.first().map(convert_source_entry).transpose()?;
let aux_sources: Vec<(String, CookbookSource)> = rbpkg
.source
.sources
.first()
.map(convert_source)
.transpose()?
.map(|mut source| {
source.patches = rbpkg.patches.files.clone();
source
});
.iter()
.skip(1)
.filter_map(|s| {
s.name.as_ref().map(|n| {
convert_source_entry(s)
.map(|body| (n.clone(), body))
})
})
.collect::<Result<_, _>>()?;
let source = primary_source.map(|mut source| {
source.patches = rbpkg.patches.files.clone();
source
});
let build = convert_build(rbpkg)?;
let package = build_package_section(rbpkg);
let optional_packages = build_optional_packages(rbpkg);
toml::to_string_pretty(&CookbookRecipe {
let recipe = CookbookRecipe {
source,
build,
package,
optional_packages,
})
.map_err(CubError::from)
};
let mut value: toml::Value =
toml::Value::try_from(&recipe).map_err(CubError::from)?;
merge_aux_sources(&mut value, &aux_sources)?;
toml::to_string_pretty(&value).map_err(CubError::from)
}
fn convert_source(source: &crate::rbpkgbuild::SourceEntry) -> Result<CookbookSource, CubError> {
fn merge_aux_sources(
value: &mut toml::Value,
aux_sources: &[(String, CookbookSource)],
) -> Result<(), CubError> {
if aux_sources.is_empty() {
return Ok(());
}
let table = value
.as_table_mut()
.ok_or_else(|| CubError::Conversion("recipe root is not a TOML table".to_string()))?;
let source_table = table
.entry("source".to_string())
.or_insert_with(|| toml::Value::Table(toml::map::Map::new()));
let source_table = source_table
.as_table_mut()
.ok_or_else(|| CubError::Conversion("[source] is not a TOML table".to_string()))?;
for (name, body) in aux_sources {
let mut nested = toml::map::Map::new();
let body_value: toml::Value = toml::Value::try_from(body).map_err(CubError::from)?;
if let Some(inner_table) = body_value.as_table() {
for (k, v) in inner_table {
nested.insert(k.clone(), v.clone());
}
}
source_table.insert(name.clone(), toml::Value::Table(nested));
}
Ok(())
}
fn convert_source_entry(source: &crate::rbpkgbuild::SourceEntry) -> Result<CookbookSource, CubError> {
let mut cookbook = CookbookSource::default();
match source.source_type {
@@ -326,6 +350,7 @@ mod tests {
},
source: SourceSection {
sources: vec![SourceEntry {
name: None,
source_type: SourceType::Git,
url: "https://example.com/repo.git".to_string(),
sha256: String::new(),
@@ -391,6 +416,7 @@ mod tests {
fn generates_tar_recipe_without_checksum() {
let mut pkg = base_pkg(BuildTemplate::Cargo);
pkg.source.sources[0] = SourceEntry {
name: None,
source_type: SourceType::Tar,
url: "https://example.com/demo.tar.gz".to_string(),
sha256: "abc123deadbeef".to_string(),
@@ -464,9 +490,11 @@ mod tests {
}
#[test]
fn errors_on_multiple_sources() {
fn emits_aux_sources_as_named_source_tables() {
let mut pkg = base_pkg(BuildTemplate::Cargo);
pkg.source.sources[0].name = None;
pkg.source.sources.push(SourceEntry {
name: Some("patches".to_string()),
source_type: SourceType::Tar,
url: "https://example.com/extra.tar.gz".to_string(),
sha256: "deadbeef".to_string(),
@@ -474,7 +502,24 @@ mod tests {
branch: String::new(),
});
let err = generate_recipe(&pkg).expect_err("multiple sources should error");
assert!(matches!(err, CubError::Conversion(_)));
let recipe = generate_recipe(&pkg).expect("multi-source should emit recipe");
assert!(recipe.contains("[source.patches]"));
assert!(recipe.contains("tar = \"https://example.com/extra.tar.gz\""));
}
#[test]
fn single_source_emits_no_named_table() {
let pkg = base_pkg(BuildTemplate::Cargo);
let recipe = generate_recipe(&pkg).expect("single-source recipe");
assert!(!recipe.contains("[source."));
assert!(recipe.contains("git = \"https://example.com/repo.git\""));
}
#[test]
fn errors_on_empty_source_list() {
let mut pkg = base_pkg(BuildTemplate::Cargo);
pkg.source.sources.clear();
let err = generate_recipe(&pkg).expect_err("empty sources should error");
assert!(matches!(err, CubError::InvalidPkgbuild(_)));
}
}
@@ -80,20 +80,17 @@ pub fn convert_pkgbuild(content: &str) -> Result<ConversionResult, CubError> {
.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 {
// Keep all sources. The primary (index 0) is emitted unnamed in the
// generated recipe; auxiliaries (index > 0) get auto-generated names
// ("source-1", "source-2", ...) so they can be referenced from the
// build script and the Redox cookbook.
if sources.len() > 1 {
warnings.push(format!(
"PKGBUILD has {original_sources_len} sources; using the first as primary ('{}'). Auxiliary sources dropped: [{}]",
"PKGBUILD has {} sources; primary is '{}' and {} auxiliary source(s) preserved with auto-generated names",
sources.len(),
sources[0],
sources[1..].join(", "),
sources.len() - 1
));
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) {
@@ -157,7 +154,15 @@ pub fn convert_pkgbuild(content: &str) -> Result<ConversionResult, CubError> {
.into_iter()
.enumerate()
.map(|(index, source)| {
source_from_arch(source, sha256sums.get(index).map(String::as_str))
if index == 0 {
source_from_arch(source, sha256sums.get(index).map(String::as_str))
} else {
source_from_arch_named(
source,
sha256sums.get(index).map(String::as_str),
format!("source-{index}"),
)
}
})
.collect(),
},
@@ -590,6 +595,7 @@ pub fn source_from_arch(entry: String, sha256: Option<&str>) -> SourceEntry {
};
SourceEntry {
name: None,
sha256: if matches!(source_type, SourceType::Tar) {
sha256.unwrap_or_default().to_string()
} else {
@@ -602,6 +608,16 @@ pub fn source_from_arch(entry: String, sha256: Option<&str>) -> SourceEntry {
}
}
pub fn source_from_arch_named(
entry: String,
sha256: Option<&str>,
name: String,
) -> SourceEntry {
let mut entry = source_from_arch(entry, sha256);
entry.name = Some(name);
entry
}
pub fn extract_scalar_assignment(content: &str, name: &str) -> Option<String> {
extract_assignment(content, name).map(|raw| parse_scalar(&raw))
}
@@ -52,6 +52,8 @@ pub struct SourceSection {
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SourceEntry {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(rename = "type")]
pub source_type: SourceType,
#[serde(default)]
@@ -159,6 +159,7 @@ mod tests {
},
source: SourceSection {
sources: vec![SourceEntry {
name: None,
source_type: SourceType::Tar,
url: "https://example.com/demo.tar.xz".to_string(),
sha256: "abc123".to_string(),
@@ -69,7 +69,7 @@ build() {
value["source"]["tar"].as_str(),
Some("https://example.com/demo-1.2.3.tar.xz")
);
assert_eq!(value["source"]["blake3"].as_str(), None);
assert_eq!(value["source"].get("blake3").and_then(|v| v.as_str()), None);
}
#[test]
+15 -13
View File
@@ -1,19 +1,22 @@
# libpciaccess — X.Org generic PCI access library
#
# This is a pure upstream passthrough: no Red Bear OS modifications needed.
# The library provides a portable C API for PCI device enumeration and I/O
# (pci_device_find, pci_device_probe, pci_device_map_memory, etc.) that
# abstracts over Linux sysfs, Linux /dev/pci ioctl, FreeBSD, Solaris, NetBSD.
# This is a Red Bear OS in-tree fork (per AGENTS.md Rule 1). The upstream
# 0.19 source has been copied to local/sources/libpciaccess/ and patched
# with a Redox OS backend (src/redox_pci.c) plus matching __redox__
# branches in src/common_init.c and src/meson.build.
#
# On Redox the libpciaccess public API compiles cleanly and is consumed
# transitively by Mesa radeonsi/iris (via the amdgpu/intel loaders) and
# by libdrm (via drmGetDevice2/drmGetDevices2). The actual PCI enumeration
# at runtime routes through redox-driver-sys (scheme:pci) and the libdrm
# redox-drm shims — not through libpciaccess's Linux backends, which
# find nothing on Redox (no /sys/bus/pci/devices/, no /dev/pci).
# The Redox backend enumerates /scheme/pci/ (populated by the userspace
# redox-driver-pci daemon) and routes BAR mappings through
# /scheme/memory/physical. I/O-space BARs return ENOSYS because all
# PCI I/O on Redox is MMIO.
#
# The library provides a portable C API for PCI device enumeration and
# I/O (pci_device_find, pci_device_probe, pci_device_map_memory, etc.)
# and is consumed transitively by Mesa radeonsi/iris (via the amdgpu/
# intel loaders) and by libdrm (via drmGetDevice2/drmGetDevices2).
#
# Build system: meson only (autotools removed upstream in 0.18).
# Source: https://xorg.freedesktop.org/releases/individual/lib/libpciaccess-0.19.tar.xz
# Source: local fork of https://xorg.freedesktop.org/releases/individual/lib/libpciaccess-0.19.tar.xz
# Build deps: meson, ninja-build (and the cookbook's host pkg-config).
# Runtime deps: only libc (relibc on Redox).
# Optional deps: zlib (gzip-compressed pci.ids lookup) — disabled for now
@@ -21,8 +24,7 @@
# stub is verified or replaced with a full zlib port.
[source]
tar = "https://xorg.freedesktop.org/releases/individual/lib/libpciaccess-0.19.tar.xz"
blake3 = "2bd8a8cc35aa4bb34dbb043547496367ba66d27b1e3b84a9cae47f0ee29c9c66"
path = "../../../local/sources/libpciaccess"
[build]
template = "custom"