Add CUB package builder and include in all Red Bear OS configs
CUB (Red Bear OS Package Builder) is a Rust CLI tool that combines package management and building:
- RBPKGBUILD parser (TOML format) with full spec support
- Cookbook adapter converting RBPKGBUILD to recipe.toml
- PKGBUILD (Arch AUR) to RBPKGBUILD conversion with Linuxism detection
- Dependency mapping (Arch to Redox names)
- pkgar package creation integration
- Build environment setup with Cookbook env vars
- CLI with pacman-style shortcuts: -S, -Ss, -B, -G, -Pi, -Sua, -Sc, --import-aur
28 cub-lib tests passing. cub-cli compiles with local pkgutils.
Added cub = {} to redbear-desktop, redbear-full, redbear-minimal configs.
Created recipe symlink and updated integrate-redbear.sh.
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -15,3 +15,6 @@ redbear-release = {}
|
|||||||
|
|
||||||
# Terminal file manager (Midnight Commander port)
|
# Terminal file manager (Midnight Commander port)
|
||||||
mc = {}
|
mc = {}
|
||||||
|
|
||||||
|
# Package builder (cub -S/-B/-G CLI)
|
||||||
|
cub = {}
|
||||||
|
|||||||
@@ -30,6 +30,9 @@ firmware-loader = {}
|
|||||||
evdevd = {}
|
evdevd = {}
|
||||||
udev-shim = {}
|
udev-shim = {}
|
||||||
|
|
||||||
|
# Package builder (cub -S/-B/-G CLI)
|
||||||
|
cub = {}
|
||||||
|
|
||||||
# RBOS meta-package (dependencies, default config)
|
# RBOS meta-package (dependencies, default config)
|
||||||
redbear-meta = {}
|
redbear-meta = {}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ redbear-release = {}
|
|||||||
# Terminal file manager
|
# Terminal file manager
|
||||||
mc = {}
|
mc = {}
|
||||||
|
|
||||||
|
# Package builder
|
||||||
|
cub = {}
|
||||||
|
|
||||||
# Firmware loading
|
# Firmware loading
|
||||||
firmware-loader = {}
|
firmware-loader = {}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
[source]
|
||||||
|
path = "source"
|
||||||
|
|
||||||
|
[build]
|
||||||
|
template = "cargo"
|
||||||
|
cargopath = "cub-cli"
|
||||||
|
|
||||||
|
[package]
|
||||||
|
dependencies = ["pkgutils"]
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
[workspace]
|
||||||
|
resolver = "2"
|
||||||
|
members = [
|
||||||
|
"cub-lib",
|
||||||
|
"cub-cli",
|
||||||
|
]
|
||||||
|
default-members = [
|
||||||
|
"cub-cli",
|
||||||
|
]
|
||||||
|
|
||||||
|
[workspace.package]
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Red Bear OS Package Builder"
|
||||||
|
license = "MIT"
|
||||||
|
authors = ["Red Bear OS Contributors"]
|
||||||
|
repository = "https://gitlab.redox-os.org/redox-os/redox"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[workspace.dependencies]
|
||||||
|
serde = "1"
|
||||||
|
serde_derive = "1"
|
||||||
|
toml = "0.8.2"
|
||||||
|
thiserror = "2"
|
||||||
|
clap = { version = "4.3", features = ["cargo", "derive"] }
|
||||||
|
|
||||||
|
[patch.crates-io]
|
||||||
|
ring = { git = "https://gitlab.redox-os.org/redox-os/ring.git", branch = "redox-0.17.8" }
|
||||||
|
cc-11 = { git = "https://github.com/tea/cc-rs", branch="riscv-abi-arch-fix", package = "cc" }
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
[package]
|
||||||
|
name = "cub-cli"
|
||||||
|
default-run = "cub"
|
||||||
|
description = "Red Bear OS Package Builder CLI"
|
||||||
|
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "cub"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
cub-lib = { path = "../cub-lib" }
|
||||||
|
redox-pkg = { path = "../../../../../../recipes/core/pkgutils/source/pkg-lib", default-features = false, features = ["indicatif"] }
|
||||||
|
clap = { workspace = true }
|
||||||
|
termion = "4.0.6"
|
||||||
@@ -0,0 +1,801 @@
|
|||||||
|
use std::cell::RefCell;
|
||||||
|
use std::env;
|
||||||
|
use std::ffi::OsString;
|
||||||
|
use std::fs;
|
||||||
|
use std::io;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::process::Command;
|
||||||
|
use std::rc::Rc;
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
|
use clap::{Parser, Subcommand};
|
||||||
|
use cub::converter::{self, ConversionReport, ConversionResult};
|
||||||
|
use cub::error::CubError;
|
||||||
|
use cub::rbpkgbuild::RbPkgBuild;
|
||||||
|
use cub::rbsrcinfo::RbSrcInfo;
|
||||||
|
use cub::sandbox::SandboxConfig;
|
||||||
|
use pkg::callback::IndicatifCallback;
|
||||||
|
use pkg::{Library, PackageName};
|
||||||
|
|
||||||
|
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";
|
||||||
|
|
||||||
|
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> {
|
||||||
|
converter::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")]
|
||||||
|
#[command(arg_required_else_help = true)]
|
||||||
|
struct Cli {
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: 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 },
|
||||||
|
/// Build and install a local RBPKGBUILD directory
|
||||||
|
Build { dir: String },
|
||||||
|
/// Fetch a BUR recipe into the current directory
|
||||||
|
Get { 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 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> {
|
||||||
|
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> {
|
||||||
|
let callback = new_pkg_callback();
|
||||||
|
Library::new_local(
|
||||||
|
source_dir,
|
||||||
|
pubkey_dir,
|
||||||
|
&self.install_path,
|
||||||
|
&self.target,
|
||||||
|
callback,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
match cli.command {
|
||||||
|
Commands::Install { package } => install_package(&context, &package)?,
|
||||||
|
Commands::Search { query } => search_packages(&context, &query)?,
|
||||||
|
Commands::Build { dir } => build_local_dir(&context, Path::new(&dir))?,
|
||||||
|
Commands::Get { package } => fetch_bur_recipe(&package)?,
|
||||||
|
Commands::Inspect { target } => inspect_target(&context, &target)?,
|
||||||
|
Commands::ImportAur { target } => import_aur_target(&target)?,
|
||||||
|
Commands::UpdateAll => update_all(&context)?,
|
||||||
|
Commands::CleanCache => clean_cache()?,
|
||||||
|
}
|
||||||
|
|
||||||
|
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 Some(flag) = rest.first().and_then(|value| value.to_str()) else {
|
||||||
|
return Ok(collected);
|
||||||
|
};
|
||||||
|
|
||||||
|
match flag {
|
||||||
|
"-S" => rewrite_value_command(binary, rest, "install", "package"),
|
||||||
|
"-Ss" => rewrite_value_command(binary, rest, "search", "query"),
|
||||||
|
"-B" => rewrite_value_command(binary, rest, "build", "dir"),
|
||||||
|
"-G" => rewrite_value_command(binary, rest, "get", "package"),
|
||||||
|
"-Pi" => rewrite_value_command(binary, rest, "inspect", "target"),
|
||||||
|
"--import-aur" => rewrite_value_command(binary, rest, "import-aur", "target"),
|
||||||
|
"-Sua" => rewrite_flag_command(binary, rest, "update-all"),
|
||||||
|
"-Sc" => rewrite_flag_command(binary, rest, "clean-cache"),
|
||||||
|
_ => Ok(collected),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rewrite_value_command(
|
||||||
|
binary: 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, OsString::from(subcommand), 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>> {
|
||||||
|
let mut rewritten = vec![binary, 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>> {
|
||||||
|
let package_name = PackageName::new(package.to_string())?;
|
||||||
|
let mut library = context.open_library()?;
|
||||||
|
|
||||||
|
match library.install(vec![package_name.clone()], false) {
|
||||||
|
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>> {
|
||||||
|
let mut library = context.open_library()?;
|
||||||
|
let official_matches = library.search(query)?;
|
||||||
|
let bur_matches = search_cached_bur(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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if bur_matches.is_empty() {
|
||||||
|
println!("Cached BUR: no matches for {query:?}");
|
||||||
|
} else {
|
||||||
|
println!("Cached BUR:");
|
||||||
|
for entry in bur_matches {
|
||||||
|
if let Some(description) = &entry.description {
|
||||||
|
println!(" {} - {}", entry.name, description);
|
||||||
|
} else {
|
||||||
|
println!(" {}", entry.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_local_dir(context: &AppContext, dir: &Path) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let rbpkg_path = dir.join("RBPKGBUILD");
|
||||||
|
let rbpkg = RbPkgBuild::from_file(&rbpkg_path)?;
|
||||||
|
rbpkg.validate()?;
|
||||||
|
|
||||||
|
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], false)?;
|
||||||
|
let applied = apply_library_changes(&mut library)?;
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"Built and installed {} successfully ({} change(s)).",
|
||||||
|
rbpkg.package.name, applied
|
||||||
|
);
|
||||||
|
|
||||||
|
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);
|
||||||
|
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 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())?)?;
|
||||||
|
println!("{info:#?}");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn import_aur_target(target: &str) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
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(&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>> {
|
||||||
|
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 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))?;
|
||||||
|
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 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 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 base = env::temp_dir();
|
||||||
|
let nanos = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.map(|duration| duration.as_nanos())
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
for attempt in 0..128 {
|
||||||
|
let candidate = base.join(format!("{prefix}-{}-{nanos}-{attempt}", std::process::id()));
|
||||||
|
if !candidate.exists() {
|
||||||
|
fs::create_dir_all(&candidate)?;
|
||||||
|
return Ok(candidate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(io::Error::new(
|
||||||
|
io::ErrorKind::AlreadyExists,
|
||||||
|
format!("failed to allocate temporary directory for {prefix}"),
|
||||||
|
)
|
||||||
|
.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
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(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
[package]
|
||||||
|
name = "cub-lib"
|
||||||
|
description = "Red Bear OS Package Builder Library"
|
||||||
|
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "cub"
|
||||||
|
doctest = false
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde = { workspace = true }
|
||||||
|
serde_derive = { workspace = true }
|
||||||
|
toml = { workspace = true }
|
||||||
|
thiserror = { workspace = true }
|
||||||
|
hex = "0.4"
|
||||||
|
blake3 = "1"
|
||||||
|
walkdir = "2"
|
||||||
|
tempfile = "3"
|
||||||
|
|
||||||
|
# pkgar integration for package creation
|
||||||
|
pkgar = { version = "0.2.2", optional = true }
|
||||||
|
pkgar-core = { version = "0.2.2", optional = true }
|
||||||
|
pkgar-keys = { version = "0.2.2", optional = true }
|
||||||
|
|
||||||
|
# HTTP for source fetching
|
||||||
|
reqwest = { version = "0.12", default-features = false, features = ["blocking", "rustls-tls"], optional = true }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["full"]
|
||||||
|
full = ["pkgar", "pkgar-core", "pkgar-keys", "reqwest"]
|
||||||
@@ -0,0 +1,463 @@
|
|||||||
|
use crate::deps::map_dependency;
|
||||||
|
use crate::error::CubError;
|
||||||
|
use crate::rbpkgbuild::{
|
||||||
|
BuildSection, BuildTemplate, CompatSection, ConversionStatus, DependenciesSection,
|
||||||
|
InstallSection, PackageSection, PatchesSection, PolicySection, RbPkgBuild, SourceEntry,
|
||||||
|
SourceSection, SourceType,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct ConversionResult {
|
||||||
|
pub rbpkg: RbPkgBuild,
|
||||||
|
pub report: ConversionReport,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ConversionReport {
|
||||||
|
pub status: ConversionStatus,
|
||||||
|
pub warnings: Vec<String>,
|
||||||
|
pub actions_required: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn convert_pkgbuild(content: &str) -> Result<ConversionResult, CubError> {
|
||||||
|
let pkgname = extract_scalar_assignment(content, "pkgname")
|
||||||
|
.ok_or_else(|| CubError::Conversion("missing pkgname in PKGBUILD".to_string()))?;
|
||||||
|
let pkgver = extract_scalar_assignment(content, "pkgver")
|
||||||
|
.ok_or_else(|| CubError::Conversion("missing pkgver in PKGBUILD".to_string()))?;
|
||||||
|
|
||||||
|
let pkgrel = extract_scalar_assignment(content, "pkgrel")
|
||||||
|
.and_then(|value| value.parse::<u32>().ok())
|
||||||
|
.unwrap_or(1);
|
||||||
|
let pkgdesc = extract_scalar_assignment(content, "pkgdesc").unwrap_or_default();
|
||||||
|
let url = extract_scalar_assignment(content, "url").unwrap_or_default();
|
||||||
|
let licenses = extract_array_assignment(content, "license").unwrap_or_default();
|
||||||
|
let depends = extract_array_assignment(content, "depends").unwrap_or_default();
|
||||||
|
let makedepends = extract_array_assignment(content, "makedepends").unwrap_or_default();
|
||||||
|
let checkdepends = extract_array_assignment(content, "checkdepends").unwrap_or_default();
|
||||||
|
let sources = extract_array_assignment(content, "source").unwrap_or_default();
|
||||||
|
let sha256sums = extract_array_assignment(content, "sha256sums").unwrap_or_default();
|
||||||
|
|
||||||
|
let template = detect_build_template(content);
|
||||||
|
let mut warnings = detect_linuxisms(content);
|
||||||
|
let mut actions_required = Vec::new();
|
||||||
|
|
||||||
|
let mapped_runtime = map_dep_list(&depends, &mut warnings, &mut actions_required);
|
||||||
|
let mapped_build = map_dep_list(&makedepends, &mut warnings, &mut actions_required);
|
||||||
|
let mapped_check = map_dep_list(&checkdepends, &mut warnings, &mut actions_required);
|
||||||
|
|
||||||
|
if sources.is_empty() {
|
||||||
|
warnings.push("PKGBUILD does not define any source entries".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let status = if warnings.is_empty() && actions_required.is_empty() {
|
||||||
|
ConversionStatus::Full
|
||||||
|
} else {
|
||||||
|
ConversionStatus::Partial
|
||||||
|
};
|
||||||
|
|
||||||
|
let rbpkg = RbPkgBuild {
|
||||||
|
format: 1,
|
||||||
|
package: PackageSection {
|
||||||
|
name: sanitize_pkgname(&pkgname),
|
||||||
|
version: pkgver,
|
||||||
|
release: pkgrel,
|
||||||
|
description: pkgdesc,
|
||||||
|
homepage: url,
|
||||||
|
license: licenses,
|
||||||
|
architectures: vec!["x86_64-unknown-redox".to_string()],
|
||||||
|
maintainers: Vec::new(),
|
||||||
|
},
|
||||||
|
source: SourceSection {
|
||||||
|
sources: sources
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(index, source)| {
|
||||||
|
source_from_arch(source, sha256sums.get(index).map(String::as_str))
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
},
|
||||||
|
dependencies: DependenciesSection {
|
||||||
|
build: mapped_build,
|
||||||
|
runtime: mapped_runtime,
|
||||||
|
check: mapped_check,
|
||||||
|
optional: Vec::new(),
|
||||||
|
provides: Vec::new(),
|
||||||
|
conflicts: Vec::new(),
|
||||||
|
},
|
||||||
|
build: BuildSection {
|
||||||
|
template,
|
||||||
|
..BuildSection::default()
|
||||||
|
},
|
||||||
|
install: InstallSection::default(),
|
||||||
|
patches: PatchesSection::default(),
|
||||||
|
compat: CompatSection {
|
||||||
|
imported_from: "aur".to_string(),
|
||||||
|
original_pkgbuild: content.to_string(),
|
||||||
|
conversion_status: status.clone(),
|
||||||
|
target: "x86_64-unknown-redox".to_string(),
|
||||||
|
},
|
||||||
|
policy: PolicySection::default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
rbpkg.validate()?;
|
||||||
|
let _ = rbpkg.to_srcinfo();
|
||||||
|
|
||||||
|
Ok(ConversionResult {
|
||||||
|
rbpkg,
|
||||||
|
report: ConversionReport {
|
||||||
|
status,
|
||||||
|
warnings,
|
||||||
|
actions_required,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_dep_list(
|
||||||
|
deps: &[String],
|
||||||
|
warnings: &mut Vec<String>,
|
||||||
|
actions_required: &mut Vec<String>,
|
||||||
|
) -> Vec<String> {
|
||||||
|
let mut mapped = Vec::new();
|
||||||
|
|
||||||
|
for dep in deps {
|
||||||
|
let mapping = map_dependency(dep);
|
||||||
|
if mapping.mapped.is_empty() {
|
||||||
|
warnings.push(format!(
|
||||||
|
"dependency '{}' has no Redox mapping and was omitted",
|
||||||
|
mapping.original
|
||||||
|
));
|
||||||
|
actions_required.push(format!(
|
||||||
|
"port or replace dependency '{}' manually",
|
||||||
|
mapping.original
|
||||||
|
));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !mapping.is_exact {
|
||||||
|
warnings.push(format!(
|
||||||
|
"dependency '{}' mapped to '{}'",
|
||||||
|
mapping.original, mapping.mapped
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if !mapped.contains(&mapping.mapped) {
|
||||||
|
mapped.push(mapping.mapped);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mapped
|
||||||
|
}
|
||||||
|
|
||||||
|
fn detect_build_template(content: &str) -> BuildTemplate {
|
||||||
|
let lowered = content.to_ascii_lowercase();
|
||||||
|
|
||||||
|
if lowered.contains("cargo build") || lowered.contains("cargo install") {
|
||||||
|
BuildTemplate::Cargo
|
||||||
|
} else if lowered.contains("meson setup") || lowered.contains(" meson ") {
|
||||||
|
BuildTemplate::Meson
|
||||||
|
} else if lowered.contains("cmake") {
|
||||||
|
BuildTemplate::Cmake
|
||||||
|
} else if lowered.contains("./configure") || lowered.contains(" configure ") {
|
||||||
|
BuildTemplate::Configure
|
||||||
|
} else {
|
||||||
|
BuildTemplate::Custom
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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",
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut warnings = Vec::new();
|
||||||
|
for (needle, warning) in checks {
|
||||||
|
if lowered.contains(needle) {
|
||||||
|
warnings.push(warning.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
warnings
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sanitize_pkgname(name: &str) -> String {
|
||||||
|
name.trim_matches('"')
|
||||||
|
.to_ascii_lowercase()
|
||||||
|
.replace('_', "-")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn source_from_arch(entry: String, sha256: Option<&str>) -> SourceEntry {
|
||||||
|
let normalized = normalize_source_entry(&entry);
|
||||||
|
let source_type = if normalized.starts_with("git+")
|
||||||
|
|| normalized.starts_with("git://")
|
||||||
|
|| normalized.ends_with(".git")
|
||||||
|
{
|
||||||
|
SourceType::Git
|
||||||
|
} else {
|
||||||
|
SourceType::Tar
|
||||||
|
};
|
||||||
|
|
||||||
|
SourceEntry {
|
||||||
|
sha256: if matches!(source_type, SourceType::Tar) {
|
||||||
|
sha256.unwrap_or_default().to_string()
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
},
|
||||||
|
url: normalized,
|
||||||
|
source_type,
|
||||||
|
rev: String::new(),
|
||||||
|
branch: String::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_source_entry(entry: &str) -> String {
|
||||||
|
let stripped = entry
|
||||||
|
.split_once("::")
|
||||||
|
.map(|(_, value)| value)
|
||||||
|
.unwrap_or(entry)
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
stripped
|
||||||
|
.strip_prefix("git+")
|
||||||
|
.unwrap_or(stripped)
|
||||||
|
.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_scalar_assignment(content: &str, name: &str) -> Option<String> {
|
||||||
|
extract_assignment(content, name).map(|raw| parse_scalar(&raw))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_array_assignment(content: &str, name: &str) -> Option<Vec<String>> {
|
||||||
|
extract_assignment(content, name).map(|raw| parse_array(&raw))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_assignment(content: &str, name: &str) -> Option<String> {
|
||||||
|
let prefix = format!("{name}=");
|
||||||
|
let mut lines = content.lines();
|
||||||
|
|
||||||
|
while let Some(line) = lines.next() {
|
||||||
|
let trimmed = line.trim_start();
|
||||||
|
if !trimmed.starts_with(&prefix) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut value = trimmed[prefix.len()..].trim().to_string();
|
||||||
|
if value.starts_with('(') {
|
||||||
|
let mut depth = paren_balance(&value);
|
||||||
|
while depth > 0 {
|
||||||
|
let Some(next) = lines.next() else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
value.push('\n');
|
||||||
|
value.push_str(next.trim());
|
||||||
|
depth += paren_balance(next);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
while value.ends_with('\\') {
|
||||||
|
value.pop();
|
||||||
|
let Some(next) = lines.next() else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
value.push(' ');
|
||||||
|
value.push_str(next.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Some(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paren_balance(input: &str) -> i32 {
|
||||||
|
let opens = input.chars().filter(|ch| *ch == '(').count() as i32;
|
||||||
|
let closes = input.chars().filter(|ch| *ch == ')').count() as i32;
|
||||||
|
opens - closes
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_scalar(raw: &str) -> String {
|
||||||
|
let binding = strip_unquoted_comment(raw);
|
||||||
|
let stripped = binding.trim();
|
||||||
|
if let Some(unquoted) = unquote(stripped) {
|
||||||
|
unquoted
|
||||||
|
} else {
|
||||||
|
stripped.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_array(raw: &str) -> Vec<String> {
|
||||||
|
let binding = strip_unquoted_comment(raw);
|
||||||
|
let trimmed = binding.trim();
|
||||||
|
let inner = trimmed
|
||||||
|
.strip_prefix('(')
|
||||||
|
.and_then(|value| value.strip_suffix(')'))
|
||||||
|
.unwrap_or(trimmed);
|
||||||
|
|
||||||
|
shell_split(inner)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn strip_unquoted_comment(input: &str) -> String {
|
||||||
|
let mut single = false;
|
||||||
|
let mut double = false;
|
||||||
|
let mut result = String::new();
|
||||||
|
|
||||||
|
for ch in input.chars() {
|
||||||
|
match ch {
|
||||||
|
'\'' if !double => {
|
||||||
|
single = !single;
|
||||||
|
result.push(ch);
|
||||||
|
}
|
||||||
|
'"' if !single => {
|
||||||
|
double = !double;
|
||||||
|
result.push(ch);
|
||||||
|
}
|
||||||
|
'#' if !single && !double => break,
|
||||||
|
_ => result.push(ch),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unquote(value: &str) -> Option<String> {
|
||||||
|
if value.len() >= 2 {
|
||||||
|
let bytes = value.as_bytes();
|
||||||
|
let first = bytes[0] as char;
|
||||||
|
let last = bytes[value.len() - 1] as char;
|
||||||
|
if (first == '\'' && last == '\'') || (first == '"' && last == '"') {
|
||||||
|
return Some(value[1..value.len() - 1].to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shell_split(input: &str) -> Vec<String> {
|
||||||
|
let mut items = Vec::new();
|
||||||
|
let mut current = String::new();
|
||||||
|
let mut quote: Option<char> = None;
|
||||||
|
let mut escape = false;
|
||||||
|
|
||||||
|
for ch in input.chars() {
|
||||||
|
if escape {
|
||||||
|
current.push(ch);
|
||||||
|
escape = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
match ch {
|
||||||
|
'\\' => escape = true,
|
||||||
|
'\'' | '"' => {
|
||||||
|
if quote == Some(ch) {
|
||||||
|
quote = None;
|
||||||
|
} else if quote.is_none() {
|
||||||
|
quote = Some(ch);
|
||||||
|
} else {
|
||||||
|
current.push(ch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'#' if quote.is_none() => break,
|
||||||
|
ch if ch.is_whitespace() && quote.is_none() => {
|
||||||
|
if !current.is_empty() {
|
||||||
|
items.push(current.clone());
|
||||||
|
current.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => current.push(ch),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !current.is_empty() {
|
||||||
|
items.push(current);
|
||||||
|
}
|
||||||
|
|
||||||
|
items
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
const PKGBUILD: &str = r#"
|
||||||
|
pkgname=demo_pkg
|
||||||
|
pkgver=1.2.3
|
||||||
|
pkgrel=4
|
||||||
|
pkgdesc="Demo application"
|
||||||
|
url="https://example.com/demo"
|
||||||
|
license=('MIT')
|
||||||
|
depends=('glibc' 'openssl>=1.1' 'systemd')
|
||||||
|
makedepends=('cargo' 'pkg-config')
|
||||||
|
checkdepends=('python')
|
||||||
|
source=('https://example.com/demo-1.2.3.tar.xz')
|
||||||
|
sha256sums=('abc123deadbeef')
|
||||||
|
|
||||||
|
build() {
|
||||||
|
cargo build --release
|
||||||
|
}
|
||||||
|
|
||||||
|
package() {
|
||||||
|
install -Dm755 target/release/demo "$pkgdir/usr/bin/demo"
|
||||||
|
systemctl --version >/dev/null
|
||||||
|
}
|
||||||
|
"#;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn converts_pkgbuild_to_rbpkgbuild() {
|
||||||
|
let result = convert_pkgbuild(PKGBUILD).expect("convert PKGBUILD");
|
||||||
|
|
||||||
|
assert_eq!(result.rbpkg.package.name, "demo-pkg");
|
||||||
|
assert_eq!(result.rbpkg.package.version, "1.2.3");
|
||||||
|
assert_eq!(result.rbpkg.package.release, 4);
|
||||||
|
assert_eq!(result.rbpkg.build.template, BuildTemplate::Cargo);
|
||||||
|
assert_eq!(
|
||||||
|
result.rbpkg.dependencies.runtime,
|
||||||
|
vec!["relibc", "openssl3"]
|
||||||
|
);
|
||||||
|
assert_eq!(result.rbpkg.dependencies.build, vec!["cargo", "pkg-config"]);
|
||||||
|
assert_eq!(result.rbpkg.dependencies.check, vec!["python"]);
|
||||||
|
assert_eq!(result.rbpkg.source.sources.len(), 1);
|
||||||
|
assert_eq!(result.rbpkg.source.sources[0].sha256, "abc123deadbeef");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reports_linuxisms_and_unmapped_deps() {
|
||||||
|
let result = convert_pkgbuild(PKGBUILD).expect("convert PKGBUILD");
|
||||||
|
|
||||||
|
assert!(matches!(result.report.status, ConversionStatus::Partial));
|
||||||
|
assert!(result
|
||||||
|
.report
|
||||||
|
.warnings
|
||||||
|
.iter()
|
||||||
|
.any(|w| w.contains("systemctl")));
|
||||||
|
assert!(result
|
||||||
|
.report
|
||||||
|
.actions_required
|
||||||
|
.iter()
|
||||||
|
.any(|w| w.contains("systemd")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_multiline_arrays() {
|
||||||
|
let input = "depends=(\n 'glibc'\n 'zlib'\n)\n";
|
||||||
|
let parsed = extract_array_assignment(input, "depends").expect("depends array");
|
||||||
|
|
||||||
|
assert_eq!(parsed, vec!["glibc", "zlib"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn detects_meson_template() {
|
||||||
|
let input = "pkgname=demo\npkgver=1\nmeson setup build\n";
|
||||||
|
assert_eq!(detect_build_template(input), BuildTemplate::Meson);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,324 @@
|
|||||||
|
use serde_derive::Serialize;
|
||||||
|
|
||||||
|
use crate::error::CubError;
|
||||||
|
use crate::rbpkgbuild::{BuildTemplate, RbPkgBuild, SourceType};
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct CookbookRecipe {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
source: Option<CookbookSource>,
|
||||||
|
build: CookbookBuild,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
package: Option<CookbookPackage>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Serialize)]
|
||||||
|
struct CookbookSource {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
git: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
tar: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
branch: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
rev: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
blake3: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||||
|
patches: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct CookbookBuild {
|
||||||
|
template: String,
|
||||||
|
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||||
|
dependencies: Vec<String>,
|
||||||
|
#[serde(rename = "dev-dependencies", skip_serializing_if = "Vec::is_empty")]
|
||||||
|
dev_dependencies: Vec<String>,
|
||||||
|
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||||
|
cargoflags: Vec<String>,
|
||||||
|
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||||
|
configureflags: Vec<String>,
|
||||||
|
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||||
|
cmakeflags: Vec<String>,
|
||||||
|
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||||
|
mesonflags: Vec<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
script: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct CookbookPackage {
|
||||||
|
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||||
|
dependencies: Vec<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
version: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
description: Option<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 = rbpkg
|
||||||
|
.source
|
||||||
|
.sources
|
||||||
|
.first()
|
||||||
|
.map(convert_source)
|
||||||
|
.transpose()?
|
||||||
|
.map(|mut source| {
|
||||||
|
source.patches = rbpkg.patches.files.clone();
|
||||||
|
source
|
||||||
|
});
|
||||||
|
let build = convert_build(rbpkg)?;
|
||||||
|
let package = build_package_section(rbpkg);
|
||||||
|
|
||||||
|
toml::to_string_pretty(&CookbookRecipe {
|
||||||
|
source,
|
||||||
|
build,
|
||||||
|
package,
|
||||||
|
})
|
||||||
|
.map_err(CubError::from)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn convert_source(source: &crate::rbpkgbuild::SourceEntry) -> Result<CookbookSource, CubError> {
|
||||||
|
let mut cookbook = CookbookSource::default();
|
||||||
|
|
||||||
|
match source.source_type {
|
||||||
|
SourceType::Git => {
|
||||||
|
cookbook.git = Some(source.url.clone());
|
||||||
|
cookbook.branch = non_empty(&source.branch);
|
||||||
|
cookbook.rev = non_empty(&source.rev);
|
||||||
|
}
|
||||||
|
SourceType::Tar => {
|
||||||
|
cookbook.tar = Some(source.url.clone());
|
||||||
|
cookbook.blake3 = non_empty(&source.sha256);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(cookbook)
|
||||||
|
}
|
||||||
|
|
||||||
|
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(),
|
||||||
|
cargoflags: Vec::new(),
|
||||||
|
configureflags: Vec::new(),
|
||||||
|
cmakeflags: Vec::new(),
|
||||||
|
mesonflags: Vec::new(),
|
||||||
|
script: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
match rbpkg.build.template {
|
||||||
|
BuildTemplate::Cargo => {
|
||||||
|
if rbpkg.build.release {
|
||||||
|
build.cargoflags.push("--release".to_string());
|
||||||
|
}
|
||||||
|
if !rbpkg.build.features.is_empty() {
|
||||||
|
build.cargoflags.push("--features".to_string());
|
||||||
|
build.cargoflags.push(rbpkg.build.features.join(","));
|
||||||
|
}
|
||||||
|
build.cargoflags.extend(rbpkg.build.args.clone());
|
||||||
|
}
|
||||||
|
BuildTemplate::Configure => build.configureflags = rbpkg.build.args.clone(),
|
||||||
|
BuildTemplate::Cmake => build.cmakeflags = rbpkg.build.args.clone(),
|
||||||
|
BuildTemplate::Meson => build.mesonflags = rbpkg.build.args.clone(),
|
||||||
|
BuildTemplate::Custom => {
|
||||||
|
let script = custom_script(rbpkg)?;
|
||||||
|
build.script = Some(script);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(build)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_package_section(rbpkg: &RbPkgBuild) -> Option<CookbookPackage> {
|
||||||
|
let description = non_empty(&rbpkg.package.description);
|
||||||
|
let version = Some(if rbpkg.package.release > 0 {
|
||||||
|
format!("{}-{}", rbpkg.package.version, rbpkg.package.release)
|
||||||
|
} else {
|
||||||
|
rbpkg.package.version.clone()
|
||||||
|
});
|
||||||
|
|
||||||
|
if rbpkg.dependencies.runtime.is_empty() && description.is_none() && version.is_none() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(CookbookPackage {
|
||||||
|
dependencies: rbpkg.dependencies.runtime.clone(),
|
||||||
|
version,
|
||||||
|
description,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn custom_script(rbpkg: &RbPkgBuild) -> Result<String, CubError> {
|
||||||
|
let mut parts = Vec::new();
|
||||||
|
|
||||||
|
parts.extend(rbpkg.build.prepare.iter().cloned());
|
||||||
|
parts.extend(rbpkg.build.build_script.iter().cloned());
|
||||||
|
if rbpkg.policy.allow_tests {
|
||||||
|
parts.extend(rbpkg.build.check.iter().cloned());
|
||||||
|
}
|
||||||
|
parts.extend(rbpkg.build.install_script.iter().cloned());
|
||||||
|
|
||||||
|
if parts.is_empty() {
|
||||||
|
return Err(CubError::InvalidPkgbuild(
|
||||||
|
"custom template requires at least one prepare/build/check/install command".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(parts.join("\n"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn template_name(template: &BuildTemplate) -> &'static str {
|
||||||
|
match template {
|
||||||
|
BuildTemplate::Custom => "custom",
|
||||||
|
BuildTemplate::Cargo => "cargo",
|
||||||
|
BuildTemplate::Configure => "configure",
|
||||||
|
BuildTemplate::Cmake => "cmake",
|
||||||
|
BuildTemplate::Meson => "meson",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn non_empty(value: &str) -> Option<String> {
|
||||||
|
let trimmed = value.trim();
|
||||||
|
if trimmed.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(trimmed.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::rbpkgbuild::{
|
||||||
|
BuildSection, BuildTemplate, CompatSection, ConversionStatus, DependenciesSection,
|
||||||
|
InstallSection, PackageSection, PatchesSection, PolicySection, RbPkgBuild, SourceEntry,
|
||||||
|
SourceSection, SourceType,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn base_pkg(template: BuildTemplate) -> RbPkgBuild {
|
||||||
|
RbPkgBuild {
|
||||||
|
format: 1,
|
||||||
|
package: PackageSection {
|
||||||
|
name: "demo".to_string(),
|
||||||
|
version: "1.0.0".to_string(),
|
||||||
|
release: 1,
|
||||||
|
description: "demo package".to_string(),
|
||||||
|
homepage: String::new(),
|
||||||
|
license: vec!["MIT".to_string()],
|
||||||
|
architectures: vec!["x86_64-unknown-redox".to_string()],
|
||||||
|
maintainers: Vec::new(),
|
||||||
|
},
|
||||||
|
source: SourceSection {
|
||||||
|
sources: vec![SourceEntry {
|
||||||
|
source_type: SourceType::Git,
|
||||||
|
url: "https://example.com/repo.git".to_string(),
|
||||||
|
sha256: String::new(),
|
||||||
|
rev: "abc123".to_string(),
|
||||||
|
branch: "main".to_string(),
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
dependencies: DependenciesSection {
|
||||||
|
build: vec!["cargo".to_string()],
|
||||||
|
runtime: vec!["openssl3".to_string()],
|
||||||
|
check: vec!["python".to_string()],
|
||||||
|
optional: Vec::new(),
|
||||||
|
provides: Vec::new(),
|
||||||
|
conflicts: Vec::new(),
|
||||||
|
},
|
||||||
|
build: BuildSection {
|
||||||
|
template,
|
||||||
|
release: true,
|
||||||
|
features: vec!["cli".to_string(), "full".to_string()],
|
||||||
|
args: vec!["--locked".to_string()],
|
||||||
|
build_dir: String::new(),
|
||||||
|
prepare: vec!["./autogen.sh".to_string()],
|
||||||
|
build_script: vec!["make".to_string()],
|
||||||
|
check: vec!["make test".to_string()],
|
||||||
|
install_script: vec!["make install DESTDIR=\"${COOKBOOK_STAGE}\"".to_string()],
|
||||||
|
},
|
||||||
|
install: InstallSection::default(),
|
||||||
|
patches: PatchesSection {
|
||||||
|
files: vec!["redox.patch".to_string()],
|
||||||
|
},
|
||||||
|
compat: CompatSection {
|
||||||
|
imported_from: String::new(),
|
||||||
|
original_pkgbuild: String::new(),
|
||||||
|
conversion_status: ConversionStatus::Full,
|
||||||
|
target: String::new(),
|
||||||
|
},
|
||||||
|
policy: PolicySection::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn generates_cargo_recipe() {
|
||||||
|
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["source"]["git"].as_str(),
|
||||||
|
Some("https://example.com/repo.git")
|
||||||
|
);
|
||||||
|
assert_eq!(value["build"]["template"].as_str(), Some("cargo"));
|
||||||
|
assert_eq!(value["build"]["dependencies"][0].as_str(), Some("cargo"));
|
||||||
|
assert_eq!(value["source"]["patches"][0].as_str(), Some("redox.patch"));
|
||||||
|
assert_eq!(
|
||||||
|
value["package"]["dependencies"][0].as_str(),
|
||||||
|
Some("openssl3")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn generates_tar_recipe_with_checksum() {
|
||||||
|
let mut pkg = base_pkg(BuildTemplate::Cargo);
|
||||||
|
pkg.source.sources[0] = SourceEntry {
|
||||||
|
source_type: SourceType::Tar,
|
||||||
|
url: "https://example.com/demo.tar.gz".to_string(),
|
||||||
|
sha256: "abc123deadbeef".to_string(),
|
||||||
|
rev: String::new(),
|
||||||
|
branch: String::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let recipe = generate_recipe(&pkg).expect("generate recipe");
|
||||||
|
let value: toml::Value = toml::from_str(&recipe).expect("parse generated recipe");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
value["source"]["tar"].as_str(),
|
||||||
|
Some("https://example.com/demo.tar.gz")
|
||||||
|
);
|
||||||
|
assert_eq!(value["source"]["blake3"].as_str(), Some("abc123deadbeef"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn generates_custom_script() {
|
||||||
|
let recipe = generate_recipe(&base_pkg(BuildTemplate::Custom)).expect("generate recipe");
|
||||||
|
let value: toml::Value = toml::from_str(&recipe).expect("parse generated recipe");
|
||||||
|
let script = value["build"]["script"].as_str().expect("custom script");
|
||||||
|
|
||||||
|
assert!(script.contains("./autogen.sh"));
|
||||||
|
assert!(
|
||||||
|
script.contains("make\n") || script.ends_with("make") || script.contains("make test")
|
||||||
|
);
|
||||||
|
assert!(script.contains("make install"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn omits_test_commands_when_policy_disallows_them() {
|
||||||
|
let mut pkg = base_pkg(BuildTemplate::Custom);
|
||||||
|
pkg.policy.allow_tests = false;
|
||||||
|
|
||||||
|
let recipe = generate_recipe(&pkg).expect("generate recipe");
|
||||||
|
assert!(!recipe.contains("make test"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct MappedDep {
|
||||||
|
pub original: String,
|
||||||
|
pub mapped: String,
|
||||||
|
pub is_exact: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn map_dependency(arch_name: &str) -> MappedDep {
|
||||||
|
let cleaned = arch_name.trim();
|
||||||
|
let base = dependency_base_name(cleaned);
|
||||||
|
|
||||||
|
let (mapped, is_exact) = match base.as_str() {
|
||||||
|
"glibc" => ("relibc".to_string(), false),
|
||||||
|
"gcc" | "make" => ("build-base".to_string(), false),
|
||||||
|
"pkg-config" => ("pkg-config".to_string(), true),
|
||||||
|
"openssl" => ("openssl3".to_string(), false),
|
||||||
|
"zlib" => ("zlib".to_string(), true),
|
||||||
|
"libffi" => ("libffi".to_string(), true),
|
||||||
|
"pcre2" => ("pcre2".to_string(), true),
|
||||||
|
"ncurses" => ("ncurses".to_string(), true),
|
||||||
|
"readline" => ("readline".to_string(), true),
|
||||||
|
"curl" => ("curl".to_string(), true),
|
||||||
|
"git" => ("git".to_string(), true),
|
||||||
|
"python" => ("python".to_string(), true),
|
||||||
|
"rust" => ("rust".to_string(), true),
|
||||||
|
"cargo" => ("cargo".to_string(), true),
|
||||||
|
"cmake" => ("cmake".to_string(), true),
|
||||||
|
"meson" => ("meson".to_string(), true),
|
||||||
|
"autoconf" => ("autoconf".to_string(), true),
|
||||||
|
"automake" => ("automake".to_string(), true),
|
||||||
|
"libtool" => ("libtool".to_string(), true),
|
||||||
|
"systemd" => (String::new(), false),
|
||||||
|
"dbus" => ("dbus".to_string(), true),
|
||||||
|
_ => (base.clone(), true),
|
||||||
|
};
|
||||||
|
|
||||||
|
MappedDep {
|
||||||
|
original: cleaned.to_string(),
|
||||||
|
mapped,
|
||||||
|
is_exact,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn map_dependencies(arch_deps: &[String]) -> Vec<MappedDep> {
|
||||||
|
arch_deps.iter().map(|dep| map_dependency(dep)).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dependency_base_name(name: &str) -> String {
|
||||||
|
let trimmed = name.trim();
|
||||||
|
let no_prefix = trimmed.strip_prefix("host:").unwrap_or(trimmed);
|
||||||
|
|
||||||
|
no_prefix
|
||||||
|
.chars()
|
||||||
|
.take_while(|ch| !matches!(ch, '<' | '>' | '=' | ':' | ' ' | '\t'))
|
||||||
|
.collect::<String>()
|
||||||
|
.to_ascii_lowercase()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn maps_known_dependency() {
|
||||||
|
let mapped = map_dependency("glibc");
|
||||||
|
|
||||||
|
assert_eq!(mapped.original, "glibc");
|
||||||
|
assert_eq!(mapped.mapped, "relibc");
|
||||||
|
assert!(!mapped.is_exact);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn keeps_unknown_dependency_name() {
|
||||||
|
let mapped = map_dependency("expat");
|
||||||
|
|
||||||
|
assert_eq!(mapped.mapped, "expat");
|
||||||
|
assert!(mapped.is_exact);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn strips_version_constraints() {
|
||||||
|
let mapped = map_dependency("openssl>=1.1");
|
||||||
|
|
||||||
|
assert_eq!(mapped.original, "openssl>=1.1");
|
||||||
|
assert_eq!(mapped.mapped, "openssl3");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn marks_unavailable_dependency() {
|
||||||
|
let mapped = map_dependency("systemd");
|
||||||
|
|
||||||
|
assert!(mapped.mapped.is_empty());
|
||||||
|
assert!(!mapped.is_exact);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn maps_collections() {
|
||||||
|
let deps = vec!["glibc".to_string(), "cmake".to_string()];
|
||||||
|
let mapped = map_dependencies(&deps);
|
||||||
|
|
||||||
|
assert_eq!(mapped.len(), 2);
|
||||||
|
assert_eq!(mapped[0].mapped, "relibc");
|
||||||
|
assert_eq!(mapped[1].mapped, "cmake");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum CubError {
|
||||||
|
#[error("IO error: {0}")]
|
||||||
|
Io(#[from] std::io::Error),
|
||||||
|
#[error("TOML parse error: {0}")]
|
||||||
|
TomlParse(#[from] toml::de::Error),
|
||||||
|
#[error("TOML serialize error: {0}")]
|
||||||
|
TomlSerialize(#[from] toml::ser::Error),
|
||||||
|
#[error("Invalid RBPKGBUILD: {0}")]
|
||||||
|
InvalidPkgbuild(String),
|
||||||
|
#[error("Build failed: {0}")]
|
||||||
|
BuildFailed(String),
|
||||||
|
#[error("Package not found: {0}")]
|
||||||
|
PackageNotFound(String),
|
||||||
|
#[error("Conversion error: {0}")]
|
||||||
|
Conversion(String),
|
||||||
|
#[error("Dependency resolution failed: {0}")]
|
||||||
|
Dependency(String),
|
||||||
|
#[error("Sandbox error: {0}")]
|
||||||
|
Sandbox(String),
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
pub mod rbpkgbuild;
|
||||||
|
pub mod rbsrcinfo;
|
||||||
|
pub mod cookbook;
|
||||||
|
pub mod converter;
|
||||||
|
pub mod deps;
|
||||||
|
pub mod sandbox;
|
||||||
|
#[cfg(feature = "full")]
|
||||||
|
pub mod package;
|
||||||
|
pub mod error;
|
||||||
|
|
||||||
|
pub use error::CubError;
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use serde_derive::Serialize;
|
||||||
|
|
||||||
|
use crate::error::CubError;
|
||||||
|
use crate::rbpkgbuild::RbPkgBuild;
|
||||||
|
|
||||||
|
pub struct PackageCreator {
|
||||||
|
pub name: String,
|
||||||
|
pub version: String,
|
||||||
|
pub target: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PackageCreator {
|
||||||
|
pub fn create_from_stage(
|
||||||
|
stage_dir: &Path,
|
||||||
|
output_path: &Path,
|
||||||
|
secret_key_path: &Path,
|
||||||
|
) -> Result<(), CubError> {
|
||||||
|
if !stage_dir.is_dir() {
|
||||||
|
return Err(CubError::PackageNotFound(format!(
|
||||||
|
"stage directory does not exist: {}",
|
||||||
|
stage_dir.display()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
pkgar_keys::get_skey(secret_key_path).map_err(|err| {
|
||||||
|
CubError::BuildFailed(format!(
|
||||||
|
"failed to load pkgar secret key {}: {err}",
|
||||||
|
secret_key_path.display()
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
pkgar::folder_entries(stage_dir).map_err(|err| {
|
||||||
|
CubError::BuildFailed(format!(
|
||||||
|
"failed to scan stage directory {}: {err}",
|
||||||
|
stage_dir.display()
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let flags = pkgar_core::HeaderFlags::latest(
|
||||||
|
pkgar_core::Architecture::Independent,
|
||||||
|
pkgar_core::Packaging::Uncompressed,
|
||||||
|
);
|
||||||
|
pkgar::create_with_flags(secret_key_path, output_path, stage_dir, flags).map_err(|err| {
|
||||||
|
CubError::BuildFailed(format!(
|
||||||
|
"failed to create pkgar archive {}: {err}",
|
||||||
|
output_path.display()
|
||||||
|
))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn generate_package_toml(rbpkg: &RbPkgBuild) -> String {
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct PackageMetadata {
|
||||||
|
name: String,
|
||||||
|
version: String,
|
||||||
|
target: String,
|
||||||
|
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||||
|
depends: Vec<String>,
|
||||||
|
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||||
|
optdepends: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
let metadata = PackageMetadata {
|
||||||
|
name: rbpkg.package.name.clone(),
|
||||||
|
version: if rbpkg.package.release > 0 {
|
||||||
|
format!("{}-{}", rbpkg.package.version, rbpkg.package.release)
|
||||||
|
} else {
|
||||||
|
rbpkg.package.version.clone()
|
||||||
|
},
|
||||||
|
target: rbpkg
|
||||||
|
.package
|
||||||
|
.architectures
|
||||||
|
.first()
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| "x86_64-unknown-redox".to_string()),
|
||||||
|
depends: rbpkg.dependencies.runtime.clone(),
|
||||||
|
optdepends: rbpkg.dependencies.optional.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
match toml::to_string_pretty(&metadata) {
|
||||||
|
Ok(rendered) => rendered,
|
||||||
|
Err(_) => format!(
|
||||||
|
"name = \"{}\"\nversion = \"{}\"\ntarget = \"{}\"\n",
|
||||||
|
metadata.name, metadata.version, metadata.target
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::rbpkgbuild::{
|
||||||
|
BuildSection, CompatSection, ConversionStatus, DependenciesSection, InstallSection,
|
||||||
|
PackageSection, PatchesSection, PolicySection, RbPkgBuild, SourceSection,
|
||||||
|
};
|
||||||
|
use tempfile::tempdir;
|
||||||
|
|
||||||
|
fn sample_rbpkgbuild() -> RbPkgBuild {
|
||||||
|
RbPkgBuild {
|
||||||
|
format: 1,
|
||||||
|
package: PackageSection {
|
||||||
|
name: "demo".to_string(),
|
||||||
|
version: "1.0.0".to_string(),
|
||||||
|
release: 1,
|
||||||
|
description: "demo package".to_string(),
|
||||||
|
homepage: String::new(),
|
||||||
|
license: Vec::new(),
|
||||||
|
architectures: vec!["x86_64-unknown-redox".to_string()],
|
||||||
|
maintainers: Vec::new(),
|
||||||
|
},
|
||||||
|
source: SourceSection::default(),
|
||||||
|
dependencies: DependenciesSection {
|
||||||
|
build: Vec::new(),
|
||||||
|
runtime: vec!["openssl3".to_string()],
|
||||||
|
check: Vec::new(),
|
||||||
|
optional: Vec::new(),
|
||||||
|
provides: vec!["demo-virtual".to_string()],
|
||||||
|
conflicts: vec!["demo-old".to_string()],
|
||||||
|
},
|
||||||
|
build: BuildSection {
|
||||||
|
build_script: vec!["make".to_string()],
|
||||||
|
install_script: vec!["make install".to_string()],
|
||||||
|
..BuildSection::default()
|
||||||
|
},
|
||||||
|
install: InstallSection::default(),
|
||||||
|
patches: PatchesSection::default(),
|
||||||
|
compat: CompatSection {
|
||||||
|
imported_from: String::new(),
|
||||||
|
original_pkgbuild: String::new(),
|
||||||
|
conversion_status: ConversionStatus::Full,
|
||||||
|
target: String::new(),
|
||||||
|
},
|
||||||
|
policy: PolicySection::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn generates_package_toml() {
|
||||||
|
let mut rbpkg = sample_rbpkgbuild();
|
||||||
|
rbpkg.dependencies.optional = vec!["git".to_string()];
|
||||||
|
|
||||||
|
let rendered = PackageCreator::generate_package_toml(&rbpkg);
|
||||||
|
|
||||||
|
assert!(rendered.contains("name = \"demo\""));
|
||||||
|
assert!(rendered.contains("version = \"1.0.0-1\""));
|
||||||
|
assert!(rendered.contains("target = \"x86_64-unknown-redox\""));
|
||||||
|
assert!(rendered.contains("depends = [\"openssl3\"]"));
|
||||||
|
assert!(rendered.contains("optdepends = [\"git\"]"));
|
||||||
|
assert!(!rendered.contains("dependencies ="));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn errors_when_stage_dir_is_missing() {
|
||||||
|
let temp = tempdir().expect("tempdir");
|
||||||
|
let err = PackageCreator::create_from_stage(
|
||||||
|
&temp.path().join("missing-stage"),
|
||||||
|
&temp.path().join("out.pkgar"),
|
||||||
|
&temp.path().join("secret.toml"),
|
||||||
|
)
|
||||||
|
.expect_err("missing stage should fail");
|
||||||
|
|
||||||
|
assert!(matches!(err, CubError::PackageNotFound(_)));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,406 @@
|
|||||||
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use serde_derive::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::error::CubError;
|
||||||
|
use crate::rbsrcinfo::RbSrcInfo;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct RbPkgBuild {
|
||||||
|
pub format: u32,
|
||||||
|
pub package: PackageSection,
|
||||||
|
#[serde(default)]
|
||||||
|
pub source: SourceSection,
|
||||||
|
#[serde(default)]
|
||||||
|
pub dependencies: DependenciesSection,
|
||||||
|
#[serde(default)]
|
||||||
|
pub build: BuildSection,
|
||||||
|
#[serde(default)]
|
||||||
|
pub install: InstallSection,
|
||||||
|
#[serde(default)]
|
||||||
|
pub patches: PatchesSection,
|
||||||
|
#[serde(default)]
|
||||||
|
pub compat: CompatSection,
|
||||||
|
#[serde(default)]
|
||||||
|
pub policy: PolicySection,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct PackageSection {
|
||||||
|
pub name: String,
|
||||||
|
pub version: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub release: u32,
|
||||||
|
#[serde(default)]
|
||||||
|
pub description: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub homepage: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub license: Vec<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub architectures: Vec<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub maintainers: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct SourceSection {
|
||||||
|
#[serde(default)]
|
||||||
|
pub sources: Vec<SourceEntry>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct SourceEntry {
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
pub source_type: SourceType,
|
||||||
|
#[serde(default)]
|
||||||
|
pub url: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub sha256: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub rev: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub branch: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum SourceType {
|
||||||
|
Tar,
|
||||||
|
Git,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct DependenciesSection {
|
||||||
|
#[serde(default)]
|
||||||
|
pub build: Vec<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub runtime: Vec<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub check: Vec<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub optional: Vec<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub provides: Vec<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub conflicts: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct BuildSection {
|
||||||
|
#[serde(default)]
|
||||||
|
pub template: BuildTemplate,
|
||||||
|
#[serde(default)]
|
||||||
|
pub release: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
pub features: Vec<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub args: Vec<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub build_dir: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub prepare: Vec<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub build_script: Vec<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub check: Vec<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub install_script: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum BuildTemplate {
|
||||||
|
#[default]
|
||||||
|
Custom,
|
||||||
|
Cargo,
|
||||||
|
Configure,
|
||||||
|
Cmake,
|
||||||
|
Meson,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct InstallSection {
|
||||||
|
#[serde(default)]
|
||||||
|
pub bins: Vec<InstallEntry>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub libs: Vec<InstallEntry>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub headers: Vec<InstallEntry>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub docs: Vec<InstallEntry>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub man: Vec<InstallEntry>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct InstallEntry {
|
||||||
|
pub from: String,
|
||||||
|
pub to: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct PatchesSection {
|
||||||
|
#[serde(default)]
|
||||||
|
pub files: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct CompatSection {
|
||||||
|
#[serde(default)]
|
||||||
|
pub imported_from: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub original_pkgbuild: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub conversion_status: ConversionStatus,
|
||||||
|
#[serde(default)]
|
||||||
|
pub target: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum ConversionStatus {
|
||||||
|
#[default]
|
||||||
|
Full,
|
||||||
|
Partial,
|
||||||
|
Manual,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct PolicySection {
|
||||||
|
#[serde(default)]
|
||||||
|
pub allow_network: bool,
|
||||||
|
#[serde(default = "default_true")]
|
||||||
|
pub allow_tests: bool,
|
||||||
|
#[serde(default = "default_true")]
|
||||||
|
pub review_required: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_true() -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RbPkgBuild {
|
||||||
|
pub fn from_file(path: impl AsRef<Path>) -> Result<RbPkgBuild, CubError> {
|
||||||
|
let contents = fs::read_to_string(path)?;
|
||||||
|
Self::from_str(&contents)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_str(s: &str) -> Result<RbPkgBuild, CubError> {
|
||||||
|
let parsed: RbPkgBuild = toml::from_str(s)?;
|
||||||
|
parsed.validate()?;
|
||||||
|
Ok(parsed)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_toml(&self) -> Result<String, CubError> {
|
||||||
|
self.validate()?;
|
||||||
|
toml::to_string_pretty(self).map_err(CubError::from)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validate(&self) -> Result<(), CubError> {
|
||||||
|
if self.format != 1 {
|
||||||
|
return Err(CubError::InvalidPkgbuild(format!(
|
||||||
|
"unsupported format {}, expected 1",
|
||||||
|
self.format
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.package.name.is_empty() {
|
||||||
|
return Err(CubError::InvalidPkgbuild(
|
||||||
|
"package.name must not be empty".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if !valid_package_name(&self.package.name) {
|
||||||
|
return Err(CubError::InvalidPkgbuild(format!(
|
||||||
|
"package.name must match [a-z0-9-_]+: {}",
|
||||||
|
self.package.name
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.package.version.trim().is_empty() {
|
||||||
|
return Err(CubError::InvalidPkgbuild(
|
||||||
|
"package.version must not be empty".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self
|
||||||
|
.package
|
||||||
|
.architectures
|
||||||
|
.iter()
|
||||||
|
.any(|arch| arch == "x86_64-unknown-redox")
|
||||||
|
{
|
||||||
|
return Err(CubError::InvalidPkgbuild(
|
||||||
|
"package.architectures must include x86_64-unknown-redox".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
for source in &self.source.sources {
|
||||||
|
if source.url.trim().is_empty() {
|
||||||
|
return Err(CubError::InvalidPkgbuild(
|
||||||
|
"source entry url must not be empty".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if matches!(source.source_type, SourceType::Git) && source.url.contains(' ') {
|
||||||
|
return Err(CubError::InvalidPkgbuild(format!(
|
||||||
|
"git source url must not contain spaces: {}",
|
||||||
|
source.url
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i, source) in self.source.sources.iter().enumerate() {
|
||||||
|
match source.source_type {
|
||||||
|
SourceType::Tar => {
|
||||||
|
if source.sha256.is_empty() {
|
||||||
|
return Err(CubError::InvalidPkgbuild(format!(
|
||||||
|
"source[{}]: tar source requires sha256 checksum",
|
||||||
|
i
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SourceType::Git => {
|
||||||
|
if source.rev.is_empty() && source.branch.is_empty() {
|
||||||
|
// Warning only for MVP: some git sources intentionally track default branch.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if matches!(self.build.template, BuildTemplate::Custom)
|
||||||
|
&& self.build.prepare.is_empty()
|
||||||
|
&& self.build.build_script.is_empty()
|
||||||
|
&& self.build.install_script.is_empty()
|
||||||
|
&& self.install.bins.is_empty()
|
||||||
|
&& self.install.libs.is_empty()
|
||||||
|
&& self.install.headers.is_empty()
|
||||||
|
&& self.install.docs.is_empty()
|
||||||
|
&& self.install.man.is_empty()
|
||||||
|
{
|
||||||
|
return Err(CubError::InvalidPkgbuild(
|
||||||
|
"custom builds require prepare/build/install instructions".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_srcinfo(&self) -> RbSrcInfo {
|
||||||
|
RbSrcInfo::from_rbpkgbuild(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn valid_package_name(name: &str) -> bool {
|
||||||
|
name.chars()
|
||||||
|
.all(|ch| ch.is_ascii_lowercase() || ch.is_ascii_digit() || ch == '-' || ch == '_')
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use tempfile::NamedTempFile;
|
||||||
|
|
||||||
|
const SAMPLE_TOML: &str = r#"
|
||||||
|
format = 1
|
||||||
|
|
||||||
|
[package]
|
||||||
|
name = "demo-pkg"
|
||||||
|
version = "1.0.0"
|
||||||
|
release = 1
|
||||||
|
description = "demo package"
|
||||||
|
homepage = "https://example.com"
|
||||||
|
license = ["MIT"]
|
||||||
|
architectures = ["x86_64-unknown-redox", "aarch64-unknown-redox"]
|
||||||
|
maintainers = ["Red Bear OS"]
|
||||||
|
|
||||||
|
[source]
|
||||||
|
sources = [
|
||||||
|
{ type = "git", url = "https://example.com/repo.git", rev = "abc123", branch = "main" }
|
||||||
|
]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
build = ["cargo"]
|
||||||
|
runtime = ["openssl3"]
|
||||||
|
|
||||||
|
[build]
|
||||||
|
template = "cargo"
|
||||||
|
release = true
|
||||||
|
features = ["std"]
|
||||||
|
|
||||||
|
[policy]
|
||||||
|
allow_network = false
|
||||||
|
"#;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_valid_rbpkgbuild() {
|
||||||
|
let pkg = RbPkgBuild::from_str(SAMPLE_TOML).expect("parse RBPKGBUILD");
|
||||||
|
|
||||||
|
assert_eq!(pkg.format, 1);
|
||||||
|
assert_eq!(pkg.package.name, "demo-pkg");
|
||||||
|
assert_eq!(pkg.build.template, BuildTemplate::Cargo);
|
||||||
|
assert!(pkg.build.release);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_invalid_name() {
|
||||||
|
let invalid = SAMPLE_TOML.replace("demo-pkg", "DemoPkg");
|
||||||
|
let err = RbPkgBuild::from_str(&invalid).expect_err("invalid name should fail");
|
||||||
|
|
||||||
|
assert!(matches!(err, CubError::InvalidPkgbuild(_)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_missing_redox_architecture() {
|
||||||
|
let invalid = SAMPLE_TOML.replace(
|
||||||
|
"[\"x86_64-unknown-redox\", \"aarch64-unknown-redox\"]",
|
||||||
|
"[\"x86_64-unknown-linux-gnu\"]",
|
||||||
|
);
|
||||||
|
let err = RbPkgBuild::from_str(&invalid).expect_err("missing redox arch should fail");
|
||||||
|
|
||||||
|
assert!(matches!(err, CubError::InvalidPkgbuild(_)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_tar_source_without_sha256() {
|
||||||
|
let invalid = SAMPLE_TOML.replace(
|
||||||
|
r#"{ type = "git", url = "https://example.com/repo.git", rev = "abc123", branch = "main" }"#,
|
||||||
|
r#"{ type = "tar", url = "https://example.com/demo.tar.gz" }"#,
|
||||||
|
);
|
||||||
|
let err =
|
||||||
|
RbPkgBuild::from_str(&invalid).expect_err("tar source without sha256 should fail");
|
||||||
|
|
||||||
|
assert!(matches!(err, CubError::InvalidPkgbuild(_)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn round_trips_to_toml() {
|
||||||
|
let pkg = RbPkgBuild::from_str(SAMPLE_TOML).expect("parse RBPKGBUILD");
|
||||||
|
let toml = pkg.to_toml().expect("serialize RBPKGBUILD");
|
||||||
|
let reparsed = RbPkgBuild::from_str(&toml).expect("reparse RBPKGBUILD");
|
||||||
|
|
||||||
|
assert_eq!(reparsed.package.name, "demo-pkg");
|
||||||
|
assert_eq!(reparsed.build.features, vec!["std"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_from_file() {
|
||||||
|
let file = NamedTempFile::new().expect("temp file");
|
||||||
|
fs::write(file.path(), SAMPLE_TOML).expect("write RBPKGBUILD");
|
||||||
|
|
||||||
|
let pkg = RbPkgBuild::from_file(file.path()).expect("read RBPKGBUILD");
|
||||||
|
assert_eq!(pkg.package.version, "1.0.0");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn converts_to_srcinfo() {
|
||||||
|
let pkg = RbPkgBuild::from_str(SAMPLE_TOML).expect("parse RBPKGBUILD");
|
||||||
|
let srcinfo = pkg.to_srcinfo();
|
||||||
|
|
||||||
|
assert_eq!(srcinfo.pkgname, "demo-pkg");
|
||||||
|
assert_eq!(srcinfo.pkgver, "1.0.0");
|
||||||
|
assert_eq!(srcinfo.makedepends, vec!["cargo"]);
|
||||||
|
assert_eq!(srcinfo.depends, vec!["openssl3"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,226 @@
|
|||||||
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use crate::error::CubError;
|
||||||
|
use crate::rbpkgbuild::{RbPkgBuild, SourceType};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||||
|
pub struct RbSrcInfo {
|
||||||
|
pub pkgname: String,
|
||||||
|
pub pkgver: String,
|
||||||
|
pub pkgrel: u32,
|
||||||
|
pub pkgdesc: String,
|
||||||
|
pub arch: String,
|
||||||
|
pub depends: Vec<String>,
|
||||||
|
pub makedepends: Vec<String>,
|
||||||
|
pub source: Vec<String>,
|
||||||
|
pub sha256sums: Vec<String>,
|
||||||
|
pub provides: Vec<String>,
|
||||||
|
pub conflicts: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RbSrcInfo {
|
||||||
|
pub fn from_file(path: impl AsRef<Path>) -> Result<RbSrcInfo, CubError> {
|
||||||
|
let contents = fs::read_to_string(path)?;
|
||||||
|
Self::from_str(&contents)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_string(&self) -> String {
|
||||||
|
let mut lines = Vec::new();
|
||||||
|
|
||||||
|
push_scalar(&mut lines, "pkgname", &self.pkgname);
|
||||||
|
push_scalar(&mut lines, "pkgver", &self.pkgver);
|
||||||
|
lines.push(format!("pkgrel = {}", self.pkgrel));
|
||||||
|
push_scalar(&mut lines, "pkgdesc", &self.pkgdesc);
|
||||||
|
push_scalar(&mut lines, "arch", &self.arch);
|
||||||
|
|
||||||
|
push_list(&mut lines, "depends", &self.depends);
|
||||||
|
push_list(&mut lines, "makedepends", &self.makedepends);
|
||||||
|
push_list(&mut lines, "source", &self.source);
|
||||||
|
push_list(&mut lines, "sha256sums", &self.sha256sums);
|
||||||
|
push_list(&mut lines, "provides", &self.provides);
|
||||||
|
push_list(&mut lines, "conflicts", &self.conflicts);
|
||||||
|
|
||||||
|
let mut output = lines.join("\n");
|
||||||
|
output.push('\n');
|
||||||
|
output
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_rbpkgbuild(rb: &RbPkgBuild) -> Self {
|
||||||
|
let mut sha256sums = Vec::new();
|
||||||
|
let source = rb
|
||||||
|
.source
|
||||||
|
.sources
|
||||||
|
.iter()
|
||||||
|
.map(|entry| {
|
||||||
|
if matches!(entry.source_type, SourceType::Tar) && !entry.sha256.is_empty() {
|
||||||
|
sha256sums.push(entry.sha256.clone());
|
||||||
|
}
|
||||||
|
entry.url.clone()
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
pkgname: rb.package.name.clone(),
|
||||||
|
pkgver: rb.package.version.clone(),
|
||||||
|
pkgrel: rb.package.release,
|
||||||
|
pkgdesc: rb.package.description.clone(),
|
||||||
|
arch: rb
|
||||||
|
.package
|
||||||
|
.architectures
|
||||||
|
.first()
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| "x86_64-unknown-redox".to_string()),
|
||||||
|
depends: rb.dependencies.runtime.clone(),
|
||||||
|
makedepends: rb.dependencies.build.clone(),
|
||||||
|
source,
|
||||||
|
sha256sums,
|
||||||
|
provides: rb.dependencies.provides.clone(),
|
||||||
|
conflicts: rb.dependencies.conflicts.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_str(contents: &str) -> Result<RbSrcInfo, CubError> {
|
||||||
|
let mut info = RbSrcInfo::default();
|
||||||
|
|
||||||
|
for raw_line in contents.lines() {
|
||||||
|
let line = raw_line.trim();
|
||||||
|
if line.is_empty() || line.starts_with('#') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some((key, value)) = line.split_once('=') else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let key = key.trim();
|
||||||
|
let value = value.trim().trim_matches('"');
|
||||||
|
|
||||||
|
match key {
|
||||||
|
"pkgname" => info.pkgname = value.to_string(),
|
||||||
|
"pkgver" => info.pkgver = value.to_string(),
|
||||||
|
"pkgrel" => {
|
||||||
|
info.pkgrel = value.parse().map_err(|_| {
|
||||||
|
CubError::InvalidPkgbuild(format!("invalid pkgrel in .RBSRCINFO: {value}"))
|
||||||
|
})?
|
||||||
|
}
|
||||||
|
"pkgdesc" => info.pkgdesc = value.to_string(),
|
||||||
|
"arch" => info.arch = value.to_string(),
|
||||||
|
"depends" => info.depends.push(value.to_string()),
|
||||||
|
"makedepends" => info.makedepends.push(value.to_string()),
|
||||||
|
"source" => info.source.push(value.to_string()),
|
||||||
|
"sha256sums" => info.sha256sums.push(value.to_string()),
|
||||||
|
"provides" => info.provides.push(value.to_string()),
|
||||||
|
"conflicts" => info.conflicts.push(value.to_string()),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(info)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_scalar(lines: &mut Vec<String>, key: &str, value: &str) {
|
||||||
|
if !value.is_empty() {
|
||||||
|
lines.push(format!("{key} = {value}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_list(lines: &mut Vec<String>, key: &str, values: &[String]) {
|
||||||
|
for value in values {
|
||||||
|
if !value.is_empty() {
|
||||||
|
lines.push(format!("{key} = {value}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::rbpkgbuild::{
|
||||||
|
BuildSection, CompatSection, ConversionStatus, DependenciesSection, InstallSection,
|
||||||
|
PackageSection, PatchesSection, PolicySection, RbPkgBuild, SourceEntry, SourceSection,
|
||||||
|
SourceType,
|
||||||
|
};
|
||||||
|
use tempfile::NamedTempFile;
|
||||||
|
|
||||||
|
fn sample_rbpkgbuild() -> RbPkgBuild {
|
||||||
|
RbPkgBuild {
|
||||||
|
format: 1,
|
||||||
|
package: PackageSection {
|
||||||
|
name: "demo".to_string(),
|
||||||
|
version: "1.2.3".to_string(),
|
||||||
|
release: 4,
|
||||||
|
description: "Demo package".to_string(),
|
||||||
|
homepage: String::new(),
|
||||||
|
license: vec!["MIT".to_string()],
|
||||||
|
architectures: vec!["x86_64-unknown-redox".to_string()],
|
||||||
|
maintainers: Vec::new(),
|
||||||
|
},
|
||||||
|
source: SourceSection {
|
||||||
|
sources: vec![SourceEntry {
|
||||||
|
source_type: SourceType::Tar,
|
||||||
|
url: "https://example.com/demo.tar.xz".to_string(),
|
||||||
|
sha256: "abc123".to_string(),
|
||||||
|
rev: String::new(),
|
||||||
|
branch: String::new(),
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
dependencies: DependenciesSection {
|
||||||
|
build: vec!["cmake".to_string()],
|
||||||
|
runtime: vec!["zlib".to_string()],
|
||||||
|
check: Vec::new(),
|
||||||
|
optional: Vec::new(),
|
||||||
|
provides: vec!["demo-virtual".to_string()],
|
||||||
|
conflicts: vec!["demo-old".to_string()],
|
||||||
|
},
|
||||||
|
build: BuildSection::default(),
|
||||||
|
install: InstallSection::default(),
|
||||||
|
patches: PatchesSection::default(),
|
||||||
|
compat: CompatSection {
|
||||||
|
imported_from: String::new(),
|
||||||
|
original_pkgbuild: String::new(),
|
||||||
|
conversion_status: ConversionStatus::Full,
|
||||||
|
target: String::new(),
|
||||||
|
},
|
||||||
|
policy: PolicySection::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn converts_from_rbpkgbuild() {
|
||||||
|
let info = RbSrcInfo::from_rbpkgbuild(&sample_rbpkgbuild());
|
||||||
|
|
||||||
|
assert_eq!(info.pkgname, "demo");
|
||||||
|
assert_eq!(info.pkgver, "1.2.3");
|
||||||
|
assert_eq!(info.pkgrel, 4);
|
||||||
|
assert_eq!(info.depends, vec!["zlib"]);
|
||||||
|
assert_eq!(info.makedepends, vec!["cmake"]);
|
||||||
|
assert_eq!(info.sha256sums, vec!["abc123"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn serializes_and_parses_round_trip() {
|
||||||
|
let info = RbSrcInfo::from_rbpkgbuild(&sample_rbpkgbuild());
|
||||||
|
let rendered = info.to_string();
|
||||||
|
let reparsed = RbSrcInfo::from_str(&rendered).expect("parse .RBSRCINFO");
|
||||||
|
|
||||||
|
assert_eq!(reparsed, info);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_from_file() {
|
||||||
|
let file = NamedTempFile::new().expect("temp file");
|
||||||
|
fs::write(
|
||||||
|
file.path(),
|
||||||
|
"pkgname = demo\npkgver = 1.0.0\npkgrel = 1\narch = x86_64-unknown-redox\n",
|
||||||
|
)
|
||||||
|
.expect("write .RBSRCINFO");
|
||||||
|
|
||||||
|
let info = RbSrcInfo::from_file(file.path()).expect("read .RBSRCINFO");
|
||||||
|
assert_eq!(info.pkgname, "demo");
|
||||||
|
assert_eq!(info.pkgver, "1.0.0");
|
||||||
|
assert_eq!(info.pkgrel, 1);
|
||||||
|
assert_eq!(info.arch, "x86_64-unknown-redox");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
use std::collections::{BTreeSet, HashMap};
|
||||||
|
use std::fs;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use crate::error::CubError;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct SandboxConfig {
|
||||||
|
pub target: String,
|
||||||
|
pub gnu_target: String,
|
||||||
|
pub destdir: PathBuf,
|
||||||
|
pub prefix: String,
|
||||||
|
pub cores: u32,
|
||||||
|
pub allow_network: bool,
|
||||||
|
pub source_dir: PathBuf,
|
||||||
|
pub build_dir: PathBuf,
|
||||||
|
pub stage_dir: PathBuf,
|
||||||
|
pub sysroot_dir: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SandboxConfig {
|
||||||
|
pub fn new(source_dir: &Path) -> Self {
|
||||||
|
let root = source_dir.join(".cub-sandbox");
|
||||||
|
let build_dir = root.join("build");
|
||||||
|
let stage_dir = root.join("stage");
|
||||||
|
let sysroot_dir = root.join("sysroot");
|
||||||
|
|
||||||
|
Self {
|
||||||
|
target: "x86_64-unknown-redox".to_string(),
|
||||||
|
gnu_target: "x86_64-redox".to_string(),
|
||||||
|
destdir: stage_dir.clone(),
|
||||||
|
prefix: "/usr".to_string(),
|
||||||
|
cores: std::thread::available_parallelism()
|
||||||
|
.map(|count| count.get() as u32)
|
||||||
|
.unwrap_or(1),
|
||||||
|
allow_network: false,
|
||||||
|
source_dir: source_dir.to_path_buf(),
|
||||||
|
build_dir,
|
||||||
|
stage_dir,
|
||||||
|
sysroot_dir,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn env_vars(&self) -> HashMap<String, String> {
|
||||||
|
let mut env = HashMap::new();
|
||||||
|
let current_path = std::env::var("PATH").unwrap_or_default();
|
||||||
|
let tool_path = self.sysroot_dir.join("bin");
|
||||||
|
|
||||||
|
env.insert(
|
||||||
|
"COOKBOOK_SOURCE".to_string(),
|
||||||
|
self.source_dir.display().to_string(),
|
||||||
|
);
|
||||||
|
env.insert(
|
||||||
|
"COOKBOOK_STAGE".to_string(),
|
||||||
|
self.stage_dir.display().to_string(),
|
||||||
|
);
|
||||||
|
env.insert(
|
||||||
|
"COOKBOOK_SYSROOT".to_string(),
|
||||||
|
self.sysroot_dir.display().to_string(),
|
||||||
|
);
|
||||||
|
env.insert("COOKBOOK_TARGET".to_string(), self.target.clone());
|
||||||
|
env.insert(
|
||||||
|
"COOKBOOK_HOST_TARGET".to_string(),
|
||||||
|
"x86_64-unknown-linux-gnu".to_string(),
|
||||||
|
);
|
||||||
|
env.insert("COOKBOOK_MAKE_JOBS".to_string(), self.cores.to_string());
|
||||||
|
env.insert("DESTDIR".to_string(), self.stage_dir.display().to_string());
|
||||||
|
env.insert("TARGET".to_string(), self.target.clone());
|
||||||
|
env.insert("GNU_TARGET".to_string(), self.gnu_target.clone());
|
||||||
|
env.insert(
|
||||||
|
"PATH".to_string(),
|
||||||
|
if current_path.is_empty() {
|
||||||
|
tool_path.display().to_string()
|
||||||
|
} else {
|
||||||
|
format!("{}:{}", tool_path.display(), current_path)
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
env
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setup(&self) -> Result<(), CubError> {
|
||||||
|
for dir in [
|
||||||
|
&self.build_dir,
|
||||||
|
&self.stage_dir,
|
||||||
|
&self.sysroot_dir,
|
||||||
|
&self.destdir,
|
||||||
|
] {
|
||||||
|
fs::create_dir_all(dir).map_err(|err| {
|
||||||
|
CubError::Sandbox(format!("failed to create {}: {err}", dir.display()))
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cleanup(&self) -> Result<(), CubError> {
|
||||||
|
let mut dirs = BTreeSet::new();
|
||||||
|
dirs.insert(self.destdir.clone());
|
||||||
|
dirs.insert(self.stage_dir.clone());
|
||||||
|
dirs.insert(self.build_dir.clone());
|
||||||
|
dirs.insert(self.sysroot_dir.clone());
|
||||||
|
|
||||||
|
for dir in dirs.into_iter().rev() {
|
||||||
|
if dir.exists() {
|
||||||
|
fs::remove_dir_all(&dir).map_err(|err| {
|
||||||
|
CubError::Sandbox(format!("failed to remove {}: {err}", dir.display()))
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use tempfile::tempdir;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn builds_expected_defaults() {
|
||||||
|
let temp = tempdir().expect("tempdir");
|
||||||
|
let sandbox = SandboxConfig::new(temp.path());
|
||||||
|
|
||||||
|
assert_eq!(sandbox.target, "x86_64-unknown-redox");
|
||||||
|
assert_eq!(sandbox.gnu_target, "x86_64-redox");
|
||||||
|
assert_eq!(sandbox.prefix, "/usr");
|
||||||
|
assert!(sandbox.cores >= 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn exposes_cookbook_environment() {
|
||||||
|
let temp = tempdir().expect("tempdir");
|
||||||
|
let sandbox = SandboxConfig::new(temp.path());
|
||||||
|
let env = sandbox.env_vars();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
env.get("COOKBOOK_TARGET"),
|
||||||
|
Some(&"x86_64-unknown-redox".to_string())
|
||||||
|
);
|
||||||
|
assert_eq!(env.get("GNU_TARGET"), Some(&"x86_64-redox".to_string()));
|
||||||
|
assert!(env
|
||||||
|
.get("PATH")
|
||||||
|
.expect("PATH set")
|
||||||
|
.starts_with(&sandbox.sysroot_dir.join("bin").display().to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sets_up_and_cleans_directories() {
|
||||||
|
let temp = tempdir().expect("tempdir");
|
||||||
|
let sandbox = SandboxConfig::new(temp.path());
|
||||||
|
|
||||||
|
sandbox.setup().expect("setup sandbox");
|
||||||
|
assert!(sandbox.build_dir.exists());
|
||||||
|
assert!(sandbox.stage_dir.exists());
|
||||||
|
assert!(sandbox.sysroot_dir.exists());
|
||||||
|
|
||||||
|
sandbox.cleanup().expect("cleanup sandbox");
|
||||||
|
assert!(!sandbox.build_dir.exists());
|
||||||
|
assert!(!sandbox.stage_dir.exists());
|
||||||
|
assert!(!sandbox.sysroot_dir.exists());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -154,6 +154,7 @@ symlink "../../local/recipes/system/redbear-meta" "recipes/system/redbear-meta"
|
|||||||
symlink "../../local/recipes/system/udev-shim" "recipes/system/udev-shim"
|
symlink "../../local/recipes/system/udev-shim" "recipes/system/udev-shim"
|
||||||
symlink "../../local/recipes/core/ext4d" "recipes/core/ext4d"
|
symlink "../../local/recipes/core/ext4d" "recipes/core/ext4d"
|
||||||
symlink "../../local/recipes/tui/mc" "recipes/tui/mc"
|
symlink "../../local/recipes/tui/mc" "recipes/tui/mc"
|
||||||
|
symlink "../../local/recipes/system/cub" "recipes/system/cub"
|
||||||
status "Custom recipe symlinks ready"
|
status "Custom recipe symlinks ready"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
|
|||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
../../local/recipes/system/cub
|
||||||
Reference in New Issue
Block a user