use std::collections::{HashMap, VecDeque};
use std::io::Read;
use std::path::{Path, PathBuf};
use std::sync::{Arc, mpsc::Receiver};
use color_eyre::eyre::{self, eyre, Result, WrapErr};
use flate2::read::GzDecoder;
use serde::{Deserialize, Serialize};
use crate::dirs;
use crate::lfs;
use crate::plan::InstallationPlan;
use crate::models::*;
use crate::packages_stream;
use crate::repo::{RepoReleaseItem, RepoRevise, should_refresh_release_file, ReleaseStatus};
use crate::download::get_package_file_path;
#[cfg(unix)]
use crate::transaction::run_transaction_batch;
#[cfg(unix)]
#[allow(unused_imports)]
use crate::run;
pub const AUR_BASE_URL: &str = "https://aur.archlinux.org/cgit/aur.git/snapshot";
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct AurPackage {
#[serde(default, rename = "Name")]
pub name: String,
#[serde(default, rename = "PackageBase")]
pub package_base: String,
#[serde(default, rename = "Version")]
pub version: String,
#[serde(default, rename = "Description")]
pub description: Option<String>,
#[serde(default, rename = "URL")]
pub url: Option<String>,
#[serde(default, rename = "NumVotes")]
pub num_votes: u32,
#[serde(default, rename = "Popularity")]
pub popularity: f64,
#[serde(default, rename = "OutOfDate")]
pub out_of_date: Option<u64>,
#[serde(default, rename = "Maintainer")]
pub maintainer: Option<String>,
#[serde(default, rename = "Submitter")]
pub submitter: Option<String>,
#[serde(default, rename = "FirstSubmitted")]
pub first_submitted: u64,
#[serde(default, rename = "LastModified")]
pub last_modified: u64,
#[serde(default, rename = "OptDepends")]
pub opt_depends: Vec<String>,
#[serde(default, rename = "Depends")]
pub depends: Vec<String>,
#[serde(default, rename = "MakeDepends")]
pub make_depends: Vec<String>,
#[serde(default, rename = "CheckDepends")]
pub check_depends: Vec<String>,
#[serde(default, rename = "Conflicts")]
pub conflicts: Vec<String>,
#[serde(default, rename = "Provides")]
pub provides: Vec<String>,
#[serde(default, rename = "Replaces")]
pub replaces: Vec<String>,
#[serde(default, rename = "Groups")]
pub groups: Vec<String>,
#[serde(default, rename = "Keywords")]
pub keywords: Vec<String>,
#[serde(default, rename = "License")]
pub license: Vec<String>,
#[serde(default, rename = "CoMaintainers")]
pub co_maintainers: Vec<String>,
}
pub fn parse_aur_metadata(repo: &RepoRevise, _release_path: &PathBuf) -> Result<Vec<RepoReleaseItem>> {
let mut release_items = Vec::new();
let repo_dir = dirs::get_repo_dir(&repo);
let output_path = repo_dir.join("packages.txt");
let url = repo.index_url.clone();
let location = url.split('/').last().unwrap_or("packages-meta-ext-v1.json.gz").to_string();
let download_path = crate::mirror::Mirrors::url_to_cache_path(&url, &repo.repodata_name)
.with_context(|| format!("Failed to convert URL to cache path: {}", url))?;
let release_status = should_refresh_release_file(&download_path, repo)?;
let need_download = matches!(release_status, ReleaseStatus::NeedDownload | ReleaseStatus::NeedUpdate);
let need_convert = !lfs::exists_on_host(&output_path) || {
let repoindex_path = repo_dir.join("RepoIndex.json");
!lfs::exists_on_host(&repoindex_path)
};
release_items.push(RepoReleaseItem {
repo_revise: repo.clone(),
need_download,
need_convert,
arch: repo.arch.clone(),
url,
package_baseurl: AUR_BASE_URL.to_string(),
hash_type: "SHA256".to_string(),
location,
is_packages: true,
output_path,
download_path,
..Default::default()
});
Ok(release_items)
}
pub fn process_packages_content(data_rx: Receiver<Vec<u8>>, repo_dir: &PathBuf, revise: &RepoReleaseItem) -> Result<PackagesFileInfo> {
log::debug!("Starting to process AUR packages content for {} (hash: {}, size: {})", revise.location, revise.hash, revise.size);
validate_download_path(revise)?;
log::debug!("Creating ReceiverHasher with hash='{}', size={}", revise.hash, revise.size);
let receiver_reader = packages_stream::ReceiverHasher::new_with_size(
data_rx,
revise.hash.clone(),
revise.size.try_into().map_err(|e| eyre::eyre!("Failed to convert size {} to u64: {}", revise.size, e))?
);
let mut decoder = GzDecoder::new(receiver_reader);
let mut json_content = String::new();
decoder.read_to_string(&mut json_content)
.map_err(|e| eyre::eyre!("Failed to decompress and read AUR JSON: {}", e))?;
log::debug!("Successfully decompressed AUR JSON, size: {} bytes", json_content.len());
let aur_packages: Vec<AurPackage> = serde_json::from_str(&json_content)
.map_err(|e| eyre::eyre!("Failed to parse AUR JSON array: {}", e))?;
log::info!("Parsed {} AUR packages from JSON", aur_packages.len());
let mut derived_files = packages_stream::PackagesStreamline::new(revise, repo_dir, process_line)
.map_err(|e| eyre::eyre!("Failed to initialize PackagesStreamline for {}: {}", revise.location, e))?;
let mut nr_packages = 0;
let mut nr_provides = 0;
let mut nr_essentials = 0;
for aur_pkg in &aur_packages {
let package = convert_aur_to_package(aur_pkg, &revise.repo_revise.repodata_name)?;
write_package_to_stream(&package, &mut derived_files)?;
nr_packages += 1;
nr_provides += package.provides.len();
if package.pkgname == "filesystem" || package.pkgname == "base" {
nr_essentials += 1;
}
}
finalize_processing(&mut derived_files, repo_dir, revise, nr_packages, nr_provides, nr_essentials)
}
fn validate_download_path(revise: &RepoReleaseItem) -> Result<()> {
if !revise.need_download && revise.need_convert {
log::debug!("Processing already downloaded file: {}", revise.download_path.display());
if !lfs::exists_on_host(&revise.download_path) {
return Err(eyre::eyre!("Downloaded file does not exist: {}", revise.download_path.display()));
}
let metadata = lfs::metadata_on_host(&revise.download_path)
.map_err(|e| eyre::eyre!("Failed to get metadata for {}: {}", revise.download_path.display(), e))?;
if metadata.len() == 0 {
return Err(eyre::eyre!("Downloaded file is empty: {}", revise.download_path.display()));
}
log::debug!("Downloaded file size: {} bytes", metadata.len());
}
Ok(())
}
fn convert_aur_to_package(aur_pkg: &AurPackage, repodata_name: &str) -> Result<Package> {
let arch = "any".to_string();
let location = format!("{}.tar.gz", aur_pkg.package_base);
let mut package = Package {
pkgname: aur_pkg.name.clone(),
version: aur_pkg.version.clone(),
arch,
source: Some(aur_pkg.package_base.clone()),
location,
summary: aur_pkg.description.clone().unwrap_or_default(),
homepage: aur_pkg.url.clone().unwrap_or_default(),
maintainer: aur_pkg.maintainer.clone().unwrap_or_default(),
requires: aur_pkg.depends.clone(),
build_requires: aur_pkg.make_depends.clone(),
check_requires: aur_pkg.check_depends.clone(),
recommends: aur_pkg.opt_depends.clone(),
provides: aur_pkg.provides.clone(),
conflicts: aur_pkg.conflicts.clone(),
obsoletes: aur_pkg.replaces.clone(),
section: Some(aur_pkg.groups.join(", ")),
tag: Some(aur_pkg.keywords.join(", ")),
repodata_name: repodata_name.to_string(),
..Default::default()
};
package.pkgkey = crate::package::format_pkgkey(&package.pkgname, &package.version, &package.arch);
Ok(package)
}
fn write_dependency_field(
deps: &[String],
field_name: &str,
output: &mut String,
) {
let filtered: Vec<&str> = deps.iter()
.filter(|dep| !dep.is_empty())
.map(|s| s.as_str())
.collect();
if !filtered.is_empty() {
output.push_str(&format!("{}: {}\n", field_name, filtered.join(" ")));
}
}
fn write_package_to_stream(package: &Package, derived_files: &mut packages_stream::PackagesStreamline) -> Result<()> {
derived_files.output.push_str("\n");
derived_files.on_new_paragraph();
derived_files.output.push_str(&format!("pkgname: {}\n", package.pkgname));
derived_files.on_new_pkgname(&package.pkgname);
derived_files.output.push_str(&format!("version: {}\n", package.version));
derived_files.output.push_str(&format!("arch: {}\n", package.arch));
if let Some(ref source) = package.source {
derived_files.output.push_str(&format!("source: {}\n", source));
}
if !package.location.is_empty() {
derived_files.output.push_str(&format!("location: {}\n", package.location));
}
if !package.summary.is_empty() {
derived_files.output.push_str(&format!("summary: {}\n", package.summary));
}
if !package.homepage.is_empty() {
derived_files.output.push_str(&format!("homepage: {}\n", package.homepage));
}
if !package.maintainer.is_empty() {
derived_files.output.push_str(&format!("maintainer: {}\n", package.maintainer));
}
if let Some(ref section) = package.section {
if !section.is_empty() {
derived_files.output.push_str(&format!("section: {}\n", section));
}
}
if let Some(ref tag) = package.tag {
if !tag.is_empty() {
derived_files.output.push_str(&format!("tag: {}\n", tag));
}
}
write_dependency_field(&package.provides, "provides", &mut derived_files.output);
write_dependency_field(&package.requires, "requires", &mut derived_files.output);
write_dependency_field(&package.build_requires, "buildRequires", &mut derived_files.output);
write_dependency_field(&package.check_requires, "checkRequires", &mut derived_files.output);
write_dependency_field(&package.recommends, "recommends", &mut derived_files.output);
write_dependency_field(&package.conflicts, "conflicts", &mut derived_files.output);
write_dependency_field(&package.obsoletes, "obsoletes", &mut derived_files.output);
write_dependency_field(&package.requires_pre, "requiresPre", &mut derived_files.output);
write_dependency_field(&package.suggests, "suggests", &mut derived_files.output);
write_dependency_field(&package.enhances, "enhances", &mut derived_files.output);
write_dependency_field(&package.supplements, "supplements", &mut derived_files.output);
derived_files.on_output()
.map_err(|e| eyre::eyre!("Failed to write package output: {}", e))?;
Ok(())
}
fn finalize_processing(
derived_files: &mut packages_stream::PackagesStreamline,
_repo_dir: &PathBuf,
revise: &RepoReleaseItem,
nr_packages: usize,
nr_provides: usize,
nr_essentials: usize,
) -> Result<PackagesFileInfo> {
if !derived_files.current_pkgname.is_empty() {
log::debug!("Finalizing last package: {}", derived_files.current_pkgname);
derived_files.on_new_paragraph();
}
log::debug!("Finalizing processing for {}", revise.location);
derived_files.on_finish(revise)
.map_err(|e| eyre::eyre!("Failed to finalize processing for {}: {}", revise.location, e))?;
Ok(PackagesFileInfo {
filename: revise.location.clone(),
sha256sum: revise.hash.clone(),
datetime: String::new(),
size: revise.size as u64,
nr_packages,
nr_provides,
nr_essentials,
})
}
fn process_line(_line: &str, _derived_files: &mut packages_stream::PackagesStreamline) -> Result<()> {
Ok(())
}
pub fn is_aur_package(pkgkey: &str) -> bool {
if let Ok(package) = crate::package_cache::load_package_info(pkgkey) {
package.repodata_name == "aur"
} else {
false
}
}
fn extract_aur_source(
tarball_path: &Path,
pkgname: &str,
build_dir: &Path,
) -> Result<PathBuf> {
use std::fs::File;
let pkg_root_dir = build_dir.join(pkgname);
if lfs::exists_on_host(&pkg_root_dir) {
lfs::remove_dir_all(&pkg_root_dir)?;
}
lfs::create_dir_all_with_case_sensitivity(build_dir)?;
let tar_gz = File::open(tarball_path)
.with_context(|| format!("Failed to open tarball {}", tarball_path.display()))?;
let tar = flate2::read::GzDecoder::new(tar_gz);
let mut archive = tar::Archive::new(tar);
crate::tar_extract::unpack_tar_archive(&mut archive, build_dir, None)
.with_context(|| format!("Failed to unpack tarball {}", tarball_path.display()))?;
let pkgbuild_path = find_pkgbuild(&pkg_root_dir)?;
Ok(pkgbuild_path
.parent()
.ok_or_else(|| eyre!("PKGBUILD path has no parent directory"))?
.to_path_buf())
}
#[cfg(unix)]
fn run_makepkg(
pkgbase: &str,
pkg_build_dir: &Path,
build_dir: &Path,
env_root: &Path,
) -> Result<()> {
let log_file = build_dir.join(format!("{}.log", pkgbase));
std::fs::File::create(&log_file)?;
let log_file_str = log_file.to_str()
.ok_or_else(|| eyre!("Invalid UTF-8 in log file path"))?;
let pkg_build_dir_str = pkg_build_dir.to_str()
.ok_or_else(|| eyre!("Invalid UTF-8 in build directory path"))?;
println!("less {}", log_file.display());
let sh_path = crate::run::find_command_in_env_path("bash", env_root)
.map_err(|e| eyre!("Failed to find bash in environment: {}", e))?;
let makepkg_cmd = format!(
"if [ ! -x /usr/local/bin/makepkg ]; then
sed 's/if (( EUID == 0 )); then/if test $USER = root; then/' /usr/bin/makepkg > /usr/local/bin/makepkg && chmod +x /usr/local/bin/makepkg;
fi;
export PACMAN=true
cd {} && /usr/local/bin/makepkg --force --nodeps --nosign --skippgpcheck --noconfirm --noprogressbar --nocheck --config /etc/makepkg.conf > {} 2>&1",
pkg_build_dir_str,
log_file_str
);
let run_options = crate::run::RunOptions {
command: sh_path.to_string_lossy().to_string(),
args: vec!["-c".to_string(), makepkg_cmd],
chdir_to_env_root: false,
skip_namespace_isolation: false,
timeout: 0,
..Default::default()
};
match crate::run::fork_and_execute(env_root, &run_options) {
Ok(None) => {
log::info!("makepkg completed successfully for {}", pkgbase);
Ok(())
}
Ok(Some(_)) => {
unreachable!("Foreground process should not return PID")
}
Err(e) => {
eprintln!("makepkg failed for {}: {}", pkgbase, e);
Err(eyre!(
"makepkg failed for {}: {}",
pkgbase, e
))
}
}
}
fn prepare_aur_source_dir(
pkgkey: &str,
pkgbase: &str,
build_dir: &Path,
) -> Result<PathBuf> {
let git_dir_in_build = build_dir.join(pkgbase);
if git_dir_in_build.is_dir() && lfs::exists_on_host(&git_dir_in_build.join(".git")) {
log::info!("Using git directory directly from build dir: {}", git_dir_in_build.display());
Ok(git_dir_in_build)
} else {
let source_path_str = get_package_file_path(pkgkey)?;
let source_path = PathBuf::from(source_path_str);
extract_aur_source(&source_path, pkgbase, build_dir)
}
}
fn find_and_verify_built_packages(
pkgbase: &str,
pkgkeys: &[String],
aur_packages: &InstalledPackagesMap,
pkg_build_dir: &Path,
is_post_makepkg: bool,
) -> Result<Vec<(PathBuf, String)>> {
use crate::package;
let version = pkgkeys.first()
.and_then(|pkgkey| package::parse_pkgkey(pkgkey).ok())
.map(|(_name, version, _arch)| version)
.ok_or_else(|| eyre!("Failed to parse version from pkgkey"))?;
let all_built = match find_built_package(pkg_build_dir, pkgbase, &version) {
Ok(packages) => packages,
Err(_) if !is_post_makepkg => {
return Ok(Vec::new());
}
Err(e) => return Err(e),
};
let mapped = map_built_aur_packages(&all_built, pkgkeys, aur_packages)?;
let found_pkgkeys: std::collections::HashSet<String> = mapped
.iter()
.map(|(_path, original_key)| original_key.clone())
.collect();
let mut missing = Vec::new();
for pkgkey in pkgkeys {
if !found_pkgkeys.contains(pkgkey) {
if let Ok((name, version, _)) = package::parse_pkgkey(pkgkey) {
missing.push(format!("{} ({})", name, version));
}
}
}
if !missing.is_empty() {
if is_post_makepkg {
return Err(eyre!(
"Missing built packages for: {}",
missing.join(", ")
));
} else {
return Ok(Vec::new());
}
}
Ok(mapped)
}
#[cfg(unix)]
pub fn build_and_install_aur_packages(
plan: &mut crate::plan::InstallationPlan,
aur_packages: &InstalledPackagesMap,
) -> Result<InstalledPackagesMap> {
if aur_packages.is_empty() {
return Ok(HashMap::new());
}
log::info!("Building {} AUR packages", aur_packages.len());
let (base_to_pkgkeys, sorted_bases) = group_aur_packages_by_base(aur_packages)?;
let mut completed_aur_packages = HashMap::new();
let mut aur_mapping_all_rounds: HashMap<String, String> = HashMap::new();
let build_dir = dirs().user_aur_builds.clone();
std::fs::create_dir_all(&build_dir)?;
for (pkgbase, _depth) in sorted_bases {
if let Some(pkgkeys) = base_to_pkgkeys.get(&pkgbase) {
let mapped = build_aur_packages_for_base(
&pkgbase,
pkgkeys,
aur_packages,
&build_dir,
&plan.env_root,
)?;
let (mut this_round_aur_packages, this_round_pkgkey_mapping) =
unpack_link_built_aur_packages(
plan,
&mapped,
)?;
if !this_round_aur_packages.is_empty() {
postinstall_built_aur_round(
plan,
&mut this_round_aur_packages,
&this_round_pkgkey_mapping,
&mut aur_mapping_all_rounds,
)?;
completed_aur_packages.extend(this_round_aur_packages.into_iter());
}
}
}
if !plan.skipped_reinstalls.is_empty() && !aur_mapping_all_rounds.is_empty() {
fixup_installed_packages_values(
&aur_mapping_all_rounds,
&mut plan.skipped_reinstalls,
)
.with_context(|| {
"Failed to normalize dependency fields for skipped reinstalls using AUR mappings"
})?;
}
Ok(completed_aur_packages)
}
fn group_aur_packages_by_base(
aur_packages: &InstalledPackagesMap,
) -> Result<(
HashMap<String, Vec<String>>,
Vec<(String, u16)>,
)> {
let mut base_to_pkgkeys: HashMap<String, Vec<String>> = HashMap::new();
let mut base_min_depth: HashMap<String, u16> = HashMap::new();
for (pkgkey, info) in aur_packages {
let package = crate::package_cache::load_package_info(pkgkey)
.with_context(|| {
format!("Failed to load package info for AUR pkgkey {}", pkgkey)
})?;
let pkgbase = package
.source
.as_deref()
.unwrap_or_else(|| package.pkgname.as_str())
.to_string();
base_to_pkgkeys
.entry(pkgbase.clone())
.or_default()
.push(pkgkey.clone());
base_min_depth
.entry(pkgbase)
.and_modify(|depth| {
if info.depend_depth < *depth {
*depth = info.depend_depth;
}
})
.or_insert(info.depend_depth);
}
let mut sorted_bases: Vec<(String, u16)> = base_min_depth.into_iter().collect();
sorted_bases.sort_by_key(|(_, depth)| *depth);
Ok((base_to_pkgkeys, sorted_bases))
}
fn build_aur_packages_for_base(
pkgbase: &str,
pkgkeys: &[String],
aur_packages: &InstalledPackagesMap,
build_dir: &Path,
env_root: &Path,
) -> Result<Vec<(PathBuf, String)>> {
let rep_pkgkey = &pkgkeys[0];
log::info!(
"Building AUR pkgbase '{}' using representative pkgkey '{}'",
pkgbase,
rep_pkgkey
);
let pkg_build_dir = prepare_aur_source_dir(rep_pkgkey, pkgbase, build_dir)?;
let pre_check_mapped = find_and_verify_built_packages(
pkgbase,
pkgkeys,
aur_packages,
&pkg_build_dir,
false,
)?;
let mapped = if !pre_check_mapped.is_empty() {
log::info!("Found already built packages for pkgbase '{}', skipping build", pkgbase);
pre_check_mapped
} else {
run_makepkg(pkgbase, &pkg_build_dir, build_dir, env_root)?;
find_and_verify_built_packages(
pkgbase,
pkgkeys,
aur_packages,
&pkg_build_dir,
true,
)?
};
for (built_pkg, _) in &mapped {
log::info!("Built package: {}", built_pkg.display());
}
Ok(mapped)
}
fn unpack_link_built_aur_packages(
plan: &mut crate::plan::InstallationPlan,
mapped_packages: &[(PathBuf, String)],
) -> Result<(
InstalledPackagesMap,
HashMap<String, String>,
)> {
let mut this_round_aur_packages = std::collections::HashMap::new();
let mut this_round_pkgkey_mapping: HashMap<String, String> = HashMap::new();
for (built_pkg_path, original_key) in mapped_packages {
process_built_aur_package(
plan,
built_pkg_path,
original_key,
&mut this_round_aur_packages,
&mut this_round_pkgkey_mapping,
)?;
}
Ok((this_round_aur_packages, this_round_pkgkey_mapping))
}
fn unpack_and_link_package(
plan: &mut crate::plan::InstallationPlan,
built_pkg_path: &Path,
) -> Result<(String, String, crate::package::PackageLine)> {
let built_pkg_path_str = built_pkg_path.to_str().ok_or_else(|| {
eyre!(
"Invalid UTF-8 in built package path: {}",
built_pkg_path.display()
)
})?;
let final_dir = crate::store::unpack_mv_package(
built_pkg_path_str,
None,
Some(&plan.store_pkglines_by_pkgname),
)
.with_context(|| {
format!(
"Failed to unpack built package: {}",
built_pkg_path.display()
)
})?;
let pkgline = final_dir.file_name()
.and_then(|name| name.to_str())
.ok_or_else(|| eyre!("Invalid UTF-8 in package directory name: {}", final_dir.display()))?
.to_string();
let parsed = crate::package::parse_pkgline(&pkgline)
.map_err(|e| eyre!("Failed to parse package line: {}", e))?;
let actual_pkgkey = crate::package::format_pkgkey(&parsed.pkgname, &parsed.version, &parsed.arch);
let store_fs_dir = plan.store_root.join(&pkgline).join("fs");
crate::link::link_package(plan, &store_fs_dir)
.with_context(|| {
format!(
"Failed to link built package: {}",
built_pkg_path.display()
)
})?;
Ok((actual_pkgkey, pkgline, parsed))
}
fn validate_package_metadata(
built_pkg_path: &Path,
actual_pkgkey: &str,
) -> Result<(String, String, String)> {
let (act_name, act_version, act_arch) = infer_name_version_from_arch_pkgfile(built_pkg_path)
.with_context(|| format!("Failed to infer package metadata from filename: {}", built_pkg_path.display()))?;
let (actual_name, actual_version, actual_arch) = crate::package::parse_pkgkey(actual_pkgkey)
.map_err(|e| eyre!("Failed to parse actual pkgkey '{}': {}", actual_pkgkey, e))?;
if act_name != actual_name || act_version != actual_version {
return Err(eyre!(
"Package key mismatch for '{}': inferred '{}-{}' but unpacked package has '{}-{}'",
built_pkg_path.display(),
act_name, act_version,
actual_name, actual_version
));
}
if act_arch != actual_arch {
log::warn!(
"Architecture mismatch for '{}': filename indicates '{}' but package metadata says '{}'",
built_pkg_path.display(),
act_arch,
actual_arch
);
}
Ok((act_name, act_version, act_arch))
}
fn update_source_package_info(
plan: &mut crate::plan::InstallationPlan,
original_key: &str,
actual_pkgkey: &str,
pkgline: &str,
parsed: &crate::package::PackageLine,
act_name: &str,
source_name: &str,
this_round_aur_packages: &mut InstalledPackagesMap,
) -> Result<()> {
if let Some(source_info) = plan.new_pkgs.get_mut(original_key) {
let source_info_mut = Arc::make_mut(source_info);
if act_name == source_name {
source_info_mut.pkgline = pkgline.to_string();
source_info_mut.arch = parsed.arch.clone();
this_round_aur_packages.insert(actual_pkgkey.to_string(), Arc::clone(source_info));
} else {
source_info_mut.depends.insert(actual_pkgkey.to_string());
let mut split_info = (**source_info).clone();
split_info.pkgline = pkgline.to_string();
split_info.arch = parsed.arch.clone();
split_info.depends.clear();
split_info.rdepends.insert(original_key.to_string());
plan.new_pkgs.insert(actual_pkgkey.to_string(), Arc::new(split_info));
this_round_aur_packages.insert(actual_pkgkey.to_string(), plan.new_pkgs[actual_pkgkey].clone());
}
}
Ok(())
}
fn update_package_relationships(
plan: &mut crate::plan::InstallationPlan,
original_key: &str,
actual_pkgkey: &str,
pkgline: &str,
parsed: &crate::package::PackageLine,
act_name: &str,
this_round_aur_packages: &mut InstalledPackagesMap,
this_round_pkgkey_mapping: &mut HashMap<String, String>,
) -> Result<()> {
if original_key != actual_pkgkey {
this_round_pkgkey_mapping.insert(original_key.to_string(), actual_pkgkey.to_string());
}
let (source_name, _source_version, _source_arch) = crate::package::parse_pkgkey(original_key)
.map_err(|e| eyre!("Failed to parse source pkgkey '{}': {}", original_key, e))?;
update_source_package_info(
plan,
original_key,
actual_pkgkey,
pkgline,
parsed,
act_name,
&source_name,
this_round_aur_packages,
)?;
Ok(())
}
fn process_built_aur_package(
plan: &mut crate::plan::InstallationPlan,
built_pkg_path: &PathBuf,
original_key: &String,
this_round_aur_packages: &mut InstalledPackagesMap,
this_round_pkgkey_mapping: &mut HashMap<String, String>,
) -> Result<()> {
let (actual_pkgkey, pkgline, parsed) = unpack_and_link_package(plan, built_pkg_path.as_path())?;
let (act_name, _act_version, _act_arch) = validate_package_metadata(built_pkg_path.as_path(), &actual_pkgkey)?;
update_package_relationships(
plan,
original_key.as_str(),
&actual_pkgkey,
&pkgline,
&parsed,
&act_name,
this_round_aur_packages,
this_round_pkgkey_mapping,
)?;
Ok(())
}
fn map_built_aur_packages(
built_pkg_paths: &[PathBuf],
pkgkeys: &[String],
aur_packages: &InstalledPackagesMap,
) -> Result<Vec<(PathBuf, String)>> {
use crate::package;
let mut namever2entry: HashMap<(String, String), (String, Arc<InstalledPackageInfo>)> =
HashMap::new();
for original_pkgkey in pkgkeys {
if let Some(info) = aur_packages.get(original_pkgkey) {
if let Ok((name, version, _)) = package::parse_pkgkey(original_pkgkey) {
namever2entry
.entry((name, version))
.or_insert_with(|| (original_pkgkey.clone(), Arc::clone(info)));
}
}
}
let mut mapped = Vec::new();
for built_pkg_path in built_pkg_paths {
if let Some(original_key) =
map_built_package_to_entry(built_pkg_path, &namever2entry)
{
mapped.push((
built_pkg_path.clone(),
original_key,
));
}
}
Ok(mapped)
}
fn map_built_package_to_entry(
built_pkg_path: &Path,
namever2entry: &HashMap<(String, String), (String, Arc<InstalledPackageInfo>)>,
) -> Option<String> {
let (act_name, act_version, _act_arch) =
match infer_name_version_from_arch_pkgfile(built_pkg_path) {
Ok((name, version, arch)) => (name, version, arch),
Err(e) => {
log::warn!(
"AUR build produced package file '{}' with unrecognized filename format: {}",
built_pkg_path.display(),
e
);
return None;
}
};
if let Some((original_key, _info)) =
namever2entry.get(&(act_name.clone(), act_version.clone()))
{
Some(original_key.clone())
} else {
log::info!(
"AUR build produced package '{}' ({}, {}), skipping -- not in install plan",
built_pkg_path.display(),
act_name,
act_version,
);
None
}
}
fn postinstall_built_aur_round(
plan: &mut InstallationPlan,
this_round_aur_packages: &mut InstalledPackagesMap,
this_round_pkgkey_mapping: &HashMap<String, String>,
aur_mapping_all_rounds: &mut HashMap<String, String>,
) -> Result<()> {
if !this_round_pkgkey_mapping.is_empty() {
fixup_aur_plan_keys(plan, this_round_pkgkey_mapping)
.with_context(|| "Failed to fixup AUR plan keys for current round")?;
aur_mapping_all_rounds.extend(this_round_pkgkey_mapping.clone());
}
fixup_installed_packages_values(
this_round_pkgkey_mapping,
this_round_aur_packages,
)
.with_context(|| {
"Failed to normalize dependency fields for AUR packages in current round"
})?;
fixup_installed_packages_values(
this_round_pkgkey_mapping,
&mut plan.new_pkgs,
)
.with_context(|| {
"Failed to normalize dependency fields for all new packages using AUR mappings"
})?;
plan.batch.new_pkgkeys.clear();
for k in this_round_aur_packages.keys() {
plan.batch.new_pkgkeys.insert(k.clone());
}
run_transaction_batch(plan)?;
Ok(())
}
fn fixup_aur_plan_keys(
plan: &mut InstallationPlan,
pkgkey_mapping: &HashMap<String, String>,
) -> Result<()> {
for op in &mut plan.ordered_operations {
if let Some(ref mut pkgkey) = op.new_pkgkey {
if let Some(mapped_key) = pkgkey_mapping.get(pkgkey) {
let old_key = pkgkey.clone();
*pkgkey = mapped_key.clone();
if let Some(pkg_info) = plan.new_pkgs.get_mut(&old_key) {
let pkg_info_mut = Arc::make_mut(pkg_info);
if pkg_info_mut.pkgline.contains(&old_key) {
pkg_info_mut.pkgline = pkg_info_mut.pkgline.replace(&old_key, mapped_key);
}
let info = plan.new_pkgs.remove(&old_key).unwrap();
plan.new_pkgs.insert(mapped_key.clone(), info);
}
}
}
}
Ok(())
}
fn fixup_installed_packages_values(
pkgkey_mapping: &HashMap<String, String>,
pkgs: &mut InstalledPackagesMap,
) -> Result<()> {
use crate::package;
let rewrite_key = |k: &String, mapping: &HashMap<String, String>| -> String {
if let Some(mapped) = mapping.get(k) {
mapped.clone()
} else {
k.clone()
}
};
for (pkgkey, info) in pkgs.iter_mut() {
let info_mut = Arc::make_mut(info);
if info_mut.arch != std::env::consts::ARCH {
if let Ok((_name, _version, arch)) = package::parse_pkgkey(pkgkey) {
info_mut.arch = arch;
}
}
info_mut.depends = info_mut
.depends
.iter()
.map(|k| rewrite_key(k, pkgkey_mapping))
.collect();
info_mut.rdepends = info_mut
.rdepends
.iter()
.map(|k| rewrite_key(k, pkgkey_mapping))
.collect();
info_mut.bdepends = info_mut
.bdepends
.iter()
.map(|k| rewrite_key(k, pkgkey_mapping))
.collect();
info_mut.rbdepends = info_mut
.rbdepends
.iter()
.map(|k| rewrite_key(k, pkgkey_mapping))
.collect();
}
Ok(())
}
fn find_pkgbuild(dir: &Path) -> Result<PathBuf> {
let mut queue: VecDeque<(PathBuf, usize)> = VecDeque::new();
queue.push_back((dir.to_path_buf(), 0));
while let Some((current, depth)) = queue.pop_front() {
let candidate = current.join("PKGBUILD");
if lfs::exists_on_host(&candidate) {
return Ok(candidate);
}
if depth >= 3 {
continue;
}
for entry in std::fs::read_dir(¤t)? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
queue.push_back((path, depth + 1));
}
}
}
Err(eyre!("PKGBUILD not found in {}", dir.display()))
}
#[cfg(unix)]
pub fn find_built_package(dir: &Path, pkgbase: &str, version: &str) -> Result<Vec<PathBuf>> {
let mut built = Vec::new();
for entry in std::fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if let Some(ext) = path.extension() {
if ext == "zst" || ext == "xz" {
if path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("")
.contains(".pkg.tar")
{
match infer_name_version_from_arch_pkgfile(&path) {
Ok((_name, ver, _arch)) => {
if ver == version {
built.push(path);
}
}
Err(_) => {
continue;
}
}
}
}
}
}
if built.is_empty() {
Err(eyre!("Built package not found in {} for pkgbase {} version {}", dir.display(), pkgbase, version))
} else {
Ok(built)
}
}
fn infer_name_version_from_arch_pkgfile(path: &Path) -> Result<(String, String, String)> {
let filename = path
.file_name()
.and_then(|s| s.to_str())
.ok_or_else(|| eyre!("Invalid UTF-8 in package filename: {}", path.display()))?;
let base = filename
.strip_suffix(".pkg.tar.zst")
.or_else(|| filename.strip_suffix(".pkg.tar.xz"))
.ok_or_else(|| eyre!("Unsupported Arch package filename (suffix): {}", filename))?;
let parts: Vec<&str> = base.rsplitn(3, '-').collect();
if parts.len() < 3 {
return Err(eyre!(
"Invalid package filename format (expected at least 3 components): {}",
filename
));
}
let arch = parts[0];
let pkgrel = parts[1];
let namever = parts[2];
if !pkgrel.chars().all(|c| c.is_ascii_digit()) {
return Err(eyre!(
"Invalid pkgrel component (expected numeric): {}",
filename
));
}
if let Some(idx) = namever.rfind('-') {
let name = &namever[..idx];
let pkgver = &namever[idx + 1..];
if name.is_empty() || pkgver.is_empty() {
return Err(eyre!(
"Failed to infer (name, version) from package filename: {}",
filename
));
}
let version = format!("{}-{}", pkgver, pkgrel);
Ok((name.to_string(), version, arch.to_string()))
} else {
let version = format!("-{}", pkgrel);
Ok((namever.to_string(), version, arch.to_string()))
}
}