diff --git a/crates/uv-distribution-types/src/traits.rs b/crates/uv-distribution-types/src/traits.rs index de3eb0dda..0ddf735e8 100644 --- a/crates/uv-distribution-types/src/traits.rs +++ b/crates/uv-distribution-types/src/traits.rs @@ -6,10 +6,11 @@ use uv_pep508::VerbatimUrl; use crate::error::Error; use crate::{ BuiltDist, CachedDirectUrlDist, CachedDist, CachedRegistryDist, DirectUrlBuiltDist, - DirectUrlSourceDist, Dist, DistributionId, GitSourceDist, InstalledDirectUrlDist, - InstalledDist, InstalledEggInfoDirectory, InstalledEggInfoFile, InstalledLegacyEditable, - InstalledRegistryDist, InstalledVersion, LocalDist, PackageId, PathBuiltDist, PathSourceDist, - RegistryBuiltWheel, RegistrySourceDist, ResourceId, SourceDist, VersionId, VersionOrUrlRef, + DirectUrlSourceDist, DirectorySourceDist, Dist, DistributionId, GitSourceDist, + InstalledDirectUrlDist, InstalledDist, InstalledEggInfoDirectory, InstalledEggInfoFile, + InstalledLegacyEditable, InstalledRegistryDist, InstalledVersion, LocalDist, PackageId, + PathBuiltDist, PathSourceDist, RegistryBuiltWheel, RegistrySourceDist, ResourceId, SourceDist, + VersionId, VersionOrUrlRef, }; pub trait Name { @@ -219,6 +220,12 @@ impl std::fmt::Display for PathSourceDist { } } +impl std::fmt::Display for DirectorySourceDist { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}{}", self.name(), self.version_or_url()) + } +} + impl std::fmt::Display for RegistryBuiltWheel { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}{}", self.name(), self.version_or_url()) diff --git a/crates/uv-distribution/src/distribution_database.rs b/crates/uv-distribution/src/distribution_database.rs index 1dbdf064f..06783f642 100644 --- a/crates/uv-distribution/src/distribution_database.rs +++ b/crates/uv-distribution/src/distribution_database.rs @@ -321,6 +321,8 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> { .boxed_local() .await?; + // Validate that the metadata is consistent with the distribution. + // If the wheel was unzipped previously, respect it. Source distributions are // cached under a unique revision ID, so unzipped directories are never stale. match built_wheel.target.canonicalize() { @@ -397,7 +399,10 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> { .await; match result { - Ok(metadata) => Ok(ArchiveMetadata::from_metadata23(metadata)), + Ok(metadata) => { + // Validate that the metadata is consistent with the distribution. + Ok(ArchiveMetadata::from_metadata23(metadata)) + } Err(err) if err.is_http_streaming_unsupported() => { warn!("Streaming unsupported when fetching metadata for {dist}; downloading wheel directly ({err})"); @@ -461,6 +466,8 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> { .boxed_local() .await?; + // Validate that the metadata is consistent with the distribution. + Ok(metadata) } diff --git a/crates/uv-distribution/src/error.rs b/crates/uv-distribution/src/error.rs index ee8ab68ba..72ee339ad 100644 --- a/crates/uv-distribution/src/error.rs +++ b/crates/uv-distribution/src/error.rs @@ -56,12 +56,26 @@ pub enum Error { #[error("Built wheel has an invalid filename")] WheelFilename(#[from] WheelFilenameError), #[error("Package metadata name `{metadata}` does not match given name `{given}`")] - NameMismatch { + WheelMetadataNameMismatch { given: PackageName, metadata: PackageName, }, #[error("Package metadata version `{metadata}` does not match given version `{given}`")] - VersionMismatch { given: Version, metadata: Version }, + WheelMetadataVersionMismatch { given: Version, metadata: Version }, + #[error( + "Package metadata name `{metadata}` does not match `{filename}` from the wheel filename" + )] + WheelFilenameNameMismatch { + filename: PackageName, + metadata: PackageName, + }, + #[error( + "Package metadata version `{metadata}` does not match `{filename}` from the wheel filename" + )] + WheelFilenameVersionMismatch { + filename: Version, + metadata: Version, + }, #[error("Failed to parse metadata from built wheel")] Metadata(#[from] uv_pypi_types::MetadataError), #[error("Failed to read metadata: `{}`", _0.user_display())] diff --git a/crates/uv-distribution/src/source/built_wheel_metadata.rs b/crates/uv-distribution/src/source/built_wheel_metadata.rs index 919f06d4f..09e62d78e 100644 --- a/crates/uv-distribution/src/source/built_wheel_metadata.rs +++ b/crates/uv-distribution/src/source/built_wheel_metadata.rs @@ -6,6 +6,8 @@ use uv_cache_info::CacheInfo; use uv_distribution_filename::WheelFilename; use uv_distribution_types::Hashed; use uv_fs::files; +use uv_normalize::PackageName; +use uv_pep440::Version; use uv_platform_tags::Tags; use uv_pypi_types::HashDigest; @@ -56,6 +58,12 @@ impl BuiltWheelMetadata { self.hashes = hashes; self } + + /// Returns `true` if the wheel matches the given package name and version. + pub(crate) fn matches(&self, name: Option<&PackageName>, version: Option<&Version>) -> bool { + name.map_or(true, |name| self.filename.name == *name) + && version.map_or(true, |version| self.filename.version == *version) + } } impl Hashed for BuiltWheelMetadata { diff --git a/crates/uv-distribution/src/source/mod.rs b/crates/uv-distribution/src/source/mod.rs index daaff2952..1edc63ce6 100644 --- a/crates/uv-distribution/src/source/mod.rs +++ b/crates/uv-distribution/src/source/mod.rs @@ -34,7 +34,8 @@ use uv_distribution_types::{ use uv_extract::hash::Hasher; use uv_fs::{rename_with_retry, write_atomic, LockedFile}; use uv_metadata::read_archive_metadata; -use uv_pep440::release_specifiers_to_ranges; +use uv_normalize::PackageName; +use uv_pep440::{release_specifiers_to_ranges, Version}; use uv_platform_tags::Tags; use uv_pypi_types::{HashAlgorithm, HashDigest, Metadata12, RequiresTxt, ResolutionMetadata}; use uv_types::{BuildContext, SourceBuildTrait}; @@ -416,7 +417,9 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { }; // If the cache contains a compatible wheel, return it. - if let Some(built_wheel) = BuiltWheelMetadata::find_in_cache(tags, &cache_shard) { + if let Some(built_wheel) = BuiltWheelMetadata::find_in_cache(tags, &cache_shard) + .filter(|built_wheel| built_wheel.matches(source.name(), source.version())) + { return Ok(built_wheel.with_hashes(revision.into_hashes())); } @@ -520,10 +523,13 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { // If the cache contains compatible metadata, return it. let metadata_entry = cache_shard.entry(METADATA); - if let Some(metadata) = read_cached_metadata(&metadata_entry).await? { + if let Some(metadata) = CachedMetadata::read(&metadata_entry) + .await? + .filter(|metadata| metadata.matches(source.name(), source.version())) + { debug!("Using cached metadata for: {source}"); return Ok(ArchiveMetadata { - metadata: Metadata::from_metadata23(metadata), + metadata: Metadata::from_metadata23(metadata.into()), hashes: revision.into_hashes(), }); } @@ -732,7 +738,9 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { }; // If the cache contains a compatible wheel, return it. - if let Some(built_wheel) = BuiltWheelMetadata::find_in_cache(tags, &cache_shard) { + if let Some(built_wheel) = BuiltWheelMetadata::find_in_cache(tags, &cache_shard) + .filter(|built_wheel| built_wheel.matches(source.name(), source.version())) + { return Ok(built_wheel); } @@ -824,10 +832,13 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { // If the cache contains compatible metadata, return it. let metadata_entry = cache_shard.entry(METADATA); - if let Some(metadata) = read_cached_metadata(&metadata_entry).await? { + if let Some(metadata) = CachedMetadata::read(&metadata_entry) + .await? + .filter(|metadata| metadata.matches(source.name(), source.version())) + { debug!("Using cached metadata for: {source}"); return Ok(ArchiveMetadata { - metadata: Metadata::from_metadata23(metadata), + metadata: Metadata::from_metadata23(metadata.into()), hashes: revision.into_hashes(), }); } @@ -1000,7 +1011,9 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { }; // If the cache contains a compatible wheel, return it. - if let Some(built_wheel) = BuiltWheelMetadata::find_in_cache(tags, &cache_shard) { + if let Some(built_wheel) = BuiltWheelMetadata::find_in_cache(tags, &cache_shard) + .filter(|built_wheel| built_wheel.matches(source.name(), source.version())) + { return Ok(built_wheel); } @@ -1095,11 +1108,14 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { // If the cache contains compatible metadata, return it. let metadata_entry = cache_shard.entry(METADATA); - if let Some(metadata) = read_cached_metadata(&metadata_entry).await? { + if let Some(metadata) = CachedMetadata::read(&metadata_entry) + .await? + .filter(|metadata| metadata.matches(source.name(), source.version())) + { debug!("Using cached metadata for: {source}"); return Ok(ArchiveMetadata::from( Metadata::from_workspace( - metadata, + metadata.into(), resource.install_path.as_ref(), None, self.build_context.locations(), @@ -1278,7 +1294,9 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { }; // If the cache contains a compatible wheel, return it. - if let Some(built_wheel) = BuiltWheelMetadata::find_in_cache(tags, &cache_shard) { + if let Some(built_wheel) = BuiltWheelMetadata::find_in_cache(tags, &cache_shard) + .filter(|built_wheel| built_wheel.matches(source.name(), source.version())) + { return Ok(built_wheel); } @@ -1388,7 +1406,10 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { .map_err(Error::CacheRead)? .is_fresh() { - if let Some(metadata) = read_cached_metadata(&metadata_entry).await? { + if let Some(metadata) = CachedMetadata::read(&metadata_entry) + .await? + .filter(|metadata| metadata.matches(source.name(), source.version())) + { let path = if let Some(subdirectory) = resource.subdirectory { Cow::Owned(fetch.path().join(subdirectory)) } else { @@ -1398,7 +1419,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { debug!("Using cached metadata for: {source}"); return Ok(ArchiveMetadata::from( Metadata::from_workspace( - metadata, + metadata.into(), &path, None, self.build_context.locations(), @@ -1798,6 +1819,14 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { .await .map_err(Error::Build)?; + // Read the metadata from the wheel. + let filename = WheelFilename::from_str(&disk_filename)?; + let metadata = read_wheel_metadata(&filename, &temp_dir.path().join(&disk_filename))?; + + // Validate the metadata. + validate_metadata(source, &metadata)?; + validate_filename(&filename, &metadata)?; + // Move the wheel to the cache. rename_with_retry( temp_dir.path().join(&disk_filename), @@ -1806,13 +1835,6 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { .await .map_err(Error::CacheWrite)?; - // Read the metadata from the wheel. - let filename = WheelFilename::from_str(&disk_filename)?; - let metadata = read_wheel_metadata(&filename, &cache_shard.join(&disk_filename))?; - - // Validate the metadata. - validate(source, &metadata)?; - debug!("Finished building: {source}"); Ok((disk_filename, filename, metadata)) } @@ -1883,7 +1905,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { let metadata = ResolutionMetadata::parse_metadata(&content)?; // Validate the metadata. - validate(source, &metadata)?; + validate_metadata(source, &metadata)?; Ok(Some(metadata)) } @@ -1899,7 +1921,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { debug!("Found static `pyproject.toml` for: {source}"); // Validate the metadata. - validate(source, &metadata)?; + validate_metadata(source, &metadata)?; return Ok(Some(metadata)); } @@ -1929,7 +1951,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { debug!("Found static `PKG-INFO` for: {source}"); // Validate the metadata. - validate(source, &metadata)?; + validate_metadata(source, &metadata)?; return Ok(Some(metadata)); } @@ -1953,7 +1975,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { debug!("Found static `egg-info` for: {source}"); // Validate the metadata. - validate(source, &metadata)?; + validate_metadata(source, &metadata)?; return Ok(Some(metadata)); } @@ -2065,10 +2087,13 @@ pub fn prune(cache: &Cache) -> Result { } /// Validate that the source distribution matches the built metadata. -fn validate(source: &BuildableSource<'_>, metadata: &ResolutionMetadata) -> Result<(), Error> { +fn validate_metadata( + source: &BuildableSource<'_>, + metadata: &ResolutionMetadata, +) -> Result<(), Error> { if let Some(name) = source.name() { if metadata.name != *name { - return Err(Error::NameMismatch { + return Err(Error::WheelMetadataNameMismatch { metadata: metadata.name.clone(), given: name.clone(), }); @@ -2077,7 +2102,7 @@ fn validate(source: &BuildableSource<'_>, metadata: &ResolutionMetadata) -> Resu if let Some(version) = source.version() { if metadata.version != *version { - return Err(Error::VersionMismatch { + return Err(Error::WheelMetadataVersionMismatch { metadata: metadata.version.clone(), given: version.clone(), }); @@ -2087,6 +2112,25 @@ fn validate(source: &BuildableSource<'_>, metadata: &ResolutionMetadata) -> Resu Ok(()) } +/// Validate that the source distribution matches the built filename. +fn validate_filename(filename: &WheelFilename, metadata: &ResolutionMetadata) -> Result<(), Error> { + if metadata.name != filename.name { + return Err(Error::WheelFilenameNameMismatch { + metadata: metadata.name.clone(), + filename: filename.name.clone(), + }); + } + + if metadata.version != filename.version { + return Err(Error::WheelFilenameVersionMismatch { + metadata: metadata.version.clone(), + filename: filename.version.clone(), + }); + } + + Ok(()) +} + /// A pointer to a source distribution revision in the cache, fetched from an HTTP archive. /// /// Encoded with `MsgPack`, and represented on disk by a `.http` file. @@ -2335,14 +2379,30 @@ async fn read_requires_dist(project_root: &Path) -> Result Result, Error> { - match fs::read(&cache_entry.path()).await { - Ok(cached) => Ok(Some(rmp_serde::from_slice(&cached)?)), - Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None), - Err(err) => Err(Error::CacheRead(err)), +/// Wheel metadata stored in the source distribution cache. +#[derive(Debug, Clone)] +struct CachedMetadata(ResolutionMetadata); + +impl CachedMetadata { + /// Read an existing cached [`ResolutionMetadata`], if it exists. + async fn read(cache_entry: &CacheEntry) -> Result, Error> { + match fs::read(&cache_entry.path()).await { + Ok(cached) => Ok(Some(Self(rmp_serde::from_slice(&cached)?))), + Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None), + Err(err) => Err(Error::CacheRead(err)), + } + } + + /// Returns `true` if the metadata matches the given package name and version. + fn matches(&self, name: Option<&PackageName>, version: Option<&Version>) -> bool { + name.map_or(true, |name| self.0.name == *name) + && version.map_or(true, |version| self.0.version == *version) + } +} + +impl From for ResolutionMetadata { + fn from(value: CachedMetadata) -> Self { + value.0 } } diff --git a/crates/uv-installer/src/plan.rs b/crates/uv-installer/src/plan.rs index 19216df61..0ef3c08a8 100644 --- a/crates/uv-installer/src/plan.rs +++ b/crates/uv-installer/src/plan.rs @@ -1,5 +1,5 @@ use anyhow::{bail, Result}; -use tracing::debug; +use tracing::{debug, warn}; use uv_cache::{Cache, CacheBucket, WheelCache}; use uv_cache_info::{CacheInfo, Timestamp}; @@ -135,6 +135,7 @@ impl<'a> Planner<'a> { Some(&entry.dist) }) { debug!("Requirement already cached: {distribution}"); + // STOPSHIP(charlie): If these are mismatched, skip and warn. cached.push(CachedDist::Registry(distribution.clone())); continue; } @@ -176,6 +177,7 @@ impl<'a> Planner<'a> { ); debug!("URL wheel requirement already cached: {cached_dist}"); + // STOPSHIP(charlie): If these are mismatched, skip and warn. cached.push(CachedDist::Url(cached_dist)); continue; } @@ -236,6 +238,9 @@ impl<'a> Planner<'a> { if *entry.index.url() != sdist.index { return None; } + if entry.dist.filename.name != sdist.name { + return None; + } if entry.dist.filename.version != sdist.version { return None; }; @@ -256,20 +261,36 @@ impl<'a> Planner<'a> { // Find the most-compatible wheel from the cache, since we don't know // the filename in advance. if let Some(wheel) = built_index.url(sdist)? { - let cached_dist = wheel.into_url_dist(sdist.url.clone()); - debug!("URL source requirement already cached: {cached_dist}"); - cached.push(CachedDist::Url(cached_dist)); - continue; + if wheel.filename.name == sdist.name { + let cached_dist = wheel.into_url_dist(sdist.url.clone()); + debug!("URL source requirement already cached: {cached_dist}"); + cached.push(CachedDist::Url(cached_dist)); + continue; + } + + warn!( + "Cached wheel filename does not match requested distribution for: `{}` (found: `{}`)", + sdist, + wheel.filename + ); } } Dist::Source(SourceDist::Git(sdist)) => { // Find the most-compatible wheel from the cache, since we don't know // the filename in advance. if let Some(wheel) = built_index.git(sdist) { - let cached_dist = wheel.into_url_dist(sdist.url.clone()); - debug!("Git source requirement already cached: {cached_dist}"); - cached.push(CachedDist::Url(cached_dist)); - continue; + if wheel.filename.name == sdist.name { + let cached_dist = wheel.into_url_dist(sdist.url.clone()); + debug!("Git source requirement already cached: {cached_dist}"); + cached.push(CachedDist::Url(cached_dist)); + continue; + } + + warn!( + "Cached wheel filename does not match requested distribution for: `{}` (found: `{}`)", + sdist, + wheel.filename + ); } } Dist::Source(SourceDist::Path(sdist)) => { @@ -281,10 +302,18 @@ impl<'a> Planner<'a> { // Find the most-compatible wheel from the cache, since we don't know // the filename in advance. if let Some(wheel) = built_index.path(sdist)? { - let cached_dist = wheel.into_url_dist(sdist.url.clone()); - debug!("Path source requirement already cached: {cached_dist}"); - cached.push(CachedDist::Url(cached_dist)); - continue; + if wheel.filename.name == sdist.name { + let cached_dist = wheel.into_url_dist(sdist.url.clone()); + debug!("Path source requirement already cached: {cached_dist}"); + cached.push(CachedDist::Url(cached_dist)); + continue; + } + + warn!( + "Cached wheel filename does not match requested distribution for: `{}` (found: `{}`)", + sdist, + wheel.filename + ); } } Dist::Source(SourceDist::Directory(sdist)) => { @@ -296,14 +325,22 @@ impl<'a> Planner<'a> { // Find the most-compatible wheel from the cache, since we don't know // the filename in advance. if let Some(wheel) = built_index.directory(sdist)? { - let cached_dist = if sdist.editable { - wheel.into_editable(sdist.url.clone()) - } else { - wheel.into_url_dist(sdist.url.clone()) - }; - debug!("Directory source requirement already cached: {cached_dist}"); - cached.push(CachedDist::Url(cached_dist)); - continue; + if wheel.filename.name == sdist.name { + let cached_dist = if sdist.editable { + wheel.into_editable(sdist.url.clone()) + } else { + wheel.into_url_dist(sdist.url.clone()) + }; + debug!("Directory source requirement already cached: {cached_dist}"); + cached.push(CachedDist::Url(cached_dist)); + continue; + } + + warn!( + "Cached wheel filename does not match requested distribution for: `{}` (found: `{}`)", + sdist, + wheel.filename + ); } } } diff --git a/crates/uv-installer/src/preparer.rs b/crates/uv-installer/src/preparer.rs index ba40a60a0..e61775757 100644 --- a/crates/uv-installer/src/preparer.rs +++ b/crates/uv-installer/src/preparer.rs @@ -162,7 +162,35 @@ impl<'a, Context: BuildContext> Preparer<'a, Context> { .expect("missing value for registered task"); match result.as_ref() { - Ok(cached) => Ok(cached.clone()), + Ok(cached) => { + // Validate that the wheel is compatible with the distribution. + // + // `get_or_build_wheel` is guaranteed to return a wheel that matches the + // distribution. But there could be multiple requested distributions that share + // a cache entry in `in_flight`, so we need to double-check here. + // + // For example, if two requirements are based on the same local path, but use + // different names, then they'll share an `in_flight` entry, but one of the two + // should be rejected (since at least one of the names will not match the + // package name). + if *dist.name() != cached.filename().name { + let err = uv_distribution::Error::WheelMetadataNameMismatch { + given: dist.name().clone(), + metadata: cached.filename().name.clone(), + }; + return Err(Error::from_dist(dist, err)); + } + if let Some(version) = dist.version() { + if *version != cached.filename().version { + let err = uv_distribution::Error::WheelMetadataVersionMismatch { + given: version.clone(), + metadata: cached.filename().version.clone(), + }; + return Err(Error::from_dist(dist, err)); + } + } + Ok(cached.clone()) + } Err(err) => Err(Error::Thread(err.to_string())), } } diff --git a/crates/uv-resolver/src/resolver/provider.rs b/crates/uv-resolver/src/resolver/provider.rs index b17ae7ff7..943dfa864 100644 --- a/crates/uv-resolver/src/resolver/provider.rs +++ b/crates/uv-resolver/src/resolver/provider.rs @@ -198,10 +198,10 @@ impl<'a, Context: BuildContext> ResolverProvider for DefaultResolverProvider<'a, } kind => Err(uv_client::Error::from(kind).into()), }, - uv_distribution::Error::VersionMismatch { .. } => { + uv_distribution::Error::WheelMetadataVersionMismatch { .. } => { Ok(MetadataResponse::InconsistentMetadata(Box::new(err))) } - uv_distribution::Error::NameMismatch { .. } => { + uv_distribution::Error::WheelMetadataNameMismatch { .. } => { Ok(MetadataResponse::InconsistentMetadata(Box::new(err))) } uv_distribution::Error::Metadata(err) => { diff --git a/crates/uv/tests/it/lock.rs b/crates/uv/tests/it/lock.rs index 9337baaba..63dd46516 100644 --- a/crates/uv/tests/it/lock.rs +++ b/crates/uv/tests/it/lock.rs @@ -18874,3 +18874,39 @@ fn lock_derivation_chain_extended() -> Result<()> { Ok(()) } + +/// The project itself is marked as an editable dependency, but under the wrong name. The project +/// itself isn't a package. +#[test] +fn mismatched_name_self_editable() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["foo"] + + [tool.uv.sources] + foo = { path = ".", editable = true } + "#, + )?; + + // Running `uv sync` should generate a lockfile. + uv_snapshot!(context.filters(), context.sync(), @r###" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + × Failed to build `foo @ file://[TEMP_DIR]/` + ╰─▶ Package metadata name `project` does not match given name `foo` + help: `foo` was included because `project` (v0.1.0) depends on `foo` + "###); + + Ok(()) +} diff --git a/crates/uv/tests/it/sync.rs b/crates/uv/tests/it/sync.rs index c3c5b8ac4..f11a72098 100644 --- a/crates/uv/tests/it/sync.rs +++ b/crates/uv/tests/it/sync.rs @@ -5109,3 +5109,94 @@ fn sync_derivation_chain_group() -> Result<()> { Ok(()) } + +/// The project itself is marked as an editable dependency, but under the wrong name. The project +/// is a package. +#[test] +fn mismatched_name_self_editable() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["foo"] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + + [tool.uv.sources] + foo = { path = ".", editable = true } + "#, + )?; + + uv_snapshot!(context.filters(), context.sync(), @r###" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + × Failed to build `foo @ file://[TEMP_DIR]/` + ╰─▶ Package metadata name `project` does not match given name `foo` + help: `foo` was included because `project` (v0.1.0) depends on `foo` + "###); + + Ok(()) +} + +/// A wheel is available in the cache, but was requested under the wrong name. +#[test] +fn mismatched_name_cached_wheel() -> Result<()> { + let context = TestContext::new("3.12"); + + // Cache the `iniconfig` wheel. + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["iniconfig @ https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz"] + "#, + )?; + + uv_snapshot!(context.filters(), context.sync(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + iniconfig==2.0.0 (from https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz) + "###); + + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["foo @ https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz"] + "#, + )?; + + uv_snapshot!(context.filters(), context.sync(), @r###" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × Failed to download and build `foo @ https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz` + ╰─▶ Package metadata name `iniconfig` does not match given name `foo` + "###); + + Ok(()) +}