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, Identifier, IndexUrl, Name, Node, Requirement,
17 RequiresPython, ResolutionDiagnostic, ResolvedDist, SourceDist,
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, ParsedUrlError, VerbatimParsedUrl, Yanked};
24
25use crate::graph_ops::{marker_reachability, simplify_conflict_markers};
26use crate::pins::FilePins;
27use crate::preferences::Preferences;
28use crate::redirect::url_to_precise;
29use crate::resolution::AnnotatedDist;
30use crate::resolution_mode::ResolutionStrategy;
31use crate::resolver::{Resolution, ResolutionDependencyEdge, ResolutionPackage};
32use crate::universal_marker::{ConflictMarker, UniversalMarker};
33use crate::{InMemoryIndex, MetadataResponse, Options, ResolveError, VersionsResponse};
34
35#[derive(Debug)]
40pub struct ResolverOutput {
41 pub(crate) graph: Graph<ResolutionGraphNode, UniversalMarker, Directed>,
43 pub(crate) requires_python: RequiresPython,
45 pub(crate) fork_markers: Vec<UniversalMarker>,
48 pub(crate) diagnostics: Vec<ResolutionDiagnostic>,
50 pub(crate) requirements: Vec<Requirement>,
52 pub(crate) constraints: Constraints,
54 pub(crate) overrides: Overrides,
56 pub(crate) options: Options,
58}
59
60#[derive(Debug, Clone)]
61#[expect(clippy::large_enum_variant)]
62pub(crate) enum ResolutionGraphNode {
63 Root,
64 Dist(AnnotatedDist),
65}
66
67impl ResolutionGraphNode {
68 pub(crate) fn marker(&self) -> &UniversalMarker {
69 match self {
70 Self::Root => &UniversalMarker::TRUE,
71 Self::Dist(dist) => &dist.marker,
72 }
73 }
74
75 pub(crate) fn package_extra_names(&self) -> Option<(&PackageName, &ExtraName)> {
76 match self {
77 Self::Root => None,
78 Self::Dist(dist) => {
79 let extra = dist.extra.as_ref()?;
80 Some((&dist.name, extra))
81 }
82 }
83 }
84
85 pub(crate) fn package_group_names(&self) -> Option<(&PackageName, &GroupName)> {
86 match self {
87 Self::Root => None,
88 Self::Dist(dist) => {
89 let group = dist.group.as_ref()?;
90 Some((&dist.name, group))
91 }
92 }
93 }
94
95 pub(crate) fn package_name(&self) -> Option<&PackageName> {
96 match self {
97 Self::Root => None,
98 Self::Dist(dist) => Some(&dist.name),
99 }
100 }
101}
102
103impl Display for ResolutionGraphNode {
104 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
105 match self {
106 Self::Root => f.write_str("root"),
107 Self::Dist(dist) => Display::fmt(dist, f),
108 }
109 }
110}
111
112#[derive(Debug, Eq, PartialEq, Hash)]
113struct PackageRef<'a> {
114 package_name: &'a PackageName,
115 version: &'a Version,
116 url: Option<&'a VerbatimParsedUrl>,
117 index: Option<&'a IndexUrl>,
118 extra: Option<&'a ExtraName>,
119 group: Option<&'a GroupName>,
120}
121
122impl ResolverOutput {
123 pub(crate) fn from_state(
125 resolutions: &[Resolution],
126 requirements: Vec<Requirement>,
127 constraints: Constraints,
128 overrides: Overrides,
129 preferences: &Preferences,
130 index: &InMemoryIndex,
131 git: &GitResolver,
132 requires_python: RequiresPython,
133 conflicts: &Conflicts,
134 resolution_strategy: &ResolutionStrategy,
135 options: Options,
136 ) -> Result<Self, ResolveError> {
137 let size_guess = resolutions[0].nodes.len();
138 let mut graph: Graph<ResolutionGraphNode, UniversalMarker, Directed> =
139 Graph::with_capacity(size_guess, size_guess);
140 let mut inverse: FxHashMap<PackageRef, NodeIndex<u32>> =
141 FxHashMap::with_capacity_and_hasher(size_guess, FxBuildHasher);
142 let mut diagnostics = Vec::new();
143
144 let root_index = graph.add_node(ResolutionGraphNode::Root);
146
147 let mut seen = FxHashSet::default();
148 for resolution in resolutions {
149 for (package, version) in &resolution.nodes {
151 if !seen.insert((package, version)) {
152 continue;
154 }
155 Self::add_version(
156 &mut graph,
157 &mut inverse,
158 &mut diagnostics,
159 preferences,
160 &resolution.pins,
161 index,
162 git,
163 package,
164 version,
165 )?;
166 }
167 }
168
169 let mut seen = FxHashSet::default();
170 for resolution in resolutions {
171 let marker = resolution.env.try_universal_markers().unwrap_or_default();
172
173 for edge in &resolution.edges {
176 if !seen.insert((edge, marker)) {
177 continue;
179 }
180
181 Self::add_edge(&mut graph, &mut inverse, root_index, edge, marker);
182 }
183 }
184
185 let fork_markers: Vec<UniversalMarker> = if let [resolution] = resolutions {
186 resolution
190 .env
191 .try_universal_markers()
192 .into_iter()
193 .filter(|marker| !marker.is_true())
194 .collect()
195 } else {
196 resolutions
197 .iter()
198 .map(|resolution| resolution.env.try_universal_markers().unwrap_or_default())
199 .collect()
200 };
201
202 let mut reachability = marker_reachability(&graph, &fork_markers);
204
205 let conflict_marker = ConflictMarker::from_conflicts(conflicts);
208 for index in graph.node_indices() {
209 if let ResolutionGraphNode::Dist(dist) = &mut graph[index] {
210 dist.marker = reachability.remove(&index).unwrap_or_default();
211 dist.marker.imbibe(conflict_marker);
212 }
213 }
214 for weight in graph.edge_weights_mut() {
215 weight.imbibe(conflict_marker);
216 }
217
218 simplify_conflict_markers(conflicts, &mut graph);
219
220 graph.retain_nodes(|graph, node| !graph[node].marker().is_false());
222
223 if matches!(resolution_strategy, ResolutionStrategy::Lowest) {
224 report_missing_lower_bounds(&graph, &mut diagnostics, &constraints, &overrides);
225 }
226
227 let output = Self {
228 graph,
229 requires_python,
230 fork_markers,
231 diagnostics,
232 requirements,
233 constraints,
234 overrides,
235 options,
236 };
237
238 if conflicts.is_empty() {
253 #[allow(unused_mut, reason = "Used in debug_assertions below")]
254 let mut conflicting = output.find_conflicting_distributions();
255 if !conflicting.is_empty() {
256 tracing::warn!(
257 "found {} conflicting distributions in resolution, \
258 please report this as a bug at \
259 https://github.com/astral-sh/uv/issues/new",
260 conflicting.len()
261 );
262 }
263 #[cfg(debug_assertions)]
272 if let Some(err) = conflicting.pop() {
273 return Err(ResolveError::ConflictingDistribution(err));
274 }
275 }
276 Ok(output)
277 }
278
279 fn add_edge(
280 graph: &mut Graph<ResolutionGraphNode, UniversalMarker>,
281 inverse: &mut FxHashMap<PackageRef<'_>, NodeIndex>,
282 root_index: NodeIndex,
283 edge: &ResolutionDependencyEdge,
284 marker: UniversalMarker,
285 ) {
286 let from_index = edge.from.as_ref().map_or(root_index, |from| {
287 inverse[&PackageRef {
288 package_name: from,
289 version: &edge.from_version,
290 url: edge.from_url.as_ref(),
291 index: edge.from_index.as_ref(),
292 extra: edge.from_extra.as_ref(),
293 group: edge.from_group.as_ref(),
294 }]
295 });
296 let to_index = inverse[&PackageRef {
297 package_name: &edge.to,
298 version: &edge.to_version,
299 url: edge.to_url.as_ref(),
300 index: edge.to_index.as_ref(),
301 extra: edge.to_extra.as_ref(),
302 group: edge.to_group.as_ref(),
303 }];
304
305 let edge_marker = {
306 let mut edge_marker = edge.universal_marker();
307 edge_marker.and(marker);
308 edge_marker
309 };
310
311 if let Some(weight) = graph
312 .find_edge(from_index, to_index)
313 .and_then(|edge| graph.edge_weight_mut(edge))
314 {
315 weight.or(edge_marker);
318 } else {
319 graph.update_edge(from_index, to_index, edge_marker);
320 }
321 }
322
323 fn add_version<'a>(
324 graph: &mut Graph<ResolutionGraphNode, UniversalMarker>,
325 inverse: &mut FxHashMap<PackageRef<'a>, NodeIndex>,
326 diagnostics: &mut Vec<ResolutionDiagnostic>,
327 preferences: &Preferences,
328 pins: &FilePins,
329 in_memory: &InMemoryIndex,
330 git: &GitResolver,
331 package: &'a ResolutionPackage,
332 version: &'a Version,
333 ) -> Result<(), ResolveError> {
334 let ResolutionPackage {
335 name,
336 extra,
337 dev: group,
338 url,
339 index,
340 } = &package;
341 let (dist, hashes, metadata) = Self::parse_dist(
343 name,
344 index.as_ref(),
345 url.as_ref(),
346 version,
347 pins,
348 diagnostics,
349 preferences,
350 in_memory,
351 git,
352 )?;
353
354 if let Some(metadata) = metadata.as_ref() {
355 if let Some(extra) = extra {
357 if !metadata.provides_extra.contains(extra) {
358 diagnostics.push(ResolutionDiagnostic::MissingExtra {
359 dist: dist.clone(),
360 extra: extra.clone(),
361 });
362 }
363 }
364
365 if let Some(dev) = group {
367 if !metadata.dependency_groups.contains_key(dev) {
368 diagnostics.push(ResolutionDiagnostic::MissingGroup {
369 dist: dist.clone(),
370 group: dev.clone(),
371 });
372 }
373 }
374 }
375
376 let node = graph.add_node(ResolutionGraphNode::Dist(AnnotatedDist {
378 dist,
379 name: name.clone(),
380 version: version.clone(),
381 extra: extra.clone(),
382 group: group.clone(),
383 hashes,
384 metadata,
385 marker: UniversalMarker::TRUE,
386 }));
387 inverse.insert(
388 PackageRef {
389 package_name: name,
390 version,
391 url: url.as_ref(),
392 index: index.as_ref(),
393 extra: extra.as_ref(),
394 group: group.as_ref(),
395 },
396 node,
397 );
398 Ok(())
399 }
400
401 fn parse_dist(
402 name: &PackageName,
403 index: Option<&IndexUrl>,
404 url: Option<&VerbatimParsedUrl>,
405 version: &Version,
406 pins: &FilePins,
407 diagnostics: &mut Vec<ResolutionDiagnostic>,
408 preferences: &Preferences,
409 in_memory: &InMemoryIndex,
410 git: &GitResolver,
411 ) -> Result<(ResolvedDist, HashDigests, Option<Metadata>), ResolveError> {
412 Ok(if let Some(url) = url {
413 let dist = Dist::from_url(name.clone(), url_to_precise(url.clone(), git))?;
416 let metadata_id = Dist::from_url(name.clone(), url.clone())?.distribution_id();
417
418 let hashes = Self::get_hashes(
420 name,
421 index,
422 Some(url),
423 &metadata_id,
424 version,
425 preferences,
426 in_memory,
427 );
428
429 let metadata = {
431 let response = in_memory
432 .distributions()
433 .get(&metadata_id)
434 .unwrap_or_else(|| {
435 panic!("Every URL distribution should have metadata: {metadata_id:?}")
436 });
437
438 let MetadataResponse::Found(archive) = &*response else {
439 panic!("Every URL distribution should have metadata: {metadata_id:?}")
440 };
441
442 archive.metadata.clone()
443 };
444
445 (
446 ResolvedDist::Installable {
447 dist: Arc::new(dist),
448 version: Some(version.clone()),
449 },
450 hashes,
451 Some(metadata),
452 )
453 } else {
454 let (dist, metadata_id) = pins
455 .dist_and_id(name, version)
456 .expect("Every package should be pinned");
457 let dist = dist.clone();
458 let hashes_id = dist.distribution_id();
459
460 match dist.yanked() {
462 None | Some(Yanked::Bool(false)) => {}
463 Some(Yanked::Bool(true)) => {
464 diagnostics.push(ResolutionDiagnostic::YankedVersion {
465 dist: dist.clone(),
466 reason: None,
467 });
468 }
469 Some(Yanked::Reason(reason)) => {
470 diagnostics.push(ResolutionDiagnostic::YankedVersion {
471 dist: dist.clone(),
472 reason: Some(reason.to_string()),
473 });
474 }
475 }
476
477 let hashes = Self::get_hashes(
479 name,
480 index,
481 None,
482 &hashes_id,
483 version,
484 preferences,
485 in_memory,
486 );
487
488 let metadata = {
490 in_memory
491 .distributions()
492 .get(metadata_id)
493 .and_then(|response| {
494 if let MetadataResponse::Found(archive) = &*response {
495 Some(archive.metadata.clone())
496 } else {
497 None
498 }
499 })
500 };
501
502 (dist, hashes, metadata)
503 })
504 }
505
506 fn get_hashes(
509 name: &PackageName,
510 index: Option<&IndexUrl>,
511 url: Option<&VerbatimParsedUrl>,
512 metadata_id: &DistributionId,
513 version: &Version,
514 preferences: &Preferences,
515 in_memory: &InMemoryIndex,
516 ) -> HashDigests {
517 if let Some(digests) = preferences.match_hashes(name, version) {
519 if !digests.is_empty() {
520 return HashDigests::from(digests);
521 }
522 }
523
524 if let Some(metadata_response) = in_memory.distributions().get(metadata_id) {
526 if let MetadataResponse::Found(ref archive) = *metadata_response {
527 let mut digests = archive.hashes.clone();
528 digests.sort_unstable();
529 if !digests.is_empty() {
530 return digests;
531 }
532 }
533 }
534
535 if url.is_none() {
537 let implicit_response = in_memory.implicit().get(name);
539 let mut explicit_response = None;
540
541 let hashes = implicit_response
543 .as_ref()
544 .and_then(|response| {
545 if let VersionsResponse::Found(version_maps) = &**response {
546 Some(version_maps)
547 } else {
548 None
549 }
550 })
551 .into_iter()
552 .flatten()
553 .filter(|version_map| version_map.index() == index)
554 .find_map(|version_map| version_map.hashes(version))
555 .or_else(|| {
556 explicit_response = index
558 .and_then(|index| in_memory.explicit().get(&(name.clone(), index.clone())));
559 explicit_response
560 .as_ref()
561 .and_then(|response| {
562 if let VersionsResponse::Found(version_maps) = &**response {
563 Some(version_maps)
564 } else {
565 None
566 }
567 })
568 .into_iter()
569 .flatten()
570 .filter(|version_map| version_map.index() == index)
571 .find_map(|version_map| version_map.hashes(version))
572 });
573
574 if let Some(hashes) = hashes {
575 let mut digests = HashDigests::from(hashes);
576 digests.sort_unstable();
577 if !digests.is_empty() {
578 return digests;
579 }
580 }
581 }
582
583 HashDigests::empty()
584 }
585
586 fn dists(&self) -> impl Iterator<Item = &AnnotatedDist> {
588 self.graph
589 .node_indices()
590 .filter_map(move |index| match &self.graph[index] {
591 ResolutionGraphNode::Root => None,
592 ResolutionGraphNode::Dist(dist) => Some(dist),
593 })
594 }
595
596 pub(crate) fn base_dists(&self) -> impl Iterator<Item = (NodeIndex, &AnnotatedDist)> {
598 self.graph
599 .node_indices()
600 .filter_map(move |node_index| match &self.graph[node_index] {
601 ResolutionGraphNode::Root => None,
602 ResolutionGraphNode::Dist(dist) => dist.is_base().then_some((node_index, dist)),
603 })
604 }
605
606 pub fn len(&self) -> usize {
608 self.base_dists().count()
609 }
610
611 pub fn is_empty(&self) -> bool {
613 self.base_dists().next().is_none()
614 }
615
616 pub fn retain_allowed_distribution_hashes(&mut self, build_options: &BuildOptions) {
621 for node in self.graph.node_weights_mut() {
622 let ResolutionGraphNode::Dist(distribution) = node else {
623 continue;
624 };
625 let ResolvedDist::Installable { dist, .. } = &distribution.dist else {
626 continue;
627 };
628 let allowed_hashes = match dist.as_ref() {
629 Dist::Built(BuiltDist::Registry(dist))
630 if build_options.no_build_package(&distribution.name) =>
631 {
632 dist.wheels
633 .iter()
634 .flat_map(|wheel| wheel.file.hashes.iter())
635 .collect::<FxHashSet<_>>()
636 }
637 Dist::Source(SourceDist::Registry(source))
638 if build_options.no_binary_package(&distribution.name) =>
639 {
640 source.file.hashes.iter().collect::<FxHashSet<_>>()
641 }
642 _ => continue,
643 };
644 if allowed_hashes.is_empty() {
645 continue;
646 }
647
648 let hashes = distribution
649 .hashes
650 .iter()
651 .filter(|hash| allowed_hashes.contains(hash))
652 .cloned()
653 .collect::<Vec<_>>();
654 if !hashes.is_empty() {
655 distribution.hashes = HashDigests::from(hashes);
656 }
657 }
658 }
659
660 pub fn contains(&self, name: &PackageName) -> bool {
662 self.dists().any(|dist| dist.name() == name)
663 }
664
665 pub fn diagnostics(&self) -> &[ResolutionDiagnostic] {
667 &self.diagnostics
668 }
669
670 pub fn marker_tree(
691 &self,
692 index: &InMemoryIndex,
693 marker_env: &MarkerEnvironment,
694 ) -> Result<MarkerTree, Box<ParsedUrlError>> {
695 use uv_pep508::{
696 CanonicalMarkerValueString, CanonicalMarkerValueVersion, MarkerExpression,
697 MarkerOperator, MarkerTree,
698 };
699
700 #[derive(Debug, Eq, Hash, PartialEq)]
706 enum MarkerParam {
707 Version(CanonicalMarkerValueVersion),
708 String(CanonicalMarkerValueString),
709 }
710
711 fn add_marker_params_from_tree(marker_tree: MarkerTree, set: &mut IndexSet<MarkerParam>) {
713 match marker_tree.kind() {
714 MarkerTreeKind::True => {}
715 MarkerTreeKind::False => {}
716 MarkerTreeKind::Version(marker) => {
717 set.insert(MarkerParam::Version(marker.key()));
718 for (_, tree) in marker.edges() {
719 add_marker_params_from_tree(tree, set);
720 }
721 }
722 MarkerTreeKind::String(marker) => {
723 set.insert(MarkerParam::String(marker.key()));
724 for (_, tree) in marker.children() {
725 add_marker_params_from_tree(tree, set);
726 }
727 }
728 MarkerTreeKind::In(marker) => {
729 set.insert(MarkerParam::String(marker.key()));
730 for (_, tree) in marker.children() {
731 add_marker_params_from_tree(tree, set);
732 }
733 }
734 MarkerTreeKind::Contains(marker) => {
735 set.insert(MarkerParam::String(marker.key()));
736 for (_, tree) in marker.children() {
737 add_marker_params_from_tree(tree, set);
738 }
739 }
740 MarkerTreeKind::Extra(marker) => {
746 for (_, tree) in marker.children() {
747 add_marker_params_from_tree(tree, set);
748 }
749 }
750 MarkerTreeKind::List(marker) => {
751 for (_, tree) in marker.children() {
752 add_marker_params_from_tree(tree, set);
753 }
754 }
755 }
756 }
757
758 let mut seen_marker_values = IndexSet::default();
759 for i in self.graph.node_indices() {
760 let ResolutionGraphNode::Dist(dist) = &self.graph[i] else {
761 continue;
762 };
763 let metadata_id = dist.dist.distribution_id();
764 let res = index
765 .distributions()
766 .get(&metadata_id)
767 .expect("every package in resolution graph has metadata");
768 let MetadataResponse::Found(archive, ..) = &*res else {
769 panic!("Every package should have metadata: {metadata_id:?}")
770 };
771 for req in self.constraints.apply(self.overrides.apply_for(
772 &dist.name,
773 &dist.version,
774 archive.metadata.requires_dist.iter(),
775 )) {
776 add_marker_params_from_tree(req.marker, &mut seen_marker_values);
777 }
778 }
779
780 for direct_req in self
782 .constraints
783 .apply(self.overrides.apply(self.requirements.iter()))
784 {
785 add_marker_params_from_tree(direct_req.marker, &mut seen_marker_values);
786 }
787
788 let mut conjunction = MarkerTree::TRUE;
791 for marker_param in seen_marker_values {
792 let expr = match marker_param {
793 MarkerParam::Version(value_version) => {
794 let from_env = marker_env.get_version(value_version);
795 MarkerExpression::Version {
796 key: value_version.into(),
797 specifier: VersionSpecifier::equals_version(from_env.clone()),
798 }
799 }
800 MarkerParam::String(value_string) => {
801 let from_env = marker_env.get_string(value_string);
802 MarkerExpression::String {
803 key: value_string.into(),
804 operator: MarkerOperator::Equal,
805 value: from_env.into(),
806 }
807 }
808 };
809 conjunction = conjunction.and(MarkerTree::expression(expr));
810 }
811 Ok(conjunction)
812 }
813
814 fn find_conflicting_distributions(&self) -> Vec<ConflictingDistributionError> {
823 let mut name_to_markers: BTreeMap<&PackageName, Vec<(&Version, &UniversalMarker)>> =
824 BTreeMap::new();
825 for node in self.graph.node_weights() {
826 let annotated_dist = match node {
827 ResolutionGraphNode::Root => continue,
828 ResolutionGraphNode::Dist(annotated_dist) => annotated_dist,
829 };
830 name_to_markers
831 .entry(&annotated_dist.name)
832 .or_default()
833 .push((&annotated_dist.version, &annotated_dist.marker));
834 }
835 let mut dupes = vec![];
836 for (name, marker_trees) in name_to_markers {
837 for (i, (version1, marker1)) in marker_trees.iter().enumerate() {
838 for (version2, marker2) in &marker_trees[i + 1..] {
839 if version1 == version2 {
840 continue;
841 }
842 if !marker1.is_disjoint(**marker2) {
843 dupes.push(ConflictingDistributionError {
844 name: name.clone(),
845 version1: (*version1).clone(),
846 version2: (*version2).clone(),
847 marker1: **marker1,
848 marker2: **marker2,
849 });
850 }
851 }
852 }
853 }
854 dupes
855 }
856}
857
858#[derive(Debug)]
865pub struct ConflictingDistributionError {
866 name: PackageName,
867 version1: Version,
868 version2: Version,
869 marker1: UniversalMarker,
870 marker2: UniversalMarker,
871}
872
873impl std::error::Error for ConflictingDistributionError {}
874
875impl Display for ConflictingDistributionError {
876 fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
877 let Self {
878 ref name,
879 ref version1,
880 ref version2,
881 ref marker1,
882 ref marker2,
883 } = *self;
884 write!(
885 f,
886 "found conflicting versions for package `{name}`:
887 `{marker1:?}` (for version `{version1}`) is not disjoint with \
888 `{marker2:?}` (for version `{version2}`)",
889 )
890 }
891}
892
893impl From<ResolverOutput> for uv_distribution_types::Resolution {
905 fn from(output: ResolverOutput) -> Self {
906 let ResolverOutput {
907 graph,
908 diagnostics,
909 fork_markers,
910 ..
911 } = output;
912
913 assert!(
914 fork_markers.is_empty(),
915 "universal resolutions are not supported"
916 );
917
918 let mut transformed = Graph::with_capacity(graph.node_count(), graph.edge_count());
919 let mut inverse = FxHashMap::with_capacity_and_hasher(graph.node_count(), FxBuildHasher);
920
921 let root = transformed.add_node(Node::Root);
923
924 for index in graph.node_indices() {
926 let ResolutionGraphNode::Dist(dist) = &graph[index] else {
927 continue;
928 };
929 if dist.is_base() {
930 inverse.insert(
931 &dist.name,
932 transformed.add_node(Node::Dist {
933 dist: dist.dist.clone(),
934 hashes: dist.hashes.clone(),
935 install: true,
936 }),
937 );
938 }
939 }
940
941 for edge in graph.edge_indices() {
943 let (source, target) = graph.edge_endpoints(edge).unwrap();
944
945 match (&graph[source], &graph[target]) {
946 (ResolutionGraphNode::Root, ResolutionGraphNode::Dist(target_dist)) => {
947 let target = inverse[&target_dist.name()];
948 transformed.update_edge(root, target, Edge::Prod);
949 }
950 (
951 ResolutionGraphNode::Dist(source_dist),
952 ResolutionGraphNode::Dist(target_dist),
953 ) => {
954 let source = inverse[&source_dist.name()];
955 let target = inverse[&target_dist.name()];
956
957 let edge = if let Some(extra) = source_dist.extra.as_ref() {
958 Edge::Optional(extra.clone())
959 } else if let Some(group) = source_dist.group.as_ref() {
960 Edge::Dev(group.clone())
961 } else {
962 Edge::Prod
963 };
964
965 transformed.add_edge(source, target, edge);
966 }
967 _ => {
968 unreachable!("root should not contain incoming edges");
969 }
970 }
971 }
972
973 Self::new(transformed).with_diagnostics(diagnostics)
974 }
975}
976
977fn report_missing_lower_bounds(
979 graph: &Graph<ResolutionGraphNode, UniversalMarker>,
980 diagnostics: &mut Vec<ResolutionDiagnostic>,
981 constraints: &Constraints,
982 overrides: &Overrides,
983) {
984 for node_index in graph.node_indices() {
985 let ResolutionGraphNode::Dist(dist) = graph.node_weight(node_index).unwrap() else {
986 continue;
988 };
989 if !has_lower_bound(node_index, dist.name(), graph, constraints, overrides) {
990 diagnostics.push(ResolutionDiagnostic::MissingLowerBound {
991 package_name: dist.name().clone(),
992 });
993 }
994 }
995}
996
997fn has_lower_bound(
999 node_index: NodeIndex,
1000 package_name: &PackageName,
1001 graph: &Graph<ResolutionGraphNode, UniversalMarker>,
1002 constraints: &Constraints,
1003 overrides: &Overrides,
1004) -> bool {
1005 for neighbor_index in graph.neighbors_directed(node_index, Direction::Incoming) {
1006 let neighbor_dist = match graph.node_weight(neighbor_index).unwrap() {
1007 ResolutionGraphNode::Root => {
1008 return true;
1011 }
1012 ResolutionGraphNode::Dist(neighbor_dist) => neighbor_dist,
1013 };
1014
1015 if neighbor_dist.name() == package_name {
1016 return true;
1018 }
1019
1020 let Some(metadata) = neighbor_dist.metadata.as_ref() else {
1021 return true;
1023 };
1024
1025 for requirement in overrides
1028 .apply_for(
1029 neighbor_dist.name(),
1030 &neighbor_dist.version,
1031 metadata.requires_dist.iter(),
1032 )
1033 .chain(overrides.apply(metadata.dependency_groups.values().flatten()))
1034 .chain(constraints.requirements().map(Cow::Borrowed))
1036 {
1037 if requirement.name != *package_name {
1038 continue;
1039 }
1040 let Some(specifiers) = requirement.source.version_specifiers() else {
1041 return true;
1043 };
1044 if specifiers.iter().any(VersionSpecifier::has_lower_bound) {
1045 return true;
1046 }
1047 }
1048 }
1049 false
1050}