From f53d18c8ea1ea45017ffa7b9651805ed6bd4c824 Mon Sep 17 00:00:00 2001 From: Wolf Vollprecht Date: Fri, 31 Oct 2025 08:54:25 +0100 Subject: [PATCH 1/2] move to resolvo conditional requirements --- .../src/resolve/dependency_provider.rs | 263 ++++++++++++------ .../src/resolve/pypi_version_types.rs | 65 +++-- .../src/resolve/solve.rs | 38 ++- 3 files changed, 244 insertions(+), 122 deletions(-) diff --git a/crates/rattler_installs_packages/src/resolve/dependency_provider.rs b/crates/rattler_installs_packages/src/resolve/dependency_provider.rs index cb483b2..02afe0b 100644 --- a/crates/rattler_installs_packages/src/resolve/dependency_provider.rs +++ b/crates/rattler_installs_packages/src/resolve/dependency_provider.rs @@ -16,12 +16,11 @@ use elsa::FrozenMap; use itertools::Itertools; use miette::{Diagnostic, MietteDiagnostic}; use parking_lot::Mutex; -use pep440_rs::{VersionSpecifier, VersionSpecifiers}; -use pep508_rs::{ExtraName, MarkerEnvironment, Requirement, VerbatimUrl, VersionOrUrl}; +use pep508_rs::{ExtraName, MarkerEnvironment, Requirement, VersionOrUrl}; use resolvo::{ - Candidates, ConditionalRequirement, Dependencies, DependencyProvider, KnownDependencies, - NameId, Requirement as ResolvoRequirement, SolvableId, SolverCache, StringId, VersionSetId, - utils::Pool, + Candidates, ConditionalRequirement, Dependencies, DependencyProvider, HintDependenciesAvailable, + KnownDependencies, NameId, Requirement as ResolvoRequirement, SolvableId, SolverCache, + StringId, VersionSetId, utils::Pool, }; use std::{any::Any, borrow::Borrow, cmp::Ordering, rc::Rc, str::FromStr, sync::Arc}; use thiserror::Error; @@ -39,6 +38,12 @@ pub(crate) struct PypiDependencyProvider { options: ResolveOptions, should_cancel_with_value: Mutex>, + + /// Maps extras to their version set IDs for use in conditions + /// The key is (package_name, extra_name) and the value is the VersionSetId + /// that represents "package[extra] is requested" + /// Boxed because FrozenMap requires Deref types + extra_conditions: FrozenMap<(NormalizedPackageName, Extra), Box>, } impl PypiDependencyProvider { @@ -63,6 +68,7 @@ impl PypiDependencyProvider { name_to_url, options, should_cancel_with_value: Default::default(), + extra_conditions: Default::default(), }) } @@ -205,6 +211,37 @@ impl PypiDependencyProvider { .await .expect("could not acquire semaphore") } + + /// Creates or retrieves a condition ID for a given package extra + /// This is used to make dependencies conditional on extras being selected + fn extra_condition(&self, package: &NormalizedPackageName, extra: &Extra) -> resolvo::ConditionId { + use resolvo::Condition; + + // Create the extra feature name + let extra_name = PypiPackageName::extra_feature(package.clone(), extra.clone()); + let name_id = self.pool.intern_package_name(extra_name); + + // Check if we already have a condition for this extra + if let Some(boxed_id) = self.extra_conditions.get(&(package.clone(), extra.clone())) { + // boxed_id is &Box + // VersionSetId is a tuple struct with one field (u32), access it directly + // Box derefs to &T automatically, so we can copy the inner value + let version_set_id = VersionSetId(boxed_id.0); + return self.pool.intern_condition(Condition::Requirement(version_set_id)); + } + + // Create a new version set for this extra (any version of the extra feature) + let version_set_id = self.pool.intern_version_set( + name_id, + PypiVersionSet::from_spec(None, &self.options.pre_release_resolution), + ); + + // Cache it for future use + self.extra_conditions.insert((package.clone(), extra.clone()), Box::new(version_set_id)); + + // Return the condition + self.pool.intern_condition(Condition::Requirement(version_set_id)) + } } #[derive(Debug, Error, Diagnostic, Clone)] @@ -258,9 +295,8 @@ impl resolvo::Interner for &PypiDependencyProvider { std::iter::empty() } - fn resolve_condition(&self, _condition: resolvo::ConditionId) -> resolvo::Condition { - // For now, we don't use conditional requirements, so we just return a simple requirement condition - resolvo::Condition::Requirement(VersionSetId::default()) + fn resolve_condition(&self, condition: resolvo::ConditionId) -> resolvo::Condition { + self.pool.resolve_condition(condition).clone() } } @@ -331,6 +367,13 @@ impl DependencyProvider for &PypiDependencyProvider { PypiVersion::Version { version: a, .. }, PypiVersion::Version { version: b, .. }, ) => b.cmp(a), + + // Extras don't need special sorting - they're always unique + (PypiVersion::Extra { .. }, PypiVersion::Extra { .. }) => Ordering::Equal, + + // Put extras at the end + (PypiVersion::Extra { .. }, _) => Ordering::Less, + (_, PypiVersion::Extra { .. }) => Ordering::Greater, } }) } @@ -339,17 +382,36 @@ impl DependencyProvider for &PypiDependencyProvider { let package_name = self.pool.resolve_package_name(name); tracing::info!("collecting {}", package_name); + // Handle extra features specially - they are virtual solvables + if let PypiPackageName::ExtraFeature(base_name, extra) = package_name { + // For extras, we create a single virtual solvable + let extra_solvable = self.pool.intern_solvable( + name, + PypiVersion::Extra { + package: base_name.clone(), + extra: extra.clone(), + }, + ); + return Some(Candidates { + candidates: vec![extra_solvable], + favored: None, + locked: None, + excluded: Vec::new(), + hint_dependencies_available: HintDependenciesAvailable::All, + }); + } + // check if we have URL variant for this name - let url_version = self.name_to_url.get(package_name.base()); + let url_version = self.name_to_url.get(package_name.base_package()); let request = if let Some(url) = url_version { ArtifactRequest::DirectUrl { - name: package_name.base().clone(), + name: package_name.base_package().clone(), url: Url::from_str(url).expect("cannot parse back url"), wheel_builder: self.wheel_builder.clone(), } } else { - ArtifactRequest::FromIndex(package_name.base().clone()) + ArtifactRequest::FromIndex(package_name.base_package().clone()) }; let lease = self.aquire_lease_to_run().await; @@ -374,13 +436,13 @@ impl DependencyProvider for &PypiDependencyProvider { } }; let mut candidates = Candidates::default(); - let locked_package = self.options.locked_packages.get(package_name.base()); - let favored_package = self.options.favored_packages.get(package_name.base()); + let locked_package = self.options.locked_packages.get(package_name.base_package()); + let favored_package = self.options.favored_packages.get(package_name.base_package()); let should_package_allow_prerelease = match &self.options.pre_release_resolution { PreReleaseResolution::Disallow => false, PreReleaseResolution::AllowIfNoOtherVersionsOrEnabled { allow_names } => { - if allow_names.contains(&package_name.base().to_string()) { + if allow_names.contains(&package_name.base_package().to_string()) { true } else { // check if we _only_ have prereleases for this name (if yes, also allow them) @@ -411,6 +473,10 @@ impl DependencyProvider for &PypiDependencyProvider { continue; } } + PypiVersion::Extra { .. } => { + // Extra versions shouldn't appear in artifacts list + unreachable!("extras should not appear in artifact list"); + } } // Add the solvable @@ -442,7 +508,7 @@ impl DependencyProvider for &PypiDependencyProvider { } // Add a locked dependency - if let Some(locked) = self.options.locked_packages.get(package_name.base()) { + if let Some(locked) = self.options.locked_packages.get(package_name.base_package()) { let version = if let Some(url) = &locked.url { PypiVersion::Url(url.clone()) } else { @@ -459,7 +525,7 @@ impl DependencyProvider for &PypiDependencyProvider { } // Add a favored dependency - if let Some(favored) = self.options.favored_packages.get(package_name.base()) { + if let Some(favored) = self.options.favored_packages.get(package_name.base_package()) { let version = if let Some(url) = &favored.url { PypiVersion::Url(url.clone()) } else { @@ -493,30 +559,25 @@ impl DependencyProvider for &PypiDependencyProvider { // Add a dependency to the base dependency when we have an extra // So that we have a connection to the base package - if let PypiPackageName::Extra(package_name, _) = package_name { + if let PypiPackageName::ExtraFeature(base_package, _) = package_name { + // Intern the base package name (creates it if it doesn't exist) let base_name_id = self .pool - .lookup_package_name(&PypiPackageName::Base(package_name.clone())) - .expect("base package not found while resolving extra"); - let specifiers = match package_version { - PypiVersion::Version { version, .. } => { - VersionOrUrl::VersionSpecifier(VersionSpecifiers::from_iter([ - VersionSpecifier::equals_version(version.clone()), - ])) - } - PypiVersion::Url(url_version) => { - VersionOrUrl::Url(VerbatimUrl::from_url(url_version.clone())) - } - }; + .intern_package_name(PypiPackageName::package(base_package.clone())); + // Extra feature solvables depend on the base package with ANY version + // The actual version constraint comes from the requirement that selected this extra let version_set_id = self.pool.intern_version_set( base_name_id, - PypiVersionSet::from_spec(Some(specifiers), &self.options.pre_release_resolution), + PypiVersionSet::from_spec(None, &self.options.pre_release_resolution), ); dependencies.requirements.push(ConditionalRequirement { condition: None, requirement: ResolvoRequirement::Single(version_set_id), }); + + // Extra features are virtual solvables with no other dependencies + return Dependencies::Known(dependencies); } // Retrieve the artifacts that are applicable for this version @@ -530,7 +591,7 @@ impl DependencyProvider for &PypiDependencyProvider { // TODO: rework this so it makes more sense from an API perspective later, I think we should add the concept of installed_and_locked or something // It is locked the package data may be available externally // So it's fine if there are no artifacts, we can just assume this has been taken care of - let locked_package = self.options.locked_packages.get(package_name.base()); + let locked_package = self.options.locked_packages.get(package_name.base_package()); match package_version { PypiVersion::Url(url) => { if locked_package.map(|p| &p.url) == Some(&Some(url.clone())) { @@ -543,6 +604,11 @@ impl DependencyProvider for &PypiDependencyProvider { return Dependencies::Known(dependencies); } } + + PypiVersion::Extra { .. } => { + // Extras don't have artifacts, this should have been handled earlier + unreachable!("extras should have been handled earlier"); + } } // Otherwise, we do expect data, and it's not fine if there are no artifacts @@ -606,46 +672,36 @@ impl DependencyProvider for &PypiDependencyProvider { } }; - // Add constraints that restrict that the extra packages are set to the same version. - if let PypiPackageName::Base(package_name) = package_name { - // Add constraints on the extras of a package - for extra in metadata.extras { - let extra_name_id = self - .pool - .intern_package_name(PypiPackageName::Extra(package_name.clone(), extra)); - - let specifiers = match package_version { - PypiVersion::Version { version, .. } => { - VersionOrUrl::VersionSpecifier(VersionSpecifiers::from_iter([ - VersionSpecifier::equals_version(version.clone()), - ])) - } - PypiVersion::Url(url_version) => { - VersionOrUrl::Url(VerbatimUrl::from_url(url_version.clone())) - } - }; - let version_set_id = self.pool.intern_version_set( - extra_name_id, - PypiVersionSet::from_spec( - Some(specifiers), - &self.options.pre_release_resolution, - ), - ); - dependencies.constrains.push(version_set_id); + // Note: In the old implementation, we added constraints here to ensure that + // extras matched the same version as their base package. However, with the new + // virtual solvable model for extras, this is no longer needed or valid. + // Extra feature solvables are virtual (they don't have real versions) and they + // automatically depend on their base package, which provides the version constraint. + // Therefore, we skip this constraint generation for now. + // + // In the future, if we need to model "provides_extras" (packages declaring which + // extras they provide), we could potentially use this logic differently. + + let extras: Vec = match package_name { + PypiPackageName::ExtraFeature(_, extra) => { + vec![ExtraName::new(extra.as_str().to_string()).unwrap()] } - } - - let extras: Vec = package_name - .extra() - .into_iter() - .map(|e| ExtraName::new(e.as_str().to_string()).unwrap()) - .collect(); + PypiPackageName::Package(_) => Vec::new(), + }; for requirement in metadata.requires_dist { - // Evaluate environment markers - if !requirement - .marker - .evaluate(&self.markers, extras.as_slice()) - { + // Extract marker and check if it's conditional on an extra + let marker = &requirement.marker; + + // Check if this dependency is conditional on a specific extra + let extra_marker = marker.top_level_extra(); + + // Evaluate environment markers (but not extra markers) + // For base packages with extra-specific dependencies, we DON'T evaluate the marker + // because we'll add them as conditional requirements instead + let is_extra_conditional = extra_marker.is_some() && matches!(package_name, PypiPackageName::Package(_)); + + if !is_extra_conditional && !marker.evaluate(&self.markers, extras.as_slice()) { + // Non-extra markers that don't match the environment continue; } @@ -656,10 +712,10 @@ impl DependencyProvider for &PypiDependencyProvider { extras: req_extras, .. } = requirement; - let package_name = PackageName::from_str(name.as_ref()).expect("invalid package name"); + let dep_package_name = PackageName::from_str(name.as_ref()).expect("invalid package name"); let dependency_name_id = self .pool - .intern_package_name(PypiPackageName::Base(package_name.clone().into())); + .intern_package_name(PypiPackageName::package(dep_package_name.clone().into())); let version_set_id = self.pool.intern_version_set( dependency_name_id, @@ -669,35 +725,62 @@ impl DependencyProvider for &PypiDependencyProvider { ), ); - if let Some(VersionOrUrl::Url(url)) = version_or_url.clone() - && let Some(given) = url.given() - { - self.name_to_url - .insert(package_name.clone().into(), given.to_owned()); + if let Some(VersionOrUrl::Url(url)) = version_or_url.clone() { + if let Some(given) = url.given() { + self.name_to_url + .insert(dep_package_name.clone().into(), given.to_owned()); + } } + // Determine if this requirement should be conditional on an extra + let condition = if let Some(pep508_rs::MarkerExpression::Extra { name: extra_value, .. }) = extra_marker { + // Extract the extra name + if let pep508_rs::MarkerValueExtra::Extra(extra_name) = extra_value { + // Get the base package that owns this dependency + // (package_name here refers to the outer package being processed, e.g., cachecontrol) + if let PypiPackageName::Package(owner_pkg) = package_name { + // Convert ExtraName to Extra + let extra = Extra::from_str(extra_name.as_ref()).expect("invalid extra"); + // Create a condition for this extra + Some(self.extra_condition(owner_pkg, &extra)) + } else { + None + } + } else { + None + } + } else { + None + }; + + // Add requirement (conditional if it depends on an extra) dependencies.requirements.push(ConditionalRequirement { - condition: None, + condition, requirement: ResolvoRequirement::Single(version_set_id), }); - // Add a unique package for each extra/optional dependency - for extra in req_extras { - let extra = Extra::from_str(extra.as_ref()).expect("invalid extra name"); - let dependency_name_id = self.pool.intern_package_name(PypiPackageName::Extra( - package_name.clone().into(), + // Add unconditional requirements for each extra feature requested + // When a package depends on foo[bar], we require both foo and foo[bar] + for extra_name in req_extras { + let extra = Extra::from_str(extra_name.as_ref()).expect("invalid extra name"); + + // Require the extra feature solvable (for the DEPENDENCY, not the owner) + let extra_feature_name = PypiPackageName::extra_feature( + dep_package_name.clone().into(), extra, - )); - let version_set_id = self.pool.intern_version_set( - dependency_name_id, - PypiVersionSet::from_spec( - version_or_url.clone(), - &self.options.pre_release_resolution, - ), ); + let extra_name_id = self.pool.intern_package_name(extra_feature_name); + // Extra features are virtual solvables, so we use None (any version) + // The actual version constraint is on the base package + let extra_version_set_id = self.pool.intern_version_set( + extra_name_id, + PypiVersionSet::from_spec(None, &self.options.pre_release_resolution), + ); + + // This requirement is unconditional - we always need the extra when requested dependencies.requirements.push(ConditionalRequirement { condition: None, - requirement: ResolvoRequirement::Single(version_set_id), + requirement: ResolvoRequirement::Single(extra_version_set_id), }); } } diff --git a/crates/rattler_installs_packages/src/resolve/pypi_version_types.rs b/crates/rattler_installs_packages/src/resolve/pypi_version_types.rs index e4dfbfc..f381d94 100644 --- a/crates/rattler_installs_packages/src/resolve/pypi_version_types.rs +++ b/crates/rattler_installs_packages/src/resolve/pypi_version_types.rs @@ -79,6 +79,14 @@ pub enum PypiVersion { }, /// Direct reference for artifact Url(Url), + /// Virtual solvable representing an extra feature + /// This is used to model conditional dependencies (e.g., requests[socks]) + Extra { + /// The base package name + package: NormalizedPackageName, + /// The extra name + extra: Extra, + }, } impl PypiVersion { @@ -87,6 +95,7 @@ impl PypiVersion { match self { PypiVersion::Url(_) => false, PypiVersion::Version { version, .. } => version.any_prerelease(), + PypiVersion::Extra { .. } => false, } } @@ -95,6 +104,7 @@ impl PypiVersion { match self { PypiVersion::Version { .. } => false, PypiVersion::Url(url) => url.scheme().contains("git"), + PypiVersion::Extra { .. } => false, } } } @@ -129,6 +139,8 @@ impl PypiVersionSet { }, ) => self.allows_prerelease || *package_allows_prerelease || !version.any_prerelease(), (None, PypiVersion::Url(_)) => true, + // Extra features always match when spec is None (used for extra version sets) + (None, PypiVersion::Extra { .. }) => true, _ => false, } } @@ -139,43 +151,60 @@ impl Display for PypiVersion { match self { PypiVersion::Version { version, .. } => write!(f, "{version}"), PypiVersion::Url(u) => write!(f, "{u}"), + PypiVersion::Extra { package, extra } => write!(f, "{}[{}]", package, extra.as_str()), } } } #[derive(PartialEq, Eq, Hash, Clone, Debug)] -/// This can either be a base package name or with an extra -/// this is used to support optional dependencies +/// Represents a PyPI package or extra "feature" +/// Extras are modeled as virtual solvables that enable conditional dependencies pub(crate) enum PypiPackageName { - /// Regular dependency - Base(NormalizedPackageName), - /// Optional dependency - Extra(NormalizedPackageName, Extra), + /// A real package + Package(NormalizedPackageName), + /// A virtual "extra feature" solvable for conditional dependencies + /// Format: package_name + "__extra__" + extra_name + ExtraFeature(NormalizedPackageName, Extra), } impl PypiPackageName { - /// Returns the actual package (normalized) name without the extra - pub fn base(&self) -> &NormalizedPackageName { - match self { - PypiPackageName::Base(normalized) => normalized, - PypiPackageName::Extra(normalized, _) => normalized, - } + /// Create a new package name + pub fn package(name: NormalizedPackageName) -> Self { + Self::Package(name) } - /// Retrieves the extra if it is available - pub fn extra(&self) -> Option<&Extra> { + /// Create a new extra feature name + pub fn extra_feature(package: NormalizedPackageName, extra: Extra) -> Self { + Self::ExtraFeature(package, extra) + } + + /// Returns the base package name + pub fn base_package(&self) -> &NormalizedPackageName { match self { - PypiPackageName::Base(_) => None, - PypiPackageName::Extra(_, e) => Some(e), + PypiPackageName::Package(name) => name, + PypiPackageName::ExtraFeature(name, _) => name, } } + + /// Returns true if this is an extra feature + pub fn is_extra_feature(&self) -> bool { + matches!(self, PypiPackageName::ExtraFeature(_, _)) + } +} + +impl From for PypiPackageName { + fn from(name: NormalizedPackageName) -> Self { + Self::Package(name) + } } impl Display for PypiPackageName { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { - PypiPackageName::Base(name) => write!(f, "{}", name), - PypiPackageName::Extra(name, extra) => write!(f, "{}[{}]", name, extra.as_str()), + PypiPackageName::Package(name) => write!(f, "{}", name), + PypiPackageName::ExtraFeature(name, extra) => { + write!(f, "{}[{}]", name, extra.as_str()) + } } } } diff --git a/crates/rattler_installs_packages/src/resolve/solve.rs b/crates/rattler_installs_packages/src/resolve/solve.rs index b92965d..6af4f99 100644 --- a/crates/rattler_installs_packages/src/resolve/solve.rs +++ b/crates/rattler_installs_packages/src/resolve/solve.rs @@ -112,7 +112,7 @@ fn resolve_inner<'r>( } in requirements { let package_name = PackageName::from_str(name.as_ref()).expect("invalid package name"); - let pypi_name = PypiPackageName::Base(package_name.clone().into()); + let pypi_name = PypiPackageName::package(package_name.clone().into()); let dependency_package_name = pool.intern_package_name(pypi_name.clone()); let version_set_id = pool.intern_version_set( dependency_package_name, @@ -123,15 +123,15 @@ fn resolve_inner<'r>( requirement: ResolvoRequirement::Single(version_set_id), }); - if let Some(VersionOrUrl::Url(url)) = &version_or_url - && let Some(given) = url.given() - { - name_to_url.insert(pypi_name.base().clone(), given.to_owned()); + if let Some(VersionOrUrl::Url(url)) = &version_or_url { + if let Some(given) = url.given() { + name_to_url.insert(pypi_name.base_package().clone(), given.to_owned()); + } } for extra in req_extras { let extra: Extra = extra.as_ref().parse().expect("invalid extra"); - let dependency_package_name = pool.intern_package_name(PypiPackageName::Extra( + let dependency_package_name = pool.intern_package_name(PypiPackageName::extra_feature( package_name.clone().into(), extra.clone(), )); @@ -182,6 +182,17 @@ fn resolve_inner<'r>( let name = provider.pool.resolve_package_name(solvable.name); let version = &solvable.record; + // Skip extra feature solvables - they're not real packages + if name.is_extra_feature() { + // Record the extra in the base package if it exists + if let PypiPackageName::ExtraFeature(base_name, extra) = name { + if let Some(entry) = result.get_mut(base_name) { + entry.extras.insert(extra.clone()); + } + } + continue; + } + let artifacts: Vec<_> = provider .cached_artifacts .get(&solvable_id) @@ -199,23 +210,22 @@ fn resolve_inner<'r>( .expect("no artifacts found for direct_url artifact"); (info.filename.version(), Some(url.clone())) } + PypiVersion::Extra { .. } => { + // This shouldn't happen since we skip extra features above + unreachable!("extra features should have been skipped"); + } }; // Get the entry in the result - let entry = result - .entry(name.base().clone()) + result + .entry(name.base_package().clone()) .or_insert_with(|| PinnedPackage { - name: name.base().clone(), + name: name.base_package().clone(), version, url, artifacts, extras: Default::default(), }); - - // Add the extra if selected - if let PypiPackageName::Extra(_, extra) = name { - entry.extras.insert(extra.clone()); - } } Ok(result.into_values().collect()) From becb32b6904cdc1fe59e7d5e4e562a687d4815b0 Mon Sep 17 00:00:00 2001 From: Wolf Vollprecht Date: Fri, 31 Oct 2025 18:05:05 +0100 Subject: [PATCH 2/2] fmt and lint --- ...ch_git_reference_with_tag_source_code.snap | 107 +++--------------- .../src/resolve/dependency_provider.rs | 97 ++++++++++------ .../src/resolve/solve.rs | 16 +-- 3 files changed, 86 insertions(+), 134 deletions(-) diff --git a/crates/rattler_installs_packages/src/artifacts/snapshots/rattler_installs_packages__artifacts__sdist__tests__build_rich_git_reference_with_tag_source_code.snap b/crates/rattler_installs_packages/src/artifacts/snapshots/rattler_installs_packages__artifacts__sdist__tests__build_rich_git_reference_with_tag_source_code.snap index ff5281e..bd29501 100644 --- a/crates/rattler_installs_packages/src/artifacts/snapshots/rattler_installs_packages__artifacts__sdist__tests__build_rich_git_reference_with_tag_source_code.snap +++ b/crates/rattler_installs_packages/src/artifacts/snapshots/rattler_installs_packages__artifacts__sdist__tests__build_rich_git_reference_with_tag_source_code.snap @@ -7,14 +7,14 @@ WheelCoreMetadata { source: "rich", normalized: "rich", }, - version: "1.0.0", + version: "14.2.0", metadata_version: MetadataVersion( - "2.4", + "2.3", ), requires_dist: [ Requirement { name: PackageName( - "colorama", + "ipywidgets", ), extras: [], version_or_url: Some( @@ -23,70 +23,22 @@ WheelCoreMetadata { [ VersionSpecifier { operator: GreaterThanEqual, - version: "0.4.3", + version: "7.5.1", }, VersionSpecifier { operator: LessThan, - version: "0.5.0", + version: "9", }, ], ), ), ), - marker: true, - origin: None, - }, - Requirement { - name: PackageName( - "commonmark", - ), - extras: [], - version_or_url: Some( - VersionSpecifier( - VersionSpecifiers( - [ - VersionSpecifier { - operator: GreaterThanEqual, - version: "0.9.0", - }, - VersionSpecifier { - operator: LessThan, - version: "0.10.0", - }, - ], - ), - ), - ), - marker: true, - origin: None, - }, - Requirement { - name: PackageName( - "dataclasses", - ), - extras: [], - version_or_url: Some( - VersionSpecifier( - VersionSpecifiers( - [ - VersionSpecifier { - operator: GreaterThanEqual, - version: "0.7", - }, - VersionSpecifier { - operator: LessThan, - version: "0.8", - }, - ], - ), - ), - ), - marker: python_full_version == '3.6.*', + marker: extra == 'jupyter', origin: None, }, Requirement { name: PackageName( - "pprintpp", + "markdown-it-py", ), extras: [], version_or_url: Some( @@ -95,11 +47,7 @@ WheelCoreMetadata { [ VersionSpecifier { operator: GreaterThanEqual, - version: "0.4.0", - }, - VersionSpecifier { - operator: LessThan, - version: "0.5.0", + version: "2.2.0", }, ], ), @@ -119,7 +67,7 @@ WheelCoreMetadata { [ VersionSpecifier { operator: GreaterThanEqual, - version: "2.6.1", + version: "2.13.0", }, VersionSpecifier { operator: LessThan, @@ -132,44 +80,21 @@ WheelCoreMetadata { marker: true, origin: None, }, - Requirement { - name: PackageName( - "typing-extensions", - ), - extras: [], - version_or_url: Some( - VersionSpecifier( - VersionSpecifiers( - [ - VersionSpecifier { - operator: GreaterThanEqual, - version: "3.7.4", - }, - VersionSpecifier { - operator: LessThan, - version: "4.0.0", - }, - ], - ), - ), - ), - marker: true, - origin: None, - }, ], requires_python: Some( VersionSpecifiers( [ VersionSpecifier { operator: GreaterThanEqual, - version: "3.6", - }, - VersionSpecifier { - operator: LessThan, - version: "4.0", + version: "3.8.0", }, ], ), ), - extras: {}, + extras: { + Extra { + source: "jupyter", + normalized: "jupyter", + }, + }, } diff --git a/crates/rattler_installs_packages/src/resolve/dependency_provider.rs b/crates/rattler_installs_packages/src/resolve/dependency_provider.rs index 02afe0b..5f18758 100644 --- a/crates/rattler_installs_packages/src/resolve/dependency_provider.rs +++ b/crates/rattler_installs_packages/src/resolve/dependency_provider.rs @@ -18,9 +18,9 @@ use miette::{Diagnostic, MietteDiagnostic}; use parking_lot::Mutex; use pep508_rs::{ExtraName, MarkerEnvironment, Requirement, VersionOrUrl}; use resolvo::{ - Candidates, ConditionalRequirement, Dependencies, DependencyProvider, HintDependenciesAvailable, - KnownDependencies, NameId, Requirement as ResolvoRequirement, SolvableId, SolverCache, - StringId, VersionSetId, utils::Pool, + Candidates, ConditionalRequirement, Dependencies, DependencyProvider, + HintDependenciesAvailable, KnownDependencies, NameId, Requirement as ResolvoRequirement, + SolvableId, SolverCache, StringId, VersionSetId, utils::Pool, }; use std::{any::Any, borrow::Borrow, cmp::Ordering, rc::Rc, str::FromStr, sync::Arc}; use thiserror::Error; @@ -214,7 +214,11 @@ impl PypiDependencyProvider { /// Creates or retrieves a condition ID for a given package extra /// This is used to make dependencies conditional on extras being selected - fn extra_condition(&self, package: &NormalizedPackageName, extra: &Extra) -> resolvo::ConditionId { + fn extra_condition( + &self, + package: &NormalizedPackageName, + extra: &Extra, + ) -> resolvo::ConditionId { use resolvo::Condition; // Create the extra feature name @@ -227,7 +231,9 @@ impl PypiDependencyProvider { // VersionSetId is a tuple struct with one field (u32), access it directly // Box derefs to &T automatically, so we can copy the inner value let version_set_id = VersionSetId(boxed_id.0); - return self.pool.intern_condition(Condition::Requirement(version_set_id)); + return self + .pool + .intern_condition(Condition::Requirement(version_set_id)); } // Create a new version set for this extra (any version of the extra feature) @@ -237,10 +243,12 @@ impl PypiDependencyProvider { ); // Cache it for future use - self.extra_conditions.insert((package.clone(), extra.clone()), Box::new(version_set_id)); + self.extra_conditions + .insert((package.clone(), extra.clone()), Box::new(version_set_id)); // Return the condition - self.pool.intern_condition(Condition::Requirement(version_set_id)) + self.pool + .intern_condition(Condition::Requirement(version_set_id)) } } @@ -436,8 +444,14 @@ impl DependencyProvider for &PypiDependencyProvider { } }; let mut candidates = Candidates::default(); - let locked_package = self.options.locked_packages.get(package_name.base_package()); - let favored_package = self.options.favored_packages.get(package_name.base_package()); + let locked_package = self + .options + .locked_packages + .get(package_name.base_package()); + let favored_package = self + .options + .favored_packages + .get(package_name.base_package()); let should_package_allow_prerelease = match &self.options.pre_release_resolution { PreReleaseResolution::Disallow => false, @@ -508,7 +522,11 @@ impl DependencyProvider for &PypiDependencyProvider { } // Add a locked dependency - if let Some(locked) = self.options.locked_packages.get(package_name.base_package()) { + if let Some(locked) = self + .options + .locked_packages + .get(package_name.base_package()) + { let version = if let Some(url) = &locked.url { PypiVersion::Url(url.clone()) } else { @@ -525,7 +543,11 @@ impl DependencyProvider for &PypiDependencyProvider { } // Add a favored dependency - if let Some(favored) = self.options.favored_packages.get(package_name.base_package()) { + if let Some(favored) = self + .options + .favored_packages + .get(package_name.base_package()) + { let version = if let Some(url) = &favored.url { PypiVersion::Url(url.clone()) } else { @@ -591,7 +613,10 @@ impl DependencyProvider for &PypiDependencyProvider { // TODO: rework this so it makes more sense from an API perspective later, I think we should add the concept of installed_and_locked or something // It is locked the package data may be available externally // So it's fine if there are no artifacts, we can just assume this has been taken care of - let locked_package = self.options.locked_packages.get(package_name.base_package()); + let locked_package = self + .options + .locked_packages + .get(package_name.base_package()); match package_version { PypiVersion::Url(url) => { if locked_package.map(|p| &p.url) == Some(&Some(url.clone())) { @@ -698,7 +723,8 @@ impl DependencyProvider for &PypiDependencyProvider { // Evaluate environment markers (but not extra markers) // For base packages with extra-specific dependencies, we DON'T evaluate the marker // because we'll add them as conditional requirements instead - let is_extra_conditional = extra_marker.is_some() && matches!(package_name, PypiPackageName::Package(_)); + let is_extra_conditional = + extra_marker.is_some() && matches!(package_name, PypiPackageName::Package(_)); if !is_extra_conditional && !marker.evaluate(&self.markers, extras.as_slice()) { // Non-extra markers that don't match the environment @@ -712,7 +738,8 @@ impl DependencyProvider for &PypiDependencyProvider { extras: req_extras, .. } = requirement; - let dep_package_name = PackageName::from_str(name.as_ref()).expect("invalid package name"); + let dep_package_name = + PackageName::from_str(name.as_ref()).expect("invalid package name"); let dependency_name_id = self .pool .intern_package_name(PypiPackageName::package(dep_package_name.clone().into())); @@ -725,27 +752,29 @@ impl DependencyProvider for &PypiDependencyProvider { ), ); - if let Some(VersionOrUrl::Url(url)) = version_or_url.clone() { - if let Some(given) = url.given() { - self.name_to_url - .insert(dep_package_name.clone().into(), given.to_owned()); - } + if let Some(VersionOrUrl::Url(url)) = version_or_url.clone() + && let Some(given) = url.given() + { + self.name_to_url + .insert(dep_package_name.clone().into(), given.to_owned()); } // Determine if this requirement should be conditional on an extra - let condition = if let Some(pep508_rs::MarkerExpression::Extra { name: extra_value, .. }) = extra_marker { + let condition = if let Some(pep508_rs::MarkerExpression::Extra { + name: extra_value, + .. + }) = extra_marker + { // Extract the extra name - if let pep508_rs::MarkerValueExtra::Extra(extra_name) = extra_value { - // Get the base package that owns this dependency - // (package_name here refers to the outer package being processed, e.g., cachecontrol) - if let PypiPackageName::Package(owner_pkg) = package_name { - // Convert ExtraName to Extra - let extra = Extra::from_str(extra_name.as_ref()).expect("invalid extra"); - // Create a condition for this extra - Some(self.extra_condition(owner_pkg, &extra)) - } else { - None - } + // and get the base package that owns this dependency + // (package_name here refers to the outer package being processed, e.g., cachecontrol) + if let pep508_rs::MarkerValueExtra::Extra(extra_name) = extra_value + && let PypiPackageName::Package(owner_pkg) = package_name + { + // Convert ExtraName to Extra + let extra = Extra::from_str(extra_name.as_ref()).expect("invalid extra"); + // Create a condition for this extra + Some(self.extra_condition(owner_pkg, &extra)) } else { None } @@ -765,10 +794,8 @@ impl DependencyProvider for &PypiDependencyProvider { let extra = Extra::from_str(extra_name.as_ref()).expect("invalid extra name"); // Require the extra feature solvable (for the DEPENDENCY, not the owner) - let extra_feature_name = PypiPackageName::extra_feature( - dep_package_name.clone().into(), - extra, - ); + let extra_feature_name = + PypiPackageName::extra_feature(dep_package_name.clone().into(), extra); let extra_name_id = self.pool.intern_package_name(extra_feature_name); // Extra features are virtual solvables, so we use None (any version) // The actual version constraint is on the base package diff --git a/crates/rattler_installs_packages/src/resolve/solve.rs b/crates/rattler_installs_packages/src/resolve/solve.rs index 6af4f99..ac9e27b 100644 --- a/crates/rattler_installs_packages/src/resolve/solve.rs +++ b/crates/rattler_installs_packages/src/resolve/solve.rs @@ -123,10 +123,10 @@ fn resolve_inner<'r>( requirement: ResolvoRequirement::Single(version_set_id), }); - if let Some(VersionOrUrl::Url(url)) = &version_or_url { - if let Some(given) = url.given() { - name_to_url.insert(pypi_name.base_package().clone(), given.to_owned()); - } + if let Some(VersionOrUrl::Url(url)) = &version_or_url + && let Some(given) = url.given() + { + name_to_url.insert(pypi_name.base_package().clone(), given.to_owned()); } for extra in req_extras { @@ -185,10 +185,10 @@ fn resolve_inner<'r>( // Skip extra feature solvables - they're not real packages if name.is_extra_feature() { // Record the extra in the base package if it exists - if let PypiPackageName::ExtraFeature(base_name, extra) = name { - if let Some(entry) = result.get_mut(base_name) { - entry.extras.insert(extra.clone()); - } + if let PypiPackageName::ExtraFeature(base_name, extra) = name + && let Some(entry) = result.get_mut(base_name) + { + entry.extras.insert(extra.clone()); } continue; }