uv_resolver/
error.rs

1use std::collections::{BTreeMap, BTreeSet, Bound};
2use std::fmt::Formatter;
3use std::sync::Arc;
4
5use indexmap::IndexSet;
6use itertools::Itertools;
7use owo_colors::OwoColorize;
8use pubgrub::{
9    DefaultStringReporter, DerivationTree, Derived, External, Range, Ranges, Reporter, Term,
10};
11use rustc_hash::FxHashMap;
12use tracing::trace;
13
14use uv_distribution_types::{
15    DerivationChain, DistErrorKind, IndexCapabilities, IndexLocations, IndexUrl, RequestedDist,
16};
17use uv_normalize::{ExtraName, InvalidNameError, PackageName};
18use uv_pep440::{LocalVersionSlice, LowerBound, Version, VersionSpecifier};
19use uv_pep508::{MarkerEnvironment, MarkerExpression, MarkerTree, MarkerValueVersion};
20use uv_platform_tags::Tags;
21use uv_pypi_types::ParsedUrl;
22use uv_redacted::DisplaySafeUrl;
23use uv_static::EnvVars;
24
25use crate::candidate_selector::CandidateSelector;
26use crate::dependency_provider::UvDependencyProvider;
27use crate::fork_indexes::ForkIndexes;
28use crate::fork_urls::ForkUrls;
29use crate::prerelease::AllowPrerelease;
30use crate::pubgrub::{PubGrubPackage, PubGrubPackageInner, PubGrubReportFormatter};
31use crate::python_requirement::PythonRequirement;
32use crate::resolution::ConflictingDistributionError;
33use crate::resolver::{
34    MetadataUnavailable, ResolverEnvironment, UnavailablePackage, UnavailableReason,
35};
36use crate::{InMemoryIndex, Options};
37
38#[derive(Debug, thiserror::Error)]
39pub enum ResolveError {
40    #[error("Failed to resolve dependencies for package `{1}=={2}`")]
41    Dependencies(
42        #[source] Box<ResolveError>,
43        PackageName,
44        Version,
45        DerivationChain,
46    ),
47
48    #[error(transparent)]
49    Client(#[from] uv_client::Error),
50
51    #[error(transparent)]
52    Distribution(#[from] uv_distribution::Error),
53
54    #[error("The channel closed unexpectedly")]
55    ChannelClosed,
56
57    #[error(transparent)]
58    Join(#[from] tokio::task::JoinError),
59
60    #[error("Attempted to wait on an unregistered task: `{_0}`")]
61    UnregisteredTask(String),
62
63    #[error(
64        "Requirements contain conflicting URLs for package `{package_name}`{}:\n- {}",
65        if env.marker_environment().is_some() {
66            String::new()
67        } else {
68            format!(" in {env}")
69        },
70        urls.iter()
71            .map(|url| format!("{}{}", DisplaySafeUrl::from(url.clone()), if url.is_editable() { " (editable)" } else { "" }))
72            .collect::<Vec<_>>()
73            .join("\n- ")
74    )]
75    ConflictingUrls {
76        package_name: PackageName,
77        urls: Vec<ParsedUrl>,
78        env: ResolverEnvironment,
79    },
80
81    #[error(
82        "Requirements contain conflicting indexes for package `{package_name}`{}:\n- {}",
83        if env.marker_environment().is_some() {
84            String::new()
85        } else {
86            format!(" in {env}")
87        },
88        indexes.iter()
89            .map(std::string::ToString::to_string)
90            .collect::<Vec<_>>()
91            .join("\n- ")
92    )]
93    ConflictingIndexesForEnvironment {
94        package_name: PackageName,
95        indexes: Vec<IndexUrl>,
96        env: ResolverEnvironment,
97    },
98
99    #[error("Requirements contain conflicting indexes for package `{0}`: `{1}` vs. `{2}`")]
100    ConflictingIndexes(PackageName, String, String),
101
102    #[error(
103        "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.",
104        name = name.cyan(),
105        requirement = format!("{name} @ {url}").cyan(),
106    )]
107    DisallowedUrl { name: PackageName, url: String },
108
109    #[error(transparent)]
110    DistributionType(#[from] uv_distribution_types::Error),
111
112    #[error(transparent)]
113    ParsedUrl(#[from] uv_pypi_types::ParsedUrlError),
114
115    #[error("{0} `{1}`")]
116    Dist(
117        DistErrorKind,
118        Box<RequestedDist>,
119        DerivationChain,
120        #[source] Arc<uv_distribution::Error>,
121    ),
122
123    #[error(transparent)]
124    NoSolution(#[from] Box<NoSolutionError>),
125
126    #[error("Attempted to construct an invalid version specifier")]
127    InvalidVersion(#[from] uv_pep440::VersionSpecifierBuildError),
128
129    #[error(
130        "In `--require-hashes` mode, all requirements must be pinned upfront with `==`, but found: `{0}`"
131    )]
132    UnhashedPackage(PackageName),
133
134    #[error("found conflicting distribution in resolution: {0}")]
135    ConflictingDistribution(ConflictingDistributionError),
136
137    #[error("Package `{0}` is unavailable")]
138    PackageUnavailable(PackageName),
139
140    #[error("Invalid extra value in conflict marker: {reason}: {raw_extra}")]
141    InvalidExtraInConflictMarker {
142        reason: String,
143        raw_extra: ExtraName,
144    },
145
146    #[error("Invalid {kind} value in conflict marker: {name_error}")]
147    InvalidValueInConflictMarker {
148        kind: &'static str,
149        #[source]
150        name_error: InvalidNameError,
151    },
152    #[error(
153        "The index returned metadata for the wrong package: expected {request} for {expected}, got {request} for {actual}"
154    )]
155    MismatchedPackageName {
156        request: &'static str,
157        expected: PackageName,
158        actual: PackageName,
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>;
171
172/// A wrapper around [`pubgrub::error::NoSolutionError`] that displays a resolution failure report.
173pub struct NoSolutionError {
174    error: pubgrub::NoSolutionError<UvDependencyProvider>,
175    index: InMemoryIndex,
176    available_versions: FxHashMap<PackageName, BTreeSet<Version>>,
177    available_indexes: FxHashMap<PackageName, BTreeSet<IndexUrl>>,
178    selector: CandidateSelector,
179    python_requirement: PythonRequirement,
180    index_locations: IndexLocations,
181    index_capabilities: IndexCapabilities,
182    unavailable_packages: FxHashMap<PackageName, UnavailablePackage>,
183    incomplete_packages: FxHashMap<PackageName, BTreeMap<Version, MetadataUnavailable>>,
184    fork_urls: ForkUrls,
185    fork_indexes: ForkIndexes,
186    env: ResolverEnvironment,
187    current_environment: MarkerEnvironment,
188    tags: Option<Tags>,
189    workspace_members: BTreeSet<PackageName>,
190    options: Options,
191}
192
193impl NoSolutionError {
194    /// Create a new [`NoSolutionError`] from a [`pubgrub::NoSolutionError`].
195    pub(crate) fn new(
196        error: pubgrub::NoSolutionError<UvDependencyProvider>,
197        index: InMemoryIndex,
198        available_versions: FxHashMap<PackageName, BTreeSet<Version>>,
199        available_indexes: FxHashMap<PackageName, BTreeSet<IndexUrl>>,
200        selector: CandidateSelector,
201        python_requirement: PythonRequirement,
202        index_locations: IndexLocations,
203        index_capabilities: IndexCapabilities,
204        unavailable_packages: FxHashMap<PackageName, UnavailablePackage>,
205        incomplete_packages: FxHashMap<PackageName, BTreeMap<Version, MetadataUnavailable>>,
206        fork_urls: ForkUrls,
207        fork_indexes: ForkIndexes,
208        env: ResolverEnvironment,
209        current_environment: MarkerEnvironment,
210        tags: Option<Tags>,
211        workspace_members: BTreeSet<PackageName>,
212        options: Options,
213    ) -> Self {
214        Self {
215            error,
216            index,
217            available_versions,
218            available_indexes,
219            selector,
220            python_requirement,
221            index_locations,
222            index_capabilities,
223            unavailable_packages,
224            incomplete_packages,
225            fork_urls,
226            fork_indexes,
227            env,
228            current_environment,
229            tags,
230            workspace_members,
231            options,
232        }
233    }
234
235    /// Given a [`DerivationTree`], collapse any [`External::FromDependencyOf`] incompatibilities
236    /// wrap an [`PubGrubPackageInner::Extra`] package.
237    pub(crate) fn collapse_proxies(derivation_tree: ErrorTree) -> ErrorTree {
238        fn collapse(derivation_tree: ErrorTree) -> Option<ErrorTree> {
239            match derivation_tree {
240                DerivationTree::Derived(derived) => {
241                    match (&*derived.cause1, &*derived.cause2) {
242                        (
243                            DerivationTree::External(External::FromDependencyOf(package1, ..)),
244                            DerivationTree::External(External::FromDependencyOf(package2, ..)),
245                        ) if package1.is_proxy() && package2.is_proxy() => None,
246                        (
247                            DerivationTree::External(External::FromDependencyOf(package, ..)),
248                            cause,
249                        ) if package.is_proxy() => collapse(cause.clone()),
250                        (
251                            cause,
252                            DerivationTree::External(External::FromDependencyOf(package, ..)),
253                        ) if package.is_proxy() => collapse(cause.clone()),
254                        (cause1, cause2) => {
255                            let cause1 = collapse(cause1.clone());
256                            let cause2 = collapse(cause2.clone());
257                            match (cause1, cause2) {
258                                (Some(cause1), Some(cause2)) => {
259                                    Some(DerivationTree::Derived(Derived {
260                                        cause1: Arc::new(cause1),
261                                        cause2: Arc::new(cause2),
262                                        ..derived
263                                    }))
264                                }
265                                (Some(cause), None) | (None, Some(cause)) => Some(cause),
266                                _ => None,
267                            }
268                        }
269                    }
270                }
271                DerivationTree::External(_) => Some(derivation_tree),
272            }
273        }
274
275        collapse(derivation_tree)
276            .expect("derivation tree should contain at least one external term")
277    }
278
279    /// Simplifies the version ranges on any incompatibilities to remove the `[max]` sentinel.
280    ///
281    /// The `[max]` sentinel is used to represent the maximum local version of a package, to
282    /// implement PEP 440 semantics for local version equality. For example, `1.0.0+foo` needs to
283    /// satisfy `==1.0.0`.
284    pub(crate) fn collapse_local_version_segments(derivation_tree: ErrorTree) -> ErrorTree {
285        fn strip(derivation_tree: ErrorTree) -> Option<ErrorTree> {
286            match derivation_tree {
287                DerivationTree::External(External::NotRoot(_, _)) => Some(derivation_tree),
288                DerivationTree::External(External::NoVersions(package, versions)) => {
289                    if SentinelRange::from(&versions).is_complement() {
290                        return None;
291                    }
292
293                    let versions = SentinelRange::from(&versions).strip();
294                    Some(DerivationTree::External(External::NoVersions(
295                        package, versions,
296                    )))
297                }
298                DerivationTree::External(External::FromDependencyOf(
299                    package1,
300                    versions1,
301                    package2,
302                    versions2,
303                )) => {
304                    let versions1 = SentinelRange::from(&versions1).strip();
305                    let versions2 = SentinelRange::from(&versions2).strip();
306                    Some(DerivationTree::External(External::FromDependencyOf(
307                        package1, versions1, package2, versions2,
308                    )))
309                }
310                DerivationTree::External(External::Custom(package, versions, reason)) => {
311                    let versions = SentinelRange::from(&versions).strip();
312                    Some(DerivationTree::External(External::Custom(
313                        package, versions, reason,
314                    )))
315                }
316                DerivationTree::Derived(mut derived) => {
317                    let cause1 = strip((*derived.cause1).clone());
318                    let cause2 = strip((*derived.cause2).clone());
319                    match (cause1, cause2) {
320                        (Some(cause1), Some(cause2)) => Some(DerivationTree::Derived(Derived {
321                            cause1: Arc::new(cause1),
322                            cause2: Arc::new(cause2),
323                            terms: std::mem::take(&mut derived.terms)
324                                .into_iter()
325                                .map(|(pkg, term)| {
326                                    let term = match term {
327                                        Term::Positive(versions) => {
328                                            Term::Positive(SentinelRange::from(&versions).strip())
329                                        }
330                                        Term::Negative(versions) => {
331                                            Term::Negative(SentinelRange::from(&versions).strip())
332                                        }
333                                    };
334                                    (pkg, term)
335                                })
336                                .collect(),
337                            shared_id: derived.shared_id,
338                        })),
339                        (Some(cause), None) | (None, Some(cause)) => Some(cause),
340                        _ => None,
341                    }
342                }
343            }
344        }
345
346        strip(derivation_tree).expect("derivation tree should contain at least one term")
347    }
348
349    /// Given a [`DerivationTree`], identify the largest required Python version that is missing.
350    pub fn find_requires_python(&self) -> LowerBound {
351        fn find(derivation_tree: &ErrorTree, minimum: &mut LowerBound) {
352            match derivation_tree {
353                DerivationTree::Derived(derived) => {
354                    find(derived.cause1.as_ref(), minimum);
355                    find(derived.cause2.as_ref(), minimum);
356                }
357                DerivationTree::External(External::FromDependencyOf(.., package, version)) => {
358                    if let PubGrubPackageInner::Python(_) = &**package {
359                        if let Some((lower, ..)) = version.bounding_range() {
360                            let lower = LowerBound::new(lower.cloned());
361                            if lower > *minimum {
362                                *minimum = lower;
363                            }
364                        }
365                    }
366                }
367                DerivationTree::External(_) => {}
368            }
369        }
370
371        let mut minimum = LowerBound::default();
372        find(&self.error, &mut minimum);
373        minimum
374    }
375
376    /// Initialize a [`NoSolutionHeader`] for this error.
377    pub fn header(&self) -> NoSolutionHeader {
378        NoSolutionHeader::new(self.env.clone())
379    }
380
381    /// Get the conflict derivation tree for external analysis
382    pub fn derivation_tree(&self) -> &ErrorTree {
383        &self.error
384    }
385
386    /// Hint at limiting the resolver environment if universal resolution failed for a target
387    /// that is not the current platform or not the current Python version.
388    fn hint_disjoint_targets(&self, f: &mut Formatter) -> std::fmt::Result {
389        // Only applicable to universal resolution.
390        let Some(markers) = self.env.fork_markers() else {
391            return Ok(());
392        };
393
394        // TODO(konsti): This is a crude approximation to telling the user the difference
395        // between their Python version and the relevant Python version range from the marker.
396        let current_python_version = self.current_environment.python_version().version.clone();
397        let current_python_marker = MarkerTree::expression(MarkerExpression::Version {
398            key: MarkerValueVersion::PythonVersion,
399            specifier: VersionSpecifier::equals_version(current_python_version.clone()),
400        });
401        if markers.is_disjoint(current_python_marker) {
402            write!(
403                f,
404                "\n\n{}{} While the active Python version is {}, \
405                the resolution failed for other Python versions supported by your \
406                project. Consider limiting your project's supported Python versions \
407                using `requires-python`.",
408                "hint".bold().cyan(),
409                ":".bold(),
410                current_python_version,
411            )?;
412        } else if !markers.evaluate(&self.current_environment, &[]) {
413            write!(
414                f,
415                "\n\n{}{} The resolution failed for an environment that is not the current one, \
416                consider limiting the environments with `tool.uv.environments`.",
417                "hint".bold().cyan(),
418                ":".bold(),
419            )?;
420        }
421        Ok(())
422    }
423
424    /// Get the packages that are involved in this error.
425    pub fn packages(&self) -> impl Iterator<Item = &PackageName> {
426        self.error
427            .packages()
428            .into_iter()
429            .filter_map(|p| p.name())
430            .unique()
431    }
432}
433
434impl std::fmt::Debug for NoSolutionError {
435    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
436        // Include every field except `index`, which doesn't implement `Debug`.
437        let Self {
438            error,
439            index: _,
440            available_versions,
441            available_indexes,
442            selector,
443            python_requirement,
444            index_locations,
445            index_capabilities,
446            unavailable_packages,
447            incomplete_packages,
448            fork_urls,
449            fork_indexes,
450            env,
451            current_environment,
452            tags,
453            workspace_members,
454            options,
455        } = self;
456        f.debug_struct("NoSolutionError")
457            .field("error", error)
458            .field("available_versions", available_versions)
459            .field("available_indexes", available_indexes)
460            .field("selector", selector)
461            .field("python_requirement", python_requirement)
462            .field("index_locations", index_locations)
463            .field("index_capabilities", index_capabilities)
464            .field("unavailable_packages", unavailable_packages)
465            .field("incomplete_packages", incomplete_packages)
466            .field("fork_urls", fork_urls)
467            .field("fork_indexes", fork_indexes)
468            .field("env", env)
469            .field("current_environment", current_environment)
470            .field("tags", tags)
471            .field("workspace_members", workspace_members)
472            .field("options", options)
473            .finish()
474    }
475}
476
477impl std::error::Error for NoSolutionError {}
478
479impl std::fmt::Display for NoSolutionError {
480    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
481        // Write the derivation report.
482        let formatter = PubGrubReportFormatter {
483            available_versions: &self.available_versions,
484            python_requirement: &self.python_requirement,
485            workspace_members: &self.workspace_members,
486            tags: self.tags.as_ref(),
487        };
488
489        // Transform the error tree for reporting
490        let mut tree = self.error.clone();
491        simplify_derivation_tree_markers(&self.python_requirement, &mut tree);
492        let should_display_tree = std::env::var_os(EnvVars::UV_INTERNAL__SHOW_DERIVATION_TREE)
493            .is_some()
494            || tracing::enabled!(tracing::Level::TRACE);
495
496        if should_display_tree {
497            display_tree(&tree, "Resolver derivation tree before reduction");
498        }
499
500        collapse_no_versions_of_workspace_members(&mut tree, &self.workspace_members);
501
502        if self.workspace_members.len() == 1 {
503            let project = self.workspace_members.iter().next().unwrap();
504            drop_root_dependency_on_project(&mut tree, project);
505        }
506
507        collapse_unavailable_versions(&mut tree);
508        collapse_redundant_depends_on_no_versions(&mut tree);
509
510        simplify_derivation_tree_ranges(
511            &mut tree,
512            &self.available_versions,
513            &self.selector,
514            &self.env,
515        );
516
517        // This needs to be applied _after_ simplification of the ranges
518        collapse_redundant_no_versions(&mut tree);
519
520        while collapse_redundant_no_versions_tree(&mut tree) {
521            // Continue collapsing until no more redundant nodes are found
522        }
523
524        if should_display_tree {
525            display_tree(&tree, "Resolver derivation tree after reduction");
526        }
527
528        let report = DefaultStringReporter::report_with_formatter(&tree, &formatter);
529        write!(f, "{report}")?;
530
531        // Include any additional hints.
532        let mut additional_hints = IndexSet::default();
533        formatter.generate_hints(
534            &tree,
535            &self.index,
536            &self.selector,
537            &self.index_locations,
538            &self.index_capabilities,
539            &self.available_indexes,
540            &self.unavailable_packages,
541            &self.incomplete_packages,
542            &self.fork_urls,
543            &self.fork_indexes,
544            &self.env,
545            self.tags.as_ref(),
546            &self.workspace_members,
547            &self.options,
548            &mut additional_hints,
549        );
550        for hint in additional_hints {
551            write!(f, "\n\n{hint}")?;
552        }
553
554        self.hint_disjoint_targets(f)?;
555
556        Ok(())
557    }
558}
559
560#[allow(clippy::print_stderr)]
561fn display_tree(
562    error: &DerivationTree<PubGrubPackage, Range<Version>, UnavailableReason>,
563    name: &str,
564) {
565    let mut lines = Vec::new();
566    display_tree_inner(error, &mut lines, 0);
567    lines.reverse();
568
569    if std::env::var_os(EnvVars::UV_INTERNAL__SHOW_DERIVATION_TREE).is_some() {
570        eprintln!("{name}\n{}", lines.join("\n"));
571    } else {
572        trace!("{name}\n{}", lines.join("\n"));
573    }
574}
575
576fn display_tree_inner(
577    error: &DerivationTree<PubGrubPackage, Range<Version>, UnavailableReason>,
578    lines: &mut Vec<String>,
579    depth: usize,
580) {
581    let prefix = "  ".repeat(depth);
582    match error {
583        DerivationTree::Derived(derived) => {
584            display_tree_inner(&derived.cause1, lines, depth + 1);
585            display_tree_inner(&derived.cause2, lines, depth + 1);
586            for (package, term) in &derived.terms {
587                match term {
588                    Term::Positive(versions) => {
589                        lines.push(format!("{prefix}term {package}{versions}"));
590                    }
591                    Term::Negative(versions) => {
592                        lines.push(format!("{prefix}term not {package}{versions}"));
593                    }
594                }
595            }
596        }
597        DerivationTree::External(external) => match external {
598            External::FromDependencyOf(package, version, dependency, dependency_version) => {
599                lines.push(format!(
600                    "{prefix}{package}{version} depends on {dependency}{dependency_version}"
601                ));
602            }
603            External::Custom(package, versions, reason) => match reason {
604                UnavailableReason::Package(_) => {
605                    lines.push(format!("{prefix}{package} {reason}"));
606                }
607                UnavailableReason::Version(_) => {
608                    lines.push(format!("{prefix}{package}{versions} {reason}"));
609                }
610            },
611            External::NoVersions(package, versions) => {
612                lines.push(format!("{prefix}no versions of {package}{versions}"));
613            }
614            External::NotRoot(package, versions) => {
615                lines.push(format!("{prefix}not root {package}{versions}"));
616            }
617        },
618    }
619}
620
621fn collapse_redundant_no_versions(
622    tree: &mut DerivationTree<PubGrubPackage, Range<Version>, UnavailableReason>,
623) {
624    match tree {
625        DerivationTree::External(_) => {}
626        DerivationTree::Derived(derived) => {
627            match (
628                Arc::make_mut(&mut derived.cause1),
629                Arc::make_mut(&mut derived.cause2),
630            ) {
631                // If we have a node for a package with no versions...
632                (
633                    DerivationTree::External(External::NoVersions(package, versions)),
634                    ref mut other,
635                )
636                | (
637                    ref mut other,
638                    DerivationTree::External(External::NoVersions(package, versions)),
639                ) => {
640                    // First, always recursively visit the other side of the tree
641                    collapse_redundant_no_versions(other);
642
643                    // Retrieve the nearest terms, either alongside this node or from the parent.
644                    let package_terms = if let DerivationTree::Derived(derived) = other {
645                        derived.terms.get(package)
646                    } else {
647                        derived.terms.get(package)
648                    };
649
650                    let Some(Term::Positive(term)) = package_terms else {
651                        return;
652                    };
653
654                    let versions = versions.complement();
655
656                    // If we're disqualifying a single version, this is important to retain, e.g,
657                    // for `only foo==1.0.0 is available`
658                    if versions.as_singleton().is_some() {
659                        return;
660                    }
661
662                    // If the range in the conclusion (terms) matches the range of no versions,
663                    // then we'll drop this node. If the range is "all versions", then there's no
664                    // also no need to enumerate the available versions.
665                    if *term != Range::full() && *term != versions {
666                        return;
667                    }
668
669                    *tree = other.clone();
670                }
671                // If not, just recurse
672                _ => {
673                    collapse_redundant_no_versions(Arc::make_mut(&mut derived.cause1));
674                    collapse_redundant_no_versions(Arc::make_mut(&mut derived.cause2));
675                }
676            }
677        }
678    }
679}
680
681/// Given a [`DerivationTree`], collapse any derived trees with two `NoVersions` nodes for the same
682/// package. For example, if we have a tree like:
683///
684/// ```text
685/// term Python>=3.7.9
686///   no versions of Python>=3.7.9, <3.8
687///   no versions of Python>=3.8
688/// ```
689///
690/// We can simplify this to:
691///
692/// ```text
693/// no versions of Python>=3.7.9
694/// ```
695///
696/// This function returns a `bool` indicating if a change was made. This allows for repeated calls,
697/// e.g., the following tree contains nested redundant trees:
698///
699/// ```text
700/// term Python>=3.10
701///   no versions of Python>=3.11, <3.12
702///   term Python>=3.10, <3.11 | >=3.12
703///     no versions of Python>=3.12
704///     no versions of Python>=3.10, <3.11
705/// ```
706///
707/// We can simplify this to:
708///
709/// ```text
710/// no versions of Python>=3.10
711/// ```
712///
713/// This appears to be common with the way the resolver currently models Python version
714/// incompatibilities.
715fn collapse_redundant_no_versions_tree(
716    tree: &mut DerivationTree<PubGrubPackage, Range<Version>, UnavailableReason>,
717) -> bool {
718    match tree {
719        DerivationTree::External(_) => false,
720        DerivationTree::Derived(derived) => {
721            match (
722                Arc::make_mut(&mut derived.cause1),
723                Arc::make_mut(&mut derived.cause2),
724            ) {
725                // If we have a tree with two `NoVersions` nodes for the same package...
726                (
727                    DerivationTree::External(External::NoVersions(package, versions)),
728                    DerivationTree::External(External::NoVersions(other_package, other_versions)),
729                ) if package == other_package => {
730                    // Retrieve the terms from the parent.
731                    let Some(Term::Positive(term)) = derived.terms.get(package) else {
732                        return false;
733                    };
734
735                    // If they're both subsets of the term, then drop this node in favor of the term
736                    if versions.subset_of(term) && other_versions.subset_of(term) {
737                        *tree = DerivationTree::External(External::NoVersions(
738                            package.clone(),
739                            term.clone(),
740                        ));
741                        return true;
742                    }
743
744                    false
745                }
746                // If not, just recurse
747                _ => {
748                    collapse_redundant_no_versions_tree(Arc::make_mut(&mut derived.cause1))
749                        || collapse_redundant_no_versions_tree(Arc::make_mut(&mut derived.cause2))
750                }
751            }
752        }
753    }
754}
755
756/// Given a [`DerivationTree`], collapse any `NoVersion` incompatibilities for workspace members
757/// to avoid saying things like "only <workspace-member>==0.1.0 is available".
758fn collapse_no_versions_of_workspace_members(
759    tree: &mut DerivationTree<PubGrubPackage, Range<Version>, UnavailableReason>,
760    workspace_members: &BTreeSet<PackageName>,
761) {
762    match tree {
763        DerivationTree::External(_) => {}
764        DerivationTree::Derived(derived) => {
765            match (
766                Arc::make_mut(&mut derived.cause1),
767                Arc::make_mut(&mut derived.cause2),
768            ) {
769                // If we have a node for a package with no versions...
770                (DerivationTree::External(External::NoVersions(package, _)), ref mut other)
771                | (ref mut other, DerivationTree::External(External::NoVersions(package, _))) => {
772                    // First, always recursively visit the other side of the tree
773                    collapse_no_versions_of_workspace_members(other, workspace_members);
774
775                    // Then, if the package is a workspace member...
776                    let (PubGrubPackageInner::Package { name, .. }
777                    | PubGrubPackageInner::Extra { name, .. }
778                    | PubGrubPackageInner::Group { name, .. }) = &**package
779                    else {
780                        return;
781                    };
782                    if !workspace_members.contains(name) {
783                        return;
784                    }
785
786                    // Replace this node with the other tree
787                    *tree = other.clone();
788                }
789                // If not, just recurse
790                _ => {
791                    collapse_no_versions_of_workspace_members(
792                        Arc::make_mut(&mut derived.cause1),
793                        workspace_members,
794                    );
795                    collapse_no_versions_of_workspace_members(
796                        Arc::make_mut(&mut derived.cause2),
797                        workspace_members,
798                    );
799                }
800            }
801        }
802    }
803}
804
805/// Given a [`DerivationTree`], collapse `NoVersions` incompatibilities that are redundant children
806/// of a dependency. For example, if we have a tree like:
807///
808/// ```text
809/// A>=1,<2 depends on B
810///     A has no versions >1,<2
811///     C depends on A>=1,<2
812/// ```
813///
814/// We can simplify this to `C depends A>=1 and A>=1 depends on B so C depends on B` without
815/// explaining that there are no other versions of A. This is dependent on range of A in "A depends
816/// on" being a subset of range of A in "depends on A". For example, in a tree like:
817///
818/// ```text
819/// A>=1,<3 depends on B
820///     A has no versions >2,<3
821///     C depends on A>=2,<3
822/// ```
823///
824/// We cannot say `C depends on A>=2 and A>=1 depends on B so C depends on B` because there is a
825/// hole in the range — `A>=1,<3` is not a subset of `A>=2,<3`.
826fn collapse_redundant_depends_on_no_versions(
827    tree: &mut DerivationTree<PubGrubPackage, Range<Version>, UnavailableReason>,
828) {
829    match tree {
830        DerivationTree::External(_) => {}
831        DerivationTree::Derived(derived) => {
832            // If one node is a dependency incompatibility...
833            match (
834                Arc::make_mut(&mut derived.cause1),
835                Arc::make_mut(&mut derived.cause2),
836            ) {
837                (
838                    DerivationTree::External(External::FromDependencyOf(package, versions, _, _)),
839                    ref mut other,
840                )
841                | (
842                    ref mut other,
843                    DerivationTree::External(External::FromDependencyOf(package, versions, _, _)),
844                ) => {
845                    // Check if the other node is the relevant form of subtree...
846                    collapse_redundant_depends_on_no_versions_inner(other, package, versions);
847                }
848                // If not, just recurse
849                _ => {
850                    collapse_redundant_depends_on_no_versions(Arc::make_mut(&mut derived.cause1));
851                    collapse_redundant_depends_on_no_versions(Arc::make_mut(&mut derived.cause2));
852                }
853            }
854        }
855    }
856}
857
858/// Helper for [`collapse_redundant_depends_on_no_versions`].
859fn collapse_redundant_depends_on_no_versions_inner(
860    tree: &mut DerivationTree<PubGrubPackage, Range<Version>, UnavailableReason>,
861    package: &PubGrubPackage,
862    versions: &Range<Version>,
863) {
864    match tree {
865        DerivationTree::External(_) => {}
866        DerivationTree::Derived(derived) => {
867            // If we're a subtree with dependency and no versions incompatibilities...
868            match (&*derived.cause1, &*derived.cause2) {
869                (
870                    DerivationTree::External(External::NoVersions(no_versions_package, _)),
871                    dependency_clause @ DerivationTree::External(External::FromDependencyOf(
872                        _,
873                        _,
874                        dependency_package,
875                        dependency_versions,
876                    )),
877                )
878                | (
879                    dependency_clause @ DerivationTree::External(External::FromDependencyOf(
880                        _,
881                        _,
882                        dependency_package,
883                        dependency_versions,
884                    )),
885                    DerivationTree::External(External::NoVersions(no_versions_package, _)),
886                )
887                // And these incompatibilities (and the parent incompatibility) all are referring to
888                // the same package...
889                if no_versions_package == dependency_package
890                    && package == no_versions_package
891                // And parent dependency versions are a subset of the versions in this tree...
892                    && versions.subset_of(dependency_versions) =>
893                {
894                    // Enumerating the available versions will be redundant and we can drop the no
895                    // versions clause entirely in favor of the dependency clause.
896                    *tree = dependency_clause.clone();
897
898                    // Note we are at a leaf of the tree so there's no further recursion to do
899                }
900                // If not, just recurse
901                _ => {
902                    collapse_redundant_depends_on_no_versions(Arc::make_mut(&mut derived.cause1));
903                    collapse_redundant_depends_on_no_versions(Arc::make_mut(&mut derived.cause2));
904                }
905            }
906        }
907    }
908}
909
910/// Simplifies the markers on pubgrub packages in the given derivation tree
911/// according to the given Python requirement.
912///
913/// For example, when there's a dependency like `foo ; python_version >=
914/// '3.11'` and `requires-python = '>=3.11'`, this simplification will remove
915/// the `python_version >= '3.11'` marker since it's implied to be true by
916/// the `requires-python` setting. This simplifies error messages by reducing
917/// noise.
918fn simplify_derivation_tree_markers(
919    python_requirement: &PythonRequirement,
920    tree: &mut DerivationTree<PubGrubPackage, Range<Version>, UnavailableReason>,
921) {
922    match tree {
923        DerivationTree::External(External::NotRoot(pkg, _)) => {
924            pkg.simplify_markers(python_requirement);
925        }
926        DerivationTree::External(External::NoVersions(pkg, _)) => {
927            pkg.simplify_markers(python_requirement);
928        }
929        DerivationTree::External(External::FromDependencyOf(pkg1, _, pkg2, _)) => {
930            pkg1.simplify_markers(python_requirement);
931            pkg2.simplify_markers(python_requirement);
932        }
933        DerivationTree::External(External::Custom(pkg, _, _)) => {
934            pkg.simplify_markers(python_requirement);
935        }
936        DerivationTree::Derived(derived) => {
937            derived.terms = std::mem::take(&mut derived.terms)
938                .into_iter()
939                .map(|(mut pkg, term)| {
940                    pkg.simplify_markers(python_requirement);
941                    (pkg, term)
942                })
943                .collect();
944            simplify_derivation_tree_markers(
945                python_requirement,
946                Arc::make_mut(&mut derived.cause1),
947            );
948            simplify_derivation_tree_markers(
949                python_requirement,
950                Arc::make_mut(&mut derived.cause2),
951            );
952        }
953    }
954}
955
956/// Given a [`DerivationTree`], collapse incompatibilities for versions of a package that are
957/// unavailable for the same reason to avoid repeating the same message for every unavailable
958/// version.
959fn collapse_unavailable_versions(
960    tree: &mut DerivationTree<PubGrubPackage, Range<Version>, UnavailableReason>,
961) {
962    match tree {
963        DerivationTree::External(_) => {}
964        DerivationTree::Derived(derived) => {
965            match (
966                Arc::make_mut(&mut derived.cause1),
967                Arc::make_mut(&mut derived.cause2),
968            ) {
969                // If we have a node for unavailable package versions
970                (
971                    DerivationTree::External(External::Custom(package, versions, reason)),
972                    ref mut other,
973                )
974                | (
975                    ref mut other,
976                    DerivationTree::External(External::Custom(package, versions, reason)),
977                ) => {
978                    // First, recursively collapse the other side of the tree
979                    collapse_unavailable_versions(other);
980
981                    // If it's not a derived tree, nothing to do.
982                    let DerivationTree::Derived(Derived {
983                        terms,
984                        shared_id,
985                        cause1,
986                        cause2,
987                    }) = other
988                    else {
989                        return;
990                    };
991
992                    // If the other tree has an unavailable package...
993                    match (&**cause1, &**cause2) {
994                        // Note the following cases are the same, but we need two matches to retain
995                        // the ordering of the causes
996                        (
997                            _,
998                            DerivationTree::External(External::Custom(
999                                other_package,
1000                                other_versions,
1001                                other_reason,
1002                            )),
1003                        ) => {
1004                            // And the package and reason are the same...
1005                            if package == other_package && reason == other_reason {
1006                                // Collapse both into a new node, with a union of their ranges
1007                                let versions = other_versions.union(versions);
1008                                let mut terms = terms.clone();
1009                                if let Some(Term::Positive(range)) = terms.get_mut(package) {
1010                                    *range = versions.clone();
1011                                }
1012                                *tree = DerivationTree::Derived(Derived {
1013                                    terms,
1014                                    shared_id: *shared_id,
1015                                    cause1: cause1.clone(),
1016                                    cause2: Arc::new(DerivationTree::External(External::Custom(
1017                                        package.clone(),
1018                                        versions,
1019                                        reason.clone(),
1020                                    ))),
1021                                });
1022                            }
1023                        }
1024                        (
1025                            DerivationTree::External(External::Custom(
1026                                other_package,
1027                                other_versions,
1028                                other_reason,
1029                            )),
1030                            _,
1031                        ) => {
1032                            // And the package and reason are the same...
1033                            if package == other_package && reason == other_reason {
1034                                // Collapse both into a new node, with a union of their ranges
1035                                let versions = other_versions.union(versions);
1036                                let mut terms = terms.clone();
1037                                if let Some(Term::Positive(range)) = terms.get_mut(package) {
1038                                    *range = versions.clone();
1039                                }
1040                                *tree = DerivationTree::Derived(Derived {
1041                                    terms,
1042                                    shared_id: *shared_id,
1043                                    cause1: Arc::new(DerivationTree::External(External::Custom(
1044                                        package.clone(),
1045                                        versions,
1046                                        reason.clone(),
1047                                    ))),
1048                                    cause2: cause2.clone(),
1049                                });
1050                            }
1051                        }
1052                        _ => {}
1053                    }
1054                }
1055                // If not, just recurse
1056                _ => {
1057                    collapse_unavailable_versions(Arc::make_mut(&mut derived.cause1));
1058                    collapse_unavailable_versions(Arc::make_mut(&mut derived.cause2));
1059                }
1060            }
1061        }
1062    }
1063}
1064
1065/// Given a [`DerivationTree`], drop dependency incompatibilities from the root
1066/// to the project.
1067///
1068/// Intended to effectively change the root to a workspace member in single project
1069/// workspaces, avoiding a level of indirection like "And because your project
1070/// requires your project, we can conclude that your project's requirements are
1071/// unsatisfiable."
1072fn drop_root_dependency_on_project(
1073    tree: &mut DerivationTree<PubGrubPackage, Range<Version>, UnavailableReason>,
1074    project: &PackageName,
1075) {
1076    match tree {
1077        DerivationTree::External(_) => {}
1078        DerivationTree::Derived(derived) => {
1079            match (
1080                Arc::make_mut(&mut derived.cause1),
1081                Arc::make_mut(&mut derived.cause2),
1082            ) {
1083                // If one node is a dependency incompatibility...
1084                (
1085                    DerivationTree::External(External::FromDependencyOf(package, _, dependency, _)),
1086                    ref mut other,
1087                )
1088                | (
1089                    ref mut other,
1090                    DerivationTree::External(External::FromDependencyOf(package, _, dependency, _)),
1091                ) => {
1092                    // And the parent is the root package...
1093                    if !matches!(&**package, PubGrubPackageInner::Root(_)) {
1094                        return;
1095                    }
1096
1097                    // And the dependency is the project...
1098                    let PubGrubPackageInner::Package { name, .. } = &**dependency else {
1099                        return;
1100                    };
1101                    if name != project {
1102                        return;
1103                    }
1104
1105                    // Recursively collapse the other side of the tree
1106                    drop_root_dependency_on_project(other, project);
1107
1108                    // Then, replace this node with the other tree
1109                    *tree = other.clone();
1110                }
1111                // If not, just recurse
1112                _ => {
1113                    drop_root_dependency_on_project(Arc::make_mut(&mut derived.cause1), project);
1114                    drop_root_dependency_on_project(Arc::make_mut(&mut derived.cause2), project);
1115                }
1116            }
1117        }
1118    }
1119}
1120
1121/// A version range that may include local version sentinels (`+[max]`).
1122#[derive(Debug)]
1123pub struct SentinelRange<'range>(&'range Range<Version>);
1124
1125impl<'range> From<&'range Range<Version>> for SentinelRange<'range> {
1126    fn from(range: &'range Range<Version>) -> Self {
1127        Self(range)
1128    }
1129}
1130
1131impl SentinelRange<'_> {
1132    /// Returns `true` if the range appears to be, e.g., `>=1.0.0, <1.0.0+[max]`.
1133    pub fn is_sentinel(&self) -> bool {
1134        self.0.iter().all(|(lower, upper)| {
1135            let (Bound::Included(lower), Bound::Excluded(upper)) = (lower, upper) else {
1136                return false;
1137            };
1138            if !lower.local().is_empty() {
1139                return false;
1140            }
1141            if upper.local() != LocalVersionSlice::Max {
1142                return false;
1143            }
1144            *lower == upper.clone().without_local()
1145        })
1146    }
1147
1148    /// Returns `true` if the range appears to be, e.g., `>1.0.0, <1.0.0+[max]` (i.e., a sentinel
1149    /// range with the non-local version removed).
1150    pub fn is_complement(&self) -> bool {
1151        self.0.iter().all(|(lower, upper)| {
1152            let (Bound::Excluded(lower), Bound::Excluded(upper)) = (lower, upper) else {
1153                return false;
1154            };
1155            if !lower.local().is_empty() {
1156                return false;
1157            }
1158            if upper.local() != LocalVersionSlice::Max {
1159                return false;
1160            }
1161            *lower == upper.clone().without_local()
1162        })
1163    }
1164
1165    /// Remove local versions sentinels (`+[max]`) from the version ranges.
1166    pub fn strip(&self) -> Ranges<Version> {
1167        self.0
1168            .iter()
1169            .map(|(lower, upper)| Self::strip_sentinel(lower.clone(), upper.clone()))
1170            .collect()
1171    }
1172
1173    /// Remove local versions sentinels (`+[max]`) from the interval.
1174    fn strip_sentinel(
1175        mut lower: Bound<Version>,
1176        mut upper: Bound<Version>,
1177    ) -> (Bound<Version>, Bound<Version>) {
1178        match (&lower, &upper) {
1179            (Bound::Unbounded, Bound::Unbounded) => {}
1180            (Bound::Unbounded, Bound::Included(v)) => {
1181                // `<=1.0.0+[max]` is equivalent to `<=1.0.0`
1182                if v.local() == LocalVersionSlice::Max {
1183                    upper = Bound::Included(v.clone().without_local());
1184                }
1185            }
1186            (Bound::Unbounded, Bound::Excluded(v)) => {
1187                // `<1.0.0+[max]` is equivalent to `<1.0.0`
1188                if v.local() == LocalVersionSlice::Max {
1189                    upper = Bound::Excluded(v.clone().without_local());
1190                }
1191            }
1192            (Bound::Included(v), Bound::Unbounded) => {
1193                // `>=1.0.0+[max]` is equivalent to `>1.0.0`
1194                if v.local() == LocalVersionSlice::Max {
1195                    lower = Bound::Excluded(v.clone().without_local());
1196                }
1197            }
1198            (Bound::Included(v), Bound::Included(b)) => {
1199                // `>=1.0.0+[max]` is equivalent to `>1.0.0`
1200                if v.local() == LocalVersionSlice::Max {
1201                    lower = Bound::Excluded(v.clone().without_local());
1202                }
1203                // `<=1.0.0+[max]` is equivalent to `<=1.0.0`
1204                if b.local() == LocalVersionSlice::Max {
1205                    upper = Bound::Included(b.clone().without_local());
1206                }
1207            }
1208            (Bound::Included(v), Bound::Excluded(b)) => {
1209                // `>=1.0.0+[max]` is equivalent to `>1.0.0`
1210                if v.local() == LocalVersionSlice::Max {
1211                    lower = Bound::Excluded(v.clone().without_local());
1212                }
1213                // `<1.0.0+[max]` is equivalent to `<1.0.0`
1214                if b.local() == LocalVersionSlice::Max {
1215                    upper = Bound::Included(b.clone().without_local());
1216                }
1217            }
1218            (Bound::Excluded(v), Bound::Unbounded) => {
1219                // `>1.0.0+[max]` is equivalent to `>1.0.0`
1220                if v.local() == LocalVersionSlice::Max {
1221                    lower = Bound::Excluded(v.clone().without_local());
1222                }
1223            }
1224            (Bound::Excluded(v), Bound::Included(b)) => {
1225                // `>1.0.0+[max]` is equivalent to `>1.0.0`
1226                if v.local() == LocalVersionSlice::Max {
1227                    lower = Bound::Excluded(v.clone().without_local());
1228                }
1229                // `<=1.0.0+[max]` is equivalent to `<=1.0.0`
1230                if b.local() == LocalVersionSlice::Max {
1231                    upper = Bound::Included(b.clone().without_local());
1232                }
1233            }
1234            (Bound::Excluded(v), Bound::Excluded(b)) => {
1235                // `>1.0.0+[max]` is equivalent to `>1.0.0`
1236                if v.local() == LocalVersionSlice::Max {
1237                    lower = Bound::Excluded(v.clone().without_local());
1238                }
1239                // `<1.0.0+[max]` is equivalent to `<1.0.0`
1240                if b.local() == LocalVersionSlice::Max {
1241                    upper = Bound::Excluded(b.clone().without_local());
1242                }
1243            }
1244        }
1245        (lower, upper)
1246    }
1247}
1248
1249/// A prefix match, e.g., `==2.4.*`, which is desugared to a range like `>=2.4.dev0,<2.5.dev0`.
1250#[derive(Debug, Clone, PartialEq, Eq)]
1251pub(crate) struct PrefixMatch<'a> {
1252    version: &'a Version,
1253}
1254
1255impl<'a> PrefixMatch<'a> {
1256    /// Determine whether a given range is equivalent to a prefix match (e.g., `==2.4.*`).
1257    ///
1258    /// Prefix matches are desugared to (e.g.) `>=2.4.dev0,<2.5.dev0`, but we want to render them
1259    /// as `==2.4.*` in error messages.
1260    pub(crate) fn from_range(lower: &'a Bound<Version>, upper: &'a Bound<Version>) -> Option<Self> {
1261        let Bound::Included(lower) = lower else {
1262            return None;
1263        };
1264        let Bound::Excluded(upper) = upper else {
1265            return None;
1266        };
1267        if lower.is_pre() || lower.is_post() || lower.is_local() {
1268            return None;
1269        }
1270        if upper.is_pre() || upper.is_post() || upper.is_local() {
1271            return None;
1272        }
1273        if lower.dev() != Some(0) {
1274            return None;
1275        }
1276        if upper.dev() != Some(0) {
1277            return None;
1278        }
1279        if lower.release().len() != upper.release().len() {
1280            return None;
1281        }
1282
1283        // All segments should be the same, except the last one, which should be incremented.
1284        let num_segments = lower.release().len();
1285        for (i, (lower, upper)) in lower
1286            .release()
1287            .iter()
1288            .zip(upper.release().iter())
1289            .enumerate()
1290        {
1291            if i == num_segments - 1 {
1292                if lower + 1 != *upper {
1293                    return None;
1294                }
1295            } else {
1296                if lower != upper {
1297                    return None;
1298                }
1299            }
1300        }
1301
1302        Some(PrefixMatch { version: lower })
1303    }
1304}
1305
1306impl std::fmt::Display for PrefixMatch<'_> {
1307    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1308        write!(f, "=={}.*", self.version.only_release())
1309    }
1310}
1311
1312#[derive(Debug)]
1313pub struct NoSolutionHeader {
1314    /// The [`ResolverEnvironment`] that caused the failure.
1315    env: ResolverEnvironment,
1316    /// The additional context for the resolution failure.
1317    context: Option<&'static str>,
1318}
1319
1320impl NoSolutionHeader {
1321    /// Create a new [`NoSolutionHeader`] with the given [`ResolverEnvironment`].
1322    pub fn new(env: ResolverEnvironment) -> Self {
1323        Self { env, context: None }
1324    }
1325
1326    /// Set the context for the resolution failure.
1327    #[must_use]
1328    pub fn with_context(mut self, context: &'static str) -> Self {
1329        self.context = Some(context);
1330        self
1331    }
1332}
1333
1334impl std::fmt::Display for NoSolutionHeader {
1335    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1336        match (self.context, self.env.end_user_fork_display()) {
1337            (None, None) => write!(f, "No solution found when resolving dependencies:"),
1338            (Some(context), None) => write!(
1339                f,
1340                "No solution found when resolving {context} dependencies:"
1341            ),
1342            (None, Some(split)) => write!(
1343                f,
1344                "No solution found when resolving dependencies for {split}:"
1345            ),
1346            (Some(context), Some(split)) => write!(
1347                f,
1348                "No solution found when resolving {context} dependencies for {split}:"
1349            ),
1350        }
1351    }
1352}
1353
1354/// Given a [`DerivationTree`], simplify version ranges using the available versions for each
1355/// package.
1356fn simplify_derivation_tree_ranges(
1357    tree: &mut DerivationTree<PubGrubPackage, Range<Version>, UnavailableReason>,
1358    available_versions: &FxHashMap<PackageName, BTreeSet<Version>>,
1359    candidate_selector: &CandidateSelector,
1360    resolver_environment: &ResolverEnvironment,
1361) {
1362    match tree {
1363        DerivationTree::External(external) => match external {
1364            External::FromDependencyOf(package1, versions1, package2, versions2) => {
1365                if let Some(simplified) = simplify_range(
1366                    versions1,
1367                    package1,
1368                    available_versions,
1369                    candidate_selector,
1370                    resolver_environment,
1371                ) {
1372                    *versions1 = simplified;
1373                }
1374                if let Some(simplified) = simplify_range(
1375                    versions2,
1376                    package2,
1377                    available_versions,
1378                    candidate_selector,
1379                    resolver_environment,
1380                ) {
1381                    *versions2 = simplified;
1382                }
1383            }
1384            External::NoVersions(package, versions) => {
1385                if let Some(simplified) = simplify_range(
1386                    versions,
1387                    package,
1388                    available_versions,
1389                    candidate_selector,
1390                    resolver_environment,
1391                ) {
1392                    *versions = simplified;
1393                }
1394            }
1395            External::Custom(package, versions, _) => {
1396                if let Some(simplified) = simplify_range(
1397                    versions,
1398                    package,
1399                    available_versions,
1400                    candidate_selector,
1401                    resolver_environment,
1402                ) {
1403                    *versions = simplified;
1404                }
1405            }
1406            External::NotRoot(..) => (),
1407        },
1408        DerivationTree::Derived(derived) => {
1409            // Recursively simplify both sides of the tree
1410            simplify_derivation_tree_ranges(
1411                Arc::make_mut(&mut derived.cause1),
1412                available_versions,
1413                candidate_selector,
1414                resolver_environment,
1415            );
1416            simplify_derivation_tree_ranges(
1417                Arc::make_mut(&mut derived.cause2),
1418                available_versions,
1419                candidate_selector,
1420                resolver_environment,
1421            );
1422
1423            // Simplify the terms
1424            derived.terms = std::mem::take(&mut derived.terms)
1425                .into_iter()
1426                .map(|(pkg, term)| {
1427                    let term = match term {
1428                        Term::Positive(versions) => Term::Positive(
1429                            simplify_range(
1430                                &versions,
1431                                &pkg,
1432                                available_versions,
1433                                candidate_selector,
1434                                resolver_environment,
1435                            )
1436                            .unwrap_or(versions),
1437                        ),
1438                        Term::Negative(versions) => Term::Negative(
1439                            simplify_range(
1440                                &versions,
1441                                &pkg,
1442                                available_versions,
1443                                candidate_selector,
1444                                resolver_environment,
1445                            )
1446                            .unwrap_or(versions),
1447                        ),
1448                    };
1449                    (pkg, term)
1450                })
1451                .collect();
1452        }
1453    }
1454}
1455
1456/// Helper function to simplify a version range using available versions for a package.
1457///
1458/// If the range cannot be simplified, `None` is returned.
1459fn simplify_range(
1460    range: &Range<Version>,
1461    package: &PubGrubPackage,
1462    available_versions: &FxHashMap<PackageName, BTreeSet<Version>>,
1463    candidate_selector: &CandidateSelector,
1464    resolver_environment: &ResolverEnvironment,
1465) -> Option<Range<Version>> {
1466    // If there's not a package name or available versions, we can't simplify anything
1467    let name = package.name()?;
1468    let versions = available_versions.get(name)?;
1469
1470    // If this is a full range, there's nothing to simplify
1471    if range == &Range::full() {
1472        return None;
1473    }
1474
1475    // If there's only one version available and it's in the range, return just that version
1476    if let Some(version) = versions.iter().next() {
1477        if versions.len() == 1 && range.contains(version) {
1478            return Some(Range::singleton(version.clone()));
1479        }
1480    }
1481
1482    // Check if pre-releases are allowed
1483    let prereleases_not_allowed = candidate_selector
1484        .prerelease_strategy()
1485        .allows(name, resolver_environment)
1486        != AllowPrerelease::Yes;
1487
1488    let any_prerelease = range.iter().any(|(start, end)| {
1489        let is_pre1 = match start {
1490            Bound::Included(version) => version.any_prerelease(),
1491            Bound::Excluded(version) => version.any_prerelease(),
1492            Bound::Unbounded => false,
1493        };
1494        let is_pre2 = match end {
1495            Bound::Included(version) => version.any_prerelease(),
1496            Bound::Excluded(version) => version.any_prerelease(),
1497            Bound::Unbounded => false,
1498        };
1499        is_pre1 || is_pre2
1500    });
1501
1502    // Simplify the range, as implemented in PubGrub
1503    Some(range.simplify(versions.iter().filter(|version| {
1504        // If there are pre-releases in the range segments, we need to include pre-releases
1505        if any_prerelease {
1506            return true;
1507        }
1508
1509        // If pre-releases are not allowed, filter out pre-releases
1510        if prereleases_not_allowed && version.any_prerelease() {
1511            return false;
1512        }
1513
1514        // Otherwise, include the version
1515        true
1516    })))
1517}