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::{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/// The output of a successful resolution.
36///
37/// Includes a complete resolution graph in which every node represents a pinned package and every
38/// edge represents a dependency between two pinned packages.
39#[derive(Debug)]
40pub struct ResolverOutput {
41    /// The underlying graph.
42    pub(crate) graph: Graph<ResolutionGraphNode, UniversalMarker, Directed>,
43    /// The range of supported Python versions.
44    pub(crate) requires_python: RequiresPython,
45    /// If the resolution had non-identical forks, store the forks in the lockfile so we can
46    /// recreate them in subsequent resolutions.
47    pub(crate) fork_markers: Vec<UniversalMarker>,
48    /// Any diagnostics that were encountered while building the graph.
49    pub(crate) diagnostics: Vec<ResolutionDiagnostic>,
50    /// The requirements that were used to build the graph.
51    pub(crate) requirements: Vec<Requirement>,
52    /// The constraints that were used to build the graph.
53    pub(crate) constraints: Constraints,
54    /// The overrides that were used to build the graph.
55    pub(crate) overrides: Overrides,
56    /// The options that were used to build the graph.
57    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    /// Create a new [`ResolverOutput`] from the resolved PubGrub state.
124    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        // Add the root node.
145        let root_index = graph.add_node(ResolutionGraphNode::Root);
146
147        let mut seen = FxHashSet::default();
148        for resolution in resolutions {
149            // Add every package to the graph.
150            for (package, version) in &resolution.nodes {
151                if !seen.insert((package, version)) {
152                    // Insert each node only once.
153                    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            // Add every edge to the graph, propagating the marker for the current fork, if
174            // necessary.
175            for edge in &resolution.edges {
176                if !seen.insert((edge, marker)) {
177                    // Insert each node only once.
178                    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            // In the case of a singleton marker, we only include it if it's not
187            // always true. Otherwise, we keep our `fork_markers` empty as there
188            // are no forks.
189            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        // Compute and apply the marker reachability.
203        let mut reachability = marker_reachability(&graph, &fork_markers);
204
205        // Apply the reachability to the graph and imbibe world
206        // knowledge about conflicts.
207        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        // Discard any unreachable nodes.
221        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        // We only do conflicting distribution detection when no
239        // conflicting groups have been specified. The reason here
240        // is that when there are conflicting groups, then from the
241        // perspective of marker expressions only, it may look like
242        // one can install different versions of the same package for
243        // the same marker environment. However, the thing preventing
244        // this is that the only way this should be possible is if
245        // one tries to install two or more conflicting extras at
246        // the same time. At which point, uv will report an error,
247        // thereby sidestepping the possibility of installing different
248        // versions of the same package into the same virtualenv. ---AG
249        //
250        // FIXME: When `UniversalMarker` supports extras/groups, we can
251        // re-enable this.
252        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            // When testing, we materialize any conflicting distributions as an
264            // error to ensure any relevant tests fail. Otherwise, we just leave
265            // it at the warning message above. The reason for not returning an
266            // error "in production" is that an incorrect resolution may only be
267            // incorrect in certain marker environments, but fine in most others.
268            // Returning an error in that case would make `uv` unusable whenever
269            // the bug occurs, but letting it through means `uv` *could* still be
270            // usable.
271            #[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            // If either the existing marker or new marker is `true`, then the dependency is
316            // included unconditionally, and so the combined marker is `true`.
317            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        // Map the package to a distribution.
342        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            // Validate the extra.
356            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            // Validate the development dependency group.
366            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        // Add the distribution to the graph.
377        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            // Create the locked distribution and recover the metadata using the original URL that
414            // was requested during resolution.
415            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            // Extract the hashes.
419            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            // Extract the metadata.
430            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            // Track yanks for any registry distributions.
461            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            // Extract the hashes.
478            let hashes = Self::get_hashes(
479                name,
480                index,
481                None,
482                &hashes_id,
483                version,
484                preferences,
485                in_memory,
486            );
487
488            // Extract the metadata.
489            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    /// Identify the hashes for a concrete distribution, preserving any hashes that were provided
507    /// by the lockfile.
508    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        // 1. Look for hashes from the lockfile.
518        if let Some(digests) = preferences.match_hashes(name, version) {
519            if !digests.is_empty() {
520                return HashDigests::from(digests);
521            }
522        }
523
524        // 2. Look for hashes for the distribution (i.e., the specific wheel or source distribution).
525        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        // 3. Look for hashes from the registry, which are served at the package level.
536        if url.is_none() {
537            // Query the implicit and explicit indexes (lazily) for the hashes.
538            let implicit_response = in_memory.implicit().get(name);
539            let mut explicit_response = None;
540
541            // Search in the implicit indexes.
542            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                    // Search in the explicit indexes.
557                    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    /// Returns an iterator over the distinct packages in the graph.
587    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    /// Returns an iterator over the base distributions in the graph.
597    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    /// Return the number of distinct packages in the graph.
607    pub fn len(&self) -> usize {
608        self.base_dists().count()
609    }
610
611    /// Return `true` if there are no packages in the graph.
612    pub fn is_empty(&self) -> bool {
613        self.base_dists().next().is_none()
614    }
615
616    /// Retain registry hashes only for artifacts permitted by package-specific build options.
617    ///
618    /// All available wheel hashes remain eligible when source builds are disabled so the
619    /// resulting requirements can still be installed on other supported platforms.
620    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    /// Returns `true` if the graph contains the given package.
661    pub fn contains(&self, name: &PackageName) -> bool {
662        self.dists().any(|dist| dist.name() == name)
663    }
664
665    /// Return the [`ResolutionDiagnostic`]s that were encountered while building the graph.
666    pub fn diagnostics(&self) -> &[ResolutionDiagnostic] {
667        &self.diagnostics
668    }
669
670    /// Return the marker tree specific to this resolution.
671    ///
672    /// This accepts an in-memory-index and marker environment, all
673    /// of which should be the same values given to the resolver that produced
674    /// this graph.
675    ///
676    /// The marker tree returned corresponds to an expression that, when true,
677    /// this resolution is guaranteed to be correct. Note though that it's
678    /// possible for resolution to be correct even if the returned marker
679    /// expression is false.
680    ///
681    /// For example, if the root package has a dependency `foo; sys_platform ==
682    /// "macos"` and resolution was performed on Linux, then the marker tree
683    /// returned will contain a `sys_platform == "linux"` expression. This
684    /// means that whenever the marker expression evaluates to true (i.e., the
685    /// current platform is Linux), then the resolution here is correct. But
686    /// it is possible that the resolution is also correct on other platforms
687    /// that aren't macOS, such as Windows. (It is unclear at time of writing
688    /// whether this is fundamentally impossible to compute, or just impossible
689    /// to compute in some cases.)
690    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        /// A subset of the possible marker values.
701        ///
702        /// We only track the marker parameters that are referenced in a marker
703        /// expression. We'll use references to the parameter later to generate
704        /// values based on the current marker environment.
705        #[derive(Debug, Eq, Hash, PartialEq)]
706        enum MarkerParam {
707            Version(CanonicalMarkerValueVersion),
708            String(CanonicalMarkerValueString),
709        }
710
711        /// Add all marker parameters from the given tree to the given set.
712        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                // We specifically don't care about these for the
741                // purposes of generating a marker string for a lock
742                // file. Quoted strings are marker values given by the
743                // user. We don't track those here, since we're only
744                // interested in which markers are used.
745                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        // Ensure that we consider markers from direct dependencies.
781        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        // Generate the final marker expression as a conjunction of
789        // strict equality terms.
790        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    /// Returns a sequence of conflicting distribution errors from this
815    /// resolution.
816    ///
817    /// Correct resolutions always return an empty sequence. A non-empty
818    /// sequence implies there is a package with two distinct versions in the
819    /// same marker environment in this resolution. This in turn implies that
820    /// an installation in that marker environment could wind up trying to
821    /// install different versions of the same package, which is not allowed.
822    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/// An error that occurs for conflicting versions of the same package.
859///
860/// Specifically, this occurs when two distributions with the same package
861/// name are found with distinct versions in at least one possible marker
862/// environment. This error reflects an error that could occur when installing
863/// the corresponding resolution into that marker environment.
864#[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
893/// Convert a [`ResolverOutput`] into a [`uv_distribution_types::Resolution`].
894///
895/// This involves converting [`ResolutionGraphNode`]s into [`Node`]s, which in turn involves
896/// dropping any extras and dependency groups from the graph nodes. Instead, each package is
897/// collapsed into a single node, with  extras and dependency groups annotating the _edges_, rather
898/// than being represented as separate nodes. This is a more natural representation, but a further
899/// departure from the PubGrub model.
900///
901/// For simplicity, this transformation makes the assumption that the resolution only applies to a
902/// subset of markers, i.e., it shouldn't be called on universal resolutions, and expects only a
903/// single version of each package to be present in the graph.
904impl 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        // Create the root node.
922        let root = transformed.add_node(Node::Root);
923
924        // Re-add the nodes to the reduced graph.
925        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        // Re-add the edges to the reduced graph.
942        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
977/// Find any packages that don't have any lower bound on them when in resolution-lowest mode.
978fn 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            // Ignore the root package.
987            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
997/// Whether the given package has a lower version bound by another package.
998fn 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                // We already handled direct dependencies with a missing constraint
1009                // separately.
1010                return true;
1011            }
1012            ResolutionGraphNode::Dist(neighbor_dist) => neighbor_dist,
1013        };
1014
1015        if neighbor_dist.name() == package_name {
1016            // Only warn for real packages, not for virtual packages such as dev nodes.
1017            return true;
1018        }
1019
1020        let Some(metadata) = neighbor_dist.metadata.as_ref() else {
1021            // We can't check for lower bounds if we lack metadata.
1022            return true;
1023        };
1024
1025        // Get all individual specifier for the current package and check if any has a lower
1026        // bound.
1027        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            // Constraints are missing from the graph.
1035            .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                // URL requirements are a bound.
1042                return true;
1043            };
1044            if specifiers.iter().any(VersionSpecifier::has_lower_bound) {
1045                return true;
1046            }
1047        }
1048    }
1049    false
1050}