diff --git a/src/lock_file/satisfiability.rs b/src/lock_file/satisfiability.rs index 7a4f4bfc6..1fed49348 100644 --- a/src/lock_file/satisfiability.rs +++ b/src/lock_file/satisfiability.rs @@ -48,6 +48,9 @@ pub enum PlatformUnsat { #[error("there are more conda packages in the lock-file than are used by the environment")] TooManyCondaPackages, + #[error("missing purls")] + MissingPurls, + #[error("corrupted lock-file entry for '{0}'")] CorruptedEntry(String, ConversionError), @@ -167,6 +170,20 @@ pub fn verify_platform_satisfiability( } } + // to reflect new purls for pypi packages + // we need to invalidate the locked environment + // if all conda packages have empty purls + if environment.has_pypi_dependencies() + && pypi_packages.is_empty() + && !conda_packages + .iter() + .any(|record| !record.package_record.purls.is_empty()) + { + { + return Err(PlatformUnsat::MissingPurls); + } + } + // Create a lookup table from package name to package record. Returns an error if we find a // duplicate entry for a record let repodata_records_by_name = match RepoDataRecordsByName::from_unique_iter(conda_packages) { diff --git a/src/lock_file/update.rs b/src/lock_file/update.rs index 0f69ef3bd..966b13f6a 100644 --- a/src/lock_file/update.rs +++ b/src/lock_file/update.rs @@ -1470,6 +1470,19 @@ async fn spawn_solve_pypi_task( tokio::join!(repodata_records, prefix, semaphore.acquire_owned()); let environment_name = environment.name().clone(); + + let pypi_name_mapping_location = environment.project().pypi_name_mapping_source(); + + let mut conda_records = repodata_records.records.clone(); + + pypi_mapping::amend_pypi_purls( + environment.project().client().clone(), + pypi_name_mapping_location, + &mut conda_records, + None, + ) + .await?; + // let (pypi_packages, duration) = tokio::spawn( let (pypi_packages, duration) = async move { let pb = SolveProgressBar::new( @@ -1493,7 +1506,7 @@ async fn spawn_solve_pypi_task( .map(|(name, requirement)| (name.as_normalized().clone(), requirement)) .collect(), system_requirements, - &repodata_records.records, + &conda_records, platform, &pb.pb, &python_path, diff --git a/src/pypi_mapping/prefix_pypi_name_mapping.rs b/src/pypi_mapping/prefix_pypi_name_mapping.rs index 6421a52a1..989b3cdac 100644 --- a/src/pypi_mapping/prefix_pypi_name_mapping.rs +++ b/src/pypi_mapping/prefix_pypi_name_mapping.rs @@ -64,6 +64,11 @@ pub async fn conda_pypi_name_mapping( ) -> miette::Result> { let filtered_packages = conda_packages .iter() + // because we later skip adding purls for packages + // that have purls + // here we only filter packages that don't them + // to save some requests + .filter(|package| package.package_record.purls.is_empty()) .filter_map(|package| { package .package_record diff --git a/tests/solve_group_tests.rs b/tests/solve_group_tests.rs index b38c17366..38a2d2b76 100644 --- a/tests/solve_group_tests.rs +++ b/tests/solve_group_tests.rs @@ -1,8 +1,12 @@ +use std::str::FromStr; + use crate::common::{ package_database::{Package, PackageDatabase}, LockFileExt, PixiControl, }; -use rattler_conda_types::Platform; +use rattler_conda_types::{PackageName, Platform}; +use rattler_lock::DEFAULT_ENVIRONMENT_NAME; +use serial_test::serial; use tempfile::TempDir; use url::Url; @@ -83,3 +87,64 @@ async fn conda_solve_group_functionality() { "test should contain bar" ); } + +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[serial] +// #[cfg_attr(not(feature = "slow_integration_tests"), ignore)] +async fn test_purl_are_added_for_pypi() { + let pixi = PixiControl::new().unwrap(); + pixi.init().await.unwrap(); + // Add and update lockfile with this version of python + pixi.add("boltons").with_install(true).await.unwrap(); + + let lock_file = pixi.up_to_date_lock_file().await.unwrap(); + + // Check if boltons has a purl + lock_file + .default_environment() + .unwrap() + .packages(Platform::current()) + .unwrap() + .for_each(|dep| { + if dep.as_conda().unwrap().package_record().name + == PackageName::from_str("boltons").unwrap() + { + assert!(dep.as_conda().unwrap().package_record().purls.is_empty()); + } + }); + + // Add boltons from pypi + pixi.add("boltons") + .with_install(true) + .set_type(pixi::DependencyType::PypiDependency) + .await + .unwrap(); + + let lock_file = pixi.up_to_date_lock_file().await.unwrap(); + + // Check if boltons has a purl + lock_file + .default_environment() + .unwrap() + .packages(Platform::current()) + .unwrap() + .for_each(|dep| { + if dep.as_conda().unwrap().package_record().name + == PackageName::from_str("boltons").unwrap() + { + assert!(!dep.as_conda().unwrap().package_record().purls.is_empty()); + } + }); + + // Check if boltons exists only as conda dependency + assert!(lock_file.contains_match_spec( + DEFAULT_ENVIRONMENT_NAME, + Platform::current(), + "boltons" + )); + assert!(!lock_file.contains_pypi_package( + DEFAULT_ENVIRONMENT_NAME, + Platform::current(), + "boltons" + )); +}