diff --git a/src/global/install.rs b/src/global/install.rs index 54b0bc9b8..6ce0a04ae 100644 --- a/src/global/install.rs +++ b/src/global/install.rs @@ -498,9 +498,15 @@ pub(crate) async fn sync(config: &Config, assume_yes: bool) -> Result<(), miette let env_dir = EnvDir::new(env_root.clone(), env_name.clone()).await?; let prefix = Prefix::new(env_dir.path()); - let prefix_records = prefix.find_installed_packages(Some(50)).await?; + let repodata_records = prefix + .find_installed_packages(Some(50)) + .await? + .into_iter() + .map(|r| r.repodata_record) + .collect_vec(); - if !specs_match_local_environment(&specs, prefix_records, parsed_environment.platform()) { + if !local_environment_matches_spec(repodata_records, &specs, parsed_environment.platform()) + { install_environment( &specs, &parsed_environment, @@ -530,18 +536,16 @@ pub(crate) async fn sync(config: &Config, assume_yes: bool) -> Result<(), miette /// This function verifies that all the given specifications are present in the /// local environment's prefix records and that there are no extra entries in /// the prefix records that do not match any of the specifications. -fn specs_match_local_environment>( +fn local_environment_matches_spec( + prefix_records: Vec, specs: &IndexMap, - prefix_records: Vec, platform: Option, ) -> bool { // Check whether all specs in the manifest are present in the installed // environment - let specs_in_manifest_are_present = specs.values().all(|spec| { - prefix_records - .iter() - .any(|record| spec.matches(record.as_ref())) - }); + let specs_in_manifest_are_present = specs + .values() + .all(|spec| prefix_records.iter().any(|record| spec.matches(record))); if !specs_in_manifest_are_present { return false; @@ -549,31 +553,32 @@ fn specs_match_local_environment>( // Check whether all packages in the installed environment have the correct // platform - let platform_specs_match_env = prefix_records.iter().all(|record| { - let Ok(package_platform) = Platform::from_str(&record.as_ref().package_record.subdir) - else { - return true; - }; + if let Some(platform) = platform { + let platform_specs_match_env = prefix_records.iter().all(|record| { + let Ok(package_platform) = Platform::from_str(&record.package_record.subdir) else { + return true; + }; + + match package_platform { + Platform::NoArch => true, + p if p == platform => true, + _ => false, + } + }); - match package_platform { - Platform::NoArch => true, - p if Some(p) == platform => true, - _ => false, + if !platform_specs_match_env { + return false; } - }); - - if !platform_specs_match_env { - return false; } - fn prune_dependencies>( - mut remaining_prefix_records: Vec, - matched_record: &T, - ) -> Vec { + fn prune_dependencies( + mut remaining_prefix_records: Vec, + matched_record: &RepoDataRecord, + ) -> Vec { let mut work_queue = Vec::from([matched_record.as_ref().clone()]); while let Some(current_record) = work_queue.pop() { - let dependencies = ¤t_record.as_ref().depends; + let dependencies = ¤t_record.depends; for dependency in dependencies { let Ok(match_spec) = MatchSpec::from_str(dependency, ParseStrictness::Lenient) else { @@ -581,7 +586,7 @@ fn specs_match_local_environment>( }; let Some(index) = remaining_prefix_records .iter() - .position(|record| match_spec.matches(&record.as_ref().package_record)) + .position(|record| match_spec.matches(&record.package_record)) else { continue; }; @@ -597,7 +602,7 @@ fn specs_match_local_environment>( // Process each spec and remove matched entries and their dependencies let remaining_prefix_records = specs.iter().fold(prefix_records, |mut acc, (name, spec)| { let Some(index) = acc.iter().position(|record| { - record.as_ref().package_record.name == *name && spec.matches(record.as_ref()) + record.package_record.name == *name && spec.matches(record.as_ref()) }) else { return acc; }; @@ -609,3 +614,128 @@ fn specs_match_local_environment>( // the environment doesn't contain records that don't match the manifest remaining_prefix_records.is_empty() } + +#[cfg(test)] +mod tests { + use indexmap::IndexMap; + use rattler_conda_types::{MatchSpec, PackageName, ParseStrictness, Platform}; + use rattler_lock::LockFile; + use rstest::{fixture, rstest}; + + use super::*; + + #[fixture] + fn ripgrep_specs() -> IndexMap { + IndexMap::from([( + PackageName::from_str("ripgrep").unwrap(), + MatchSpec::from_str("ripgrep=14.1.0", ParseStrictness::Strict).unwrap(), + )]) + } + + #[fixture] + fn ripgrep_records() -> Vec { + LockFile::from_str(include_str!("./test_data/lockfiles/ripgrep.lock")) + .unwrap() + .default_environment() + .unwrap() + .conda_repodata_records_for_platform(Platform::Linux64) + .unwrap() + .unwrap() + } + + #[fixture] + fn ripgrep_bat_specs() -> IndexMap { + IndexMap::from([ + ( + PackageName::from_str("ripgrep").unwrap(), + MatchSpec::from_str("ripgrep=14.1.0", ParseStrictness::Strict).unwrap(), + ), + ( + PackageName::from_str("bat").unwrap(), + MatchSpec::from_str("bat=0.24.0", ParseStrictness::Strict).unwrap(), + ), + ]) + } + + #[fixture] + fn ripgrep_bat_records() -> Vec { + LockFile::from_str(include_str!("./test_data/lockfiles/ripgrep_bat.lock")) + .unwrap() + .default_environment() + .unwrap() + .conda_repodata_records_for_platform(Platform::Linux64) + .unwrap() + .unwrap() + } + + #[rstest] + fn test_local_environment_matches_spec( + ripgrep_records: Vec, + ripgrep_specs: IndexMap, + ) { + assert!(local_environment_matches_spec( + ripgrep_records, + &ripgrep_specs, + None + )); + } + + #[rstest] + fn test_local_environment_misses_entries_for_specs( + mut ripgrep_records: Vec, + ripgrep_specs: IndexMap, + ) { + // Remove last repodata record + ripgrep_records.pop(); + + assert!(!local_environment_matches_spec( + ripgrep_records, + &ripgrep_specs, + None + )); + } + + #[rstest] + fn test_local_environment_has_too_many_entries_to_match_spec( + ripgrep_bat_records: Vec, + ripgrep_specs: IndexMap, + ripgrep_bat_specs: IndexMap, + ) { + assert!(!local_environment_matches_spec( + ripgrep_bat_records.clone(), + &ripgrep_specs, + None + ), "The function needs to detect that records coming from ripgrep and bat don't match ripgrep alone."); + + assert!( + local_environment_matches_spec(ripgrep_bat_records, &ripgrep_bat_specs, None), + "The records and specs match and the function should return `true`." + ); + } + + #[rstest] + fn test_local_environment_matches_given_platform( + ripgrep_records: Vec, + ripgrep_specs: IndexMap, + ) { + assert!( + local_environment_matches_spec( + ripgrep_records, + &ripgrep_specs, + Some(Platform::Linux64) + ), + "The records contains only linux-64 entries" + ); + } + + #[rstest] + fn test_local_environment_doesnt_match_given_platform( + ripgrep_records: Vec, + ripgrep_specs: IndexMap, + ) { + assert!( + !local_environment_matches_spec(ripgrep_records, &ripgrep_specs, Some(Platform::Win64),), + "The record contains linux-64 entries, so the function should always return `false`" + ); + } +} diff --git a/src/global/test_data/lockfiles/ripgrep.lock b/src/global/test_data/lockfiles/ripgrep.lock new file mode 100644 index 000000000..58467b6cc --- /dev/null +++ b/src/global/test_data/lockfiles/ripgrep.lock @@ -0,0 +1,106 @@ +version: 5 +environments: + default: + channels: + - url: https://conda.anaconda.org/conda-forge/ + packages: + linux-64: + - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-14.1.0-h77fa898_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-14.1.0-h69a702a_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-14.1.0-h77fa898_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ripgrep-14.1.0-he8a937b_0.conda +packages: +- kind: conda + name: _libgcc_mutex + version: '0.1' + build: conda_forge + subdir: linux-64 + url: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 + sha256: fe51de6107f9edc7aa4f786a70f4a883943bc9d39b3bb7307c04c41410990726 + md5: d7c89558ba9fa0495403155b64376d81 + license: None + size: 2562 + timestamp: 1578324546067 +- kind: conda + name: _openmp_mutex + version: '4.5' + build: 2_gnu + build_number: 16 + subdir: linux-64 + url: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 + sha256: fbe2c5e56a653bebb982eda4876a9178aedfc2b545f25d0ce9c4c0b508253d22 + md5: 73aaf86a425cc6e73fcf236a5a46396d + depends: + - _libgcc_mutex 0.1 conda_forge + - libgomp >=7.5.0 + constrains: + - openmp_impl 9999 + license: BSD-3-Clause + license_family: BSD + size: 23621 + timestamp: 1650670423406 +- kind: conda + name: libgcc + version: 14.1.0 + build: h77fa898_1 + build_number: 1 + subdir: linux-64 + url: https://conda.anaconda.org/conda-forge/linux-64/libgcc-14.1.0-h77fa898_1.conda + sha256: 10fa74b69266a2be7b96db881e18fa62cfa03082b65231e8d652e897c4b335a3 + md5: 002ef4463dd1e2b44a94a4ace468f5d2 + depends: + - _libgcc_mutex 0.1 conda_forge + - _openmp_mutex >=4.5 + constrains: + - libgomp 14.1.0 h77fa898_1 + - libgcc-ng ==14.1.0=*_1 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + size: 846380 + timestamp: 1724801836552 +- kind: conda + name: libgcc-ng + version: 14.1.0 + build: h69a702a_1 + build_number: 1 + subdir: linux-64 + url: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-14.1.0-h69a702a_1.conda + sha256: b91f7021e14c3d5c840fbf0dc75370d6e1f7c7ff4482220940eaafb9c64613b7 + md5: 1efc0ad219877a73ef977af7dbb51f17 + depends: + - libgcc 14.1.0 h77fa898_1 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + size: 52170 + timestamp: 1724801842101 +- kind: conda + name: libgomp + version: 14.1.0 + build: h77fa898_1 + build_number: 1 + subdir: linux-64 + url: https://conda.anaconda.org/conda-forge/linux-64/libgomp-14.1.0-h77fa898_1.conda + sha256: c96724c8ae4ee61af7674c5d9e5a3fbcf6cd887a40ad5a52c99aa36f1d4f9680 + md5: 23c255b008c4f2ae008f81edcabaca89 + depends: + - _libgcc_mutex 0.1 conda_forge + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + size: 460218 + timestamp: 1724801743478 +- kind: conda + name: ripgrep + version: 14.1.0 + build: he8a937b_0 + subdir: linux-64 + url: https://conda.anaconda.org/conda-forge/linux-64/ripgrep-14.1.0-he8a937b_0.conda + sha256: 4fcf37724b87440765cb3c6cf573e99d12fc631001426a0309d132f495c3d62a + md5: 5a476f7033a8a1b9175626b5ebf86d1d + depends: + - libgcc-ng >=12 + license: MIT + license_family: MIT + size: 1683808 + timestamp: 1705520837423 diff --git a/src/global/test_data/lockfiles/ripgrep_bat.lock b/src/global/test_data/lockfiles/ripgrep_bat.lock new file mode 100644 index 000000000..db8c94dbe --- /dev/null +++ b/src/global/test_data/lockfiles/ripgrep_bat.lock @@ -0,0 +1,121 @@ +version: 5 +environments: + default: + channels: + - url: https://conda.anaconda.org/conda-forge/ + packages: + linux-64: + - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/bat-0.24.0-he8a937b_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-14.1.0-h77fa898_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-14.1.0-h69a702a_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-14.1.0-h77fa898_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ripgrep-14.1.0-he8a937b_0.conda +packages: +- kind: conda + name: _libgcc_mutex + version: '0.1' + build: conda_forge + subdir: linux-64 + url: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 + sha256: fe51de6107f9edc7aa4f786a70f4a883943bc9d39b3bb7307c04c41410990726 + md5: d7c89558ba9fa0495403155b64376d81 + license: None + size: 2562 + timestamp: 1578324546067 +- kind: conda + name: _openmp_mutex + version: '4.5' + build: 2_gnu + build_number: 16 + subdir: linux-64 + url: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 + sha256: fbe2c5e56a653bebb982eda4876a9178aedfc2b545f25d0ce9c4c0b508253d22 + md5: 73aaf86a425cc6e73fcf236a5a46396d + depends: + - _libgcc_mutex 0.1 conda_forge + - libgomp >=7.5.0 + constrains: + - openmp_impl 9999 + license: BSD-3-Clause + license_family: BSD + size: 23621 + timestamp: 1650670423406 +- kind: conda + name: bat + version: 0.24.0 + build: he8a937b_0 + subdir: linux-64 + url: https://conda.anaconda.org/conda-forge/linux-64/bat-0.24.0-he8a937b_0.conda + sha256: fd0a7aae7f4c52ddf2ac5098dcb9a8f4b7ab1ccdd88633390a73a9d1be3b7de2 + md5: 18da2a0103ba121e1425266e7ba51327 + depends: + - libgcc-ng >=12 + license: MIT + license_family: MIT + size: 2527297 + timestamp: 1697062695381 +- kind: conda + name: libgcc + version: 14.1.0 + build: h77fa898_1 + build_number: 1 + subdir: linux-64 + url: https://conda.anaconda.org/conda-forge/linux-64/libgcc-14.1.0-h77fa898_1.conda + sha256: 10fa74b69266a2be7b96db881e18fa62cfa03082b65231e8d652e897c4b335a3 + md5: 002ef4463dd1e2b44a94a4ace468f5d2 + depends: + - _libgcc_mutex 0.1 conda_forge + - _openmp_mutex >=4.5 + constrains: + - libgomp 14.1.0 h77fa898_1 + - libgcc-ng ==14.1.0=*_1 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + size: 846380 + timestamp: 1724801836552 +- kind: conda + name: libgcc-ng + version: 14.1.0 + build: h69a702a_1 + build_number: 1 + subdir: linux-64 + url: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-14.1.0-h69a702a_1.conda + sha256: b91f7021e14c3d5c840fbf0dc75370d6e1f7c7ff4482220940eaafb9c64613b7 + md5: 1efc0ad219877a73ef977af7dbb51f17 + depends: + - libgcc 14.1.0 h77fa898_1 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + size: 52170 + timestamp: 1724801842101 +- kind: conda + name: libgomp + version: 14.1.0 + build: h77fa898_1 + build_number: 1 + subdir: linux-64 + url: https://conda.anaconda.org/conda-forge/linux-64/libgomp-14.1.0-h77fa898_1.conda + sha256: c96724c8ae4ee61af7674c5d9e5a3fbcf6cd887a40ad5a52c99aa36f1d4f9680 + md5: 23c255b008c4f2ae008f81edcabaca89 + depends: + - _libgcc_mutex 0.1 conda_forge + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + size: 460218 + timestamp: 1724801743478 +- kind: conda + name: ripgrep + version: 14.1.0 + build: he8a937b_0 + subdir: linux-64 + url: https://conda.anaconda.org/conda-forge/linux-64/ripgrep-14.1.0-he8a937b_0.conda + sha256: 4fcf37724b87440765cb3c6cf573e99d12fc631001426a0309d132f495c3d62a + md5: 5a476f7033a8a1b9175626b5ebf86d1d + depends: + - libgcc-ng >=12 + license: MIT + license_family: MIT + size: 1683808 + timestamp: 1705520837423