Refactor `Resolution` type to retain dependency graph (#9106)

## Summary

This PR should not contain any user-visible changes, but the goal is to
refactor the `Resolution` type to retain a dependency graph. We want to
be able to explain _why_ a given package was excluded on error (see:
https://github.com/astral-sh/uv/issues/8962), which in turn requires
that at install time, we can go back and figure out the dependency
chain. At present, `Resolution` is just a map from package name to
distribution; this PR remodels it as a graph in which each node is a
package, and the edges contain markers plus extras or dependency groups.
This commit is contained in:
Charlie Marsh 2024-11-14 15:25:34 -05:00 committed by GitHub
parent ed130b0c11
commit a552f74308
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 432 additions and 182 deletions

1
Cargo.lock generated
View File

@ -4884,6 +4884,7 @@ dependencies = [
"fs-err",
"itertools 0.13.0",
"jiff",
"petgraph",
"rkyv",
"rustc-hash",
"schemars",

View File

@ -33,6 +33,7 @@ bitflags = { workspace = true }
fs-err = { workspace = true }
itertools = { workspace = true }
jiff = { workspace = true }
petgraph = { workspace = true }
rkyv = { workspace = true }
rustc-hash = { workspace = true }
schemars = { workspace = true, optional = true }

View File

@ -1,55 +1,76 @@
use std::collections::BTreeMap;
use uv_distribution_filename::DistExtension;
use uv_normalize::{ExtraName, GroupName, PackageName};
use uv_pep508::MarkerTree;
use uv_pypi_types::{HashDigest, RequirementSource};
use crate::{BuiltDist, Diagnostic, Dist, Name, ResolvedDist, SourceDist};
/// A set of packages pinned at specific versions.
///
/// This is similar to [`ResolverOutput`], but represents a resolution for a subset of all
/// marker environments. For example, the resolution is guaranteed to contain at most one version
/// for a given package.
#[derive(Debug, Default, Clone)]
pub struct Resolution {
packages: BTreeMap<PackageName, ResolvedDist>,
hashes: BTreeMap<PackageName, Vec<HashDigest>>,
graph: petgraph::graph::DiGraph<Node, Edge>,
diagnostics: Vec<ResolutionDiagnostic>,
}
impl Resolution {
/// Create a new resolution from the given pinned packages.
pub fn new(
packages: BTreeMap<PackageName, ResolvedDist>,
hashes: BTreeMap<PackageName, Vec<HashDigest>>,
diagnostics: Vec<ResolutionDiagnostic>,
) -> Self {
pub fn new(graph: petgraph::graph::DiGraph<Node, Edge>) -> Self {
Self {
packages,
hashes,
diagnostics,
graph,
diagnostics: Vec::new(),
}
}
/// Return the underlying graph of the resolution.
pub fn graph(&self) -> &petgraph::graph::DiGraph<Node, Edge> {
&self.graph
}
/// Add [`Diagnostics`] to the resolution.
#[must_use]
pub fn with_diagnostics(mut self, diagnostics: Vec<ResolutionDiagnostic>) -> Self {
self.diagnostics.extend(diagnostics);
self
}
/// Return the hashes for the given package name, if they exist.
pub fn get_hashes(&self, package_name: &PackageName) -> &[HashDigest] {
self.hashes.get(package_name).map_or(&[], Vec::as_slice)
}
/// Iterate over the [`PackageName`] entities in this resolution.
pub fn packages(&self) -> impl Iterator<Item = &PackageName> {
self.packages.keys()
pub fn hashes(&self) -> impl Iterator<Item = (&ResolvedDist, &[HashDigest])> {
self.graph
.node_indices()
.filter_map(move |node| match &self.graph[node] {
Node::Dist {
dist,
hashes,
install,
..
} if *install => Some((dist, hashes.as_slice())),
_ => None,
})
}
/// Iterate over the [`ResolvedDist`] entities in this resolution.
pub fn distributions(&self) -> impl Iterator<Item = &ResolvedDist> {
self.packages.values()
self.graph
.raw_nodes()
.iter()
.filter_map(|node| match &node.weight {
Node::Dist { dist, install, .. } if *install => Some(dist),
_ => None,
})
}
/// Return the number of distributions in this resolution.
pub fn len(&self) -> usize {
self.packages.len()
self.distributions().count()
}
/// Return `true` if there are no pinned packages in this resolution.
pub fn is_empty(&self) -> bool {
self.packages.is_empty()
self.distributions().next().is_none()
}
/// Return the [`ResolutionDiagnostic`]s that were produced during resolution.
@ -59,45 +80,32 @@ impl Resolution {
/// Filter the resolution to only include packages that match the given predicate.
#[must_use]
pub fn filter(self, predicate: impl Fn(&ResolvedDist) -> bool) -> Self {
let packages = self
.packages
.into_iter()
.filter(|(_, dist)| predicate(dist))
.collect::<BTreeMap<_, _>>();
let hashes = self
.hashes
.into_iter()
.filter(|(name, _)| packages.contains_key(name))
.collect();
let diagnostics = self.diagnostics.clone();
Self {
packages,
hashes,
diagnostics,
pub fn filter(mut self, predicate: impl Fn(&ResolvedDist) -> bool) -> Self {
for node in self.graph.node_weights_mut() {
if let Node::Dist { dist, install, .. } = node {
if !predicate(dist) {
*install = false;
}
}
}
self
}
/// Map over the resolved distributions in this resolution.
///
/// For efficiency, the map function should return `None` if the resolved distribution is
/// unchanged.
#[must_use]
pub fn map(self, predicate: impl Fn(ResolvedDist) -> ResolvedDist) -> Self {
let packages = self
.packages
.into_iter()
.map(|(name, dist)| (name, predicate(dist)))
.collect::<BTreeMap<_, _>>();
let hashes = self
.hashes
.into_iter()
.filter(|(name, _)| packages.contains_key(name))
.collect();
let diagnostics = self.diagnostics.clone();
Self {
packages,
hashes,
diagnostics,
pub fn map(mut self, predicate: impl Fn(&ResolvedDist) -> Option<ResolvedDist>) -> Self {
for node in self.graph.node_weights_mut() {
if let Node::Dist { dist, .. } = node {
if let Some(transformed) = predicate(dist) {
*dist = transformed;
}
}
}
self
}
}
#[derive(Debug, Clone, Hash)]
@ -166,15 +174,46 @@ impl Diagnostic for ResolutionDiagnostic {
}
}
/// A node in the resolution, along with whether its been filtered out.
///
/// We retain filtered nodes as we still need to be able to trace dependencies through the graph
/// (e.g., to determine why a package was included in the resolution).
#[derive(Debug, Clone)]
pub enum Node {
Root,
Dist {
dist: ResolvedDist,
hashes: Vec<HashDigest>,
install: bool,
},
}
/// An edge in the resolution graph, along with the marker that must be satisfied to traverse it.
#[derive(Debug, Clone)]
pub enum Edge {
Prod(MarkerTree),
Optional(ExtraName, MarkerTree),
Dev(GroupName, MarkerTree),
}
impl Edge {
/// Return the [`MarkerTree`] for this edge.
pub fn marker(&self) -> &MarkerTree {
match self {
Self::Prod(marker) => marker,
Self::Optional(_, marker) => marker,
Self::Dev(_, marker) => marker,
}
}
}
impl From<&ResolvedDist> for RequirementSource {
fn from(resolved_dist: &ResolvedDist) -> Self {
match resolved_dist {
ResolvedDist::Installable { dist, .. } => match dist {
ResolvedDist::Installable { dist, version } => match dist {
Dist::Built(BuiltDist::Registry(wheels)) => RequirementSource::Registry {
specifier: uv_pep440::VersionSpecifiers::from(
uv_pep440::VersionSpecifier::equals_version(
wheels.best_wheel().filename.version.clone(),
),
uv_pep440::VersionSpecifier::equals_version(version.clone()),
),
index: Some(wheels.best_wheel().index.url().clone()),
},

View File

@ -6,7 +6,7 @@ use std::path::{Component, Path, PathBuf};
use either::Either;
use petgraph::visit::IntoNodeReferences;
use petgraph::{Directed, Graph};
use petgraph::Graph;
use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};
use url::Url;
@ -22,8 +22,6 @@ use crate::graph_ops::marker_reachability;
use crate::lock::{Package, PackageId, Source};
use crate::{InstallTarget, LockError};
type LockGraph<'lock> = Graph<Node<'lock>, Edge, Directed>;
/// An export of a [`Lock`] that renders in `requirements.txt` format.
#[derive(Debug)]
pub struct RequirementsTxtExport<'lock> {
@ -42,7 +40,7 @@ impl<'lock> RequirementsTxtExport<'lock> {
install_options: &'lock InstallOptions,
) -> Result<Self, LockError> {
let size_guess = target.lock().packages.len();
let mut petgraph = LockGraph::with_capacity(size_guess, size_guess);
let mut petgraph = Graph::with_capacity(size_guess, size_guess);
let mut inverse = FxHashMap::with_capacity_and_hasher(size_guess, FxBuildHasher);
let mut queue: VecDeque<(&Package, Option<&ExtraName>)> = VecDeque::new();
@ -282,16 +280,13 @@ impl std::fmt::Display for RequirementsTxtExport<'_> {
}
}
/// A node in the [`LockGraph`].
/// A node in the graph.
#[derive(Debug, Clone, PartialEq, Eq)]
enum Node<'lock> {
Root,
Package(&'lock Package),
}
/// The edges of the [`LockGraph`].
type Edge = MarkerTree;
/// A flat requirement, with its associated marker.
#[derive(Debug, Clone, PartialEq, Eq)]
struct Requirement<'lock> {

View File

@ -1,11 +1,13 @@
use either::Either;
use petgraph::Graph;
use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};
use std::collections::hash_map::Entry;
use std::collections::{BTreeMap, VecDeque};
use either::Either;
use rustc_hash::FxHashSet;
use uv_configuration::{BuildOptions, DevGroupsManifest, ExtrasSpecification, InstallOptions};
use uv_distribution_types::{Resolution, ResolvedDist};
use uv_distribution_types::{Edge, Node, Resolution, ResolvedDist};
use uv_normalize::{ExtraName, GroupName, PackageName, DEV_DEPENDENCIES};
use uv_pep508::MarkerTree;
use uv_platform_tags::Tags;
use uv_pypi_types::{ResolverMarkerEnvironment, VerbatimParsedUrl};
use uv_workspace::dependency_groups::{DependencyGroupError, FlatDependencyGroups};
@ -155,12 +157,18 @@ impl<'env> InstallTarget<'env> {
build_options: &BuildOptions,
install_options: &InstallOptions,
) -> Result<Resolution, LockError> {
let size_guess = self.lock().packages.len();
let mut petgraph = Graph::with_capacity(size_guess, size_guess);
let mut inverse = FxHashMap::with_capacity_and_hasher(size_guess, FxBuildHasher);
let mut queue: VecDeque<(&Package, Option<&ExtraName>)> = VecDeque::new();
let mut seen = FxHashSet::default();
let root = petgraph.add_node(Node::Root);
// Add the workspace packages to the queue.
for root_name in self.packages() {
let root = self
let dist = self
.lock()
.find_by_name(root_name)
.map_err(|_| LockErrorKind::MultipleRootPackages {
@ -170,21 +178,30 @@ impl<'env> InstallTarget<'env> {
name: root_name.clone(),
})?;
if dev.prod() {
// Add the base package.
queue.push_back((root, None));
// Add the workspace package to the graph.
let index = petgraph.add_node(if dev.prod() {
self.package_to_node(dist, tags, build_options, install_options)?
} else {
self.non_installable_node(dist, tags)?
});
inverse.insert(&dist.id, index);
// Add any extras.
// Add an edge from the root.
petgraph.add_edge(root, index, Edge::Prod(MarkerTree::TRUE));
if dev.prod() {
// Push its dependencies on the queue.
queue.push_back((dist, None));
match extras {
ExtrasSpecification::None => {}
ExtrasSpecification::All => {
for extra in root.optional_dependencies.keys() {
queue.push_back((root, Some(extra)));
for extra in dist.optional_dependencies.keys() {
queue.push_back((dist, Some(extra)));
}
}
ExtrasSpecification::Some(extras) => {
for extra in extras {
queue.push_back((root, Some(extra)));
queue.push_back((dist, Some(extra)));
}
}
}
@ -192,9 +209,38 @@ impl<'env> InstallTarget<'env> {
// Add any dev dependencies.
for group in dev.iter() {
for dep in root.dependency_groups.get(group).into_iter().flatten() {
if dep.complexified_marker.evaluate(marker_env, &[]) {
for dep in dist.dependency_groups.get(group).into_iter().flatten() {
if !dep.complexified_marker.evaluate(marker_env, &[]) {
continue;
}
let dep_dist = self.lock().find_by_id(&dep.package_id);
// Add the dependency to the graph.
let dep_index = match inverse.entry(&dep.package_id) {
Entry::Vacant(entry) => {
let index = petgraph.add_node(self.package_to_node(
dep_dist,
tags,
build_options,
install_options,
)?);
entry.insert(index);
index
}
Entry::Occupied(entry) => {
let index = *entry.get();
index
}
};
petgraph.add_edge(
index,
dep_index,
Edge::Dev(group.clone(), dep.complexified_marker.clone()),
);
// Push its dependencies on the queue.
if seen.insert((&dep.package_id, None)) {
queue.push_back((dep_dist, None));
}
@ -206,7 +252,6 @@ impl<'env> InstallTarget<'env> {
}
}
}
}
// Add any dependency groups that are exclusive to the workspace root (e.g., dev
// dependencies in (legacy) non-project workspace roots).
@ -215,9 +260,12 @@ impl<'env> InstallTarget<'env> {
.map_err(|err| LockErrorKind::DependencyGroup { err })?;
for group in dev.iter() {
for dependency in groups.get(group).into_iter().flatten() {
if dependency.marker.evaluate(marker_env, &[]) {
if !dependency.marker.evaluate(marker_env, &[]) {
continue;
}
let root_name = &dependency.name;
let root = self
let dist = self
.lock()
.find_by_markers(root_name, marker_env)
.map_err(|_| LockErrorKind::MultipleRootPackages {
@ -227,28 +275,89 @@ impl<'env> InstallTarget<'env> {
name: root_name.clone(),
})?;
// Add the base package.
queue.push_back((root, None));
// Add the workspace package to the graph.
let index = match inverse.entry(&dist.id) {
Entry::Vacant(entry) => {
let index = petgraph.add_node(self.package_to_node(
dist,
tags,
build_options,
install_options,
)?);
entry.insert(index);
index
}
Entry::Occupied(entry) => {
let index = *entry.get();
index
}
};
// Add any extras.
// Add an edge from the root.
petgraph.add_edge(
root,
index,
Edge::Dev(group.clone(), dependency.marker.clone()),
);
// Push its dependencies on the queue.
queue.push_back((dist, None));
for extra in &dependency.extras {
queue.push_back((root, Some(extra)));
}
queue.push_back((dist, Some(extra)));
}
}
}
let mut map = BTreeMap::default();
let mut hashes = BTreeMap::default();
while let Some((dist, extra)) = queue.pop_front() {
while let Some((package, extra)) = queue.pop_front() {
let deps = if let Some(extra) = extra {
Either::Left(dist.optional_dependencies.get(extra).into_iter().flatten())
Either::Left(
package
.optional_dependencies
.get(extra)
.into_iter()
.flatten(),
)
} else {
Either::Right(dist.dependencies.iter())
Either::Right(package.dependencies.iter())
};
for dep in deps {
if dep.complexified_marker.evaluate(marker_env, &[]) {
if !dep.complexified_marker.evaluate(marker_env, &[]) {
continue;
}
let dep_dist = self.lock().find_by_id(&dep.package_id);
// Add the dependency to the graph.
let dep_index = match inverse.entry(&dep.package_id) {
Entry::Vacant(entry) => {
let index = petgraph.add_node(self.package_to_node(
dep_dist,
tags,
build_options,
install_options,
)?);
entry.insert(index);
index
}
Entry::Occupied(entry) => {
let index = *entry.get();
index
}
};
// Add the edge.
let index = inverse[&package.id];
petgraph.add_edge(
index,
dep_index,
if let Some(extra) = extra {
Edge::Optional(extra.clone(), dep.complexified_marker.clone())
} else {
Edge::Prod(dep.complexified_marker.clone())
},
);
// Push its dependencies on the queue.
if seen.insert((&dep.package_id, None)) {
queue.push_back((dep_dist, None));
}
@ -259,26 +368,65 @@ impl<'env> InstallTarget<'env> {
}
}
}
if install_options.include_package(
&dist.id.name,
self.project_name(),
self.lock().members(),
) {
map.insert(
dist.id.name.clone(),
ResolvedDist::Installable {
dist: dist.to_dist(
Ok(Resolution::new(petgraph))
}
/// Create an installable [`Node`] from a [`Package`].
fn installable_node(
&self,
package: &Package,
tags: &Tags,
build_options: &BuildOptions,
) -> Result<Node, LockError> {
let dist = package.to_dist(
self.workspace().install_path(),
TagPolicy::Required(tags),
build_options,
)?,
version: dist.id.version.clone(),
},
);
hashes.insert(dist.id.name.clone(), dist.hashes());
)?;
let version = package.version().clone();
let dist = ResolvedDist::Installable { dist, version };
let hashes = package.hashes();
Ok(Node::Dist {
dist,
hashes,
install: true,
})
}
/// Create a non-installable [`Node`] from a [`Package`].
fn non_installable_node(&self, package: &Package, tags: &Tags) -> Result<Node, LockError> {
let dist = package.to_dist(
self.workspace().install_path(),
TagPolicy::Preferred(tags),
&BuildOptions::default(),
)?;
let version = package.version().clone();
let dist = ResolvedDist::Installable { dist, version };
let hashes = package.hashes();
Ok(Node::Dist {
dist,
hashes,
install: false,
})
}
/// Convert a lockfile entry to a graph [`Node`].
fn package_to_node(
&self,
package: &Package,
tags: &Tags,
build_options: &BuildOptions,
install_options: &InstallOptions,
) -> Result<Node, LockError> {
if install_options.include_package(
package.name(),
self.project_name(),
self.lock().members(),
) {
self.installable_node(package, tags, build_options)
} else {
self.non_installable_node(package, tags)
}
let diagnostics = vec![];
Ok(Resolution::new(map, hashes, diagnostics))
}
}

View File

@ -2,12 +2,10 @@ use std::collections::BTreeSet;
use owo_colors::OwoColorize;
use petgraph::visit::EdgeRef;
use petgraph::Direction;
use petgraph::{Directed, Direction, Graph};
use rustc_hash::{FxBuildHasher, FxHashMap};
use uv_distribution_types::{
DistributionMetadata, Name, SourceAnnotation, SourceAnnotations, VersionId,
};
use uv_distribution_types::{DistributionMetadata, Name, SourceAnnotation, SourceAnnotations};
use uv_normalize::PackageName;
use uv_pep508::MarkerTree;
@ -305,11 +303,9 @@ pub enum AnnotationStyle {
}
/// We don't need the edge markers anymore since we switched to propagated markers.
type IntermediatePetGraph<'dist> =
petgraph::graph::Graph<DisplayResolutionGraphNode<'dist>, (), petgraph::Directed>;
type IntermediatePetGraph<'dist> = Graph<DisplayResolutionGraphNode<'dist>, (), Directed>;
type RequirementsTxtGraph<'dist> =
petgraph::graph::Graph<RequirementsTxtDist<'dist>, (), petgraph::Directed>;
type RequirementsTxtGraph<'dist> = Graph<RequirementsTxtDist<'dist>, (), Directed>;
/// Reduce the graph, such that all nodes for a single package are combined, regardless of
/// the extras, as long as they have the same version and markers.
@ -324,8 +320,10 @@ type RequirementsTxtGraph<'dist> =
/// We also remove the root node, to simplify the graph structure.
fn combine_extras<'dist>(graph: &IntermediatePetGraph<'dist>) -> RequirementsTxtGraph<'dist> {
/// Return the key for a node.
fn version_marker(dist: &RequirementsTxtDist) -> (VersionId, MarkerTree) {
(dist.version_id(), dist.markers.clone())
fn version_marker<'dist>(
dist: &'dist RequirementsTxtDist,
) -> (&'dist PackageName, &'dist MarkerTree) {
(dist.name(), dist.markers)
}
let mut next = RequirementsTxtGraph::with_capacity(graph.node_count(), graph.edge_count());

View File

@ -10,8 +10,8 @@ use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};
use uv_configuration::{Constraints, Overrides};
use uv_distribution::Metadata;
use uv_distribution_types::{
Dist, DistributionMetadata, IndexUrl, Name, ResolutionDiagnostic, ResolvedDist, VersionId,
VersionOrUrlRef,
Dist, DistributionMetadata, Edge, IndexUrl, Name, Node, ResolutionDiagnostic, ResolvedDist,
VersionId, VersionOrUrlRef,
};
use uv_git::GitResolver;
use uv_normalize::{ExtraName, GroupName, PackageName};
@ -275,7 +275,7 @@ impl ResolverOutput {
}
fn add_edge(
petgraph: &mut Graph<ResolutionGraphNode, MarkerTree>,
graph: &mut Graph<ResolutionGraphNode, MarkerTree>,
inverse: &mut FxHashMap<PackageRef<'_>, NodeIndex>,
root_index: NodeIndex,
edge: &ResolutionDependencyEdge,
@ -306,20 +306,20 @@ impl ResolverOutput {
edge_marker
};
if let Some(marker) = petgraph
if let Some(marker) = graph
.find_edge(from_index, to_index)
.and_then(|edge| petgraph.edge_weight_mut(edge))
.and_then(|edge| graph.edge_weight_mut(edge))
{
// If either the existing marker or new marker is `true`, then the dependency is
// included unconditionally, and so the combined marker is `true`.
marker.or(edge_marker);
} else {
petgraph.update_edge(from_index, to_index, edge_marker);
graph.update_edge(from_index, to_index, edge_marker);
}
}
fn add_version<'a>(
petgraph: &mut Graph<ResolutionGraphNode, MarkerTree>,
graph: &mut Graph<ResolutionGraphNode, MarkerTree>,
inverse: &mut FxHashMap<PackageRef<'a>, NodeIndex>,
diagnostics: &mut Vec<ResolutionDiagnostic>,
preferences: &Preferences,
@ -372,7 +372,7 @@ impl ResolverOutput {
}
// Add the distribution to the graph.
let node = petgraph.add_node(ResolutionGraphNode::Dist(AnnotatedDist {
let node = graph.add_node(ResolutionGraphNode::Dist(AnnotatedDist {
dist,
name: name.clone(),
version: version.clone(),
@ -814,29 +814,98 @@ impl Display for ConflictingDistributionError {
}
}
/// Convert a [`ResolverOutput`] into a [`uv_distribution_types::Resolution`].
///
/// This involves converting [`ResolutionGraphNode`]s into [`Node`]s, which in turn involves
/// dropping any extras and dependency groups from the graph nodes. Instead, each package is
/// collapsed into a single node, with extras and dependency groups annotating the _edges_, rather
/// than being represented as separate nodes. This is a more natural representation, but a further
/// departure from the PubGrub model.
///
/// For simplicity, this transformation makes the assumption that the resolution only applies to a
/// subset of markers, i.e., it shouldn't be called on universal resolutions, and expects only a
/// single version of each package to be present in the graph.
impl From<ResolverOutput> for uv_distribution_types::Resolution {
fn from(graph: ResolverOutput) -> Self {
Self::new(
graph
.dists()
.map(|node| (node.name().clone(), node.dist.clone()))
.collect(),
graph
.dists()
.map(|node| (node.name().clone(), node.hashes.clone()))
.collect(),
graph.diagnostics,
)
fn from(output: ResolverOutput) -> Self {
let ResolverOutput {
graph,
diagnostics,
fork_markers,
..
} = output;
assert!(
fork_markers.is_empty(),
"universal resolutions are not supported"
);
let mut transformed = Graph::with_capacity(graph.node_count(), graph.edge_count());
let mut inverse = FxHashMap::with_capacity_and_hasher(graph.node_count(), FxBuildHasher);
// Create the root node.
let root = transformed.add_node(Node::Root);
// Re-add the nodes to the reduced graph.
for index in graph.node_indices() {
let ResolutionGraphNode::Dist(dist) = &graph[index] else {
continue;
};
if dist.is_base() {
inverse.insert(
&dist.name,
transformed.add_node(Node::Dist {
dist: dist.dist.clone(),
hashes: dist.hashes.clone(),
install: true,
}),
);
}
}
// Re-add the edges to the reduced graph.
for edge in graph.edge_indices() {
let (source, target) = graph.edge_endpoints(edge).unwrap();
let marker = graph[edge].clone();
match (&graph[source], &graph[target]) {
(ResolutionGraphNode::Root, ResolutionGraphNode::Dist(target_dist)) => {
let target = inverse[&target_dist.name()];
transformed.update_edge(root, target, Edge::Prod(marker));
}
(
ResolutionGraphNode::Dist(source_dist),
ResolutionGraphNode::Dist(target_dist),
) => {
let source = inverse[&source_dist.name()];
let target = inverse[&target_dist.name()];
let edge = if let Some(extra) = source_dist.extra.as_ref() {
Edge::Optional(extra.clone(), marker)
} else if let Some(dev) = source_dist.dev.as_ref() {
Edge::Dev(dev.clone(), marker)
} else {
Edge::Prod(marker)
};
transformed.add_edge(source, target, edge);
}
_ => {
unreachable!("root should not contain incoming edges");
}
}
}
uv_distribution_types::Resolution::new(transformed).with_diagnostics(diagnostics)
}
}
/// Find any packages that don't have any lower bound on them when in resolution-lowest mode.
fn report_missing_lower_bounds(
petgraph: &Graph<ResolutionGraphNode, MarkerTree>,
graph: &Graph<ResolutionGraphNode, MarkerTree>,
diagnostics: &mut Vec<ResolutionDiagnostic>,
) {
for node_index in petgraph.node_indices() {
let ResolutionGraphNode::Dist(dist) = petgraph.node_weight(node_index).unwrap() else {
for node_index in graph.node_indices() {
let ResolutionGraphNode::Dist(dist) = graph.node_weight(node_index).unwrap() else {
// Ignore the root package.
continue;
};
@ -847,7 +916,7 @@ fn report_missing_lower_bounds(
// have to drop.
continue;
}
if !has_lower_bound(node_index, dist.name(), petgraph) {
if !has_lower_bound(node_index, dist.name(), graph) {
diagnostics.push(ResolutionDiagnostic::MissingLowerBound {
package_name: dist.name().clone(),
});
@ -859,10 +928,10 @@ fn report_missing_lower_bounds(
fn has_lower_bound(
node_index: NodeIndex,
package_name: &PackageName,
petgraph: &Graph<ResolutionGraphNode, MarkerTree>,
graph: &Graph<ResolutionGraphNode, MarkerTree>,
) -> bool {
for neighbor_index in petgraph.neighbors_directed(node_index, Direction::Incoming) {
let neighbor_dist = match petgraph.node_weight(neighbor_index).unwrap() {
for neighbor_index in graph.neighbors_directed(node_index, Direction::Incoming) {
let neighbor_dist = match graph.node_weight(neighbor_index).unwrap() {
ResolutionGraphNode::Root => {
// We already handled direct dependencies with a missing constraint
// separately.

View File

@ -274,8 +274,7 @@ impl HashStrategy {
) -> Result<Self, HashStrategyError> {
let mut hashes = FxHashMap::<VersionId, Vec<HashDigest>>::default();
for dist in resolution.distributions() {
let digests = resolution.get_hashes(dist.name());
for (dist, digests) in resolution.hashes() {
if digests.is_empty() {
// Under `--require-hashes`, every requirement must include a hash.
if mode.is_require() {

View File

@ -733,8 +733,8 @@ pub(crate) fn diagnose_environment(
for diagnostic in site_packages.diagnostics(markers)? {
// Only surface diagnostics that are "relevant" to the current resolution.
if resolution
.packages()
.any(|package| diagnostic.includes(package))
.distributions()
.any(|dist| diagnostic.includes(dist.name()))
{
writeln!(
printer.stderr(),

View File

@ -510,19 +510,19 @@ fn apply_editable_mode(
version,
} = dist
else {
return dist;
return None;
};
ResolvedDist::Installable {
Some(ResolvedDist::Installable {
dist: Dist::Source(SourceDist::Directory(DirectorySourceDist {
name,
install_path,
name: name.clone(),
install_path: install_path.clone(),
editable: false,
r#virtual: false,
url,
url: url.clone(),
})),
version,
}
version: version.clone(),
})
}),
}
}