1501 lines
47 KiB
Rust
1501 lines
47 KiB
Rust
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<ConversionResult, CubError> {
|
|
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<Commands>,
|
|
}
|
|
|
|
#[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<Library, pkg::backend::Error> {
|
|
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<Library, pkg::backend::Error> {
|
|
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<dyn std::error::Error>> {
|
|
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<dyn std::error::Error>> {
|
|
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<dyn std::error::Error>> {
|
|
#[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<dyn std::error::Error>> {
|
|
let mut command = Cli::command();
|
|
command.print_help()?;
|
|
println!();
|
|
Ok(())
|
|
}
|
|
|
|
fn rewrite_shortcut_args(
|
|
args: impl IntoIterator<Item = OsString>,
|
|
) -> Result<Vec<OsString>, Box<dyn std::error::Error>> {
|
|
let collected: Vec<OsString> = 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<Vec<OsString>, Box<dyn std::error::Error>> {
|
|
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<Vec<OsString>, Box<dyn std::error::Error>> {
|
|
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<Vec<OsString>, Box<dyn std::error::Error>> {
|
|
rewrite_flag_command_with_prefix(binary, &[], rest, subcommand)
|
|
}
|
|
|
|
fn rewrite_flag_command_with_prefix(
|
|
binary: OsString,
|
|
prefix: &[OsString],
|
|
rest: &[OsString],
|
|
subcommand: &str,
|
|
) -> Result<Vec<OsString>, Box<dyn std::error::Error>> {
|
|
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<RefCell<IndicatifCallback>> {
|
|
let mut callback = IndicatifCallback::new();
|
|
callback.set_interactive(true);
|
|
Rc::new(RefCell::new(callback))
|
|
}
|
|
|
|
fn install_package(context: &AppContext, package: &str) -> Result<(), Box<dyn std::error::Error>> {
|
|
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::<usize>() {
|
|
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<dyn std::error::Error>> {
|
|
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<dyn std::error::Error>> {
|
|
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<dyn std::error::Error>> {
|
|
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<dyn std::error::Error>> {
|
|
host_only_notice("system-upgrade")?;
|
|
sync_sources()?;
|
|
update_all(context)
|
|
}
|
|
|
|
fn build_local_dir(context: &AppContext, dir: &Path) -> Result<(), Box<dyn std::error::Error>> {
|
|
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<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");
|
|
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<dyn std::error::Error>> {
|
|
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<dyn std::error::Error>> {
|
|
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<dyn std::error::Error>> {
|
|
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<dyn std::error::Error>> {
|
|
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<dyn std::error::Error>> {
|
|
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<dyn std::error::Error>> {
|
|
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<dyn std::error::Error>> {
|
|
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<dyn std::error::Error>> {
|
|
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<dyn std::error::Error>> {
|
|
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<dyn std::error::Error>> {
|
|
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<dyn std::error::Error>> {
|
|
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<usize, Box<dyn std::error::Error>> {
|
|
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<dyn std::error::Error>> {
|
|
if cfg!(not(target_os = "redox")) {
|
|
return Err(format!(
|
|
"'cubl {command}' requires Red Bear OS package database.\n\
|
|
On Linux, use:\n \
|
|
cubl search <query> — search AUR\n \
|
|
cubl -G <pkg> — fetch and convert AUR PKGBUILD\n \
|
|
cubl -B <dir> — cook recipe with host cross-compiler\n \
|
|
cubl -Si <pkg> — 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<dyn std::error::Error>> {
|
|
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<String>,
|
|
}
|
|
|
|
fn search_cached_bur(query: &str) -> Result<Vec<BurMatch>, Box<dyn std::error::Error>> {
|
|
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<PathBuf, Box<dyn std::error::Error>> {
|
|
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<PathBuf, Box<dyn std::error::Error>> {
|
|
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<CubStore, Box<dyn std::error::Error>> {
|
|
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<PathBuf, Box<dyn std::error::Error>> {
|
|
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<PathBuf, Box<dyn std::error::Error>> {
|
|
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<PathBuf, Box<dyn std::error::Error>> {
|
|
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<bool, io::Error> {
|
|
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<PathBuf, Box<dyn std::error::Error>> {
|
|
let path = tempfile::Builder::new().prefix(prefix).tempdir()?.keep();
|
|
Ok(path)
|
|
}
|
|
|
|
fn copy_dir_recursive(src: &Path, dst: &Path) -> Result<(), Box<dyn std::error::Error>> {
|
|
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::<Vec<_>>()
|
|
.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<dyn std::error::Error>> {
|
|
if target.trim_start().starts_with('-') {
|
|
return Err(io::Error::new(
|
|
io::ErrorKind::InvalidInput,
|
|
format!("invalid git target: {target}"),
|
|
)
|
|
.into());
|
|
}
|
|
|
|
Ok(())
|
|
}
|