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