use std::{ collections::BTreeSet, path::{Path, PathBuf}, }; use pkg::{InstallState, Package, PackageName, PackagePrefix, PackageState}; use pkgar::ext::PackageSrcExt; use pkgar_core::HeaderFlags; use crate::{ Error, config::CookConfig, cook::{cook_build::BuildResult, fetch, fs::*, pty::PtyOut}, log_to_pty, recipe::{BuildKind, CookRecipe, OptionalPackageRecipe}, }; pub fn package( recipe: &CookRecipe, build_result: &BuildResult, cook_config: &CookConfig, logger: &PtyOut, ) -> Result<(), String> { let name = &recipe.name; let target_dir = &recipe.target_dir(); let auto_deps = &build_result.auto_deps; if recipe.recipe.build.kind == BuildKind::None { // metapackages don't have stage dir and optional packages package_toml( target_dir.join("stage.toml"), recipe, None, None, recipe.recipe.package.dependencies.clone(), &auto_deps, )?; return Ok(()); } let secret_path = "build/id_ed25519.toml"; let public_path = "build/id_ed25519.pub.toml"; if !Path::new(secret_path).is_file() || !Path::new(public_path).is_file() { if !Path::new("build").is_dir() { create_dir(Path::new("build"))?; } let (public_key, secret_key) = pkgar_keys::SecretKeyFile::new(); public_key .save(public_path) .map_err(|err| format!("failed to save pkgar public key: {:?}", err))?; secret_key .save(secret_path) .map_err(|err| format!("failed to save pkgar secret key: {:?}", err))?; } let packages = recipe.recipe.get_packages_list(); for package in packages { let (stage_dir, package_file, package_meta) = package_stage_paths(package, target_dir); // Rebuild package if stage is newer if package_file.is_file() && !build_result.cached { log_to_pty!(logger, "DEBUG: updating '{}'", package_file.display()); remove_all(&package_file)?; if package_meta.is_file() { remove_all(&package_meta)?; } } if !package_file.is_file() { pkgar::create_with_flags( secret_path, package_file.to_str().unwrap(), stage_dir.to_str().unwrap(), HeaderFlags::latest( pkgar_core::Architecture::Independent, match cook_config.compressed { true => pkgar_core::Packaging::LZMA2, false => pkgar_core::Packaging::Uncompressed, }, ), ) .map_err(|err| format!("failed to create pkgar archive: {:?}", err))?; } let deps = if package.is_some() { BTreeSet::from([name.with_prefix(PackagePrefix::Any)]) } else { auto_deps.clone() }; if !package_meta.is_file() { let name = match package { Some(p) => PackageName::new(format!("{}.{}", name.name(), p.name)) .map_err(|e| format!("{}", e))?, None => name.clone(), }; let package_deps = match package { Some(p) => p .dependencies .iter() .map(|dep| { if dep.name().is_empty() { name.with_suffix(dep.suffix()) } else { dep.clone() } }) .collect(), None => recipe.recipe.package.dependencies.clone(), }; package_toml( package_meta, recipe, Some((Path::new(public_path), &package_file)), package, package_deps, &deps, )?; } } Ok(()) } pub fn package_toml( toml_path: PathBuf, recipe: &CookRecipe, package_file: Option<(&Path, &PathBuf)>, package_suffix: Option<&OptionalPackageRecipe>, mut package_deps: Vec, auto_deps: &BTreeSet, ) -> Result<(), String> { for dep in auto_deps.iter() { if !package_deps.contains(dep) { package_deps.push(dep.clone()); } } let (hash, network_size, storage_size) = if let Some((pkey_path, archive_path)) = package_file { use pkgar_core::PackageSrc; let pkey = pkgar_keys::PublicKeyFile::open(pkey_path) .map_err(|e| format!("Unable to read public key: {e:?}"))? .pkey; let mut package = pkgar::PackageFile::new(archive_path, &pkey).map_err(|e| { format!( "Unable to read packaged pkgar file {}: {e:?}", archive_path.display(), ) })?; let mt = std::fs::metadata(archive_path).map_err(|e| { format!( "Unable to read packaged pkgar file {}: {e:?}", archive_path.display(), ) })?; let package_size = mt.len(); let header = package.header(); let storage_size = match header.flags.packaging() { pkgar_core::Packaging::LZMA2 => { let mut size = header .total_size() .map_err(|e| Error::Pkgar(pkgar::Error::Core(e)))? as u64; let entries = package .read_entries() .map_err(|e| format!("Unable to get lzma entry: {e}"))?; for entry in entries { let data_reader = package .data_reader(&entry) .map_err(|e| format!("Unable to read lzma entry: {e}"))?; size += data_reader.unpacked_size; package .restore_reader(data_reader.into_inner()) .map_err(|e| format!("Unable to put lzma entry: {e}"))?; } size } _ => package_size, }; ( blake3::Hash::from_bytes(package.header().blake3) .to_hex() .to_string(), package_size, storage_size, ) } else { ("".into(), 0, 0) }; let ident_source = fetch::fetch_get_source_info(recipe)?; let package = Package { name: PackageName::new(get_package_name( recipe.name.without_prefix(), package_suffix, )) .unwrap(), version: recipe.guess_version().unwrap_or("TODO".into()), target: recipe.target.to_string(), blake3: hash, network_size, storage_size, depends: package_deps, commit_identifier: ident_source.commit_identifier, source_identifier: ident_source.source_identifier, time_identifier: ident_source.time_identifier, ..Default::default() }; serialize_and_write(&toml_path, &package)?; return Ok(()); } pub fn package_target(name: &PackageName) -> &'static str { if name.is_host() { redoxer::host_target() } else { redoxer::target() } } pub fn package_stage_paths( package: Option<&OptionalPackageRecipe>, target_dir: &Path, ) -> (PathBuf, PathBuf, PathBuf) { let mut target_dir = target_dir.to_path_buf(); if let Some(cross_target) = std::env::var("COOKBOOK_CROSS_TARGET").ok() { if cross_target != "" { // TODO: automatically pass COOKBOOK_CROSS_GNU_TARGET? target_dir = target_dir.join(cross_target) } } package_name_paths(package, &target_dir, "stage") } pub fn package_source_paths( package: Option<&OptionalPackageRecipe>, target_dir: &Path, ) -> (PathBuf, PathBuf, PathBuf) { package_name_paths(package, target_dir, "source") } fn package_name_paths( package: Option<&OptionalPackageRecipe>, target_dir: &Path, name: &str, ) -> (PathBuf, PathBuf, PathBuf) { let prefix_name = get_package_name(name, package); let package_stage = target_dir.join(&prefix_name); let package_file = package_stage.with_added_extension("pkgar"); let package_meta = package_stage.with_added_extension("toml"); (package_stage, package_file, package_meta) } pub fn get_package_name(name: &str, package: Option<&OptionalPackageRecipe>) -> String { get_package_name_inner(name, package.map(|p| p.name.as_str())) } fn get_package_name_inner(name: &str, package: Option<&str>) -> String { let mut prefix_name = name.to_string(); if let Some(package) = package { prefix_name.push('.'); prefix_name.push_str(package); } prefix_name } pub fn package_handle_push( state: &mut PackageState, archive_path: &Path, sysroot_dir: &Path, reinstall: bool, ) -> crate::Result { let archive_toml = archive_path.with_extension("toml"); let pkey_path = "build/id_ed25519.pub.toml"; let pkg_toml = Package::from_file(&archive_toml)?; match state.installed.get(&pkg_toml.name) { Some(s) if !reinstall && pkg_toml.blake3 == s.blake3 => Ok(true), Some(s) => { // "local" is what remote name from installer is hardcoded into let remote_name = "local".to_string(); let install_state = InstallState::from_package(&pkg_toml, remote_name, s.manual, s.dependents.clone()); // TODO: use pkgar::replace unless forced reinstall pkgar::extract(pkey_path, &archive_path, sysroot_dir)?; state.installed.insert(pkg_toml.name.clone(), install_state); Ok(false) } None => { // "local" is what remote name from installer is hardcoded into let remote_name = "local".to_string(); // TODO: Handle manual & depedents let install_state = InstallState::from_package(&pkg_toml, remote_name, true, BTreeSet::new()); pkgar::extract(pkey_path, &archive_path, sysroot_dir)?; // TODO: Inject dependencies // TODO: Check if we need to inject remote key state.installed.insert(pkg_toml.name.clone(), install_state); Ok(false) } } }