diff --git a/crates/uv-client/src/registry_client.rs b/crates/uv-client/src/registry_client.rs index a7aed57e8..c1014423d 100644 --- a/crates/uv-client/src/registry_client.rs +++ b/crates/uv-client/src/registry_client.rs @@ -1149,14 +1149,6 @@ impl SimpleMetadata { warn!("Skipping file for {package_name}: {}", file.filename); continue; }; - if filename.name() != package_name { - warn!( - "Skipping file with mismatched package name: `{}` vs. `{}`", - filename.name(), - package_name - ); - continue; - } let file = match File::try_from(file, &base) { Ok(file) => file, Err(err) => { diff --git a/crates/uv-resolver/src/resolver/availability.rs b/crates/uv-resolver/src/resolver/availability.rs index 0015199be..7c2b7c9b7 100644 --- a/crates/uv-resolver/src/resolver/availability.rs +++ b/crates/uv-resolver/src/resolver/availability.rs @@ -2,6 +2,7 @@ use std::fmt::{Display, Formatter}; use crate::resolver::{MetadataUnavailable, VersionFork}; use uv_distribution_types::IncompatibleDist; +use uv_normalize::PackageName; use uv_pep440::{Version, VersionSpecifiers}; use uv_platform_tags::{AbiTag, Tags}; @@ -38,6 +39,8 @@ pub enum UnavailableVersion { InconsistentMetadata, /// The wheel has an invalid structure. InvalidStructure, + /// A package with the wrong name is on an index page. + PackageNameMismatch { dist_name: PackageName }, /// The wheel metadata was not found in the cache and the network is not available. Offline, /// The source distribution has a `requires-python` requirement that is not met by the installed @@ -52,6 +55,11 @@ impl UnavailableVersion { Self::InvalidMetadata => "invalid metadata".into(), Self::InconsistentMetadata => "inconsistent metadata".into(), Self::InvalidStructure => "an invalid package format".into(), + Self::PackageNameMismatch { + dist_name: wrong_name, + } => { + format!("the wrong package name in the index page (`{wrong_name}`)") + } Self::Offline => "to be downloaded from a registry".into(), Self::RequiresPython(requires_python) => { format!("Python {requires_python}") @@ -65,6 +73,7 @@ impl UnavailableVersion { Self::InvalidMetadata => format!("has {self}"), Self::InconsistentMetadata => format!("has {self}"), Self::InvalidStructure => format!("has {self}"), + Self::PackageNameMismatch { .. } => format!("has {self}"), Self::Offline => format!("needs {self}"), Self::RequiresPython(..) => format!("requires {self}"), } @@ -76,6 +85,7 @@ impl UnavailableVersion { Self::InvalidMetadata => format!("have {self}"), Self::InconsistentMetadata => format!("have {self}"), Self::InvalidStructure => format!("have {self}"), + Self::PackageNameMismatch { .. } => format!("have {self}"), Self::Offline => format!("need {self}"), Self::RequiresPython(..) => format!("require {self}"), } @@ -93,6 +103,7 @@ impl UnavailableVersion { Self::InvalidMetadata => None, Self::InconsistentMetadata => None, Self::InvalidStructure => None, + Self::PackageNameMismatch { .. } => None, Self::Offline => None, Self::RequiresPython(..) => None, } diff --git a/crates/uv-resolver/src/resolver/mod.rs b/crates/uv-resolver/src/resolver/mod.rs index b91d80e3b..eac3db951 100644 --- a/crates/uv-resolver/src/resolver/mod.rs +++ b/crates/uv-resolver/src/resolver/mod.rs @@ -1370,7 +1370,11 @@ impl ResolverState ResolverState ResolverState>() .join(", ") ); - self.visit_candidate(candidate, dist, package, name, pins, request_sink)?; - self.visit_candidate( + if let Some(incompatibility) = + self.visit_candidate(candidate, dist, package, name, pins, request_sink)? + { + return Ok(Some(incompatibility)); + } + if let Some(incompatibility) = self.visit_candidate( &base_candidate, base_dist, package, name, pins, request_sink, - )?; + )? { + return Ok(Some(incompatibility)); + } let forks = vec![ VersionFork { @@ -1611,6 +1624,8 @@ impl ResolverState ResolverState, - ) -> Result<(), ResolveError> { + ) -> Result, ResolveError> { + // If there is a package with a different name then the index page on the index, return + // an unavailability. + if dist.name() != (name) { + return Ok(Some(ResolverVersion::Unavailable( + candidate.version().clone(), + UnavailableVersion::PackageNameMismatch { + dist_name: dist.name().clone(), + }, + ))); + } + // We want to return a package pinned to a specific version; but we _also_ want to // store the exact file that we selected to satisfy that version. pins.insert(candidate, dist); @@ -1649,7 +1675,7 @@ impl ResolverState ResolverState Result<()> { Ok(()) } + +/// Test for cases where the index page for a package contains distributions for a different +/// package. +#[tokio::test] +async fn package_name_on_index_package_mismatch() -> Result<()> { + let context = TestContext::new("3.12"); + let server = MockServer::start().await; + + // An index for anyio, with tqdm and anyio distributions. + let networkx_page = r#" + + + +

Links for anyio

+ tqdm-4.67.1-py3-none-any.whl
+ tqdm-4.67.1.tar.gz
+ anyio-4.10.0-py3-none-any.whl
+ anyio-4.10.0.tar.gz
+ + + "#; + Mock::given(method("GET")) + .and(path("/anyio/")) + .respond_with(ResponseTemplate::new(200).set_body_raw(networkx_page, "text/html")) + .mount(&server) + .await; + + // Skip over the tqdm version, resolve to the older anyio version. + let requirements_in = context.temp_dir.child("requirements.in"); + requirements_in.write_str("anyio")?; + + uv_snapshot!(context + .pip_compile() + .env_remove(EnvVars::UV_EXCLUDE_NEWER) + .arg("--extra-index-url") + .arg(server.uri()) + .arg("requirements.in"), @r" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv pip compile --cache-dir [CACHE_DIR] requirements.in + anyio==4.10.0 + # via -r requirements.in + idna==3.10 + # via anyio + sniffio==1.3.1 + # via anyio + typing-extensions==4.15.0 + # via anyio + + ----- stderr ----- + Resolved 4 packages in [TIME] + "); + + // Error with our custom incompatibility when there are only bogus packages in the version + // range. + let requirements_in = context.temp_dir.child("requirements.in"); + requirements_in.write_str("anyio>4.10")?; + + uv_snapshot!(context + .pip_compile() + .env_remove(EnvVars::UV_EXCLUDE_NEWER) + .arg("--extra-index-url") + .arg(server.uri()) + .arg("requirements.in"), @r" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × No solution found when resolving dependencies: + ╰─▶ Because only the following versions of anyio are available: + anyio<4.10 + anyio==4.67.1 + and anyio==4.67.1 has the wrong package name in the index page (`tqdm`), we can conclude that anyio>4.10 cannot be used. + And because you require anyio>4.10, we can conclude that your requirements are unsatisfiable. + "); + + Ok(()) +} diff --git a/crates/uv/tests/it/pip_install.rs b/crates/uv/tests/it/pip_install.rs index ab8added9..6c2008824 100644 --- a/crates/uv/tests/it/pip_install.rs +++ b/crates/uv/tests/it/pip_install.rs @@ -11688,11 +11688,14 @@ async fn bogus_redirect() -> Result<()> { .arg("sniffio"), @r" success: false - exit_code: 2 + exit_code: 1 ----- stdout ----- ----- stderr ----- - error: The index returned metadata for the wrong package: expected distribution for sniffio, got distribution for anyio + × No solution found when resolving dependencies: + ╰─▶ Because all versions of sniffio have the wrong package name in the index page (`anyio`) and you require sniffio, we can conclude that your requirements are unsatisfiable. + + hint: Pre-releases are available for `sniffio` in the requested range (e.g., 4.0.0rc1), but pre-releases weren't enabled (try: `--prerelease=allow`) " );