diff --git a/crates/uv-resolver/src/pins.rs b/crates/uv-resolver/src/pins.rs index d471f745a..827a8e6f4 100644 --- a/crates/uv-resolver/src/pins.rs +++ b/crates/uv-resolver/src/pins.rs @@ -29,4 +29,15 @@ impl FilePins { ) -> Option<&ResolvedDist> { self.0.get(name)?.get(version) } + + /// Add the pins in `other` to `self`. + /// + /// This assumes that if a version for a particular package exists in + /// both `self` and `other`, then they will both correspond to identical + /// distributions. + pub(crate) fn union(&mut self, other: FilePins) { + for (name, versions) in other.0 { + self.0.entry(name).or_default().extend(versions); + } + } } diff --git a/crates/uv-resolver/src/resolution/graph.rs b/crates/uv-resolver/src/resolution/graph.rs index 1058deeea..6f5e87d45 100644 --- a/crates/uv-resolver/src/resolution/graph.rs +++ b/crates/uv-resolver/src/resolution/graph.rs @@ -1,23 +1,24 @@ use std::hash::BuildHasherDefault; -use pubgrub::solver::{Kind, State}; -use pubgrub::type_aliases::SelectedDependencies; use rustc_hash::{FxHashMap, FxHashSet}; use distribution_types::{ Dist, DistributionMetadata, Name, Requirement, ResolutionDiagnostic, VersionId, VersionOrUrlRef, }; -use pep440_rs::VersionSpecifier; +use pep440_rs::{Version, VersionSpecifier}; use pep508_rs::MarkerEnvironment; +use petgraph::{ + graph::{Graph, NodeIndex}, + Directed, +}; use pypi_types::{ParsedUrlError, Yanked}; -use uv_normalize::PackageName; +use uv_normalize::{ExtraName, PackageName}; -use crate::dependency_provider::UvDependencyProvider; -use crate::pins::FilePins; use crate::preferences::Preferences; use crate::pubgrub::{PubGrubDistribution, PubGrubPackageInner}; use crate::redirect::url_to_precise; use crate::resolution::AnnotatedDist; +use crate::resolver::Resolution; use crate::{ lock, InMemoryIndex, Lock, LockError, Manifest, MetadataResponse, ResolveError, VersionsResponse, @@ -28,7 +29,7 @@ use crate::{ #[derive(Debug)] pub struct ResolutionGraph { /// The underlying graph. - pub(crate) petgraph: petgraph::graph::Graph, + pub(crate) petgraph: Graph, /// Any diagnostics that were encountered while building the graph. pub(crate) diagnostics: Vec, } @@ -38,250 +39,206 @@ impl ResolutionGraph { #[allow(clippy::too_many_arguments)] pub(crate) fn from_state( index: &InMemoryIndex, - selection: &SelectedDependencies, - pins: &FilePins, - state: &State, preferences: &Preferences, + resolution: Resolution, ) -> anyhow::Result { // Add every package to the graph. - let mut petgraph = - petgraph::graph::Graph::::with_capacity( - selection.len(), - selection.len(), + let mut petgraph: Graph = + Graph::with_capacity(resolution.packages.len(), resolution.packages.len()); + let mut inverse: FxHashMap<(&PackageName, &Version, &Option), NodeIndex> = + FxHashMap::with_capacity_and_hasher( + resolution.packages.len(), + BuildHasherDefault::default(), ); - let mut inverse = - FxHashMap::with_capacity_and_hasher(selection.len(), BuildHasherDefault::default()); let mut diagnostics = Vec::new(); - for (package, version) in selection { - match &**package { - PubGrubPackageInner::Package { - name, - extra, - marker: None, - url: None, - } => { - // Create the distribution. - let dist = pins - .get(name, version) - .expect("Every package should be pinned") - .clone(); + for (package, versions) in &resolution.packages { + for version in versions { + match &**package { + PubGrubPackageInner::Package { + name, + extra, + marker: None, + url: None, + } => { + // Create the distribution. + let dist = resolution + .pins + .get(name, version) + .expect("Every package should be pinned") + .clone(); - // Track yanks for any registry distributions. - match dist.yanked() { - None | Some(Yanked::Bool(false)) => {} - Some(Yanked::Bool(true)) => { - diagnostics.push(ResolutionDiagnostic::YankedVersion { - dist: dist.clone(), - reason: None, - }); + // Track yanks for any registry distributions. + match dist.yanked() { + None | Some(Yanked::Bool(false)) => {} + Some(Yanked::Bool(true)) => { + diagnostics.push(ResolutionDiagnostic::YankedVersion { + dist: dist.clone(), + reason: None, + }); + } + Some(Yanked::Reason(reason)) => { + diagnostics.push(ResolutionDiagnostic::YankedVersion { + dist: dist.clone(), + reason: Some(reason.clone()), + }); + } } - Some(Yanked::Reason(reason)) => { - diagnostics.push(ResolutionDiagnostic::YankedVersion { - dist: dist.clone(), - reason: Some(reason.clone()), - }); - } - } - // Extract the hashes, preserving those that were already present in the - // lockfile if necessary. - let hashes = if let Some(digests) = preferences - .match_hashes(name, version) - .filter(|digests| !digests.is_empty()) - { - digests.to_vec() - } else if let Some(versions_response) = index.packages().get(name) { - if let VersionsResponse::Found(ref version_maps) = *versions_response { - version_maps - .iter() - .find_map(|version_map| version_map.hashes(version)) - .map(|mut digests| { - digests.sort_unstable(); - digests - }) - .unwrap_or_default() + // Extract the hashes, preserving those that were already present in the + // lockfile if necessary. + let hashes = if let Some(digests) = preferences + .match_hashes(name, version) + .filter(|digests| !digests.is_empty()) + { + digests.to_vec() + } else if let Some(versions_response) = index.packages().get(name) { + if let VersionsResponse::Found(ref version_maps) = *versions_response { + version_maps + .iter() + .find_map(|version_map| version_map.hashes(version)) + .map(|mut digests| { + digests.sort_unstable(); + digests + }) + .unwrap_or_default() + } else { + vec![] + } } else { vec![] - } - } else { - vec![] - }; + }; - // Extract the metadata. - let metadata = { - let dist = PubGrubDistribution::from_registry(name, version); + // Extract the metadata. + let metadata = { + let dist = PubGrubDistribution::from_registry(name, version); - let response = index - .distributions() - .get(&dist.version_id()) - .unwrap_or_else(|| { + let response = index + .distributions() + .get(&dist.version_id()) + .unwrap_or_else(|| { + panic!( + "Every package should have metadata: {:?}", + dist.version_id() + ) + }); + + let MetadataResponse::Found(archive) = &*response else { panic!( "Every package should have metadata: {:?}", dist.version_id() ) - }); + }; - let MetadataResponse::Found(archive) = &*response else { - panic!( - "Every package should have metadata: {:?}", - dist.version_id() - ) + archive.metadata.clone() }; - archive.metadata.clone() - }; - - // Validate the extra. - if let Some(extra) = extra { - if !metadata.provides_extras.contains(extra) { - diagnostics.push(ResolutionDiagnostic::MissingExtra { - dist: dist.clone(), - extra: extra.clone(), - }); + // Validate the extra. + if let Some(extra) = extra { + if !metadata.provides_extras.contains(extra) { + diagnostics.push(ResolutionDiagnostic::MissingExtra { + dist: dist.clone(), + extra: extra.clone(), + }); + } } + + // Add the distribution to the graph. + let index = petgraph.add_node(AnnotatedDist { + dist, + extra: extra.clone(), + hashes, + metadata, + }); + inverse.insert((name, version, extra), index); } - // Add the distribution to the graph. - let index = petgraph.add_node(AnnotatedDist { - dist, - extra: extra.clone(), - hashes, - metadata, - }); - inverse.insert((name, extra), index); - } + PubGrubPackageInner::Package { + name, + extra, + marker: None, + url: Some(url), + } => { + // Create the distribution. + let dist = Dist::from_url(name.clone(), url_to_precise(url.clone()))?; - PubGrubPackageInner::Package { - name, - extra, - marker: None, - url: Some(url), - } => { - // Create the distribution. - let dist = Dist::from_url(name.clone(), url_to_precise(url.clone()))?; - - // Extract the hashes, preserving those that were already present in the - // lockfile if necessary. - let hashes = if let Some(digests) = preferences - .match_hashes(name, version) - .filter(|digests| !digests.is_empty()) - { - digests.to_vec() - } else if let Some(metadata_response) = - index.distributions().get(&dist.version_id()) - { - if let MetadataResponse::Found(ref archive) = *metadata_response { - let mut digests = archive.hashes.clone(); - digests.sort_unstable(); - digests + // Extract the hashes, preserving those that were already present in the + // lockfile if necessary. + let hashes = if let Some(digests) = preferences + .match_hashes(name, version) + .filter(|digests| !digests.is_empty()) + { + digests.to_vec() + } else if let Some(metadata_response) = + index.distributions().get(&dist.version_id()) + { + if let MetadataResponse::Found(ref archive) = *metadata_response { + let mut digests = archive.hashes.clone(); + digests.sort_unstable(); + digests + } else { + vec![] + } } else { vec![] - } - } else { - vec![] - }; + }; - // Extract the metadata. - let metadata = { - let dist = PubGrubDistribution::from_url(name, url); + // Extract the metadata. + let metadata = { + let dist = PubGrubDistribution::from_url(name, url); - let response = index - .distributions() - .get(&dist.version_id()) - .unwrap_or_else(|| { + let response = index + .distributions() + .get(&dist.version_id()) + .unwrap_or_else(|| { + panic!( + "Every package should have metadata: {:?}", + dist.version_id() + ) + }); + + let MetadataResponse::Found(archive) = &*response else { panic!( "Every package should have metadata: {:?}", dist.version_id() ) - }); + }; - let MetadataResponse::Found(archive) = &*response else { - panic!( - "Every package should have metadata: {:?}", - dist.version_id() - ) + archive.metadata.clone() }; - archive.metadata.clone() - }; - - // Validate the extra. - if let Some(extra) = extra { - if !metadata.provides_extras.contains(extra) { - diagnostics.push(ResolutionDiagnostic::MissingExtra { - dist: dist.clone().into(), - extra: extra.clone(), - }); + // Validate the extra. + if let Some(extra) = extra { + if !metadata.provides_extras.contains(extra) { + diagnostics.push(ResolutionDiagnostic::MissingExtra { + dist: dist.clone().into(), + extra: extra.clone(), + }); + } } + + // Add the distribution to the graph. + let index = petgraph.add_node(AnnotatedDist { + dist: dist.into(), + extra: extra.clone(), + hashes, + metadata, + }); + inverse.insert((name, version, extra), index); } - // Add the distribution to the graph. - let index = petgraph.add_node(AnnotatedDist { - dist: dist.into(), - extra: extra.clone(), - hashes, - metadata, - }); - inverse.insert((name, extra), index); - } - - _ => {} - }; + _ => {} + }; + } } // Add every edge to the graph. - for (package, version) in selection { - for id in &state.incompatibilities[package] { - if let Kind::FromDependencyOf(self_package, self_version, dependency_package, ..) = - &state.incompatibility_store[*id].kind - { - // `Kind::FromDependencyOf` will include inverse dependencies. That is, if we're - // looking for a package `A`, this list will include incompatibilities of - // package `B` _depending on_ `A`. We're only interested in packages that `A` - // depends on. - if package != self_package { - continue; - } - - if !self_version.contains(version) { - continue; - } - - let PubGrubPackageInner::Package { - name: self_name, - extra: self_extra, - .. - } = &**self_package - else { - continue; - }; - - match &**dependency_package { - PubGrubPackageInner::Package { - name: dependency_name, - extra: dependency_extra, - .. - } => { - let self_index = &inverse[&(self_name, self_extra)]; - let dependency_index = &inverse[&(dependency_name, dependency_extra)]; - petgraph.update_edge(*self_index, *dependency_index, ()); - } - - PubGrubPackageInner::Extra { - name: dependency_name, - extra: dependency_extra, - .. - } => { - let self_index = &inverse[&(self_name, self_extra)]; - let dependency_extra = Some(dependency_extra.clone()); - let dependency_index = &inverse[&(dependency_name, &dependency_extra)]; - petgraph.update_edge(*self_index, *dependency_index, ()); - } - - _ => {} - } - } + for (names, version_set) in resolution.dependencies { + for versions in version_set { + let from_index = + inverse[&(&names.from, &versions.from_version, &versions.from_extra)]; + let to_index = inverse[&(&names.to, &versions.to_version, &versions.to_extra)]; + petgraph.update_edge(from_index, to_index, versions.to_version.clone()); } } @@ -462,8 +419,8 @@ impl ResolutionGraph { for node_index in self.petgraph.node_indices() { let dist = &self.petgraph[node_index]; let mut locked_dist = lock::Distribution::from_annotated_dist(dist)?; - for edge in self.petgraph.neighbors(node_index) { - let dependency_dist = &self.petgraph[edge]; + for neighbor in self.petgraph.neighbors(node_index) { + let dependency_dist = &self.petgraph[neighbor]; locked_dist.add_dependency(dependency_dist); } locked_dists.push(locked_dist); diff --git a/crates/uv-resolver/src/resolver/mod.rs b/crates/uv-resolver/src/resolver/mod.rs index f82cc51a1..010b2fded 100644 --- a/crates/uv-resolver/src/resolver/mod.rs +++ b/crates/uv-resolver/src/resolver/mod.rs @@ -32,7 +32,7 @@ use pypi_types::Metadata23; pub(crate) use urls::Urls; use uv_configuration::{Constraints, Overrides}; use uv_distribution::{ArchiveMetadata, DistributionDatabase}; -use uv_normalize::PackageName; +use uv_normalize::{ExtraName, PackageName}; use uv_types::{BuildContext, HashStrategy, InstalledPackagesProvider}; use crate::candidate_selector::{CandidateDist, CandidateSelector}; @@ -315,7 +315,7 @@ impl ResolverState ResolverState ResolverState>, } +impl SolveState { + fn into_resolution(self) -> Resolution { + let packages = self.pubgrub.partial_solution.extract_solution(); + let mut dependencies: FxHashMap< + ResolutionDependencyNames, + FxHashSet, + > = FxHashMap::default(); + for (package, self_version) in &packages { + for id in &self.pubgrub.incompatibilities[package] { + let pubgrub::solver::Kind::FromDependencyOf( + ref self_package, + ref self_range, + ref dependency_package, + ref dependency_range, + ) = self.pubgrub.incompatibility_store[*id].kind + else { + continue; + }; + if package != self_package { + continue; + } + if !self_range.contains(self_version) { + continue; + } + let Some(dependency_version) = packages.get(dependency_package) else { + continue; + }; + if !dependency_range.contains(dependency_version) { + continue; + } + + let PubGrubPackageInner::Package { + name: ref self_name, + extra: ref self_extra, + .. + } = &**self_package + else { + continue; + }; + + match **dependency_package { + PubGrubPackageInner::Package { + name: ref dependency_name, + extra: ref dependency_extra, + .. + } => { + if self_name == dependency_name { + continue; + } + let names = ResolutionDependencyNames { + from: self_name.clone(), + to: dependency_name.clone(), + }; + let versions = ResolutionDependencyVersions { + from_version: self_version.clone(), + from_extra: self_extra.clone(), + to_version: dependency_version.clone(), + to_extra: dependency_extra.clone(), + }; + dependencies.entry(names).or_default().insert(versions); + } + + PubGrubPackageInner::Extra { + name: ref dependency_name, + extra: ref dependency_extra, + .. + } => { + if self_name == dependency_name { + continue; + } + let names = ResolutionDependencyNames { + from: self_name.clone(), + to: dependency_name.clone(), + }; + let versions = ResolutionDependencyVersions { + from_version: self_version.clone(), + from_extra: self_extra.clone(), + to_version: dependency_version.clone(), + to_extra: Some(dependency_extra.clone()), + }; + dependencies.entry(names).or_default().insert(versions); + } + + _ => {} + } + } + } + let packages = packages + .into_iter() + .map(|(package, version)| (package, FxHashSet::from_iter([version]))) + .collect(); + Resolution { + packages, + dependencies, + pins: self.pins, + } + } +} + +#[derive(Debug, Default)] +pub(crate) struct Resolution { + pub(crate) packages: FxHashMap>, + pub(crate) dependencies: + FxHashMap>, + pub(crate) pins: FilePins, +} + +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub(crate) struct ResolutionDependencyNames { + pub(crate) from: PackageName, + pub(crate) to: PackageName, +} + +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub(crate) struct ResolutionDependencyVersions { + pub(crate) from_version: Version, + pub(crate) from_extra: Option, + pub(crate) to_version: Version, + pub(crate) to_extra: Option, +} + +impl Resolution { + fn union(&mut self, other: Resolution) { + for (other_package, other_versions) in other.packages { + self.packages + .entry(other_package) + .or_default() + .extend(other_versions); + } + for (names, versions) in other.dependencies { + self.dependencies.entry(names).or_default().extend(versions); + } + self.pins.union(other.pins); + } +} + /// Fetch the metadata for an item #[derive(Debug)] #[allow(clippy::large_enum_variant)]