Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
307 changes: 281 additions & 26 deletions src/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -53,6 +56,60 @@ impl Status {
}
}

#[derive(Debug)]
struct BuiltArtifact {
path: String,
name: String,
version: Version,
provides: Vec<Depend>,
depends: Vec<Depend>,
}

impl BuiltArtifact {
fn load(config: &Config, path: &str) -> Result<Self> {
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,
Expand All @@ -71,7 +128,7 @@ struct Installer {
conflict: bool,
devel_info: DevelInfo,
new_devel_info: DevelInfo,
built: Vec<String>,
built: Vec<BuiltArtifact>,
}

pub async fn install(config: &mut Config, targets_str: &[String]) -> Result<()> {
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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<String, String>,
debug_paths: &HashMap<String, String>,
) {
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::<Result<Vec<_>>>()?;

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(
Expand Down Expand Up @@ -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)
};

Expand Down Expand Up @@ -1850,6 +1912,100 @@ fn trim_dep_ver(dep: &str, trim: bool) -> &str {
}
}

fn chroot_dependencies(config: &Config, base: &Base) -> Vec<Depend> {
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::<Vec<_>>();

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>,
Expand Down Expand Up @@ -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"]
);
}
}