Skip to main content

uv_resolver/
error.rs

1use std::collections::{BTreeMap, BTreeSet, Bound};
2use std::fmt::{Debug, Formatter};
3use std::ops::Deref;
4use std::sync::{Arc, OnceLock};
5
6use indexmap::IndexSet;
7use itertools::Itertools;
8use owo_colors::OwoColorize;
9use pubgrub::{DerivationTree, Derived, External, Map, Range, Ranges, Term};
10use rustc_hash::{FxHashMap, FxHashSet};
11use tracing::trace;
12
13use uv_distribution_types::{
14    DerivationChain, DistErrorKind, IndexCapabilities, IndexLocations, IndexUrl, RequestedDist,
15};
16use uv_normalize::{ExtraName, InvalidNameError, PackageName};
17use uv_pep440::{LocalVersionSlice, LowerBound, Version};
18use uv_pep508::MarkerEnvironment;
19use uv_platform_tags::Tags;
20use uv_pypi_types::ParsedUrl;
21use uv_redacted::DisplaySafeUrl;
22use uv_static::EnvVars;
23
24use crate::candidate_selector::CandidateSelector;
25use crate::dependency_provider::UvDependencyProvider;
26use crate::fork_indexes::ForkIndexes;
27use crate::fork_urls::ForkUrls;
28use crate::prerelease::AllowPrerelease;
29use crate::pubgrub::{
30    PubGrubHint, PubGrubPackage, PubGrubPackageInner, PubGrubReportFormatter,
31    report_derivation_tree,
32};
33use crate::python_requirement::PythonRequirement;
34use crate::resolution::ConflictingDistributionError;
35use crate::resolver::{
36    MetadataUnavailable, ResolverEnvironment, UnavailablePackage, UnavailableReason,
37};
38use crate::{InMemoryIndex, Options};
39
40#[derive(Debug, thiserror::Error)]
41pub enum ResolveError {
42    #[error("Failed to resolve dependencies for package `{1}=={2}`")]
43    Dependencies(#[source] Box<Self>, PackageName, Version, DerivationChain),
44
45    #[error(transparent)]
46    Client(#[from] uv_client::Error),
47
48    #[error(transparent)]
49    Distribution(#[from] uv_distribution::Error),
50
51    #[error("The channel closed unexpectedly")]
52    ChannelClosed,
53
54    #[error("Attempted to wait on an unregistered task: `{_0}`")]
55    UnregisteredTask(String),
56
57    #[error(
58        "Requirements contain conflicting URLs for package `{package_name}`{}:\n- {}",
59        if env.marker_environment().is_some() {
60            String::new()
61        } else {
62            format!(" in {env}")
63        },
64        urls.iter()
65            .map(|url| format!("{}{}", DisplaySafeUrl::from(url.clone()), if url.is_editable() { " (editable)" } else { "" }))
66            .collect::<Vec<_>>()
67            .join("\n- ")
68    )]
69    ConflictingUrls {
70        package_name: PackageName,
71        urls: Vec<ParsedUrl>,
72        env: ResolverEnvironment,
73    },
74
75    #[error(
76        "Requirements contain conflicting indexes for package `{package_name}`{}:\n- {}",
77        if env.marker_environment().is_some() {
78            String::new()
79        } else {
80            format!(" in {env}")
81        },
82        indexes.iter()
83            .map(std::string::ToString::to_string)
84            .collect::<Vec<_>>()
85            .join("\n- ")
86    )]
87    ConflictingIndexesForEnvironment {
88        package_name: PackageName,
89        indexes: Vec<IndexUrl>,
90        env: ResolverEnvironment,
91    },
92
93    #[error("Requirements contain conflicting indexes for package `{0}`: `{1}` vs. `{2}`")]
94    ConflictingIndexes(PackageName, String, String),
95
96    #[error(
97        "Package `{name}` was included as a URL dependency. URL dependencies must be expressed as direct requirements or constraints. Consider adding `{requirement}` to your dependencies or constraints file.",
98        name = name.cyan(),
99        requirement = format!("{name} @ {url}").cyan(),
100    )]
101    DisallowedUrl { name: PackageName, url: String },
102
103    #[error(transparent)]
104    DistributionType(#[from] uv_distribution_types::Error),
105
106    #[error("{0} `{1}`")]
107    Dist(
108        DistErrorKind,
109        Box<RequestedDist>,
110        DerivationChain,
111        #[source] Arc<uv_distribution::Error>,
112    ),
113
114    #[error(transparent)]
115    NoSolution(#[from] Box<NoSolutionError>),
116
117    #[error("Attempted to construct an invalid version specifier")]
118    InvalidVersion(#[from] uv_pep440::VersionSpecifierBuildError),
119
120    #[error(
121        "In `--require-hashes` mode, all requirements must be pinned upfront with `==`, but found: `{0}`"
122    )]
123    UnhashedPackage(PackageName),
124
125    #[error("found conflicting distribution in resolution: {0}")]
126    ConflictingDistribution(ConflictingDistributionError),
127
128    #[error("Package `{0}` is unavailable")]
129    PackageUnavailable(PackageName),
130
131    #[error("Invalid extra value in conflict marker: {reason}: {raw_extra}")]
132    InvalidExtraInConflictMarker {
133        reason: String,
134        raw_extra: ExtraName,
135    },
136
137    #[error("Invalid {kind} value in conflict marker: {name_error}")]
138    InvalidValueInConflictMarker {
139        kind: &'static str,
140        #[source]
141        name_error: InvalidNameError,
142    },
143    #[error(
144        "The index returned metadata for the wrong package: expected {request} for {expected}, got {request} for {actual}"
145    )]
146    MismatchedPackageName {
147        request: &'static str,
148        expected: PackageName,
149        actual: PackageName,
150    },
151}
152
153impl uv_errors::Hint for ResolveError {
154    fn hints(&self) -> uv_errors::Hints<'_> {
155        match self {
156            Self::NoSolution(no_solution) => uv_errors::Hint::hints(no_solution.as_ref()),
157            Self::Client(error) => uv_errors::Hint::hints(error),
158            Self::Distribution(error) => uv_errors::Hint::hints(error),
159            Self::Dependencies(error, ..) => uv_errors::Hint::hints(error.as_ref()),
160            _ => uv_errors::Hints::none(),
161        }
162    }
163}
164
165impl<T> From<tokio::sync::mpsc::error::SendError<T>> for ResolveError {
166    /// Drop the value we want to send to not leak the private type we're sending.
167    /// The tokio error only says "channel closed", so we don't lose information.
168    fn from(_value: tokio::sync::mpsc::error::SendError<T>) -> Self {
169        Self::ChannelClosed
170    }
171}
172
173pub type ErrorTree = DerivationTree<PubGrubPackage, Range<Version>, UnavailableReason>;
174type ErrorExternal = External<PubGrubPackage, Range<Version>, UnavailableReason>;
175type ErrorDerived = Derived<PubGrubPackage, Range<Version>, UnavailableReason>;
176type ErrorTerms = Map<PubGrubPackage, Term<Range<Version>>>;
177
178/// Visit each distinct package in a derivation tree without recursive calls.
179///
180/// The iteration order is unspecified, matching the set semantics of
181/// [`DerivationTree::packages`].
182pub(crate) fn derivation_tree_packages(
183    derivation_tree: &ErrorTree,
184) -> impl Iterator<Item = &PubGrubPackage> {
185    let mut packages = FxHashSet::default();
186    let mut trees = vec![derivation_tree];
187
188    while let Some(tree) = trees.pop() {
189        match tree {
190            DerivationTree::External(external) => match external {
191                External::FromDependencyOf(package, _, dependency, _) => {
192                    packages.insert(package);
193                    packages.insert(dependency);
194                }
195                External::NoVersions(package, _)
196                | External::NotRoot(package, _)
197                | External::Custom(package, _, _) => {
198                    packages.insert(package);
199                }
200            },
201            DerivationTree::Derived(derived) => {
202                packages.extend(derived.terms.keys());
203                trees.push(&derived.cause1);
204                trees.push(&derived.cause2);
205            }
206        }
207    }
208
209    packages.into_iter()
210}
211
212/// Drop an exclusively owned derivation tree without recursing through its children.
213///
214/// Shared [`Arc`] children are left for their remaining owners; once the last owner is processed,
215/// [`Arc::try_unwrap`] exposes the child for iterative destruction.
216pub(crate) fn drop_derivation_tree(derivation_tree: ErrorTree) {
217    let mut trees = vec![derivation_tree];
218
219    while let Some(tree) = trees.pop() {
220        if let DerivationTree::Derived(derived) = tree {
221            if let Ok(cause1) = Arc::try_unwrap(derived.cause1) {
222                trees.push(cause1);
223            }
224            if let Ok(cause2) = Arc::try_unwrap(derived.cause2) {
225                trees.push(cause2);
226            }
227        }
228    }
229}
230
231/// Own a derivation tree whose destruction must not recurse through the process stack.
232///
233/// The `Option` allows [`Drop`] to take ownership of the tree and applies the same iterative
234/// teardown during normal returns and unwinding.
235#[derive(Clone)]
236struct StackSafeErrorTree(Option<ErrorTree>);
237
238impl StackSafeErrorTree {
239    fn new(derivation_tree: ErrorTree) -> Self {
240        Self(Some(derivation_tree))
241    }
242
243    fn into_inner(mut self) -> ErrorTree {
244        self.0.take().expect("derivation tree is only taken once")
245    }
246}
247
248impl Deref for StackSafeErrorTree {
249    type Target = ErrorTree;
250
251    fn deref(&self) -> &Self::Target {
252        self.0
253            .as_ref()
254            .expect("derivation tree is only taken during drop")
255    }
256}
257
258impl Drop for StackSafeErrorTree {
259    fn drop(&mut self) {
260        if let Some(derivation_tree) = self.0.take() {
261            drop_derivation_tree(derivation_tree);
262        }
263    }
264}
265
266impl Debug for StackSafeErrorTree {
267    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
268        debug_derivation_tree(self, f)
269    }
270}
271
272/// Preserve PubGrub's [`Debug`] representation without recursive formatting.
273fn debug_derivation_tree(
274    derivation_tree: &ErrorTree,
275    formatter: &mut Formatter<'_>,
276) -> std::fmt::Result {
277    enum Frame<'a> {
278        Tree(&'a ErrorTree),
279        Text(&'static str),
280    }
281
282    let mut frames = vec![Frame::Tree(derivation_tree)];
283
284    while let Some(frame) = frames.pop() {
285        match frame {
286            Frame::Tree(DerivationTree::External(external)) => {
287                write!(formatter, "External({external:?})")?;
288            }
289            Frame::Tree(DerivationTree::Derived(derived)) => {
290                write!(
291                    formatter,
292                    "Derived(Derived {{ terms: {:?}, shared_id: {:?}, cause1: ",
293                    derived.terms, derived.shared_id
294                )?;
295                frames.push(Frame::Text(" })"));
296                frames.push(Frame::Tree(&derived.cause2));
297                frames.push(Frame::Text(", cause2: "));
298                frames.push(Frame::Tree(&derived.cause1));
299            }
300            Frame::Text(text) => formatter.write_str(text)?,
301        }
302    }
303
304    Ok(())
305}
306
307struct DerivedMetadata {
308    terms: ErrorTerms,
309    shared_id: Option<usize>,
310}
311
312enum TreeTask {
313    Visit(StackSafeErrorTree),
314    Rebuild(DerivedMetadata),
315}
316
317fn schedule_derived(tasks: &mut Vec<TreeTask>, derived: ErrorDerived) {
318    let Derived {
319        terms,
320        shared_id,
321        cause1,
322        cause2,
323    } = derived;
324    tasks.push(TreeTask::Rebuild(DerivedMetadata { terms, shared_id }));
325    tasks.push(TreeTask::Visit(StackSafeErrorTree::new(
326        Arc::unwrap_or_clone(cause2),
327    )));
328    tasks.push(TreeTask::Visit(StackSafeErrorTree::new(
329        Arc::unwrap_or_clone(cause1),
330    )));
331}
332
333fn transform_derivation_tree(
334    derivation_tree: ErrorTree,
335    mut transform_external: impl FnMut(ErrorExternal) -> Option<ErrorTree>,
336    mut transform_derived: impl FnMut(
337        DerivedMetadata,
338        Option<ErrorTree>,
339        Option<ErrorTree>,
340    ) -> Option<ErrorTree>,
341) -> Option<ErrorTree> {
342    let mut tasks = vec![TreeTask::Visit(StackSafeErrorTree::new(derivation_tree))];
343    let mut results: Vec<Option<StackSafeErrorTree>> = Vec::new();
344
345    while let Some(task) = tasks.pop() {
346        match task {
347            TreeTask::Visit(tree) => match tree.into_inner() {
348                DerivationTree::External(external) => {
349                    results.push(transform_external(external).map(StackSafeErrorTree::new));
350                }
351                DerivationTree::Derived(derived) => schedule_derived(&mut tasks, derived),
352            },
353            TreeTask::Rebuild(metadata) => {
354                let cause2 = results
355                    .pop()
356                    .expect("every derived tree has a second transformed cause")
357                    .map(StackSafeErrorTree::into_inner);
358                let cause1 = results
359                    .pop()
360                    .expect("every derived tree has a first transformed cause")
361                    .map(StackSafeErrorTree::into_inner);
362                results
363                    .push(transform_derived(metadata, cause1, cause2).map(StackSafeErrorTree::new));
364            }
365        }
366    }
367
368    results
369        .pop()
370        .expect("the root derivation tree produces one transformed result")
371        .map(StackSafeErrorTree::into_inner)
372}
373
374fn map_derivation_tree(
375    derivation_tree: ErrorTree,
376    mut transform_external: impl FnMut(ErrorExternal) -> ErrorTree,
377    mut transform_derived: impl FnMut(DerivedMetadata, ErrorTree, ErrorTree) -> ErrorTree,
378) -> ErrorTree {
379    transform_derivation_tree(
380        derivation_tree,
381        |external| Some(transform_external(external)),
382        |metadata, cause1, cause2| {
383            Some(transform_derived(
384                metadata,
385                cause1.expect("map transformations retain the first cause"),
386                cause2.expect("map transformations retain the second cause"),
387            ))
388        },
389    )
390    .expect("map transformations retain the root")
391}
392
393fn derived_tree(metadata: DerivedMetadata, cause1: ErrorTree, cause2: ErrorTree) -> ErrorTree {
394    DerivationTree::Derived(Derived {
395        terms: metadata.terms,
396        shared_id: metadata.shared_id,
397        cause1: Arc::new(cause1),
398        cause2: Arc::new(cause2),
399    })
400}
401
402/// A wrapper around [`pubgrub::error::NoSolutionError`] that displays a resolution failure report.
403pub struct NoSolutionError {
404    error: StackSafeErrorTree,
405    index: InMemoryIndex,
406    /// The versions that were available for each package after `exclude-newer` filtering.
407    ///
408    /// For versions available before filtering, see [`NoSolutionError::available_versions`].
409    included_versions: FxHashMap<PackageName, BTreeSet<Version>>,
410    /// The versions available for each package.
411    ///
412    /// These version sets are not filtered by `exclude-newer`. See
413    /// [`NoSolutionError::included_versions`] instead if filtered versions are needed.
414    ///
415    /// These versions are filtered by [`EnvVars::UV_TEST_AVAILABLE_VERSION_CUTOFF`] for
416    /// deterministic output in tests.
417    available_versions: FxHashMap<PackageName, BTreeSet<Version>>,
418    available_indexes: FxHashMap<PackageName, BTreeSet<IndexUrl>>,
419    selector: CandidateSelector,
420    python_requirement: PythonRequirement,
421    index_locations: IndexLocations,
422    index_capabilities: IndexCapabilities,
423    unavailable_packages: FxHashMap<PackageName, UnavailablePackage>,
424    incomplete_packages: FxHashMap<PackageName, BTreeMap<Version, MetadataUnavailable>>,
425    fork_urls: ForkUrls,
426    fork_indexes: ForkIndexes,
427    env: ResolverEnvironment,
428    current_environment: MarkerEnvironment,
429    tags: Option<Tags>,
430    workspace_members: BTreeSet<PackageName>,
431    options: Options,
432    /// Cached report and hints, computed once on first access.
433    cached: OnceLock<(String, IndexSet<PubGrubHint>)>,
434}
435
436impl NoSolutionError {
437    /// Create a new [`NoSolutionError`] from a [`pubgrub::NoSolutionError`].
438    pub(crate) fn new(
439        error: pubgrub::NoSolutionError<UvDependencyProvider>,
440        index: InMemoryIndex,
441        included_versions: FxHashMap<PackageName, BTreeSet<Version>>,
442        available_versions: FxHashMap<PackageName, BTreeSet<Version>>,
443        available_indexes: FxHashMap<PackageName, BTreeSet<IndexUrl>>,
444        selector: CandidateSelector,
445        python_requirement: PythonRequirement,
446        index_locations: IndexLocations,
447        index_capabilities: IndexCapabilities,
448        unavailable_packages: FxHashMap<PackageName, UnavailablePackage>,
449        incomplete_packages: FxHashMap<PackageName, BTreeMap<Version, MetadataUnavailable>>,
450        fork_urls: ForkUrls,
451        fork_indexes: ForkIndexes,
452        env: ResolverEnvironment,
453        current_environment: MarkerEnvironment,
454        tags: Option<Tags>,
455        workspace_members: BTreeSet<PackageName>,
456        options: Options,
457    ) -> Self {
458        Self {
459            error: StackSafeErrorTree::new(error),
460            index,
461            included_versions,
462            available_versions,
463            available_indexes,
464            selector,
465            python_requirement,
466            index_locations,
467            index_capabilities,
468            unavailable_packages,
469            incomplete_packages,
470            fork_urls,
471            fork_indexes,
472            env,
473            current_environment,
474            tags,
475            workspace_members,
476            options,
477            cached: OnceLock::new(),
478        }
479    }
480
481    /// Get the cached report and hints, computing them on first access.
482    fn cached(&self) -> &(String, IndexSet<PubGrubHint>) {
483        self.cached.get_or_init(|| self.compute_report_and_hints())
484    }
485
486    /// Given a [`DerivationTree`], collapse any [`External::FromDependencyOf`] incompatibilities
487    /// wrap an [`PubGrubPackageInner::Extra`] package.
488    pub(crate) fn collapse_proxies(derivation_tree: ErrorTree) -> ErrorTree {
489        fn is_proxy(tree: &ErrorTree) -> bool {
490            matches!(
491                tree,
492                DerivationTree::External(External::FromDependencyOf(package, ..))
493                    if package.is_proxy()
494            )
495        }
496
497        transform_derivation_tree(
498            derivation_tree,
499            |external| Some(DerivationTree::External(external)),
500            |metadata, cause1, cause2| match (cause1, cause2) {
501                (Some(cause1), Some(cause2)) if is_proxy(&cause1) && is_proxy(&cause2) => None,
502                (Some(cause), other) if is_proxy(&cause) => other,
503                (other, Some(cause)) if is_proxy(&cause) => other,
504                (Some(cause1), Some(cause2)) => Some(derived_tree(metadata, cause1, cause2)),
505                (Some(cause), None) | (None, Some(cause)) => Some(cause),
506                (None, None) => None,
507            },
508        )
509        .expect("derivation tree should contain at least one external term")
510    }
511
512    /// Simplifies the version ranges on any incompatibilities to remove the `[max]` sentinel.
513    ///
514    /// The `[max]` sentinel is used to represent the maximum local version of a package, to
515    /// implement PEP 440 semantics for local version equality. For example, `1.0.0+foo` needs to
516    /// satisfy `==1.0.0`.
517    pub(crate) fn collapse_local_version_segments(derivation_tree: ErrorTree) -> ErrorTree {
518        transform_derivation_tree(
519            derivation_tree,
520            |external| match external {
521                external @ External::NotRoot(_, _) => Some(DerivationTree::External(external)),
522                External::NoVersions(package, versions) => {
523                    if SentinelRange::from(&versions).is_complement() {
524                        return None;
525                    }
526
527                    let versions = SentinelRange::from(&versions).strip();
528                    Some(DerivationTree::External(External::NoVersions(
529                        package, versions,
530                    )))
531                }
532                External::FromDependencyOf(package1, versions1, package2, versions2) => {
533                    let versions1 = SentinelRange::from(&versions1).strip();
534                    let versions2 = SentinelRange::from(&versions2).strip();
535                    Some(DerivationTree::External(External::FromDependencyOf(
536                        package1, versions1, package2, versions2,
537                    )))
538                }
539                External::Custom(package, versions, reason) => {
540                    let versions = SentinelRange::from(&versions).strip();
541                    Some(DerivationTree::External(External::Custom(
542                        package, versions, reason,
543                    )))
544                }
545            },
546            |mut metadata, cause1, cause2| {
547                metadata.terms = metadata
548                    .terms
549                    .into_iter()
550                    .map(|(package, term)| {
551                        let term = match term {
552                            Term::Positive(versions) => {
553                                Term::Positive(SentinelRange::from(&versions).strip())
554                            }
555                            Term::Negative(versions) => {
556                                Term::Negative(SentinelRange::from(&versions).strip())
557                            }
558                        };
559                        (package, term)
560                    })
561                    .collect();
562
563                match (cause1, cause2) {
564                    (Some(cause1), Some(cause2)) => Some(derived_tree(metadata, cause1, cause2)),
565                    (Some(cause), None) | (None, Some(cause)) => Some(cause),
566                    (None, None) => None,
567                }
568            },
569        )
570        .expect("derivation tree should contain at least one term")
571    }
572
573    /// Given a [`DerivationTree`], identify the largest required Python version that is missing.
574    pub fn find_requires_python(&self) -> LowerBound {
575        let mut minimum = LowerBound::default();
576        let mut trees = vec![&*self.error];
577
578        while let Some(derivation_tree) = trees.pop() {
579            match derivation_tree {
580                DerivationTree::Derived(derived) => {
581                    trees.push(&derived.cause2);
582                    trees.push(&derived.cause1);
583                }
584                DerivationTree::External(External::FromDependencyOf(.., package, version)) => {
585                    if let PubGrubPackageInner::Python(_) = &**package {
586                        if let Some((lower, ..)) = version.bounding_range() {
587                            let lower = LowerBound::new(lower.cloned());
588                            if lower > minimum {
589                                minimum = lower;
590                            }
591                        }
592                    }
593                }
594                DerivationTree::External(_) => {}
595            }
596        }
597
598        minimum
599    }
600
601    /// Initialize a [`NoSolutionHeader`] for this error.
602    pub fn header(&self) -> NoSolutionHeader {
603        NoSolutionHeader::new(self.env.clone())
604    }
605
606    /// Get the packages that are involved in this error.
607    pub fn packages(&self) -> impl Iterator<Item = &PackageName> {
608        derivation_tree_packages(&self.error)
609            .filter_map(|p| p.name())
610            .unique()
611    }
612
613    /// Generate the report and hints for this resolution failure.
614    ///
615    /// Returns the formatted report string and structured [`PubGrubHint`] values.
616    /// The result is cached so repeated calls (e.g., from both `Display` and
617    /// explicit hint collection) don't recompute the derivation tree.
618    /// Return the formatted report string.
619    pub fn report(&self) -> &str {
620        &self.cached().0
621    }
622
623    /// Return the computed PubGrub hints.
624    fn pubgrub_hints(&self) -> &IndexSet<PubGrubHint> {
625        &self.cached().1
626    }
627
628    /// Compute the reduced derivation tree, formatted report string, and hints.
629    fn compute_report_and_hints(&self) -> (String, IndexSet<PubGrubHint>) {
630        let formatter = PubGrubReportFormatter {
631            included_versions: &self.included_versions,
632            available_versions: &self.available_versions,
633            python_requirement: &self.python_requirement,
634            workspace_members: &self.workspace_members,
635            tags: self.tags.as_ref(),
636        };
637
638        // Transform the error tree for reporting
639        let mut tree = simplify_derivation_tree_markers(
640            self.error.clone().into_inner(),
641            &self.python_requirement,
642        );
643        let should_display_tree = std::env::var_os(EnvVars::UV_INTERNAL__SHOW_DERIVATION_TREE)
644            .is_some()
645            || tracing::enabled!(tracing::Level::TRACE);
646
647        if should_display_tree {
648            display_tree(&tree, "Resolver derivation tree before reduction");
649        }
650
651        tree = collapse_no_versions_of_workspace_members(tree, &self.workspace_members);
652
653        if self.workspace_members.len() == 1 {
654            let project = self.workspace_members.iter().next().unwrap();
655            tree = drop_root_dependency_on_project(tree, project);
656        }
657
658        tree = collapse_unavailable_versions(tree);
659        tree = collapse_redundant_depends_on_no_versions(tree);
660
661        tree = simplify_derivation_tree_ranges(
662            tree,
663            &self.included_versions,
664            &self.selector,
665            &self.env,
666        );
667
668        // This needs to be applied _after_ simplification of the ranges
669        tree = collapse_redundant_no_versions(tree);
670
671        loop {
672            let (collapsed, changed) = collapse_redundant_no_versions_tree(tree);
673            tree = collapsed;
674            if !changed {
675                break;
676            }
677        }
678
679        if should_display_tree {
680            display_tree(&tree, "Resolver derivation tree after reduction");
681        }
682
683        let report = report_derivation_tree(&tree, &formatter);
684
685        let inherited_exclude_newer_ranges = FxHashMap::default();
686        let mut hints = IndexSet::default();
687        formatter.generate_hints(
688            &tree,
689            &self.index,
690            &self.selector,
691            &self.index_locations,
692            &self.index_capabilities,
693            &self.available_indexes,
694            &self.unavailable_packages,
695            &self.incomplete_packages,
696            &self.fork_urls,
697            &self.fork_indexes,
698            &self.env,
699            &self.current_environment,
700            self.tags.as_ref(),
701            &self.workspace_members,
702            &self.options,
703            &inherited_exclude_newer_ranges,
704            &mut hints,
705        );
706
707        (report, hints)
708    }
709}
710
711impl std::fmt::Debug for NoSolutionError {
712    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
713        // Include every field except `index` (no Debug) and `cached` (derived).
714        let Self {
715            error,
716            index: _,
717            included_versions,
718            available_versions,
719            available_indexes,
720            selector,
721            python_requirement,
722            index_locations,
723            index_capabilities,
724            unavailable_packages,
725            incomplete_packages,
726            fork_urls,
727            fork_indexes,
728            env,
729            current_environment,
730            tags,
731            workspace_members,
732            options,
733            cached: _,
734        } = self;
735        f.debug_struct("NoSolutionError")
736            .field("error", error)
737            .field("included_versions", included_versions)
738            .field("available_versions", available_versions)
739            .field("available_indexes", available_indexes)
740            .field("selector", selector)
741            .field("python_requirement", python_requirement)
742            .field("index_locations", index_locations)
743            .field("index_capabilities", index_capabilities)
744            .field("unavailable_packages", unavailable_packages)
745            .field("incomplete_packages", incomplete_packages)
746            .field("fork_urls", fork_urls)
747            .field("fork_indexes", fork_indexes)
748            .field("env", env)
749            .field("current_environment", current_environment)
750            .field("tags", tags)
751            .field("workspace_members", workspace_members)
752            .field("options", options)
753            .finish()
754    }
755}
756
757impl std::error::Error for NoSolutionError {}
758
759impl uv_errors::Hint for NoSolutionError {
760    fn hints(&self) -> uv_errors::Hints<'_> {
761        self.pubgrub_hints()
762            .iter()
763            .map(ToString::to_string)
764            .collect()
765    }
766}
767
768impl uv_errors::Hint for Box<NoSolutionError> {
769    fn hints(&self) -> uv_errors::Hints<'_> {
770        self.as_ref().hints()
771    }
772}
773
774impl std::fmt::Display for NoSolutionError {
775    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
776        // Write only the derivation report. Hints are available separately
777        // via `hints()` and rendered by the caller.
778        write!(f, "{}", self.report())
779    }
780}
781
782#[expect(clippy::print_stderr)]
783fn display_tree(
784    error: &DerivationTree<PubGrubPackage, Range<Version>, UnavailableReason>,
785    name: &str,
786) {
787    let mut lines = Vec::new();
788    display_tree_inner(error, &mut lines);
789    lines.reverse();
790
791    if std::env::var_os(EnvVars::UV_INTERNAL__SHOW_DERIVATION_TREE).is_some() {
792        eprintln!("{name}\n{}", lines.join("\n"));
793    } else {
794        trace!("{name}\n{}", lines.join("\n"));
795    }
796}
797
798fn display_tree_inner(
799    error: &DerivationTree<PubGrubPackage, Range<Version>, UnavailableReason>,
800    lines: &mut Vec<String>,
801) {
802    enum Frame<'a> {
803        Tree(&'a ErrorTree, usize),
804        Terms(&'a ErrorTerms, usize),
805    }
806
807    let mut frames = vec![Frame::Tree(error, 0)];
808    while let Some(frame) = frames.pop() {
809        match frame {
810            Frame::Tree(DerivationTree::Derived(derived), depth) => {
811                frames.push(Frame::Terms(&derived.terms, depth));
812                frames.push(Frame::Tree(&derived.cause2, depth + 1));
813                frames.push(Frame::Tree(&derived.cause1, depth + 1));
814            }
815            Frame::Tree(DerivationTree::External(external), depth) => {
816                let prefix = "  ".repeat(depth);
817                match external {
818                    External::FromDependencyOf(
819                        package,
820                        version,
821                        dependency,
822                        dependency_version,
823                    ) => {
824                        lines.push(format!(
825                            "{prefix}{package}{version} depends on {dependency}{dependency_version}"
826                        ));
827                    }
828                    External::Custom(package, versions, reason) => match reason {
829                        UnavailableReason::Package(_) => {
830                            lines.push(format!("{prefix}{package} {reason}"));
831                        }
832                        UnavailableReason::Version(_) => {
833                            lines.push(format!("{prefix}{package}{versions} {reason}"));
834                        }
835                    },
836                    External::NoVersions(package, versions) => {
837                        lines.push(format!("{prefix}no versions of {package}{versions}"));
838                    }
839                    External::NotRoot(package, versions) => {
840                        lines.push(format!("{prefix}not root {package}{versions}"));
841                    }
842                }
843            }
844            Frame::Terms(terms, depth) => {
845                let prefix = "  ".repeat(depth);
846                for (package, term) in terms {
847                    match term {
848                        Term::Positive(versions) => {
849                            lines.push(format!("{prefix}term {package}{versions}"));
850                        }
851                        Term::Negative(versions) => {
852                            lines.push(format!("{prefix}term not {package}{versions}"));
853                        }
854                    }
855                }
856            }
857        }
858    }
859}
860
861fn can_drop_no_versions(
862    package: &PubGrubPackage,
863    versions: &Range<Version>,
864    other: &ErrorTree,
865    parent_terms: &ErrorTerms,
866) -> bool {
867    let package_terms = if let DerivationTree::Derived(derived) = other {
868        derived.terms.get(package)
869    } else {
870        parent_terms.get(package)
871    };
872    let Some(Term::Positive(term)) = package_terms else {
873        return false;
874    };
875    let versions = versions.complement();
876
877    // Retain exclusions of a single version because they produce useful messages like
878    // "only foo==1.0.0 is available". Otherwise, the clause is redundant when the conclusion
879    // covers either all versions or exactly the remaining range.
880    versions.as_singleton().is_none() && (*term == Range::full() || *term == versions)
881}
882
883fn collapse_redundant_no_versions(tree: ErrorTree) -> ErrorTree {
884    map_derivation_tree(
885        tree,
886        DerivationTree::External,
887        |metadata, cause1, cause2| {
888            if let DerivationTree::External(External::NoVersions(package, versions)) = &cause1
889                && can_drop_no_versions(package, versions, &cause2, &metadata.terms)
890            {
891                return cause2;
892            }
893
894            if let DerivationTree::External(External::NoVersions(package, versions)) = &cause2
895                && can_drop_no_versions(package, versions, &cause1, &metadata.terms)
896            {
897                return cause1;
898            }
899
900            derived_tree(metadata, cause1, cause2)
901        },
902    )
903}
904
905/// Given a [`DerivationTree`], collapse any derived trees with two `NoVersions` nodes for the same
906/// package. For example, if we have a tree like:
907///
908/// ```text
909/// term Python>=3.7.9
910///   no versions of Python>=3.7.9, <3.8
911///   no versions of Python>=3.8
912/// ```
913///
914/// We can simplify this to:
915///
916/// ```text
917/// no versions of Python>=3.7.9
918/// ```
919///
920/// This function returns a `bool` indicating if a change was made. This allows for repeated calls,
921/// e.g., the following tree contains nested redundant trees:
922///
923/// ```text
924/// term Python>=3.10
925///   no versions of Python>=3.11, <3.12
926///   term Python>=3.10, <3.11 | >=3.12
927///     no versions of Python>=3.12
928///     no versions of Python>=3.10, <3.11
929/// ```
930///
931/// We can simplify this to:
932///
933/// ```text
934/// no versions of Python>=3.10
935/// ```
936///
937/// This appears to be common with the way the resolver currently models Python version
938/// incompatibilities.
939fn collapse_redundant_no_versions_tree(tree: ErrorTree) -> (ErrorTree, bool) {
940    let mut changed = false;
941    let tree = map_derivation_tree(
942        tree,
943        DerivationTree::External,
944        |metadata, cause1, cause2| {
945            if let (
946                DerivationTree::External(External::NoVersions(package, versions)),
947                DerivationTree::External(External::NoVersions(other_package, other_versions)),
948            ) = (&cause1, &cause2)
949                && package == other_package
950                && let Some(Term::Positive(term)) = metadata.terms.get(package)
951                && versions.subset_of(term)
952                && other_versions.subset_of(term)
953            {
954                changed = true;
955                DerivationTree::External(External::NoVersions(package.clone(), term.clone()))
956            } else {
957                derived_tree(metadata, cause1, cause2)
958            }
959        },
960    );
961    (tree, changed)
962}
963
964fn is_workspace_member(
965    package: &PubGrubPackage,
966    workspace_members: &BTreeSet<PackageName>,
967) -> bool {
968    let (PubGrubPackageInner::Package { name, .. }
969    | PubGrubPackageInner::Extra { name, .. }
970    | PubGrubPackageInner::Group { name, .. }) = &**package
971    else {
972        return false;
973    };
974    workspace_members.contains(name)
975}
976
977/// Given a [`DerivationTree`], collapse any `NoVersion` incompatibilities for workspace members
978/// to avoid saying things like "only <workspace-member>==0.1.0 is available".
979fn collapse_no_versions_of_workspace_members(
980    tree: ErrorTree,
981    workspace_members: &BTreeSet<PackageName>,
982) -> ErrorTree {
983    map_derivation_tree(
984        tree,
985        DerivationTree::External,
986        |metadata, cause1, cause2| {
987            if let DerivationTree::External(External::NoVersions(package, _)) = &cause1
988                && is_workspace_member(package, workspace_members)
989            {
990                return cause2;
991            }
992            if let DerivationTree::External(External::NoVersions(package, _)) = &cause2
993                && is_workspace_member(package, workspace_members)
994            {
995                return cause1;
996            }
997            derived_tree(metadata, cause1, cause2)
998        },
999    )
1000}
1001
1002fn collapse_redundant_dependency_child(
1003    tree: ErrorTree,
1004    package: &PubGrubPackage,
1005    versions: &Range<Version>,
1006) -> ErrorTree {
1007    let DerivationTree::Derived(derived) = &tree else {
1008        return tree;
1009    };
1010    let dependency_clause = match (&*derived.cause1, &*derived.cause2) {
1011        (
1012            DerivationTree::External(External::NoVersions(no_versions_package, _)),
1013            dependency_clause @ DerivationTree::External(External::FromDependencyOf(
1014                _,
1015                _,
1016                dependency_package,
1017                dependency_versions,
1018            )),
1019        )
1020        | (
1021            dependency_clause @ DerivationTree::External(External::FromDependencyOf(
1022                _,
1023                _,
1024                dependency_package,
1025                dependency_versions,
1026            )),
1027            DerivationTree::External(External::NoVersions(no_versions_package, _)),
1028        ) if no_versions_package == dependency_package
1029            && package == no_versions_package
1030            && versions.subset_of(dependency_versions) =>
1031        {
1032            Some(dependency_clause.clone())
1033        }
1034        _ => None,
1035    };
1036
1037    dependency_clause.unwrap_or(tree)
1038}
1039
1040/// Given a [`DerivationTree`], collapse `NoVersions` incompatibilities that are redundant children
1041/// of a dependency. For example, if we have a tree like:
1042///
1043/// ```text
1044/// A>=1,<2 depends on B
1045///     A has no versions >1,<2
1046///     C depends on A>=1,<2
1047/// ```
1048///
1049/// We can simplify this to `C depends on A>=1 and A>=1 depends on B so C depends on B` without
1050/// explaining that there are no other versions of A. This requires the range of A in "A depends
1051/// on B" to be a subset of the range in "C depends on A". For example, in a tree like:
1052///
1053/// ```text
1054/// A>=1,<3 depends on B
1055///     A has no versions >2,<3
1056///     C depends on A>=2,<3
1057/// ```
1058///
1059/// We cannot apply the same simplification because `A>=1,<3` is not a subset of `A>=2,<3`.
1060fn collapse_redundant_depends_on_no_versions(tree: ErrorTree) -> ErrorTree {
1061    map_derivation_tree(
1062        tree,
1063        DerivationTree::External,
1064        |metadata, cause1, cause2| {
1065            if let DerivationTree::External(External::FromDependencyOf(package, versions, _, _)) =
1066                &cause1
1067            {
1068                let cause2 = collapse_redundant_dependency_child(cause2, package, versions);
1069                return derived_tree(metadata, cause1, cause2);
1070            }
1071            if let DerivationTree::External(External::FromDependencyOf(package, versions, _, _)) =
1072                &cause2
1073            {
1074                let cause1 = collapse_redundant_dependency_child(cause1, package, versions);
1075                return derived_tree(metadata, cause1, cause2);
1076            }
1077            derived_tree(metadata, cause1, cause2)
1078        },
1079    )
1080}
1081
1082/// Simplifies the markers on pubgrub packages in the given derivation tree
1083/// according to the given Python requirement.
1084///
1085/// For example, when there's a dependency like `foo ; python_version >= '3.11'` and
1086/// `requires-python = '>=3.11'`, this removes the redundant `python_version >= '3.11'` marker from
1087/// the error message.
1088fn simplify_derivation_tree_markers(
1089    tree: ErrorTree,
1090    python_requirement: &PythonRequirement,
1091) -> ErrorTree {
1092    map_derivation_tree(
1093        tree,
1094        |mut external| {
1095            match &mut external {
1096                External::NotRoot(package, _) | External::NoVersions(package, _) => {
1097                    package.simplify_markers(python_requirement);
1098                }
1099                External::FromDependencyOf(package1, _, package2, _) => {
1100                    package1.simplify_markers(python_requirement);
1101                    package2.simplify_markers(python_requirement);
1102                }
1103                External::Custom(package, _, _) => package.simplify_markers(python_requirement),
1104            }
1105            DerivationTree::External(external)
1106        },
1107        |mut metadata, cause1, cause2| {
1108            metadata.terms = metadata
1109                .terms
1110                .into_iter()
1111                .map(|(mut package, term)| {
1112                    package.simplify_markers(python_requirement);
1113                    (package, term)
1114                })
1115                .collect();
1116            derived_tree(metadata, cause1, cause2)
1117        },
1118    )
1119}
1120
1121fn merge_unavailable_versions(
1122    package: &PubGrubPackage,
1123    versions: &Range<Version>,
1124    reason: &UnavailableReason,
1125    other: &ErrorTree,
1126) -> Option<ErrorTree> {
1127    let DerivationTree::Derived(derived) = other else {
1128        return None;
1129    };
1130    let merge = |cause: &ErrorTree| {
1131        let DerivationTree::External(External::Custom(other_package, other_versions, other_reason)) =
1132            cause
1133        else {
1134            return None;
1135        };
1136        (package == other_package && reason == other_reason).then(|| other_versions.union(versions))
1137    };
1138
1139    // Keep the two cases separate to preserve the ordering of the causes.
1140    let (unchanged_cause, merged_versions, merged_is_cause2) =
1141        if let Some(merged_versions) = merge(&derived.cause2) {
1142            (derived.cause1.clone(), merged_versions, true)
1143        } else {
1144            let merged_versions = merge(&derived.cause1)?;
1145            (derived.cause2.clone(), merged_versions, false)
1146        };
1147
1148    let merged_cause = Arc::new(DerivationTree::External(External::Custom(
1149        package.clone(),
1150        merged_versions.clone(),
1151        reason.clone(),
1152    )));
1153    let (cause1, cause2) = if merged_is_cause2 {
1154        (unchanged_cause, merged_cause)
1155    } else {
1156        (merged_cause, unchanged_cause)
1157    };
1158
1159    let mut terms = derived.terms.clone();
1160    if let Some(Term::Positive(range)) = terms.get_mut(package) {
1161        *range = merged_versions;
1162    }
1163    Some(DerivationTree::Derived(Derived {
1164        terms,
1165        shared_id: derived.shared_id,
1166        cause1,
1167        cause2,
1168    }))
1169}
1170
1171/// Given a [`DerivationTree`], collapse incompatibilities for versions of a package that are
1172/// unavailable for the same reason to avoid repeating the same message for every unavailable
1173/// version.
1174fn collapse_unavailable_versions(tree: ErrorTree) -> ErrorTree {
1175    map_derivation_tree(
1176        tree,
1177        DerivationTree::External,
1178        |metadata, cause1, cause2| {
1179            if let DerivationTree::External(External::Custom(package, versions, reason)) = &cause1
1180                && let Some(tree) = merge_unavailable_versions(package, versions, reason, &cause2)
1181            {
1182                return tree;
1183            }
1184            if let DerivationTree::External(External::Custom(package, versions, reason)) = &cause2
1185                && let Some(tree) = merge_unavailable_versions(package, versions, reason, &cause1)
1186            {
1187                return tree;
1188            }
1189            derived_tree(metadata, cause1, cause2)
1190        },
1191    )
1192}
1193
1194fn is_root_dependency_on_project(external: &ErrorExternal, project: &PackageName) -> bool {
1195    let External::FromDependencyOf(package, _, dependency, _) = external else {
1196        return false;
1197    };
1198    if !matches!(&**package, PubGrubPackageInner::Root(_)) {
1199        return false;
1200    }
1201    matches!(&**dependency, PubGrubPackageInner::Package { name, .. } if name == project)
1202}
1203
1204/// Given a [`DerivationTree`], drop dependency incompatibilities from the root to the project.
1205///
1206/// This effectively changes the root to the workspace member in a single-project workspace,
1207/// avoiding an extra level of indirection like "your project requires your project".
1208///
1209/// A direct dependency incompatibility is also a traversal boundary: if it is not the
1210/// root-to-project edge, leave that subtree unchanged. After removing a matching edge, continue
1211/// through only the opposite cause.
1212fn drop_root_dependency_on_project(tree: ErrorTree, project: &PackageName) -> ErrorTree {
1213    let mut tasks = vec![TreeTask::Visit(StackSafeErrorTree::new(tree))];
1214    let mut results = Vec::new();
1215
1216    while let Some(task) = tasks.pop() {
1217        match task {
1218            TreeTask::Visit(tree) => match tree.into_inner() {
1219                DerivationTree::External(external) => {
1220                    results.push(StackSafeErrorTree::new(DerivationTree::External(external)));
1221                }
1222                DerivationTree::Derived(derived) => {
1223                    let first_dependency = match derived.cause1.as_ref() {
1224                        DerivationTree::External(external @ External::FromDependencyOf(..)) => {
1225                            Some((external, true))
1226                        }
1227                        _ => match derived.cause2.as_ref() {
1228                            DerivationTree::External(external @ External::FromDependencyOf(..)) => {
1229                                Some((external, false))
1230                            }
1231                            _ => None,
1232                        },
1233                    };
1234
1235                    if let Some((external, dependency_is_cause1)) = first_dependency {
1236                        if is_root_dependency_on_project(external, project) {
1237                            let other = if dependency_is_cause1 {
1238                                Arc::unwrap_or_clone(derived.cause2)
1239                            } else {
1240                                Arc::unwrap_or_clone(derived.cause1)
1241                            };
1242                            tasks.push(TreeTask::Visit(StackSafeErrorTree::new(other)));
1243                        } else {
1244                            results.push(StackSafeErrorTree::new(DerivationTree::Derived(derived)));
1245                        }
1246                    } else {
1247                        schedule_derived(&mut tasks, derived);
1248                    }
1249                }
1250            },
1251            TreeTask::Rebuild(metadata) => {
1252                let cause2 = results
1253                    .pop()
1254                    .expect("every derived tree has a second reduced cause")
1255                    .into_inner();
1256                let cause1 = results
1257                    .pop()
1258                    .expect("every derived tree has a first reduced cause")
1259                    .into_inner();
1260                results.push(StackSafeErrorTree::new(derived_tree(
1261                    metadata, cause1, cause2,
1262                )));
1263            }
1264        }
1265    }
1266
1267    results
1268        .pop()
1269        .expect("the root derivation tree produces one reduced result")
1270        .into_inner()
1271}
1272
1273/// A version range that may include local version sentinels (`+[max]`).
1274#[derive(Debug)]
1275pub struct SentinelRange<'range>(&'range Range<Version>);
1276
1277impl<'range> From<&'range Range<Version>> for SentinelRange<'range> {
1278    fn from(range: &'range Range<Version>) -> Self {
1279        Self(range)
1280    }
1281}
1282
1283impl SentinelRange<'_> {
1284    /// Returns `true` if the range appears to be, e.g., `>=1.0.0, <1.0.0+[max]`.
1285    pub(crate) fn is_sentinel(&self) -> bool {
1286        self.0.iter().all(|(lower, upper)| {
1287            let (Bound::Included(lower), Bound::Excluded(upper)) = (lower, upper) else {
1288                return false;
1289            };
1290            if !lower.local().is_empty() {
1291                return false;
1292            }
1293            if upper.local() != LocalVersionSlice::Max {
1294                return false;
1295            }
1296            *lower == upper.clone().without_local()
1297        })
1298    }
1299
1300    /// Returns `true` if the range appears to be, e.g., `>1.0.0, <1.0.0+[max]` (i.e., a sentinel
1301    /// range with the non-local version removed).
1302    fn is_complement(&self) -> bool {
1303        self.0.iter().all(|(lower, upper)| {
1304            let (Bound::Excluded(lower), Bound::Excluded(upper)) = (lower, upper) else {
1305                return false;
1306            };
1307            if !lower.local().is_empty() {
1308                return false;
1309            }
1310            if upper.local() != LocalVersionSlice::Max {
1311                return false;
1312            }
1313            *lower == upper.clone().without_local()
1314        })
1315    }
1316
1317    /// Remove local versions sentinels (`+[max]`) from the version ranges.
1318    pub fn strip(&self) -> Ranges<Version> {
1319        self.0
1320            .iter()
1321            .map(|(lower, upper)| Self::strip_sentinel(lower.cloned(), upper.cloned()))
1322            .collect()
1323    }
1324
1325    /// Remove local versions sentinels (`+[max]`) from the interval.
1326    fn strip_sentinel(
1327        mut lower: Bound<Version>,
1328        mut upper: Bound<Version>,
1329    ) -> (Bound<Version>, Bound<Version>) {
1330        match (&lower, &upper) {
1331            (Bound::Unbounded, Bound::Unbounded) => {}
1332            (Bound::Unbounded, Bound::Included(v)) => {
1333                // `<=1.0.0+[max]` is equivalent to `<=1.0.0`
1334                if v.local() == LocalVersionSlice::Max {
1335                    upper = Bound::Included(v.clone().without_local());
1336                }
1337            }
1338            (Bound::Unbounded, Bound::Excluded(v)) => {
1339                // `<1.0.0+[max]` is equivalent to `<1.0.0`
1340                if v.local() == LocalVersionSlice::Max {
1341                    upper = Bound::Excluded(v.clone().without_local());
1342                }
1343            }
1344            (Bound::Included(v), Bound::Unbounded) => {
1345                // `>=1.0.0+[max]` is equivalent to `>1.0.0`
1346                if v.local() == LocalVersionSlice::Max {
1347                    lower = Bound::Excluded(v.clone().without_local());
1348                }
1349            }
1350            (Bound::Included(v), Bound::Included(b)) => {
1351                // `>=1.0.0+[max]` is equivalent to `>1.0.0`
1352                if v.local() == LocalVersionSlice::Max {
1353                    lower = Bound::Excluded(v.clone().without_local());
1354                }
1355                // `<=1.0.0+[max]` is equivalent to `<=1.0.0`
1356                if b.local() == LocalVersionSlice::Max {
1357                    upper = Bound::Included(b.clone().without_local());
1358                }
1359            }
1360            (Bound::Included(v), Bound::Excluded(b)) => {
1361                // `>=1.0.0+[max]` is equivalent to `>1.0.0`
1362                if v.local() == LocalVersionSlice::Max {
1363                    lower = Bound::Excluded(v.clone().without_local());
1364                }
1365                // `<1.0.0+[max]` is equivalent to `<1.0.0`
1366                if b.local() == LocalVersionSlice::Max {
1367                    upper = Bound::Included(b.clone().without_local());
1368                }
1369            }
1370            (Bound::Excluded(v), Bound::Unbounded) => {
1371                // `>1.0.0+[max]` is equivalent to `>1.0.0`
1372                if v.local() == LocalVersionSlice::Max {
1373                    lower = Bound::Excluded(v.clone().without_local());
1374                }
1375            }
1376            (Bound::Excluded(v), Bound::Included(b)) => {
1377                // `>1.0.0+[max]` is equivalent to `>1.0.0`
1378                if v.local() == LocalVersionSlice::Max {
1379                    lower = Bound::Excluded(v.clone().without_local());
1380                }
1381                // `<=1.0.0+[max]` is equivalent to `<=1.0.0`
1382                if b.local() == LocalVersionSlice::Max {
1383                    upper = Bound::Included(b.clone().without_local());
1384                }
1385            }
1386            (Bound::Excluded(v), Bound::Excluded(b)) => {
1387                // `>1.0.0+[max]` is equivalent to `>1.0.0`
1388                if v.local() == LocalVersionSlice::Max {
1389                    lower = Bound::Excluded(v.clone().without_local());
1390                }
1391                // `<1.0.0+[max]` is equivalent to `<1.0.0`
1392                if b.local() == LocalVersionSlice::Max {
1393                    upper = Bound::Excluded(b.clone().without_local());
1394                }
1395            }
1396        }
1397        (lower, upper)
1398    }
1399}
1400
1401/// A prefix match, e.g., `==2.4.*`, which is desugared to a range like `>=2.4.dev0,<2.5.dev0`.
1402#[derive(Debug, Clone, PartialEq, Eq)]
1403pub(crate) struct PrefixMatch<'a> {
1404    version: &'a Version,
1405}
1406
1407impl<'a> PrefixMatch<'a> {
1408    /// Determine whether a given range is equivalent to a prefix match (e.g., `==2.4.*`).
1409    ///
1410    /// Prefix matches are desugared to (e.g.) `>=2.4.dev0,<2.5.dev0`, but we want to render them
1411    /// as `==2.4.*` in error messages.
1412    pub(crate) fn from_range(lower: Bound<&'a Version>, upper: Bound<&'a Version>) -> Option<Self> {
1413        let Bound::Included(lower) = lower else {
1414            return None;
1415        };
1416        let Bound::Excluded(upper) = upper else {
1417            return None;
1418        };
1419        if lower.is_pre() || lower.is_post() || lower.is_local() {
1420            return None;
1421        }
1422        if upper.is_pre() || upper.is_post() || upper.is_local() {
1423            return None;
1424        }
1425        if lower.dev() != Some(0) {
1426            return None;
1427        }
1428        if upper.dev() != Some(0) {
1429            return None;
1430        }
1431        if lower.release().len() != upper.release().len() {
1432            return None;
1433        }
1434
1435        // All segments should be the same, except the last one, which should be incremented.
1436        let num_segments = lower.release().len();
1437        for (i, (lower, upper)) in lower
1438            .release()
1439            .iter()
1440            .zip(upper.release().iter())
1441            .enumerate()
1442        {
1443            if i == num_segments - 1 {
1444                if lower + 1 != *upper {
1445                    return None;
1446                }
1447            } else {
1448                if lower != upper {
1449                    return None;
1450                }
1451            }
1452        }
1453
1454        Some(PrefixMatch { version: lower })
1455    }
1456}
1457
1458impl std::fmt::Display for PrefixMatch<'_> {
1459    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1460        write!(f, "=={}.*", self.version.only_release())
1461    }
1462}
1463
1464#[derive(Debug)]
1465pub struct NoSolutionHeader {
1466    /// The [`ResolverEnvironment`] that caused the failure.
1467    env: ResolverEnvironment,
1468    /// The additional context for the resolution failure.
1469    context: Option<&'static str>,
1470}
1471
1472impl NoSolutionHeader {
1473    /// Create a new [`NoSolutionHeader`] with the given [`ResolverEnvironment`].
1474    fn new(env: ResolverEnvironment) -> Self {
1475        Self { env, context: None }
1476    }
1477
1478    /// Set the context for the resolution failure.
1479    #[must_use]
1480    pub fn with_context(mut self, context: &'static str) -> Self {
1481        self.context = Some(context);
1482        self
1483    }
1484}
1485
1486impl std::fmt::Display for NoSolutionHeader {
1487    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1488        match (self.context, self.env.end_user_fork_display()) {
1489            (None, None) => write!(f, "No solution found when resolving dependencies:"),
1490            (Some(context), None) => write!(
1491                f,
1492                "No solution found when resolving {context} dependencies:"
1493            ),
1494            (None, Some(split)) => write!(
1495                f,
1496                "No solution found when resolving dependencies for {split}:"
1497            ),
1498            (Some(context), Some(split)) => write!(
1499                f,
1500                "No solution found when resolving {context} dependencies for {split}:"
1501            ),
1502        }
1503    }
1504}
1505
1506/// Given a [`DerivationTree`], simplify version ranges using the included versions for each
1507/// package.
1508fn simplify_derivation_tree_ranges(
1509    tree: ErrorTree,
1510    included_versions: &FxHashMap<PackageName, BTreeSet<Version>>,
1511    candidate_selector: &CandidateSelector,
1512    resolver_environment: &ResolverEnvironment,
1513) -> ErrorTree {
1514    map_derivation_tree(
1515        tree,
1516        |mut external| {
1517            match &mut external {
1518                External::FromDependencyOf(package1, versions1, package2, versions2) => {
1519                    if let Some(simplified) = simplify_range(
1520                        versions1,
1521                        package1,
1522                        included_versions,
1523                        candidate_selector,
1524                        resolver_environment,
1525                    ) {
1526                        *versions1 = simplified;
1527                    }
1528                    if let Some(simplified) = simplify_range(
1529                        versions2,
1530                        package2,
1531                        included_versions,
1532                        candidate_selector,
1533                        resolver_environment,
1534                    ) {
1535                        *versions2 = simplified;
1536                    }
1537                }
1538                External::NoVersions(package, versions) => {
1539                    if let Some(simplified) = simplify_range(
1540                        versions,
1541                        package,
1542                        included_versions,
1543                        candidate_selector,
1544                        resolver_environment,
1545                    ) {
1546                        *versions = simplified;
1547                    }
1548                }
1549                External::Custom(package, versions, _) => {
1550                    if let Some(simplified) = simplify_range(
1551                        versions,
1552                        package,
1553                        included_versions,
1554                        candidate_selector,
1555                        resolver_environment,
1556                    ) {
1557                        *versions = simplified;
1558                    }
1559                }
1560                External::NotRoot(..) => {}
1561            }
1562            DerivationTree::External(external)
1563        },
1564        |mut metadata, cause1, cause2| {
1565            metadata.terms = metadata
1566                .terms
1567                .into_iter()
1568                .map(|(package, term)| {
1569                    let term = match term {
1570                        Term::Positive(versions) => Term::Positive(
1571                            simplify_range(
1572                                &versions,
1573                                &package,
1574                                included_versions,
1575                                candidate_selector,
1576                                resolver_environment,
1577                            )
1578                            .unwrap_or(versions),
1579                        ),
1580                        Term::Negative(versions) => Term::Negative(
1581                            simplify_range(
1582                                &versions,
1583                                &package,
1584                                included_versions,
1585                                candidate_selector,
1586                                resolver_environment,
1587                            )
1588                            .unwrap_or(versions),
1589                        ),
1590                    };
1591                    (package, term)
1592                })
1593                .collect();
1594            derived_tree(metadata, cause1, cause2)
1595        },
1596    )
1597}
1598
1599/// Helper function to simplify a version range using included versions for a package.
1600///
1601/// If the range cannot be simplified, `None` is returned.
1602fn simplify_range(
1603    range: &Range<Version>,
1604    package: &PubGrubPackage,
1605    included_versions: &FxHashMap<PackageName, BTreeSet<Version>>,
1606    candidate_selector: &CandidateSelector,
1607    resolver_environment: &ResolverEnvironment,
1608) -> Option<Range<Version>> {
1609    // If there's not a package name or included versions, we can't simplify anything
1610    let name = package.name()?;
1611    let versions = included_versions.get(name)?;
1612
1613    // If this is a full range, there's nothing to simplify
1614    if range == &Range::full() {
1615        return None;
1616    }
1617
1618    // If there's only one version available and it's in the range, return just that version
1619    if let Some(version) = versions.iter().next() {
1620        if versions.len() == 1 && range.contains(version) {
1621            return Some(Range::singleton(version.clone()));
1622        }
1623    }
1624
1625    // Check if pre-releases are allowed
1626    let prereleases_not_allowed = candidate_selector
1627        .prerelease_strategy()
1628        .allows(name, resolver_environment)
1629        != AllowPrerelease::Yes;
1630
1631    let any_prerelease = range.iter().any(|(start, end)| {
1632        let is_pre1 = match start {
1633            Bound::Included(version) => version.any_prerelease(),
1634            Bound::Excluded(version) => version.any_prerelease(),
1635            Bound::Unbounded => false,
1636        };
1637        let is_pre2 = match end {
1638            Bound::Included(version) => version.any_prerelease(),
1639            Bound::Excluded(version) => version.any_prerelease(),
1640            Bound::Unbounded => false,
1641        };
1642        is_pre1 || is_pre2
1643    });
1644
1645    // Simplify the range, as implemented in PubGrub
1646    Some(range.simplify(versions.iter().filter(|version| {
1647        // If there are pre-releases in the range segments, we need to include pre-releases
1648        if any_prerelease {
1649            return true;
1650        }
1651
1652        // If pre-releases are not allowed, filter out pre-releases
1653        if prereleases_not_allowed && version.any_prerelease() {
1654            return false;
1655        }
1656
1657        // Otherwise, include the version
1658        true
1659    })))
1660}
1661
1662#[cfg(test)]
1663mod tests {
1664    use super::*;
1665
1666    fn deep_derivation_tree() -> ErrorTree {
1667        let package = PubGrubPackage::from(PubGrubPackageInner::Root(None));
1668        let leaf = ErrorTree::External(External::NotRoot(package, Version::new([1_u64])));
1669        let mut tree = leaf.clone();
1670
1671        for _ in 0..100_000 {
1672            tree = ErrorTree::Derived(Derived {
1673                terms: pubgrub::Map::default(),
1674                shared_id: None,
1675                cause1: Arc::new(tree),
1676                cause2: Arc::new(leaf.clone()),
1677            });
1678        }
1679
1680        tree
1681    }
1682
1683    #[test]
1684    fn drops_transformed_derivation_tree_without_recursion() -> std::io::Result<()> {
1685        let thread = std::thread::Builder::new()
1686            .stack_size(256 * 1024)
1687            .spawn(|| {
1688                let _tree = StackSafeErrorTree::new(deep_derivation_tree());
1689            })?;
1690
1691        assert!(thread.join().is_ok());
1692        Ok(())
1693    }
1694
1695    #[test]
1696    fn derivation_tree_packages_are_unique() {
1697        let tree = StackSafeErrorTree::new(deep_derivation_tree());
1698        assert_eq!(derivation_tree_packages(&tree).count(), 1);
1699    }
1700
1701    #[test]
1702    fn collapse_proxies_drops_source_tree_without_recursion() -> std::io::Result<()> {
1703        let thread = std::thread::Builder::new()
1704            .stack_size(256 * 1024)
1705            .spawn(|| {
1706                let tree = NoSolutionError::collapse_proxies(deep_derivation_tree());
1707                drop_derivation_tree(tree);
1708            })?;
1709
1710        assert!(thread.join().is_ok());
1711        Ok(())
1712    }
1713
1714    #[test]
1715    fn collapse_local_versions_drops_source_tree_without_recursion() -> std::io::Result<()> {
1716        let thread = std::thread::Builder::new()
1717            .stack_size(256 * 1024)
1718            .spawn(|| {
1719                let tree = NoSolutionError::collapse_local_version_segments(deep_derivation_tree());
1720                drop_derivation_tree(tree);
1721            })?;
1722
1723        assert!(thread.join().is_ok());
1724        Ok(())
1725    }
1726
1727    #[test]
1728    fn formats_debug_derivation_tree_without_recursion() -> std::io::Result<()> {
1729        let thread = std::thread::Builder::new()
1730            .stack_size(256 * 1024)
1731            .spawn(|| {
1732                let tree = StackSafeErrorTree::new(deep_derivation_tree());
1733                let _formatted = format!("{tree:?}");
1734            })?;
1735
1736        assert!(thread.join().is_ok());
1737        Ok(())
1738    }
1739
1740    #[test]
1741    fn iterative_debug_matches_pubgrub_debug() {
1742        let package = PubGrubPackage::from(PubGrubPackageInner::Root(None));
1743        let leaf = ErrorTree::External(External::NotRoot(package, Version::new([1_u64])));
1744        let tree = StackSafeErrorTree::new(ErrorTree::Derived(Derived {
1745            terms: pubgrub::Map::default(),
1746            shared_id: Some(1),
1747            cause1: Arc::new(leaf.clone()),
1748            cause2: Arc::new(leaf),
1749        }));
1750
1751        let inner = &*tree;
1752        assert_eq!(format!("{inner:?}"), format!("{tree:?}"));
1753    }
1754}