diff --git a/crates/distribution-types/src/index_url.rs b/crates/distribution-types/src/index_url.rs index 91077db0c8bb..19175c608c61 100644 --- a/crates/distribution-types/src/index_url.rs +++ b/crates/distribution-types/src/index_url.rs @@ -19,7 +19,7 @@ static DEFAULT_INDEX_URL: LazyLock = LazyLock::new(|| IndexUrl::Pypi(VerbatimUrl::from_url(PYPI_URL.clone()))); /// The URL of an index to use for fetching packages (e.g., PyPI). -#[derive(Debug, Clone, Hash, Eq, PartialEq)] +#[derive(Debug, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)] pub enum IndexUrl { Pypi(VerbatimUrl), Url(VerbatimUrl), @@ -384,9 +384,14 @@ impl<'a> IndexLocations { } } - /// Return an iterator over all [`IndexUrl`] entries. + /// Return an iterator over all [`IndexUrl`] entries in order. + /// + /// Prioritizes the extra indexes over the main index. + /// + /// If `no_index` was enabled, then this always returns an empty + /// iterator. pub fn indexes(&'a self) -> impl Iterator + 'a { - self.index().into_iter().chain(self.extra_index()) + self.extra_index().chain(self.index()) } /// Return an iterator over the [`FlatIndexLocation`] entries. diff --git a/crates/uv-resolver/src/error.rs b/crates/uv-resolver/src/error.rs index 8411d98dbbc6..43357f94b0d4 100644 --- a/crates/uv-resolver/src/error.rs +++ b/crates/uv-resolver/src/error.rs @@ -6,7 +6,7 @@ use indexmap::IndexSet; use pubgrub::{DefaultStringReporter, DerivationTree, Derived, External, Range, Reporter}; use rustc_hash::FxHashMap; -use distribution_types::{BuiltDist, IndexLocations, InstalledDist, SourceDist}; +use distribution_types::{BuiltDist, IndexLocations, IndexUrl, InstalledDist, SourceDist}; use pep440_rs::Version; use pep508_rs::MarkerTree; use tracing::trace; @@ -122,6 +122,7 @@ pub(crate) type ErrorTree = DerivationTree, Unava pub struct NoSolutionError { error: pubgrub::NoSolutionError, available_versions: FxHashMap>, + available_indexes: FxHashMap>, selector: CandidateSelector, python_requirement: PythonRequirement, index_locations: IndexLocations, @@ -137,6 +138,7 @@ impl NoSolutionError { pub(crate) fn new( error: pubgrub::NoSolutionError, available_versions: FxHashMap>, + available_indexes: FxHashMap>, selector: CandidateSelector, python_requirement: PythonRequirement, index_locations: IndexLocations, @@ -149,6 +151,7 @@ impl NoSolutionError { Self { error, available_versions, + available_indexes, selector, python_requirement, index_locations, @@ -254,6 +257,7 @@ impl std::fmt::Display for NoSolutionError { &tree, &self.selector, &self.index_locations, + &self.available_indexes, &self.unavailable_packages, &self.incomplete_packages, &self.fork_urls, diff --git a/crates/uv-resolver/src/pubgrub/report.rs b/crates/uv-resolver/src/pubgrub/report.rs index 042803110d9e..f272020bcbb5 100644 --- a/crates/uv-resolver/src/pubgrub/report.rs +++ b/crates/uv-resolver/src/pubgrub/report.rs @@ -8,8 +8,9 @@ use owo_colors::OwoColorize; use pubgrub::{DerivationTree, Derived, External, Map, Range, ReportFormatter, Term}; use rustc_hash::FxHashMap; -use distribution_types::IndexLocations; +use distribution_types::{IndexLocations, IndexUrl}; use pep440_rs::Version; +use uv_configuration::IndexStrategy; use uv_normalize::PackageName; use crate::candidate_selector::CandidateSelector; @@ -504,6 +505,7 @@ impl PubGrubReportFormatter<'_> { derivation_tree: &ErrorTree, selector: &CandidateSelector, index_locations: &IndexLocations, + available_indexes: &FxHashMap>, unavailable_packages: &FxHashMap, incomplete_packages: &FxHashMap>, fork_urls: &ForkUrls, @@ -528,12 +530,14 @@ impl PubGrubReportFormatter<'_> { ); } - // Check for no versions due to no `--find-links` flat index + // Check for no versions due to no `--find-links` flat index. Self::index_hints( package, name, set, + selector, index_locations, + available_indexes, unavailable_packages, incomplete_packages, output_hints, @@ -582,6 +586,7 @@ impl PubGrubReportFormatter<'_> { &derived.cause1, selector, index_locations, + available_indexes, unavailable_packages, incomplete_packages, fork_urls, @@ -593,6 +598,7 @@ impl PubGrubReportFormatter<'_> { &derived.cause2, selector, index_locations, + available_indexes, unavailable_packages, incomplete_packages, fork_urls, @@ -608,7 +614,9 @@ impl PubGrubReportFormatter<'_> { package: &PubGrubPackage, name: &PackageName, set: &Range, + selector: &CandidateSelector, index_locations: &IndexLocations, + available_indexes: &FxHashMap>, unavailable_packages: &FxHashMap, incomplete_packages: &FxHashMap>, hints: &mut IndexSet, @@ -686,6 +694,27 @@ impl PubGrubReportFormatter<'_> { } } } + + // Add hints due to the package being available on an index, but not at the correct version, + // with subsequent indexes that were _not_ queried. + if matches!(selector.index_strategy(), IndexStrategy::FirstIndex) { + if let Some(found_index) = available_indexes.get(name).and_then(BTreeSet::first) { + // Determine whether the index is the last-available index. If not, then some + // indexes were not queried, and could contain a compatible version. + if let Some(next_index) = index_locations + .indexes() + .skip_while(|url| *url != found_index) + .nth(1) + { + hints.insert(PubGrubHint::UncheckedIndex { + package: package.clone(), + range: set.clone(), + found_index: found_index.clone(), + next_index: next_index.clone(), + }); + } + } + } } fn prerelease_available_hint( @@ -819,11 +848,25 @@ pub(crate) enum PubGrubHint { // excluded from `PartialEq` and `Hash` package_requires_python: Range, }, + /// A non-workspace package depends on a workspace package, which is likely shadowing a + /// transitive dependency. DependsOnWorkspacePackage { package: PubGrubPackage, dependency: PubGrubPackage, workspace: bool, }, + /// A package was available on an index, but not at the correct version, and at least one + /// subsequent index was not queried. As such, a compatible version may be available on an + /// one of the remaining indexes. + UncheckedIndex { + package: PubGrubPackage, + // excluded from `PartialEq` and `Hash` + range: Range, + // excluded from `PartialEq` and `Hash` + found_index: IndexUrl, + // excluded from `PartialEq` and `Hash` + next_index: IndexUrl, + }, } /// This private enum mirrors [`PubGrubHint`] but only includes fields that should be @@ -869,6 +912,9 @@ enum PubGrubHintCore { dependency: PubGrubPackage, workspace: bool, }, + UncheckedIndex { + package: PubGrubPackage, + }, } impl From for PubGrubHintCore { @@ -921,6 +967,7 @@ impl From for PubGrubHintCore { dependency, workspace, }, + PubGrubHint::UncheckedIndex { package, .. } => Self::UncheckedIndex { package }, } } } @@ -1140,6 +1187,24 @@ impl std::fmt::Display for PubGrubHint { dependency, ) } + Self::UncheckedIndex { + package, + range, + found_index, + next_index, + } => { + write!( + f, + "{}{} `{}` was found on {}, but not at the requested version ({}). A compatible version may be available on a subsequent index (e.g., {}). By default, uv will only consider versions that are published on the first index that contains a given package, to avoid dependency confusion attacks. If all indexes are equally trusted, use `{}` to consider all versions from all indexes, regardless of the order in which they were defined.", + "hint".bold().cyan(), + ":".bold(), + package, + found_index.cyan(), + PackageRange::compatibility(package, range, None).cyan(), + next_index.cyan(), + "--index-strategy unsafe-best-match".green(), + ) + } } } } diff --git a/crates/uv-resolver/src/resolver/mod.rs b/crates/uv-resolver/src/resolver/mod.rs index ba42d1371837..d98b64674291 100644 --- a/crates/uv-resolver/src/resolver/mod.rs +++ b/crates/uv-resolver/src/resolver/mod.rs @@ -1944,6 +1944,7 @@ impl ResolverState ResolverState ResolverState Option<&IndexUrl> { + match &self.inner { + VersionMapInner::Eager(_) => None, + VersionMapInner::Lazy(lazy) => Some(&lazy.index), + } + } + /// Return an iterator over the versions and distributions. /// /// Note that the value returned in this iterator is a [`VersionMapDist`], diff --git a/crates/uv/tests/pip_compile.rs b/crates/uv/tests/pip_compile.rs index 9b10f706cd2a..d41089f374e2 100644 --- a/crates/uv/tests/pip_compile.rs +++ b/crates/uv/tests/pip_compile.rs @@ -10682,6 +10682,8 @@ fn compile_index_url_first_match() -> Result<()> { ----- stderr ----- × No solution found when resolving dependencies: ╰─▶ Because there is no version of jinja2==3.1.0 and you require jinja2==3.1.0, we can conclude that your requirements are unsatisfiable. + + hint: `jinja2` was found on https://download.pytorch.org/whl/cpu, but not at the requested version (jinja2==3.1.0). A compatible version may be available on a subsequent index (e.g., https://pypi.org/simple). By default, uv will only consider versions that are published on the first index that contains a given package, to avoid dependency confusion attacks. If all indexes are equally trusted, use `--index-strategy unsafe-best-match` to consider all versions from all indexes, regardless of the order in which they were defined. "### );