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