diff --git a/src/install.rs b/src/install.rs index 4e7bbed1..11b0f562 100644 --- a/src/install.rs +++ b/src/install.rs @@ -25,8 +25,11 @@ use crate::upgrade::{get_upgrades, Upgrades}; use crate::util::{ask, repo_aur_pkgs, split_repo_aur_targets}; use crate::{args, exec, news, print_error, printtr, repo}; -use alpm::{Alpm, Depend, Version}; -use alpm_utils::depends::{satisfies, satisfies_nover, satisfies_provide, satisfies_provide_nover}; +use alpm::{Alpm, Depend, SigLevel, Version}; +use alpm_utils::depends::{ + satisfies, satisfies_dep, satisfies_dep_nover, satisfies_nover, satisfies_provide, + satisfies_provide_nover, +}; use alpm_utils::{DbListExt, Targ}; use ansiterm::Style; use anyhow::{bail, ensure, Context, Result}; @@ -53,6 +56,60 @@ impl Status { } } +#[derive(Debug)] +struct BuiltArtifact { + path: String, + name: String, + version: Version, + provides: Vec, + depends: Vec, +} + +impl BuiltArtifact { + fn load(config: &Config, path: &str) -> Result { + let package = config + .alpm + .pkg_load(path.as_bytes(), false, SigLevel::NONE) + .with_context(|| format!("load built package {path}"))?; + + Ok(Self { + path: path.to_owned(), + name: package.name().to_owned(), + version: Version::new(package.version().as_str()), + provides: package + .provides() + .iter() + .map(|dep| dep.to_depend()) + .collect(), + depends: package + .depends() + .iter() + .map(|dep| dep.to_depend()) + .collect(), + }) + } + + fn matches_name(&self, dependency: &Depend, ignore_version: bool) -> bool { + if ignore_version { + satisfies_dep_nover(dependency, &self.name) + } else { + satisfies_dep(dependency, &self.name, &self.version) + } + } + + fn matches_provide(&self, dependency: &Depend, ignore_version: bool) -> bool { + if ignore_version { + self.provides + .iter() + .any(|provide| satisfies_provide_nover(dependency, provide)) + } else { + self.provides + .iter() + .any(|provide| satisfies_provide(dependency, provide)) + } + } +} + struct Installer { refresh: usize, sysupgrade: usize, @@ -71,7 +128,7 @@ struct Installer { conflict: bool, devel_info: DevelInfo, new_devel_info: DevelInfo, - built: Vec, + built: Vec, } pub async fn install(config: &mut Config, targets_str: &[String]) -> Result<()> { @@ -541,10 +598,7 @@ impl Installer { env.extend(pkgdest.map(|p| ("PKGDEST".to_string(), p.to_string()))); if config.chroot { - let mut extra = Vec::new(); - if config.repos == LocalRepos::None { - extra.extend(self.built.iter().map(|s| s.as_str())); - } + let extra = self.chroot_artifacts(config, base); let mut chroot_flags: Vec<&str> = config.chroot_flags.iter().map(|s| s.as_str()).collect(); chroot_flags.push("-cu"); @@ -588,10 +642,7 @@ impl Installer { if needs_build { // actual build if config.chroot { - let mut extra = Vec::new(); - if config.repos == LocalRepos::None { - extra.extend(self.built.iter().map(|s| s.as_str())); - } + let extra = self.chroot_artifacts(config, base); self.chroot .build( dir, @@ -625,28 +676,39 @@ impl Installer { let debug_paths = self.debug_paths(config, base, &pkgdests)?; self.add_pkg(config, base, repo, &pkgdests, &debug_paths)?; - self.queue_install(base, &pkgdests, &debug_paths); + self.record_built_artifacts(config, base, &pkgdests, &debug_paths)?; Ok((pkgdests, version)) } - fn queue_install( + fn record_built_artifacts( &mut self, - base: &mut Base, + config: &Config, + base: &Base, pkgdest: &HashMap, debug_paths: &HashMap, - ) { - let to_install: Vec<_> = match base { - Base::Aur(a) => a.pkgs.iter().map(|a| a.pkg.name.as_str()).collect(), - Base::Pkgbuild(c) => c.pkgs.iter().map(|a| a.pkg.pkgname.as_str()).collect(), - }; - - let to_install = to_install - .iter() - .filter_map(|p| pkgdest.get(*p)) + ) -> Result<()> { + let artifacts = base + .packages() + .filter_map(|package| pkgdest.get(package)) .chain(debug_paths.values()) - .cloned(); + .map(|path| BuiltArtifact::load(config, path)) + .collect::>>()?; + + self.built.extend(artifacts); + Ok(()) + } + + fn chroot_artifacts<'a>(&'a self, config: &Config, base: &Base) -> Vec<&'a str> { + if config.repos != LocalRepos::None { + return Vec::new(); + } - self.built.extend(to_install); + let dependencies = chroot_dependencies(config, base); + select_chroot_artifacts( + &self.built, + &dependencies, + config.args.count("d", "nodeps") > 0, + ) } fn add_pkg( @@ -785,7 +847,7 @@ impl Installer { let (pkgdests, version) = parse_package_list(config, &dir, pkgdest)?; let debug_paths = self.debug_paths(config, base, &pkgdests)?; self.add_pkg(config, base, repo, &pkgdests, &debug_paths)?; - self.queue_install(base, &pkgdests, &debug_paths); + self.record_built_artifacts(config, base, &pkgdests, &debug_paths)?; (pkgdests, version) }; @@ -1850,6 +1912,100 @@ fn trim_dep_ver(dep: &str, trim: bool) -> &str { } } +fn chroot_dependencies(config: &Config, base: &Base) -> Vec { + let mut dependencies = Vec::new(); + + match base { + Base::Aur(base) => { + for package in &base.pkgs { + dependencies.extend( + package + .pkg + .depends + .iter() + .map(|dep| Depend::new(dep.as_str())), + ); + dependencies.extend( + package + .pkg + .make_depends + .iter() + .map(|dep| Depend::new(dep.as_str())), + ); + if !config.no_check { + dependencies.extend( + package + .pkg + .check_depends + .iter() + .map(|dep| Depend::new(dep.as_str())), + ); + } + } + } + Base::Pkgbuild(base) => { + dependencies + .extend(supported_deps(config, &base.srcinfo.base.makedepends).map(Depend::new)); + if !config.no_check { + dependencies.extend( + supported_deps(config, &base.srcinfo.base.checkdepends).map(Depend::new), + ); + } + for package in &base.pkgs { + dependencies.extend(supported_deps(config, &package.pkg.depends).map(Depend::new)); + } + } + } + + dependencies +} + +fn select_chroot_artifacts<'a>( + artifacts: &'a [BuiltArtifact], + requirements: &[Depend], + ignore_version: bool, +) -> Vec<&'a str> { + let mut selected = HashSet::new(); + let mut pending = requirements + .iter() + .map(|dependency| dependency.to_depend()) + .collect::>(); + + while let Some(requirement) = pending.pop() { + let artifact = + artifacts + .iter() + .enumerate() + .rev() + .find(|(_, artifact)| artifact.matches_name(&requirement, ignore_version)) + .or_else(|| { + artifacts.iter().enumerate().rev().find(|(_, artifact)| { + artifact.matches_provide(&requirement, ignore_version) + }) + }); + + let Some((index, artifact)) = artifact else { + continue; + }; + + if selected.insert(index) { + pending.extend( + artifact + .depends + .iter() + .map(|dependency| dependency.to_depend()), + ); + } + } + + artifacts + .iter() + .enumerate() + .filter(|(index, _)| selected.contains(index)) + .map(|(_, artifact)| artifact.path.as_str()) + .collect() +} + fn check_deps_local<'a>( alpm: &Alpm, missing: &mut Vec<&'a str>, @@ -2185,3 +2341,102 @@ fn needs_install(config: &Config, base: &Base, version: &str, pkg: &str) -> bool fn is_ver_char(c: char) -> bool { matches!(c, '<' | '=' | '>') } + +#[cfg(test)] +mod chroot_artifact_tests { + use super::{select_chroot_artifacts, BuiltArtifact}; + use alpm::{Depend, Version}; + + fn artifact( + path: &str, + name: &str, + version: &str, + provides: &[&str], + depends: &[&str], + ) -> BuiltArtifact { + BuiltArtifact { + path: path.to_owned(), + name: name.to_owned(), + version: Version::new(version), + provides: provides.iter().map(|dep| Depend::new(*dep)).collect(), + depends: depends.iter().map(|dep| Depend::new(*dep)).collect(), + } + } + + #[test] + fn selects_only_the_required_artifact_closure() { + let artifacts = vec![ + artifact("/pkg/foundation.pkg.tar.zst", "foundation", "1-1", &[], &[]), + artifact( + "/pkg/dependent.pkg.tar.zst", + "dependent", + "1-1", + &[], + &["foundation>=1"], + ), + artifact("/pkg/unrelated.pkg.tar.zst", "unrelated", "1-1", &[], &[]), + ]; + let requirements = vec![Depend::new("dependent>=1")]; + + assert_eq!( + select_chroot_artifacts(&artifacts, &requirements, false), + vec!["/pkg/foundation.pkg.tar.zst", "/pkg/dependent.pkg.tar.zst",] + ); + } + + #[test] + fn prefers_a_package_name_over_a_later_provider() { + let artifacts = vec![ + artifact("/pkg/foo.pkg.tar.zst", "foo", "1-1", &[], &[]), + artifact( + "/pkg/foo-git.pkg.tar.zst", + "foo-git", + "1-1", + &["foo=1"], + &[], + ), + ]; + let requirements = vec![Depend::new("foo>=1")]; + + assert_eq!( + select_chroot_artifacts(&artifacts, &requirements, false), + vec!["/pkg/foo.pkg.tar.zst"] + ); + } + + #[test] + fn selects_a_versioned_provider() { + let artifacts = vec![artifact( + "/pkg/provider.pkg.tar.zst", + "provider", + "1-1", + &["virtual-dependency=2"], + &[], + )]; + let requirements = vec![Depend::new("virtual-dependency>=2")]; + + assert_eq!( + select_chroot_artifacts(&artifacts, &requirements, false), + vec!["/pkg/provider.pkg.tar.zst"] + ); + } + + #[test] + fn excludes_artifacts_that_do_not_satisfy_the_required_version() { + let artifacts = vec![artifact("/pkg/foo.pkg.tar.zst", "foo", "1-1", &[], &[])]; + let requirements = vec![Depend::new("foo>=2")]; + + assert!(select_chroot_artifacts(&artifacts, &requirements, false).is_empty()); + } + + #[test] + fn ignores_versions_when_nodeps_is_requested() { + let artifacts = vec![artifact("/pkg/foo.pkg.tar.zst", "foo", "1-1", &[], &[])]; + let requirements = vec![Depend::new("foo>=2")]; + + assert_eq!( + select_chroot_artifacts(&artifacts, &requirements, true), + vec!["/pkg/foo.pkg.tar.zst"] + ); + } +}