Skip to main content

uv_resolver/resolution/
output.rs

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::{Constraints, Overrides};
14use uv_distribution::Metadata;
15use uv_distribution_types::{
16    Dist, DistributionId, Edge, Identifier, IndexUrl, Name, Node, Requirement, RequiresPython,
17    ResolutionDiagnostic, ResolvedDist,
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::{
34    InMemoryIndex, MetadataResponse, Options, PythonRequirement, ResolveError, VersionsResponse,
35};
36
37/// The output of a successful resolution.
38///
39/// Includes a complete resolution graph in which every node represents a pinned package and every
40/// edge represents a dependency between two pinned packages.
41#[derive(Debug)]
42pub struct ResolverOutput {
43    /// The underlying graph.
44    pub(crate) graph: Graph<ResolutionGraphNode, UniversalMarker, Directed>,
45    /// The range of supported Python versions.
46    pub(crate) requires_python: RequiresPython,
47    /// If the resolution had non-identical forks, store the forks in the lockfile so we can
48    /// recreate them in subsequent resolutions.
49    pub(crate) fork_markers: Vec<UniversalMarker>,
50    /// Any diagnostics that were encountered while building the graph.
51    pub(crate) diagnostics: Vec<ResolutionDiagnostic>,
52    /// The requirements that were used to build the graph.
53    pub(crate) requirements: Vec<Requirement>,
54    /// The constraints that were used to build the graph.
55    pub(crate) constraints: Constraints,
56    /// The overrides that were used to build the graph.
57    pub(crate) overrides: Overrides,
58    /// The options that were used to build the graph.
59    pub(crate) options: Options,
60}
61
62#[derive(Debug, Clone)]
63#[expect(clippy::large_enum_variant)]
64pub(crate) enum ResolutionGraphNode {
65    Root,
66    Dist(AnnotatedDist),
67}
68
69impl ResolutionGraphNode {
70    pub(crate) fn marker(&self) -> &UniversalMarker {
71        match self {
72            Self::Root => &UniversalMarker::TRUE,
73            Self::Dist(dist) => &dist.marker,
74        }
75    }
76
77    pub(crate) fn package_extra_names(&self) -> Option<(&PackageName, &ExtraName)> {
78        match self {
79            Self::Root => None,
80            Self::Dist(dist) => {
81                let extra = dist.extra.as_ref()?;
82                Some((&dist.name, extra))
83            }
84        }
85    }
86
87    pub(crate) fn package_group_names(&self) -> Option<(&PackageName, &GroupName)> {
88        match self {
89            Self::Root => None,
90            Self::Dist(dist) => {
91                let group = dist.group.as_ref()?;
92                Some((&dist.name, group))
93            }
94        }
95    }
96
97    pub(crate) fn package_name(&self) -> Option<&PackageName> {
98        match self {
99            Self::Root => None,
100            Self::Dist(dist) => Some(&dist.name),
101        }
102    }
103}
104
105impl Display for ResolutionGraphNode {
106    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
107        match self {
108            Self::Root => f.write_str("root"),
109            Self::Dist(dist) => Display::fmt(dist, f),
110        }
111    }
112}
113
114#[derive(Debug, Eq, PartialEq, Hash)]
115struct PackageRef<'a> {
116    package_name: &'a PackageName,
117    version: &'a Version,
118    url: Option<&'a VerbatimParsedUrl>,
119    index: Option<&'a IndexUrl>,
120    extra: Option<&'a ExtraName>,
121    group: Option<&'a GroupName>,
122}
123
124impl ResolverOutput {
125    /// Create a new [`ResolverOutput`] from the resolved PubGrub state.
126    pub(crate) fn from_state(
127        resolutions: &[Resolution],
128        requirements: &[Requirement],
129        constraints: &Constraints,
130        overrides: &Overrides,
131        preferences: &Preferences,
132        index: &InMemoryIndex,
133        git: &GitResolver,
134        python: &PythonRequirement,
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        // Add the root node.
147        let root_index = graph.add_node(ResolutionGraphNode::Root);
148
149        let mut seen = FxHashSet::default();
150        for resolution in resolutions {
151            // Add every package to the graph.
152            for (package, version) in &resolution.nodes {
153                if !seen.insert((package, version)) {
154                    // Insert each node only once.
155                    continue;
156                }
157                Self::add_version(
158                    &mut graph,
159                    &mut inverse,
160                    &mut diagnostics,
161                    preferences,
162                    &resolution.pins,
163                    index,
164                    git,
165                    package,
166                    version,
167                )?;
168            }
169        }
170
171        let mut seen = FxHashSet::default();
172        for resolution in resolutions {
173            let marker = resolution.env.try_universal_markers().unwrap_or_default();
174
175            // Add every edge to the graph, propagating the marker for the current fork, if
176            // necessary.
177            for edge in &resolution.edges {
178                if !seen.insert((edge, marker)) {
179                    // Insert each node only once.
180                    continue;
181                }
182
183                Self::add_edge(&mut graph, &mut inverse, root_index, edge, marker);
184            }
185        }
186
187        // Extract the `Requires-Python` range, if provided.
188        let requires_python = python.target().clone();
189
190        let fork_markers: Vec<UniversalMarker> = if let [resolution] = resolutions {
191            // In the case of a singleton marker, we only include it if it's not
192            // always true. Otherwise, we keep our `fork_markers` empty as there
193            // are no forks.
194            resolution
195                .env
196                .try_universal_markers()
197                .into_iter()
198                .filter(|marker| !marker.is_true())
199                .collect()
200        } else {
201            resolutions
202                .iter()
203                .map(|resolution| resolution.env.try_universal_markers().unwrap_or_default())
204                .collect()
205        };
206
207        // Compute and apply the marker reachability.
208        let mut reachability = marker_reachability(&graph, &fork_markers);
209
210        // Apply the reachability to the graph and imbibe world
211        // knowledge about conflicts.
212        let conflict_marker = ConflictMarker::from_conflicts(conflicts);
213        for index in graph.node_indices() {
214            if let ResolutionGraphNode::Dist(dist) = &mut graph[index] {
215                dist.marker = reachability.remove(&index).unwrap_or_default();
216                dist.marker.imbibe(conflict_marker);
217            }
218        }
219        for weight in graph.edge_weights_mut() {
220            weight.imbibe(conflict_marker);
221        }
222
223        simplify_conflict_markers(conflicts, &mut graph);
224
225        // Discard any unreachable nodes.
226        graph.retain_nodes(|graph, node| !graph[node].marker().is_false());
227
228        if matches!(resolution_strategy, ResolutionStrategy::Lowest) {
229            report_missing_lower_bounds(&graph, &mut diagnostics, constraints, overrides);
230        }
231
232        let output = Self {
233            graph,
234            requires_python,
235            diagnostics,
236            requirements: requirements.to_vec(),
237            constraints: constraints.clone(),
238            overrides: overrides.clone(),
239            options,
240            fork_markers,
241        };
242
243        // We only do conflicting distribution detection when no
244        // conflicting groups have been specified. The reason here
245        // is that when there are conflicting groups, then from the
246        // perspective of marker expressions only, it may look like
247        // one can install different versions of the same package for
248        // the same marker environment. However, the thing preventing
249        // this is that the only way this should be possible is if
250        // one tries to install two or more conflicting extras at
251        // the same time. At which point, uv will report an error,
252        // thereby sidestepping the possibility of installing different
253        // versions of the same package into the same virtualenv. ---AG
254        //
255        // FIXME: When `UniversalMarker` supports extras/groups, we can
256        // re-enable this.
257        if conflicts.is_empty() {
258            #[allow(unused_mut, reason = "Used in debug_assertions below")]
259            let mut conflicting = output.find_conflicting_distributions();
260            if !conflicting.is_empty() {
261                tracing::warn!(
262                    "found {} conflicting distributions in resolution, \
263                 please report this as a bug at \
264                 https://github.com/astral-sh/uv/issues/new",
265                    conflicting.len()
266                );
267            }
268            // When testing, we materialize any conflicting distributions as an
269            // error to ensure any relevant tests fail. Otherwise, we just leave
270            // it at the warning message above. The reason for not returning an
271            // error "in production" is that an incorrect resolution may only be
272            // incorrect in certain marker environments, but fine in most others.
273            // Returning an error in that case would make `uv` unusable whenever
274            // the bug occurs, but letting it through means `uv` *could* still be
275            // usable.
276            #[cfg(debug_assertions)]
277            if let Some(err) = conflicting.pop() {
278                return Err(ResolveError::ConflictingDistribution(err));
279            }
280        }
281        Ok(output)
282    }
283
284    fn add_edge(
285        graph: &mut Graph<ResolutionGraphNode, UniversalMarker>,
286        inverse: &mut FxHashMap<PackageRef<'_>, NodeIndex>,
287        root_index: NodeIndex,
288        edge: &ResolutionDependencyEdge,
289        marker: UniversalMarker,
290    ) {
291        let from_index = edge.from.as_ref().map_or(root_index, |from| {
292            inverse[&PackageRef {
293                package_name: from,
294                version: &edge.from_version,
295                url: edge.from_url.as_ref(),
296                index: edge.from_index.as_ref(),
297                extra: edge.from_extra.as_ref(),
298                group: edge.from_group.as_ref(),
299            }]
300        });
301        let to_index = inverse[&PackageRef {
302            package_name: &edge.to,
303            version: &edge.to_version,
304            url: edge.to_url.as_ref(),
305            index: edge.to_index.as_ref(),
306            extra: edge.to_extra.as_ref(),
307            group: edge.to_group.as_ref(),
308        }];
309
310        let edge_marker = {
311            let mut edge_marker = edge.universal_marker();
312            edge_marker.and(marker);
313            edge_marker
314        };
315
316        if let Some(weight) = graph
317            .find_edge(from_index, to_index)
318            .and_then(|edge| graph.edge_weight_mut(edge))
319        {
320            // If either the existing marker or new marker is `true`, then the dependency is
321            // included unconditionally, and so the combined marker is `true`.
322            weight.or(edge_marker);
323        } else {
324            graph.update_edge(from_index, to_index, edge_marker);
325        }
326    }
327
328    fn add_version<'a>(
329        graph: &mut Graph<ResolutionGraphNode, UniversalMarker>,
330        inverse: &mut FxHashMap<PackageRef<'a>, NodeIndex>,
331        diagnostics: &mut Vec<ResolutionDiagnostic>,
332        preferences: &Preferences,
333        pins: &FilePins,
334        in_memory: &InMemoryIndex,
335        git: &GitResolver,
336        package: &'a ResolutionPackage,
337        version: &'a Version,
338    ) -> Result<(), ResolveError> {
339        let ResolutionPackage {
340            name,
341            extra,
342            dev: group,
343            url,
344            index,
345        } = &package;
346        // Map the package to a distribution.
347        let (dist, hashes, metadata) = Self::parse_dist(
348            name,
349            index.as_ref(),
350            url.as_ref(),
351            version,
352            pins,
353            diagnostics,
354            preferences,
355            in_memory,
356            git,
357        )?;
358
359        if let Some(metadata) = metadata.as_ref() {
360            // Validate the extra.
361            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            // Validate the development dependency group.
371            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        // Add the distribution to the graph.
382        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        in_memory: &InMemoryIndex,
415        git: &GitResolver,
416    ) -> Result<(ResolvedDist, HashDigests, Option<Metadata>), ResolveError> {
417        Ok(if let Some(url) = url {
418            // Create the locked distribution and recover the metadata using the original URL that
419            // was requested during resolution.
420            let dist = Dist::from_url(name.clone(), url_to_precise(url.clone(), git))?;
421            let metadata_id = Dist::from_url(name.clone(), url.clone())?.distribution_id();
422
423            // Extract the hashes.
424            let hashes = Self::get_hashes(
425                name,
426                index,
427                Some(url),
428                &metadata_id,
429                version,
430                preferences,
431                in_memory,
432            );
433
434            // Extract the metadata.
435            let metadata = {
436                let response = in_memory
437                    .distributions()
438                    .get(&metadata_id)
439                    .unwrap_or_else(|| {
440                        panic!("Every URL distribution should have metadata: {metadata_id:?}")
441                    });
442
443                let MetadataResponse::Found(archive) = &*response else {
444                    panic!("Every URL distribution should have metadata: {metadata_id:?}")
445                };
446
447                archive.metadata.clone()
448            };
449
450            (
451                ResolvedDist::Installable {
452                    dist: Arc::new(dist),
453                    version: Some(version.clone()),
454                },
455                hashes,
456                Some(metadata),
457            )
458        } else {
459            let (dist, metadata_id) = pins
460                .dist_and_id(name, version)
461                .expect("Every package should be pinned");
462            let dist = dist.clone();
463            let hashes_id = dist.distribution_id();
464
465            // Track yanks for any registry distributions.
466            match dist.yanked() {
467                None | Some(Yanked::Bool(false)) => {}
468                Some(Yanked::Bool(true)) => {
469                    diagnostics.push(ResolutionDiagnostic::YankedVersion {
470                        dist: dist.clone(),
471                        reason: None,
472                    });
473                }
474                Some(Yanked::Reason(reason)) => {
475                    diagnostics.push(ResolutionDiagnostic::YankedVersion {
476                        dist: dist.clone(),
477                        reason: Some(reason.to_string()),
478                    });
479                }
480            }
481
482            // Extract the hashes.
483            let hashes = Self::get_hashes(
484                name,
485                index,
486                None,
487                &hashes_id,
488                version,
489                preferences,
490                in_memory,
491            );
492
493            // Extract the metadata.
494            let metadata = {
495                in_memory
496                    .distributions()
497                    .get(metadata_id)
498                    .and_then(|response| {
499                        if let MetadataResponse::Found(archive) = &*response {
500                            Some(archive.metadata.clone())
501                        } else {
502                            None
503                        }
504                    })
505            };
506
507            (dist, hashes, metadata)
508        })
509    }
510
511    /// Identify the hashes for a concrete distribution, preserving any hashes that were provided
512    /// by the lockfile.
513    fn get_hashes(
514        name: &PackageName,
515        index: Option<&IndexUrl>,
516        url: Option<&VerbatimParsedUrl>,
517        metadata_id: &DistributionId,
518        version: &Version,
519        preferences: &Preferences,
520        in_memory: &InMemoryIndex,
521    ) -> HashDigests {
522        // 1. Look for hashes from the lockfile.
523        if let Some(digests) = preferences.match_hashes(name, version) {
524            if !digests.is_empty() {
525                return HashDigests::from(digests);
526            }
527        }
528
529        // 2. Look for hashes for the distribution (i.e., the specific wheel or source distribution).
530        if let Some(metadata_response) = in_memory.distributions().get(metadata_id) {
531            if let MetadataResponse::Found(ref archive) = *metadata_response {
532                let mut digests = archive.hashes.clone();
533                digests.sort_unstable();
534                if !digests.is_empty() {
535                    return digests;
536                }
537            }
538        }
539
540        // 3. Look for hashes from the registry, which are served at the package level.
541        if url.is_none() {
542            // Query the implicit and explicit indexes (lazily) for the hashes.
543            let implicit_response = in_memory.implicit().get(name);
544            let mut explicit_response = None;
545
546            // Search in the implicit indexes.
547            let hashes = implicit_response
548                .as_ref()
549                .and_then(|response| {
550                    if let VersionsResponse::Found(version_maps) = &**response {
551                        Some(version_maps)
552                    } else {
553                        None
554                    }
555                })
556                .into_iter()
557                .flatten()
558                .filter(|version_map| version_map.index() == index)
559                .find_map(|version_map| version_map.hashes(version))
560                .or_else(|| {
561                    // Search in the explicit indexes.
562                    explicit_response = index
563                        .and_then(|index| in_memory.explicit().get(&(name.clone(), index.clone())));
564                    explicit_response
565                        .as_ref()
566                        .and_then(|response| {
567                            if let VersionsResponse::Found(version_maps) = &**response {
568                                Some(version_maps)
569                            } else {
570                                None
571                            }
572                        })
573                        .into_iter()
574                        .flatten()
575                        .filter(|version_map| version_map.index() == index)
576                        .find_map(|version_map| version_map.hashes(version))
577                });
578
579            if let Some(hashes) = hashes {
580                let mut digests = HashDigests::from(hashes);
581                digests.sort_unstable();
582                if !digests.is_empty() {
583                    return digests;
584                }
585            }
586        }
587
588        HashDigests::empty()
589    }
590
591    /// Returns an iterator over the distinct packages in the graph.
592    fn dists(&self) -> impl Iterator<Item = &AnnotatedDist> {
593        self.graph
594            .node_indices()
595            .filter_map(move |index| match &self.graph[index] {
596                ResolutionGraphNode::Root => None,
597                ResolutionGraphNode::Dist(dist) => Some(dist),
598            })
599    }
600
601    /// Returns an iterator over the base distributions in the graph.
602    pub(crate) fn base_dists(&self) -> impl Iterator<Item = (NodeIndex, &AnnotatedDist)> {
603        self.graph
604            .node_indices()
605            .filter_map(move |node_index| match &self.graph[node_index] {
606                ResolutionGraphNode::Root => None,
607                ResolutionGraphNode::Dist(dist) => dist.is_base().then_some((node_index, dist)),
608            })
609    }
610
611    /// Return the number of distinct packages in the graph.
612    pub fn len(&self) -> usize {
613        self.base_dists().count()
614    }
615
616    /// Return `true` if there are no packages in the graph.
617    pub fn is_empty(&self) -> bool {
618        self.base_dists().next().is_none()
619    }
620
621    /// Returns `true` if the graph contains the given package.
622    pub fn contains(&self, name: &PackageName) -> bool {
623        self.dists().any(|dist| dist.name() == name)
624    }
625
626    /// Return the [`ResolutionDiagnostic`]s that were encountered while building the graph.
627    pub fn diagnostics(&self) -> &[ResolutionDiagnostic] {
628        &self.diagnostics
629    }
630
631    /// Return the marker tree specific to this resolution.
632    ///
633    /// This accepts an in-memory-index and marker environment, all
634    /// of which should be the same values given to the resolver that produced
635    /// this graph.
636    ///
637    /// The marker tree returned corresponds to an expression that, when true,
638    /// this resolution is guaranteed to be correct. Note though that it's
639    /// possible for resolution to be correct even if the returned marker
640    /// expression is false.
641    ///
642    /// For example, if the root package has a dependency `foo; sys_platform ==
643    /// "macos"` and resolution was performed on Linux, then the marker tree
644    /// returned will contain a `sys_platform == "linux"` expression. This
645    /// means that whenever the marker expression evaluates to true (i.e., the
646    /// current platform is Linux), then the resolution here is correct. But
647    /// it is possible that the resolution is also correct on other platforms
648    /// that aren't macOS, such as Windows. (It is unclear at time of writing
649    /// whether this is fundamentally impossible to compute, or just impossible
650    /// to compute in some cases.)
651    pub fn marker_tree(
652        &self,
653        index: &InMemoryIndex,
654        marker_env: &MarkerEnvironment,
655    ) -> Result<MarkerTree, Box<ParsedUrlError>> {
656        use uv_pep508::{
657            CanonicalMarkerValueString, CanonicalMarkerValueVersion, MarkerExpression,
658            MarkerOperator, MarkerTree,
659        };
660
661        /// A subset of the possible marker values.
662        ///
663        /// We only track the marker parameters that are referenced in a marker
664        /// expression. We'll use references to the parameter later to generate
665        /// values based on the current marker environment.
666        #[derive(Debug, Eq, Hash, PartialEq)]
667        enum MarkerParam {
668            Version(CanonicalMarkerValueVersion),
669            String(CanonicalMarkerValueString),
670        }
671
672        /// Add all marker parameters from the given tree to the given set.
673        fn add_marker_params_from_tree(marker_tree: MarkerTree, set: &mut IndexSet<MarkerParam>) {
674            match marker_tree.kind() {
675                MarkerTreeKind::True => {}
676                MarkerTreeKind::False => {}
677                MarkerTreeKind::Version(marker) => {
678                    set.insert(MarkerParam::Version(marker.key()));
679                    for (_, tree) in marker.edges() {
680                        add_marker_params_from_tree(tree, set);
681                    }
682                }
683                MarkerTreeKind::String(marker) => {
684                    set.insert(MarkerParam::String(marker.key()));
685                    for (_, tree) in marker.children() {
686                        add_marker_params_from_tree(tree, set);
687                    }
688                }
689                MarkerTreeKind::In(marker) => {
690                    set.insert(MarkerParam::String(marker.key()));
691                    for (_, tree) in marker.children() {
692                        add_marker_params_from_tree(tree, set);
693                    }
694                }
695                MarkerTreeKind::Contains(marker) => {
696                    set.insert(MarkerParam::String(marker.key()));
697                    for (_, tree) in marker.children() {
698                        add_marker_params_from_tree(tree, set);
699                    }
700                }
701                // We specifically don't care about these for the
702                // purposes of generating a marker string for a lock
703                // file. Quoted strings are marker values given by the
704                // user. We don't track those here, since we're only
705                // interested in which markers are used.
706                MarkerTreeKind::Extra(marker) => {
707                    for (_, tree) in marker.children() {
708                        add_marker_params_from_tree(tree, set);
709                    }
710                }
711                MarkerTreeKind::List(marker) => {
712                    for (_, tree) in marker.children() {
713                        add_marker_params_from_tree(tree, set);
714                    }
715                }
716            }
717        }
718
719        let mut seen_marker_values = IndexSet::default();
720        for i in self.graph.node_indices() {
721            let ResolutionGraphNode::Dist(dist) = &self.graph[i] else {
722                continue;
723            };
724            let metadata_id = dist.dist.distribution_id();
725            let res = index
726                .distributions()
727                .get(&metadata_id)
728                .expect("every package in resolution graph has metadata");
729            let MetadataResponse::Found(archive, ..) = &*res else {
730                panic!("Every package should have metadata: {metadata_id:?}")
731            };
732            for req in self.constraints.apply(self.overrides.apply_for(
733                &dist.name,
734                &dist.version,
735                archive.metadata.requires_dist.iter(),
736            )) {
737                add_marker_params_from_tree(req.marker, &mut seen_marker_values);
738            }
739        }
740
741        // Ensure that we consider markers from direct dependencies.
742        for direct_req in self
743            .constraints
744            .apply(self.overrides.apply(self.requirements.iter()))
745        {
746            add_marker_params_from_tree(direct_req.marker, &mut seen_marker_values);
747        }
748
749        // Generate the final marker expression as a conjunction of
750        // strict equality terms.
751        let mut conjunction = MarkerTree::TRUE;
752        for marker_param in seen_marker_values {
753            let expr = match marker_param {
754                MarkerParam::Version(value_version) => {
755                    let from_env = marker_env.get_version(value_version);
756                    MarkerExpression::Version {
757                        key: value_version.into(),
758                        specifier: VersionSpecifier::equals_version(from_env.clone()),
759                    }
760                }
761                MarkerParam::String(value_string) => {
762                    let from_env = marker_env.get_string(value_string);
763                    MarkerExpression::String {
764                        key: value_string.into(),
765                        operator: MarkerOperator::Equal,
766                        value: from_env.into(),
767                    }
768                }
769            };
770            conjunction.and(MarkerTree::expression(expr));
771        }
772        Ok(conjunction)
773    }
774
775    /// Returns a sequence of conflicting distribution errors from this
776    /// resolution.
777    ///
778    /// Correct resolutions always return an empty sequence. A non-empty
779    /// sequence implies there is a package with two distinct versions in the
780    /// same marker environment in this resolution. This in turn implies that
781    /// an installation in that marker environment could wind up trying to
782    /// install different versions of the same package, which is not allowed.
783    fn find_conflicting_distributions(&self) -> Vec<ConflictingDistributionError> {
784        let mut name_to_markers: BTreeMap<&PackageName, Vec<(&Version, &UniversalMarker)>> =
785            BTreeMap::new();
786        for node in self.graph.node_weights() {
787            let annotated_dist = match node {
788                ResolutionGraphNode::Root => continue,
789                ResolutionGraphNode::Dist(annotated_dist) => annotated_dist,
790            };
791            name_to_markers
792                .entry(&annotated_dist.name)
793                .or_default()
794                .push((&annotated_dist.version, &annotated_dist.marker));
795        }
796        let mut dupes = vec![];
797        for (name, marker_trees) in name_to_markers {
798            for (i, (version1, marker1)) in marker_trees.iter().enumerate() {
799                for (version2, marker2) in &marker_trees[i + 1..] {
800                    if version1 == version2 {
801                        continue;
802                    }
803                    if !marker1.is_disjoint(**marker2) {
804                        dupes.push(ConflictingDistributionError {
805                            name: name.clone(),
806                            version1: (*version1).clone(),
807                            version2: (*version2).clone(),
808                            marker1: **marker1,
809                            marker2: **marker2,
810                        });
811                    }
812                }
813            }
814        }
815        dupes
816    }
817}
818
819/// An error that occurs for conflicting versions of the same package.
820///
821/// Specifically, this occurs when two distributions with the same package
822/// name are found with distinct versions in at least one possible marker
823/// environment. This error reflects an error that could occur when installing
824/// the corresponding resolution into that marker environment.
825#[derive(Debug)]
826pub struct ConflictingDistributionError {
827    name: PackageName,
828    version1: Version,
829    version2: Version,
830    marker1: UniversalMarker,
831    marker2: UniversalMarker,
832}
833
834impl std::error::Error for ConflictingDistributionError {}
835
836impl Display for ConflictingDistributionError {
837    fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
838        let Self {
839            ref name,
840            ref version1,
841            ref version2,
842            ref marker1,
843            ref marker2,
844        } = *self;
845        write!(
846            f,
847            "found conflicting versions for package `{name}`:
848             `{marker1:?}` (for version `{version1}`) is not disjoint with \
849             `{marker2:?}` (for version `{version2}`)",
850        )
851    }
852}
853
854/// Convert a [`ResolverOutput`] into a [`uv_distribution_types::Resolution`].
855///
856/// This involves converting [`ResolutionGraphNode`]s into [`Node`]s, which in turn involves
857/// dropping any extras and dependency groups from the graph nodes. Instead, each package is
858/// collapsed into a single node, with  extras and dependency groups annotating the _edges_, rather
859/// than being represented as separate nodes. This is a more natural representation, but a further
860/// departure from the PubGrub model.
861///
862/// For simplicity, this transformation makes the assumption that the resolution only applies to a
863/// subset of markers, i.e., it shouldn't be called on universal resolutions, and expects only a
864/// single version of each package to be present in the graph.
865impl From<ResolverOutput> for uv_distribution_types::Resolution {
866    fn from(output: ResolverOutput) -> Self {
867        let ResolverOutput {
868            graph,
869            diagnostics,
870            fork_markers,
871            ..
872        } = output;
873
874        assert!(
875            fork_markers.is_empty(),
876            "universal resolutions are not supported"
877        );
878
879        let mut transformed = Graph::with_capacity(graph.node_count(), graph.edge_count());
880        let mut inverse = FxHashMap::with_capacity_and_hasher(graph.node_count(), FxBuildHasher);
881
882        // Create the root node.
883        let root = transformed.add_node(Node::Root);
884
885        // Re-add the nodes to the reduced graph.
886        for index in graph.node_indices() {
887            let ResolutionGraphNode::Dist(dist) = &graph[index] else {
888                continue;
889            };
890            if dist.is_base() {
891                inverse.insert(
892                    &dist.name,
893                    transformed.add_node(Node::Dist {
894                        dist: dist.dist.clone(),
895                        hashes: dist.hashes.clone(),
896                        install: true,
897                    }),
898                );
899            }
900        }
901
902        // Re-add the edges to the reduced graph.
903        for edge in graph.edge_indices() {
904            let (source, target) = graph.edge_endpoints(edge).unwrap();
905
906            match (&graph[source], &graph[target]) {
907                (ResolutionGraphNode::Root, ResolutionGraphNode::Dist(target_dist)) => {
908                    let target = inverse[&target_dist.name()];
909                    transformed.update_edge(root, target, Edge::Prod);
910                }
911                (
912                    ResolutionGraphNode::Dist(source_dist),
913                    ResolutionGraphNode::Dist(target_dist),
914                ) => {
915                    let source = inverse[&source_dist.name()];
916                    let target = inverse[&target_dist.name()];
917
918                    let edge = if let Some(extra) = source_dist.extra.as_ref() {
919                        Edge::Optional(extra.clone())
920                    } else if let Some(group) = source_dist.group.as_ref() {
921                        Edge::Dev(group.clone())
922                    } else {
923                        Edge::Prod
924                    };
925
926                    transformed.add_edge(source, target, edge);
927                }
928                _ => {
929                    unreachable!("root should not contain incoming edges");
930                }
931            }
932        }
933
934        Self::new(transformed).with_diagnostics(diagnostics)
935    }
936}
937
938/// Find any packages that don't have any lower bound on them when in resolution-lowest mode.
939fn report_missing_lower_bounds(
940    graph: &Graph<ResolutionGraphNode, UniversalMarker>,
941    diagnostics: &mut Vec<ResolutionDiagnostic>,
942    constraints: &Constraints,
943    overrides: &Overrides,
944) {
945    for node_index in graph.node_indices() {
946        let ResolutionGraphNode::Dist(dist) = graph.node_weight(node_index).unwrap() else {
947            // Ignore the root package.
948            continue;
949        };
950        if !has_lower_bound(node_index, dist.name(), graph, constraints, overrides) {
951            diagnostics.push(ResolutionDiagnostic::MissingLowerBound {
952                package_name: dist.name().clone(),
953            });
954        }
955    }
956}
957
958/// Whether the given package has a lower version bound by another package.
959fn has_lower_bound(
960    node_index: NodeIndex,
961    package_name: &PackageName,
962    graph: &Graph<ResolutionGraphNode, UniversalMarker>,
963    constraints: &Constraints,
964    overrides: &Overrides,
965) -> bool {
966    for neighbor_index in graph.neighbors_directed(node_index, Direction::Incoming) {
967        let neighbor_dist = match graph.node_weight(neighbor_index).unwrap() {
968            ResolutionGraphNode::Root => {
969                // We already handled direct dependencies with a missing constraint
970                // separately.
971                return true;
972            }
973            ResolutionGraphNode::Dist(neighbor_dist) => neighbor_dist,
974        };
975
976        if neighbor_dist.name() == package_name {
977            // Only warn for real packages, not for virtual packages such as dev nodes.
978            return true;
979        }
980
981        let Some(metadata) = neighbor_dist.metadata.as_ref() else {
982            // We can't check for lower bounds if we lack metadata.
983            return true;
984        };
985
986        // Get all individual specifier for the current package and check if any has a lower
987        // bound.
988        for requirement in overrides
989            .apply_for(
990                neighbor_dist.name(),
991                &neighbor_dist.version,
992                metadata.requires_dist.iter(),
993            )
994            .chain(overrides.apply(metadata.dependency_groups.values().flatten()))
995            // Constraints are missing from the graph.
996            .chain(constraints.requirements().map(Cow::Borrowed))
997        {
998            if requirement.name != *package_name {
999                continue;
1000            }
1001            let Some(specifiers) = requirement.source.version_specifiers() else {
1002                // URL requirements are a bound.
1003                return true;
1004            };
1005            if specifiers.iter().any(VersionSpecifier::has_lower_bound) {
1006                return true;
1007            }
1008        }
1009    }
1010    false
1011}