7706617e7f
cub redesign (local/recipes/system/cub/): - AUR RPC v5 client (serde_json) with search/info - ~/.cub/ user-local recipe/source/repo storage - Enhanced PKGBUILD parser: optdepends, .SRCINFO, split packages, 19 linuxism patterns - Recipe generation: host: prefix on dev-deps, shallow_clone, cargopath, installs, optional-packages - Dependency resolver: scans build errors for missing commands/headers/libs/pkgconfig, maps to packages - Dependency installation: checks installed packages, fetches AUR deps, interactive prompt - ~110 Arc→Redox dependency mappings - ratatui TUI: search, info, install, build, query views - 14 Arch-style CLI switches (-S/-Si/-Syu/-G/-R/-Q/-Qi/-Ql) - 65 tests, 0 failures, clean build Phase 1-5 native build tools (local/recipes/dev/): - P1 Substrate: tar, m4, diffutils (gnulib bypass), mkfifo kernel patch (1085 lines) - P2 Build Systems: bison, flex, meson (standalone wrapper), ninja-build, libtool - P3 Native GCC: gcc-native, binutils-native (cross-compiled for redox host) - P4 Native LLVM: llvm-native (clang + lld from monorepo) - P5 Native Rust: rust-native (rustc + cargo) - Groups: build-essential-native, dev-essential expanded Config: - redbear-mini: +7 tools (diffutils, tar, bison, flex, meson, ninja, m4) - redbear-full: +4 native tools (gcc, binutils, llvm, rust) - All recipes moved to local/ with symlinks for cookbook discovery (Red Bear policy) Docs: - BUILD-TOOLS-PORTING-PLAN.md: phased porting roadmap - CUB-WORKFLOW-ASSESSMENT.md: gap analysis and integration assessment
439 lines
19 KiB
Rust
439 lines
19 KiB
Rust
use std::collections::HashMap;
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct ResolvedDep {
|
|
pub missing: String,
|
|
pub package: String,
|
|
pub kind: DepKind,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
pub enum DepKind {
|
|
Command,
|
|
Header,
|
|
Library,
|
|
PkgConfig,
|
|
Unknown,
|
|
}
|
|
|
|
pub struct DepResolver {
|
|
command_map: HashMap<String, String>,
|
|
header_map: HashMap<String, String>,
|
|
library_map: HashMap<String, String>,
|
|
pkgconfig_map: HashMap<String, String>,
|
|
}
|
|
|
|
impl DepResolver {
|
|
pub fn new() -> Self {
|
|
let mut command_map = HashMap::new();
|
|
let mut header_map = HashMap::new();
|
|
let mut library_map = HashMap::new();
|
|
|
|
// ── Compilers ──
|
|
for cmd in &["gcc", "g++", "cc", "c++", "cpp"] {
|
|
command_map.insert(cmd.to_string(), "gcc-native".to_string());
|
|
}
|
|
for cmd in &["clang", "clang++", "clang-cpp", "clang-cl"] {
|
|
command_map.insert(cmd.to_string(), "llvm-native".to_string());
|
|
}
|
|
for cmd in &["rustc", "cargo", "rustdoc", "rustfmt", "clippy-driver"] {
|
|
command_map.insert(cmd.to_string(), "rust-native".to_string());
|
|
}
|
|
command_map.insert("nasm".to_string(), "nasm".to_string());
|
|
command_map.insert("yasm".to_string(), "yasm".to_string());
|
|
|
|
// ── Build systems ──
|
|
for cmd in &["make", "gmake", "gnumake"] {
|
|
command_map.insert(cmd.to_string(), "gnu-make".to_string());
|
|
}
|
|
command_map.insert("cmake".to_string(), "cmake".to_string());
|
|
command_map.insert("meson".to_string(), "meson".to_string());
|
|
for cmd in &["ninja", "ninja-build"] {
|
|
command_map.insert(cmd.to_string(), "ninja-build".to_string());
|
|
}
|
|
command_map.insert("autoconf".to_string(), "autoconf".to_string());
|
|
command_map.insert("autoheader".to_string(), "autoconf".to_string());
|
|
command_map.insert("autoreconf".to_string(), "autoconf".to_string());
|
|
command_map.insert("autoscan".to_string(), "autoconf".to_string());
|
|
command_map.insert("automake".to_string(), "automake".to_string());
|
|
command_map.insert("aclocal".to_string(), "automake".to_string());
|
|
command_map.insert("libtool".to_string(), "libtool".to_string());
|
|
command_map.insert("libtoolize".to_string(), "libtool".to_string());
|
|
command_map.insert("m4".to_string(), "m4".to_string());
|
|
command_map.insert("pkg-config".to_string(), "pkg-config".to_string());
|
|
command_map.insert("pkgconf".to_string(), "pkg-config".to_string());
|
|
|
|
// ── Binutils ──
|
|
for cmd in &["ld", "ar", "as", "nm", "strip", "objdump", "objcopy",
|
|
"ranlib", "readelf", "size", "strings", "addr2line"] {
|
|
command_map.insert(cmd.to_string(), "binutils-native".to_string());
|
|
}
|
|
|
|
// ── Text / file tools ──
|
|
command_map.insert("patch".to_string(), "patch".to_string());
|
|
for cmd in &["sed", "gsed"] {
|
|
command_map.insert(cmd.to_string(), "sed".to_string());
|
|
}
|
|
for cmd in &["grep", "egrep", "fgrep", "rgrep"] {
|
|
command_map.insert(cmd.to_string(), "gnu-grep".to_string());
|
|
}
|
|
for cmd in &["awk", "gawk", "mawk", "nawk"] {
|
|
command_map.insert(cmd.to_string(), "gawk".to_string());
|
|
}
|
|
for cmd in &["diff", "cmp", "diff3", "sdiff"] {
|
|
command_map.insert(cmd.to_string(), "diffutils".to_string());
|
|
}
|
|
|
|
// ── Archives ──
|
|
command_map.insert("tar".to_string(), "uutils-tar".to_string());
|
|
for cmd in &["gzip", "gunzip", "zcat"] {
|
|
command_map.insert(cmd.to_string(), "gzip".to_string());
|
|
}
|
|
for cmd in &["bzip2", "bunzip2"] {
|
|
command_map.insert(cmd.to_string(), "bzip2".to_string());
|
|
}
|
|
for cmd in &["xz", "unxz", "lzma"] {
|
|
command_map.insert(cmd.to_string(), "xz".to_string());
|
|
}
|
|
for cmd in &["zstd", "unzstd", "zstdcat"] {
|
|
command_map.insert(cmd.to_string(), "zstd".to_string());
|
|
}
|
|
|
|
// ── VCS / Network ──
|
|
command_map.insert("git".to_string(), "git".to_string());
|
|
for cmd in &["curl", "wget"] {
|
|
command_map.insert(cmd.to_string(), "curl".to_string());
|
|
}
|
|
|
|
// ── Languages ──
|
|
for cmd in &["python", "python3"] {
|
|
command_map.insert(cmd.to_string(), "python312".to_string());
|
|
}
|
|
command_map.insert("perl".to_string(), "perl5".to_string());
|
|
command_map.insert("lua".to_string(), "lua".to_string());
|
|
command_map.insert("ruby".to_string(), "ruby".to_string());
|
|
|
|
// ── Shell ──
|
|
for cmd in &["bash", "sh"] {
|
|
command_map.insert(cmd.to_string(), "bash".to_string());
|
|
}
|
|
|
|
// ── Parser generators ──
|
|
for cmd in &["flex", "lex"] {
|
|
command_map.insert(cmd.to_string(), "flex".to_string());
|
|
}
|
|
for cmd in &["bison", "yacc"] {
|
|
command_map.insert(cmd.to_string(), "bison".to_string());
|
|
}
|
|
command_map.insert("gperf".to_string(), "gperf".to_string());
|
|
|
|
// ── i18n / docs ──
|
|
for cmd in &["gettext", "msgfmt", "xgettext", "msgmerge"] {
|
|
command_map.insert(cmd.to_string(), "gettext".to_string());
|
|
}
|
|
for cmd in &["intltool-update", "intltool-extract", "intltool-merge"] {
|
|
command_map.insert(cmd.to_string(), "intltool".to_string());
|
|
}
|
|
for cmd in &["makeinfo", "texi2any", "texi2dvi", "texi2pdf"] {
|
|
command_map.insert(cmd.to_string(), "texinfo".to_string());
|
|
}
|
|
for cmd in &["help2man"] {
|
|
command_map.insert(cmd.to_string(), "help2man".to_string());
|
|
}
|
|
|
|
// ── Core system ──
|
|
command_map.insert("install".to_string(), "coreutils".to_string());
|
|
for cmd in &["cp", "mv", "rm", "ln", "mkdir", "rmdir", "chmod", "chown",
|
|
"cat", "echo", "touch", "ls", "find", "xargs", "dirname",
|
|
"basename", "tr", "cut", "sort", "uniq", "wc", "head", "tail"] {
|
|
command_map.insert(cmd.to_string(), "coreutils".to_string());
|
|
}
|
|
|
|
// ── Header files → packages ──
|
|
header_map.insert("stdio.h".to_string(), "relibc".to_string());
|
|
header_map.insert("stdlib.h".to_string(), "relibc".to_string());
|
|
header_map.insert("string.h".to_string(), "relibc".to_string());
|
|
header_map.insert("unistd.h".to_string(), "relibc".to_string());
|
|
header_map.insert("fcntl.h".to_string(), "relibc".to_string());
|
|
header_map.insert("signal.h".to_string(), "relibc".to_string());
|
|
header_map.insert("pthread.h".to_string(), "relibc".to_string());
|
|
header_map.insert("dlfcn.h".to_string(), "relibc".to_string());
|
|
header_map.insert("zlib.h".to_string(), "zlib".to_string());
|
|
header_map.insert("bzlib.h".to_string(), "bzip2".to_string());
|
|
header_map.insert("lzma.h".to_string(), "xz".to_string());
|
|
header_map.insert("zstd.h".to_string(), "zstd".to_string());
|
|
header_map.insert("openssl/ssl.h".to_string(), "openssl3".to_string());
|
|
header_map.insert("curl/curl.h".to_string(), "curl".to_string());
|
|
header_map.insert("expat.h".to_string(), "expat".to_string());
|
|
header_map.insert("ffi.h".to_string(), "libffi".to_string());
|
|
header_map.insert("pcre2.h".to_string(), "pcre2".to_string());
|
|
header_map.insert("ncurses.h".to_string(), "ncurses".to_string());
|
|
header_map.insert("readline/readline.h".to_string(), "readline".to_string());
|
|
header_map.insert("sqlite3.h".to_string(), "sqlite3".to_string());
|
|
header_map.insert("fontconfig/fontconfig.h".to_string(), "fontconfig".to_string());
|
|
header_map.insert("freetype2/freetype/freetype.h".to_string(), "freetype".to_string());
|
|
header_map.insert("harfbuzz/hb.h".to_string(), "harfbuzz".to_string());
|
|
header_map.insert("png.h".to_string(), "libpng".to_string());
|
|
header_map.insert("jpeglib.h".to_string(), "libjpeg-turbo".to_string());
|
|
|
|
// ── Library files → packages ──
|
|
library_map.insert("libz".to_string(), "zlib".to_string());
|
|
library_map.insert("libbz2".to_string(), "bzip2".to_string());
|
|
library_map.insert("liblzma".to_string(), "xz".to_string());
|
|
library_map.insert("libzstd".to_string(), "zstd".to_string());
|
|
library_map.insert("libssl".to_string(), "openssl3".to_string());
|
|
library_map.insert("libcrypto".to_string(), "openssl3".to_string());
|
|
library_map.insert("libcurl".to_string(), "curl".to_string());
|
|
library_map.insert("libexpat".to_string(), "expat".to_string());
|
|
library_map.insert("libffi".to_string(), "libffi".to_string());
|
|
library_map.insert("libpcre2".to_string(), "pcre2".to_string());
|
|
library_map.insert("libncurses".to_string(), "ncurses".to_string());
|
|
library_map.insert("libreadline".to_string(), "readline".to_string());
|
|
library_map.insert("libsqlite3".to_string(), "sqlite3".to_string());
|
|
library_map.insert("libpng".to_string(), "libpng".to_string());
|
|
library_map.insert("libjpeg".to_string(), "libjpeg-turbo".to_string());
|
|
library_map.insert("libfontconfig".to_string(), "fontconfig".to_string());
|
|
library_map.insert("libfreetype".to_string(), "freetype".to_string());
|
|
library_map.insert("libharfbuzz".to_string(), "harfbuzz".to_string());
|
|
library_map.insert("libxml2".to_string(), "libxml2".to_string());
|
|
library_map.insert("libxslt".to_string(), "libxslt".to_string());
|
|
|
|
let mut pkgconfig_map = HashMap::new();
|
|
pkgconfig_map.insert("gtk+-3.0".to_string(), "gtk".to_string());
|
|
pkgconfig_map.insert("gtk4".to_string(), "gtk".to_string());
|
|
pkgconfig_map.insert("glib-2.0".to_string(), "glib".to_string());
|
|
pkgconfig_map.insert("gobject-2.0".to_string(), "glib".to_string());
|
|
pkgconfig_map.insert("gio-2.0".to_string(), "glib".to_string());
|
|
pkgconfig_map.insert("cairo".to_string(), "cairo".to_string());
|
|
pkgconfig_map.insert("pango".to_string(), "pango".to_string());
|
|
pkgconfig_map.insert("atk".to_string(), "atk".to_string());
|
|
pkgconfig_map.insert("gdk-pixbuf-2.0".to_string(), "gdk-pixbuf".to_string());
|
|
pkgconfig_map.insert("libpng".to_string(), "libpng".to_string());
|
|
pkgconfig_map.insert("libjpeg".to_string(), "libjpeg-turbo".to_string());
|
|
pkgconfig_map.insert("freetype2".to_string(), "freetype".to_string());
|
|
pkgconfig_map.insert("fontconfig".to_string(), "fontconfig".to_string());
|
|
pkgconfig_map.insert("harfbuzz".to_string(), "harfbuzz".to_string());
|
|
pkgconfig_map.insert("openssl".to_string(), "openssl3".to_string());
|
|
pkgconfig_map.insert("libcurl".to_string(), "curl".to_string());
|
|
pkgconfig_map.insert("zlib".to_string(), "zlib".to_string());
|
|
pkgconfig_map.insert("bzip2".to_string(), "bzip2".to_string());
|
|
pkgconfig_map.insert("liblzma".to_string(), "xz".to_string());
|
|
pkgconfig_map.insert("libzstd".to_string(), "zstd".to_string());
|
|
pkgconfig_map.insert("expat".to_string(), "expat".to_string());
|
|
pkgconfig_map.insert("libffi".to_string(), "libffi".to_string());
|
|
pkgconfig_map.insert("libpcre2-8".to_string(), "pcre2".to_string());
|
|
pkgconfig_map.insert("ncurses".to_string(), "ncurses".to_string());
|
|
pkgconfig_map.insert("readline".to_string(), "readline".to_string());
|
|
pkgconfig_map.insert("sqlite3".to_string(), "sqlite3".to_string());
|
|
pkgconfig_map.insert("dbus-1".to_string(), "dbus".to_string());
|
|
pkgconfig_map.insert("wayland-client".to_string(), "wayland".to_string());
|
|
pkgconfig_map.insert("wayland-server".to_string(), "wayland".to_string());
|
|
pkgconfig_map.insert("x11".to_string(), "libx11".to_string());
|
|
pkgconfig_map.insert("xcb".to_string(), "libxcb".to_string());
|
|
pkgconfig_map.insert("libxml-2.0".to_string(), "libxml2".to_string());
|
|
pkgconfig_map.insert("libxslt".to_string(), "libxslt".to_string());
|
|
pkgconfig_map.insert("alsa".to_string(), "alsa-lib".to_string());
|
|
pkgconfig_map.insert("libpulse".to_string(), "pulseaudio".to_string());
|
|
|
|
Self {
|
|
command_map,
|
|
header_map,
|
|
library_map,
|
|
pkgconfig_map,
|
|
}
|
|
}
|
|
|
|
pub fn scan_build_error(&self, error_output: &str) -> Vec<ResolvedDep> {
|
|
let mut resolved = Vec::new();
|
|
|
|
for line in error_output.lines() {
|
|
let line_lower = line.to_ascii_lowercase();
|
|
|
|
if let Some(header) = extract_missing_header(&line_lower) {
|
|
let base = std::path::Path::new(&header)
|
|
.file_name()
|
|
.and_then(|n| n.to_str())
|
|
.unwrap_or(&header)
|
|
.to_string();
|
|
if let Some(pkg) = self.header_map.get(&base.to_ascii_lowercase()) {
|
|
resolved.push(ResolvedDep {
|
|
missing: header,
|
|
package: pkg.clone(),
|
|
kind: DepKind::Header,
|
|
});
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if let Some(lib) = extract_missing_library(&line_lower) {
|
|
let key = lib.to_ascii_lowercase();
|
|
let pkg = self
|
|
.library_map
|
|
.get(&key)
|
|
.or_else(|| self.library_map.get(&format!("lib{}", key)))
|
|
.cloned();
|
|
if let Some(pkg) = pkg {
|
|
resolved.push(ResolvedDep {
|
|
missing: format!("lib{}", lib),
|
|
package: pkg,
|
|
kind: DepKind::Library,
|
|
});
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if let Some(pc) = extract_missing_pkgconfig(&line_lower) {
|
|
let key = pc.to_ascii_lowercase();
|
|
let pkg = self
|
|
.command_map
|
|
.get(&key)
|
|
.or_else(|| self.pkgconfig_map.get(&key))
|
|
.cloned();
|
|
if let Some(pkg) = pkg {
|
|
resolved.push(ResolvedDep {
|
|
missing: pc,
|
|
package: pkg,
|
|
kind: DepKind::PkgConfig,
|
|
});
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if let Some(cmd) = extract_command_not_found(&line_lower) {
|
|
if let Some(pkg) = self.command_map.get(&cmd.to_ascii_lowercase()) {
|
|
if !resolved.iter().any(|r: &ResolvedDep| r.missing == cmd) {
|
|
resolved.push(ResolvedDep {
|
|
missing: cmd,
|
|
package: pkg.clone(),
|
|
kind: DepKind::Command,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
resolved
|
|
}
|
|
}
|
|
|
|
impl Default for DepResolver {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
fn extract_command_not_found(line_lower: &str) -> Option<String> {
|
|
// "sh: line 1: gcc: command not found"
|
|
if line_lower.contains(": command not found") {
|
|
if let Some(rest) = line_lower.strip_suffix(": command not found") {
|
|
if let Some(cmd) = rest.rsplit(':').next() {
|
|
let cmd = cmd.trim();
|
|
if !cmd.is_empty() && cmd.len() < 50 {
|
|
return Some(cmd.to_string());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// "make: gcc: No such file or directory"
|
|
if line_lower.contains(": no such file or directory") {
|
|
let rest = line_lower.replace(": no such file or directory", "");
|
|
if let Some(cmd) = rest.rsplit(':').next() {
|
|
let cmd = cmd.trim();
|
|
if !cmd.is_empty() && cmd.len() < 50 {
|
|
return Some(cmd.to_string());
|
|
}
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
fn extract_missing_header(line_lower: &str) -> Option<String> {
|
|
// "fatal error: X.h: No such file or directory"
|
|
if line_lower.contains("fatal error:") && line_lower.contains("no such file") {
|
|
if let Some(after) = line_lower.split("fatal error:").nth(1) {
|
|
if let Some(header) = after.split(':').next() {
|
|
let h = header.trim();
|
|
if !h.is_empty() {
|
|
return Some(h.to_string());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
fn extract_missing_library(line_lower: &str) -> Option<String> {
|
|
if line_lower.contains("cannot find -l") {
|
|
for part in line_lower.split_whitespace() {
|
|
if part.starts_with("-l") && part.len() > 2 {
|
|
let mut lib = part[2..].to_string();
|
|
lib = lib.trim_end_matches(|c: char| !c.is_alphanumeric() && c != '_').to_string();
|
|
if !lib.is_empty() && lib.chars().all(|c| c.is_alphanumeric() || c == '_') {
|
|
return Some(lib);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
fn extract_missing_pkgconfig(line_lower: &str) -> Option<String> {
|
|
// "No package 'gtk+-3.0' found"
|
|
if line_lower.contains("no package '") {
|
|
if let Some(after) = line_lower.split("no package '").nth(1) {
|
|
if let Some(pkg) = after.split('\'').next() {
|
|
let p = pkg.trim();
|
|
if !p.is_empty() {
|
|
return Some(p.to_string());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn detects_command_not_found() {
|
|
let output = "sh: line 1: gcc: command not found\nmake: *** [all] Error 127";
|
|
let resolver = DepResolver::new();
|
|
let deps = resolver.scan_build_error(output);
|
|
assert!(deps.iter().any(|d| d.missing == "gcc" && d.package == "gcc-native"));
|
|
}
|
|
|
|
#[test]
|
|
fn detects_missing_header() {
|
|
let output = "src/main.c:3:10: fatal error: zlib.h: No such file or directory";
|
|
let resolver = DepResolver::new();
|
|
let deps = resolver.scan_build_error(output);
|
|
assert!(deps.iter().any(|d| d.missing.contains("zlib.h") && d.package == "zlib"));
|
|
}
|
|
|
|
#[test]
|
|
fn detects_missing_library() {
|
|
let output = "/usr/bin/ld: cannot find -lz: No such file or directory";
|
|
let resolver = DepResolver::new();
|
|
let deps = resolver.scan_build_error(output);
|
|
assert!(deps.iter().any(|d| d.package == "zlib"));
|
|
}
|
|
|
|
#[test]
|
|
fn detects_missing_pkgconfig() {
|
|
let output = "Package gtk+-3.0 was not found in the pkg-config search path.\nNo package 'gtk+-3.0' found";
|
|
let resolver = DepResolver::new();
|
|
let deps = resolver.scan_build_error(output);
|
|
assert!(deps.iter().any(|d| d.missing == "gtk+-3.0"));
|
|
}
|
|
|
|
#[test]
|
|
fn detects_make_command_not_found() {
|
|
let output = "make: cmake: No such file or directory";
|
|
let resolver = DepResolver::new();
|
|
let deps = resolver.scan_build_error(output);
|
|
assert!(deps.iter().any(|d| d.missing == "cmake" && d.package == "cmake"));
|
|
}
|
|
}
|