Show expected and available ABI tags in resolver errors (#10527)

## Summary

The idea here is to show both (1) an example of a compatible tag and (2)
the tags that were available, whenever we fail to resolve due to an
abscence of matching wheels.

Closes https://github.com/astral-sh/uv/issues/2777.
This commit is contained in:
Charlie Marsh 2025-01-13 20:03:11 -05:00 committed by GitHub
parent e0e8ba582a
commit 2ffa31946d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 509 additions and 66 deletions

1
Cargo.lock generated
View File

@ -5019,6 +5019,7 @@ dependencies = [
"fs-err 3.0.0",
"itertools 0.14.0",
"jiff",
"owo-colors",
"petgraph",
"rkyv",
"rustc-hash",

View File

@ -34,6 +34,7 @@ bitflags = { workspace = true }
fs-err = { workspace = true }
itertools = { workspace = true }
jiff = { workspace = true }
owo-colors = { workspace = true }
petgraph = { workspace = true }
rkyv = { workspace = true }
rustc-hash = { workspace = true }

View File

@ -1,12 +1,14 @@
use std::collections::BTreeSet;
use std::fmt::{Display, Formatter};
use arcstr::ArcStr;
use owo_colors::OwoColorize;
use tracing::debug;
use uv_distribution_filename::{BuildTag, WheelFilename};
use uv_pep440::VersionSpecifiers;
use uv_pep508::{MarkerExpression, MarkerOperator, MarkerTree, MarkerValueString};
use uv_platform_tags::{IncompatibleTag, TagPriority};
use uv_platform_tags::{AbiTag, IncompatibleTag, TagPriority, Tags};
use uv_pypi_types::{HashDigest, Yanked};
use crate::{
@ -164,6 +166,40 @@ impl IncompatibleDist {
Self::Unavailable => format!("have {self}"),
}
}
pub fn context_message(
&self,
tags: Option<&Tags>,
requires_python: Option<AbiTag>,
) -> Option<String> {
match self {
Self::Wheel(incompatibility) => match incompatibility {
IncompatibleWheel::Tag(IncompatibleTag::Python) => {
let tag = tags?.python_tag().map(ToString::to_string)?;
Some(format!("(e.g., `{tag}`)", tag = tag.cyan()))
}
IncompatibleWheel::Tag(IncompatibleTag::Abi) => {
let tag = tags?.abi_tag().map(ToString::to_string)?;
Some(format!("(e.g., `{tag}`)", tag = tag.cyan()))
}
IncompatibleWheel::Tag(IncompatibleTag::AbiPythonVersion) => {
let tag = requires_python?;
Some(format!("(e.g., `{tag}`)", tag = tag.cyan()))
}
IncompatibleWheel::Tag(IncompatibleTag::Platform) => {
let tag = tags?.platform_tag().map(ToString::to_string)?;
Some(format!("(e.g., `{tag}`)", tag = tag.cyan()))
}
IncompatibleWheel::Tag(IncompatibleTag::Invalid) => None,
IncompatibleWheel::NoBinary => None,
IncompatibleWheel::Yanked(..) => None,
IncompatibleWheel::ExcludeNewer(..) => None,
IncompatibleWheel::RequiresPython(..) => None,
},
Self::Source(..) => None,
Self::Unavailable => None,
}
}
}
impl Display for IncompatibleDist {
@ -246,6 +282,8 @@ pub enum IncompatibleWheel {
/// The wheel tags do not match those of the target Python platform.
Tag(IncompatibleTag),
/// The required Python version is not a superset of the target Python version range.
///
/// TODO(charlie): Consider making this two variants to reduce enum size.
RequiresPython(VersionSpecifiers, PythonRequirementKind),
/// The wheel was yanked.
Yanked(Yanked),
@ -483,6 +521,40 @@ impl PrioritizedDist {
pub fn best_wheel(&self) -> Option<&(RegistryBuiltWheel, WheelCompatibility)> {
self.0.best_wheel_index.map(|i| &self.0.wheels[i])
}
/// Returns the set of all Python tags for the distribution.
pub fn python_tags(&self) -> BTreeSet<&str> {
self.0
.wheels
.iter()
.flat_map(|(wheel, _)| wheel.filename.python_tag.iter().map(String::as_str))
.collect()
}
/// Returns the set of all ABI tags for the distribution.
pub fn abi_tags(&self) -> BTreeSet<&str> {
self.0
.wheels
.iter()
.flat_map(|(wheel, _)| wheel.filename.abi_tag.iter().map(String::as_str))
.collect()
}
/// Returns the set of platform tags for the distribution that are ABI-compatible with the given
/// tags.
pub fn platform_tags<'a>(&'a self, tags: &'a Tags) -> BTreeSet<&'a str> {
let mut candidates = BTreeSet::new();
for (wheel, _) in &self.0.wheels {
for wheel_py in &wheel.filename.python_tag {
for wheel_abi in &wheel.filename.abi_tag {
if tags.is_compatible_abi(wheel_py.as_str(), wheel_abi.as_str()) {
candidates.extend(wheel.filename.platform_tag.iter().map(String::as_str));
}
}
}
}
candidates
}
}
impl<'a> CompatibleDist<'a> {

View File

@ -5,8 +5,7 @@ use std::{cmp, num::NonZeroU32};
use rustc_hash::FxHashMap;
use crate::abi_tag::AbiTag;
use crate::{Arch, LanguageTag, Os, Platform, PlatformError};
use crate::{AbiTag, Arch, LanguageTag, Os, Platform, PlatformError};
#[derive(Debug, thiserror::Error)]
pub enum TagsError {
@ -75,6 +74,8 @@ pub struct Tags {
/// `python_tag` |--> `abi_tag` |--> `platform_tag` |--> priority
#[allow(clippy::type_complexity)]
map: Arc<FxHashMap<String, FxHashMap<String, FxHashMap<String, TagPriority>>>>,
/// The highest-priority tag for the Python version and platform.
best: Option<(String, String, String)>,
}
impl Tags {
@ -83,6 +84,9 @@ impl Tags {
/// Tags are prioritized based on their position in the given vector. Specifically, tags that
/// appear earlier in the vector are given higher priority than tags that appear later.
pub fn new(tags: Vec<(String, String, String)>) -> Self {
// Store the highest-priority tag for each component.
let best = tags.first().cloned();
// Index the tags by Python version, ABI, and platform.
let mut map = FxHashMap::default();
for (index, (py, abi, platform)) in tags.into_iter().rev().enumerate() {
@ -93,7 +97,11 @@ impl Tags {
.entry(platform)
.or_insert(TagPriority::try_from(index).expect("valid tag priority"));
}
Self { map: Arc::new(map) }
Self {
map: Arc::new(map),
best,
}
}
/// Returns the compatible tags for the given Python implementation (e.g., `cpython`), version,
@ -291,6 +299,30 @@ impl Tags {
}
max_compatibility
}
/// Return the highest-priority Python tag for the [`Tags`].
pub fn python_tag(&self) -> Option<&str> {
self.best.as_ref().map(|(py, _, _)| py.as_str())
}
/// Return the highest-priority ABI tag for the [`Tags`].
pub fn abi_tag(&self) -> Option<&str> {
self.best.as_ref().map(|(_, abi, _)| abi.as_str())
}
/// Return the highest-priority platform tag for the [`Tags`].
pub fn platform_tag(&self) -> Option<&str> {
self.best.as_ref().map(|(_, _, platform)| platform.as_str())
}
/// Returns `true` if the given language and ABI tags are compatible with the current
/// environment.
pub fn is_compatible_abi<'a>(&'a self, python_tag: &'a str, abi_tag: &'a str) -> bool {
self.map
.get(python_tag)
.map(|abis| abis.contains_key(abi_tag))
.unwrap_or(false)
}
}
/// The priority of a platform tag.

View File

@ -236,6 +236,7 @@ impl CandidateSelector {
return Some(Candidate {
name: package_name,
version,
prioritized: None,
dist: CandidateDist::Compatible(CompatibleDist::InstalledDist(
dist,
)),
@ -302,6 +303,7 @@ impl CandidateSelector {
return Some(Candidate {
name: package_name,
version,
prioritized: None,
dist: CandidateDist::Compatible(CompatibleDist::InstalledDist(dist)),
choice_kind: VersionChoiceKind::Installed,
});
@ -583,6 +585,8 @@ pub(crate) struct Candidate<'a> {
name: &'a PackageName,
/// The version of the package.
version: &'a Version,
/// The prioritized distribution for the package.
prioritized: Option<&'a PrioritizedDist>,
/// The distributions to use for resolving and installing the package.
dist: CandidateDist<'a>,
/// Whether this candidate was selected from a preference.
@ -599,6 +603,7 @@ impl<'a> Candidate<'a> {
Self {
name,
version,
prioritized: Some(dist),
dist: CandidateDist::from(dist),
choice_kind,
}
@ -632,6 +637,11 @@ impl<'a> Candidate<'a> {
pub(crate) fn dist(&self) -> &CandidateDist<'a> {
&self.dist
}
/// Return the prioritized distribution for the candidate.
pub(crate) fn prioritized(&self) -> Option<&PrioritizedDist> {
self.prioritized
}
}
impl Name for Candidate<'_> {

View File

@ -14,10 +14,12 @@ use uv_distribution_types::{
};
use uv_normalize::{ExtraName, PackageName};
use uv_pep440::{LocalVersionSlice, Version};
use uv_platform_tags::Tags;
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};
@ -27,7 +29,7 @@ use crate::resolution::ConflictingDistributionError;
use crate::resolver::{
MetadataUnavailable, ResolverEnvironment, UnavailablePackage, UnavailableReason,
};
use crate::Options;
use crate::{InMemoryIndex, Options};
#[derive(Debug, thiserror::Error)]
pub enum ResolveError {
@ -130,9 +132,9 @@ impl<T> From<tokio::sync::mpsc::error::SendError<T>> for ResolveError {
pub(crate) type ErrorTree = DerivationTree<PubGrubPackage, Range<Version>, UnavailableReason>;
/// A wrapper around [`pubgrub::error::NoSolutionError`] that displays a resolution failure report.
#[derive(Debug)]
pub struct NoSolutionError {
error: pubgrub::NoSolutionError<UvDependencyProvider>,
index: InMemoryIndex,
available_versions: FxHashMap<PackageName, BTreeSet<Version>>,
available_indexes: FxHashMap<PackageName, BTreeSet<IndexUrl>>,
selector: CandidateSelector,
@ -142,7 +144,9 @@ pub struct NoSolutionError {
unavailable_packages: FxHashMap<PackageName, UnavailablePackage>,
incomplete_packages: FxHashMap<PackageName, BTreeMap<Version, MetadataUnavailable>>,
fork_urls: ForkUrls,
fork_indexes: ForkIndexes,
env: ResolverEnvironment,
tags: Option<Tags>,
workspace_members: BTreeSet<PackageName>,
options: Options,
}
@ -151,6 +155,7 @@ 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,
@ -160,12 +165,15 @@ impl NoSolutionError {
unavailable_packages: FxHashMap<PackageName, UnavailablePackage>,
incomplete_packages: FxHashMap<PackageName, BTreeMap<Version, MetadataUnavailable>>,
fork_urls: ForkUrls,
fork_indexes: ForkIndexes,
env: ResolverEnvironment,
tags: Option<Tags>,
workspace_members: BTreeSet<PackageName>,
options: Options,
) -> Self {
Self {
error,
index,
available_versions,
available_indexes,
selector,
@ -175,7 +183,9 @@ impl NoSolutionError {
unavailable_packages,
incomplete_packages,
fork_urls,
fork_indexes,
env,
tags,
workspace_members,
options,
}
@ -328,6 +338,47 @@ impl NoSolutionError {
}
}
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,
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("tags", tags)
.field("workspace_members", workspace_members)
.field("options", options)
.finish()
}
}
impl std::error::Error for NoSolutionError {}
impl std::fmt::Display for NoSolutionError {
@ -337,6 +388,7 @@ impl std::fmt::Display for NoSolutionError {
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
@ -385,6 +437,7 @@ impl std::fmt::Display for NoSolutionError {
let mut additional_hints = IndexSet::default();
formatter.generate_hints(
&tree,
&self.index,
&self.selector,
&self.index_locations,
&self.index_capabilities,
@ -392,6 +445,7 @@ impl std::fmt::Display for NoSolutionError {
&self.unavailable_packages,
&self.incomplete_packages,
&self.fork_urls,
&self.fork_indexes,
&self.env,
&self.workspace_members,
&self.options,

View File

@ -1,12 +1,26 @@
use std::cmp::Ordering;
use std::collections::{BTreeMap, BTreeSet};
use std::ops::Bound;
use indexmap::IndexSet;
use itertools::Itertools;
use owo_colors::OwoColorize;
use pubgrub::{DerivationTree, Derived, External, Map, Range, ReportFormatter, Term};
use rustc_hash::FxHashMap;
use std::cmp::Ordering;
use std::collections::{BTreeMap, BTreeSet};
use std::ops::Bound;
use std::str::FromStr;
use super::{PubGrubPackage, PubGrubPackageInner, PubGrubPython};
use crate::candidate_selector::CandidateSelector;
use crate::error::ErrorTree;
use crate::fork_indexes::ForkIndexes;
use crate::fork_urls::ForkUrls;
use crate::prerelease::AllowPrerelease;
use crate::python_requirement::{PythonRequirement, PythonRequirementSource};
use crate::resolver::{
MetadataUnavailable, UnavailablePackage, UnavailableReason, UnavailableVersion,
};
use crate::{
Flexibility, InMemoryIndex, Options, RequiresPython, ResolverEnvironment, VersionsResponse,
};
use uv_configuration::{IndexStrategy, NoBinary, NoBuild};
use uv_distribution_types::{
IncompatibleDist, IncompatibleSource, IncompatibleWheel, Index, IndexCapabilities,
@ -14,28 +28,21 @@ use uv_distribution_types::{
};
use uv_normalize::PackageName;
use uv_pep440::{Version, VersionSpecifiers};
use crate::candidate_selector::CandidateSelector;
use crate::error::ErrorTree;
use crate::fork_urls::ForkUrls;
use crate::prerelease::AllowPrerelease;
use crate::python_requirement::{PythonRequirement, PythonRequirementSource};
use crate::resolver::{
MetadataUnavailable, UnavailablePackage, UnavailableReason, UnavailableVersion,
};
use crate::{Flexibility, Options, RequiresPython, ResolverEnvironment};
use super::{PubGrubPackage, PubGrubPackageInner, PubGrubPython};
use uv_platform_tags::{AbiTag, IncompatibleTag, LanguageTag, Tags};
#[derive(Debug)]
pub(crate) struct PubGrubReportFormatter<'a> {
/// The versions that were available for each package
/// The versions that were available for each package.
pub(crate) available_versions: &'a FxHashMap<PackageName, BTreeSet<Version>>,
/// The versions that were available for each package
/// The versions that were available for each package.
pub(crate) python_requirement: &'a PythonRequirement,
/// The members of the workspace.
pub(crate) workspace_members: &'a BTreeSet<PackageName>,
/// The compatible tags for the resolution.
pub(crate) tags: Option<&'a Tags>,
}
impl ReportFormatter<PubGrubPackage, Range<Version>, UnavailableReason>
@ -111,20 +118,25 @@ impl ReportFormatter<PubGrubPackage, Range<Version>, UnavailableReason>
} else {
match reason {
UnavailableReason::Package(reason) => {
format!(
"{}{}",
Padded::new("", &package, " "),
reason.singular_message()
)
let message = reason.singular_message();
format!("{}{}", package, Padded::new(" ", &message, ""),)
}
UnavailableReason::Version(reason) => {
let range = self.compatible_range(package, set);
let reason = if range.plural() {
let message = if range.plural() {
reason.plural_message()
} else {
reason.singular_message()
};
format!("{}{reason}", Padded::new("", &range, " "))
let context = reason.context_message(
self.tags,
self.python_requirement.target().abi_tag(),
);
if let Some(context) = context {
format!("{}{}{}", range, Padded::new(" ", &message, " "), context)
} else {
format!("{}{}", range, Padded::new(" ", &message, ""))
}
}
}
}
@ -513,6 +525,7 @@ impl PubGrubReportFormatter<'_> {
pub(crate) fn generate_hints(
&self,
derivation_tree: &ErrorTree,
index: &InMemoryIndex,
selector: &CandidateSelector,
index_locations: &IndexLocations,
index_capabilities: &IndexCapabilities,
@ -520,6 +533,7 @@ impl PubGrubReportFormatter<'_> {
unavailable_packages: &FxHashMap<PackageName, UnavailablePackage>,
incomplete_packages: &FxHashMap<PackageName, BTreeMap<Version, MetadataUnavailable>>,
fork_urls: &ForkUrls,
fork_indexes: &ForkIndexes,
env: &ResolverEnvironment,
workspace_members: &BTreeSet<PackageName>,
options: &Options,
@ -555,27 +569,42 @@ impl PubGrubReportFormatter<'_> {
incomplete_packages,
output_hints,
);
}
// Check for unavailable versions due to `--no-build` or `--no-binary`.
if let UnavailableReason::Version(UnavailableVersion::IncompatibleDist(
incompatibility,
)) = reason
{
match incompatibility {
IncompatibleDist::Wheel(IncompatibleWheel::NoBinary) => {
output_hints.insert(PubGrubHint::NoBinary {
package: package.clone(),
option: options.build_options.no_binary().clone(),
});
if let UnavailableReason::Version(UnavailableVersion::IncompatibleDist(
incompatibility,
)) = reason
{
match incompatibility {
// Check for unavailable versions due to `--no-build` or `--no-binary`.
IncompatibleDist::Wheel(IncompatibleWheel::NoBinary) => {
output_hints.insert(PubGrubHint::NoBinary {
package: package.clone(),
option: options.build_options.no_binary().clone(),
});
}
IncompatibleDist::Source(IncompatibleSource::NoBuild) => {
output_hints.insert(PubGrubHint::NoBuild {
package: package.clone(),
option: options.build_options.no_build().clone(),
});
}
// Check for unavailable versions due to incompatible tags.
IncompatibleDist::Wheel(IncompatibleWheel::Tag(tag)) => {
if let Some(hint) = self.tag_hint(
package,
name,
set,
*tag,
index,
selector,
fork_indexes,
env,
) {
output_hints.insert(hint);
}
}
_ => {}
}
IncompatibleDist::Source(IncompatibleSource::NoBuild) => {
output_hints.insert(PubGrubHint::NoBuild {
package: package.clone(),
option: options.build_options.no_build().clone(),
});
}
_ => {}
}
}
}
@ -661,6 +690,7 @@ impl PubGrubReportFormatter<'_> {
DerivationTree::Derived(derived) => {
self.generate_hints(
&derived.cause1,
index,
selector,
index_locations,
index_capabilities,
@ -668,6 +698,7 @@ impl PubGrubReportFormatter<'_> {
unavailable_packages,
incomplete_packages,
fork_urls,
fork_indexes,
env,
workspace_members,
options,
@ -675,6 +706,7 @@ impl PubGrubReportFormatter<'_> {
);
self.generate_hints(
&derived.cause2,
index,
selector,
index_locations,
index_capabilities,
@ -682,6 +714,7 @@ impl PubGrubReportFormatter<'_> {
unavailable_packages,
incomplete_packages,
fork_urls,
fork_indexes,
env,
workspace_members,
options,
@ -691,6 +724,106 @@ impl PubGrubReportFormatter<'_> {
};
}
/// Generate a [`PubGrubHint`] for a package that doesn't have any wheels matching the current
/// Python version, ABI, or platform.
fn tag_hint(
&self,
package: &PubGrubPackage,
name: &PackageName,
set: &Range<Version>,
tag: IncompatibleTag,
index: &InMemoryIndex,
selector: &CandidateSelector,
fork_indexes: &ForkIndexes,
env: &ResolverEnvironment,
) -> Option<PubGrubHint> {
let response = if let Some(url) = fork_indexes.get(name) {
index.explicit().get(&(name.clone(), url.clone()))
} else {
index.implicit().get(name)
}?;
let VersionsResponse::Found(version_maps) = &*response else {
return None;
};
let candidate = selector.select_no_preference(name, set, version_maps, env)?;
let prioritized = candidate.prioritized()?;
match tag {
IncompatibleTag::Invalid => None,
IncompatibleTag::Python => {
// Return all available language tags.
let tags = prioritized
.python_tags()
.into_iter()
.filter_map(|tag| LanguageTag::from_str(tag).ok())
.collect::<BTreeSet<_>>();
if tags.is_empty() {
None
} else {
Some(PubGrubHint::LanguageTags {
package: package.clone(),
version: candidate.version().clone(),
tags,
})
}
}
IncompatibleTag::Abi | IncompatibleTag::AbiPythonVersion => {
let tags = prioritized
.abi_tags()
.into_iter()
.filter_map(|tag| AbiTag::from_str(tag).ok())
// Ignore `none`, which is universally compatible.
//
// As an example, `none` can appear here if we're solving for Python 3.13, and
// the distribution includes a wheel for `cp312-none-macosx_11_0_arm64`.
//
// In that case, the wheel isn't compatible, but when solving for Python 3.13,
// the `cp312` Python tag _can_ be compatible (e.g., for `cp312-abi3-macosx_11_0_arm64.whl`),
// so this is considered an ABI incompatibility rather than Python incompatibility.
.filter(|tag| *tag != AbiTag::None)
.collect::<BTreeSet<_>>();
if tags.is_empty() {
None
} else {
Some(PubGrubHint::AbiTags {
package: package.clone(),
version: candidate.version().clone(),
tags,
})
}
}
IncompatibleTag::Platform => {
// We don't want to report all available platforms, since it's plausible that there
// are wheels for the current platform, but at a different ABI. For example, when
// solving for Python 3.13 on macOS, `cp312-cp312-macosx_11_0_arm64` could be
// available along with `cp313-cp313-manylinux2014`. In this case, we'd consider
// the distribution to be platform-incompatible, since `cp313-cp313` matches the
// compatible wheel tags. But showing `macosx_11_0_arm64` here would be misleading.
//
// So, instead, we only show the platforms that are linked to otherwise-compatible
// wheels (e.g., `manylinux2014` in `cp313-cp313-manylinux2014`). In other words,
// we only show platforms for ABI-compatible wheels.
let tags = prioritized
.platform_tags(self.tags?)
.into_iter()
.map(ToString::to_string)
.collect::<Vec<_>>();
if tags.is_empty() {
None
} else {
Some(PubGrubHint::PlatformTags {
package: package.clone(),
version: candidate.version().clone(),
tags,
})
}
}
}
}
fn index_hints(
package: &PubGrubPackage,
name: &PackageName,
@ -991,6 +1124,30 @@ pub(crate) enum PubGrubHint {
UnauthorizedIndex { index: IndexUrl },
/// An index returned a Forbidden (403) response.
ForbiddenIndex { index: IndexUrl },
/// No wheels are available for a package, and using source distributions was disabled.
LanguageTags {
package: PubGrubPackage,
// excluded from `PartialEq` and `Hash`
version: Version,
// excluded from `PartialEq` and `Hash`
tags: BTreeSet<LanguageTag>,
},
/// No wheels are available for a package, and using source distributions was disabled.
AbiTags {
package: PubGrubPackage,
// excluded from `PartialEq` and `Hash`
version: Version,
// excluded from `PartialEq` and `Hash`
tags: BTreeSet<AbiTag>,
},
/// No wheels are available for a package, and using source distributions was disabled.
PlatformTags {
package: PubGrubPackage,
// excluded from `PartialEq` and `Hash`
version: Version,
// excluded from `PartialEq` and `Hash`
tags: Vec<String>,
},
}
/// This private enum mirrors [`PubGrubHint`] but only includes fields that should be
@ -1052,6 +1209,15 @@ enum PubGrubHintCore {
NoBinary {
package: PubGrubPackage,
},
LanguageTags {
package: PubGrubPackage,
},
AbiTags {
package: PubGrubPackage,
},
PlatformTags {
package: PubGrubPackage,
},
}
impl From<PubGrubHint> for PubGrubHintCore {
@ -1109,6 +1275,9 @@ impl From<PubGrubHint> for PubGrubHintCore {
PubGrubHint::ForbiddenIndex { index } => Self::ForbiddenIndex { index },
PubGrubHint::NoBuild { package, .. } => Self::NoBuild { package },
PubGrubHint::NoBinary { package, .. } => Self::NoBinary { package },
PubGrubHint::LanguageTags { package, .. } => Self::LanguageTags { package },
PubGrubHint::AbiTags { package, .. } => Self::AbiTags { package },
PubGrubHint::PlatformTags { package, .. } => Self::PlatformTags { package },
}
}
}
@ -1415,6 +1584,60 @@ impl std::fmt::Display for PubGrubHint {
package.cyan(),
)
}
Self::LanguageTags {
package,
version,
tags,
} => {
let s = if tags.len() == 1 { "" } else { "s" };
write!(
f,
"{}{} Wheels are available for `{}` ({}) with the following Python tag{s}: {}",
"hint".bold().cyan(),
":".bold(),
package.cyan(),
format!("v{version}").cyan(),
tags.iter()
.map(|tag| format!("`{}`", tag.cyan()))
.join(", "),
)
}
Self::AbiTags {
package,
version,
tags,
} => {
let s = if tags.len() == 1 { "" } else { "s" };
write!(
f,
"{}{} Wheels are available for `{}` ({}) with the following ABI tag{s}: {}",
"hint".bold().cyan(),
":".bold(),
package.cyan(),
format!("v{version}").cyan(),
tags.iter()
.map(|tag| format!("`{}`", tag.cyan()))
.join(", "),
)
}
Self::PlatformTags {
package,
version,
tags,
} => {
let s = if tags.len() == 1 { "" } else { "s" };
write!(
f,
"{}{} Wheels are available for `{}` ({}) on the following platform{s}: {}",
"hint".bold().cyan(),
":".bold(),
package.cyan(),
format!("v{version}").cyan(),
tags.iter()
.map(|tag| format!("`{}`", tag.cyan()))
.join(", "),
)
}
}
}
}

View File

@ -7,6 +7,7 @@ use pubgrub::Range;
use uv_distribution_filename::WheelFilename;
use uv_pep440::{release_specifiers_to_ranges, Version, VersionSpecifier, VersionSpecifiers};
use uv_pep508::{MarkerExpression, MarkerTree, MarkerValueVersion};
use uv_platform_tags::AbiTag;
/// The `Requires-Python` requirement specifier.
///
@ -303,6 +304,23 @@ impl RequiresPython {
&self.range
}
/// Returns a wheel tag that's compatible with the `Requires-Python` specifier.
pub fn abi_tag(&self) -> Option<AbiTag> {
match self.range.lower().as_ref() {
Bound::Included(version) | Bound::Excluded(version) => {
let major = version.release().first().copied()?;
let major = u8::try_from(major).ok()?;
let minor = version.release().get(1).copied()?;
let minor = u8::try_from(minor).ok()?;
Some(AbiTag::CPython {
gil_disabled: false,
python_version: (major, minor),
})
}
Bound::Unbounded => None,
}
}
/// Simplifies the given markers in such a way as to assume that
/// the Python version is constrained by this Python version bound.
///

View File

@ -1,9 +1,9 @@
use std::fmt::{Display, Formatter};
use crate::resolver::{MetadataUnavailable, VersionFork};
use uv_distribution_types::IncompatibleDist;
use uv_pep440::{Version, VersionSpecifiers};
use crate::resolver::{MetadataUnavailable, VersionFork};
use uv_platform_tags::{AbiTag, Tags};
/// The reason why a package or a version cannot be used.
#[derive(Debug, Clone, Eq, PartialEq)]
@ -80,6 +80,23 @@ impl UnavailableVersion {
UnavailableVersion::RequiresPython(..) => format!("require {self}"),
}
}
pub(crate) fn context_message(
&self,
tags: Option<&Tags>,
requires_python: Option<AbiTag>,
) -> Option<String> {
match self {
UnavailableVersion::IncompatibleDist(invalid_dist) => {
invalid_dist.context_message(tags, requires_python)
}
UnavailableVersion::InvalidMetadata => None,
UnavailableVersion::InconsistentMetadata => None,
UnavailableVersion::InvalidStructure => None,
UnavailableVersion::Offline => None,
UnavailableVersion::RequiresPython(..) => None,
}
}
}
impl Display for UnavailableVersion {

View File

@ -113,6 +113,7 @@ struct ResolverState<InstalledPackages: InstalledPackagesProvider> {
dependency_mode: DependencyMode,
hasher: HashStrategy,
env: ResolverEnvironment,
tags: Option<Tags>,
python_requirement: PythonRequirement,
conflicts: Conflicts,
workspace_members: BTreeSet<PackageName>,
@ -181,6 +182,7 @@ impl<'a, Context: BuildContext, InstalledPackages: InstalledPackagesProvider>
options,
hasher,
env,
tags.cloned(),
python_requirement,
conflicts,
index,
@ -202,6 +204,7 @@ impl<Provider: ResolverProvider, InstalledPackages: InstalledPackagesProvider>
options: Options,
hasher: &HashStrategy,
env: ResolverEnvironment,
tags: Option<Tags>,
python_requirement: &PythonRequirement,
conflicts: Conflicts,
index: &InMemoryIndex,
@ -229,6 +232,7 @@ impl<Provider: ResolverProvider, InstalledPackages: InstalledPackagesProvider>
hasher: hasher.clone(),
locations: locations.clone(),
env,
tags,
python_requirement: python_requirement.clone(),
conflicts,
installed_packages,
@ -346,11 +350,9 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
return Err(self.convert_no_solution_err(
err,
state.fork_urls,
&state.fork_indexes,
state.fork_indexes,
state.env,
&visited,
&self.locations,
&self.capabilities,
));
}
Ok(conflicts) => {
@ -1219,6 +1221,8 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
// If the version is incompatible because no distributions are compatible, exit early.
return Ok(Some(ResolverVersion::Unavailable(
candidate.version().clone(),
// TODO(charlie): We can avoid this clone; the candidate is dropped here and
// owns the incompatibility.
UnavailableVersion::IncompatibleDist(incompatibility.clone()),
)));
}
@ -2338,11 +2342,9 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
&self,
mut err: pubgrub::NoSolutionError<UvDependencyProvider>,
fork_urls: ForkUrls,
fork_indexes: &ForkIndexes,
fork_indexes: ForkIndexes,
env: ResolverEnvironment,
visited: &FxHashSet<PackageName>,
index_locations: &IndexLocations,
index_capabilities: &IndexCapabilities,
) -> ResolveError {
err = NoSolutionError::collapse_local_version_segments(NoSolutionError::collapse_proxies(
err,
@ -2413,16 +2415,19 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
ResolveError::NoSolution(NoSolutionError::new(
err,
self.index.clone(),
available_versions,
available_indexes,
self.selector.clone(),
self.python_requirement.clone(),
index_locations.clone(),
index_capabilities.clone(),
self.locations.clone(),
self.capabilities.clone(),
unavailable_packages,
incomplete_packages,
fork_urls,
fork_indexes,
env,
self.tags.clone(),
self.workspace_members.clone(),
self.options.clone(),
))

View File

@ -6756,7 +6756,9 @@ fn lock_requires_python_no_wheels() -> Result<()> {
----- stderr -----
× No solution found when resolving dependencies:
Because dearpygui==1.9.1 has no wheels with a matching Python version tag and your project depends on dearpygui==1.9.1, we can conclude that your project's requirements are unsatisfiable.
Because dearpygui==1.9.1 has no wheels with a matching Python version tag (e.g., `cp312`) and your project depends on dearpygui==1.9.1, we can conclude that your project's requirements are unsatisfiable.
hint: Wheels are available for `dearpygui` (v1.9.1) with the following ABI tags: `cp37m`, `cp38`, `cp39`, `cp310`, `cp311`
"###);
Ok(())

View File

@ -13944,8 +13944,12 @@ fn invalid_platform() -> Result<()> {
----- stderr -----
× No solution found when resolving dependencies:
Because only open3d<=0.18.0 is available and open3d<=0.15.2 has no wheels with a matching Python ABI tag, we can conclude that open3d<=0.15.2 cannot be used.
And because open3d>=0.16.0,<=0.18.0 has no wheels with a matching platform tag and you require open3d, we can conclude that your requirements are unsatisfiable.
Because only open3d<=0.18.0 is available and open3d<=0.15.2 has no wheels with a matching Python ABI tag (e.g., `cp310`), we can conclude that open3d<=0.15.2 cannot be used.
And because open3d>=0.16.0,<=0.18.0 has no wheels with a matching platform tag (e.g., `manylinux_2_17_x86_64`) and you require open3d, we can conclude that your requirements are unsatisfiable.
hint: Wheels are available for `open3d` (v0.15.2) with the following ABI tags: `cp36m`, `cp37m`, `cp38`, `cp39`
hint: Wheels are available for `open3d` (v0.18.0) on the following platforms: `macosx_11_0_x86_64`, `macosx_13_0_arm64`, `manylinux_2_27_aarch64`, `manylinux_2_27_x86_64`, `win_amd64`
"###);
Ok(())

View File

@ -4088,7 +4088,7 @@ fn no_sdist_no_wheels_with_matching_abi() {
----- stderr -----
× No solution found when resolving dependencies:
Because only package-a==1.0.0 is available and package-a==1.0.0 has no wheels with a matching Python ABI tag, we can conclude that all versions of package-a cannot be used.
Because only package-a==1.0.0 is available and package-a==1.0.0 has no wheels with a matching Python ABI tag (e.g., `cp38`), we can conclude that all versions of package-a cannot be used.
And because you require package-a, we can conclude that your requirements are unsatisfiable.
"###);
@ -4129,8 +4129,10 @@ fn no_sdist_no_wheels_with_matching_platform() {
----- stderr -----
× No solution found when resolving dependencies:
Because only package-a==1.0.0 is available and package-a==1.0.0 has no wheels with a matching platform tag, we can conclude that all versions of package-a cannot be used.
Because only package-a==1.0.0 is available and package-a==1.0.0 has no wheels with a matching platform tag (e.g., `manylinux_2_17_x86_64`), we can conclude that all versions of package-a cannot be used.
And because you require package-a, we can conclude that your requirements are unsatisfiable.
hint: Wheels are available for `package-a` (v1.0.0) on the following platform: `macosx_10_0_ppc64`
"###);
assert_not_installed(
@ -4170,8 +4172,10 @@ fn no_sdist_no_wheels_with_matching_python() {
----- stderr -----
× No solution found when resolving dependencies:
Because only package-a==1.0.0 is available and package-a==1.0.0 has no wheels with a matching Python implementation tag, we can conclude that all versions of package-a cannot be used.
Because only package-a==1.0.0 is available and package-a==1.0.0 has no wheels with a matching Python implementation tag (e.g., `cp38`), we can conclude that all versions of package-a cannot be used.
And because you require package-a, we can conclude that your requirements are unsatisfiable.
hint: Wheels are available for `package-a` (v1.0.0) with the following Python tag: `graalpy310`
"###);
assert_not_installed(