mirror of https://github.com/astral-sh/uv
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:
parent
e0e8ba582a
commit
2ffa31946d
|
|
@ -5019,6 +5019,7 @@ dependencies = [
|
|||
"fs-err 3.0.0",
|
||||
"itertools 0.14.0",
|
||||
"jiff",
|
||||
"owo-colors",
|
||||
"petgraph",
|
||||
"rkyv",
|
||||
"rustc-hash",
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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<'_> {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(", "),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
///
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
))
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Reference in New Issue