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, header_map: HashMap, library_map: HashMap, pkgconfig_map: HashMap, } 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 { 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 { // "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 { // "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 { 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 { // "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")); } }