1use std::borrow::Cow;
2use std::collections::BTreeMap;
3use std::fmt::{Display, Formatter};
4use std::sync::Arc;
5
6use indexmap::IndexSet;
7use petgraph::{
8 Directed, Direction,
9 graph::{Graph, NodeIndex},
10};
11use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};
12
13use uv_configuration::{BuildOptions, Constraints, Overrides};
14use uv_distribution::Metadata;
15use uv_distribution_types::{
16 BuiltDist, Dist, DistributionId, Edge, HashCollection, Identifier, IndexUrl, Name, Node,
17 Requirement, RequiresPython, ResolutionDiagnostic, ResolvedDist, SourceDist, parse_url_hashes,
18};
19use uv_git::GitResolver;
20use uv_normalize::{ExtraName, GroupName, PackageName};
21use uv_pep440::{Version, VersionSpecifier};
22use uv_pep508::{MarkerEnvironment, MarkerTree, MarkerTreeKind};
23use uv_pypi_types::{Conflicts, HashDigests, ParsedUrl, ParsedUrlError, VerbatimParsedUrl, Yanked};
24use uv_types::HashStrategy;
25
26use crate::graph_ops::{marker_reachability, simplify_conflict_markers};
27use crate::pins::FilePins;
28use crate::preferences::Preferences;
29use crate::redirect::url_to_precise;
30use crate::resolution::AnnotatedDist;
31use crate::resolution_mode::ResolutionStrategy;
32use crate::resolver::{Resolution, ResolutionDependencyEdge, ResolutionPackage};
33use crate::universal_marker::{ConflictMarker, UniversalMarker};
34use crate::{InMemoryIndex, MetadataResponse, Options, ResolveError, VersionsResponse};
35
36#[derive(Debug)]
41pub struct ResolverOutput {
42 pub(crate) graph: Graph<ResolutionGraphNode, UniversalMarker, Directed>,
44 pub(crate) requires_python: RequiresPython,
46 pub(crate) fork_markers: Vec<UniversalMarker>,
49 pub(crate) diagnostics: Vec<ResolutionDiagnostic>,
51 pub(crate) requirements: Vec<Requirement>,
53 pub(crate) constraints: Constraints,
55 pub(crate) overrides: Overrides,
57 pub(crate) options: Options,
59}
60
61#[derive(Debug, Clone)]
62#[expect(clippy::large_enum_variant)]
63pub(crate) enum ResolutionGraphNode {
64 Root,
65 Dist(AnnotatedDist),
66}
67
68impl ResolutionGraphNode {
69 pub(crate) fn marker(&self) -> &UniversalMarker {
70 match self {
71 Self::Root => &UniversalMarker::TRUE,
72 Self::Dist(dist) => &dist.marker,
73 }
74 }
75
76 pub(crate) fn package_extra_names(&self) -> Option<(&PackageName, &ExtraName)> {
77 match self {
78 Self::Root => None,
79 Self::Dist(dist) => {
80 let extra = dist.extra.as_ref()?;
81 Some((&dist.name, extra))
82 }
83 }
84 }
85
86 pub(crate) fn package_group_names(&self) -> Option<(&PackageName, &GroupName)> {
87 match self {
88 Self::Root => None,
89 Self::Dist(dist) => {
90 let group = dist.group.as_ref()?;
91 Some((&dist.name, group))
92 }
93 }
94 }
95
96 pub(crate) fn package_name(&self) -> Option<&PackageName> {
97 match self {
98 Self::Root => None,
99 Self::Dist(dist) => Some(&dist.name),
100 }
101 }
102}
103
104impl Display for ResolutionGraphNode {
105 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
106 match self {
107 Self::Root => f.write_str("root"),
108 Self::Dist(dist) => Display::fmt(dist, f),
109 }
110 }
111}
112
113#[derive(Debug, Eq, PartialEq, Hash)]
114struct PackageRef<'a> {
115 package_name: &'a PackageName,
116 version: &'a Version,
117 url: Option<&'a VerbatimParsedUrl>,
118 index: Option<&'a IndexUrl>,
119 extra: Option<&'a ExtraName>,
120 group: Option<&'a GroupName>,
121}
122
123impl ResolverOutput {
124 pub(crate) fn from_state(
126 resolutions: &[Resolution],
127 requirements: Vec<Requirement>,
128 constraints: Constraints,
129 overrides: Overrides,
130 preferences: &Preferences,
131 hasher: &HashStrategy,
132 index: &InMemoryIndex,
133 git: &GitResolver,
134 requires_python: RequiresPython,
135 conflicts: &Conflicts,
136 resolution_strategy: &ResolutionStrategy,
137 options: Options,
138 ) -> Result<Self, ResolveError> {
139 let size_guess = resolutions[0].nodes.len();
140 let mut graph: Graph<ResolutionGraphNode, UniversalMarker, Directed> =
141 Graph::with_capacity(size_guess, size_guess);
142 let mut inverse: FxHashMap<PackageRef, NodeIndex<u32>> =
143 FxHashMap::with_capacity_and_hasher(size_guess, FxBuildHasher);
144 let mut diagnostics = Vec::new();
145
146 let root_index = graph.add_node(ResolutionGraphNode::Root);
148
149 let mut seen = FxHashSet::default();
150 for resolution in resolutions {
151 for (package, version) in &resolution.nodes {
153 if !seen.insert((package, version)) {
154 continue;
156 }
157 Self::add_version(
158 &mut graph,
159 &mut inverse,
160 &mut diagnostics,
161 preferences,
162 hasher,
163 &resolution.pins,
164 index,
165 git,
166 package,
167 version,
168 )?;
169 }
170 }
171
172 let mut seen = FxHashSet::default();
173 for resolution in resolutions {
174 let marker = resolution.env.try_universal_markers().unwrap_or_default();
175
176 for edge in &resolution.edges {
179 if !seen.insert((edge, marker)) {
180 continue;
182 }
183
184 Self::add_edge(&mut graph, &mut inverse, root_index, edge, marker);
185 }
186 }
187
188 let fork_markers: Vec<UniversalMarker> = if let [resolution] = resolutions {
189 resolution
193 .env
194 .try_universal_markers()
195 .into_iter()
196 .filter(|marker| !marker.is_true())
197 .collect()
198 } else {
199 resolutions
200 .iter()
201 .map(|resolution| resolution.env.try_universal_markers().unwrap_or_default())
202 .collect()
203 };
204
205 let mut reachability = marker_reachability(&graph, &fork_markers);
207
208 let conflict_marker = ConflictMarker::from_conflicts(conflicts);
211 for index in graph.node_indices() {
212 if let ResolutionGraphNode::Dist(dist) = &mut graph[index] {
213 dist.marker = reachability.remove(&index).unwrap_or_default();
214 dist.marker.imbibe(conflict_marker);
215 }
216 }
217 for weight in graph.edge_weights_mut() {
218 weight.imbibe(conflict_marker);
219 }
220
221 simplify_conflict_markers(conflicts, &mut graph);
222
223 graph.retain_nodes(|graph, node| !graph[node].marker().is_false());
225
226 if matches!(resolution_strategy, ResolutionStrategy::Lowest) {
227 report_missing_lower_bounds(&graph, &mut diagnostics, &constraints, &overrides);
228 }
229
230 let output = Self {
231 graph,
232 requires_python,
233 fork_markers,
234 diagnostics,
235 requirements,
236 constraints,
237 overrides,
238 options,
239 };
240
241 if conflicts.is_empty() {
256 #[allow(unused_mut, reason = "Used in debug_assertions below")]
257 let mut conflicting = output.find_conflicting_distributions();
258 if !conflicting.is_empty() {
259 tracing::warn!(
260 "found {} conflicting distributions in resolution, \
261 please report this as a bug at \
262 https://github.com/astral-sh/uv/issues/new",
263 conflicting.len()
264 );
265 }
266 #[cfg(debug_assertions)]
275 if let Some(err) = conflicting.pop() {
276 return Err(ResolveError::ConflictingDistribution(err));
277 }
278 }
279 Ok(output)
280 }
281
282 fn add_edge(
283 graph: &mut Graph<ResolutionGraphNode, UniversalMarker>,
284 inverse: &mut FxHashMap<PackageRef<'_>, NodeIndex>,
285 root_index: NodeIndex,
286 edge: &ResolutionDependencyEdge,
287 marker: UniversalMarker,
288 ) {
289 let from_index = edge.from.as_ref().map_or(root_index, |from| {
290 inverse[&PackageRef {
291 package_name: from,
292 version: &edge.from_version,
293 url: edge.from_url.as_ref(),
294 index: edge.from_index.as_ref(),
295 extra: edge.from_extra.as_ref(),
296 group: edge.from_group.as_ref(),
297 }]
298 });
299 let to_index = inverse[&PackageRef {
300 package_name: &edge.to,
301 version: &edge.to_version,
302 url: edge.to_url.as_ref(),
303 index: edge.to_index.as_ref(),
304 extra: edge.to_extra.as_ref(),
305 group: edge.to_group.as_ref(),
306 }];
307
308 let edge_marker = {
309 let mut edge_marker = edge.universal_marker();
310 edge_marker.and(marker);
311 edge_marker
312 };
313
314 if let Some(weight) = graph
315 .find_edge(from_index, to_index)
316 .and_then(|edge| graph.edge_weight_mut(edge))
317 {
318 weight.or(edge_marker);
321 } else {
322 graph.update_edge(from_index, to_index, edge_marker);
323 }
324 }
325
326 fn add_version<'a>(
327 graph: &mut Graph<ResolutionGraphNode, UniversalMarker>,
328 inverse: &mut FxHashMap<PackageRef<'a>, NodeIndex>,
329 diagnostics: &mut Vec<ResolutionDiagnostic>,
330 preferences: &Preferences,
331 hasher: &HashStrategy,
332 pins: &FilePins,
333 in_memory: &InMemoryIndex,
334 git: &GitResolver,
335 package: &'a ResolutionPackage,
336 version: &'a Version,
337 ) -> Result<(), ResolveError> {
338 let ResolutionPackage {
339 name,
340 extra,
341 dev: group,
342 url,
343 index,
344 } = &package;
345 let (dist, hashes, metadata) = Self::parse_dist(
347 name,
348 index.as_ref(),
349 url.as_ref(),
350 version,
351 pins,
352 diagnostics,
353 preferences,
354 hasher,
355 in_memory,
356 git,
357 )?;
358
359 if let Some(metadata) = metadata.as_ref() {
360 if let Some(extra) = extra {
362 if !metadata.provides_extra.contains(extra) {
363 diagnostics.push(ResolutionDiagnostic::MissingExtra {
364 dist: dist.clone(),
365 extra: extra.clone(),
366 });
367 }
368 }
369
370 if let Some(dev) = group {
372 if !metadata.dependency_groups.contains_key(dev) {
373 diagnostics.push(ResolutionDiagnostic::MissingGroup {
374 dist: dist.clone(),
375 group: dev.clone(),
376 });
377 }
378 }
379 }
380
381 let node = graph.add_node(ResolutionGraphNode::Dist(AnnotatedDist {
383 dist,
384 name: name.clone(),
385 version: version.clone(),
386 extra: extra.clone(),
387 group: group.clone(),
388 hashes,
389 metadata,
390 marker: UniversalMarker::TRUE,
391 }));
392 inverse.insert(
393 PackageRef {
394 package_name: name,
395 version,
396 url: url.as_ref(),
397 index: index.as_ref(),
398 extra: extra.as_ref(),
399 group: group.as_ref(),
400 },
401 node,
402 );
403 Ok(())
404 }
405
406 fn parse_dist(
407 name: &PackageName,
408 index: Option<&IndexUrl>,
409 url: Option<&VerbatimParsedUrl>,
410 version: &Version,
411 pins: &FilePins,
412 diagnostics: &mut Vec<ResolutionDiagnostic>,
413 preferences: &Preferences,
414 hasher: &HashStrategy,
415 in_memory: &InMemoryIndex,
416 git: &GitResolver,
417 ) -> Result<(ResolvedDist, HashDigests, Option<Metadata>), ResolveError> {
418 Ok(if let Some(url) = url {
419 let dist = Dist::from_url(name.clone(), url_to_precise(url.clone(), git))?;
422 let metadata_id = Dist::from_url(name.clone(), url.clone())?.distribution_id();
423
424 let hashes = Self::get_hashes(
426 name,
427 index,
428 Some(url),
429 &metadata_id,
430 version,
431 preferences,
432 hasher,
433 in_memory,
434 );
435
436 let metadata = {
438 let response = in_memory
439 .distributions()
440 .get(&metadata_id)
441 .unwrap_or_else(|| {
442 panic!("Every URL distribution should have metadata: {metadata_id:?}")
443 });
444
445 let MetadataResponse::Found(archive) = &*response else {
446 panic!("Every URL distribution should have metadata: {metadata_id:?}")
447 };
448
449 archive.metadata.clone()
450 };
451
452 (
453 ResolvedDist::Installable {
454 dist: Arc::new(dist),
455 version: Some(version.clone()),
456 },
457 hashes,
458 Some(metadata),
459 )
460 } else {
461 let (dist, metadata_id) = pins
462 .dist_and_id(name, version)
463 .expect("Every package should be pinned");
464 let dist = dist.clone();
465 let hashes_id = dist.distribution_id();
466
467 match dist.yanked() {
469 None | Some(Yanked::Bool(false)) => {}
470 Some(Yanked::Bool(true)) => {
471 diagnostics.push(ResolutionDiagnostic::YankedVersion {
472 dist: dist.clone(),
473 reason: None,
474 });
475 }
476 Some(Yanked::Reason(reason)) => {
477 diagnostics.push(ResolutionDiagnostic::YankedVersion {
478 dist: dist.clone(),
479 reason: Some(reason.to_string()),
480 });
481 }
482 }
483
484 let hashes = Self::get_hashes(
486 name,
487 index,
488 None,
489 &hashes_id,
490 version,
491 preferences,
492 hasher,
493 in_memory,
494 );
495
496 let metadata = {
498 in_memory
499 .distributions()
500 .get(metadata_id)
501 .and_then(|response| {
502 if let MetadataResponse::Found(archive) = &*response {
503 Some(archive.metadata.clone())
504 } else {
505 None
506 }
507 })
508 };
509
510 (dist, hashes, metadata)
511 })
512 }
513
514 fn get_hashes(
517 name: &PackageName,
518 index: Option<&IndexUrl>,
519 url: Option<&VerbatimParsedUrl>,
520 metadata_id: &DistributionId,
521 version: &Version,
522 preferences: &Preferences,
523 hasher: &HashStrategy,
524 in_memory: &InMemoryIndex,
525 ) -> HashDigests {
526 if let Some(digests) = preferences.match_hashes(name, version) {
528 if !digests.is_empty() {
529 return HashDigests::from(digests);
530 }
531 }
532
533 if let Some(url) = url
535 && let ParsedUrl::Archive(_) = &url.parsed_url
536 && hasher.collection() != HashCollection::None
537 && !hasher
538 .archive_policy_for_url(&url.verbatim)
539 .requires_validation()
540 && let Some(hashes) = parse_url_hashes(&url.verbatim)
541 {
542 return hashes;
543 }
544
545 if let Some(metadata_response) = in_memory.distributions().get(metadata_id) {
547 if let MetadataResponse::Found(ref archive) = *metadata_response {
548 let mut digests = archive.hashes.clone();
549 digests.sort_unstable();
550 if !digests.is_empty() {
551 return digests;
552 }
553 }
554 }
555
556 if url.is_none() {
558 let implicit_response = in_memory.implicit().get(name);
560 let mut explicit_response = None;
561
562 let hashes = implicit_response
564 .as_ref()
565 .and_then(|response| {
566 if let VersionsResponse::Found(version_maps) = &**response {
567 Some(version_maps)
568 } else {
569 None
570 }
571 })
572 .into_iter()
573 .flatten()
574 .filter(|version_map| version_map.index() == index)
575 .find_map(|version_map| version_map.hashes(version))
576 .or_else(|| {
577 explicit_response = index
579 .and_then(|index| in_memory.explicit().get(&(name.clone(), index.clone())));
580 explicit_response
581 .as_ref()
582 .and_then(|response| {
583 if let VersionsResponse::Found(version_maps) = &**response {
584 Some(version_maps)
585 } else {
586 None
587 }
588 })
589 .into_iter()
590 .flatten()
591 .filter(|version_map| version_map.index() == index)
592 .find_map(|version_map| version_map.hashes(version))
593 });
594
595 if let Some(hashes) = hashes {
596 let mut digests = HashDigests::from(hashes);
597 digests.sort_unstable();
598 if !digests.is_empty() {
599 return digests;
600 }
601 }
602 }
603
604 HashDigests::empty()
605 }
606
607 fn dists(&self) -> impl Iterator<Item = &AnnotatedDist> {
609 self.graph
610 .node_indices()
611 .filter_map(move |index| match &self.graph[index] {
612 ResolutionGraphNode::Root => None,
613 ResolutionGraphNode::Dist(dist) => Some(dist),
614 })
615 }
616
617 pub(crate) fn base_dists(&self) -> impl Iterator<Item = (NodeIndex, &AnnotatedDist)> {
619 self.graph
620 .node_indices()
621 .filter_map(move |node_index| match &self.graph[node_index] {
622 ResolutionGraphNode::Root => None,
623 ResolutionGraphNode::Dist(dist) => dist.is_base().then_some((node_index, dist)),
624 })
625 }
626
627 pub fn len(&self) -> usize {
629 self.base_dists().count()
630 }
631
632 pub fn is_empty(&self) -> bool {
634 self.base_dists().next().is_none()
635 }
636
637 pub fn retain_allowed_distribution_hashes(&mut self, build_options: &BuildOptions) {
642 for node in self.graph.node_weights_mut() {
643 let ResolutionGraphNode::Dist(distribution) = node else {
644 continue;
645 };
646 let ResolvedDist::Installable { dist, .. } = &distribution.dist else {
647 continue;
648 };
649 let allowed_hashes = match dist.as_ref() {
650 Dist::Built(BuiltDist::Registry(dist))
651 if build_options.no_build_package(&distribution.name) =>
652 {
653 dist.wheels
654 .iter()
655 .flat_map(|wheel| wheel.file.hashes.iter())
656 .collect::<FxHashSet<_>>()
657 }
658 Dist::Source(SourceDist::Registry(source))
659 if build_options.no_binary_package(&distribution.name) =>
660 {
661 source.file.hashes.iter().collect::<FxHashSet<_>>()
662 }
663 _ => continue,
664 };
665 if allowed_hashes.is_empty() {
666 continue;
667 }
668
669 let hashes = distribution
670 .hashes
671 .iter()
672 .filter(|hash| allowed_hashes.contains(hash))
673 .cloned()
674 .collect::<Vec<_>>();
675 if !hashes.is_empty() {
676 distribution.hashes = HashDigests::from(hashes);
677 }
678 }
679 }
680
681 pub fn contains(&self, name: &PackageName) -> bool {
683 self.dists().any(|dist| dist.name() == name)
684 }
685
686 pub fn diagnostics(&self) -> &[ResolutionDiagnostic] {
688 &self.diagnostics
689 }
690
691 pub fn marker_tree(
712 &self,
713 index: &InMemoryIndex,
714 marker_env: &MarkerEnvironment,
715 ) -> Result<MarkerTree, Box<ParsedUrlError>> {
716 use uv_pep508::{
717 CanonicalMarkerValueString, CanonicalMarkerValueVersion, MarkerExpression,
718 MarkerOperator, MarkerTree,
719 };
720
721 #[derive(Debug, Eq, Hash, PartialEq)]
727 enum MarkerParam {
728 Version(CanonicalMarkerValueVersion),
729 String(CanonicalMarkerValueString),
730 }
731
732 fn add_marker_params_from_tree(marker_tree: MarkerTree, set: &mut IndexSet<MarkerParam>) {
734 match marker_tree.kind() {
735 MarkerTreeKind::True => {}
736 MarkerTreeKind::False => {}
737 MarkerTreeKind::Version(marker) => {
738 set.insert(MarkerParam::Version(marker.key()));
739 for (_, tree) in marker.edges() {
740 add_marker_params_from_tree(tree, set);
741 }
742 }
743 MarkerTreeKind::String(marker) => {
744 set.insert(MarkerParam::String(marker.key()));
745 for (_, tree) in marker.children() {
746 add_marker_params_from_tree(tree, set);
747 }
748 }
749 MarkerTreeKind::In(marker) => {
750 set.insert(MarkerParam::String(marker.key()));
751 for (_, tree) in marker.children() {
752 add_marker_params_from_tree(tree, set);
753 }
754 }
755 MarkerTreeKind::Contains(marker) => {
756 set.insert(MarkerParam::String(marker.key()));
757 for (_, tree) in marker.children() {
758 add_marker_params_from_tree(tree, set);
759 }
760 }
761 MarkerTreeKind::Extra(marker) => {
767 for (_, tree) in marker.children() {
768 add_marker_params_from_tree(tree, set);
769 }
770 }
771 MarkerTreeKind::List(marker) => {
772 for (_, tree) in marker.children() {
773 add_marker_params_from_tree(tree, set);
774 }
775 }
776 }
777 }
778
779 let mut seen_marker_values = IndexSet::default();
780 for i in self.graph.node_indices() {
781 let ResolutionGraphNode::Dist(dist) = &self.graph[i] else {
782 continue;
783 };
784 let metadata_id = dist.dist.distribution_id();
785 let res = index
786 .distributions()
787 .get(&metadata_id)
788 .expect("every package in resolution graph has metadata");
789 let MetadataResponse::Found(archive, ..) = &*res else {
790 panic!("Every package should have metadata: {metadata_id:?}")
791 };
792 for req in self.constraints.apply(self.overrides.apply_for(
793 &dist.name,
794 &dist.version,
795 archive.metadata.requires_dist.iter(),
796 )) {
797 add_marker_params_from_tree(req.marker, &mut seen_marker_values);
798 }
799 }
800
801 for direct_req in self
803 .constraints
804 .apply(self.overrides.apply(self.requirements.iter()))
805 {
806 add_marker_params_from_tree(direct_req.marker, &mut seen_marker_values);
807 }
808
809 let mut conjunction = MarkerTree::TRUE;
812 for marker_param in seen_marker_values {
813 let expr = match marker_param {
814 MarkerParam::Version(value_version) => {
815 let from_env = marker_env.get_version(value_version);
816 MarkerExpression::Version {
817 key: value_version.into(),
818 specifier: VersionSpecifier::equals_version(from_env.clone()),
819 }
820 }
821 MarkerParam::String(value_string) => {
822 let from_env = marker_env.get_string(value_string);
823 MarkerExpression::String {
824 key: value_string.into(),
825 operator: MarkerOperator::Equal,
826 value: from_env.into(),
827 }
828 }
829 };
830 conjunction = conjunction.and(MarkerTree::expression(expr));
831 }
832 Ok(conjunction)
833 }
834
835 fn find_conflicting_distributions(&self) -> Vec<ConflictingDistributionError> {
844 let mut name_to_markers: BTreeMap<&PackageName, Vec<(&Version, &UniversalMarker)>> =
845 BTreeMap::new();
846 for node in self.graph.node_weights() {
847 let annotated_dist = match node {
848 ResolutionGraphNode::Root => continue,
849 ResolutionGraphNode::Dist(annotated_dist) => annotated_dist,
850 };
851 name_to_markers
852 .entry(&annotated_dist.name)
853 .or_default()
854 .push((&annotated_dist.version, &annotated_dist.marker));
855 }
856 let mut dupes = vec![];
857 for (name, marker_trees) in name_to_markers {
858 for (i, (version1, marker1)) in marker_trees.iter().enumerate() {
859 for (version2, marker2) in &marker_trees[i + 1..] {
860 if version1 == version2 {
861 continue;
862 }
863 if !marker1.is_disjoint(**marker2) {
864 dupes.push(ConflictingDistributionError {
865 name: name.clone(),
866 version1: (*version1).clone(),
867 version2: (*version2).clone(),
868 marker1: **marker1,
869 marker2: **marker2,
870 });
871 }
872 }
873 }
874 }
875 dupes
876 }
877}
878
879#[derive(Debug)]
886pub struct ConflictingDistributionError {
887 name: PackageName,
888 version1: Version,
889 version2: Version,
890 marker1: UniversalMarker,
891 marker2: UniversalMarker,
892}
893
894impl std::error::Error for ConflictingDistributionError {}
895
896impl Display for ConflictingDistributionError {
897 fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
898 let Self {
899 ref name,
900 ref version1,
901 ref version2,
902 ref marker1,
903 ref marker2,
904 } = *self;
905 write!(
906 f,
907 "found conflicting versions for package `{name}`:
908 `{marker1:?}` (for version `{version1}`) is not disjoint with \
909 `{marker2:?}` (for version `{version2}`)",
910 )
911 }
912}
913
914impl From<ResolverOutput> for uv_distribution_types::Resolution {
926 fn from(output: ResolverOutput) -> Self {
927 let ResolverOutput {
928 graph,
929 diagnostics,
930 fork_markers,
931 ..
932 } = output;
933
934 assert!(
935 fork_markers.is_empty(),
936 "universal resolutions are not supported"
937 );
938
939 let mut transformed = Graph::with_capacity(graph.node_count(), graph.edge_count());
940 let mut inverse = FxHashMap::with_capacity_and_hasher(graph.node_count(), FxBuildHasher);
941
942 let root = transformed.add_node(Node::Root);
944
945 for index in graph.node_indices() {
947 let ResolutionGraphNode::Dist(dist) = &graph[index] else {
948 continue;
949 };
950 if dist.is_base() {
951 inverse.insert(
952 &dist.name,
953 transformed.add_node(Node::Dist {
954 dist: dist.dist.clone(),
955 hashes: dist.hashes.clone(),
956 install: true,
957 }),
958 );
959 }
960 }
961
962 for edge in graph.edge_indices() {
964 let (source, target) = graph.edge_endpoints(edge).unwrap();
965
966 match (&graph[source], &graph[target]) {
967 (ResolutionGraphNode::Root, ResolutionGraphNode::Dist(target_dist)) => {
968 let target = inverse[&target_dist.name()];
969 transformed.update_edge(root, target, Edge::Prod);
970 }
971 (
972 ResolutionGraphNode::Dist(source_dist),
973 ResolutionGraphNode::Dist(target_dist),
974 ) => {
975 let source = inverse[&source_dist.name()];
976 let target = inverse[&target_dist.name()];
977
978 let edge = if let Some(extra) = source_dist.extra.as_ref() {
979 Edge::Optional(extra.clone())
980 } else if let Some(group) = source_dist.group.as_ref() {
981 Edge::Dev(group.clone())
982 } else {
983 Edge::Prod
984 };
985
986 transformed.add_edge(source, target, edge);
987 }
988 _ => {
989 unreachable!("root should not contain incoming edges");
990 }
991 }
992 }
993
994 Self::new(transformed).with_diagnostics(diagnostics)
995 }
996}
997
998fn report_missing_lower_bounds(
1000 graph: &Graph<ResolutionGraphNode, UniversalMarker>,
1001 diagnostics: &mut Vec<ResolutionDiagnostic>,
1002 constraints: &Constraints,
1003 overrides: &Overrides,
1004) {
1005 for node_index in graph.node_indices() {
1006 let ResolutionGraphNode::Dist(dist) = graph.node_weight(node_index).unwrap() else {
1007 continue;
1009 };
1010 if !has_lower_bound(node_index, dist.name(), graph, constraints, overrides) {
1011 diagnostics.push(ResolutionDiagnostic::MissingLowerBound {
1012 package_name: dist.name().clone(),
1013 });
1014 }
1015 }
1016}
1017
1018fn has_lower_bound(
1020 node_index: NodeIndex,
1021 package_name: &PackageName,
1022 graph: &Graph<ResolutionGraphNode, UniversalMarker>,
1023 constraints: &Constraints,
1024 overrides: &Overrides,
1025) -> bool {
1026 for neighbor_index in graph.neighbors_directed(node_index, Direction::Incoming) {
1027 let neighbor_dist = match graph.node_weight(neighbor_index).unwrap() {
1028 ResolutionGraphNode::Root => {
1029 return true;
1032 }
1033 ResolutionGraphNode::Dist(neighbor_dist) => neighbor_dist,
1034 };
1035
1036 if neighbor_dist.name() == package_name {
1037 return true;
1039 }
1040
1041 let Some(metadata) = neighbor_dist.metadata.as_ref() else {
1042 return true;
1044 };
1045
1046 for requirement in overrides
1049 .apply_for(
1050 neighbor_dist.name(),
1051 &neighbor_dist.version,
1052 metadata.requires_dist.iter(),
1053 )
1054 .chain(overrides.apply(metadata.dependency_groups.values().flatten()))
1055 .chain(constraints.requirements().map(Cow::Borrowed))
1057 {
1058 if requirement.name != *package_name {
1059 continue;
1060 }
1061 let Some(specifiers) = requirement.source.version_specifiers() else {
1062 return true;
1064 };
1065 if specifiers.iter().any(VersionSpecifier::has_lower_bound) {
1066 return true;
1067 }
1068 }
1069 }
1070 false
1071}