use super::{verify_environment_satisfiability, verify_platform_satisfiability, PlatformUnsat};
use crate::{consts, project::Environment, project::SolveGroup, Project};
use itertools::Itertools;
use rattler_conda_types::Platform;
use rattler_lock::{LockFile, Package};
use std::collections::{HashMap, HashSet};
pub struct OutdatedEnvironments<'p> {
pub conda: HashMap<Environment<'p>, HashSet<Platform>>,
pub pypi: HashMap<Environment<'p>, HashSet<Platform>>,
}
impl<'p> OutdatedEnvironments<'p> {
pub fn from_project_and_lock_file(project: &'p Project, lock_file: &LockFile) -> Self {
let mut outdated_conda: HashMap<_, HashSet<_>> = HashMap::new();
let mut outdated_pypi: HashMap<_, HashSet<_>> = HashMap::new();
find_unsatisfiable_targets(project, lock_file, &mut outdated_conda, &mut outdated_pypi);
let (mut conda_solve_groups_out_of_date, mut pypi_solve_groups_out_of_date) =
map_outdated_targets_to_solve_groups(&outdated_conda, &outdated_pypi);
find_inconsistent_solve_groups(
project,
lock_file,
&outdated_conda,
&mut conda_solve_groups_out_of_date,
&mut pypi_solve_groups_out_of_date,
);
for (solve_group, platforms) in conda_solve_groups_out_of_date {
for env in solve_group.environments() {
outdated_conda
.entry(env.clone())
.or_default()
.extend(platforms.iter().copied());
}
}
for (solve_group, platforms) in pypi_solve_groups_out_of_date {
for env in solve_group.environments() {
outdated_pypi
.entry(env.clone())
.or_default()
.extend(platforms.iter().copied());
}
}
for (environment, platforms) in outdated_conda.iter() {
outdated_pypi
.entry(environment.clone())
.or_default()
.extend(platforms.iter().copied());
}
Self {
conda: outdated_conda,
pypi: outdated_pypi,
}
}
pub fn is_empty(&self) -> bool {
self.conda.is_empty() && self.pypi.is_empty()
}
}
fn find_unsatisfiable_targets<'p>(
project: &'p Project,
lock_file: &LockFile,
outdated_conda: &mut HashMap<Environment<'p>, HashSet<Platform>>,
outdated_pypi: &mut HashMap<Environment<'p>, HashSet<Platform>>,
) {
for environment in project.environments() {
let platforms = environment.platforms();
let Some(locked_environment) = lock_file.environment(environment.name().as_str()) else {
tracing::info!(
"environment '{0}' is out of date because it does not exist in the lock-file.",
environment.name().fancy_display()
);
outdated_conda
.entry(environment.clone())
.or_default()
.extend(platforms);
continue;
};
if let Err(unsat) = verify_environment_satisfiability(&environment, &locked_environment) {
tracing::info!(
"environment '{0}' is out of date because {unsat}",
environment.name().fancy_display()
);
outdated_conda
.entry(environment.clone())
.or_default()
.extend(platforms);
continue;
}
for platform in platforms {
match verify_platform_satisfiability(&environment, &locked_environment, platform) {
Ok(_) => {}
Err(unsat @ PlatformUnsat::UnsatisfiableRequirement(_, _)) => {
tracing::info!(
"the pypi dependencies of environment '{0}' for platform {platform} are out of date because {unsat}",
environment.name().fancy_display()
);
outdated_pypi
.entry(environment.clone())
.or_default()
.insert(platform);
}
Err(unsat) => {
tracing::info!(
"the dependencies of environment '{0}' for platform {platform} are out of date because {unsat}",
environment.name().fancy_display()
);
outdated_conda
.entry(environment.clone())
.or_default()
.insert(platform);
}
}
}
}
}
fn map_outdated_targets_to_solve_groups<'p>(
outdated_conda: &HashMap<Environment<'p>, HashSet<Platform>>,
outdated_pypi: &HashMap<Environment<'p>, HashSet<Platform>>,
) -> (
HashMap<SolveGroup<'p>, HashSet<Platform>>,
HashMap<SolveGroup<'p>, HashSet<Platform>>,
) {
let mut conda_solve_groups_out_of_date = HashMap::new();
let mut pypi_solve_groups_out_of_date = HashMap::new();
for (environment, platforms) in outdated_conda.iter() {
let Some(solve_group) = environment.solve_group() else {
continue;
};
conda_solve_groups_out_of_date
.entry(solve_group)
.or_insert_with(HashSet::new)
.extend(platforms.iter().copied());
}
for (environment, platforms) in outdated_pypi.iter() {
let Some(solve_group) = environment.solve_group() else {
continue;
};
pypi_solve_groups_out_of_date
.entry(solve_group)
.or_insert_with(HashSet::new)
.extend(platforms.iter().copied());
}
(
conda_solve_groups_out_of_date,
pypi_solve_groups_out_of_date,
)
}
fn find_inconsistent_solve_groups<'p>(
project: &'p Project,
lock_file: &LockFile,
outdated_conda: &HashMap<Environment<'p>, HashSet<Platform>>,
conda_solve_groups_out_of_date: &mut HashMap<SolveGroup<'p>, HashSet<Platform>>,
pypi_solve_groups_out_of_date: &mut HashMap<SolveGroup<'p>, HashSet<Platform>>,
) {
let solve_groups = project.solve_groups();
let solve_groups_and_platforms = solve_groups.iter().flat_map(|solve_group| {
solve_group
.environments()
.flat_map(|env| env.platforms())
.unique()
.map(move |platform| (solve_group, platform))
});
for (solve_group, platform) in solve_groups_and_platforms {
let mut conda_package_mismatch = false;
let mut pypi_package_mismatch = false;
let mut conda_packages_by_name = HashMap::new();
let mut pypi_packages_by_name = HashMap::new();
for env in solve_group.environments() {
if outdated_conda
.get(&env)
.and_then(|p| p.get(&platform))
.is_some()
{
break;
}
let Some(locked_env) = lock_file.environment(env.name().as_str()) else {
continue;
};
for package in locked_env.packages(platform).into_iter().flatten() {
match package {
Package::Conda(pkg) => {
match conda_packages_by_name.get(&pkg.package_record().name) {
None => {
conda_packages_by_name
.insert(pkg.package_record().name.clone(), pkg.url().clone());
}
Some(url) if pkg.url() != url => {
conda_package_mismatch = true;
}
_ => {}
}
}
Package::Pypi(pkg) => {
match pypi_packages_by_name.get(&pkg.data().package.name) {
None => {
pypi_packages_by_name
.insert(pkg.data().package.name.clone(), pkg.url().clone());
}
Some(url) if pkg.url() != url => {
pypi_package_mismatch = true;
}
_ => {}
}
}
}
if conda_package_mismatch {
pypi_package_mismatch = true;
break;
}
}
if conda_package_mismatch {
pypi_package_mismatch = true;
break;
}
}
if conda_package_mismatch {
tracing::info!("the locked conda packages in solve group {} are not consistent for all environments for platform {}",
consts::SOLVE_GROUP_STYLE.apply_to(solve_group.name()),
consts::PLATFORM_STYLE.apply_to(platform));
conda_solve_groups_out_of_date
.entry(solve_group.clone())
.or_default()
.insert(platform);
}
if pypi_package_mismatch && !conda_package_mismatch {
tracing::info!("the locked pypi packages in solve group {} are not consistent for all environments for platform {}",
consts::SOLVE_GROUP_STYLE.apply_to(solve_group.name()),
consts::PLATFORM_STYLE.apply_to(platform));
pypi_solve_groups_out_of_date
.entry(solve_group.clone())
.or_default()
.insert(platform);
}
}
}