cub: full AUR package manager + Phase 1-5 native build tools
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
This commit is contained in:
@@ -3,11 +3,14 @@ use std::env;
|
||||
use std::ffi::OsString;
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
use std::rc::Rc;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
use clap::{CommandFactory, Parser, Subcommand};
|
||||
use cub::aur::{AurClient, AurPackage};
|
||||
use cub::cook;
|
||||
@@ -475,6 +478,27 @@ fn build_local_dir(context: &AppContext, dir: &Path) -> Result<(), Box<dyn std::
|
||||
let rbpkg = RbPkgBuild::from_file(&rbpkg_path)?;
|
||||
rbpkg.validate()?;
|
||||
|
||||
let missing = check_missing_dependencies(context, &rbpkg)?;
|
||||
if !missing.is_empty() {
|
||||
println!(
|
||||
"The following dependencies are not installed and must be resolved before building {}:",
|
||||
rbpkg.package.name
|
||||
);
|
||||
for (dep, kind) in &missing {
|
||||
println!(" - {} ({})", dep, kind);
|
||||
}
|
||||
println!();
|
||||
println!("Would you like to try installing missing dependencies? [y/N]");
|
||||
|
||||
let mut answer = String::new();
|
||||
io::stdin().read_line(&mut answer)?;
|
||||
if answer.trim().to_ascii_lowercase().starts_with('y') {
|
||||
resolve_dependencies_interactive(context, &missing)?;
|
||||
} else {
|
||||
println!("Proceeding with build — it may fail if dependencies are missing at cook time.");
|
||||
}
|
||||
}
|
||||
|
||||
let work_dir = create_temp_dir("cub-build")?;
|
||||
let recipe_dir = work_dir.join(&rbpkg.package.name);
|
||||
CookbookAdapter::write_recipe_dir(&rbpkg, &recipe_dir)?;
|
||||
@@ -525,6 +549,126 @@ fn build_local_dir(context: &AppContext, dir: &Path) -> Result<(), Box<dyn std::
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_missing_dependencies(
|
||||
context: &AppContext,
|
||||
rbpkg: &RbPkgBuild,
|
||||
) -> Result<Vec<(String, String)>, Box<dyn std::error::Error>> {
|
||||
let library = context.open_library()?;
|
||||
let installed: Vec<String> = library
|
||||
.get_installed_packages()?
|
||||
.into_iter()
|
||||
.map(|p| p.to_string().to_ascii_lowercase())
|
||||
.collect();
|
||||
|
||||
let mut missing = Vec::new();
|
||||
let all_deps: Vec<(&String, &str)> = rbpkg
|
||||
.dependencies
|
||||
.build
|
||||
.iter()
|
||||
.map(|d| (d, "build dependency"))
|
||||
.chain(
|
||||
rbpkg
|
||||
.dependencies
|
||||
.runtime
|
||||
.iter()
|
||||
.map(|d| (d, "runtime dependency")),
|
||||
)
|
||||
.collect();
|
||||
|
||||
let mut seen = HashSet::new();
|
||||
for (dep, kind) in all_deps {
|
||||
let lower = dep.to_ascii_lowercase();
|
||||
if !seen.insert(lower.clone()) {
|
||||
continue;
|
||||
}
|
||||
if installed.contains(&lower) {
|
||||
continue;
|
||||
}
|
||||
missing.push((dep.clone(), kind.to_string()));
|
||||
}
|
||||
|
||||
Ok(missing)
|
||||
}
|
||||
|
||||
fn resolve_dependencies_interactive(
|
||||
context: &AppContext,
|
||||
missing: &[(String, String)],
|
||||
) -> 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;
|
||||
}
|
||||
};
|
||||
|
||||
print!(" installing {dep} from official repo... ");
|
||||
io::stdout().flush()?;
|
||||
|
||||
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()?;
|
||||
|
||||
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 _ = apply_library_changes(&mut library);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn fetch_aur_to_store(package: &str) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let store = CubStore::new()?;
|
||||
store.init()?;
|
||||
let recipe_dir = store.recipes_dir().join(package);
|
||||
if recipe_dir.exists() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let repo_url = aur_repo_url(package);
|
||||
let clone_dir = create_temp_dir("cub-dep-aur")?;
|
||||
|
||||
let status = Command::new("git")
|
||||
.arg("clone")
|
||||
.arg("--depth")
|
||||
.arg("1")
|
||||
.arg("--")
|
||||
.arg(&repo_url)
|
||||
.arg(&clone_dir)
|
||||
.status()?;
|
||||
|
||||
if !status.success() {
|
||||
return Err(Box::new(CubError::BuildFailed(format!(
|
||||
"failed to clone AUR source from {repo_url}"
|
||||
))));
|
||||
}
|
||||
|
||||
let pkgbuild_path = clone_dir.join("PKGBUILD");
|
||||
let pkgbuild_content = fs::read_to_string(&pkgbuild_path)?;
|
||||
let conversion = pkgbuild::convert_pkgbuild(&pkgbuild_content)?;
|
||||
|
||||
fs::create_dir_all(&recipe_dir)?;
|
||||
fs::write(recipe_dir.join("RBPKGBUILD"), conversion.rbpkg.to_toml()?)?;
|
||||
cub::recipe::save_recipe_to_store(&conversion.rbpkg, &store)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn fetch_bur_recipe(package: &str) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let source_dir = ensure_bur_package_dir(package)?;
|
||||
let destination = env::current_dir()?.join(package);
|
||||
|
||||
@@ -93,6 +93,7 @@ pub fn convert_pkgbuild(content: &str) -> Result<ConversionResult, CubError> {
|
||||
original_pkgbuild: content.to_string(),
|
||||
conversion_status: status.clone(),
|
||||
target: "x86_64-unknown-redox".to_string(),
|
||||
split_packages: Vec::new(),
|
||||
},
|
||||
policy: PolicySection::default(),
|
||||
};
|
||||
|
||||
@@ -10,6 +10,8 @@ struct CookbookRecipe {
|
||||
build: CookbookBuild,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
package: Option<CookbookPackage>,
|
||||
#[serde(rename = "optional-packages", skip_serializing_if = "Vec::is_empty")]
|
||||
optional_packages: Vec<OptionalPackage>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Serialize)]
|
||||
@@ -24,6 +26,8 @@ struct CookbookSource {
|
||||
rev: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
blake3: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
shallow_clone: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
patches: Vec<String>,
|
||||
}
|
||||
@@ -35,6 +39,8 @@ struct CookbookBuild {
|
||||
dependencies: Vec<String>,
|
||||
#[serde(rename = "dev-dependencies", skip_serializing_if = "Vec::is_empty")]
|
||||
dev_dependencies: Vec<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
cargopath: Option<String>,
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
cargoflags: Vec<String>,
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
@@ -55,15 +61,42 @@ struct CookbookPackage {
|
||||
version: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
description: Option<String>,
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
installs: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct OptionalPackage {
|
||||
name: String,
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
dependencies: Vec<String>,
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
files: Vec<String>,
|
||||
}
|
||||
|
||||
pub fn generate_recipe(rbpkg: &RbPkgBuild) -> Result<String, CubError> {
|
||||
rbpkg.validate()?;
|
||||
|
||||
if rbpkg.source.sources.len() > 1 {
|
||||
return Err(CubError::Conversion(
|
||||
"Cookbook recipe generation currently supports a single primary source".to_string(),
|
||||
));
|
||||
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(", ")
|
||||
)));
|
||||
}
|
||||
|
||||
let source = rbpkg
|
||||
@@ -78,11 +111,13 @@ pub fn generate_recipe(rbpkg: &RbPkgBuild) -> Result<String, CubError> {
|
||||
});
|
||||
let build = convert_build(rbpkg)?;
|
||||
let package = build_package_section(rbpkg);
|
||||
let optional_packages = build_optional_packages(rbpkg);
|
||||
|
||||
toml::to_string_pretty(&CookbookRecipe {
|
||||
source,
|
||||
build,
|
||||
package,
|
||||
optional_packages,
|
||||
})
|
||||
.map_err(CubError::from)
|
||||
}
|
||||
@@ -95,10 +130,13 @@ fn convert_source(source: &crate::rbpkgbuild::SourceEntry) -> Result<CookbookSou
|
||||
cookbook.git = Some(source.url.clone());
|
||||
cookbook.branch = non_empty(&source.branch);
|
||||
cookbook.rev = non_empty(&source.rev);
|
||||
cookbook.shallow_clone = Some(true);
|
||||
}
|
||||
SourceType::Tar => {
|
||||
cookbook.tar = Some(source.url.clone());
|
||||
cookbook.blake3 = non_empty(&source.sha256);
|
||||
if !source.sha256.is_empty() {
|
||||
cookbook.blake3 = Some(source.sha256.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,7 +147,8 @@ fn convert_build(rbpkg: &RbPkgBuild) -> Result<CookbookBuild, CubError> {
|
||||
let mut build = CookbookBuild {
|
||||
template: template_name(&rbpkg.build.template).to_string(),
|
||||
dependencies: rbpkg.dependencies.build.clone(),
|
||||
dev_dependencies: rbpkg.dependencies.check.clone(),
|
||||
dev_dependencies: prefix_host_deps(&rbpkg.dependencies.check),
|
||||
cargopath: non_empty(&rbpkg.build.build_dir),
|
||||
cargoflags: Vec::new(),
|
||||
configureflags: Vec::new(),
|
||||
cmakeflags: Vec::new(),
|
||||
@@ -148,17 +187,60 @@ fn build_package_section(rbpkg: &RbPkgBuild) -> Option<CookbookPackage> {
|
||||
rbpkg.package.version.clone()
|
||||
});
|
||||
|
||||
if rbpkg.dependencies.runtime.is_empty() && description.is_none() && version.is_none() {
|
||||
let mut installs = Vec::new();
|
||||
for entry in &rbpkg.install.bins {
|
||||
installs.push(format!("/usr/bin/{}", entry.to.split('/').last().unwrap_or(&entry.to)));
|
||||
}
|
||||
|
||||
if rbpkg.dependencies.runtime.is_empty() && description.is_none() && version.is_none() && installs.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(CookbookPackage {
|
||||
dependencies: rbpkg.dependencies.runtime.clone(),
|
||||
version,
|
||||
description,
|
||||
installs,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn build_optional_packages(rbpkg: &RbPkgBuild) -> Vec<OptionalPackage> {
|
||||
let mut packages = Vec::new();
|
||||
|
||||
for name in &rbpkg.compat.split_packages {
|
||||
if name == &rbpkg.package.name {
|
||||
continue;
|
||||
}
|
||||
packages.push(OptionalPackage {
|
||||
name: name.clone(),
|
||||
dependencies: Vec::new(),
|
||||
files: vec![format!("usr/**")],
|
||||
});
|
||||
}
|
||||
|
||||
if !rbpkg.dependencies.optional.is_empty() && packages.is_empty() {
|
||||
packages.push(OptionalPackage {
|
||||
name: "optional-deps".to_string(),
|
||||
dependencies: rbpkg.dependencies.optional.clone(),
|
||||
files: Vec::new(),
|
||||
});
|
||||
}
|
||||
|
||||
packages
|
||||
}
|
||||
|
||||
fn prefix_host_deps(deps: &[String]) -> Vec<String> {
|
||||
deps.iter()
|
||||
.map(|d| {
|
||||
if d.starts_with("host:") {
|
||||
d.clone()
|
||||
} else {
|
||||
format!("host:{}", d)
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn custom_script(rbpkg: &RbPkgBuild) -> Result<String, CubError> {
|
||||
let mut parts = Vec::new();
|
||||
|
||||
@@ -169,6 +251,27 @@ fn custom_script(rbpkg: &RbPkgBuild) -> Result<String, CubError> {
|
||||
}
|
||||
parts.extend(rbpkg.build.install_script.iter().cloned());
|
||||
|
||||
if !rbpkg.install.bins.is_empty() {
|
||||
let mut install_lines = Vec::new();
|
||||
for entry in &rbpkg.install.bins {
|
||||
install_lines.push(format!(
|
||||
"install -Dm755 {} \"${{COOKBOOK_STAGE}}/{}\"",
|
||||
entry.from, entry.to
|
||||
));
|
||||
}
|
||||
parts.extend(install_lines);
|
||||
}
|
||||
if !rbpkg.install.libs.is_empty() {
|
||||
let mut install_lines = Vec::new();
|
||||
for entry in &rbpkg.install.libs {
|
||||
install_lines.push(format!(
|
||||
"install -Dm644 {} \"${{COOKBOOK_STAGE}}/{}\"",
|
||||
entry.from, entry.to
|
||||
));
|
||||
}
|
||||
parts.extend(install_lines);
|
||||
}
|
||||
|
||||
if parts.is_empty() {
|
||||
return Err(CubError::InvalidPkgbuild(
|
||||
"custom template requires at least one prepare/build/check/install command".to_string(),
|
||||
@@ -232,7 +335,7 @@ mod tests {
|
||||
build: vec!["cargo".to_string()],
|
||||
runtime: vec!["openssl3".to_string()],
|
||||
check: vec!["python".to_string()],
|
||||
optional: Vec::new(),
|
||||
optional: vec!["git".to_string()],
|
||||
provides: Vec::new(),
|
||||
conflicts: Vec::new(),
|
||||
},
|
||||
@@ -256,6 +359,7 @@ mod tests {
|
||||
original_pkgbuild: String::new(),
|
||||
conversion_status: ConversionStatus::Full,
|
||||
target: String::new(),
|
||||
split_packages: Vec::new(),
|
||||
},
|
||||
policy: PolicySection::default(),
|
||||
}
|
||||
@@ -277,6 +381,7 @@ mod tests {
|
||||
value["package"]["dependencies"][0].as_str(),
|
||||
Some("openssl3")
|
||||
);
|
||||
assert_eq!(value["source"]["shallow_clone"].as_bool(), Some(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -321,4 +426,40 @@ mod tests {
|
||||
let recipe = generate_recipe(&pkg).expect("generate recipe");
|
||||
assert!(!recipe.contains("make test"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prefixes_dev_dependencies_with_host() {
|
||||
let recipe = generate_recipe(&base_pkg(BuildTemplate::Cargo)).expect("generate recipe");
|
||||
let value: toml::Value = toml::from_str(&recipe).expect("parse generated recipe");
|
||||
|
||||
assert_eq!(
|
||||
value["build"]["dev-dependencies"][0].as_str(),
|
||||
Some("host:python")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generates_optional_packages_for_optional_deps() {
|
||||
let recipe = generate_recipe(&base_pkg(BuildTemplate::Cargo)).expect("generate recipe");
|
||||
let value: toml::Value = toml::from_str(&recipe).expect("parse generated recipe");
|
||||
|
||||
let opt = &value["optional-packages"][0];
|
||||
assert_eq!(opt["name"].as_str(), Some("optional-deps"));
|
||||
assert_eq!(opt["dependencies"][0].as_str(), Some("git"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn errors_on_multiple_sources() {
|
||||
let mut pkg = base_pkg(BuildTemplate::Cargo);
|
||||
pkg.source.sources.push(SourceEntry {
|
||||
source_type: SourceType::Tar,
|
||||
url: "https://example.com/extra.tar.gz".to_string(),
|
||||
sha256: "deadbeef".to_string(),
|
||||
rev: String::new(),
|
||||
branch: String::new(),
|
||||
});
|
||||
|
||||
let err = generate_recipe(&pkg).expect_err("multiple sources should error");
|
||||
assert!(matches!(err, CubError::Conversion(_)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,73 @@ pub fn map_dependency(arch_name: &str) -> MappedDep {
|
||||
"libtool" => ("libtool".to_string(), true),
|
||||
"systemd" => (String::new(), false),
|
||||
"dbus" => ("dbus".to_string(), true),
|
||||
"bzip2" | "libbz2" => ("bzip2".to_string(), true),
|
||||
"xz" | "liblzma" => ("xz".to_string(), true),
|
||||
"zstd" | "libzstd" => ("zstd".to_string(), true),
|
||||
"lz4" | "liblz4" => ("lz4".to_string(), true),
|
||||
"expat" | "libexpat" => ("expat".to_string(), true),
|
||||
"libxml2" => ("libxml2".to_string(), false),
|
||||
"libxslt" => ("libxslt".to_string(), false),
|
||||
"libarchive" => ("libarchive".to_string(), true),
|
||||
"libuv" => ("libuv".to_string(), true),
|
||||
"nghttp2" | "libnghttp2" => ("nghttp2".to_string(), true),
|
||||
"sqlite" | "sqlite3" => ("sqlite3".to_string(), true),
|
||||
"libsodium" => ("libsodium".to_string(), false),
|
||||
"libssh2" => ("libssh2".to_string(), false),
|
||||
"openssh" => ("openssh".to_string(), false),
|
||||
"gmp" | "libgmp" => ("libgmp".to_string(), true),
|
||||
"mpfr" | "libmpfr" => ("libmpfr".to_string(), true),
|
||||
"mpc" | "libmpc" => ("mpc".to_string(), true),
|
||||
"isl" | "libisl" => ("isl".to_string(), false),
|
||||
"gdbm" => ("gdbm".to_string(), false),
|
||||
"libcap" | "libcap-ng" => (String::new(), false),
|
||||
"pam" | "linux-pam" => (String::new(), false),
|
||||
"acl" | "libacl" | "attr" | "libattr" => (String::new(), false),
|
||||
"libselinux" => (String::new(), false),
|
||||
"dbus-glib" => ("dbus".to_string(), false),
|
||||
"alsa-lib" | "alsa" => (String::new(), false),
|
||||
"pulseaudio" | "libpulse" => (String::new(), false),
|
||||
"jack" | "jack2" => (String::new(), false),
|
||||
"gstreamer" => (String::new(), false),
|
||||
"libdrm" => ("libdrm".to_string(), true),
|
||||
"mesa" | "mesa-libgl" => ("mesa".to_string(), false),
|
||||
"libglvnd" => (String::new(), false),
|
||||
"vulkan-icd-loader" | "vulkan-loader" => (String::new(), false),
|
||||
"libusb" => (String::new(), false),
|
||||
"libinput" => (String::new(), false),
|
||||
"libevdev" => (String::new(), false),
|
||||
"mtdev" => (String::new(), false),
|
||||
"libwacom" => (String::new(), false),
|
||||
"libxrandr" | "libxext" | "libxrender" | "libxi" | "libxfixes"
|
||||
| "libxdamage" | "libxcomposite" | "libxcursor" | "libxinerama"
|
||||
| "libxshmfence" | "libxkbcommon" | "libxau" | "libxdmcp"
|
||||
| "libxxf86vm" | "libxtst" | "libxt" | "libxmu" | "libxpm"
|
||||
| "libxkbfile" | "libxres" | "libxscrnsaver" | "libxv"
|
||||
| "libxvmc" | "libsm" | "libice" => (String::new(), false),
|
||||
"icu" | "libicu" | "icu4c" => ("icu".to_string(), false),
|
||||
"libunistring" => (String::new(), false),
|
||||
"pcre" | "libpcre" => ("pcre2".to_string(), false),
|
||||
"libelf" | "elfutils" => ("elfutils".to_string(), false),
|
||||
"dw" | "libdw" => (String::new(), false),
|
||||
"libunwind" => (String::new(), false),
|
||||
"gperf" => ("gperf".to_string(), true),
|
||||
"intltool" => ("intltool".to_string(), false),
|
||||
"gettext" | "gnu-gettext" => ("gettext".to_string(), true),
|
||||
"texinfo" => ("texinfo".to_string(), false),
|
||||
"help2man" => (String::new(), false),
|
||||
"gtk-doc" => (String::new(), false),
|
||||
"gobject-introspection" => (String::new(), false),
|
||||
"vala" => (String::new(), false),
|
||||
"python-setuptools" | "python-wheel" | "python-pip"
|
||||
| "python-build" | "python-installer" => ("python".to_string(), false),
|
||||
"ruby" | "rubygems" => (String::new(), false),
|
||||
"nodejs" | "npm" => (String::new(), false),
|
||||
"java-runtime" | "jre-openjdk" | "jdk-openjdk" => (String::new(), false),
|
||||
"ghc" | "haskell" | "cabal" => (String::new(), false),
|
||||
"go" | "golang" => (String::new(), false),
|
||||
"lua" | "luajit" => ("lua".to_string(), true),
|
||||
"tcl" | "tclsh" => (String::new(), false),
|
||||
"tk" | "wish" => (String::new(), false),
|
||||
_ => (base.clone(), true),
|
||||
};
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ pub mod pkgbuild;
|
||||
pub mod rbpkgbuild;
|
||||
pub mod rbsrcinfo;
|
||||
pub mod recipe;
|
||||
pub mod resolver;
|
||||
pub mod sandbox;
|
||||
pub mod storage;
|
||||
|
||||
|
||||
@@ -132,6 +132,7 @@ mod tests {
|
||||
original_pkgbuild: String::new(),
|
||||
conversion_status: ConversionStatus::Full,
|
||||
target: String::new(),
|
||||
split_packages: Vec::new(),
|
||||
},
|
||||
policy: PolicySection::default(),
|
||||
}
|
||||
|
||||
@@ -126,6 +126,7 @@ pub fn convert_pkgbuild(content: &str) -> Result<ConversionResult, CubError> {
|
||||
original_pkgbuild: content.to_string(),
|
||||
conversion_status: status.clone(),
|
||||
target: "x86_64-unknown-redox".to_string(),
|
||||
split_packages: split_packages,
|
||||
},
|
||||
policy: PolicySection::default(),
|
||||
};
|
||||
@@ -328,22 +329,25 @@ pub fn detect_build_template(content: &str) -> BuildTemplate {
|
||||
pub fn detect_linuxisms(content: &str) -> Vec<String> {
|
||||
let lowered = content.to_ascii_lowercase();
|
||||
let checks = [
|
||||
(
|
||||
"systemctl",
|
||||
"uses systemctl, which is not available on Redox",
|
||||
),
|
||||
(
|
||||
"/usr/lib/systemd",
|
||||
"references /usr/lib/systemd, which is Linux-specific",
|
||||
),
|
||||
(
|
||||
"systemd",
|
||||
"references systemd, which is unavailable on Redox",
|
||||
),
|
||||
(
|
||||
"/proc",
|
||||
"references /proc, which may require Redox-specific adaptation",
|
||||
),
|
||||
("systemctl", "uses systemctl, which is not available on Redox"),
|
||||
("/usr/lib/systemd", "references /usr/lib/systemd, which is Linux-specific"),
|
||||
("systemd", "references systemd, which is unavailable on Redox"),
|
||||
("/proc", "references /proc, which may require Redox-specific adaptation"),
|
||||
("dbus-daemon", "uses dbus-daemon, verify D-Bus service compatibility"),
|
||||
("dbus-launch", "uses dbus-launch, verify D-Bus session compatibility"),
|
||||
("systemd-udevd", "references systemd-udevd, unavailable on Redox (use udev-shim)"),
|
||||
("udevadm", "uses udevadm, unavailable on Redox"),
|
||||
("/sys/class", "references /sys/class, may require Redox-specific adaptation"),
|
||||
("/sys/devices", "references /sys/devices, may require Redox-specific adaptation"),
|
||||
("/run/", "references /run/, may require Redox-specific adaptation"),
|
||||
("systemd-tmpfiles", "uses systemd-tmpfiles, unavailable on Redox"),
|
||||
("systemd-sysusers", "uses systemd-sysusers, unavailable on Redox"),
|
||||
("libsystemd", "links against libsystemd, unavailable on Redox"),
|
||||
("libudev", "links against libudev, may need udev-shim"),
|
||||
("polkit", "references polkit, verify PolicyKit compatibility"),
|
||||
("pam_systemd", "references pam_systemd, unavailable on Redox"),
|
||||
("elogind", "references elogind, unavailable on Redox"),
|
||||
("logind", "references logind, unavailable on Redox (use redbear-sessiond)"),
|
||||
];
|
||||
|
||||
let mut warnings = Vec::new();
|
||||
|
||||
@@ -156,6 +156,8 @@ pub struct CompatSection {
|
||||
pub conversion_status: ConversionStatus,
|
||||
#[serde(default)]
|
||||
pub target: String,
|
||||
#[serde(default)]
|
||||
pub split_packages: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
|
||||
|
||||
@@ -182,6 +182,7 @@ mod tests {
|
||||
original_pkgbuild: String::new(),
|
||||
conversion_status: ConversionStatus::Full,
|
||||
target: String::new(),
|
||||
split_packages: Vec::new(),
|
||||
},
|
||||
policy: PolicySection::default(),
|
||||
}
|
||||
|
||||
@@ -0,0 +1,438 @@
|
||||
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"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user