diff --git a/Cargo.lock b/Cargo.lock index 5179d688f8..ba9af29361 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5180,9 +5180,9 @@ dependencies = [ [[package]] name = "resolvo" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037a5f3f35329f29885132d030c628f3142d2a098d0abb30513c572b900692fd" +checksum = "670175f9a825ad2419bea0e14bfe74e5dcc0227ec7a652a655b1c11e2b911754" dependencies = [ "ahash", "bitvec", diff --git a/crates/rattler-bin/Cargo.toml b/crates/rattler-bin/Cargo.toml index ab4ee8cafe..1a3e9c755f 100644 --- a/crates/rattler-bin/Cargo.toml +++ b/crates/rattler-bin/Cargo.toml @@ -29,7 +29,7 @@ indicatif = { workspace = true } once_cell = { workspace = true } rattler = { workspace = true, features = ["indicatif"] } rattler_conda_types = { workspace = true, default-features = false } -rattler_networking = { workspace = true, default-features = false, features = ["gcs", "s3", "system-integration"] } +rattler_networking = { workspace = true, default-features = false, features = ["gcs", "s3", "system-integration", "netrc-rs"] } rattler_repodata_gateway = { workspace = true, default-features = false, features = ["gateway"] } rattler_solve = { workspace = true, default-features = false, features = ["resolvo", "libsolv_c"] } rattler_virtual_packages = { workspace = true, default-features = false } diff --git a/crates/rattler-bin/src/commands/create.rs b/crates/rattler-bin/src/commands/create.rs index a55760f4a3..0d08ec6320 100644 --- a/crates/rattler-bin/src/commands/create.rs +++ b/crates/rattler-bin/src/commands/create.rs @@ -295,7 +295,7 @@ pub async fn create(opt: Opt) -> anyhow::Result<()> { if transaction.operations.is_empty() { println!("No operations necessary"); } else { - print_transaction(&transaction, solver_result.features); + print_transaction(&transaction, solver_result.extras); } return Ok(()); @@ -326,7 +326,7 @@ pub async fn create(opt: Opt) -> anyhow::Result<()> { console::style(console::Emoji("✔", "")).green(), install_start.elapsed() ); - print_transaction(&result.transaction, solver_result.features); + print_transaction(&result.transaction, solver_result.extras); } Ok(()) diff --git a/crates/rattler_conda_types/src/repo_data_record.rs b/crates/rattler_conda_types/src/repo_data_record.rs index 82ef880106..27a3fcd273 100644 --- a/crates/rattler_conda_types/src/repo_data_record.rs +++ b/crates/rattler_conda_types/src/repo_data_record.rs @@ -40,5 +40,5 @@ pub struct SolverResult { /// The records that are part of the solution to the solver task. pub records: Vec, /// The features of the records that are part of the solution to the solver task. - pub features: HashMap>, + pub extras: HashMap>, } diff --git a/crates/rattler_solve/Cargo.toml b/crates/rattler_solve/Cargo.toml index c270337476..ed8cf84674 100644 --- a/crates/rattler_solve/Cargo.toml +++ b/crates/rattler_solve/Cargo.toml @@ -41,7 +41,7 @@ default = ["resolvo"] libsolv_c = ["dep:rattler_libsolv_c", "dep:libc"] resolvo_diagnostics = ["resolvo?/diagnostics"] resolvo = ["dep:resolvo", "dep:futures"] -experimental_extras = [] +experimental_extras = ["rattler_conda_types/experimental_extras"] [[bench]] name = "bench" diff --git a/crates/rattler_solve/benches/sorting_bench.rs b/crates/rattler_solve/benches/sorting_bench.rs index d1dc3df82d..772ee296d3 100644 --- a/crates/rattler_solve/benches/sorting_bench.rs +++ b/crates/rattler_solve/benches/sorting_bench.rs @@ -40,9 +40,7 @@ fn bench_sort(c: &mut Criterion, sparse_repo_data: &SparseRepoData, spec: &str) ) .expect("failed to create dependency provider"); - let name = dependency_provider - .pool - .intern_package_name(package_name.as_normalized()); + let name = dependency_provider.pool.intern_package_name(&package_name); let version_set = dependency_provider .pool .intern_version_set(name, match_spec.into_nameless().1.into()); diff --git a/crates/rattler_solve/src/libsolv_c/mod.rs b/crates/rattler_solve/src/libsolv_c/mod.rs index b643b2e0e9..d9688477d1 100644 --- a/crates/rattler_solve/src/libsolv_c/mod.rs +++ b/crates/rattler_solve/src/libsolv_c/mod.rs @@ -287,7 +287,7 @@ impl super::SolverImpl for Solver { Ok(SolverResult { records: required_records, - features: HashMap::new(), + extras: HashMap::new(), }) } } diff --git a/crates/rattler_solve/src/resolvo/conda_sorting.rs b/crates/rattler_solve/src/resolvo/conda_sorting.rs index 3747dc26e9..96726e2b13 100644 --- a/crates/rattler_solve/src/resolvo/conda_sorting.rs +++ b/crates/rattler_solve/src/resolvo/conda_sorting.rs @@ -94,7 +94,7 @@ impl<'a, 'repo> SolvableSorter<'a, 'repo> { }; // Otherwise, select the variant with the highest version - match (self.strategy, a_record.version().cmp(b_record.version())) { + match (self.strategy, a_record.version().cmp(&b_record.version())) { (CompareStrategy::Default, Ordering::Greater) | (CompareStrategy::LowestVersion, Ordering::Less) => return Ordering::Less, (CompareStrategy::Default, Ordering::Less) @@ -359,29 +359,32 @@ pub(super) fn find_highest_version( let pool = &solver.provider().pool; - candidates + let mut highest_version = None; + for record in candidates .iter() .map(|id| &pool.resolve_solvable(*id).record) - .fold(None, |init, record| { - Some(init.map_or_else( - || { - ( - record.version().clone(), - !record.track_features().is_empty(), - ) - }, - |(version, has_tracked_features)| { - if &version < record.version() { - ( - record.version().clone(), - !record.track_features().is_empty(), - ) - } else { - (version, has_tracked_features) - } - }, - )) - }) + { + let (version, has_tracked_features) = match record { + SolverPackageRecord::Record(record) => ( + record.package_record.version.version(), + !record.package_record.track_features.is_empty(), + ), + SolverPackageRecord::VirtualPackage(record) => (&record.version, false), + SolverPackageRecord::Extra { .. } => continue, + }; + highest_version = highest_version.map_or_else( + || Some((version.clone(), has_tracked_features)), + |(highest_version, current_has_tracked_features)| { + if version > &highest_version { + Some((version.clone(), has_tracked_features)) + } else { + Some((highest_version, current_has_tracked_features)) + } + }, + ); + } + + highest_version }) .clone() } diff --git a/crates/rattler_solve/src/resolvo/mod.rs b/crates/rattler_solve/src/resolvo/mod.rs index f6661fbb59..787d22c603 100644 --- a/crates/rattler_solve/src/resolvo/mod.rs +++ b/crates/rattler_solve/src/resolvo/mod.rs @@ -6,16 +6,14 @@ use std::{ collections::{HashMap, HashSet}, fmt::{Display, Formatter}, marker::PhantomData, - ops::Deref, }; use chrono::{DateTime, Utc}; use conda_sorting::SolvableSorter; use itertools::Itertools; use rattler_conda_types::{ - package::ArchiveType, version_spec::EqualityOperator, BuildNumberSpec, GenericVirtualPackage, - MatchSpec, Matches, NamelessMatchSpec, OrdOperator, PackageName, PackageRecord, - ParseMatchSpecError, ParseStrictness, RepoDataRecord, SolverResult, StringMatcher, VersionSpec, + package::ArchiveType, GenericVirtualPackage, MatchSpec, Matches, NamelessMatchSpec, + PackageName, ParseMatchSpecError, ParseStrictness, RepoDataRecord, SolverResult, }; use resolvo::{ utils::{Pool, VersionSet}, @@ -51,52 +49,35 @@ impl<'a> FromIterator<&'a RepoDataRecord> for RepoData<'a> { impl<'a> SolverRepoData<'a> for RepoData<'a> {} /// Wrapper around `MatchSpec` so that we can use it in the `resolvo` pool +#[allow(clippy::large_enum_variant)] #[derive(Debug, Clone, Eq, PartialEq, Hash)] -pub struct SolverMatchSpec<'a> { - inner: NamelessMatchSpec, - feature: Option, - _marker: PhantomData<&'a PackageRecord>, -} +pub enum SolverMatchSpec<'a> { + /// Represents a requirement on another package. + MatchSpec(NamelessMatchSpec), -impl<'a> SolverMatchSpec<'a> { - /// Returns a reference to this match spec with the given feature enabled - pub fn with_feature(&self, feature: String) -> SolverMatchSpec<'a> { - Self { - inner: self.inner.clone(), - feature: Some(feature), - _marker: self._marker, - } - } + /// The name already uniquely identifies a single package. So we don't need + /// any special "spec". here. + Extra, - /// Returns a mutable reference to this match spec after enabling the given - /// feature - pub fn set_feature(&mut self, feature: String) -> &SolverMatchSpec<'a> { - self.feature = Some(feature); - self - } + /// A helper variant to make sure we can add a lifetime to this enum. + _Phantom(PhantomData<&'a ()>), } impl From for SolverMatchSpec<'_> { fn from(value: NamelessMatchSpec) -> Self { - Self { - inner: value, - feature: None, - _marker: PhantomData, - } + SolverMatchSpec::MatchSpec(value) } } impl Display for SolverMatchSpec<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.inner) - } -} - -impl Deref for SolverMatchSpec<'_> { - type Target = NamelessMatchSpec; - - fn deref(&self) -> &Self::Target { - &self.inner + match self { + SolverMatchSpec::MatchSpec(spec) => { + write!(f, "{spec}") + } + SolverMatchSpec::Extra => Ok(()), + SolverMatchSpec::_Phantom(_) => unreachable!(), + } } } @@ -104,17 +85,25 @@ impl<'a> VersionSet for SolverMatchSpec<'a> { type V = SolverPackageRecord<'a>; } -/// Wrapper around [`PackageRecord`] so that we can use it in resolvo pool +/// Wrapper around [`RepoDataRecord`] so that we can use it in resolvo pool. +/// Also represents a virtual package or an extra of a package. #[derive(Eq, PartialEq)] pub enum SolverPackageRecord<'a> { /// Represents a record from the repodata Record(&'a RepoDataRecord), - /// Represents a record with a specific feature enabled - RecordWithFeature(&'a RepoDataRecord, String), - /// Represents a virtual package. VirtualPackage(&'a GenericVirtualPackage), + + /// Represents a named extra for a particular package name. e.g. + /// `numpy[blas]` + Extra { + /// The name of the package + package: PackageName, + + /// The extra to activate in the package + extra: String, + }, } impl PartialOrd for SolverPackageRecord<'_> { @@ -127,7 +116,8 @@ impl Ord for SolverPackageRecord<'_> { fn cmp(&self, other: &Self) -> Ordering { self.name() .cmp(other.name()) - .then_with(|| self.version().cmp(other.version())) + .then_with(|| self.extra().cmp(&other.extra())) + .then_with(|| self.version().cmp(&other.version())) .then_with(|| self.build_number().cmp(&other.build_number())) .then_with(|| self.timestamp().cmp(&other.timestamp())) } @@ -136,47 +126,46 @@ impl Ord for SolverPackageRecord<'_> { impl SolverPackageRecord<'_> { fn name(&self) -> &PackageName { match self { - SolverPackageRecord::Record(rec) | SolverPackageRecord::RecordWithFeature(rec, _) => { - &rec.package_record.name - } + SolverPackageRecord::Record(rec) => &rec.package_record.name, + SolverPackageRecord::Extra { package, .. } => package, SolverPackageRecord::VirtualPackage(rec) => &rec.name, } } - fn version(&self) -> &rattler_conda_types::Version { + fn extra(&self) -> Option<&String> { match self { - SolverPackageRecord::Record(rec) | SolverPackageRecord::RecordWithFeature(rec, _) => { - rec.package_record.version.version() - } - SolverPackageRecord::VirtualPackage(rec) => &rec.version, + SolverPackageRecord::Extra { extra, .. } => Some(extra), + SolverPackageRecord::Record(_) | SolverPackageRecord::VirtualPackage(_) => None, + } + } + + fn version(&self) -> Option<&rattler_conda_types::Version> { + match self { + SolverPackageRecord::Record(rec) => Some(rec.package_record.version.version()), + SolverPackageRecord::VirtualPackage(rec) => Some(&rec.version), + SolverPackageRecord::Extra { .. } => None, } } fn track_features(&self) -> &[String] { const EMPTY: [String; 0] = []; match self { - SolverPackageRecord::Record(rec) | SolverPackageRecord::RecordWithFeature(rec, _) => { - &rec.package_record.track_features - } - SolverPackageRecord::VirtualPackage(_rec) => &EMPTY, + SolverPackageRecord::Record(rec) => &rec.package_record.track_features, + SolverPackageRecord::Extra { .. } | SolverPackageRecord::VirtualPackage(..) => &EMPTY, } } fn build_number(&self) -> u64 { match self { - SolverPackageRecord::Record(rec) | SolverPackageRecord::RecordWithFeature(rec, _) => { - rec.package_record.build_number - } - SolverPackageRecord::VirtualPackage(_rec) => 0, + SolverPackageRecord::Record(rec) => rec.package_record.build_number, + SolverPackageRecord::Extra { .. } | SolverPackageRecord::VirtualPackage(..) => 0, } } fn timestamp(&self) -> Option<&chrono::DateTime> { match self { - SolverPackageRecord::Record(rec) | SolverPackageRecord::RecordWithFeature(rec, _) => { - rec.package_record.timestamp.as_ref() - } - SolverPackageRecord::VirtualPackage(_rec) => None, + SolverPackageRecord::Record(rec) => rec.package_record.timestamp.as_ref(), + SolverPackageRecord::Extra { .. } | SolverPackageRecord::VirtualPackage(..) => None, } } } @@ -187,8 +176,8 @@ impl Display for SolverPackageRecord<'_> { SolverPackageRecord::Record(rec) => { write!(f, "{}", &rec.package_record) } - SolverPackageRecord::RecordWithFeature(rec, feature) => { - write!(f, "{}[{}]", &rec.package_record, feature) + SolverPackageRecord::Extra { package, extra } => { + write!(f, "{}[{}]", package.as_normalized(), extra) } SolverPackageRecord::VirtualPackage(rec) => { write!(f, "{rec}") @@ -200,18 +189,24 @@ impl Display for SolverPackageRecord<'_> { /// Represents the type of name that is being used in the pool. #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub enum NameType { - /// A package name without a feature. + /// A simple package Base(String), - /// A package name with a feature. - BaseWithFeature(String, String), + /// An extra of a package (e.g. `numpy[blas]`) + Extra { + /// The package name + package: String, + + /// The extra to activate. + extra: String, + }, } impl Display for NameType { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { NameType::Base(name) => write!(f, "{name}"), - NameType::BaseWithFeature(name, feature) => write!(f, "{name}[{feature}]"), + NameType::Extra { package, extra } => write!(f, "{package}[{extra}]"), } } } @@ -219,14 +214,20 @@ impl Display for NameType { impl Ord for NameType { fn cmp(&self, other: &Self) -> std::cmp::Ordering { match (self, other) { - // Compare names first - (NameType::Base(name1), NameType::Base(name2)) - | (NameType::BaseWithFeature(name1, _), NameType::BaseWithFeature(name2, _)) => { - name1.cmp(name2) - } - // WithoutFeature comes before WithFeature - (NameType::Base(_), NameType::BaseWithFeature(_, _)) => std::cmp::Ordering::Greater, - (NameType::BaseWithFeature(_, _), NameType::Base(_)) => std::cmp::Ordering::Less, + // Compare names first, then extras + (NameType::Base(name1), NameType::Base(name2)) => name1.cmp(name2), + ( + NameType::Extra { + package: a, + extra: extra_a, + }, + NameType::Extra { + package: b, + extra: extra_b, + }, + ) => a.cmp(b).then_with(|| extra_a.cmp(extra_b)), + (NameType::Base(_), NameType::Extra { .. }) => std::cmp::Ordering::Greater, + (NameType::Extra { .. }, NameType::Base(_)) => std::cmp::Ordering::Less, } } } @@ -237,9 +238,9 @@ impl PartialOrd for NameType { } } -impl From<&str> for NameType { - fn from(value: &str) -> Self { - NameType::Base(value.to_owned()) +impl From<&PackageName> for NameType { + fn from(value: &PackageName) -> Self { + NameType::Base(value.as_normalized().to_owned()) } } @@ -250,7 +251,9 @@ impl From<&str> for NameType { pub struct CondaDependencyProvider<'a> { /// The pool that deduplicates data used by the provider. pub pool: Pool, NameType>, + name_to_condition: RefCell>, + /// Holds all the cached candidates for each package name. records: HashMap, matchspec_to_highest_version: @@ -284,7 +287,7 @@ impl<'a> CondaDependencyProvider<'a> { // Add virtual packages to the records for virtual_package in virtual_packages { - let name = pool.intern_package_name(virtual_package.name.as_normalized()); + let name = pool.intern_package_name(&virtual_package.name); let solvable = pool.intern_solvable(name, SolverPackageRecord::VirtualPackage(virtual_package)); records.entry(name).or_default().candidates.push(solvable); @@ -294,7 +297,7 @@ impl<'a> CondaDependencyProvider<'a> { let direct_dependencies = match_specs .iter() .filter_map(|spec| spec.name.as_ref()) - .map(|name| pool.intern_package_name(name.as_normalized())) + .map(|name| pool.intern_package_name(name)) .collect(); // TODO: Normalize these channel names to urls so we can compare them correctly. @@ -376,131 +379,104 @@ impl<'a> CondaDependencyProvider<'a> { } for record in ordered_repodata { - let package_name = - pool.intern_package_name(record.package_record.name.as_normalized()); + let package_name = pool.intern_package_name(&record.package_record.name); let solvable_id = pool.intern_solvable(package_name, SolverPackageRecord::Record(record)); - // Collect all candidates first - let mut all_entries = vec![(package_name, solvable_id)]; + // Update records with all entries in a single mutable borrow + let candidates = records.entry(package_name).or_default(); + candidates.candidates.push(solvable_id); - // Add feature-enabled solvables - for feature in record.package_record.experimental_extra_depends.keys() { - let package_name_with_feature = - pool.intern_package_name(NameType::BaseWithFeature( - record.package_record.name.as_normalized().to_owned(), - feature.clone(), - )); - let feature_solvable = pool.intern_solvable( - package_name_with_feature, - SolverPackageRecord::RecordWithFeature(record, feature.clone()), - ); - let package_name_with_feature = - pool.intern_package_name(NameType::BaseWithFeature( - record.package_record.name.as_normalized().to_owned(), - feature.clone(), + // Filter out any records that are newer than a specific date. + match (&exclude_newer, &record.package_record.timestamp) { + (Some(exclude_newer), Some(record_timestamp)) + if record_timestamp > exclude_newer => + { + let reason = pool.intern_string(format!( + "the package is uploaded after the cutoff date of {exclude_newer}" )); - - all_entries.push((package_name_with_feature, feature_solvable)); - } - - // Update records with all entries in a single mutable borrow - for (pkg_name, solvable) in all_entries { - let candidates = records.entry(pkg_name).or_default(); - candidates.candidates.push(solvable); - - // Filter out any records that are newer than a specific date. - match (&exclude_newer, &record.package_record.timestamp) { - (Some(exclude_newer), Some(record_timestamp)) - if record_timestamp > exclude_newer => - { - let reason = pool.intern_string(format!( - "the package is uploaded after the cutoff date of {exclude_newer}" - )); - candidates.excluded.push((solvable, reason)); - } - _ => {} + candidates.excluded.push((solvable_id, reason)); } + _ => {} + } - // Add to excluded when package is not in the specified channel. - if !channel_specific_specs.is_empty() { - if let Some(spec) = channel_specific_specs.iter().find(|&&spec| { - spec.name - .as_ref() - .expect("expecting a name") - .as_normalized() - == record.package_record.name.as_normalized() - }) { - // Check if the spec has a channel, and compare it to the repodata - // channel - if let Some(spec_channel) = &spec.channel { - if record.channel.as_ref() != Some(&spec_channel.canonical_name()) { - tracing::debug!("Ignoring {} {} because it was not requested from that channel.", &record.package_record.name.as_normalized(), match &record.channel { + // Add to excluded when package is not in the specified channel. + if !channel_specific_specs.is_empty() { + if let Some(spec) = channel_specific_specs.iter().find(|&&spec| { + spec.name + .as_ref() + .expect("expecting a name") + .as_normalized() + == record.package_record.name.as_normalized() + }) { + // Check if the spec has a channel, and compare it to the repodata + // channel + if let Some(spec_channel) = &spec.channel { + if record.channel.as_ref() != Some(&spec_channel.canonical_name()) { + tracing::debug!("Ignoring {} {} because it was not requested from that channel.", &record.package_record.name.as_normalized(), match &record.channel { Some(channel) => format!("from {}", &channel), None => "without a channel".to_string(), }); - // Add record to the excluded with reason of being in the non - // requested channel. - let message = format!( - "candidate not in requested channel: '{}'", - spec_channel - .name - .clone() - .unwrap_or(spec_channel.base_url.to_string()) - ); - candidates - .excluded - .push((solvable, pool.intern_string(message))); - continue; - } + // Add record to the excluded with reason of being in the non + // requested channel. + let message = format!( + "candidate not in requested channel: '{}'", + spec_channel + .name + .clone() + .unwrap_or(spec_channel.base_url.to_string()) + ); + candidates + .excluded + .push((solvable_id, pool.intern_string(message))); + continue; } } } + } - // Enforce channel priority - if let (Some(first_channel), ChannelPriority::Strict) = ( - package_name_found_in_channel - .get(record.package_record.name.as_normalized()), - channel_priority, - ) { - // Add the record to the excluded list when it is from a different channel. - if first_channel != &&record.channel { - if let Some(channel) = &record.channel { - tracing::debug!( - "Ignoring '{}' from '{}' because of strict channel priority.", - &record.package_record.name.as_normalized(), - channel - ); - candidates.excluded.push(( - solvable, - pool.intern_string(format!( - "due to strict channel priority not using this option from: '{channel}'", - )), - )); - } else { - tracing::debug!( + // Enforce channel priority + if let (Some(first_channel), ChannelPriority::Strict) = ( + package_name_found_in_channel.get(record.package_record.name.as_normalized()), + channel_priority, + ) { + // Add the record to the excluded list when it is from a different channel. + if first_channel != &&record.channel { + if let Some(channel) = &record.channel { + tracing::debug!( + "Ignoring '{}' from '{}' because of strict channel priority.", + &record.package_record.name.as_normalized(), + channel + ); + candidates.excluded.push(( + solvable_id, + pool.intern_string(format!( + "due to strict channel priority not using this option from: '{channel}'", + )), + )); + } else { + tracing::debug!( "Ignoring '{}' without a channel because of strict channel priority.", &record.package_record.name.as_normalized(), ); - candidates.excluded.push(( - solvable, - pool.intern_string("due to strict channel priority not using from an unknown channel".to_string()), - )); - } + candidates.excluded.push(( + solvable_id, + pool.intern_string("due to strict channel priority not using from an unknown channel".to_string()), + )); } - } else { - package_name_found_in_channel.insert( - record.package_record.name.as_normalized().to_string(), - &record.channel, - ); } + } else { + package_name_found_in_channel.insert( + record.package_record.name.as_normalized().to_string(), + &record.channel, + ); } } } // Add favored packages to the records for favored_record in favored_records { - let name = pool.intern_package_name(favored_record.package_record.name.as_normalized()); + let name = pool.intern_package_name(&favored_record.package_record.name); let solvable = pool.intern_solvable(name, SolverPackageRecord::Record(favored_record)); let candidates = records.entry(name).or_default(); candidates.candidates.push(solvable); @@ -508,7 +484,7 @@ impl<'a> CondaDependencyProvider<'a> { } for locked_record in locked_records { - let name = pool.intern_package_name(locked_record.package_record.name.as_normalized()); + let name = pool.intern_package_name(&locked_record.package_record.name); let solvable = pool.intern_solvable(name, SolverPackageRecord::Record(locked_record)); let candidates = records.entry(name).or_default(); candidates.candidates.push(solvable); @@ -522,6 +498,7 @@ impl<'a> CondaDependencyProvider<'a> { Ok(Self { pool, + name_to_condition: RefCell::default(), records, matchspec_to_highest_version: RefCell::default(), parse_match_spec_cache: RefCell::default(), @@ -532,9 +509,22 @@ impl<'a> CondaDependencyProvider<'a> { } /// Returns all package names - pub fn package_names(&self) -> impl Iterator + '_ { + pub fn package_names(&self) -> impl Iterator + use<'_, 'a> { self.records.keys().copied() } + + fn extra_condition(&self, package: &PackageName, extra: &str) -> ConditionId { + let name_id = self.pool.intern_package_name(NameType::Extra { + package: package.as_normalized().to_owned(), + extra: extra.to_owned(), + }); + let mut name_to_condition = self.name_to_condition.borrow_mut(); + *name_to_condition.entry(name_id).or_insert_with(|| { + let version_set = extra_version_set(&self.pool, package.clone(), extra.to_owned()); + self.pool + .intern_condition(Condition::Requirement(version_set)) + }) + } } /// The reason why the solver was cancelled @@ -566,12 +556,13 @@ impl Interner for CondaDependencyProvider<'_> { let versions = solvables .iter() - .map(|&id| self.pool.resolve_solvable(id).record.version()) + .filter_map(|&id| self.pool.resolve_solvable(id).record.version()) .sorted() .format(" | "); let name = self.display_solvable_name(solvables[0]); - format!("{name} {versions}") + let result = format!("{name} {versions}"); + result.trim_end().to_string() } fn display_name(&self, name: NameId) -> impl Display + '_ { @@ -630,112 +621,100 @@ impl DependencyProvider for CondaDependencyProvider<'_> { } async fn get_candidates(&self, name: NameId) -> Option { - self.records.get(&name).cloned() + match self.pool.resolve_package_name(name) { + NameType::Base(_) => self.records.get(&name).cloned(), + NameType::Extra { package, extra } => { + // For extras, we need to create a new candidates object + // that contains only the extra solvable. + let extra_solvable = add_extra( + &self.pool, + PackageName::new_unchecked(package), + extra.clone(), + ); + Some(Candidates { + candidates: vec![extra_solvable], + favored: None, + locked: None, + excluded: Vec::new(), + hint_dependencies_available: HintDependenciesAvailable::All, + }) + } + } } async fn get_dependencies(&self, solvable: SolvableId) -> Dependencies { let mut dependencies = KnownDependencies::default(); - // Get the record and any feature that might be enabled - let (record, feature) = match &self.pool.resolve_solvable(solvable).record { - SolverPackageRecord::Record(rec) => (rec, None), - SolverPackageRecord::RecordWithFeature(rec, feature) => (rec, Some(feature)), - SolverPackageRecord::VirtualPackage(_) => return Dependencies::Known(dependencies), + let record = match &self.pool.resolve_solvable(solvable).record { + SolverPackageRecord::Record(rec) => rec, + SolverPackageRecord::Extra { .. } | SolverPackageRecord::VirtualPackage(_) => { + return Dependencies::Known(dependencies) + } }; let mut parse_match_spec_cache = self.parse_match_spec_cache.borrow_mut(); - // If this is a feature-enabled package, add its feature dependencies - if let Some(feature_name) = feature { - // Find the feature's dependencies - if let Some(deps) = record - .package_record - .experimental_extra_depends - .get(feature_name) - { - // Add each dependency for this feature - for req in deps { - let version_set_id = match parse_match_spec( - &self.pool, - req, - &mut parse_match_spec_cache, - ) { - Ok(version_set_id) => version_set_id, - Err(e) => { - let reason = self.pool.intern_string(format!( - "the optional dependency '{req}' for feature '{feature_name}' failed to parse: {e}" - )); - return Dependencies::Unknown(reason); - } - }; - - dependencies - .requirements - .extend(version_set_id.into_iter().map(ConditionalRequirement::from)); - } + // Add regular dependencies + for depends in record.package_record.depends.iter() { + let version_set_id = + match parse_match_spec(&self.pool, depends, &mut parse_match_spec_cache) { + Ok(version_set_id) => version_set_id, + Err(e) => { + let reason = self.pool.intern_string(format!( + "the dependency '{depends}' failed to parse: {e}", + )); - // Add a dependency back to the base package with exact version - let base_spec = MatchSpec { - name: Some(record.package_record.name.clone()), - version: Some(VersionSpec::Exact( - EqualityOperator::Equals, - record.package_record.version.version().clone(), - )), - build: Some(StringMatcher::Exact(record.package_record.build.clone())), - build_number: Some(BuildNumberSpec::new( - OrdOperator::Eq, - record.package_record.build_number, - )), - subdir: Some(record.package_record.subdir.clone()), - md5: record.package_record.md5, - sha256: record.package_record.sha256, - extras: None, - ..Default::default() + return Dependencies::Unknown(reason); + } }; - let (name, nameless_spec) = base_spec.into_nameless(); - let name_id = self.pool.intern_package_name( - name.expect("cannot use matchspec without a name") - .as_normalized(), - ); - let version_set_id = self.pool.intern_version_set(name_id, nameless_spec.into()); - dependencies.requirements.push(version_set_id.into()); - } - } else { - // Add regular dependencies - for depends in record.package_record.depends.iter() { - let version_set_id = - match parse_match_spec(&self.pool, depends, &mut parse_match_spec_cache) { - Ok(version_set_id) => version_set_id, - Err(e) => { - let reason = self.pool.intern_string(format!( - "the dependency '{depends}' failed to parse: {e}", - )); + dependencies + .requirements + .extend(version_set_id.into_iter().map(ConditionalRequirement::from)); + } - return Dependencies::Unknown(reason); - } - }; + // Add constraints from the record + for constrains in record.package_record.constrains.iter() { + let version_set_ids = + match parse_match_spec(&self.pool, constrains, &mut parse_match_spec_cache) { + Ok(version_set_id) => version_set_id, + Err(e) => { + let reason = self.pool.intern_string(format!( + "the constrains '{constrains}' failed to parse: {e}", + )); - dependencies - .requirements - .extend(version_set_id.into_iter().map(ConditionalRequirement::from)); - } + return Dependencies::Unknown(reason); + } + }; + dependencies.constrains.extend(version_set_ids); + } - for constrains in record.package_record.constrains.iter() { - let version_set_id = - match parse_match_spec(&self.pool, constrains, &mut parse_match_spec_cache) { - Ok(version_set_id) => version_set_id, - Err(e) => { - let reason = self.pool.intern_string(format!( - "the constrains '{constrains}' failed to parse: {e}", - )); + // Add extras + for (extra, matchspec) in record + .package_record + .experimental_extra_depends + .iter() + .flat_map(|(extra, deps)| deps.iter().map(move |dep| (extra, dep))) + { + let version_set_ids = + match parse_match_spec(&self.pool, matchspec, &mut parse_match_spec_cache) { + Ok(version_set_id) => version_set_id, + Err(e) => { + let reason = self.pool.intern_string(format!( + "the constrains '{matchspec}' failed to parse: {e}", + )); - return Dependencies::Unknown(reason); - } - }; - for version_set_id in version_set_id { - dependencies.constrains.push(version_set_id); - } + return Dependencies::Unknown(reason); + } + }; + + // Add them as conditional requirements (e.g. `numpy; if extra`). + let condition_id = self.extra_condition(&record.package_record.name, extra); + for version_set_id in version_set_ids { + dependencies.requirements.push(ConditionalRequirement { + requirement: version_set_id.into(), + condition: Some(condition_id), + }); } } @@ -749,53 +728,54 @@ impl DependencyProvider for CondaDependencyProvider<'_> { inverse: bool, ) -> Vec { let spec = self.pool.resolve_version_set(version_set); - - candidates - .iter() - .copied() - .filter(|c| { - let record = &self.pool.resolve_solvable(*c).record; - match record { - SolverPackageRecord::Record(rec) => { - // Base package matches if spec matches and no features are required - - spec.matches(*rec) != inverse - } - SolverPackageRecord::RecordWithFeature(rec, feature) => { - // Feature-enabled package matches if spec matches and feature is required - - if spec.matches(*rec) { - if let Some(spec_feature) = &spec.feature { - (*spec_feature == *feature) != inverse - } else { - inverse + match spec { + SolverMatchSpec::MatchSpec(spec) => { + candidates + .iter() + .copied() + .filter(|c| { + let record = &self.pool.resolve_solvable(*c).record; + match record { + SolverPackageRecord::Record(rec) => { + // Base package matches if spec matches and no features are required + spec.matches(*rec) != inverse } - } else { - inverse - } - } - SolverPackageRecord::VirtualPackage(GenericVirtualPackage { - version, - build_string, - .. - }) => { - if let Some(spec) = spec.version.as_ref() { - if !spec.matches(version) { - return inverse; - } - } + SolverPackageRecord::VirtualPackage(GenericVirtualPackage { + version, + build_string, + .. + }) => { + if let Some(spec) = spec.version.as_ref() { + if !spec.matches(version) { + return inverse; + } + } + + if let Some(build_match) = spec.build.as_ref() { + if !build_match.matches(build_string) { + return inverse; + } + } - if let Some(build_match) = spec.build.as_ref() { - if !build_match.matches(build_string) { - return inverse; + !inverse + } + SolverPackageRecord::Extra { .. } => { + unreachable!("extras should never be compared to matchspecs") } } - - !inverse - } + }) + .collect() + } + SolverMatchSpec::Extra => { + // Extras are already filtered by name. + if inverse { + Vec::new() + } else { + candidates.to_vec() } - }) - .collect() + } + SolverMatchSpec::_Phantom(_) => unreachable!(), + } } fn should_cancel_with_value(&self) -> Option> { @@ -843,43 +823,18 @@ impl super::SolverImpl for Solver { // Construct the requirements that the solver needs to satisfy. let virtual_package_requirements = task.virtual_packages.iter().map(|spec| { - let name_id = provider.pool.intern_package_name(spec.name.as_normalized()); + let name_id = provider.pool.intern_package_name(&spec.name); provider .pool .intern_version_set(name_id, NamelessMatchSpec::default().into()) }); - let root_requirements = task.specs.iter().flat_map(|spec| { - let (name, nameless_spec) = spec.clone().into_nameless(); - let features = &spec.extras; - let name = name.expect("cannot use matchspec without a name"); - let name_id = provider.pool.intern_package_name(name.as_normalized()); - let mut reqs = vec![provider - .pool - .intern_version_set(name_id, nameless_spec.clone().into())]; - - // Add requirements for optional features if specified - if let Some(features) = features { - for feature in features { - // Create a version set that matches the feature-enabled package - let package_name_with_feature = - NameType::BaseWithFeature(name.as_normalized().to_owned(), feature.clone()); - let feature_name_id = - provider.pool.intern_package_name(package_name_with_feature); - - let mut solver_match_spec: SolverMatchSpec<'_> = nameless_spec.clone().into(); - let _ = solver_match_spec.set_feature(feature.clone()); - - let feature_version_set = provider - .pool - .intern_version_set(feature_name_id, solver_match_spec); - reqs.push(feature_version_set); - } - } - reqs - }); + let root_requirements = task + .specs + .into_iter() + .flat_map(|spec| version_sets_for_match_spec(&provider.pool, spec)); - let all_requirements: Vec = virtual_package_requirements + let all_requirements: Vec<_> = virtual_package_requirements .chain(root_requirements) .map(ConditionalRequirement::from) .collect(); @@ -888,9 +843,10 @@ impl super::SolverImpl for Solver { .constraints .iter() .map(|spec| { - let (name, spec) = spec.clone().into_nameless(); - let name = name.expect("cannot use matchspec without a name"); - let name_id = provider.pool.intern_package_name(name.as_normalized()); + let (Some(name), spec) = spec.clone().into_nameless() else { + unimplemented!("matchspecs without a name are not supported"); + }; + let name_id = provider.pool.intern_package_name(&name); provider.pool.intern_version_set(name_id, spec.into()) }) .collect(); @@ -913,7 +869,7 @@ impl super::SolverImpl for Solver { })?; // Get the resulting packages from the solver. - let mut features: HashMap> = HashMap::new(); + let mut extras: HashMap> = HashMap::new(); let mut records = Vec::new(); for id in solvables { @@ -921,17 +877,17 @@ impl super::SolverImpl for Solver { SolverPackageRecord::Record(rec) => { records.push((*rec).clone()); } - SolverPackageRecord::RecordWithFeature(rec, feature) => { - features - .entry(rec.package_record.name.clone()) + SolverPackageRecord::Extra { package, extra } => { + extras + .entry(package.clone()) .or_default() - .push(feature.clone()); + .push(extra.clone()); } SolverPackageRecord::VirtualPackage(_) => {} } } - Ok(SolverResult { records, features }) + Ok(SolverResult { records, extras }) } } @@ -944,39 +900,72 @@ fn parse_match_spec( return Ok(spec_id.clone()); } + // Parse the match spec and extract the name of the package it depends on. let match_spec = MatchSpec::from_str(spec_str, ParseStrictness::Lenient)?; - let (name, spec) = match_spec.into_nameless(); - let mut version_set_ids = vec![]; - if let Some(ref features) = spec.extras { - let spec_with_feature: SolverMatchSpec<'_> = spec.clone().into(); - - for feature in features { - let name_with_feature = NameType::BaseWithFeature( - name.as_ref() - .expect("Packages with no name are not supported") - .as_normalized() - .to_owned(), - feature.clone(), - ); - let dependency_name = pool.intern_package_name(name_with_feature); - - let version_set_id = pool.intern_version_set( - dependency_name, - spec_with_feature.with_feature(feature.clone()), - ); - version_set_ids.push(version_set_id); - } - } else { - let dependency_name = pool.intern_package_name( - name.as_ref() - .expect("Packages with no name are not supported") - .as_normalized(), - ); - let version_set_id = pool.intern_version_set(dependency_name, spec.into()); - version_set_ids.push(version_set_id); - } + // Get the version sets for the match spec. + let version_set_ids = version_sets_for_match_spec(pool, match_spec); + + // Store in the match spec cache parse_match_spec_cache.insert(spec_str.to_string(), version_set_ids.clone()); Ok(version_set_ids) } + +fn version_sets_for_match_spec( + pool: &Pool, NameType>, + spec: MatchSpec, +) -> Vec { + let (Some(name), spec) = spec.into_nameless() else { + unimplemented!("matchspecs without a name are not supported"); + }; + + // Add a dependency on each extra. + let mut version_set_ids = vec![]; + for extra in spec.extras.iter().flatten() { + version_set_ids.push(extra_version_set(pool, name.clone(), extra.clone())); + } + + // Create a version set for the match spec itself. + let dependency_name = pool.intern_package_name(&name); + let version_set_id = pool.intern_version_set(dependency_name, spec.into()); + version_set_ids.push(version_set_id); + + version_set_ids +} + +/// Adds a particular "extra" to the set of solvables +pub fn add_extra( + pool: &Pool, NameType>, + package_name: PackageName, + extra: String, +) -> SolvableId { + let name = NameType::Extra { + package: package_name.as_normalized().to_owned(), + extra: extra.clone(), + }; + let name_id = pool.intern_package_name(name); + + // Ensure that there is a single solvable for this extra. + pool.intern_solvable( + name_id, + SolverPackageRecord::Extra { + package: package_name, + extra, + }, + ) +} + +/// Returns a version set for a particular package name and extra. +pub fn extra_version_set( + pool: &Pool, NameType>, + package_name: PackageName, + extra: String, +) -> VersionSetId { + let name = NameType::Extra { + package: package_name.as_normalized().to_owned(), + extra, + }; + let name_id = pool.intern_package_name(name); + pool.intern_version_set(name_id, SolverMatchSpec::Extra) +} diff --git a/crates/rattler_solve/tests/backends.rs b/crates/rattler_solve/tests/backends.rs index b930c78eb8..0c526dd6b9 100644 --- a/crates/rattler_solve/tests/backends.rs +++ b/crates/rattler_solve/tests/backends.rs @@ -724,14 +724,13 @@ mod resolvo { use rattler_solve::{SolveStrategy, SolverImpl, SolverTask}; use url::Url; + #[cfg(feature = "experimental_extras")] + use super::dummy_channel_with_optional_dependencies_json_path; use super::{ dummy_channel_json_path, installed_package, solve, solve_real_world, FromStr, GenericVirtualPackage, SimpleSolveTask, SolveError, Version, }; - #[cfg(feature = "experimental_extras")] - use super::dummy_channel_with_optional_dependencies_json_path; - solver_backend_tests!(rattler_solve::resolvo::Solver); #[test] @@ -976,361 +975,117 @@ mod resolvo { insta::assert_snapshot!(result.unwrap_err()); } - #[cfg(feature = "experimental_extras")] - /// Installs `foo` while enabling a single optional dependency `[with-latest-bors]`. - /// This should pull in `bors >=2.0`. - #[test] - fn test_solve_dummy_repo_extra_depends_foo_latest_bors_resolvo() { - let mut result = solve::( - &[dummy_channel_with_optional_dependencies_json_path()], - SimpleSolveTask { - specs: &["foo[extras=[with-latest-bors]]"], - ..SimpleSolveTask::default() - }, - ) - .unwrap(); - - result - .records - .sort_by(|a, b| a.package_record.name.cmp(&b.package_record.name)); - - assert_eq!(result.records.len(), 2); - assert_eq!(result.records[1].package_record.name.as_normalized(), "foo"); - assert_eq!( - result.features.get("foo"), - Some(&vec!["with-latest-bors".to_string()]) - ); - assert_eq!( - result.records[1].package_record.version, - Version::from_str("2.0.2").unwrap(), - "expected lowest version of foobar" - ); - - assert_eq!( - result.records[0].package_record.name.as_normalized(), - "bors" - ); - assert_eq!( - result.records[0].package_record.version, - Version::from_str("2.1").unwrap(), - "expected highest compatible version of bors" - ); - } + /// A test that checks that extras can be used to select optional dependencies. #[cfg(feature = "experimental_extras")] - /// Installs `cuda-version` with `[with-cudadev]` which depends on `"foo >=4.0.2", "bar >=1.2.3"`. #[test] - fn test_solve_dummy_repo_extra_depends_cuda_dev_resolvo() { + fn test_optional_dependency() { let mut result = solve::( &[dummy_channel_with_optional_dependencies_json_path()], SimpleSolveTask { - specs: &["cuda-version[extras=[with-cudadev]]"], + specs: &["foo[extras=[with-bar]]"], ..SimpleSolveTask::default() }, ) .unwrap(); + // Sort the records by package name to make the test deterministic result .records .sort_by(|a, b| a.package_record.name.cmp(&b.package_record.name)); - assert_eq!(result.records.len(), 3); - assert_eq!(result.records[0].package_record.name.as_normalized(), "bar"); - - assert_eq!( - result.records[0].package_record.version, - Version::from_str("1.2.3").unwrap(), - "expected version 1.2.3 of bar" - ); - - // The cuda-version with feature `with-cudadev`: - assert_eq!( - result.records[1].package_record.name.as_normalized(), - "cuda-version" - ); - assert_eq!( - result.records[1].package_record.version, - Version::from_str("12.5").unwrap(), - "expected version 12.5 of cuda-version" - ); - - assert_eq!( - result.features.get("cuda-version"), - Some(&vec!["with-cudadev".to_string()]) - ); - - assert_eq!(result.records[2].package_record.name.as_normalized(), "foo"); - assert_eq!( - result.records[2].package_record.version, - Version::from_str("4.0.2").unwrap(), - "expected version 4.0.2 of foo" - ); - } - - #[cfg(feature = "experimental_extras")] - /// Attempts to enable two optional features that conflict: `[with-oldbors,with-latest-bors]`. - /// This should fail because one requests `bors <2.0` and the other requests `bors >=2.0`. - #[test] - fn test_solve_dummy_repo_extra_depends_conflict_resolvo() { - let result = solve::( - &[dummy_channel_with_optional_dependencies_json_path()], - SimpleSolveTask { - specs: &["foo[extras=[with-oldbors,with-latest-bors]]"], - ..SimpleSolveTask::default() - }, - ); - - insta::assert_snapshot!(result.unwrap_err()); - } - - #[cfg(feature = "experimental_extras")] - /// Enables multiple optional dependencies in the same spec (like `[with-baz2,with-bar]`). - /// This should pull in `baz >=2.0` and `bar >=1.2.3` if both can coexist. - #[test] - fn test_solve_dummy_repo_extra_depends_foo_multi_resolvo() { - let mut result = solve::( - &[dummy_channel_with_optional_dependencies_json_path()], - SimpleSolveTask { - specs: &["foo[extras=[with-baz2,with-bar]]"], - ..SimpleSolveTask::default() - }, - ) - .unwrap(); - - result - .records - .sort_by(|a, b| a.package_record.name.cmp(&b.package_record.name)); - - assert_eq!(result.records.len(), 3); + // Collect the extras into a vector + let extras = result.extras.into_iter().collect_vec(); + // Make sure we have two packages `foo` and `bar` + assert_eq!(result.records.len(), 2); assert_eq!(result.records[0].package_record.name.as_normalized(), "bar"); - assert_eq!( - result.records[0].package_record.version, - Version::from_str("1.2.3").unwrap(), - "expected version 1.2.3 of bar" - ); - - assert_eq!(result.records[1].package_record.name.as_normalized(), "baz"); - assert_eq!( - result.records[1].package_record.version, - Version::from_str("2.0").unwrap(), - "expected version 2.0 of baz" - ); - - assert_eq!(result.records[2].package_record.name.as_normalized(), "foo"); - assert_eq!( - result.records[2].package_record.version, - Version::from_str("3.0.2").unwrap(), - "expected version 3.0.2 of foo" - ); - let mut features = result.features.get("foo").unwrap().clone(); - features.sort(); - result.features.insert("foo".parse().unwrap(), features); - assert_eq!( - result.features.get("foo"), - Some(&vec!["with-bar".to_string(), "with-baz2".to_string()]) - ); - } - - #[cfg(feature = "experimental_extras")] - /// Should install xfoo with the feature with-issue717 which requires `with-issue717[with-bors21]` hence pulling in bors 2.1 as well - #[test] - fn test_solve_dummy_repo_extra_depends_xfoo_extra_depends_with_features() { - let mut result = solve::( - &[dummy_channel_with_optional_dependencies_json_path()], - SimpleSolveTask { - specs: &["xfoo[extras=[with-issue717]]"], - ..SimpleSolveTask::default() - }, - ) - .unwrap(); - - result - .records - .sort_by(|a, b| a.package_record.name.cmp(&b.package_record.name)); - - assert_eq!(result.records.len(), 3); - assert_eq!( - result.records[0].package_record.name.as_normalized(), - "bors" - ); - assert_eq!( - result.records[0].package_record.version, - Version::from_str("2.1").unwrap(), - "expected version 2.1 of bors" - ); - - assert_eq!( - result.records[1].package_record.name.as_normalized(), - "issue_717" - ); - assert_eq!( - result.records[1].package_record.version, - Version::from_str("2.1").unwrap(), - "expected version 2.1 of issue_717" - ); - - assert_eq!( - result.records[2].package_record.name.as_normalized(), - "xfoo" - ); - assert_eq!( - result.records[2].package_record.version, - Version::from_str("2.0").unwrap(), - "expected version 2.0 of xfoo" - ); - assert_eq!( - result.features.get("xfoo"), - Some(&vec!["with-issue717".to_string()]) - ); - } - - #[cfg(feature = "experimental_extras")] - /// Tests what happens when a feature depends on the base package but with another feature enabled - #[test] - fn test_solve_dummy_repo_extra_depends_recursive_feature() { - let result = solve::( - &[dummy_channel_with_optional_dependencies_json_path()], - SimpleSolveTask { - specs: &["foo[extras=[with-recursive]]"], - ..SimpleSolveTask::default() - }, - ); - - // Sort records by name for stable test results - insta::assert_snapshot!(result.unwrap_err()); - } - - #[cfg(feature = "experimental_extras")] - /// Tests that an optional dependency can restrict the highest version of a base dependency - #[test] - fn test_solve_dummy_repo_extra_depends_version_restriction() { - let result = solve::( - &[dummy_channel_with_optional_dependencies_json_path()], - SimpleSolveTask { - specs: &["foo[extras=[with-version-restrict]]"], - ..SimpleSolveTask::default() - }, - ) - .unwrap(); - - assert_eq!(result.records.len(), 1); - // Both records should be foo - assert_eq!(result.records[0].package_record.name.as_normalized(), "foo"); - assert_eq!( - result.records[0].package_record.version, - Version::from_str("3.0.2").unwrap(), - "expected version 3.0.2 of foo due to version restriction from feature" - ); - assert_eq!( - result.features.get("foo"), - Some(&vec!["with-version-restrict".to_string()]) - ); - } - - #[cfg(feature = "experimental_extras")] - /// Tests what happens if a feature introduces a dependency on the base package itself - #[test] - fn test_solve_dummy_repo_extra_depends_self_dependency() { - let mut result = solve::( - &[dummy_channel_with_optional_dependencies_json_path()], - SimpleSolveTask { - specs: &["foo[extras=[with-self]]"], - ..SimpleSolveTask::default() - }, - ) - .unwrap(); - - // Sort records by name for stable test results - result - .records - .sort_by(|a, b| a.package_record.name.cmp(&b.package_record.name)); + assert_eq!(result.records[1].package_record.name.as_normalized(), "foo"); - assert_eq!(result.records.len(), 1); - // Both records should be foo - assert_eq!(result.records[0].package_record.name.as_normalized(), "foo"); - assert_eq!( - result.records[0].package_record.version, - Version::from_str("2.0.2").unwrap(), - "expected version 2.0.2 of foo" - ); - assert_eq!( - result.features.get("foo"), - Some(&vec!["with-self".to_string()]) - ); + // Make sure there is an extra feature `with-bar` for `foo` + assert_eq!(extras.len(), 1); + assert_eq!(extras[0].0.as_normalized(), "foo"); + assert_eq!(extras[0].1, vec!["with-bar"]); } + /// A test that checks that extras influence the version selection of other packages. #[cfg(feature = "experimental_extras")] - /// Tests what happens if there are two packages for foo but only the package with the lower version has the package that is requested #[test] - fn test_solve_dummy_repo_extra_depends_feature_only_in_older() { + fn test_optional_dependency_restrict() { let mut result = solve::( &[dummy_channel_with_optional_dependencies_json_path()], SimpleSolveTask { - specs: &["foo[extras=[legacy-only]]"], + specs: &["foo[extras=[with-bar]]", "bar"], ..SimpleSolveTask::default() }, ) .unwrap(); - // Sort records by name for stable test results + // Sort the records by package name to make the test deterministic result .records .sort_by(|a, b| a.package_record.name.cmp(&b.package_record.name)); + // Even though 2 versions of `bar` are available, we should have + // selected the one with version "1" because it is restricted by the + // extra added to foo. assert_eq!(result.records.len(), 2); - - // Both records should be foo + assert_eq!(result.records[0].file_name, "bar-1-xxx.tar.bz2"); assert_eq!(result.records[1].package_record.name.as_normalized(), "foo"); - assert_eq!( - result.records[1].package_record.version, - Version::from_str("2.0.2").unwrap(), - "expected older version 2.0.2 of foo since it has the required feature" - ); - assert_eq!( - result.features.get("foo"), - Some(&vec!["legacy-only".to_string()]) - ); - - assert_eq!(result.records[0].package_record.name.as_normalized(), "bar"); - assert_eq!( - result.records[0].package_record.version, - Version::from_str("1.2.3").unwrap(), - "expected version 1.2.3 of bar" - ); } + /// A test that checks that if two extras have conflicting dependencies the + /// solution is unsolvable. #[cfg(feature = "experimental_extras")] - /// Test what happens if a feature is requested that doesn't exist #[test] - fn test_solve_dummy_repo_extra_depends_nonexistent_feature() { + fn test_optional_dependency_conflicting_extras() { let result = solve::( &[dummy_channel_with_optional_dependencies_json_path()], SimpleSolveTask { - specs: &["foo[extras=[does-not-exist]]"], + specs: &["conflicting-extras[extras=[extra1, extra2]]"], ..SimpleSolveTask::default() }, ); - assert_eq!( - result.unwrap_err().to_string(), - "Cannot solve the request because of: No candidates were found for foo[does-not-exist] *.\n" - ); + insta::assert_snapshot!(result.unwrap_err(), @r###" + Cannot solve the request because of: The following packages are incompatible + ├─ conflicting-extras[extra1] can be installed with any of the following options: + │ └─ conflicting-extras[extra1] + ├─ conflicting-extras[extra2] can be installed with any of the following options: + │ └─ conflicting-extras[extra2] + └─ conflicting-extras * cannot be installed because there are no viable options: + └─ conflicting-extras 1 would require + ├─ bar >=2, which can be installed with any of the following options: + │ └─ bar 2 + └─ bar <2, which cannot be installed because there are no viable options: + └─ bar 1, which conflicts with the versions reported above. + "###); } + /// A test that checks that extras can cause conflicts with other package + /// dependencies. #[cfg(feature = "experimental_extras")] - /// Test what happens when the only package that provides a certain feature cannot be selected due to a conflict #[test] - fn test_solve_dummy_repo_extra_depends_feature_conflict() { + fn test_optional_dependency_conflicting_with_package() { let result = solve::( &[dummy_channel_with_optional_dependencies_json_path()], SimpleSolveTask { - specs: &["foo[extras=[with-bar]]", "foo>=4.0"], + specs: &["conflicting-extras[extras=[extra1]]", "bar>=2"], ..SimpleSolveTask::default() }, ); - insta::assert_snapshot!(result.unwrap_err()); + insta::assert_snapshot!(result.unwrap_err(), @r###" + Cannot solve the request because of: The following packages are incompatible + ├─ conflicting-extras[extra1] can be installed with any of the following options: + │ └─ conflicting-extras[extra1] + ├─ bar >=2 can be installed with any of the following options: + │ └─ bar 2 + └─ conflicting-extras * cannot be installed because there are no viable options: + └─ conflicting-extras 1 would require + └─ bar <2, which cannot be installed because there are no viable options: + └─ bar 1, which conflicts with the versions reported above. + "###); } } diff --git a/crates/rattler_solve/tests/sorting.rs b/crates/rattler_solve/tests/sorting.rs index 5dba6d1a14..54836ace0a 100644 --- a/crates/rattler_solve/tests/sorting.rs +++ b/crates/rattler_solve/tests/sorting.rs @@ -54,9 +54,7 @@ fn create_sorting_snapshot(package_name: &str, strategy: SolveStrategy) -> Strin ) .expect("failed to create dependency provider"); - let name = dependency_provider - .pool - .intern_package_name(package_name.as_normalized()); + let name = dependency_provider.pool.intern_package_name(&package_name); let version_set = dependency_provider .pool .intern_version_set(name, match_spec.into_nameless().1.into()); diff --git a/js-rattler/Cargo.lock b/js-rattler/Cargo.lock index a340041a17..36fb0c5217 100644 --- a/js-rattler/Cargo.lock +++ b/js-rattler/Cargo.lock @@ -530,7 +530,7 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "file_url" -version = "0.2.5" +version = "0.2.6" dependencies = [ "itertools", "percent-encoding", @@ -786,7 +786,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.9.0", + "indexmap 2.10.0", "slab", "tokio", "tokio-util", @@ -1137,9 +1137,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.9.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" dependencies = [ "equivalent", "hashbrown 0.15.4", @@ -1515,12 +1515,14 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "petgraph" -version = "0.7.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" +checksum = "54acf3a685220b533e437e264e4d932cfbdc4cc7ec0cd232ed73c08d03b8a7ca" dependencies = [ "fixedbitset", - "indexmap 2.9.0", + "hashbrown 0.15.4", + "indexmap 2.10.0", + "serde", ] [[package]] @@ -1790,7 +1792,7 @@ dependencies = [ [[package]] name = "rattler_cache" -version = "0.3.26" +version = "0.3.29" dependencies = [ "anyhow", "dashmap", @@ -1806,6 +1808,7 @@ dependencies = [ "rattler_digest", "rattler_networking", "rattler_package_streaming", + "rattler_redaction", "rayon", "reqwest", "reqwest-middleware", @@ -1820,7 +1823,7 @@ dependencies = [ [[package]] name = "rattler_conda_types" -version = "0.35.6" +version = "0.37.0" dependencies = [ "chrono", "core-foundation", @@ -1830,7 +1833,7 @@ dependencies = [ "fxhash", "glob", "hex", - "indexmap 2.9.0", + "indexmap 2.10.0", "itertools", "lazy-regex", "nom", @@ -1859,7 +1862,7 @@ dependencies = [ [[package]] name = "rattler_digest" -version = "1.1.4" +version = "1.1.5" dependencies = [ "blake2", "digest", @@ -1883,7 +1886,7 @@ dependencies = [ [[package]] name = "rattler_networking" -version = "0.25.6" +version = "0.25.8" dependencies = [ "anyhow", "async-trait", @@ -1905,7 +1908,7 @@ dependencies = [ [[package]] name = "rattler_package_streaming" -version = "0.22.45" +version = "0.22.48" dependencies = [ "bzip2", "chrono", @@ -1942,7 +1945,7 @@ dependencies = [ [[package]] name = "rattler_repodata_gateway" -version = "0.23.7" +version = "0.23.10" dependencies = [ "anyhow", "async-compression", @@ -1998,7 +2001,7 @@ dependencies = [ [[package]] name = "rattler_solve" -version = "2.1.6" +version = "2.1.8" dependencies = [ "chrono", "futures", @@ -2160,16 +2163,16 @@ dependencies = [ [[package]] name = "resolvo" -version = "0.9.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dba027c8e5dd4b5e5a690cfcfb3900d5ffe6985adb048cbd111d5aa596a6c0c8" +checksum = "037a5f3f35329f29885132d030c628f3142d2a098d0abb30513c572b900692fd" dependencies = [ "ahash", "bitvec", "elsa", "event-listener", "futures", - "indexmap 2.9.0", + "indexmap 2.10.0", "itertools", "petgraph", "tracing", @@ -2469,7 +2472,7 @@ dependencies = [ "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.9.0", + "indexmap 2.10.0", "schemars", "serde", "serde_derive", @@ -2496,7 +2499,7 @@ version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap 2.9.0", + "indexmap 2.10.0", "itoa", "ryu", "serde", @@ -3604,7 +3607,7 @@ dependencies = [ "arbitrary", "crc32fast", "flate2", - "indexmap 2.9.0", + "indexmap 2.10.0", "memchr", "time", "zopfli", diff --git a/py-rattler/Cargo.lock b/py-rattler/Cargo.lock index 2ef70192ea..b99d576b53 100644 --- a/py-rattler/Cargo.lock +++ b/py-rattler/Cargo.lock @@ -4521,9 +4521,9 @@ dependencies = [ [[package]] name = "resolvo" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037a5f3f35329f29885132d030c628f3142d2a098d0abb30513c572b900692fd" +checksum = "670175f9a825ad2419bea0e14bfe74e5dcc0227ec7a652a655b1c11e2b911754" dependencies = [ "ahash", "bitvec", diff --git a/test-data/channels/dummy-optional-dependencies/noarch/repodata.json b/test-data/channels/dummy-optional-dependencies/noarch/repodata.json index 7afe3fa037..f14c614aa7 100644 --- a/test-data/channels/dummy-optional-dependencies/noarch/repodata.json +++ b/test-data/channels/dummy-optional-dependencies/noarch/repodata.json @@ -1,528 +1,55 @@ { - "info": { - "subdir": "noarch", - "base_url": "../linux-64" - }, - "packages": { - "cuda-version-12.5-hd4f0392_3.conda": { - "build": "hd4f0392_3", - "build_number": 3, - "depends": [], - "constrains": [ - "__cuda >=12.1" - ], - "license": "LicenseRef-NVIDIA-End-User-License-Agreement", - "license_family": "LicenseRef-NVIDIA-End-User-License-Agreement", - "md5": "6ae1a563a4aa61e55e8ae8260f0d021b", - "name": "cuda-version", - "sha256": "e45a5d14909296abd0784a073da9ee5c420fa58671fbc999f8a9ec898cf3486b", - "size": 21151, - "subdir": "noarch", - "timestamp": 1716314536803, - "version": "12.5", - "extra_depends": { - "with-cudadev": [ - "foo >=4.0.2", - "bar >=1.2.3" - ] - } - }, - "foo-2.0.2-py36h1af98f8_1.tar.bz2": { - "build": "py36h1af98f8_1", - "build_number": 1, - "depends": [], - "license": "MIT", - "license_family": "MIT", - "md5": "d65ab674acf3b7294ebacaec05fc5b54", - "name": "foo", - "sha256": "1154fceeb5c4ee9bb97d245713ac21eb1910237c724d2b7103747215663273c2", - "size": 414494, - "subdir": "noarch", - "timestamp": 1605110689658, - "version": "2.0.2", - "extra_depends": { - "with-oldbors": [ - "bors <2.0" - ], - "with-latest-bors": [ - "bors >=2.0" - ], - "legacy-only": [ - "bar >=1.0" - ], - "with-recursive": [ - "foo[extras=[with-bar]]" - ], - "with-self": [ - "foo >=2.0" - ] - } - }, - "foo-3.0.2-py36h1af98f8_1.conda": { - "build": "py36h1af98f8_1", - "build_number": 1, - "depends": [], - "license": "MIT", - "license_family": "MIT", - "md5": "fb731d9290f0bcbf3a054665f33ec94f", - "name": "foo", - "sha256": "67a63bec3fd3205170eaad532d487595b8aaceb9814d13c6858d7bac3ef24cd4", - "size": 414494, - "subdir": "noarch", - "timestamp": 1715610974000, - "version": "3.0.2", - "extra_depends": { - "with-baz2": [ - "baz >=2.0" - ], - "with-bar": [ - "bar >=1.2.3" - ], - "with-version-restrict": [ - "foo <3.0.3" - ] - } - }, - "foo-3.0.3-py36h1af98f8_2.conda": { - "build": "py36h1af98f8_2", - "build_number": 2, - "depends": [], - "license": "MIT", - "license_family": "MIT", - "md5": "fb731d9290f0bcbf3a054665f33ec94f", - "name": "foo", - "sha256": "67a63bec3fd3205170eaad532d487595b8aaceb9814d13c6858d7bac3ef24cd4", - "size": 414494, - "subdir": "noarch", - "timestamp": 1715610974000, - "version": "3.0.3", - "extra_depends": { - "with-foobar21": [ - "foobar >=2.1" - ], - "with-bors2": [ - "bors >=1.2.1" - ] - } - }, - "foo-3.0.4-py36h1af98f8_3.conda": { - "build": "py36h1af98f8_3", - "build_number": 3, - "depends": [], - "constrains": [ - "bors <2.0" - ], - "license": "MIT", - "license_family": "MIT", - "md5": "fb731d9290f0bcbf3a054665f33ec94f", - "name": "foo", - "sha256": "67a63bec3fd3205170eaad532d487595b8aaceb9814d13c6858d7bac3ef24cd4", - "size": 414494, - "subdir": "noarch", - "timestamp": 1715610974000, - "version": "3.0.4", - "extra_depends": { - "with-issue717": [ - "issue_717 >=2.1" - ], - "with-xfoo2": [ - "xfoo >=2" - ] - } - }, - "foo-3.1.0-py36h1af98f8_1.conda": { - "build": "py36h1af98f8_1", - "build_number": 1, - "depends": [], - "license": "MIT", - "license_family": "MIT", - "md5": "fb731d9290f0bcbf3a054665f33ec94f", - "name": "foo", - "sha256": "67a63bec3fd3205170eaad532d487595b8aaceb9814d13c6858d7bac3ef24cd4", - "size": 414494, - "subdir": "noarch", - "timestamp": 1715610974000, - "version": "3.1.0", - "extra_depends": { - "with-bar": [ - "bar >=1.2.3" - ] - } - }, - "foo-3.2.0-py36h1af98f8_1.conda": { - "build": "py36h1af98f8_1", - "build_number": 1, - "depends": [], - "license": "MIT", - "license_family": "MIT", - "md5": "fb731d9290f0bcbf3a054665f33ec94f", - "name": "foo", - "sha256": "67a63bec3fd3205170eaad532d487595b8aaceb9814d13c6858d7bac3ef24cd4", - "size": 414494, - "subdir": "noarch", - "timestamp": 1715610974000, - "version": "3.2.0", - "extra_depends": { - "with-bar": [ - "bar >=1.2.3" - ] - } - }, - "foo-3.3.0-py36h1af98f8_1.conda": { - "build": "py36h1af98f8_1", - "build_number": 1, - "depends": [], - "license": "MIT", - "license_family": "MIT", - "md5": "fb731d9290f0bcbf3a054665f33ec94f", - "name": "foo", - "sha256": "67a63bec3fd3205170eaad532d487595b8aaceb9814d13c6858d7bac3ef24cd4", - "size": 414494, - "subdir": "noarch", - "timestamp": 1715610974000, - "version": "3.3.0", - "extra_depends": { - "with-bar": [ - "bar >=1.2.3" - ] - } - }, - "foo-3.4.0-py36h1af98f8_1.conda": { - "build": "py36h1af98f8_1", - "build_number": 1, - "depends": [], - "license": "MIT", - "license_family": "MIT", - "md5": "fb731d9290f0bcbf3a054665f33ec94f", - "name": "foo", - "sha256": "67a63bec3fd3205170eaad532d487595b8aaceb9814d13c6858d7bac3ef24cd4", - "size": 414494, - "subdir": "noarch", - "timestamp": 1715610974000, - "version": "3.4.0", - "extra_depends": { - "with-bar": [ - "bar >=1.2.3" - ] - } - }, - "foo-3.5.0-py36h1af98f8_1.conda": { - "build": "py36h1af98f8_1", - "build_number": 1, - "depends": [], - "license": "MIT", - "license_family": "MIT", - "md5": "fb731d9290f0bcbf3a054665f33ec94f", - "name": "foo", - "sha256": "67a63bec3fd3205170eaad532d487595b8aaceb9814d13c6858d7bac3ef24cd4", - "size": 414494, - "subdir": "noarch", - "timestamp": 1715610974000, - "version": "3.5.0", - "extra_depends": { - "with-bar": [ - "bar >=1.2.3" - ] - } - }, - "foo-4.0.2-py36h1af98f8_2.tar.bz2": { - "build": "py36h1af98f8_2", - "build_number": 1, - "depends": [], - "license": "MIT", - "license_family": "MIT", - "md5": "bc13aa58e2092bcb0b97c561373d3905", - "name": "foo", - "sha256": "97ec377d2ad83dfef1194b7aa31b0c9076194e10d995a6e696c9d07dd782b14a", - "size": 414494, - "subdir": "noarch", - "timestamp": 1715610974000, - "version": "4.0.2", - "extra_depends": { - "with-xbar": [ - "xbar >=1" - ] - } - }, - "bar-1.0-unix_py36h1af98f8_2.tar.bz2": { - "build": "unix_py36h1af98f8_2", - "build_number": 1, - "depends": [ - ], - "license": "MIT", - "license_family": "MIT", - "md5": "bc13aa58e2092bcb0b97c561373d3905", - "name": "bar", - "sha256": "97ec377d2ad83dfef1194b7aa31b0c9076194e10d995a6e696c9d07dd782b14a", - "size": 414494, - "subdir": "noarch", - "timestamp": 1605110689658, - "version": "1.2.3", - "extra_depends": { - "with-xfoo": [ - "xfoo >=1" - ] - } - }, - "baz-1.0-unix_py36h1af98f8_2.tar.bz2": { - "build": "unix_py36h1af98f8_2", - "build_number": 1, - "depends": [ - "__unix" - ], - "license": "MIT", - "license_family": "MIT", - "md5": "bc13aa58e2092bcb0b97c561373d3905", - "name": "baz", - "sha256": "97ec377d2ad83dfef1194b7aa31b0c9076194e10d995a6e696c9d07dd782b14a", - "size": 414494, - "subdir": "noarch", - "timestamp": 1605110689658, - "version": "1.2.3", - "extra_depends": { - "with-foobar": [ - "foobar >=2.0" - ] - } - }, - "baz-2.0-unix_py36h1af98f8_2.tar.bz2": { - "build": "unix_py36h1af98f8_2", - "build_number": 1, - "depends": [], - "license": "MIT", - "license_family": "MIT", - "md5": "bc13aa58e2092bcb0b97c561373d3905", - "name": "baz", - "sha256": "97ec377d2ad83dfef1194b7aa31b0c9076194e10d995a6e696c9d07dd782b14a", - "size": 414494, - "subdir": "noarch", - "timestamp": 1715610974000, - "version": "2.0", - "extra_depends": { - "with-bors": [ - "bors >=2.0" - ] - } - }, - "bors-1.0-bla_1.tar.bz2": { - "build": "bla_1", - "build_number": 1, - "depends": [], - "license": "MIT", - "license_family": "MIT", - "md5": "bc13aa58e2092bcb0b97c561373d3905", - "name": "bors", - "sha256": "97ec377d2ad83dfef1194b7aa31b0c9076194e10d995a6e696c9d07dd782b14a", - "size": 414494, - "subdir": "noarch", - "timestamp": 1605110689658, - "version": "1.0", - "extra_depends": { - "with-baz10": [ - "baz >=1.0" - ] - } - }, - "bors-1.1-bla_1.tar.bz2": { - "build": "bla_1", - "build_number": 1, - "depends": [], - "license": "MIT", - "license_family": "MIT", - "md5": "bc13aa58e2092bcb0b97c561373d3905", - "name": "bors", - "sha256": "97ec377d2ad83dfef1194b7aa31b0c9076194e10d995a6e696c9d07dd782b14a", - "size": 414494, - "subdir": "noarch", - "timestamp": 1605110689658, - "version": "1.1", - "extra_depends": { - "with-foobar20": [ - "foobar >=2.0" - ] - } - }, - "bors-1.2.1-bla_1.tar.bz2": { - "build": "bla_1", - "build_number": 1, - "depends": [], - "license": "MIT", - "license_family": "MIT", - "md5": "bc13aa58e2092bcb0b97c561373d3905", - "name": "bors", - "sha256": "97ec377d2ad83dfef1194b7aa31b0c9076194e10d995a6e696c9d07dd782b14a", - "size": 414494, - "subdir": "noarch", - "timestamp": 1605110689658, - "version": "1.2.1", - "extra_depends": { - "with-baz2": [ - "baz >=2.0" - ] - } - }, - "bors-2.0-bla_1.tar.bz2": { - "build": "bla_1", - "build_number": 1, - "depends": [], - "license": "MIT", - "license_family": "MIT", - "md5": "bc13aa58e2092bcb0b97c561373d3905", - "name": "bors", - "sha256": "97ec377d2ad83dfef1194b7aa31b0c9076194e10d995a6e696c9d07dd782b14a", - "size": 414494, - "subdir": "noarch", - "timestamp": 1715610974000, - "version": "2.0", - "extra_depends": { - "with-xbar": [ - "xbar >=1" - ] - } - }, - "bors-2.1-bla_1.tar.bz2": { - "build": "bla_1", - "build_number": 1, - "depends": [], - "license": "MIT", - "license_family": "MIT", - "md5": "bc13aa58e2092bcb0b97c561373d3905", - "name": "bors", - "sha256": "97ec377d2ad83dfef1194b7aa31b0c9076194e10d995a6e696c9d07dd782b14a", - "size": 414494, - "subdir": "noarch", - "timestamp": 1715610974000, - "version": "2.1", - "extra_depends": { - "with-cuda": [ - "cuda-version >=12.5" - ] - } - }, - "foobar-2.0-bla_1.tar.bz2": { - "build": "bla_1", - "build_number": 1, - "depends": [ - "bors <2.0" - ], - "license": "MIT", - "license_family": "MIT", - "md5": "bc13aa58e2092bcb0b97c561373d3905", - "name": "foobar", - "sha256": "97ec377d2ad83dfef1194b7aa31b0c9076194e10d995a6e696c9d07dd782b14a", - "size": 414494, - "subdir": "noarch", - "timestamp": 1605110689658, - "version": "2.0", - "extra_depends": { - "with-baz10": [ - "baz >=1.0" - ] - } - }, - "foobar-2.1-bla_1.tar.bz2": { - "build": "bla_1", - "build_number": 1, - "depends": [ - "bors <2.0" - ], - "license": "MIT", - "license_family": "MIT", - "md5": "bc13aa58e2092bcb0b97c561373d3905", - "name": "foobar", - "sha256": "97ec377d2ad83dfef1194b7aa31b0c9076194e10d995a6e696c9d07dd782b14a", - "size": 414494, - "subdir": "noarch", - "timestamp": 1715610974000, - "version": "2.1", - "extra_depends": { - "with-baz2": [ - "baz >=2.0" - ] - } - }, - "issue_717-2.1-bla_1.tar.bz2": { - "build": "issue_717", - "build_number": 0, - "depends": [], - "constrains": [ - "ray[version=>=2.9.0, extras=[default,data]]" - ], - "license": "MIT", - "license_family": "MIT", - "md5": "bc13aa58e2092bcb0b97c561373d3905", - "name": "issue_717", - "sha256": "97ec377d2ad83dfef1194b7aa31b0c9076194e10d995a6e696c9d07dd782b14a", - "size": 414494, - "subdir": "noarch", - "timestamp": 1715610974000, - "version": "2.1", - "extra_depends": { - "with-bors21": [ - "bors >=2.1" - ] - } - }, - "xfoo-1-xxx.tar.bz2": { - "build": "xxx", - "build_number": 0, - "depends": [], - "license": "MIT", - "license_family": "MIT", - "md5": "bc13aa58e2092bcb0b97c561373d3905", - "name": "xfoo", - "sha256": "97ec377d2ad83dfef1194b7aa31b0c9076194e10d995a6e696c9d07dd782b14a", - "size": 414494, - "subdir": "noarch", - "timestamp": 1715610974000, - "version": "1", - "extra_depends": { - "with-cuda": [ - "cuda-version >=12.5" - ] - } - }, - "xfoo-2-xxx.tar.bz2": { - "build": "xxx", - "build_number": 0, - "depends": [], - "license": "MIT", - "license_family": "MIT", - "md5": "bc13aa58e2092bcb0b97c561373d3905", - "name": "xfoo", - "sha256": "97ec377d2ad83dfef1194b7aa31b0c9076194e10d995a6e696c9d07dd782b14a", - "size": 414494, - "subdir": "noarch", - "timestamp": 1715610974000, - "version": "2", - "extra_depends": { - "with-issue717": [ - "issue_717[version=>=2.1, extras=[with-bors21]]" - ] - } - }, - "xbar-1-xxx.tar.bz2": { - "build": "xxx", - "build_number": 0, - "depends": [ - "xfoo >=2" - ], - "license": "MIT", - "license_family": "MIT", - "md5": "bc13aa58e2092bcb0b97c561373d3905", - "name": "xbar", - "sha256": "97ec377d2ad83dfef1194b7aa31b0c9076194e10d995a6e696c9d07dd782b14a", - "size": 414494, - "subdir": "noarch", - "timestamp": 1715610974000, - "version": "1", - "extra_depends": { - "with-foo4": [ - "foo >=4.0.2" - ] - } + "info": { + "subdir": "noarch", + "base_url": "../linux-64" + }, + "packages": { + "foo-1-xxx.tar.bz2": { + "name": "foo", + "version": "1", + "build": "xxx", + "build_number": 0, + "depends": [], + "subdir": "linux-64", + "extra_depends": { + "with-bar": [ + "bar <2" + ] } }, - "packages.conda": {}, - "repodata_version": 2 - } + "bar-1-xxx.tar.bz2": { + "name": "bar", + "version": "1", + "build": "xxx", + "build_number": 0, + "depends": [], + "subdir": "linux-64" + }, + "bar-2-xxx.tar.bz2": { + "name": "bar", + "version": "2", + "build": "xxx", + "build_number": 0, + "depends": [], + "subdir": "linux-64" + }, + "conflicting-extras-1-xxx.tar.bz2": { + "name": "conflicting-extras", + "version": "1", + "build": "xxx", + "build_number": 0, + "depends": [], + "subdir": "linux-64", + "extra_depends": { + "extra1": [ + "bar <2" + ], + "extra2": [ + "bar >=2" + ] + } + } + }, + "packages.conda": {}, + "repodata_version": 2 +}