use std::{
collections::{HashMap, HashSet},
io,
path::Path,
};
use rattler_conda_types::{PackageName, Platform, PrefixRecord};
use super::{InstallationResultRecord, installer::result_record::ContentComparable};
use crate::install::{PythonInfo, python::PythonInfoError};
#[derive(Debug, thiserror::Error)]
pub enum TransactionError {
#[error(transparent)]
PythonInfoError(#[from] PythonInfoError),
#[error("the operation was cancelled")]
Cancelled,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TransactionOperation<Old, New> {
Install(New),
Change {
old: Old,
new: New,
},
Reinstall {
old: Old,
new: New,
},
Remove(Old),
}
impl<Old: Clone, New: Clone> TransactionOperation<&Old, &New> {
pub fn to_owned(self) -> TransactionOperation<Old, New> {
match self {
TransactionOperation::Install(new) => TransactionOperation::Install(new.clone()),
TransactionOperation::Change { old, new } => TransactionOperation::Change {
old: old.clone(),
new: new.clone(),
},
TransactionOperation::Reinstall { old, new } => TransactionOperation::Reinstall {
old: old.clone(),
new: new.clone(),
},
TransactionOperation::Remove(old) => TransactionOperation::Remove(old.clone()),
}
}
}
impl<Old, New> TransactionOperation<Old, New> {
pub fn record_to_install(&self) -> Option<&New> {
match self {
TransactionOperation::Install(record) => Some(record),
TransactionOperation::Change { new, .. }
| TransactionOperation::Reinstall { new, .. } => Some(new),
TransactionOperation::Remove(_) => None,
}
}
}
impl<Old, New> TransactionOperation<Old, New> {
pub fn record_to_remove(&self) -> Option<&Old> {
match self {
TransactionOperation::Install(_) => None,
TransactionOperation::Change { old, .. }
| TransactionOperation::Reinstall { old, new: _ }
| TransactionOperation::Remove(old) => Some(old),
}
}
}
#[derive(Debug)]
pub struct Transaction<Old, New> {
pub operations: Vec<TransactionOperation<Old, New>>,
pub python_info: Option<PythonInfo>,
pub current_python_info: Option<PythonInfo>,
pub platform: Platform,
pub unchanged: Vec<Old>,
}
impl<Old: Clone, New: Clone> Transaction<&Old, &New> {
pub fn to_owned(self) -> Transaction<Old, New> {
Transaction {
operations: self
.operations
.into_iter()
.map(TransactionOperation::to_owned)
.collect(),
python_info: self.python_info,
current_python_info: self.current_python_info,
platform: self.platform,
unchanged: self.unchanged.into_iter().cloned().collect(),
}
}
}
impl<Old, New> Transaction<Old, New> {
pub fn removed_packages(&self) -> impl Iterator<Item = &Old> + '_ {
self.operations
.iter()
.filter_map(TransactionOperation::record_to_remove)
}
pub fn unchanged_packages(&self) -> &[Old] {
&self.unchanged
}
pub fn packages_to_uninstall(&self) -> usize {
self.removed_packages().count()
}
}
impl<Old, New> Transaction<Old, New> {
pub fn installed_packages(&self) -> impl Iterator<Item = &New> + '_ {
self.operations
.iter()
.filter_map(TransactionOperation::record_to_install)
}
pub fn packages_to_install(&self) -> usize {
self.installed_packages().count()
}
}
impl<Old, New> Transaction<Old, New>
where
Old: ContentComparable,
New: ContentComparable,
{
pub fn from_current_and_desired<
CurIter: IntoIterator<Item = Old>,
NewIter: IntoIterator<Item = New>,
>(
current: CurIter,
desired: NewIter,
reinstall: Option<&HashSet<PackageName>>,
ignored: Option<&HashSet<PackageName>>,
platform: Platform,
) -> Result<Self, TransactionError> {
let current_packages = current.into_iter().collect::<Vec<_>>();
let desired_packages = desired.into_iter().collect::<Vec<_>>();
let current_python_info = find_python_info(¤t_packages, platform)?;
let desired_python_info = find_python_info(&desired_packages, platform)?;
let needs_python_relink = match (¤t_python_info, &desired_python_info) {
(Some(current), Some(desired)) => desired.is_relink_required(current),
_ => false,
};
let mut operations = Vec::new();
let empty_hashset = HashSet::new();
let reinstall = reinstall.unwrap_or(&empty_hashset);
let ignored = ignored.unwrap_or(&empty_hashset);
let desired_names = desired_packages
.iter()
.map(|r| r.name().clone())
.collect::<HashSet<_>>();
let mut unchanged = Vec::new();
let mut current_map = HashMap::new();
for record in current_packages {
let package_name = record.name();
if desired_names.contains(package_name) {
current_map.insert(record.name().clone(), record);
} else {
if ignored.contains(package_name) {
unchanged.push(record);
} else {
operations.push(TransactionOperation::Remove(record));
}
}
}
operations.reverse();
for record in desired_packages {
let name = record.name();
let old_record = current_map.remove(name);
if ignored.contains(name) {
if let Some(old_record) = old_record {
unchanged.push(old_record);
}
} else if let Some(old_record) = old_record {
if !describe_same_content(&record, &old_record) || reinstall.contains(record.name())
{
operations.push(TransactionOperation::Change {
old: old_record,
new: record,
});
} else if needs_python_relink && old_record.noarch().is_python() {
operations.push(TransactionOperation::Reinstall {
old: old_record,
new: record,
});
} else {
unchanged.push(old_record);
}
} else {
operations.push(TransactionOperation::Install(record));
}
}
Ok(Self {
operations,
python_info: desired_python_info,
current_python_info,
platform,
unchanged,
})
}
}
impl<New> Transaction<InstallationResultRecord, New> {
pub fn into_prefix_record(
self,
prefix: impl AsRef<Path>,
) -> Result<Transaction<PrefixRecord, New>, io::Error> {
let Transaction {
operations,
python_info,
current_python_info,
platform,
unchanged,
} = self;
let convert_op = |op: TransactionOperation<InstallationResultRecord, New>,
prefix: &Path|
-> Result<_, io::Error> {
Ok(match op {
TransactionOperation::Install(new) => TransactionOperation::Install(new),
TransactionOperation::Change { old, new } => TransactionOperation::Change {
old: old.into_prefix_record(prefix)?,
new,
},
TransactionOperation::Reinstall { old, new } => TransactionOperation::Reinstall {
old: old.into_prefix_record(prefix)?,
new,
},
TransactionOperation::Remove(old) => {
TransactionOperation::Remove(old.into_prefix_record(prefix)?)
}
})
};
let operations = {
let mut ops = Vec::with_capacity(operations.len());
for op in operations {
ops.push(convert_op(op, prefix.as_ref())?);
}
ops
};
let unchanged = {
let mut unch = Vec::with_capacity(unchanged.len());
for u in unchanged {
unch.push(u.into_prefix_record(prefix.as_ref())?);
}
unch
};
Ok(Transaction {
operations,
python_info,
current_python_info,
platform,
unchanged,
})
}
}
impl<New> Transaction<PrefixRecord, New> {
pub fn into_installation_result_record(self) -> Transaction<InstallationResultRecord, New> {
let Transaction {
operations,
python_info,
current_python_info,
platform,
unchanged,
} = self;
let convert_op = |op: TransactionOperation<PrefixRecord, New>| match op {
TransactionOperation::Install(new) => TransactionOperation::Install(new),
TransactionOperation::Change { old, new } => TransactionOperation::Change {
old: InstallationResultRecord::Max(old),
new,
},
TransactionOperation::Reinstall { old, new } => TransactionOperation::Reinstall {
old: InstallationResultRecord::Max(old),
new,
},
TransactionOperation::Remove(old) => {
TransactionOperation::Remove(InstallationResultRecord::Max(old))
}
};
let operations = {
let mut ops = Vec::with_capacity(operations.len());
for op in operations {
ops.push(convert_op(op));
}
ops
};
let unchanged = {
let mut unch = Vec::with_capacity(unchanged.len());
for u in unchanged {
unch.push(InstallationResultRecord::Max(u));
}
unch
};
Transaction {
operations,
python_info,
current_python_info,
platform,
unchanged,
}
}
}
fn find_python_info(
records: impl IntoIterator<Item = impl ContentComparable>,
platform: Platform,
) -> Result<Option<PythonInfo>, PythonInfoError> {
records
.into_iter()
.find(|r| r.name().as_normalized() == "python")
.map(|record| {
PythonInfo::from_version(
record.version(),
record.python_site_packages_path(),
platform,
)
})
.map_or(Ok(None), |info| info.map(Some))
}
fn describe_same_content<T: ContentComparable, U: ContentComparable>(from: &T, to: &U) -> bool {
if from.sha256().is_some() != to.sha256().is_some() {
return false;
}
if let (Some(a), Some(b)) = (from.sha256(), to.sha256()) {
return a == b;
}
if from.md5().is_some() != to.md5().is_some() {
return false;
}
if let (Some(a), Some(b)) = (from.md5(), to.md5()) {
return a == b;
}
if matches!((from.size(), to.size()), (Some(a), Some(b)) if a != b) {
return false;
}
from.name() == to.name() && from.version() == to.version() && from.build() == to.build()
}
#[cfg(test)]
mod tests {
use std::collections::HashSet;
use assert_matches::assert_matches;
use rattler_conda_types::{Platform, prefix::Prefix};
use crate::install::{
Transaction, TransactionOperation, test_utils::download_and_get_prefix_record,
};
#[tokio::test]
async fn test_reinstall_package() {
let environment_dir = tempfile::TempDir::new().unwrap();
let prefix_record = download_and_get_prefix_record(
&Prefix::create(environment_dir.path()).unwrap(),
"https://conda.anaconda.org/conda-forge/win-64/ruff-0.0.171-py310h298983d_0.conda"
.parse()
.unwrap(),
"25c755b97189ee066576b4ae3999d5e7ff4406d236b984742194e63941838dcd",
)
.await;
let name = prefix_record.repodata_record.package_record.name.clone();
let transaction = Transaction::from_current_and_desired(
vec![prefix_record.clone()],
vec![prefix_record.clone()],
Some(&HashSet::from_iter(vec![name])),
None, Platform::current(),
)
.unwrap();
assert_matches!(
transaction.operations[0],
TransactionOperation::Change { .. }
);
}
#[tokio::test]
async fn test_ignored_packages() {
let environment_dir = tempfile::TempDir::new().unwrap();
let prefix_record = download_and_get_prefix_record(
&Prefix::create(environment_dir.path()).unwrap(),
"https://conda.anaconda.org/conda-forge/win-64/ruff-0.0.171-py310h298983d_0.conda"
.parse()
.unwrap(),
"25c755b97189ee066576b4ae3999d5e7ff4406d236b984742194e63941838dcd",
)
.await;
let name = prefix_record.repodata_record.package_record.name.clone();
let ignored_packages = Some(HashSet::from_iter(vec![name.clone()]));
let transaction = Transaction::from_current_and_desired(
vec![prefix_record.clone()],
vec![prefix_record.repodata_record.clone()],
None, ignored_packages.as_ref(),
Platform::current(),
)
.unwrap();
assert!(transaction.operations.is_empty());
let ignored_packages = Some(HashSet::from_iter(vec![name.clone()]));
let transaction = Transaction::from_current_and_desired(
vec![prefix_record.clone()],
Vec::<rattler_conda_types::RepoDataRecord>::new(), None, ignored_packages.as_ref(),
Platform::current(),
)
.unwrap();
assert!(transaction.operations.is_empty());
let ignored_packages = Some(HashSet::from_iter(vec![name.clone()]));
let transaction = Transaction::from_current_and_desired(
Vec::<rattler_conda_types::PrefixRecord>::new(), vec![prefix_record.repodata_record.clone()],
None, ignored_packages.as_ref(),
Platform::current(),
)
.unwrap();
assert!(transaction.operations.is_empty());
}
}