mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-04 19:08:04 +00:00

Some checks are pending
CI / Determine changes (push) Waiting to run
CI / lint (push) Waiting to run
CI / cargo clippy | ubuntu (push) Blocked by required conditions
CI / cargo clippy | windows (push) Blocked by required conditions
CI / cargo dev generate-all (push) Blocked by required conditions
CI / cargo shear (push) Waiting to run
CI / cargo test | ubuntu (push) Blocked by required conditions
CI / cargo test | macos (push) Blocked by required conditions
CI / cargo test | windows (push) Blocked by required conditions
CI / check windows trampoline | aarch64 (push) Blocked by required conditions
CI / integration test | pyodide on ubuntu (push) Blocked by required conditions
CI / check windows trampoline | i686 (push) Blocked by required conditions
CI / check windows trampoline | x86_64 (push) Blocked by required conditions
CI / test windows trampoline | i686 (push) Blocked by required conditions
CI / test windows trampoline | x86_64 (push) Blocked by required conditions
CI / typos (push) Waiting to run
CI / check system | pyston (push) Blocked by required conditions
CI / mkdocs (push) Waiting to run
CI / build binary | linux libc (push) Blocked by required conditions
CI / build binary | linux aarch64 (push) Blocked by required conditions
CI / build binary | linux musl (push) Blocked by required conditions
CI / build binary | macos aarch64 (push) Blocked by required conditions
CI / build binary | macos x86_64 (push) Blocked by required conditions
CI / build binary | windows x86_64 (push) Blocked by required conditions
CI / build binary | windows aarch64 (push) Blocked by required conditions
CI / cargo build (msrv) (push) Blocked by required conditions
CI / build binary | freebsd (push) Blocked by required conditions
CI / ecosystem test | pydantic/pydantic-core (push) Blocked by required conditions
CI / ecosystem test | prefecthq/prefect (push) Blocked by required conditions
CI / ecosystem test | pallets/flask (push) Blocked by required conditions
CI / smoke test | linux (push) Blocked by required conditions
CI / smoke test | linux aarch64 (push) Blocked by required conditions
CI / check system | alpine (push) Blocked by required conditions
CI / smoke test | macos (push) Blocked by required conditions
CI / smoke test | windows x86_64 (push) Blocked by required conditions
CI / smoke test | windows aarch64 (push) Blocked by required conditions
CI / integration test | conda on ubuntu (push) Blocked by required conditions
CI / integration test | deadsnakes python3.9 on ubuntu (push) Blocked by required conditions
CI / integration test | free-threaded on windows (push) Blocked by required conditions
CI / integration test | aarch64 windows implicit (push) Blocked by required conditions
CI / integration test | aarch64 windows explicit (push) Blocked by required conditions
CI / integration test | pypy on ubuntu (push) Blocked by required conditions
CI / integration test | pypy on windows (push) Blocked by required conditions
CI / integration test | graalpy on ubuntu (push) Blocked by required conditions
CI / integration test | graalpy on windows (push) Blocked by required conditions
CI / integration test | github actions (push) Blocked by required conditions
CI / integration test | free-threaded python on github actions (push) Blocked by required conditions
CI / integration test | determine publish changes (push) Blocked by required conditions
CI / integration test | registries (push) Blocked by required conditions
CI / integration test | uv publish (push) Blocked by required conditions
CI / integration test | uv_build (push) Blocked by required conditions
CI / check cache | ubuntu (push) Blocked by required conditions
CI / check cache | macos aarch64 (push) Blocked by required conditions
CI / check system | python on debian (push) Blocked by required conditions
CI / check system | python on fedora (push) Blocked by required conditions
CI / check system | python on ubuntu (push) Blocked by required conditions
CI / check system | python on rocky linux 8 (push) Blocked by required conditions
CI / check system | python on rocky linux 9 (push) Blocked by required conditions
CI / check system | graalpy on ubuntu (push) Blocked by required conditions
CI / check system | pypy on ubuntu (push) Blocked by required conditions
CI / check system | python on macos aarch64 (push) Blocked by required conditions
CI / check system | homebrew python on macos aarch64 (push) Blocked by required conditions
CI / check system | python on macos x86-64 (push) Blocked by required conditions
CI / check system | python3.10 on windows x86-64 (push) Blocked by required conditions
CI / check system | python3.10 on windows x86 (push) Blocked by required conditions
CI / check system | python3.13 on windows x86-64 (push) Blocked by required conditions
CI / check system | x86-64 python3.13 on windows aarch64 (push) Blocked by required conditions
CI / check system | aarch64 python3.13 on windows aarch64 (push) Blocked by required conditions
CI / check system | windows registry (push) Blocked by required conditions
CI / check system | python3.12 via chocolatey (push) Blocked by required conditions
CI / check system | python3.9 via pyenv (push) Blocked by required conditions
CI / check system | python3.13 (push) Blocked by required conditions
CI / check system | conda3.11 on macos aarch64 (push) Blocked by required conditions
CI / check system | conda3.8 on macos aarch64 (push) Blocked by required conditions
CI / check system | conda3.11 on linux x86-64 (push) Blocked by required conditions
CI / check system | conda3.8 on linux x86-64 (push) Blocked by required conditions
CI / check system | conda3.11 on windows x86-64 (push) Blocked by required conditions
CI / check system | conda3.8 on windows x86-64 (push) Blocked by required conditions
CI / check system | amazonlinux (push) Blocked by required conditions
CI / check system | embedded python3.10 on windows x86-64 (push) Blocked by required conditions
CI / benchmarks | walltime aarch64 linux (push) Blocked by required conditions
CI / benchmarks | instrumented (push) Blocked by required conditions
<!-- Thank you for contributing to uv! To help us out with reviewing, please consider the following: - Does this pull request include a summary of the change? (See below.) - Does this pull request include a descriptive title? - Does this pull request include references to any relevant issues? --> ## Summary <!-- What's the purpose of the change? What does it do, and why? --> In pixi we overlay the PyPI packages over the conda packages and we sometimes need to figure out what PyPI packages are involved in the no-solution error. We could parse the error message, but this is pretty error-prone, so it would be good to get access to more information. A lot of information in this module is private and should probably stay this way, but package names are easy enough to expose. This would help us a lot! I collect into a HashSet to remove duplication, and did not want to expose a rustc_hash datastructure directly, thats's why I've chosen to expose as an iterator :) Let me know if any changes need to be done, and thanks! --------- Co-authored-by: Zanie Blue <contact@zanie.dev>
1507 lines
58 KiB
Rust
1507 lines
58 KiB
Rust
use std::collections::{BTreeMap, BTreeSet, Bound};
|
|
use std::fmt::Formatter;
|
|
use std::sync::Arc;
|
|
|
|
use indexmap::IndexSet;
|
|
use itertools::Itertools;
|
|
use owo_colors::OwoColorize;
|
|
use pubgrub::{
|
|
DefaultStringReporter, DerivationTree, Derived, External, Range, Ranges, Reporter, Term,
|
|
};
|
|
use rustc_hash::FxHashMap;
|
|
use tracing::trace;
|
|
|
|
use uv_distribution_types::{
|
|
DerivationChain, DistErrorKind, IndexCapabilities, IndexLocations, IndexUrl, RequestedDist,
|
|
};
|
|
use uv_normalize::{ExtraName, InvalidNameError, PackageName};
|
|
use uv_pep440::{LocalVersionSlice, LowerBound, Version, VersionSpecifier};
|
|
use uv_pep508::{MarkerEnvironment, MarkerExpression, MarkerTree, MarkerValueVersion};
|
|
use uv_platform_tags::Tags;
|
|
use uv_pypi_types::ParsedUrl;
|
|
use uv_redacted::DisplaySafeUrl;
|
|
use uv_static::EnvVars;
|
|
|
|
use crate::candidate_selector::CandidateSelector;
|
|
use crate::dependency_provider::UvDependencyProvider;
|
|
use crate::fork_indexes::ForkIndexes;
|
|
use crate::fork_urls::ForkUrls;
|
|
use crate::prerelease::AllowPrerelease;
|
|
use crate::pubgrub::{PubGrubPackage, PubGrubPackageInner, PubGrubReportFormatter};
|
|
use crate::python_requirement::PythonRequirement;
|
|
use crate::resolution::ConflictingDistributionError;
|
|
use crate::resolver::{
|
|
MetadataUnavailable, ResolverEnvironment, UnavailablePackage, UnavailableReason,
|
|
};
|
|
use crate::{InMemoryIndex, Options};
|
|
|
|
#[derive(Debug, thiserror::Error)]
|
|
pub enum ResolveError {
|
|
#[error(transparent)]
|
|
Client(#[from] uv_client::Error),
|
|
|
|
#[error(transparent)]
|
|
Distribution(#[from] uv_distribution::Error),
|
|
|
|
#[error("The channel closed unexpectedly")]
|
|
ChannelClosed,
|
|
|
|
#[error(transparent)]
|
|
Join(#[from] tokio::task::JoinError),
|
|
|
|
#[error("Attempted to wait on an unregistered task: `{_0}`")]
|
|
UnregisteredTask(String),
|
|
|
|
#[error(
|
|
"Requirements contain conflicting URLs for package `{package_name}`{}:\n- {}",
|
|
if env.marker_environment().is_some() {
|
|
String::new()
|
|
} else {
|
|
format!(" in {env}")
|
|
},
|
|
urls.iter()
|
|
.map(|url| format!("{}{}", DisplaySafeUrl::from(url.clone()), if url.is_editable() { " (editable)" } else { "" }))
|
|
.collect::<Vec<_>>()
|
|
.join("\n- ")
|
|
)]
|
|
ConflictingUrls {
|
|
package_name: PackageName,
|
|
urls: Vec<ParsedUrl>,
|
|
env: ResolverEnvironment,
|
|
},
|
|
|
|
#[error(
|
|
"Requirements contain conflicting indexes for package `{package_name}`{}:\n- {}",
|
|
if env.marker_environment().is_some() {
|
|
String::new()
|
|
} else {
|
|
format!(" in {env}")
|
|
},
|
|
indexes.iter()
|
|
.map(std::string::ToString::to_string)
|
|
.collect::<Vec<_>>()
|
|
.join("\n- ")
|
|
)]
|
|
ConflictingIndexesForEnvironment {
|
|
package_name: PackageName,
|
|
indexes: Vec<IndexUrl>,
|
|
env: ResolverEnvironment,
|
|
},
|
|
|
|
#[error("Requirements contain conflicting indexes for package `{0}`: `{1}` vs. `{2}`")]
|
|
ConflictingIndexes(PackageName, String, String),
|
|
|
|
#[error(
|
|
"Package `{0}` attempted to resolve via URL: {1}. URL dependencies must be expressed as direct requirements or constraints. Consider adding `{0} @ {1}` to your dependencies or constraints file."
|
|
)]
|
|
DisallowedUrl(PackageName, String),
|
|
|
|
#[error(transparent)]
|
|
DistributionType(#[from] uv_distribution_types::Error),
|
|
|
|
#[error(transparent)]
|
|
ParsedUrl(#[from] uv_pypi_types::ParsedUrlError),
|
|
|
|
#[error("{0} `{1}`")]
|
|
Dist(
|
|
DistErrorKind,
|
|
Box<RequestedDist>,
|
|
DerivationChain,
|
|
#[source] Arc<uv_distribution::Error>,
|
|
),
|
|
|
|
#[error(transparent)]
|
|
NoSolution(#[from] Box<NoSolutionError>),
|
|
|
|
#[error("Attempted to construct an invalid version specifier")]
|
|
InvalidVersion(#[from] uv_pep440::VersionSpecifierBuildError),
|
|
|
|
#[error(
|
|
"In `--require-hashes` mode, all requirements must be pinned upfront with `==`, but found: `{0}`"
|
|
)]
|
|
UnhashedPackage(PackageName),
|
|
|
|
#[error("found conflicting distribution in resolution: {0}")]
|
|
ConflictingDistribution(ConflictingDistributionError),
|
|
|
|
#[error("Package `{0}` is unavailable")]
|
|
PackageUnavailable(PackageName),
|
|
|
|
#[error("Invalid extra value in conflict marker: {reason}: {raw_extra}")]
|
|
InvalidExtraInConflictMarker {
|
|
reason: String,
|
|
raw_extra: ExtraName,
|
|
},
|
|
|
|
#[error("Invalid {kind} value in conflict marker: {name_error}")]
|
|
InvalidValueInConflictMarker {
|
|
kind: &'static str,
|
|
#[source]
|
|
name_error: InvalidNameError,
|
|
},
|
|
#[error(
|
|
"The index returned metadata for the wrong package: expected {request} for {expected}, got {request} for {actual}"
|
|
)]
|
|
MismatchedPackageName {
|
|
request: &'static str,
|
|
expected: PackageName,
|
|
actual: PackageName,
|
|
},
|
|
}
|
|
|
|
impl<T> From<tokio::sync::mpsc::error::SendError<T>> for ResolveError {
|
|
/// Drop the value we want to send to not leak the private type we're sending.
|
|
/// The tokio error only says "channel closed", so we don't lose information.
|
|
fn from(_value: tokio::sync::mpsc::error::SendError<T>) -> Self {
|
|
Self::ChannelClosed
|
|
}
|
|
}
|
|
|
|
pub type ErrorTree = DerivationTree<PubGrubPackage, Range<Version>, UnavailableReason>;
|
|
|
|
/// A wrapper around [`pubgrub::error::NoSolutionError`] that displays a resolution failure report.
|
|
pub struct NoSolutionError {
|
|
error: pubgrub::NoSolutionError<UvDependencyProvider>,
|
|
index: InMemoryIndex,
|
|
available_versions: FxHashMap<PackageName, BTreeSet<Version>>,
|
|
available_indexes: FxHashMap<PackageName, BTreeSet<IndexUrl>>,
|
|
selector: CandidateSelector,
|
|
python_requirement: PythonRequirement,
|
|
index_locations: IndexLocations,
|
|
index_capabilities: IndexCapabilities,
|
|
unavailable_packages: FxHashMap<PackageName, UnavailablePackage>,
|
|
incomplete_packages: FxHashMap<PackageName, BTreeMap<Version, MetadataUnavailable>>,
|
|
fork_urls: ForkUrls,
|
|
fork_indexes: ForkIndexes,
|
|
env: ResolverEnvironment,
|
|
current_environment: MarkerEnvironment,
|
|
tags: Option<Tags>,
|
|
workspace_members: BTreeSet<PackageName>,
|
|
options: Options,
|
|
}
|
|
|
|
impl NoSolutionError {
|
|
/// Create a new [`NoSolutionError`] from a [`pubgrub::NoSolutionError`].
|
|
pub(crate) fn new(
|
|
error: pubgrub::NoSolutionError<UvDependencyProvider>,
|
|
index: InMemoryIndex,
|
|
available_versions: FxHashMap<PackageName, BTreeSet<Version>>,
|
|
available_indexes: FxHashMap<PackageName, BTreeSet<IndexUrl>>,
|
|
selector: CandidateSelector,
|
|
python_requirement: PythonRequirement,
|
|
index_locations: IndexLocations,
|
|
index_capabilities: IndexCapabilities,
|
|
unavailable_packages: FxHashMap<PackageName, UnavailablePackage>,
|
|
incomplete_packages: FxHashMap<PackageName, BTreeMap<Version, MetadataUnavailable>>,
|
|
fork_urls: ForkUrls,
|
|
fork_indexes: ForkIndexes,
|
|
env: ResolverEnvironment,
|
|
current_environment: MarkerEnvironment,
|
|
tags: Option<Tags>,
|
|
workspace_members: BTreeSet<PackageName>,
|
|
options: Options,
|
|
) -> Self {
|
|
Self {
|
|
error,
|
|
index,
|
|
available_versions,
|
|
available_indexes,
|
|
selector,
|
|
python_requirement,
|
|
index_locations,
|
|
index_capabilities,
|
|
unavailable_packages,
|
|
incomplete_packages,
|
|
fork_urls,
|
|
fork_indexes,
|
|
env,
|
|
current_environment,
|
|
tags,
|
|
workspace_members,
|
|
options,
|
|
}
|
|
}
|
|
|
|
/// Given a [`DerivationTree`], collapse any [`External::FromDependencyOf`] incompatibilities
|
|
/// wrap an [`PubGrubPackageInner::Extra`] package.
|
|
pub(crate) fn collapse_proxies(derivation_tree: ErrorTree) -> ErrorTree {
|
|
fn collapse(derivation_tree: ErrorTree) -> Option<ErrorTree> {
|
|
match derivation_tree {
|
|
DerivationTree::Derived(derived) => {
|
|
match (&*derived.cause1, &*derived.cause2) {
|
|
(
|
|
DerivationTree::External(External::FromDependencyOf(package1, ..)),
|
|
DerivationTree::External(External::FromDependencyOf(package2, ..)),
|
|
) if package1.is_proxy() && package2.is_proxy() => None,
|
|
(
|
|
DerivationTree::External(External::FromDependencyOf(package, ..)),
|
|
cause,
|
|
) if package.is_proxy() => collapse(cause.clone()),
|
|
(
|
|
cause,
|
|
DerivationTree::External(External::FromDependencyOf(package, ..)),
|
|
) if package.is_proxy() => collapse(cause.clone()),
|
|
(cause1, cause2) => {
|
|
let cause1 = collapse(cause1.clone());
|
|
let cause2 = collapse(cause2.clone());
|
|
match (cause1, cause2) {
|
|
(Some(cause1), Some(cause2)) => {
|
|
Some(DerivationTree::Derived(Derived {
|
|
cause1: Arc::new(cause1),
|
|
cause2: Arc::new(cause2),
|
|
..derived
|
|
}))
|
|
}
|
|
(Some(cause), None) | (None, Some(cause)) => Some(cause),
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
DerivationTree::External(_) => Some(derivation_tree),
|
|
}
|
|
}
|
|
|
|
collapse(derivation_tree)
|
|
.expect("derivation tree should contain at least one external term")
|
|
}
|
|
|
|
/// Simplifies the version ranges on any incompatibilities to remove the `[max]` sentinel.
|
|
///
|
|
/// The `[max]` sentinel is used to represent the maximum local version of a package, to
|
|
/// implement PEP 440 semantics for local version equality. For example, `1.0.0+foo` needs to
|
|
/// satisfy `==1.0.0`.
|
|
pub(crate) fn collapse_local_version_segments(derivation_tree: ErrorTree) -> ErrorTree {
|
|
fn strip(derivation_tree: ErrorTree) -> Option<ErrorTree> {
|
|
match derivation_tree {
|
|
DerivationTree::External(External::NotRoot(_, _)) => Some(derivation_tree),
|
|
DerivationTree::External(External::NoVersions(package, versions)) => {
|
|
if SentinelRange::from(&versions).is_complement() {
|
|
return None;
|
|
}
|
|
|
|
let versions = SentinelRange::from(&versions).strip();
|
|
Some(DerivationTree::External(External::NoVersions(
|
|
package, versions,
|
|
)))
|
|
}
|
|
DerivationTree::External(External::FromDependencyOf(
|
|
package1,
|
|
versions1,
|
|
package2,
|
|
versions2,
|
|
)) => {
|
|
let versions1 = SentinelRange::from(&versions1).strip();
|
|
let versions2 = SentinelRange::from(&versions2).strip();
|
|
Some(DerivationTree::External(External::FromDependencyOf(
|
|
package1, versions1, package2, versions2,
|
|
)))
|
|
}
|
|
DerivationTree::External(External::Custom(package, versions, reason)) => {
|
|
let versions = SentinelRange::from(&versions).strip();
|
|
Some(DerivationTree::External(External::Custom(
|
|
package, versions, reason,
|
|
)))
|
|
}
|
|
DerivationTree::Derived(mut derived) => {
|
|
let cause1 = strip((*derived.cause1).clone());
|
|
let cause2 = strip((*derived.cause2).clone());
|
|
match (cause1, cause2) {
|
|
(Some(cause1), Some(cause2)) => Some(DerivationTree::Derived(Derived {
|
|
cause1: Arc::new(cause1),
|
|
cause2: Arc::new(cause2),
|
|
terms: std::mem::take(&mut derived.terms)
|
|
.into_iter()
|
|
.map(|(pkg, term)| {
|
|
let term = match term {
|
|
Term::Positive(versions) => {
|
|
Term::Positive(SentinelRange::from(&versions).strip())
|
|
}
|
|
Term::Negative(versions) => {
|
|
Term::Negative(SentinelRange::from(&versions).strip())
|
|
}
|
|
};
|
|
(pkg, term)
|
|
})
|
|
.collect(),
|
|
shared_id: derived.shared_id,
|
|
})),
|
|
(Some(cause), None) | (None, Some(cause)) => Some(cause),
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
strip(derivation_tree).expect("derivation tree should contain at least one term")
|
|
}
|
|
|
|
/// Given a [`DerivationTree`], identify the largest required Python version that is missing.
|
|
pub fn find_requires_python(&self) -> LowerBound {
|
|
fn find(derivation_tree: &ErrorTree, minimum: &mut LowerBound) {
|
|
match derivation_tree {
|
|
DerivationTree::Derived(derived) => {
|
|
find(derived.cause1.as_ref(), minimum);
|
|
find(derived.cause2.as_ref(), minimum);
|
|
}
|
|
DerivationTree::External(External::FromDependencyOf(.., package, version)) => {
|
|
if let PubGrubPackageInner::Python(_) = &**package {
|
|
if let Some((lower, ..)) = version.bounding_range() {
|
|
let lower = LowerBound::new(lower.cloned());
|
|
if lower > *minimum {
|
|
*minimum = lower;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
DerivationTree::External(_) => {}
|
|
}
|
|
}
|
|
|
|
let mut minimum = LowerBound::default();
|
|
find(&self.error, &mut minimum);
|
|
minimum
|
|
}
|
|
|
|
/// Initialize a [`NoSolutionHeader`] for this error.
|
|
pub fn header(&self) -> NoSolutionHeader {
|
|
NoSolutionHeader::new(self.env.clone())
|
|
}
|
|
|
|
/// Get the conflict derivation tree for external analysis
|
|
pub fn derivation_tree(&self) -> &ErrorTree {
|
|
&self.error
|
|
}
|
|
|
|
/// Hint at limiting the resolver environment if universal resolution failed for a target
|
|
/// that is not the current platform or not the current Python version.
|
|
fn hint_disjoint_targets(&self, f: &mut Formatter) -> std::fmt::Result {
|
|
// Only applicable to universal resolution.
|
|
let Some(markers) = self.env.fork_markers() else {
|
|
return Ok(());
|
|
};
|
|
|
|
// TODO(konsti): This is a crude approximation to telling the user the difference
|
|
// between their Python version and the relevant Python version range from the marker.
|
|
let current_python_version = self.current_environment.python_version().version.clone();
|
|
let current_python_marker = MarkerTree::expression(MarkerExpression::Version {
|
|
key: MarkerValueVersion::PythonVersion,
|
|
specifier: VersionSpecifier::equals_version(current_python_version.clone()),
|
|
});
|
|
if markers.is_disjoint(current_python_marker) {
|
|
write!(
|
|
f,
|
|
"\n\n{}{} While the active Python version is {}, \
|
|
the resolution failed for other Python versions supported by your \
|
|
project. Consider limiting your project's supported Python versions \
|
|
using `requires-python`.",
|
|
"hint".bold().cyan(),
|
|
":".bold(),
|
|
current_python_version,
|
|
)?;
|
|
} else if !markers.evaluate(&self.current_environment, &[]) {
|
|
write!(
|
|
f,
|
|
"\n\n{}{} The resolution failed for an environment that is not the current one, \
|
|
consider limiting the environments with `tool.uv.environments`.",
|
|
"hint".bold().cyan(),
|
|
":".bold(),
|
|
)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Get the packages that are involved in this error.
|
|
pub fn packages(&self) -> impl Iterator<Item = &PackageName> {
|
|
self.error
|
|
.packages()
|
|
.into_iter()
|
|
.filter_map(|p| p.name())
|
|
.unique()
|
|
}
|
|
}
|
|
|
|
impl std::fmt::Debug for NoSolutionError {
|
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
// Include every field except `index`, which doesn't implement `Debug`.
|
|
let Self {
|
|
error,
|
|
index: _,
|
|
available_versions,
|
|
available_indexes,
|
|
selector,
|
|
python_requirement,
|
|
index_locations,
|
|
index_capabilities,
|
|
unavailable_packages,
|
|
incomplete_packages,
|
|
fork_urls,
|
|
fork_indexes,
|
|
env,
|
|
current_environment,
|
|
tags,
|
|
workspace_members,
|
|
options,
|
|
} = self;
|
|
f.debug_struct("NoSolutionError")
|
|
.field("error", error)
|
|
.field("available_versions", available_versions)
|
|
.field("available_indexes", available_indexes)
|
|
.field("selector", selector)
|
|
.field("python_requirement", python_requirement)
|
|
.field("index_locations", index_locations)
|
|
.field("index_capabilities", index_capabilities)
|
|
.field("unavailable_packages", unavailable_packages)
|
|
.field("incomplete_packages", incomplete_packages)
|
|
.field("fork_urls", fork_urls)
|
|
.field("fork_indexes", fork_indexes)
|
|
.field("env", env)
|
|
.field("current_environment", current_environment)
|
|
.field("tags", tags)
|
|
.field("workspace_members", workspace_members)
|
|
.field("options", options)
|
|
.finish()
|
|
}
|
|
}
|
|
|
|
impl std::error::Error for NoSolutionError {}
|
|
|
|
impl std::fmt::Display for NoSolutionError {
|
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
// Write the derivation report.
|
|
let formatter = PubGrubReportFormatter {
|
|
available_versions: &self.available_versions,
|
|
python_requirement: &self.python_requirement,
|
|
workspace_members: &self.workspace_members,
|
|
tags: self.tags.as_ref(),
|
|
};
|
|
|
|
// Transform the error tree for reporting
|
|
let mut tree = self.error.clone();
|
|
simplify_derivation_tree_markers(&self.python_requirement, &mut tree);
|
|
let should_display_tree = std::env::var_os(EnvVars::UV_INTERNAL__SHOW_DERIVATION_TREE)
|
|
.is_some()
|
|
|| tracing::enabled!(tracing::Level::TRACE);
|
|
|
|
if should_display_tree {
|
|
display_tree(&tree, "Resolver derivation tree before reduction");
|
|
}
|
|
|
|
collapse_no_versions_of_workspace_members(&mut tree, &self.workspace_members);
|
|
|
|
if self.workspace_members.len() == 1 {
|
|
let project = self.workspace_members.iter().next().unwrap();
|
|
drop_root_dependency_on_project(&mut tree, project);
|
|
}
|
|
|
|
collapse_unavailable_versions(&mut tree);
|
|
collapse_redundant_depends_on_no_versions(&mut tree);
|
|
|
|
simplify_derivation_tree_ranges(
|
|
&mut tree,
|
|
&self.available_versions,
|
|
&self.selector,
|
|
&self.env,
|
|
);
|
|
|
|
// This needs to be applied _after_ simplification of the ranges
|
|
collapse_redundant_no_versions(&mut tree);
|
|
|
|
while collapse_redundant_no_versions_tree(&mut tree) {
|
|
// Continue collapsing until no more redundant nodes are found
|
|
}
|
|
|
|
if should_display_tree {
|
|
display_tree(&tree, "Resolver derivation tree after reduction");
|
|
}
|
|
|
|
let report = DefaultStringReporter::report_with_formatter(&tree, &formatter);
|
|
write!(f, "{report}")?;
|
|
|
|
// Include any additional hints.
|
|
let mut additional_hints = IndexSet::default();
|
|
formatter.generate_hints(
|
|
&tree,
|
|
&self.index,
|
|
&self.selector,
|
|
&self.index_locations,
|
|
&self.index_capabilities,
|
|
&self.available_indexes,
|
|
&self.unavailable_packages,
|
|
&self.incomplete_packages,
|
|
&self.fork_urls,
|
|
&self.fork_indexes,
|
|
&self.env,
|
|
self.tags.as_ref(),
|
|
&self.workspace_members,
|
|
&self.options,
|
|
&mut additional_hints,
|
|
);
|
|
for hint in additional_hints {
|
|
write!(f, "\n\n{hint}")?;
|
|
}
|
|
|
|
self.hint_disjoint_targets(f)?;
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[allow(clippy::print_stderr)]
|
|
fn display_tree(
|
|
error: &DerivationTree<PubGrubPackage, Range<Version>, UnavailableReason>,
|
|
name: &str,
|
|
) {
|
|
let mut lines = Vec::new();
|
|
display_tree_inner(error, &mut lines, 0);
|
|
lines.reverse();
|
|
|
|
if std::env::var_os(EnvVars::UV_INTERNAL__SHOW_DERIVATION_TREE).is_some() {
|
|
eprintln!("{name}\n{}", lines.join("\n"));
|
|
} else {
|
|
trace!("{name}\n{}", lines.join("\n"));
|
|
}
|
|
}
|
|
|
|
fn display_tree_inner(
|
|
error: &DerivationTree<PubGrubPackage, Range<Version>, UnavailableReason>,
|
|
lines: &mut Vec<String>,
|
|
depth: usize,
|
|
) {
|
|
let prefix = " ".repeat(depth).to_string();
|
|
match error {
|
|
DerivationTree::Derived(derived) => {
|
|
display_tree_inner(&derived.cause1, lines, depth + 1);
|
|
display_tree_inner(&derived.cause2, lines, depth + 1);
|
|
for (package, term) in &derived.terms {
|
|
match term {
|
|
Term::Positive(versions) => {
|
|
lines.push(format!("{prefix}term {package}{versions}"));
|
|
}
|
|
Term::Negative(versions) => {
|
|
lines.push(format!("{prefix}term not {package}{versions}"));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
DerivationTree::External(external) => match external {
|
|
External::FromDependencyOf(package, version, dependency, dependency_version) => {
|
|
lines.push(format!(
|
|
"{prefix}{package}{version} depends on {dependency}{dependency_version}"
|
|
));
|
|
}
|
|
External::Custom(package, versions, reason) => match reason {
|
|
UnavailableReason::Package(_) => {
|
|
lines.push(format!("{prefix}{package} {reason}"));
|
|
}
|
|
UnavailableReason::Version(_) => {
|
|
lines.push(format!("{prefix}{package}{versions} {reason}"));
|
|
}
|
|
},
|
|
External::NoVersions(package, versions) => {
|
|
lines.push(format!("{prefix}no versions of {package}{versions}"));
|
|
}
|
|
External::NotRoot(package, versions) => {
|
|
lines.push(format!("{prefix}not root {package}{versions}"));
|
|
}
|
|
},
|
|
}
|
|
}
|
|
|
|
fn collapse_redundant_no_versions(
|
|
tree: &mut DerivationTree<PubGrubPackage, Range<Version>, UnavailableReason>,
|
|
) {
|
|
match tree {
|
|
DerivationTree::External(_) => {}
|
|
DerivationTree::Derived(derived) => {
|
|
match (
|
|
Arc::make_mut(&mut derived.cause1),
|
|
Arc::make_mut(&mut derived.cause2),
|
|
) {
|
|
// If we have a node for a package with no versions...
|
|
(
|
|
DerivationTree::External(External::NoVersions(package, versions)),
|
|
ref mut other,
|
|
)
|
|
| (
|
|
ref mut other,
|
|
DerivationTree::External(External::NoVersions(package, versions)),
|
|
) => {
|
|
// First, always recursively visit the other side of the tree
|
|
collapse_redundant_no_versions(other);
|
|
|
|
// Retrieve the nearest terms, either alongside this node or from the parent.
|
|
let package_terms = if let DerivationTree::Derived(derived) = other {
|
|
derived.terms.get(package)
|
|
} else {
|
|
derived.terms.get(package)
|
|
};
|
|
|
|
let Some(Term::Positive(term)) = package_terms else {
|
|
return;
|
|
};
|
|
|
|
let versions = versions.complement();
|
|
|
|
// If we're disqualifying a single version, this is important to retain, e.g,
|
|
// for `only foo==1.0.0 is available`
|
|
if versions.as_singleton().is_some() {
|
|
return;
|
|
}
|
|
|
|
// If the range in the conclusion (terms) matches the range of no versions,
|
|
// then we'll drop this node. If the range is "all versions", then there's no
|
|
// also no need to enumerate the available versions.
|
|
if *term != Range::full() && *term != versions {
|
|
return;
|
|
}
|
|
|
|
*tree = other.clone();
|
|
}
|
|
// If not, just recurse
|
|
_ => {
|
|
collapse_redundant_no_versions(Arc::make_mut(&mut derived.cause1));
|
|
collapse_redundant_no_versions(Arc::make_mut(&mut derived.cause2));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Given a [`DerivationTree`], collapse any derived trees with two `NoVersions` nodes for the same
|
|
/// package. For example, if we have a tree like:
|
|
///
|
|
/// ```text
|
|
/// term Python>=3.7.9
|
|
/// no versions of Python>=3.7.9, <3.8
|
|
/// no versions of Python>=3.8
|
|
/// ```
|
|
///
|
|
/// We can simplify this to:
|
|
///
|
|
/// ```text
|
|
/// no versions of Python>=3.7.9
|
|
/// ```
|
|
///
|
|
/// This function returns a `bool` indicating if a change was made. This allows for repeated calls,
|
|
/// e.g., the following tree contains nested redundant trees:
|
|
///
|
|
/// ```text
|
|
/// term Python>=3.10
|
|
/// no versions of Python>=3.11, <3.12
|
|
/// term Python>=3.10, <3.11 | >=3.12
|
|
/// no versions of Python>=3.12
|
|
/// no versions of Python>=3.10, <3.11
|
|
/// ```
|
|
///
|
|
/// We can simplify this to:
|
|
///
|
|
/// ```text
|
|
/// no versions of Python>=3.10
|
|
/// ```
|
|
///
|
|
/// This appears to be common with the way the resolver currently models Python version
|
|
/// incompatibilities.
|
|
fn collapse_redundant_no_versions_tree(
|
|
tree: &mut DerivationTree<PubGrubPackage, Range<Version>, UnavailableReason>,
|
|
) -> bool {
|
|
match tree {
|
|
DerivationTree::External(_) => false,
|
|
DerivationTree::Derived(derived) => {
|
|
match (
|
|
Arc::make_mut(&mut derived.cause1),
|
|
Arc::make_mut(&mut derived.cause2),
|
|
) {
|
|
// If we have a tree with two `NoVersions` nodes for the same package...
|
|
(
|
|
DerivationTree::External(External::NoVersions(package, versions)),
|
|
DerivationTree::External(External::NoVersions(other_package, other_versions)),
|
|
) if package == other_package => {
|
|
// Retrieve the terms from the parent.
|
|
let Some(Term::Positive(term)) = derived.terms.get(package) else {
|
|
return false;
|
|
};
|
|
|
|
// If they're both subsets of the term, then drop this node in favor of the term
|
|
if versions.subset_of(term) && other_versions.subset_of(term) {
|
|
*tree = DerivationTree::External(External::NoVersions(
|
|
package.clone(),
|
|
term.clone(),
|
|
));
|
|
return true;
|
|
}
|
|
|
|
false
|
|
}
|
|
// If not, just recurse
|
|
_ => {
|
|
collapse_redundant_no_versions_tree(Arc::make_mut(&mut derived.cause1))
|
|
|| collapse_redundant_no_versions_tree(Arc::make_mut(&mut derived.cause2))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Given a [`DerivationTree`], collapse any `NoVersion` incompatibilities for workspace members
|
|
/// to avoid saying things like "only <workspace-member>==0.1.0 is available".
|
|
fn collapse_no_versions_of_workspace_members(
|
|
tree: &mut DerivationTree<PubGrubPackage, Range<Version>, UnavailableReason>,
|
|
workspace_members: &BTreeSet<PackageName>,
|
|
) {
|
|
match tree {
|
|
DerivationTree::External(_) => {}
|
|
DerivationTree::Derived(derived) => {
|
|
match (
|
|
Arc::make_mut(&mut derived.cause1),
|
|
Arc::make_mut(&mut derived.cause2),
|
|
) {
|
|
// If we have a node for a package with no versions...
|
|
(DerivationTree::External(External::NoVersions(package, _)), ref mut other)
|
|
| (ref mut other, DerivationTree::External(External::NoVersions(package, _))) => {
|
|
// First, always recursively visit the other side of the tree
|
|
collapse_no_versions_of_workspace_members(other, workspace_members);
|
|
|
|
// Then, if the package is a workspace member...
|
|
let (PubGrubPackageInner::Package { name, .. }
|
|
| PubGrubPackageInner::Extra { name, .. }
|
|
| PubGrubPackageInner::Dev { name, .. }) = &**package
|
|
else {
|
|
return;
|
|
};
|
|
if !workspace_members.contains(name) {
|
|
return;
|
|
}
|
|
|
|
// Replace this node with the other tree
|
|
*tree = other.clone();
|
|
}
|
|
// If not, just recurse
|
|
_ => {
|
|
collapse_no_versions_of_workspace_members(
|
|
Arc::make_mut(&mut derived.cause1),
|
|
workspace_members,
|
|
);
|
|
collapse_no_versions_of_workspace_members(
|
|
Arc::make_mut(&mut derived.cause2),
|
|
workspace_members,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Given a [`DerivationTree`], collapse `NoVersions` incompatibilities that are redundant children
|
|
/// of a dependency. For example, if we have a tree like:
|
|
///
|
|
/// ```text
|
|
/// A>=1,<2 depends on B
|
|
/// A has no versions >1,<2
|
|
/// C depends on A>=1,<2
|
|
/// ```
|
|
///
|
|
/// We can simplify this to `C depends A>=1 and A>=1 depends on B so C depends on B` without
|
|
/// explaining that there are no other versions of A. This is dependent on range of A in "A depends
|
|
/// on" being a subset of range of A in "depends on A". For example, in a tree like:
|
|
///
|
|
/// ```text
|
|
/// A>=1,<3 depends on B
|
|
/// A has no versions >2,<3
|
|
/// C depends on A>=2,<3
|
|
/// ```
|
|
///
|
|
/// We cannot say `C depends on A>=2 and A>=1 depends on B so C depends on B` because there is a
|
|
/// hole in the range — `A>=1,<3` is not a subset of `A>=2,<3`.
|
|
fn collapse_redundant_depends_on_no_versions(
|
|
tree: &mut DerivationTree<PubGrubPackage, Range<Version>, UnavailableReason>,
|
|
) {
|
|
match tree {
|
|
DerivationTree::External(_) => {}
|
|
DerivationTree::Derived(derived) => {
|
|
// If one node is a dependency incompatibility...
|
|
match (
|
|
Arc::make_mut(&mut derived.cause1),
|
|
Arc::make_mut(&mut derived.cause2),
|
|
) {
|
|
(
|
|
DerivationTree::External(External::FromDependencyOf(package, versions, _, _)),
|
|
ref mut other,
|
|
)
|
|
| (
|
|
ref mut other,
|
|
DerivationTree::External(External::FromDependencyOf(package, versions, _, _)),
|
|
) => {
|
|
// Check if the other node is the relevant form of subtree...
|
|
collapse_redundant_depends_on_no_versions_inner(other, package, versions);
|
|
}
|
|
// If not, just recurse
|
|
_ => {
|
|
collapse_redundant_depends_on_no_versions(Arc::make_mut(&mut derived.cause1));
|
|
collapse_redundant_depends_on_no_versions(Arc::make_mut(&mut derived.cause2));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Helper for [`collapse_redundant_depends_on_no_versions`].
|
|
fn collapse_redundant_depends_on_no_versions_inner(
|
|
tree: &mut DerivationTree<PubGrubPackage, Range<Version>, UnavailableReason>,
|
|
package: &PubGrubPackage,
|
|
versions: &Range<Version>,
|
|
) {
|
|
match tree {
|
|
DerivationTree::External(_) => {}
|
|
DerivationTree::Derived(derived) => {
|
|
// If we're a subtree with dependency and no versions incompatibilities...
|
|
match (&*derived.cause1, &*derived.cause2) {
|
|
(
|
|
DerivationTree::External(External::NoVersions(no_versions_package, _)),
|
|
dependency_clause @ DerivationTree::External(External::FromDependencyOf(
|
|
_,
|
|
_,
|
|
dependency_package,
|
|
dependency_versions,
|
|
)),
|
|
)
|
|
| (
|
|
dependency_clause @ DerivationTree::External(External::FromDependencyOf(
|
|
_,
|
|
_,
|
|
dependency_package,
|
|
dependency_versions,
|
|
)),
|
|
DerivationTree::External(External::NoVersions(no_versions_package, _)),
|
|
)
|
|
// And these incompatibilities (and the parent incompatibility) all are referring to
|
|
// the same package...
|
|
if no_versions_package == dependency_package
|
|
&& package == no_versions_package
|
|
// And parent dependency versions are a subset of the versions in this tree...
|
|
&& versions.subset_of(dependency_versions) =>
|
|
{
|
|
// Enumerating the available versions will be redundant and we can drop the no
|
|
// versions clause entirely in favor of the dependency clause.
|
|
*tree = dependency_clause.clone();
|
|
|
|
// Note we are at a leaf of the tree so there's no further recursion to do
|
|
}
|
|
// If not, just recurse
|
|
_ => {
|
|
collapse_redundant_depends_on_no_versions(Arc::make_mut(&mut derived.cause1));
|
|
collapse_redundant_depends_on_no_versions(Arc::make_mut(&mut derived.cause2));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Simplifies the markers on pubgrub packages in the given derivation tree
|
|
/// according to the given Python requirement.
|
|
///
|
|
/// For example, when there's a dependency like `foo ; python_version >=
|
|
/// '3.11'` and `requires-python = '>=3.11'`, this simplification will remove
|
|
/// the `python_version >= '3.11'` marker since it's implied to be true by
|
|
/// the `requires-python` setting. This simplifies error messages by reducing
|
|
/// noise.
|
|
fn simplify_derivation_tree_markers(
|
|
python_requirement: &PythonRequirement,
|
|
tree: &mut DerivationTree<PubGrubPackage, Range<Version>, UnavailableReason>,
|
|
) {
|
|
match tree {
|
|
DerivationTree::External(External::NotRoot(pkg, _)) => {
|
|
pkg.simplify_markers(python_requirement);
|
|
}
|
|
DerivationTree::External(External::NoVersions(pkg, _)) => {
|
|
pkg.simplify_markers(python_requirement);
|
|
}
|
|
DerivationTree::External(External::FromDependencyOf(pkg1, _, pkg2, _)) => {
|
|
pkg1.simplify_markers(python_requirement);
|
|
pkg2.simplify_markers(python_requirement);
|
|
}
|
|
DerivationTree::External(External::Custom(pkg, _, _)) => {
|
|
pkg.simplify_markers(python_requirement);
|
|
}
|
|
DerivationTree::Derived(derived) => {
|
|
derived.terms = std::mem::take(&mut derived.terms)
|
|
.into_iter()
|
|
.map(|(mut pkg, term)| {
|
|
pkg.simplify_markers(python_requirement);
|
|
(pkg, term)
|
|
})
|
|
.collect();
|
|
simplify_derivation_tree_markers(
|
|
python_requirement,
|
|
Arc::make_mut(&mut derived.cause1),
|
|
);
|
|
simplify_derivation_tree_markers(
|
|
python_requirement,
|
|
Arc::make_mut(&mut derived.cause2),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Given a [`DerivationTree`], collapse incompatibilities for versions of a package that are
|
|
/// unavailable for the same reason to avoid repeating the same message for every unavailable
|
|
/// version.
|
|
fn collapse_unavailable_versions(
|
|
tree: &mut DerivationTree<PubGrubPackage, Range<Version>, UnavailableReason>,
|
|
) {
|
|
match tree {
|
|
DerivationTree::External(_) => {}
|
|
DerivationTree::Derived(derived) => {
|
|
match (
|
|
Arc::make_mut(&mut derived.cause1),
|
|
Arc::make_mut(&mut derived.cause2),
|
|
) {
|
|
// If we have a node for unavailable package versions
|
|
(
|
|
DerivationTree::External(External::Custom(package, versions, reason)),
|
|
ref mut other,
|
|
)
|
|
| (
|
|
ref mut other,
|
|
DerivationTree::External(External::Custom(package, versions, reason)),
|
|
) => {
|
|
// First, recursively collapse the other side of the tree
|
|
collapse_unavailable_versions(other);
|
|
|
|
// If it's not a derived tree, nothing to do.
|
|
let DerivationTree::Derived(Derived {
|
|
terms,
|
|
shared_id,
|
|
cause1,
|
|
cause2,
|
|
}) = other
|
|
else {
|
|
return;
|
|
};
|
|
|
|
// If the other tree has an unavailable package...
|
|
match (&**cause1, &**cause2) {
|
|
// Note the following cases are the same, but we need two matches to retain
|
|
// the ordering of the causes
|
|
(
|
|
_,
|
|
DerivationTree::External(External::Custom(
|
|
other_package,
|
|
other_versions,
|
|
other_reason,
|
|
)),
|
|
) => {
|
|
// And the package and reason are the same...
|
|
if package == other_package && reason == other_reason {
|
|
// Collapse both into a new node, with a union of their ranges
|
|
let versions = other_versions.union(versions);
|
|
let mut terms = terms.clone();
|
|
if let Some(Term::Positive(range)) = terms.get_mut(package) {
|
|
*range = versions.clone();
|
|
}
|
|
*tree = DerivationTree::Derived(Derived {
|
|
terms,
|
|
shared_id: *shared_id,
|
|
cause1: cause1.clone(),
|
|
cause2: Arc::new(DerivationTree::External(External::Custom(
|
|
package.clone(),
|
|
versions,
|
|
reason.clone(),
|
|
))),
|
|
});
|
|
}
|
|
}
|
|
(
|
|
DerivationTree::External(External::Custom(
|
|
other_package,
|
|
other_versions,
|
|
other_reason,
|
|
)),
|
|
_,
|
|
) => {
|
|
// And the package and reason are the same...
|
|
if package == other_package && reason == other_reason {
|
|
// Collapse both into a new node, with a union of their ranges
|
|
let versions = other_versions.union(versions);
|
|
let mut terms = terms.clone();
|
|
if let Some(Term::Positive(range)) = terms.get_mut(package) {
|
|
*range = versions.clone();
|
|
}
|
|
*tree = DerivationTree::Derived(Derived {
|
|
terms,
|
|
shared_id: *shared_id,
|
|
cause1: Arc::new(DerivationTree::External(External::Custom(
|
|
package.clone(),
|
|
versions,
|
|
reason.clone(),
|
|
))),
|
|
cause2: cause2.clone(),
|
|
});
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
// If not, just recurse
|
|
_ => {
|
|
collapse_unavailable_versions(Arc::make_mut(&mut derived.cause1));
|
|
collapse_unavailable_versions(Arc::make_mut(&mut derived.cause2));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Given a [`DerivationTree`], drop dependency incompatibilities from the root
|
|
/// to the project.
|
|
///
|
|
/// Intended to effectively change the root to a workspace member in single project
|
|
/// workspaces, avoiding a level of indirection like "And because your project
|
|
/// requires your project, we can conclude that your project's requirements are
|
|
/// unsatisfiable."
|
|
fn drop_root_dependency_on_project(
|
|
tree: &mut DerivationTree<PubGrubPackage, Range<Version>, UnavailableReason>,
|
|
project: &PackageName,
|
|
) {
|
|
match tree {
|
|
DerivationTree::External(_) => {}
|
|
DerivationTree::Derived(derived) => {
|
|
match (
|
|
Arc::make_mut(&mut derived.cause1),
|
|
Arc::make_mut(&mut derived.cause2),
|
|
) {
|
|
// If one node is a dependency incompatibility...
|
|
(
|
|
DerivationTree::External(External::FromDependencyOf(package, _, dependency, _)),
|
|
ref mut other,
|
|
)
|
|
| (
|
|
ref mut other,
|
|
DerivationTree::External(External::FromDependencyOf(package, _, dependency, _)),
|
|
) => {
|
|
// And the parent is the root package...
|
|
if !matches!(&**package, PubGrubPackageInner::Root(_)) {
|
|
return;
|
|
}
|
|
|
|
// And the dependency is the project...
|
|
let PubGrubPackageInner::Package { name, .. } = &**dependency else {
|
|
return;
|
|
};
|
|
if name != project {
|
|
return;
|
|
}
|
|
|
|
// Recursively collapse the other side of the tree
|
|
drop_root_dependency_on_project(other, project);
|
|
|
|
// Then, replace this node with the other tree
|
|
*tree = other.clone();
|
|
}
|
|
// If not, just recurse
|
|
_ => {
|
|
drop_root_dependency_on_project(Arc::make_mut(&mut derived.cause1), project);
|
|
drop_root_dependency_on_project(Arc::make_mut(&mut derived.cause2), project);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A version range that may include local version sentinels (`+[max]`).
|
|
#[derive(Debug)]
|
|
pub struct SentinelRange<'range>(&'range Range<Version>);
|
|
|
|
impl<'range> From<&'range Range<Version>> for SentinelRange<'range> {
|
|
fn from(range: &'range Range<Version>) -> Self {
|
|
Self(range)
|
|
}
|
|
}
|
|
|
|
impl SentinelRange<'_> {
|
|
/// Returns `true` if the range appears to be, e.g., `>=1.0.0, <1.0.0+[max]`.
|
|
pub fn is_sentinel(&self) -> bool {
|
|
self.0.iter().all(|(lower, upper)| {
|
|
let (Bound::Included(lower), Bound::Excluded(upper)) = (lower, upper) else {
|
|
return false;
|
|
};
|
|
if !lower.local().is_empty() {
|
|
return false;
|
|
}
|
|
if upper.local() != LocalVersionSlice::Max {
|
|
return false;
|
|
}
|
|
*lower == upper.clone().without_local()
|
|
})
|
|
}
|
|
|
|
/// Returns `true` if the range appears to be, e.g., `>1.0.0, <1.0.0+[max]` (i.e., a sentinel
|
|
/// range with the non-local version removed).
|
|
pub fn is_complement(&self) -> bool {
|
|
self.0.iter().all(|(lower, upper)| {
|
|
let (Bound::Excluded(lower), Bound::Excluded(upper)) = (lower, upper) else {
|
|
return false;
|
|
};
|
|
if !lower.local().is_empty() {
|
|
return false;
|
|
}
|
|
if upper.local() != LocalVersionSlice::Max {
|
|
return false;
|
|
}
|
|
*lower == upper.clone().without_local()
|
|
})
|
|
}
|
|
|
|
/// Remove local versions sentinels (`+[max]`) from the version ranges.
|
|
pub fn strip(&self) -> Ranges<Version> {
|
|
self.0
|
|
.iter()
|
|
.map(|(lower, upper)| Self::strip_sentinel(lower.clone(), upper.clone()))
|
|
.collect()
|
|
}
|
|
|
|
/// Remove local versions sentinels (`+[max]`) from the interval.
|
|
fn strip_sentinel(
|
|
mut lower: Bound<Version>,
|
|
mut upper: Bound<Version>,
|
|
) -> (Bound<Version>, Bound<Version>) {
|
|
match (&lower, &upper) {
|
|
(Bound::Unbounded, Bound::Unbounded) => {}
|
|
(Bound::Unbounded, Bound::Included(v)) => {
|
|
// `<=1.0.0+[max]` is equivalent to `<=1.0.0`
|
|
if v.local() == LocalVersionSlice::Max {
|
|
upper = Bound::Included(v.clone().without_local());
|
|
}
|
|
}
|
|
(Bound::Unbounded, Bound::Excluded(v)) => {
|
|
// `<1.0.0+[max]` is equivalent to `<1.0.0`
|
|
if v.local() == LocalVersionSlice::Max {
|
|
upper = Bound::Excluded(v.clone().without_local());
|
|
}
|
|
}
|
|
(Bound::Included(v), Bound::Unbounded) => {
|
|
// `>=1.0.0+[max]` is equivalent to `>1.0.0`
|
|
if v.local() == LocalVersionSlice::Max {
|
|
lower = Bound::Excluded(v.clone().without_local());
|
|
}
|
|
}
|
|
(Bound::Included(v), Bound::Included(b)) => {
|
|
// `>=1.0.0+[max]` is equivalent to `>1.0.0`
|
|
if v.local() == LocalVersionSlice::Max {
|
|
lower = Bound::Excluded(v.clone().without_local());
|
|
}
|
|
// `<=1.0.0+[max]` is equivalent to `<=1.0.0`
|
|
if b.local() == LocalVersionSlice::Max {
|
|
upper = Bound::Included(b.clone().without_local());
|
|
}
|
|
}
|
|
(Bound::Included(v), Bound::Excluded(b)) => {
|
|
// `>=1.0.0+[max]` is equivalent to `>1.0.0`
|
|
if v.local() == LocalVersionSlice::Max {
|
|
lower = Bound::Excluded(v.clone().without_local());
|
|
}
|
|
// `<1.0.0+[max]` is equivalent to `<1.0.0`
|
|
if b.local() == LocalVersionSlice::Max {
|
|
upper = Bound::Included(b.clone().without_local());
|
|
}
|
|
}
|
|
(Bound::Excluded(v), Bound::Unbounded) => {
|
|
// `>1.0.0+[max]` is equivalent to `>1.0.0`
|
|
if v.local() == LocalVersionSlice::Max {
|
|
lower = Bound::Excluded(v.clone().without_local());
|
|
}
|
|
}
|
|
(Bound::Excluded(v), Bound::Included(b)) => {
|
|
// `>1.0.0+[max]` is equivalent to `>1.0.0`
|
|
if v.local() == LocalVersionSlice::Max {
|
|
lower = Bound::Excluded(v.clone().without_local());
|
|
}
|
|
// `<=1.0.0+[max]` is equivalent to `<=1.0.0`
|
|
if b.local() == LocalVersionSlice::Max {
|
|
upper = Bound::Included(b.clone().without_local());
|
|
}
|
|
}
|
|
(Bound::Excluded(v), Bound::Excluded(b)) => {
|
|
// `>1.0.0+[max]` is equivalent to `>1.0.0`
|
|
if v.local() == LocalVersionSlice::Max {
|
|
lower = Bound::Excluded(v.clone().without_local());
|
|
}
|
|
// `<1.0.0+[max]` is equivalent to `<1.0.0`
|
|
if b.local() == LocalVersionSlice::Max {
|
|
upper = Bound::Excluded(b.clone().without_local());
|
|
}
|
|
}
|
|
}
|
|
(lower, upper)
|
|
}
|
|
}
|
|
|
|
/// A prefix match, e.g., `==2.4.*`, which is desugared to a range like `>=2.4.dev0,<2.5.dev0`.
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub(crate) struct PrefixMatch<'a> {
|
|
version: &'a Version,
|
|
}
|
|
|
|
impl<'a> PrefixMatch<'a> {
|
|
/// Determine whether a given range is equivalent to a prefix match (e.g., `==2.4.*`).
|
|
///
|
|
/// Prefix matches are desugared to (e.g.) `>=2.4.dev0,<2.5.dev0`, but we want to render them
|
|
/// as `==2.4.*` in error messages.
|
|
pub(crate) fn from_range(lower: &'a Bound<Version>, upper: &'a Bound<Version>) -> Option<Self> {
|
|
let Bound::Included(lower) = lower else {
|
|
return None;
|
|
};
|
|
let Bound::Excluded(upper) = upper else {
|
|
return None;
|
|
};
|
|
if lower.is_pre() || lower.is_post() || lower.is_local() {
|
|
return None;
|
|
}
|
|
if upper.is_pre() || upper.is_post() || upper.is_local() {
|
|
return None;
|
|
}
|
|
if lower.dev() != Some(0) {
|
|
return None;
|
|
}
|
|
if upper.dev() != Some(0) {
|
|
return None;
|
|
}
|
|
if lower.release().len() != upper.release().len() {
|
|
return None;
|
|
}
|
|
|
|
// All segments should be the same, except the last one, which should be incremented.
|
|
let num_segments = lower.release().len();
|
|
for (i, (lower, upper)) in lower
|
|
.release()
|
|
.iter()
|
|
.zip(upper.release().iter())
|
|
.enumerate()
|
|
{
|
|
if i == num_segments - 1 {
|
|
if lower + 1 != *upper {
|
|
return None;
|
|
}
|
|
} else {
|
|
if lower != upper {
|
|
return None;
|
|
}
|
|
}
|
|
}
|
|
|
|
Some(PrefixMatch { version: lower })
|
|
}
|
|
}
|
|
|
|
impl std::fmt::Display for PrefixMatch<'_> {
|
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
write!(f, "=={}.*", self.version.only_release())
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct NoSolutionHeader {
|
|
/// The [`ResolverEnvironment`] that caused the failure.
|
|
env: ResolverEnvironment,
|
|
/// The additional context for the resolution failure.
|
|
context: Option<&'static str>,
|
|
}
|
|
|
|
impl NoSolutionHeader {
|
|
/// Create a new [`NoSolutionHeader`] with the given [`ResolverEnvironment`].
|
|
pub fn new(env: ResolverEnvironment) -> Self {
|
|
Self { env, context: None }
|
|
}
|
|
|
|
/// Set the context for the resolution failure.
|
|
#[must_use]
|
|
pub fn with_context(mut self, context: &'static str) -> Self {
|
|
self.context = Some(context);
|
|
self
|
|
}
|
|
}
|
|
|
|
impl std::fmt::Display for NoSolutionHeader {
|
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
match (self.context, self.env.end_user_fork_display()) {
|
|
(None, None) => write!(f, "No solution found when resolving dependencies:"),
|
|
(Some(context), None) => write!(
|
|
f,
|
|
"No solution found when resolving {context} dependencies:"
|
|
),
|
|
(None, Some(split)) => write!(
|
|
f,
|
|
"No solution found when resolving dependencies for {split}:"
|
|
),
|
|
(Some(context), Some(split)) => write!(
|
|
f,
|
|
"No solution found when resolving {context} dependencies for {split}:"
|
|
),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Given a [`DerivationTree`], simplify version ranges using the available versions for each
|
|
/// package.
|
|
fn simplify_derivation_tree_ranges(
|
|
tree: &mut DerivationTree<PubGrubPackage, Range<Version>, UnavailableReason>,
|
|
available_versions: &FxHashMap<PackageName, BTreeSet<Version>>,
|
|
candidate_selector: &CandidateSelector,
|
|
resolver_environment: &ResolverEnvironment,
|
|
) {
|
|
match tree {
|
|
DerivationTree::External(external) => match external {
|
|
External::FromDependencyOf(package1, versions1, package2, versions2) => {
|
|
if let Some(simplified) = simplify_range(
|
|
versions1,
|
|
package1,
|
|
available_versions,
|
|
candidate_selector,
|
|
resolver_environment,
|
|
) {
|
|
*versions1 = simplified;
|
|
}
|
|
if let Some(simplified) = simplify_range(
|
|
versions2,
|
|
package2,
|
|
available_versions,
|
|
candidate_selector,
|
|
resolver_environment,
|
|
) {
|
|
*versions2 = simplified;
|
|
}
|
|
}
|
|
External::NoVersions(package, versions) => {
|
|
if let Some(simplified) = simplify_range(
|
|
versions,
|
|
package,
|
|
available_versions,
|
|
candidate_selector,
|
|
resolver_environment,
|
|
) {
|
|
*versions = simplified;
|
|
}
|
|
}
|
|
External::Custom(package, versions, _) => {
|
|
if let Some(simplified) = simplify_range(
|
|
versions,
|
|
package,
|
|
available_versions,
|
|
candidate_selector,
|
|
resolver_environment,
|
|
) {
|
|
*versions = simplified;
|
|
}
|
|
}
|
|
External::NotRoot(..) => (),
|
|
},
|
|
DerivationTree::Derived(derived) => {
|
|
// Recursively simplify both sides of the tree
|
|
simplify_derivation_tree_ranges(
|
|
Arc::make_mut(&mut derived.cause1),
|
|
available_versions,
|
|
candidate_selector,
|
|
resolver_environment,
|
|
);
|
|
simplify_derivation_tree_ranges(
|
|
Arc::make_mut(&mut derived.cause2),
|
|
available_versions,
|
|
candidate_selector,
|
|
resolver_environment,
|
|
);
|
|
|
|
// Simplify the terms
|
|
derived.terms = std::mem::take(&mut derived.terms)
|
|
.into_iter()
|
|
.map(|(pkg, term)| {
|
|
let term = match term {
|
|
Term::Positive(versions) => Term::Positive(
|
|
simplify_range(
|
|
&versions,
|
|
&pkg,
|
|
available_versions,
|
|
candidate_selector,
|
|
resolver_environment,
|
|
)
|
|
.unwrap_or(versions),
|
|
),
|
|
Term::Negative(versions) => Term::Negative(
|
|
simplify_range(
|
|
&versions,
|
|
&pkg,
|
|
available_versions,
|
|
candidate_selector,
|
|
resolver_environment,
|
|
)
|
|
.unwrap_or(versions),
|
|
),
|
|
};
|
|
(pkg, term)
|
|
})
|
|
.collect();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Helper function to simplify a version range using available versions for a package.
|
|
///
|
|
/// If the range cannot be simplified, `None` is returned.
|
|
fn simplify_range(
|
|
range: &Range<Version>,
|
|
package: &PubGrubPackage,
|
|
available_versions: &FxHashMap<PackageName, BTreeSet<Version>>,
|
|
candidate_selector: &CandidateSelector,
|
|
resolver_environment: &ResolverEnvironment,
|
|
) -> Option<Range<Version>> {
|
|
// If there's not a package name or available versions, we can't simplify anything
|
|
let name = package.name()?;
|
|
let versions = available_versions.get(name)?;
|
|
|
|
// If this is a full range, there's nothing to simplify
|
|
if range == &Range::full() {
|
|
return None;
|
|
}
|
|
|
|
// If there's only one version available and it's in the range, return just that version
|
|
if let Some(version) = versions.iter().next() {
|
|
if versions.len() == 1 && range.contains(version) {
|
|
return Some(Range::singleton(version.clone()));
|
|
}
|
|
}
|
|
|
|
// Check if pre-releases are allowed
|
|
let prereleases_not_allowed = candidate_selector
|
|
.prerelease_strategy()
|
|
.allows(name, resolver_environment)
|
|
!= AllowPrerelease::Yes;
|
|
|
|
let any_prerelease = range.iter().any(|(start, end)| {
|
|
let is_pre1 = match start {
|
|
Bound::Included(version) => version.any_prerelease(),
|
|
Bound::Excluded(version) => version.any_prerelease(),
|
|
Bound::Unbounded => false,
|
|
};
|
|
let is_pre2 = match end {
|
|
Bound::Included(version) => version.any_prerelease(),
|
|
Bound::Excluded(version) => version.any_prerelease(),
|
|
Bound::Unbounded => false,
|
|
};
|
|
is_pre1 || is_pre2
|
|
});
|
|
|
|
// Simplify the range, as implemented in PubGrub
|
|
Some(range.simplify(versions.iter().filter(|version| {
|
|
// If there are pre-releases in the range segments, we need to include pre-releases
|
|
if any_prerelease {
|
|
return true;
|
|
}
|
|
|
|
// If pre-releases are not allowed, filter out pre-releases
|
|
if prereleases_not_allowed && version.any_prerelease() {
|
|
return false;
|
|
}
|
|
|
|
// Otherwise, include the version
|
|
true
|
|
})))
|
|
}
|