use std::borrow::Cow;
use std::collections::{BTreeSet, hash_map::Entry};
use petgraph::{
Directed, Direction,
graph::{Graph, NodeIndex},
};
use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};
use uv_configuration::{Constraints, DependencyModifierScope, DependencyModifiers};
use uv_distribution_types::{
DistributionId, HashCollection, IndexUrl, Name, Requirement, RequiresPython,
ResolutionDiagnostic, parse_url_hashes,
};
use uv_normalize::PackageName;
use uv_pep440::{Version, VersionSpecifier};
use uv_pypi_types::{Conflicts, HashDigests, ParsedUrl, VerbatimParsedUrl, Yanked};
use uv_types::HashStrategy;
use crate::graph_ops::{marker_reachability, simplify_conflict_markers};
use crate::preferences::Preferences;
use crate::resolution::{AnnotatedDist, ResolutionGraphNode, ResolverOutput};
use crate::resolution_mode::ResolutionStrategy;
use crate::resolver::{
ResolutionDependencyEdge, ResolutionNode, ResolutionPackage, ResolvedFork, SelectedDistribution,
};
use crate::universal_marker::{ConflictMarker, UniversalMarker};
use crate::{InMemoryIndex, MetadataResponse, Options, ResolveError, VersionsResponse};
pub(crate) fn from_state(
mut resolutions: Vec<ResolvedFork>,
project: Option<&PackageName>,
workspace_members: &BTreeSet<PackageName>,
requirements: Vec<Requirement>,
constraints: Constraints,
modifiers: DependencyModifiers,
preferences: &Preferences,
hasher: &HashStrategy,
index: &InMemoryIndex,
requires_python: RequiresPython,
conflicts: &Conflicts,
resolution_strategy: &ResolutionStrategy,
options: Options,
) -> Result<ResolverOutput, ResolveError> {
let size_guess = resolutions[0].nodes.len();
let mut graph: Graph<ResolutionGraphNode, UniversalMarker, Directed> =
Graph::with_capacity(size_guess, size_guess);
let mut inverse: FxHashMap<ResolutionNode, NodeIndex<u32>> =
FxHashMap::with_capacity_and_hasher(size_guess, FxBuildHasher);
let mut diagnostics = Vec::new();
let root_index = graph.add_node(ResolutionGraphNode::Root);
for resolution in &mut resolutions {
for (package, selected) in resolution.nodes.drain(..) {
let node = ResolutionNode {
package,
version: selected.version().clone(),
};
let Entry::Vacant(entry) = inverse.entry(node) else {
continue;
};
let package = &entry.key().package;
let node = add_version(
&mut graph,
&mut diagnostics,
preferences,
hasher,
index,
package,
selected,
project == Some(&package.name) || workspace_members.contains(&package.name),
);
entry.insert(node);
}
}
let mut seen = FxHashSet::default();
for resolution in &resolutions {
let marker = resolution.env.try_universal_markers().unwrap_or_default();
for edge in &resolution.edges {
if !seen.insert((edge, marker)) {
continue;
}
add_edge(&mut graph, &inverse, root_index, edge, marker);
}
}
let fork_markers: Vec<UniversalMarker> = if let [resolution] = resolutions.as_slice() {
resolution
.env
.try_universal_markers()
.into_iter()
.filter(|marker| !marker.is_true())
.collect()
} else {
resolutions
.iter()
.map(|resolution| resolution.env.try_universal_markers().unwrap_or_default())
.collect()
};
let mut reachability = marker_reachability(&graph, &fork_markers);
let conflict_marker = ConflictMarker::from_conflicts(conflicts);
for index in graph.node_indices() {
if let ResolutionGraphNode::Dist(dist) = &mut graph[index] {
dist.marker = reachability.remove(&index).unwrap_or_default();
dist.marker.imbibe(conflict_marker);
}
}
for weight in graph.edge_weights_mut() {
weight.imbibe(conflict_marker);
}
simplify_conflict_markers(conflicts, &mut graph);
graph.retain_nodes(|graph, node| !graph[node].marker().is_false());
if matches!(resolution_strategy, ResolutionStrategy::Lowest) {
report_missing_lower_bounds(&graph, &mut diagnostics, &constraints, &modifiers);
}
let output = ResolverOutput {
graph,
requires_python,
fork_markers,
diagnostics,
requirements,
constraints,
modifiers,
options,
};
if conflicts.is_empty() {
#[allow(unused_mut, reason = "Used in debug_assertions below")]
let mut conflicting = output.find_conflicting_distributions();
if !conflicting.is_empty() {
tracing::warn!(
"found {} conflicting distributions in resolution, \
please report this as a bug at \
`https://github.com/astral-sh/uv/issues/new`",
conflicting.len()
);
}
#[cfg(debug_assertions)]
if let Some(err) = conflicting.pop() {
return Err(ResolveError::ConflictingDistribution(err));
}
}
Ok(output)
}
fn add_edge(
graph: &mut Graph<ResolutionGraphNode, UniversalMarker>,
inverse: &FxHashMap<ResolutionNode, NodeIndex>,
root_index: NodeIndex,
edge: &ResolutionDependencyEdge,
marker: UniversalMarker,
) {
let from_index = edge.from.as_ref().map_or(root_index, |from| inverse[from]);
let to_index = inverse[&edge.to];
let edge_marker = {
let mut edge_marker = edge.universal_marker();
edge_marker.and(marker);
edge_marker
};
if let Some(weight) = graph
.find_edge(from_index, to_index)
.and_then(|edge| graph.edge_weight_mut(edge))
{
weight.or(edge_marker);
} else {
graph.update_edge(from_index, to_index, edge_marker);
}
}
fn add_version(
graph: &mut Graph<ResolutionGraphNode, UniversalMarker>,
diagnostics: &mut Vec<ResolutionDiagnostic>,
preferences: &Preferences,
hasher: &HashStrategy,
in_memory: &InMemoryIndex,
package: &ResolutionPackage,
selected: SelectedDistribution,
is_workspace_member: bool,
) -> NodeIndex {
let ResolutionPackage {
name,
kind,
url,
index,
} = &package;
let hashes = get_hashes(
name,
index.as_ref(),
url.as_ref(),
&selected.hashes_id(),
selected.version(),
preferences,
hasher,
in_memory,
);
let (version, dist, metadata) = selected.into_parts();
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.to_string()),
}),
}
let metadata = if is_workspace_member {
metadata.map(|metadata| metadata.with_force_relative(false))
} else {
metadata
};
if let Some(metadata) = metadata.as_ref() {
if let Some(extra) = kind.extra() {
if !metadata.provides_extra.contains(extra) {
diagnostics.push(ResolutionDiagnostic::MissingExtra {
dist: dist.clone(),
extra: extra.clone(),
});
}
}
if let Some(dev) = kind.group() {
if !metadata.dependency_groups.contains_key(dev) {
diagnostics.push(ResolutionDiagnostic::MissingGroup {
dist: dist.clone(),
group: dev.clone(),
});
}
}
}
graph.add_node(ResolutionGraphNode::Dist(AnnotatedDist {
dist,
name: name.clone(),
version,
kind: kind.clone(),
hashes,
metadata,
marker: UniversalMarker::TRUE,
}))
}
fn get_hashes(
name: &PackageName,
index: Option<&IndexUrl>,
url: Option<&VerbatimParsedUrl>,
metadata_id: &DistributionId,
version: &Version,
preferences: &Preferences,
hasher: &HashStrategy,
in_memory: &InMemoryIndex,
) -> HashDigests {
if let Some(digests) = preferences.match_hashes(name, version) {
if !digests.is_empty() {
return HashDigests::from(digests);
}
}
if let Some(url) = url {
let policy = hasher.archive_policy_for_url(&url.verbatim);
if !policy.digests().is_empty() {
return HashDigests::from(policy.digests());
}
}
if let Some(url) = url
&& let ParsedUrl::Archive(_) = &url.parsed_url
&& hasher.collection() != HashCollection::None
&& !hasher
.archive_policy_for_url(&url.verbatim)
.requires_validation()
&& let Some(hashes) = parse_url_hashes(&url.verbatim)
{
return hashes;
}
if let Some(metadata_response) = in_memory.distributions().get(metadata_id) {
if let MetadataResponse::Found(ref archive) = *metadata_response {
let mut digests = archive.hashes.clone();
digests.sort_unstable();
if !digests.is_empty() {
return digests;
}
}
}
if url.is_none() {
let implicit_response = in_memory.implicit().get(name);
let mut explicit_response = None;
let hashes = implicit_response
.as_ref()
.and_then(|response| {
if let VersionsResponse::Found(version_maps) = &**response {
Some(version_maps)
} else {
None
}
})
.into_iter()
.flatten()
.filter(|version_map| version_map.index() == index)
.find_map(|version_map| version_map.hashes(version))
.or_else(|| {
explicit_response = index
.and_then(|index| in_memory.explicit().get(&(name.clone(), index.clone())));
explicit_response
.as_ref()
.and_then(|response| {
if let VersionsResponse::Found(version_maps) = &**response {
Some(version_maps)
} else {
None
}
})
.into_iter()
.flatten()
.filter(|version_map| version_map.index() == index)
.find_map(|version_map| version_map.hashes(version))
});
if let Some(hashes) = hashes {
let mut digests = HashDigests::from(hashes);
digests.sort_unstable();
if !digests.is_empty() {
return digests;
}
}
}
HashDigests::empty()
}
fn report_missing_lower_bounds(
graph: &Graph<ResolutionGraphNode, UniversalMarker>,
diagnostics: &mut Vec<ResolutionDiagnostic>,
constraints: &Constraints,
modifiers: &DependencyModifiers,
) {
let mut missing_lower_bounds = Vec::new();
for node_index in graph.node_indices() {
let ResolutionGraphNode::Dist(dist) = graph.node_weight(node_index).unwrap() else {
continue;
};
if !has_lower_bound(node_index, dist.name(), graph, constraints, modifiers) {
missing_lower_bounds.push(dist.name());
}
}
missing_lower_bounds.sort_unstable();
diagnostics.extend(missing_lower_bounds.into_iter().map(|package_name| {
ResolutionDiagnostic::MissingLowerBound {
package_name: package_name.clone(),
}
}));
}
fn has_lower_bound(
node_index: NodeIndex,
package_name: &PackageName,
graph: &Graph<ResolutionGraphNode, UniversalMarker>,
constraints: &Constraints,
modifiers: &DependencyModifiers,
) -> bool {
for neighbor_index in graph.neighbors_directed(node_index, Direction::Incoming) {
let neighbor_dist = match graph.node_weight(neighbor_index).unwrap() {
ResolutionGraphNode::Root => {
return true;
}
ResolutionGraphNode::Dist(neighbor_dist) => neighbor_dist,
};
if neighbor_dist.name() == package_name {
return true;
}
let Some(metadata) = neighbor_dist.metadata.as_ref() else {
return true;
};
for requirement in modifiers
.apply(
DependencyModifierScope::Package(neighbor_dist.name(), &neighbor_dist.version),
metadata.requires_dist.iter(),
)
.chain(modifiers.apply(
DependencyModifierScope::Global,
metadata.dependency_groups.values().flatten(),
))
.chain(constraints.requirements().map(Cow::Borrowed))
{
if requirement.name != *package_name {
continue;
}
let Some(specifiers) = requirement.source.version_specifiers() else {
return true;
};
if specifiers.iter().any(VersionSpecifier::has_lower_bound) {
return true;
}
}
}
false
}