use std::cell::RefCell; 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; use cub::error::CubError; use cub::pkgbuild::{self, ConversionReport, ConversionResult}; use cub::rbpkgbuild::RbPkgBuild; use cub::rbsrcinfo::RbSrcInfo; use cub::sandbox::SandboxConfig; use cub::storage::CubStore; use pkg::callback::IndicatifCallback; use pkg::{Library, PackageName, PackageState}; use pkgar::ext::EntryExt; use pkgar::PackageFile; use pkgar_core::PackageSrc; const DEFAULT_TARGET: &str = "x86_64-unknown-redox"; const HOST_INSTALL_PATH: &str = "/tmp/pkg_install"; const REDOX_INSTALL_PATH: &str = "/"; const PKG_DOWNLOAD_DIR: &str = "/tmp/pkg_download/"; const CUB_CACHE_DIR: &str = "/tmp/cub_cache/"; const DEFAULT_BUR_REPO_URL: &str = "https://gitlab.redox-os.org/redox-os/bur.git"; const DEFAULT_AUR_BASE_URL: &str = "https://aur.archlinux.org"; const PUBLIC_KEY_FILE: &str = "id_ed25519.pub.toml"; const DEFAULT_SECRET_KEY_FILE: &str = "id_ed25519.toml"; const PACKAGES_HEAD_DIR: &str = "var/lib/packages"; const AUR_SYNC_STAMP_FILE: &str = "aur-sync.stamp"; struct CookbookAdapter; impl CookbookAdapter { fn write_recipe_dir(rbpkg: &RbPkgBuild, recipe_dir: &Path) -> Result<(), CubError> { fs::create_dir_all(recipe_dir)?; let recipe = cub::cookbook::generate_recipe(rbpkg)?; fs::write(recipe_dir.join("recipe.toml"), recipe)?; Ok(()) } } struct PkgbuildConverter; impl PkgbuildConverter { fn convert(content: &str) -> Result { pkgbuild::convert_pkgbuild(content) } } struct PackageCreator; impl PackageCreator { fn create_pkgar( stage_dir: &Path, output_path: &Path, secret_key_path: &Path, ) -> Result<(), CubError> { cub::package::PackageCreator::create_from_stage(stage_dir, output_path, secret_key_path) } fn generate_package_toml(rbpkg: &RbPkgBuild) -> String { cub::package::PackageCreator::generate_package_toml(rbpkg) } } #[derive(Debug, Parser)] #[command(name = "cub")] #[command(version)] #[command(about = "Red Bear OS Package Builder")] struct Cli { /// Disable the default TUI launcher when no subcommand is provided #[arg(short = 'T', long = "no-tui", global = true)] no_tui: bool, #[command(subcommand)] command: Option, } #[derive(Debug, Subcommand)] enum Commands { /// Install a package from the official repo or BUR Install { package: String }, /// Search packages in the official repo and cached BUR Search { query: String }, /// Show AUR package details Info { package: String }, /// Refresh cached package metadata Sync, /// Refresh metadata and update installed packages SystemUpgrade, /// Build and install a local RBPKGBUILD directory Build { dir: String }, /// Fetch a BUR recipe into the current directory Get { package: String }, /// Import an AUR package into ~/.cub/recipes GetAur { package: String }, /// Inspect an installed package or local RBPKGBUILD Inspect { target: String }, /// Convert an AUR PKGBUILD into an RBPKGBUILD tree ImportAur { target: String }, /// Update all installed packages UpdateAll, /// Remove an installed package Remove { package: String }, /// List installed packages QueryLocal, /// Show installed package details QueryInfo { package: String }, /// List files installed by a package QueryList { package: String }, /// Remove cub and pkg download caches CleanCache, } struct AppContext { install_path: PathBuf, target: String, } impl AppContext { fn new() -> Self { let install_path = if cfg!(target_os = "redox") { PathBuf::from(REDOX_INSTALL_PATH) } else { PathBuf::from(HOST_INSTALL_PATH) }; let target = if cfg!(target_os = "redox") { env::var("TARGET").unwrap_or_else(|_| DEFAULT_TARGET.to_string()) } else { DEFAULT_TARGET.to_string() }; Self { install_path, target, } } fn open_library(&self) -> Result { self.ensure_install_layout()?; let callback = new_pkg_callback(); Library::new(&self.install_path, &self.target, callback) } fn open_local_library( &self, source_dir: &Path, pubkey_dir: &Path, ) -> Result { self.ensure_install_layout()?; let callback = new_pkg_callback(); Library::new_local( source_dir, pubkey_dir, &self.install_path, &self.target, callback, ) } fn ensure_install_layout(&self) -> Result<(), pkg::backend::Error> { fs::create_dir_all(self.install_path.join("etc/pkg.d"))?; Ok(()) } } fn main() -> Result<(), Box> { let args = rewrite_shortcut_args(env::args_os())?; let cli = Cli::parse_from(args); let context = AppContext::new(); if let Some(command) = cli.command { run_command(&context, command)?; } else if cli.no_tui { print_help_text()?; } else { launch_tui_or_help()?; } Ok(()) } fn run_command(context: &AppContext, command: Commands) -> Result<(), Box> { match command { Commands::Install { package } => install_package(context, &package)?, Commands::Search { query } => search_packages(context, &query)?, Commands::Info { package } => show_aur_info(&package)?, Commands::Sync => sync_sources()?, Commands::SystemUpgrade => system_upgrade(context)?, Commands::Build { dir } => build_local_dir(context, Path::new(&dir))?, Commands::Get { package } => fetch_bur_recipe(&package)?, Commands::GetAur { package } => get_aur_recipe(&package)?, Commands::Inspect { target } => inspect_target(context, &target)?, Commands::ImportAur { target } => import_aur_target(&target)?, Commands::UpdateAll => update_all(context)?, Commands::Remove { package } => remove_package(context, &package)?, Commands::QueryLocal => query_local_packages(context)?, Commands::QueryInfo { package } => query_local_info(context, &package)?, Commands::QueryList { package } => query_local_files(context, &package)?, Commands::CleanCache => clean_cache()?, } Ok(()) } fn launch_tui_or_help() -> Result<(), Box> { #[cfg(feature = "tui")] { use std::io::IsTerminal; if io::stdin().is_terminal() && io::stdout().is_terminal() { if let Err(error) = cub_tui::run() { eprintln!("Failed to launch cub TUI: {error}"); print_help_text()?; } } else { print_help_text()?; } return Ok(()); } #[cfg(not(feature = "tui"))] { print_help_text() } } fn print_help_text() -> Result<(), Box> { let mut command = Cli::command(); command.print_help()?; println!(); Ok(()) } fn rewrite_shortcut_args( args: impl IntoIterator, ) -> Result, Box> { let collected: Vec = args.into_iter().collect(); if collected.len() <= 1 { return Ok(collected); } let binary = collected[0].clone(); let rest = &collected[1..]; let prefix_len = leading_global_flag_count(rest); let prefix = &rest[..prefix_len]; let tail = &rest[prefix_len..]; let Some(flag) = tail.first().and_then(|value| value.to_str()) else { return Ok(collected); }; let rewrite_value = |subcommand: &str, value_name: &str| { if prefix.is_empty() { rewrite_value_command(binary.clone(), tail, subcommand, value_name) } else { rewrite_value_command_with_prefix(binary.clone(), prefix, tail, subcommand, value_name) } }; let rewrite_flag = |subcommand: &str| { if prefix.is_empty() { rewrite_flag_command(binary.clone(), tail, subcommand) } else { rewrite_flag_command_with_prefix(binary.clone(), prefix, tail, subcommand) } }; match flag { "-S" => rewrite_value("install", "package"), "-Ss" => rewrite_value("search", "query"), "-Si" => rewrite_value("info", "package"), "-Sy" => rewrite_flag("sync"), "-Syu" => rewrite_flag("system-upgrade"), "-B" => rewrite_value("build", "dir"), "-G" => rewrite_value("get-aur", "package"), "-R" => rewrite_value("remove", "package"), "-Q" => rewrite_flag("query-local"), "-Qi" => rewrite_value("query-info", "package"), "-Ql" => rewrite_value("query-list", "package"), "-Pi" => rewrite_value("inspect", "target"), "--import-aur" => rewrite_value("import-aur", "target"), "-Sua" => rewrite_flag("update-all"), "-Sc" => rewrite_flag("clean-cache"), _ => Ok(collected), } } fn leading_global_flag_count(rest: &[OsString]) -> usize { let mut count = 0; while let Some(flag) = rest.get(count).and_then(|value| value.to_str()) { if matches!(flag, "-T" | "--no-tui") { count += 1; } else { break; } } count } fn rewrite_value_command( binary: OsString, rest: &[OsString], subcommand: &str, value_name: &str, ) -> Result, Box> { rewrite_value_command_with_prefix(binary, &[], rest, subcommand, value_name) } fn rewrite_value_command_with_prefix( binary: OsString, prefix: &[OsString], rest: &[OsString], subcommand: &str, value_name: &str, ) -> Result, Box> { let Some(value) = rest.get(1) else { return Err(io::Error::new( io::ErrorKind::InvalidInput, format!("missing {value_name} for {}", rest[0].to_string_lossy()), ) .into()); }; let mut rewritten = vec![binary]; rewritten.extend(prefix.iter().cloned()); rewritten.push(OsString::from(subcommand)); rewritten.push(value.clone()); rewritten.extend(rest.iter().skip(2).cloned()); Ok(rewritten) } fn rewrite_flag_command( binary: OsString, rest: &[OsString], subcommand: &str, ) -> Result, Box> { rewrite_flag_command_with_prefix(binary, &[], rest, subcommand) } fn rewrite_flag_command_with_prefix( binary: OsString, prefix: &[OsString], rest: &[OsString], subcommand: &str, ) -> Result, Box> { let mut rewritten = vec![binary]; rewritten.extend(prefix.iter().cloned()); rewritten.push(OsString::from(subcommand)); rewritten.extend(rest.iter().skip(1).cloned()); Ok(rewritten) } fn new_pkg_callback() -> Rc> { let mut callback = IndicatifCallback::new(); callback.set_interactive(true); Rc::new(RefCell::new(callback)) } fn install_package(context: &AppContext, package: &str) -> Result<(), Box> { if cfg!(not(target_os = "redox")) { println!("Searching AUR for {package}..."); let client = AurClient::new(); let results = client.search(package, Some("name-desc"))?; if results.is_empty() { return Err(format!("{package} not found in AUR").into()); } let exact = results.iter().find(|p| p.name == package); let pkg = exact.unwrap_or(&results[0]); if exact.is_none() && results.len() > 1 { println!("No exact match for '{package}'. Closest results:"); for (i, r) in results.iter().take(5).enumerate() { println!(" {}. {}/{} — {}", i + 1, r.name, r.version, r.description); } println!("Enter number to select (or 0 to skip): "); let mut choice = String::new(); io::stdin().read_line(&mut choice)?; if let Ok(n) = choice.trim().parse::() { if n > 0 && n <= results.len().min(5) { let selected = &results[n - 1]; println!("Selected: {}/{}", selected.name, selected.version); fetch_and_save_aur(selected)?; } } return Ok(()); } println!("Found: {}/{} — {}", pkg.name, pkg.version, pkg.description); println!("Would you like to fetch this package from AUR into ~/.cub/? [y/N]"); let mut answer = String::new(); io::stdin().read_line(&mut answer)?; if answer.trim().to_ascii_lowercase().starts_with('y') { fetch_and_save_aur(pkg)?; } return Ok(()); } let package_name = PackageName::new(package.to_string())?; let mut library = context.open_library()?; match library.install(vec![package_name.clone()]) { Ok(()) => { let applied = apply_library_changes(&mut library)?; println!( "Installed {} from the official repository ({} change(s)).", package, applied ); Ok(()) } Err(pkg::backend::Error::PackageNotFound(_)) => { println!( "{} was not found in the official repository. Trying BUR...", package ); let bur_dir = ensure_bur_package_dir(package)?; build_local_dir(context, &bur_dir) } Err(error) => Err(Box::new(error)), } } fn search_packages(context: &AppContext, query: &str) -> Result<(), Box> { if cfg!(target_os = "redox") { let mut library = context.open_library()?; let official_matches = library.search(query)?; if official_matches.is_empty() { println!("Official repo: no matches for {query:?}"); } else { println!("Official repo:"); for (name, score) in official_matches { println!(" {} ({score:.2})", name); } } } let bur_matches = search_cached_bur(query)?; if !bur_matches.is_empty() { println!("Cached BUR:"); for entry in bur_matches { if let Some(description) = &entry.description { println!(" {} - {}", entry.name, description); } else { println!(" {}", entry.name); } } } match AurClient::new().search(query, None) { Ok(aur_packages) if !aur_packages.is_empty() => { println!("AUR:"); for pkg in aur_packages { let desc = if pkg.description.len() > 60 { format!("{}...", &pkg.description[..57]) } else { pkg.description.clone() }; println!(" {}/{} ({}) [votes: {}]", pkg.name, pkg.version, desc, pkg.num_votes); } } Ok(_) => println!("AUR: no matches for {query:?}"), Err(e) => eprintln!("AUR search failed: {e}"), } Ok(()) } fn show_aur_info(package: &str) -> Result<(), Box> { let client = AurClient::new(); let packages = client.info(&[package])?; let aur_package = packages .into_iter() .find(|candidate| candidate.name == package) .ok_or_else(|| { CubError::PackageNotFound(format!("{package} not found in AUR info response")) })?; print_aur_package(&aur_package); Ok(()) } fn sync_sources() -> Result<(), Box> { let bur_dir = sync_bur_repo()?; let store = init_cub_store()?; let aur_client = AurClient::new(); let aur_status = match aur_client.search("a", Some("name")) { Ok(results) => format!("ok ({})", results.len()), Err(error) => { eprintln!("Warning: failed to refresh live AUR metadata: {error}"); format!("warning ({error})") } }; let stamp_path = store.sources_dir().join(AUR_SYNC_STAMP_FILE); fs::write( &stamp_path, format!( "synced_at_unix = {}\naur_status = {:?}\n", current_unix_timestamp(), aur_status ), )?; println!("Refreshed BUR cache at {}.", bur_dir.display()); println!( "Recorded AUR sync status and wrote sync stamp to {}.", stamp_path.display() ); Ok(()) } fn system_upgrade(context: &AppContext) -> Result<(), Box> { host_only_notice("system-upgrade")?; sync_sources()?; update_all(context) } fn build_local_dir(context: &AppContext, dir: &Path) -> Result<(), Box> { cook::cook_available()?; let rbpkg_path = dir.join("RBPKGBUILD"); 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)?; let sandbox = SandboxConfig::new(&work_dir); sandbox.setup()?; let mut command = Command::new("repo"); command.arg("cook"); command.arg(&recipe_dir); command.envs(sandbox.env_vars()); let status = command.status()?; if !status.success() { return Err(Box::new(CubError::BuildFailed(format!( "repo cook {} failed with status {status}", recipe_dir.display() )))); } let stage_dir = find_stage_dir(&sandbox, &work_dir)?; let secret_key_path = resolve_secret_key_path()?; let public_key_dir = resolve_public_key_dir(&secret_key_path)?; let local_repo_dir = work_dir.join("local-repo"); let target_repo_dir = local_repo_dir.join(&context.target); fs::create_dir_all(&target_repo_dir)?; let pkgar_path = target_repo_dir.join(format!("{}.pkgar", rbpkg.package.name)); PackageCreator::create_pkgar(&stage_dir, &pkgar_path, &secret_key_path)?; let package_toml_path = target_repo_dir.join(format!("{}.toml", rbpkg.package.name)); fs::write( package_toml_path, PackageCreator::generate_package_toml(&rbpkg), )?; let package_name = PackageName::new(rbpkg.package.name.clone())?; let mut library = context.open_local_library(&local_repo_dir, &public_key_dir)?; library.install(vec![package_name])?; let applied = apply_library_changes(&mut library)?; println!( "Built and installed {} successfully ({} change(s)).", rbpkg.package.name, applied ); Ok(()) } fn check_missing_dependencies( context: &AppContext, rbpkg: &RbPkgBuild, ) -> Result, Box> { let library = context.open_library()?; let installed: Vec = 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> { 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> { 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"); if !pkgbuild_path.exists() { return Err(Box::new(CubError::PackageNotFound(format!( "AUR repository cloned but PKGBUILD not found at {}. The package may be a split package or empty.", pkgbuild_path.display() )))); } 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_and_save_aur(pkg: &AurPackage) -> Result<(), Box> { match fetch_aur_to_store(&pkg.name) { Ok(()) => { println!("Recipe saved to ~/.cub/recipes/{}/", pkg.name); println!("Build with: cubl -B ~/.cub/recipes/{}/", pkg.name); Ok(()) } Err(e) => { eprintln!("Failed to fetch {name}: {e}", name = pkg.name); eprintln!("Try manual import: cubl --import-aur {name}", name = pkg.name); Err(e) } } } fn fetch_bur_recipe(package: &str) -> Result<(), Box> { let source_dir = ensure_bur_package_dir(package)?; let destination = env::current_dir()?.join(package); if destination.exists() { return Err(io::Error::new( io::ErrorKind::AlreadyExists, format!("destination already exists: {}", destination.display()), ) .into()); } copy_dir_recursive(&source_dir, &destination)?; println!( "Fetched BUR recipe {} to {}.", package, destination.display() ); Ok(()) } fn get_aur_recipe(package: &str) -> Result<(), Box> { validate_git_target(package)?; let repo_url = aur_repo_url(package); let clone_dir = create_temp_dir("cub-aur-get")?; 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 = fs::read_to_string(&pkgbuild_path)?; let conversion = PkgbuildConverter::convert(&pkgbuild)?; let store = init_cub_store()?; let recipe_path = cub::recipe::save_recipe_to_store(&conversion.rbpkg, &store)?; let recipe_dir = recipe_path.parent().ok_or_else(|| { CubError::BuildFailed(format!( "invalid saved recipe path {}", recipe_path.display() )) })?; fs::create_dir_all(recipe_dir.join("patches"))?; fs::create_dir_all(recipe_dir.join("import"))?; fs::write(recipe_dir.join("RBPKGBUILD"), conversion.rbpkg.to_toml()?)?; fs::write( recipe_dir.join(".RBSRCINFO"), RbSrcInfo::from_rbpkgbuild(&conversion.rbpkg).to_string(), )?; fs::write(recipe_dir.join("import").join("PKGBUILD"), pkgbuild)?; let srcinfo_path = clone_dir.join(".SRCINFO"); if srcinfo_path.is_file() { fs::copy(&srcinfo_path, recipe_dir.join("import").join(".SRCINFO"))?; } let report = render_conversion_report(&conversion.report); fs::write(recipe_dir.join("import").join("report.txt"), &report)?; println!( "Imported AUR package {} into {}.", conversion.rbpkg.package.name, recipe_dir.display() ); println!("Saved cookbook recipe to {}.", recipe_path.display()); println!("{report}"); Ok(()) } fn inspect_target(context: &AppContext, target: &str) -> Result<(), Box> { let path = Path::new(target); if path.exists() { inspect_rbpkgbuild_path(path)?; return Ok(()); } let mut library = context.open_library()?; let info = library.info(PackageName::new(target.to_string())?)?; print_local_package_info(target, &info); Ok(()) } fn import_aur_target(target: &str) -> Result<(), Box> { validate_git_target(target)?; let repo_url = aur_repo_url(target); let clone_dir = create_temp_dir("cub-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 = fs::read_to_string(&pkgbuild_path)?; let conversion = PkgbuildConverter::convert(&pkgbuild)?; let output_dir = env::current_dir()?.join(&conversion.rbpkg.package.name); fs::create_dir_all(&output_dir)?; fs::create_dir_all(output_dir.join("patches"))?; fs::create_dir_all(output_dir.join("import"))?; fs::write(output_dir.join("RBPKGBUILD"), conversion.rbpkg.to_toml()?)?; fs::write( output_dir.join(".RBSRCINFO"), RbSrcInfo::from_rbpkgbuild(&conversion.rbpkg).to_string(), )?; fs::write(output_dir.join("import").join("PKGBUILD"), pkgbuild)?; let report = render_conversion_report(&conversion.report); fs::write(output_dir.join("import").join("report.txt"), &report)?; println!("Imported AUR package into {}", output_dir.display()); println!("{report}"); Ok(()) } fn update_all(context: &AppContext) -> Result<(), Box> { host_only_notice("update-all")?; let mut library = context.open_library()?; library.update(Vec::new())?; let applied = apply_library_changes(&mut library)?; println!("Updated installed packages ({} change(s)).", applied); Ok(()) } fn remove_package(context: &AppContext, package: &str) -> Result<(), Box> { host_only_notice("remove")?; let package_name = PackageName::new(package.to_string())?; let mut library = context.open_library()?; library.uninstall(vec![package_name])?; let applied = apply_library_changes(&mut library)?; println!("Removed {} ({} change(s)).", package, applied); Ok(()) } fn query_local_packages(context: &AppContext) -> Result<(), Box> { host_only_notice("query")?; let library = context.open_library()?; let mut packages = library.get_installed_packages()?; packages.sort(); if packages.is_empty() { println!("No packages are currently installed."); return Ok(()); } for package in packages { println!("{}", package); } Ok(()) } fn query_local_info(context: &AppContext, package: &str) -> Result<(), Box> { host_only_notice("query-info")?; let mut library = context.open_library()?; let info = library.info(PackageName::new(package.to_string())?)?; print_local_package_info(package, &info); Ok(()) } fn query_local_files( context: &AppContext, package: &str, ) -> Result<(), Box> { host_only_notice("query-list")?; let package_name = PackageName::new(package.to_string())?; let library = context.open_library()?; let installed_packages = library.get_installed_packages()?; if !installed_packages.contains(&package_name) { return Err(Box::new(CubError::PackageNotFound(format!( "{package} is not installed under {}", context.install_path.display() )))); } let package_state = PackageState::from_sysroot(&context.install_path)?; let install_state = package_state.installed.get(&package_name).ok_or_else(|| { CubError::PackageNotFound(format!( "{package} is not installed under {}", context.install_path.display() )) })?; let repo_key = package_state .pubkeys .get(&install_state.remote) .ok_or_else(|| { CubError::BuildFailed(format!( "missing repository public key '{}' for installed package {}", install_state.remote, package )) })?; let head_path = context .install_path .join(PACKAGES_HEAD_DIR) .join(format!("{package}.pkgar_head")); let mut package_file = PackageFile::new(&head_path, &repo_key.pkey)?; let mut paths = Vec::new(); for entry in package_file.read_entries()? { let relative = entry.check_path()?; paths.push(format!("/{}", relative.display())); } paths.sort(); if paths.is_empty() { println!("{} has no recorded installed files.", package); } else { for path in paths { println!("{} {}", package, path); } } Ok(()) } fn clean_cache() -> Result<(), Box> { remove_dir_if_exists(Path::new(PKG_DOWNLOAD_DIR))?; remove_dir_if_exists(Path::new(CUB_CACHE_DIR))?; if let Ok(store) = CubStore::new() { if let Err(error) = store.cache_clean() { eprintln!("Failed to clean Cub store sources cache: {error}"); } } println!("Removed package caches from {PKG_DOWNLOAD_DIR} and {CUB_CACHE_DIR}."); Ok(()) } fn apply_library_changes(library: &mut Library) -> Result> { match library.apply() { Ok(changes) => Ok(changes), Err(error) => { if let Err(abort_error) = library.abort() { eprintln!("Failed to abort package transaction: {abort_error}"); } Err(Box::new(error)) } } } fn host_only_notice(command: &str) -> Result<(), Box> { if cfg!(not(target_os = "redox")) { return Err(format!( "'cubl {command}' requires Red Bear OS package database.\n\ On Linux, use:\n \ cubl search — search AUR\n \ cubl -G — fetch and convert AUR PKGBUILD\n \ cubl -B — cook recipe with host cross-compiler\n \ cubl -Si — show AUR package info" ).into()); } Ok(()) } fn print_aur_package(package: &AurPackage) { println!("Repository : AUR"); println!("Name : {}", package.name); println!("Version : {}", package.version); println!("Description : {}", empty_if_blank(&package.description)); println!("URL : {}", empty_if_blank(&package.url)); println!("Licenses : {}", join_strings(&package.license)); println!("Depends On : {}", join_strings(&package.depends)); println!("Make Depends : {}", join_strings(&package.makedepends)); println!("Optional Deps : {}", join_strings(&package.optdepends)); println!("Provides : {}", join_strings(&package.provides)); println!("Conflicts With : {}", join_strings(&package.conflicts)); println!("Votes : {}", package.num_votes); println!("Popularity : {:.5}", package.popularity); println!("Last Modified : {}", package.last_modified); println!( "Out Of Date : {}", match package.out_of_date { Some(true) => "yes", Some(false) => "no", None => "unknown", } ); } fn print_local_package_info(package: &str, info: &pkg::PackageInfo) { println!("Repository : {}", info.package.remote); println!("Name : {}", info.package.package.name); println!( "Version : {}", empty_if_blank(&info.package.package.version) ); println!( "Target : {}", empty_if_blank(&info.package.package.target) ); println!("Installed : {}", yes_no(info.installed)); println!( "Depends On : {}", join_package_names(&info.package.package.depends) ); println!( "Blake3 : {}", empty_if_blank(&info.package.package.blake3) ); println!("Storage Size : {}", info.package.package.storage_size); println!("Network Size : {}", info.package.package.network_size); println!( "Source ID : {}", empty_if_blank(&info.package.package.source_identifier) ); println!( "Commit ID : {}", empty_if_blank(&info.package.package.commit_identifier) ); println!( "Published At : {}", empty_if_blank(&info.package.package.time_identifier) ); if info.package.package.name.as_str() != package { println!("Requested As : {}", package); } } fn inspect_rbpkgbuild_path(path: &Path) -> Result<(), Box> { let rbpkg_path = if path.is_dir() { path.join("RBPKGBUILD") } else { path.to_path_buf() }; let rbpkg = RbPkgBuild::from_file(&rbpkg_path)?; println!("Package:"); println!(" name = {}", rbpkg.package.name); println!(" version = {}", rbpkg.package.version); println!(" release = {}", rbpkg.package.release); println!(" description = {}", rbpkg.package.description); println!(" homepage = {}", rbpkg.package.homepage); println!(" license = {:?}", rbpkg.package.license); println!(" architectures = {:?}", rbpkg.package.architectures); println!(" maintainers = {:?}", rbpkg.package.maintainers); println!("Source:"); for source in &rbpkg.source.sources { println!( " {:?}: url={}, rev={}, branch={}, sha256={}", source.source_type, source.url, source.rev, source.branch, source.sha256 ); } println!("Dependencies:"); println!(" build = {:?}", rbpkg.dependencies.build); println!(" runtime = {:?}", rbpkg.dependencies.runtime); println!(" check = {:?}", rbpkg.dependencies.check); println!(" optional = {:?}", rbpkg.dependencies.optional); println!(" provides = {:?}", rbpkg.dependencies.provides); println!(" conflicts = {:?}", rbpkg.dependencies.conflicts); println!("Build:"); println!(" template = {:?}", rbpkg.build.template); println!(" release = {}", rbpkg.build.release); println!(" features = {:?}", rbpkg.build.features); println!(" args = {:?}", rbpkg.build.args); println!(" build_dir = {}", rbpkg.build.build_dir); println!(" prepare = {:?}", rbpkg.build.prepare); println!(" build_script = {:?}", rbpkg.build.build_script); println!(" check = {:?}", rbpkg.build.check); println!(" install_script = {:?}", rbpkg.build.install_script); println!("Install:"); println!(" bins = {:?}", rbpkg.install.bins); println!(" libs = {:?}", rbpkg.install.libs); println!(" headers = {:?}", rbpkg.install.headers); println!(" docs = {:?}", rbpkg.install.docs); println!(" man = {:?}", rbpkg.install.man); println!("Patches:"); println!(" files = {:?}", rbpkg.patches.files); println!("Compat:"); println!(" imported_from = {}", rbpkg.compat.imported_from); println!(" conversion_status = {:?}", rbpkg.compat.conversion_status); println!(" target = {}", rbpkg.compat.target); println!("Policy:"); println!(" allow_network = {}", rbpkg.policy.allow_network); println!(" allow_tests = {}", rbpkg.policy.allow_tests); println!(" review_required = {}", rbpkg.policy.review_required); println!("Generated .RBSRCINFO:"); println!("{}", rbpkg.to_srcinfo().to_string()); Ok(()) } struct BurMatch { name: String, description: Option, } fn search_cached_bur(query: &str) -> Result, Box> { let repo_dir = bur_repo_dir(); if !repo_dir.exists() { return Ok(Vec::new()); } let mut matches = Vec::new(); let lowered_query = query.to_ascii_lowercase(); for entry in fs::read_dir(repo_dir)? { let entry = entry?; let path = entry.path(); if !path.is_dir() { continue; } let Some(name) = path.file_name().and_then(|value| value.to_str()) else { continue; }; if name == ".git" { continue; } let rbpkg_path = path.join("RBPKGBUILD"); let mut description = None; let mut matched = name.to_ascii_lowercase().contains(&lowered_query); if rbpkg_path.is_file() { if let Ok(pkg) = RbPkgBuild::from_file(&rbpkg_path) { if pkg .package .name .to_ascii_lowercase() .contains(&lowered_query) || pkg .package .description .to_ascii_lowercase() .contains(&lowered_query) { matched = true; } if !pkg.package.description.trim().is_empty() { description = Some(pkg.package.description); } } } if matched { matches.push(BurMatch { name: name.to_string(), description, }); } } matches.sort_by(|left, right| left.name.cmp(&right.name)); Ok(matches) } fn ensure_bur_package_dir(package: &str) -> Result> { let repo_dir = sync_bur_repo()?; let package_dir = repo_dir.join(package); if package_dir.is_dir() { Ok(package_dir) } else { Err(Box::new(CubError::PackageNotFound(format!( "{package} not found in BUR cache {}", repo_dir.display() )))) } } fn sync_bur_repo() -> Result> { let repo_dir = bur_repo_dir(); let parent = repo_dir .parent() .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "invalid BUR cache path"))?; fs::create_dir_all(parent)?; if repo_dir.join(".git").is_dir() { let status = Command::new("git") .arg("pull") .arg("--ff-only") .current_dir(&repo_dir) .status()?; if !status.success() { return Err(Box::new(CubError::BuildFailed(format!( "failed to update BUR cache at {}", repo_dir.display() )))); } } else { let status = Command::new("git") .arg("clone") .arg(default_bur_repo_url()) .arg(&repo_dir) .status()?; if !status.success() { return Err(Box::new(CubError::BuildFailed(format!( "failed to clone BUR repository into {}", repo_dir.display() )))); } } Ok(repo_dir) } fn init_cub_store() -> Result> { let store = CubStore::new()?; store.init()?; Ok(store) } fn default_bur_repo_url() -> String { env::var("CUB_BUR_REPO_URL").unwrap_or_else(|_| DEFAULT_BUR_REPO_URL.to_string()) } fn bur_repo_dir() -> PathBuf { PathBuf::from(CUB_CACHE_DIR).join("bur") } fn aur_repo_url(target: &str) -> String { if target.contains("://") || target.ends_with(".git") { target.to_string() } else { format!("{DEFAULT_AUR_BASE_URL}/{}.git", target) } } fn resolve_secret_key_path() -> Result> { if let Some(path) = env::var_os("CUB_PKGAR_SECRET_KEY") { let candidate = PathBuf::from(path); if candidate.is_file() { return Ok(candidate); } } let home = env::var_os("HOME").map(PathBuf::from); let candidates = [ home.as_ref() .map(|path| path.join(".pkg").join(DEFAULT_SECRET_KEY_FILE)), Some(PathBuf::from("/etc/pkg").join(DEFAULT_SECRET_KEY_FILE)), Some(PathBuf::from("/pkg").join(DEFAULT_SECRET_KEY_FILE)), Some(env::current_dir()?.join(DEFAULT_SECRET_KEY_FILE)), ]; for candidate in candidates.into_iter().flatten() { if candidate.is_file() { return Ok(candidate); } } Err(Box::new(CubError::BuildFailed( "could not locate a pkgar secret key; set CUB_PKGAR_SECRET_KEY".to_string(), ))) } fn resolve_public_key_dir(secret_key_path: &Path) -> Result> { if let Some(path) = env::var_os("CUB_PKGAR_PUBKEY_DIR") { let candidate = PathBuf::from(path); if candidate.join(PUBLIC_KEY_FILE).is_file() { return Ok(candidate); } } let Some(parent) = secret_key_path.parent() else { return Err(Box::new(CubError::BuildFailed(format!( "could not determine public key directory for {}", secret_key_path.display() )))); }; if parent.join(PUBLIC_KEY_FILE).is_file() { Ok(parent.to_path_buf()) } else { Err(Box::new(CubError::BuildFailed(format!( "missing {} in {}", PUBLIC_KEY_FILE, parent.display() )))) } } fn find_stage_dir( sandbox: &SandboxConfig, search_root: &Path, ) -> Result> { let direct_candidates = [ sandbox.stage_dir.clone(), sandbox.destdir.clone(), search_root.join("stage"), search_root.join("destdir"), ]; for candidate in direct_candidates { if directory_has_entries(&candidate)? { return Ok(candidate); } } let mut stack = vec![search_root.to_path_buf()]; while let Some(dir) = stack.pop() { for entry in fs::read_dir(&dir)? { let entry = entry?; let path = entry.path(); if !path.is_dir() { continue; } let Some(name) = path.file_name().and_then(|value| value.to_str()) else { continue; }; if matches!(name, "stage" | "destdir") && directory_has_entries(&path)? { return Ok(path); } stack.push(path); } } Err(Box::new(CubError::BuildFailed(format!( "unable to locate a populated stage directory under {}", search_root.display() )))) } fn directory_has_entries(path: &Path) -> Result { if !path.is_dir() { return Ok(false); } Ok(fs::read_dir(path)?.next().transpose()?.is_some()) } fn render_conversion_report(report: &ConversionReport) -> String { let mut output = String::new(); output.push_str(&format!("Conversion: {:?}\n", report.status)); if !report.warnings.is_empty() { output.push_str("\nWarnings:\n"); for warning in &report.warnings { output.push_str(&format!("- {warning}\n")); } } if !report.actions_required.is_empty() { output.push_str("\nActions required:\n"); for action in &report.actions_required { output.push_str(&format!("- {action}\n")); } } output } fn create_temp_dir(prefix: &str) -> Result> { let path = tempfile::Builder::new().prefix(prefix).tempdir()?.keep(); Ok(path) } fn copy_dir_recursive(src: &Path, dst: &Path) -> Result<(), Box> { fs::create_dir_all(dst)?; for entry in fs::read_dir(src)? { let entry = entry?; let entry_path = entry.path(); let destination_path = dst.join(entry.file_name()); if entry_path.is_dir() { copy_dir_recursive(&entry_path, &destination_path)?; } else { fs::copy(&entry_path, &destination_path)?; } } Ok(()) } fn remove_dir_if_exists(path: &Path) -> Result<(), io::Error> { if path.exists() { fs::remove_dir_all(path)?; } Ok(()) } fn current_unix_timestamp() -> u64 { SystemTime::now() .duration_since(UNIX_EPOCH) .map(|duration| duration.as_secs()) .unwrap_or(0) } fn join_strings(values: &[String]) -> String { if values.is_empty() { "None".to_string() } else { values.join(", ") } } fn join_package_names(values: &[PackageName]) -> String { if values.is_empty() { "None".to_string() } else { values .iter() .map(ToString::to_string) .collect::>() .join(", ") } } fn empty_if_blank(value: &str) -> &str { if value.trim().is_empty() { "None" } else { value } } fn yes_no(value: bool) -> &'static str { if value { "yes" } else { "no" } } fn validate_git_target(target: &str) -> Result<(), Box> { if target.trim_start().starts_with('-') { return Err(io::Error::new( io::ErrorKind::InvalidInput, format!("invalid git target: {target}"), ) .into()); } Ok(()) }