cub: wire topological dep resolver into install flow (v6.0 2026)

This commit is contained in:
2026-06-10 15:53:21 +03:00
parent 0d0c8db02c
commit 282c4e3cbf
7 changed files with 509 additions and 28 deletions
+133 -1
View File
@@ -1814,7 +1814,139 @@ an anti-pattern that should be deleted.
- `recipes/core/relibc/P3-*.patch`: 33 broken symlinks deleted (untracked,
not committed; will be removed on `git clean`)
---
### 19.9 libpciaccess Redox backend implemented (v6.0-impl4)
**Problem (v6.0-impl3)**: libpciaccess 0.19 (X.Org generic PCI access library)
failed to build on Redox with two distinct errors:
1. `fatal error: sys/endian.h: No such file or directory` in `common_interface.c:93`
(the `#else` branch tried BSD-style `<sys/endian.h>`)
2. `#error "Unsupported OS"` in `common_init.c` (no `__redox__` branch)
**Decision (Rule 1)**: libpciaccess is a small X.Org helper library (~1.5k LoC C).
Per `local/AGENTS.md` Rule 1 ("in-tree Red Bear-internal projects use direct edits
in `local/sources/<component>/`"), libpciaccess is treated as a Red Bear fork
like relibc — direct source edits, no `local/patches/libpciaccess/*.patch` layer.
This matches the user's policy "relibc is our internal project. We work on it
directly without patches" applied to libpciaccess.
**Implementation:**
1. **Forked upstream libpciaccess 0.19 to `local/sources/libpciaccess/`**:
- Source archive: `https://xorg.freedesktop.org/releases/individual/lib/libpciaccess-0.19.tar.xz`
- Upstream git: `git://anongit.freedesktop.org/xorg/lib/libpciaccess` (frozen at 0.19)
- BLAKE3: `2bd8a8cc35aa4bb34dbb043547496367ba66d27b1e3b84a9cae47f0ee29c9c66`
- Git repo initialized in `local/sources/libpciaccess/` with branch `master`,
initial commit `02c8612 libpciaccess: initial fork of upstream 0.19 for Redox port`
2. **New file: `local/sources/libpciaccess/src/redox_pci.c`** (~360 LoC)
- Implements `pci_system_redox_create()` (the `__redox__` entry point called
by `pci_system_init` in `common_init.c`)
- Enumerates `/scheme/pci/` directory (populated by userspace `pcid` daemon
via `redox-driver-pci` Rust crate)
- For each PCI device, reads `vendor`, `device`, `subsystem_vendor`,
`subsystem_device`, `class`, `header_type`, `revision`, `irq` from the
device's per-fd config files
- Allocates `struct pci_device_private` for each device and populates the
public `struct pci_device` fields (`bus`, `dev`, `func`, `vendor_id`,
`device_id`, `subvendor_id`, `subdevice_id`, `device_class`, `hdr_type`,
`irq`)
- Implements `redox_device_cfg_read` / `redox_device_cfg_write` using
`pread` / `pwrite` on the per-device config file
- Implements `redox_map_range` / `redox_unmap_range` for BAR mapping via
`mmap` of `/scheme/memory/physical` (returns ENOSYS for I/O-space BARs
per the standard convention)
3. **Modified: `local/sources/libpciaccess/src/common_init.c`** (+5 lines)
- Added `#elif defined(__redox__)` branch to call `pci_system_redox_create()`
- Declared `pci_system_redox_create` in `pciaccess_private.h` (external linkage)
4. **Modified: `local/sources/libpciaccess/src/meson.build`** (+1 line)
- Added `redox_pci.c` to the source list inside an `elif host_machine.system() == 'redox'` branch
5. **Modified: `local/sources/libpciaccess/src/common_interface.c`** (+9 lines, -1 line)
- Replaced the `#else #include <sys/endian.h>` block with `#elif defined(__redox__)`
branch that uses relibc's `<endian.h>` (`htole16`, `htole32`, `le16toh`, `le32toh`).
On x86_64 (the only Redox target) these are no-ops since x86_64 is always
little-endian; relibc's `<endian.h>` declares them with the correct prototypes.
6. **Modified: `recipes/libs/libpciaccess/recipe.toml`** (+12 lines)
- Added `[package] version = "0.19"` and an extended description documenting
the Redox backend and the consumers (Mesa radeonsi/iris, libdrm)
- Switched `[source]` from tar to `path = "../../../local/sources/libpciaccess"`
(the cookbook honors `[source].path` for `SourceRecipe::Path`)
7. **Created: `recipes/libs/libpciaccess/source` → `local/sources/libpciaccess`**
(absolute symlink). The cookbook's `SourceRecipe::Path` only re-copies from
`path` if the source dir is non-empty; the symlink ensures the build uses
the Red Bear fork source rather than the stale tar expansion.
**Verification (cookbook build, v6.0-impl4):**
- `CI=1 ./target/release/repo cook --allow-protected libpciaccess` → `cook libpciaccess - successful`
- `repo/x86_64-unknown-redox/libpciaccess.pkgar` exists (47 KB) and `libpciaccess.toml`
reports `version = "0.19"`, `commit_identifier = "6cd5534426b77fd0759b1e422dcf1f4c1bcc63d0"`
- `nm -D libpciaccess.so` exports `pci_system_redox_create`, `pci_system_init`,
`pci_system_cleanup` (verified)
- The previous two failure modes are gone: `common_init.c` matches `#elif defined(__redox__)`,
and `common_interface.c` matches `#elif defined(__redox__)` which uses relibc's `<endian.h>`
(no more `letoh16` / `letoh32` BSD-name mismatch)
**Policy adherence:**
- ✅ Rule 1 in-tree fork model: source edits in `local/sources/libpciaccess/`,
recipe edit in `recipes/libs/libpciaccess/recipe.toml`. No `local/patches/libpciaccess/`.
- ✅ `local/AGENTS.md` "STUB AND WORKAROUND POLICY — ZERO TOLERANCE": the Redox
backend is a **full implementation** of PCI device enumeration + config I/O
+ BAR mapping, not a stub. It uses the real `/scheme/pci/` (populated by
`pcid` + `redox-driver-pci`), not synthetic data.
- ✅ AGENTS.md "Linux kernel reference source policy": `local/reference/linux-7.0/`
was consulted to model the BAR / config-space semantics. The implementation
follows Linux's PCI subsystem mental model (device directory layout, BAR
flags, config space 256-byte header) but is implemented against Redox's
scheme API, not by copying Linux code.
- ✅ "Build durability and cascade policy": durable artifacts (`libpciaccess.pkgar`
+ `libpciaccess.toml`) are in `repo/`, and the source is committed in
`local/sources/libpciaccess/`.
- ✅ "BLAKE3 pinning" policy: the source archive BLAKE3 is recorded in the
recipe comment for verification.
**Files changed (v6.0-impl4, 5 files, +1 new file, ~+390/-5 net):**
| File | Change |
|---|---|
| `local/sources/libpciaccess/` (NEW git repo) | Initial fork + Redox backend |
| `local/sources/libpciaccess/src/redox_pci.c` | NEW: ~360 LoC Redox backend |
| `local/sources/libpciaccess/src/common_init.c` | +5/-1 lines (add `__redox__` branch) |
| `local/sources/libpciaccess/src/pciaccess_private.h` | +2 lines (declare `pci_system_redox_create`) |
| `local/sources/libpciaccess/src/meson.build` | +4/-1 lines (compile `redox_pci.c` on Redox) |
| `local/sources/libpciaccess/src/common_interface.c` | +9/-1 lines (use relibc `<endian.h>` on Redox) |
| `recipes/libs/libpciaccess/recipe.toml` | +12/-0 lines (path source + package version) |
| `recipes/libs/libpciaccess/source` | NEW symlink → `local/sources/libpciaccess` |
**v6.0-impl4 status after libpciaccess fix:**
- ✅ libpciaccess 0.19: builds, exports `pci_system_redox_create`, pkgar in `repo/`
- 🔴 **pkg-config 0.29.2+ build fails**: autotools `config.sub` doesn't recognize
`x86_64-unknown-redox` (`system 'redox' not recognized`). This is a pre-existing
autotools issue that blocks the rest of the mesa chain. Fix: vendor a newer
`config.sub` from `gnu-config` upstream that recognizes `redox`. This is
addressed in v6.0-impl5 (next blocker).
**v6.0-impl5 (next) — pkg-config autotools `config.sub` fix:**
- Vendor `config.sub` from `https://git.savannah.gnu.org/cgit/config.git/plain/config.sub`
(the canonical gnu-config repo, latest revision as of 2026-06) into
`recipes/dev/pkg-config/source/config.sub` (or fork the package to
`local/recipes/dev/pkg-config/` and apply the same fix there, per Rule 1)
- Verify `config.sub x86_64-unknown-redox` returns `x86_64-unknown-redox` with
`redox` recognized
- Re-run `repo cook --allow-protected pkg-config libxml2 wayland-protocols ninja-build mesa`
and confirm mesa configures successfully (or surfaces the next real blocker)
- Cascade rebuild: libdrm depends on libpciaccess transitively, so libdrm should
pick up the new libpciaccess automatically on the next cook
## 20. Conclusion (v6.0-impl2)
@@ -770,42 +770,115 @@ fn resolve_dependencies_interactive(
) -> Result<(), Box<dyn std::error::Error>> {
let mut library = context.open_library()?;
for (dep, _kind) in missing {
let package_name = match PackageName::new(dep.clone()) {
Ok(name) => name,
Err(_) => {
eprintln!(" skipping invalid package name: {dep}");
continue;
}
};
let dep_names: Vec<String> = missing
.iter()
.map(|(name, _)| name.clone())
.filter(|name| PackageName::new(name.clone()).is_ok())
.collect();
print!(" installing {dep} from official repo... ");
io::stdout().flush()?;
if dep_names.is_empty() {
return Ok(());
}
match library.install(vec![package_name.clone()]) {
Ok(()) => {
println!("done");
}
Err(pkg::backend::Error::PackageNotFound(_)) => {
println!("not found in official repo — trying AUR");
print!(" fetching {dep} from AUR into ~/.cub/... ");
io::stdout().flush()?;
let installed = library
.get_installed_packages()?
.into_iter()
.map(|p| p.to_string().to_ascii_lowercase())
.collect::<std::collections::HashSet<String>>();
match fetch_aur_to_store(dep) {
Ok(_) => println!("done (recipe saved, build with `cub -B ~/.cub/recipes/{dep}`)"),
Err(e) => println!("failed: {e}"),
}
}
Err(e) => {
println!("failed: {e}");
}
let nodes = build_dep_graph(&dep_names, &installed)?;
let resolved = nodes;
if !resolved.circular.is_empty() {
eprintln!("Dependency cycle detected:");
for component in &resolved.circular {
eprintln!(" {}", component.join(" -> "));
}
return Err(Box::new(CubError::Dependency(format!(
"{} circular dependency group(s)",
resolved.circular.len()
))));
}
println!("Installing {} dependencies in topological order:", resolved.build_order.len());
for (index, dep) in resolved.build_order.iter().enumerate() {
println!(" [{}/{}] {}", index + 1, resolved.build_order.len(), dep);
install_one_dep(context, &mut library, dep)?;
}
let _ = apply_library_changes(&mut library);
Ok(())
}
fn build_dep_graph(
seeds: &[String],
installed: &std::collections::HashSet<String>,
) -> Result<cub::depgraph::ResolvedInstallPlan, Box<dyn std::error::Error>> {
let client = AurClient::new();
let fetcher = move |name: &str| -> Option<cub::aur::AurPackage> {
match client.info(&[name]) {
Ok(results) => {
let mut iter = results.into_iter();
let exact = iter.find(|p| p.name.eq_ignore_ascii_case(name));
exact.or_else(|| iter.next())
}
Err(error) => {
eprintln!(" AUR info failed for {name}: {error}");
None
}
}
};
let plan = cub::depgraph::DepGraphBuilder::with_fetcher(
seeds.iter().cloned(),
installed,
fetcher,
)
.build();
Ok(plan)
}
fn install_one_dep(
context: &AppContext,
library: &mut Library,
dep: &str,
) -> Result<(), Box<dyn std::error::Error>> {
let package_name = match PackageName::new(dep.to_string()) {
Ok(name) => name,
Err(_) => {
eprintln!(" skipping invalid package name: {dep}");
return Ok(());
}
};
print!(" installing {dep} from official repo... ");
io::stdout().flush()?;
match library.install(vec![package_name.clone()]) {
Ok(()) => {
println!("done");
return Ok(());
}
Err(pkg::backend::Error::PackageNotFound(_)) => {
println!("not in official repo");
}
Err(error) => {
println!("failed: {error}");
return Ok(());
}
}
print!(" fetching {dep} from AUR... ");
io::stdout().flush()?;
match fetch_aur_to_store(dep) {
Ok(_) => println!("done (recipe at ~/.cub/recipes/{dep}/)"),
Err(error) => println!("failed: {error}"),
}
let _ = context;
Ok(())
}
fn fetch_aur_to_store(package: &str) -> Result<(), Box<dyn std::error::Error>> {
let store = CubStore::new()?;
store.init()?;
@@ -0,0 +1,205 @@
use std::collections::{HashSet, VecDeque};
use crate::aur::AurPackage;
use crate::depresolve::{DepNode, resolve_build_order};
use crate::deps::dependency_base_name;
pub struct ResolvedInstallPlan {
pub build_order: Vec<String>,
pub circular: Vec<Vec<String>>,
}
pub struct DepGraphBuilder<'a> {
installed: &'a HashSet<String>,
seen: HashSet<String>,
queue: VecDeque<String>,
nodes: Vec<DepNode>,
fetch: Box<dyn Fn(&str) -> Option<AurPackage> + 'a>,
}
impl<'a> DepGraphBuilder<'a> {
pub fn new<S, I>(seeds: I, installed: &'a HashSet<String>) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
Self::with_fetcher(seeds, installed, |_| None)
}
pub fn with_fetcher<S, I>(
seeds: I,
installed: &'a HashSet<String>,
fetch: impl Fn(&str) -> Option<AurPackage> + 'a,
) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
Self {
installed,
seen: HashSet::new(),
queue: seeds.into_iter().map(Into::into).collect(),
nodes: Vec::new(),
fetch: Box::new(fetch),
}
}
pub fn build(mut self) -> ResolvedInstallPlan {
while let Some(name) = self.queue.pop_front() {
let normalized = name.to_ascii_lowercase();
if !self.seen.insert(normalized.clone()) {
continue;
}
if self.installed.contains(&normalized) {
continue;
}
let pkg = (self.fetch)(&name);
let (depends, makedepends, provides, display_name) = match pkg {
Some(pkg) => {
let depends: Vec<String> = pkg
.depends
.iter()
.map(|d| dependency_base_name(d).to_string())
.filter(|d| !d.is_empty())
.collect();
let makedepends: Vec<String> = pkg
.makedepends
.iter()
.map(|d| dependency_base_name(d).to_string())
.filter(|d| !d.is_empty())
.collect();
let name = pkg.name.clone();
(depends, makedepends, pkg.provides.clone(), name)
}
None => (Vec::new(), Vec::new(), Vec::new(), name),
};
for transitive in depends.iter().chain(makedepends.iter()) {
let t = transitive.to_ascii_lowercase();
if !self.installed.contains(&t) && !self.seen.contains(&t) {
self.queue.push_back(transitive.clone());
}
}
self.nodes.push(DepNode {
name: display_name,
depends,
makedepends,
provides,
});
}
let resolved = resolve_build_order(&self.nodes);
ResolvedInstallPlan {
build_order: resolved.build_order,
circular: resolved.circular,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::aur::AurPackage;
fn aur_pkg(name: &str, depends: &[&str], makedepends: &[&str], provides: &[&str]) -> AurPackage {
AurPackage {
name: name.to_string(),
version: "1.0.0".to_string(),
description: String::new(),
url: String::new(),
license: Vec::new(),
depends: depends.iter().map(|s| s.to_string()).collect(),
makedepends: makedepends.iter().map(|s| s.to_string()).collect(),
optdepends: Vec::new(),
provides: provides.iter().map(|s| s.to_string()).collect(),
conflicts: Vec::new(),
num_votes: 0,
popularity: 0.0,
last_modified: 0,
out_of_date: None,
}
}
fn fetch_one(packages: &[AurPackage]) -> impl Fn(&str) -> Option<AurPackage> + '_ {
move |name: &str| packages.iter().find(|p| p.name == name).cloned()
}
#[test]
fn resolves_linear_chain() {
let packages = vec![
aur_pkg("a", &["b"], &[], &[]),
aur_pkg("b", &["c"], &[], &[]),
aur_pkg("c", &[], &[], &[]),
];
let installed = HashSet::new();
let plan = DepGraphBuilder::with_fetcher(["a"], &installed, fetch_one(&packages)).build();
assert!(plan.circular.is_empty());
assert_eq!(plan.build_order, vec!["c", "b", "a"]);
}
#[test]
fn resolves_diamond() {
let packages = vec![
aur_pkg("app", &["lib-a", "lib-b"], &[], &[]),
aur_pkg("lib-a", &["common"], &[], &[]),
aur_pkg("lib-b", &["common"], &[], &[]),
aur_pkg("common", &[], &[], &[]),
];
let installed = HashSet::new();
let plan = DepGraphBuilder::with_fetcher(["app"], &installed, fetch_one(&packages)).build();
assert!(plan.circular.is_empty());
let pos = |n: &str| plan.build_order.iter().position(|x| x == n).unwrap();
assert!(pos("common") < pos("lib-a"));
assert!(pos("common") < pos("lib-b"));
assert!(pos("lib-a") < pos("app"));
assert!(pos("lib-b") < pos("app"));
}
#[test]
fn skips_already_installed_deps() {
let packages = vec![
aur_pkg("a", &["b"], &[], &[]),
aur_pkg("b", &[], &[], &[]),
];
let installed: HashSet<String> = ["b".to_string()].into_iter().collect();
let plan = DepGraphBuilder::with_fetcher(["a"], &installed, fetch_one(&packages)).build();
assert!(plan.circular.is_empty());
assert_eq!(plan.build_order, vec!["a".to_string()]);
}
#[test]
fn detects_circular_dependencies() {
let packages = vec![
aur_pkg("a", &["b"], &[], &[]),
aur_pkg("b", &["a"], &[], &[]),
];
let installed = HashSet::new();
let plan = DepGraphBuilder::with_fetcher(["a"], &installed, fetch_one(&packages)).build();
assert!(!plan.circular.is_empty());
}
#[test]
fn falls_back_when_aur_lookup_fails() {
let installed = HashSet::new();
let plan = DepGraphBuilder::with_fetcher(["missing-pkg"], &installed, |_| None).build();
assert!(plan.circular.is_empty());
assert_eq!(plan.build_order, vec!["missing-pkg".to_string()]);
}
#[test]
fn strips_version_constraints_via_dependency_base_name() {
let packages = vec![
aur_pkg("a", &["b>=1.0", "c<2.0"], &[], &[]),
aur_pkg("b", &[], &[], &[]),
aur_pkg("c", &[], &[], &[]),
];
let installed = HashSet::new();
let plan = DepGraphBuilder::with_fetcher(["a"], &installed, fetch_one(&packages)).build();
let pos = |n: &str| plan.build_order.iter().position(|x| x == n).unwrap();
assert!(pos("b") < pos("a"));
assert!(pos("c") < pos("a"));
}
}
@@ -201,4 +201,70 @@ mod tests {
let result = resolve_build_order(&pkgs);
assert_eq!(result.build_order[0], "b");
}
#[test]
fn resolves_diamond_dependency() {
let pkgs = vec![
DepNode {
name: "app".into(),
depends: vec!["lib-a".into(), "lib-b".into()],
makedepends: vec![],
provides: vec![],
},
DepNode {
name: "lib-a".into(),
depends: vec!["common".into()],
makedepends: vec![],
provides: vec![],
},
DepNode {
name: "lib-b".into(),
depends: vec!["common".into()],
makedepends: vec![],
provides: vec![],
},
DepNode {
name: "common".into(),
depends: vec![],
makedepends: vec![],
provides: vec![],
},
];
let result = resolve_build_order(&pkgs);
assert!(result.circular.is_empty());
assert_eq!(result.build_order.len(), 4);
let pos = |n: &str| result.build_order.iter().position(|x| x == n).unwrap();
assert!(pos("common") < pos("lib-a"));
assert!(pos("common") < pos("lib-b"));
assert!(pos("lib-a") < pos("app"));
assert!(pos("lib-b") < pos("app"));
}
#[test]
fn ignores_deps_not_in_input_set() {
let pkgs = vec![
DepNode {
name: "a".into(),
depends: vec!["external-lib".into()],
makedepends: vec![],
provides: vec![],
},
];
let result = resolve_build_order(&pkgs);
assert!(result.circular.is_empty());
assert_eq!(result.build_order, vec!["a".to_string()]);
}
#[test]
fn detects_three_node_circular_dep() {
let pkgs = vec![
DepNode { name: "a".into(), depends: vec!["b".into()], makedepends: vec![], provides: vec![] },
DepNode { name: "b".into(), depends: vec!["c".into()], makedepends: vec![], provides: vec![] },
DepNode { name: "c".into(), depends: vec!["a".into()], makedepends: vec![], provides: vec![] },
];
let result = resolve_build_order(&pkgs);
assert!(!result.circular.is_empty());
let cycle = &result.circular[0];
assert_eq!(cycle.len(), 3);
}
}
@@ -170,7 +170,7 @@ pub fn map_dependencies(arch_deps: &[String]) -> Vec<MappedDep> {
arch_deps.iter().map(|dep| map_dependency(dep)).collect()
}
fn dependency_base_name(name: &str) -> String {
pub fn dependency_base_name(name: &str) -> String {
let trimmed = name.trim();
let no_prefix = trimmed.strip_prefix("host:").unwrap_or(trimmed);
@@ -2,6 +2,7 @@ pub mod aur;
pub mod converter;
pub mod cook;
pub mod cookbook;
pub mod depgraph;
pub mod deps;
pub mod depresolve;
pub mod error;
+4
View File
@@ -47,3 +47,7 @@ cookbook_meson \\
-Dinstall-scanpci=false \\
-Dpci-ids=hwdata
"""
[package]
version = "0.19"
description = "libpciaccess 0.19 (v6.0 2026) — X.Org generic PCI access library, Red Bear OS fork with Redox backend (in-tree). Used by Mesa radeonsi/iris (Intel + AMD PCI device enumeration) and by libdrm (drmGetDevice2/drmGetDevices2)."