uv_resolver/resolution/
output.rs

1use std::collections::BTreeMap;
2use std::fmt::{Display, Formatter};
3use std::sync::Arc;
4
5use indexmap::IndexSet;
6use petgraph::{
7    Directed, Direction,
8    graph::{Graph, NodeIndex},
9};
10use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};
11
12use uv_configuration::{Constraints, Overrides};
13use uv_distribution::Metadata;
14use uv_distribution_types::{
15    Dist, DistributionMetadata, Edge, IndexUrl, Name, Node, Requirement, RequiresPython,
16    ResolutionDiagnostic, ResolvedDist, VersionId, VersionOrUrlRef,
17};
18use uv_git::GitResolver;
19use uv_normalize::{ExtraName, GroupName, PackageName};
20use uv_pep440::{Version, VersionSpecifier};
21use uv_pep508::{MarkerEnvironment, MarkerTree, MarkerTreeKind};
22use uv_pypi_types::{Conflicts, HashDigests, ParsedUrlError, VerbatimParsedUrl, Yanked};
23
24use crate::graph_ops::{marker_reachability, simplify_conflict_markers};
25use crate::pins::FilePins;
26use crate::preferences::Preferences;
27use crate::redirect::url_to_precise;
28use crate::resolution::AnnotatedDist;
29use crate::resolution_mode::ResolutionStrategy;
30use crate::resolver::{Resolution, ResolutionDependencyEdge, ResolutionPackage};
31use crate::universal_marker::{ConflictMarker, UniversalMarker};
32use crate::{
33    InMemoryIndex, MetadataResponse, Options, PythonRequirement, ResolveError, VersionsResponse,
34};
35
36/// The output of a successful resolution.
37///
38/// Includes a complete resolution graph in which every node represents a pinned package and every
39/// edge represents a dependency between two pinned packages.
40#[derive(Debug)]
41pub struct ResolverOutput {
42    /// The underlying graph.
43    pub(crate) graph: Graph<ResolutionGraphNode, UniversalMarker, Directed>,
44    /// The range of supported Python versions.
45    pub(crate) requires_python: RequiresPython,
46    /// If the resolution had non-identical forks, store the forks in the lockfile so we can
47    /// recreate them in subsequent resolutions.
48    pub(crate) fork_markers: Vec<UniversalMarker>,
49    /// Any diagnostics that were encountered while building the graph.
50    pub(crate) diagnostics: Vec<ResolutionDiagnostic>,
51    /// The requirements that were used to build the graph.
52    pub(crate) requirements: Vec<Requirement>,
53    /// The constraints that were used to build the graph.
54    pub(crate) constraints: Constraints,
55    /// The overrides that were used to build the graph.
56    pub(crate) overrides: Overrides,
57    /// The options that were used to build the graph.
58    pub(crate) options: Options,
59}
60
61#[derive(Debug, Clone)]
62#[allow(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    /// Create a new [`ResolverOutput`] from the resolved PubGrub state.
125    pub(crate) fn from_state(
126        resolutions: &[Resolution],
127        requirements: &[Requirement],
128        constraints: &Constraints,
129        overrides: &Overrides,
130        preferences: &Preferences,
131        index: &InMemoryIndex,
132        git: &GitResolver,
133        python: &PythonRequirement,
134        conflicts: &Conflicts,
135        resolution_strategy: &ResolutionStrategy,
136        options: Options,
137    ) -> Result<Self, ResolveError> {
138        let size_guess = resolutions[0].nodes.len();
139        let mut graph: Graph<ResolutionGraphNode, UniversalMarker, Directed> =
140            Graph::with_capacity(size_guess, size_guess);
141        let mut inverse: FxHashMap<PackageRef, NodeIndex<u32>> =
142            FxHashMap::with_capacity_and_hasher(size_guess, FxBuildHasher);
143        let mut diagnostics = Vec::new();
144
145        // Add the root node.
146        let root_index = graph.add_node(ResolutionGraphNode::Root);
147
148        let mut seen = FxHashSet::default();
149        for resolution in resolutions {
150            // Add every package to the graph.
151            for (package, version) in &resolution.nodes {
152                if !seen.insert((package, version)) {
153                    // Insert each node only once.
154                    continue;
155                }
156                Self::add_version(
157                    &mut graph,
158                    &mut inverse,
159                    &mut diagnostics,
160                    preferences,
161                    &resolution.pins,
162                    index,
163                    git,
164                    package,
165                    version,
166                )?;
167            }
168        }
169
170        let mut seen = FxHashSet::default();
171        for resolution in resolutions {
172            let marker = resolution.env.try_universal_markers().unwrap_or_default();
173
174            // Add every edge to the graph, propagating the marker for the current fork, if
175            // necessary.
176            for edge in &resolution.edges {
177                if !seen.insert((edge, marker)) {
178                    // Insert each node only once.
179                    continue;
180                }
181
182                Self::add_edge(&mut graph, &mut inverse, root_index, edge, marker);
183            }
184        }
185
186        // Extract the `Requires-Python` range, if provided.
187        let requires_python = python.target().clone();
188
189        let fork_markers: Vec<UniversalMarker> = if let [resolution] = resolutions {
190            // In the case of a singleton marker, we only include it if it's not
191            // always true. Otherwise, we keep our `fork_markers` empty as there
192            // are no forks.
193            resolution
194                .env
195                .try_universal_markers()
196                .into_iter()
197                .filter(|marker| !marker.is_true())
198                .collect()
199        } else {
200            resolutions
201                .iter()
202                .map(|resolution| resolution.env.try_universal_markers().unwrap_or_default())
203                .collect()
204        };
205
206        // Compute and apply the marker reachability.
207        let mut reachability = marker_reachability(&graph, &fork_markers);
208
209        // Apply the reachability to the graph and imbibe world
210        // knowledge about conflicts.
211        let conflict_marker = ConflictMarker::from_conflicts(conflicts);
212        for index in graph.node_indices() {
213            if let ResolutionGraphNode::Dist(dist) = &mut graph[index] {
214                dist.marker = reachability.remove(&index).unwrap_or_default();
215                dist.marker.imbibe(conflict_marker);
216            }
217        }
218        for weight in graph.edge_weights_mut() {
219            weight.imbibe(conflict_marker);
220        }
221
222        simplify_conflict_markers(conflicts, &mut graph);
223
224        // Discard any unreachable nodes.
225        graph.retain_nodes(|graph, node| !graph[node].marker().is_false());
226
227        if matches!(resolution_strategy, ResolutionStrategy::Lowest) {
228            report_missing_lower_bounds(&graph, &mut diagnostics, constraints, overrides);
229        }
230
231        let output = Self {
232            graph,
233            requires_python,
234            diagnostics,
235            requirements: requirements.to_vec(),
236            constraints: constraints.clone(),
237            overrides: overrides.clone(),
238            options,
239            fork_markers,
240        };
241
242        // We only do conflicting distribution detection when no
243        // conflicting groups have been specified. The reason here
244        // is that when there are conflicting groups, then from the
245        // perspective of marker expressions only, it may look like
246        // one can install different versions of the same package for
247        // the same marker environment. However, the thing preventing
248        // this is that the only way this should be possible is if
249        // one tries to install two or more conflicting extras at
250        // the same time. At which point, uv will report an error,
251        // thereby sidestepping the possibility of installing different
252        // versions of the same package into the same virtualenv. ---AG
253        //
254        // FIXME: When `UniversalMarker` supports extras/groups, we can
255        // re-enable this.
256        if conflicts.is_empty() {
257            #[allow(unused_mut, reason = "Used in debug_assertions below")]
258            let mut conflicting = output.find_conflicting_distributions();
259            if !conflicting.is_empty() {
260                tracing::warn!(
261                    "found {} conflicting distributions in resolution, \
262                 please report this as a bug at \
263                 https://github.com/astral-sh/uv/issues/new",
264                    conflicting.len()
265                );
266            }
267            // When testing, we materialize any conflicting distributions as an
268            // error to ensure any relevant tests fail. Otherwise, we just leave
269            // it at the warning message above. The reason for not returning an
270            // error "in production" is that an incorrect resolution may only be
271            // incorrect in certain marker environments, but fine in most others.
272            // Returning an error in that case would make `uv` unusable whenever
273            // the bug occurs, but letting it through means `uv` *could* still be
274            // usable.
275            #[cfg(debug_assertions)]
276            if let Some(err) = conflicting.pop() {
277                return Err(ResolveError::ConflictingDistribution(err));
278            }
279        }
280        Ok(output)
281    }
282
283    fn add_edge(
284        graph: &mut Graph<ResolutionGraphNode, UniversalMarker>,
285        inverse: &mut FxHashMap<PackageRef<'_>, NodeIndex>,
286        root_index: NodeIndex,
287        edge: &ResolutionDependencyEdge,
288        marker: UniversalMarker,
289    ) {
290        let from_index = edge.from.as_ref().map_or(root_index, |from| {
291            inverse[&PackageRef {
292                package_name: from,
293                version: &edge.from_version,
294                url: edge.from_url.as_ref(),
295                index: edge.from_index.as_ref(),
296                extra: edge.from_extra.as_ref(),
297                group: edge.from_group.as_ref(),
298            }]
299        });
300        let to_index = inverse[&PackageRef {
301            package_name: &edge.to,
302            version: &edge.to_version,
303            url: edge.to_url.as_ref(),
304            index: edge.to_index.as_ref(),
305            extra: edge.to_extra.as_ref(),
306            group: edge.to_group.as_ref(),
307        }];
308
309        let edge_marker = {
310            let mut edge_marker = edge.universal_marker();
311            edge_marker.and(marker);
312            edge_marker
313        };
314
315        if let Some(weight) = graph
316            .find_edge(from_index, to_index)
317            .and_then(|edge| graph.edge_weight_mut(edge))
318        {
319            // If either the existing marker or new marker is `true`, then the dependency is
320            // included unconditionally, and so the combined marker is `true`.
321            weight.or(edge_marker);
322        } else {
323            graph.update_edge(from_index, to_index, edge_marker);
324        }
325    }
326
327    fn add_version<'a>(
328        graph: &mut Graph<ResolutionGraphNode, UniversalMarker>,
329        inverse: &mut FxHashMap<PackageRef<'a>, NodeIndex>,
330        diagnostics: &mut Vec<ResolutionDiagnostic>,
331        preferences: &Preferences,
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        // Map the package to a distribution.
346        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            in_memory,
355            git,
356        )?;
357
358        if let Some(metadata) = metadata.as_ref() {
359            // Validate the extra.
360            if let Some(extra) = extra {
361                if !metadata.provides_extra.contains(extra) {
362                    diagnostics.push(ResolutionDiagnostic::MissingExtra {
363                        dist: dist.clone(),
364                        extra: extra.clone(),
365                    });
366                }
367            }
368
369            // Validate the development dependency group.
370            if let Some(dev) = group {
371                if !metadata.dependency_groups.contains_key(dev) {
372                    diagnostics.push(ResolutionDiagnostic::MissingGroup {
373                        dist: dist.clone(),
374                        group: dev.clone(),
375                    });
376                }
377            }
378        }
379
380        // Add the distribution to the graph.
381        let node = graph.add_node(ResolutionGraphNode::Dist(AnnotatedDist {
382            dist,
383            name: name.clone(),
384            version: version.clone(),
385            extra: extra.clone(),
386            group: group.clone(),
387            hashes,
388            metadata,
389            marker: UniversalMarker::TRUE,
390        }));
391        inverse.insert(
392            PackageRef {
393                package_name: name,
394                version,
395                url: url.as_ref(),
396                index: index.as_ref(),
397                extra: extra.as_ref(),
398                group: group.as_ref(),
399            },
400            node,
401        );
402        Ok(())
403    }
404
405    fn parse_dist(
406        name: &PackageName,
407        index: Option<&IndexUrl>,
408        url: Option<&VerbatimParsedUrl>,
409        version: &Version,
410        pins: &FilePins,
411        diagnostics: &mut Vec<ResolutionDiagnostic>,
412        preferences: &Preferences,
413        in_memory: &InMemoryIndex,
414        git: &GitResolver,
415    ) -> Result<(ResolvedDist, HashDigests, Option<Metadata>), ResolveError> {
416        Ok(if let Some(url) = url {
417            // Create the distribution.
418            let dist = Dist::from_url(name.clone(), url_to_precise(url.clone(), git))?;
419
420            let version_id = VersionId::from_url(&url.verbatim);
421
422            // Extract the hashes.
423            let hashes = Self::get_hashes(
424                name,
425                index,
426                Some(url),
427                &version_id,
428                version,
429                preferences,
430                in_memory,
431            );
432
433            // Extract the metadata.
434            let metadata = {
435                let response = in_memory
436                    .distributions()
437                    .get(&version_id)
438                    .unwrap_or_else(|| {
439                        panic!("Every URL distribution should have metadata: {version_id:?}")
440                    });
441
442                let MetadataResponse::Found(archive) = &*response else {
443                    panic!("Every URL distribution should have metadata: {version_id:?}")
444                };
445
446                archive.metadata.clone()
447            };
448
449            (
450                ResolvedDist::Installable {
451                    dist: Arc::new(dist),
452                    version: Some(version.clone()),
453                },
454                hashes,
455                Some(metadata),
456            )
457        } else {
458            let dist = pins
459                .get(name, version)
460                .expect("Every package should be pinned")
461                .clone();
462
463            let version_id = dist.version_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                &version_id,
488                version,
489                preferences,
490                in_memory,
491            );
492
493            // Extract the metadata.
494            let metadata = {
495                in_memory
496                    .distributions()
497                    .get(&version_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 the [`VersionId`], preserving any hashes that were provided by the
512    /// lockfile.
513    fn get_hashes(
514        name: &PackageName,
515        index: Option<&IndexUrl>,
516        url: Option<&VerbatimParsedUrl>,
517        version_id: &VersionId,
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(version_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    /// Return the number of distinct packages in the graph.
602    pub fn len(&self) -> usize {
603        self.dists().filter(|dist| dist.is_base()).count()
604    }
605
606    /// Return `true` if there are no packages in the graph.
607    pub fn is_empty(&self) -> bool {
608        self.dists().any(AnnotatedDist::is_base)
609    }
610
611    /// Returns `true` if the graph contains the given package.
612    pub fn contains(&self, name: &PackageName) -> bool {
613        self.dists().any(|dist| dist.name() == name)
614    }
615
616    /// Return the [`ResolutionDiagnostic`]s that were encountered while building the graph.
617    pub fn diagnostics(&self) -> &[ResolutionDiagnostic] {
618        &self.diagnostics
619    }
620
621    /// Return the marker tree specific to this resolution.
622    ///
623    /// This accepts an in-memory-index and marker environment, all
624    /// of which should be the same values given to the resolver that produced
625    /// this graph.
626    ///
627    /// The marker tree returned corresponds to an expression that, when true,
628    /// this resolution is guaranteed to be correct. Note though that it's
629    /// possible for resolution to be correct even if the returned marker
630    /// expression is false.
631    ///
632    /// For example, if the root package has a dependency `foo; sys_platform ==
633    /// "macos"` and resolution was performed on Linux, then the marker tree
634    /// returned will contain a `sys_platform == "linux"` expression. This
635    /// means that whenever the marker expression evaluates to true (i.e., the
636    /// current platform is Linux), then the resolution here is correct. But
637    /// it is possible that the resolution is also correct on other platforms
638    /// that aren't macOS, such as Windows. (It is unclear at time of writing
639    /// whether this is fundamentally impossible to compute, or just impossible
640    /// to compute in some cases.)
641    pub fn marker_tree(
642        &self,
643        index: &InMemoryIndex,
644        marker_env: &MarkerEnvironment,
645    ) -> Result<MarkerTree, Box<ParsedUrlError>> {
646        use uv_pep508::{
647            CanonicalMarkerValueString, CanonicalMarkerValueVersion, MarkerExpression,
648            MarkerOperator, MarkerTree,
649        };
650
651        /// A subset of the possible marker values.
652        ///
653        /// We only track the marker parameters that are referenced in a marker
654        /// expression. We'll use references to the parameter later to generate
655        /// values based on the current marker environment.
656        #[derive(Debug, Eq, Hash, PartialEq)]
657        enum MarkerParam {
658            Version(CanonicalMarkerValueVersion),
659            String(CanonicalMarkerValueString),
660        }
661
662        /// Add all marker parameters from the given tree to the given set.
663        fn add_marker_params_from_tree(marker_tree: MarkerTree, set: &mut IndexSet<MarkerParam>) {
664            match marker_tree.kind() {
665                MarkerTreeKind::True => {}
666                MarkerTreeKind::False => {}
667                MarkerTreeKind::Version(marker) => {
668                    set.insert(MarkerParam::Version(marker.key()));
669                    for (_, tree) in marker.edges() {
670                        add_marker_params_from_tree(tree, set);
671                    }
672                }
673                MarkerTreeKind::String(marker) => {
674                    set.insert(MarkerParam::String(marker.key()));
675                    for (_, tree) in marker.children() {
676                        add_marker_params_from_tree(tree, set);
677                    }
678                }
679                MarkerTreeKind::In(marker) => {
680                    set.insert(MarkerParam::String(marker.key()));
681                    for (_, tree) in marker.children() {
682                        add_marker_params_from_tree(tree, set);
683                    }
684                }
685                MarkerTreeKind::Contains(marker) => {
686                    set.insert(MarkerParam::String(marker.key()));
687                    for (_, tree) in marker.children() {
688                        add_marker_params_from_tree(tree, set);
689                    }
690                }
691                // We specifically don't care about these for the
692                // purposes of generating a marker string for a lock
693                // file. Quoted strings are marker values given by the
694                // user. We don't track those here, since we're only
695                // interested in which markers are used.
696                MarkerTreeKind::Extra(marker) => {
697                    for (_, tree) in marker.children() {
698                        add_marker_params_from_tree(tree, set);
699                    }
700                }
701                MarkerTreeKind::List(marker) => {
702                    for (_, tree) in marker.children() {
703                        add_marker_params_from_tree(tree, set);
704                    }
705                }
706            }
707        }
708
709        let mut seen_marker_values = IndexSet::default();
710        for i in self.graph.node_indices() {
711            let ResolutionGraphNode::Dist(dist) = &self.graph[i] else {
712                continue;
713            };
714            let version_id = match dist.version_or_url() {
715                VersionOrUrlRef::Version(version) => {
716                    VersionId::from_registry(dist.name().clone(), version.clone())
717                }
718                VersionOrUrlRef::Url(verbatim_url) => VersionId::from_url(verbatim_url.raw()),
719            };
720            let res = index
721                .distributions()
722                .get(&version_id)
723                .expect("every package in resolution graph has metadata");
724            let MetadataResponse::Found(archive, ..) = &*res else {
725                panic!(
726                    "Every package should have metadata: {:?}",
727                    dist.version_id()
728                )
729            };
730            for req in self
731                .constraints
732                .apply(self.overrides.apply(archive.metadata.requires_dist.iter()))
733            {
734                add_marker_params_from_tree(req.marker, &mut seen_marker_values);
735            }
736        }
737
738        // Ensure that we consider markers from direct dependencies.
739        for direct_req in self
740            .constraints
741            .apply(self.overrides.apply(self.requirements.iter()))
742        {
743            add_marker_params_from_tree(direct_req.marker, &mut seen_marker_values);
744        }
745
746        // Generate the final marker expression as a conjunction of
747        // strict equality terms.
748        let mut conjunction = MarkerTree::TRUE;
749        for marker_param in seen_marker_values {
750            let expr = match marker_param {
751                MarkerParam::Version(value_version) => {
752                    let from_env = marker_env.get_version(value_version);
753                    MarkerExpression::Version {
754                        key: value_version.into(),
755                        specifier: VersionSpecifier::equals_version(from_env.clone()),
756                    }
757                }
758                MarkerParam::String(value_string) => {
759                    let from_env = marker_env.get_string(value_string);
760                    MarkerExpression::String {
761                        key: value_string.into(),
762                        operator: MarkerOperator::Equal,
763                        value: from_env.into(),
764                    }
765                }
766            };
767            conjunction.and(MarkerTree::expression(expr));
768        }
769        Ok(conjunction)
770    }
771
772    /// Returns a sequence of conflicting distribution errors from this
773    /// resolution.
774    ///
775    /// Correct resolutions always return an empty sequence. A non-empty
776    /// sequence implies there is a package with two distinct versions in the
777    /// same marker environment in this resolution. This in turn implies that
778    /// an installation in that marker environment could wind up trying to
779    /// install different versions of the same package, which is not allowed.
780    fn find_conflicting_distributions(&self) -> Vec<ConflictingDistributionError> {
781        let mut name_to_markers: BTreeMap<&PackageName, Vec<(&Version, &UniversalMarker)>> =
782            BTreeMap::new();
783        for node in self.graph.node_weights() {
784            let annotated_dist = match node {
785                ResolutionGraphNode::Root => continue,
786                ResolutionGraphNode::Dist(annotated_dist) => annotated_dist,
787            };
788            name_to_markers
789                .entry(&annotated_dist.name)
790                .or_default()
791                .push((&annotated_dist.version, &annotated_dist.marker));
792        }
793        let mut dupes = vec![];
794        for (name, marker_trees) in name_to_markers {
795            for (i, (version1, marker1)) in marker_trees.iter().enumerate() {
796                for (version2, marker2) in &marker_trees[i + 1..] {
797                    if version1 == version2 {
798                        continue;
799                    }
800                    if !marker1.is_disjoint(**marker2) {
801                        dupes.push(ConflictingDistributionError {
802                            name: name.clone(),
803                            version1: (*version1).clone(),
804                            version2: (*version2).clone(),
805                            marker1: **marker1,
806                            marker2: **marker2,
807                        });
808                    }
809                }
810            }
811        }
812        dupes
813    }
814}
815
816/// An error that occurs for conflicting versions of the same package.
817///
818/// Specifically, this occurs when two distributions with the same package
819/// name are found with distinct versions in at least one possible marker
820/// environment. This error reflects an error that could occur when installing
821/// the corresponding resolution into that marker environment.
822#[derive(Debug)]
823pub struct ConflictingDistributionError {
824    name: PackageName,
825    version1: Version,
826    version2: Version,
827    marker1: UniversalMarker,
828    marker2: UniversalMarker,
829}
830
831impl std::error::Error for ConflictingDistributionError {}
832
833impl Display for ConflictingDistributionError {
834    fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
835        let Self {
836            ref name,
837            ref version1,
838            ref version2,
839            ref marker1,
840            ref marker2,
841        } = *self;
842        write!(
843            f,
844            "found conflicting versions for package `{name}`:
845             `{marker1:?}` (for version `{version1}`) is not disjoint with \
846             `{marker2:?}` (for version `{version2}`)",
847        )
848    }
849}
850
851/// Convert a [`ResolverOutput`] into a [`uv_distribution_types::Resolution`].
852///
853/// This involves converting [`ResolutionGraphNode`]s into [`Node`]s, which in turn involves
854/// dropping any extras and dependency groups from the graph nodes. Instead, each package is
855/// collapsed into a single node, with  extras and dependency groups annotating the _edges_, rather
856/// than being represented as separate nodes. This is a more natural representation, but a further
857/// departure from the PubGrub model.
858///
859/// For simplicity, this transformation makes the assumption that the resolution only applies to a
860/// subset of markers, i.e., it shouldn't be called on universal resolutions, and expects only a
861/// single version of each package to be present in the graph.
862impl From<ResolverOutput> for uv_distribution_types::Resolution {
863    fn from(output: ResolverOutput) -> Self {
864        let ResolverOutput {
865            graph,
866            diagnostics,
867            fork_markers,
868            ..
869        } = output;
870
871        assert!(
872            fork_markers.is_empty(),
873            "universal resolutions are not supported"
874        );
875
876        let mut transformed = Graph::with_capacity(graph.node_count(), graph.edge_count());
877        let mut inverse = FxHashMap::with_capacity_and_hasher(graph.node_count(), FxBuildHasher);
878
879        // Create the root node.
880        let root = transformed.add_node(Node::Root);
881
882        // Re-add the nodes to the reduced graph.
883        for index in graph.node_indices() {
884            let ResolutionGraphNode::Dist(dist) = &graph[index] else {
885                continue;
886            };
887            if dist.is_base() {
888                inverse.insert(
889                    &dist.name,
890                    transformed.add_node(Node::Dist {
891                        dist: dist.dist.clone(),
892                        hashes: dist.hashes.clone(),
893                        install: true,
894                    }),
895                );
896            }
897        }
898
899        // Re-add the edges to the reduced graph.
900        for edge in graph.edge_indices() {
901            let (source, target) = graph.edge_endpoints(edge).unwrap();
902
903            match (&graph[source], &graph[target]) {
904                (ResolutionGraphNode::Root, ResolutionGraphNode::Dist(target_dist)) => {
905                    let target = inverse[&target_dist.name()];
906                    transformed.update_edge(root, target, Edge::Prod);
907                }
908                (
909                    ResolutionGraphNode::Dist(source_dist),
910                    ResolutionGraphNode::Dist(target_dist),
911                ) => {
912                    let source = inverse[&source_dist.name()];
913                    let target = inverse[&target_dist.name()];
914
915                    let edge = if let Some(extra) = source_dist.extra.as_ref() {
916                        Edge::Optional(extra.clone())
917                    } else if let Some(group) = source_dist.group.as_ref() {
918                        Edge::Dev(group.clone())
919                    } else {
920                        Edge::Prod
921                    };
922
923                    transformed.add_edge(source, target, edge);
924                }
925                _ => {
926                    unreachable!("root should not contain incoming edges");
927                }
928            }
929        }
930
931        Self::new(transformed).with_diagnostics(diagnostics)
932    }
933}
934
935/// Find any packages that don't have any lower bound on them when in resolution-lowest mode.
936fn report_missing_lower_bounds(
937    graph: &Graph<ResolutionGraphNode, UniversalMarker>,
938    diagnostics: &mut Vec<ResolutionDiagnostic>,
939    constraints: &Constraints,
940    overrides: &Overrides,
941) {
942    for node_index in graph.node_indices() {
943        let ResolutionGraphNode::Dist(dist) = graph.node_weight(node_index).unwrap() else {
944            // Ignore the root package.
945            continue;
946        };
947        if !has_lower_bound(node_index, dist.name(), graph, constraints, overrides) {
948            diagnostics.push(ResolutionDiagnostic::MissingLowerBound {
949                package_name: dist.name().clone(),
950            });
951        }
952    }
953}
954
955/// Whether the given package has a lower version bound by another package.
956fn has_lower_bound(
957    node_index: NodeIndex,
958    package_name: &PackageName,
959    graph: &Graph<ResolutionGraphNode, UniversalMarker>,
960    constraints: &Constraints,
961    overrides: &Overrides,
962) -> bool {
963    for neighbor_index in graph.neighbors_directed(node_index, Direction::Incoming) {
964        let neighbor_dist = match graph.node_weight(neighbor_index).unwrap() {
965            ResolutionGraphNode::Root => {
966                // We already handled direct dependencies with a missing constraint
967                // separately.
968                return true;
969            }
970            ResolutionGraphNode::Dist(neighbor_dist) => neighbor_dist,
971        };
972
973        if neighbor_dist.name() == package_name {
974            // Only warn for real packages, not for virtual packages such as dev nodes.
975            return true;
976        }
977
978        let Some(metadata) = neighbor_dist.metadata.as_ref() else {
979            // We can't check for lower bounds if we lack metadata.
980            return true;
981        };
982
983        // Get all individual specifier for the current package and check if any has a lower
984        // bound.
985        for requirement in metadata
986            .requires_dist
987            .iter()
988            // These bounds sources are missing from the graph.
989            .chain(metadata.dependency_groups.values().flatten())
990            .chain(constraints.requirements())
991            .chain(overrides.requirements())
992        {
993            if requirement.name != *package_name {
994                continue;
995            }
996            let Some(specifiers) = requirement.source.version_specifiers() else {
997                // URL requirements are a bound.
998                return true;
999            };
1000            if specifiers.iter().any(VersionSpecifier::has_lower_bound) {
1001                return true;
1002            }
1003        }
1004    }
1005    false
1006}