mirror of https://github.com/astral-sh/uv
Consistently enforce requested-vs.-built metadata when retrieving wheels (#9484)
## Summary We were missing a bunch of edge cases, e.g., the wheel exists in the cache already. Closes https://github.com/astral-sh/uv/issues/9480.
This commit is contained in:
parent
4f2b30ca02
commit
f1ccbcb065
|
|
@ -6,10 +6,11 @@ use uv_pep508::VerbatimUrl;
|
||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
use crate::{
|
use crate::{
|
||||||
BuiltDist, CachedDirectUrlDist, CachedDist, CachedRegistryDist, DirectUrlBuiltDist,
|
BuiltDist, CachedDirectUrlDist, CachedDist, CachedRegistryDist, DirectUrlBuiltDist,
|
||||||
DirectUrlSourceDist, Dist, DistributionId, GitSourceDist, InstalledDirectUrlDist,
|
DirectUrlSourceDist, DirectorySourceDist, Dist, DistributionId, GitSourceDist,
|
||||||
InstalledDist, InstalledEggInfoDirectory, InstalledEggInfoFile, InstalledLegacyEditable,
|
InstalledDirectUrlDist, InstalledDist, InstalledEggInfoDirectory, InstalledEggInfoFile,
|
||||||
InstalledRegistryDist, InstalledVersion, LocalDist, PackageId, PathBuiltDist, PathSourceDist,
|
InstalledLegacyEditable, InstalledRegistryDist, InstalledVersion, LocalDist, PackageId,
|
||||||
RegistryBuiltWheel, RegistrySourceDist, ResourceId, SourceDist, VersionId, VersionOrUrlRef,
|
PathBuiltDist, PathSourceDist, RegistryBuiltWheel, RegistrySourceDist, ResourceId, SourceDist,
|
||||||
|
VersionId, VersionOrUrlRef,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub trait Name {
|
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 {
|
impl std::fmt::Display for RegistryBuiltWheel {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
write!(f, "{}{}", self.name(), self.version_or_url())
|
write!(f, "{}{}", self.name(), self.version_or_url())
|
||||||
|
|
|
||||||
|
|
@ -321,6 +321,8 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> {
|
||||||
.boxed_local()
|
.boxed_local()
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
// Validate that the metadata is consistent with the distribution.
|
||||||
|
|
||||||
// If the wheel was unzipped previously, respect it. Source distributions are
|
// If the wheel was unzipped previously, respect it. Source distributions are
|
||||||
// cached under a unique revision ID, so unzipped directories are never stale.
|
// cached under a unique revision ID, so unzipped directories are never stale.
|
||||||
match built_wheel.target.canonicalize() {
|
match built_wheel.target.canonicalize() {
|
||||||
|
|
@ -397,7 +399,10 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> {
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
match result {
|
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() => {
|
Err(err) if err.is_http_streaming_unsupported() => {
|
||||||
warn!("Streaming unsupported when fetching metadata for {dist}; downloading wheel directly ({err})");
|
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()
|
.boxed_local()
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
// Validate that the metadata is consistent with the distribution.
|
||||||
|
|
||||||
Ok(metadata)
|
Ok(metadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -56,12 +56,26 @@ pub enum Error {
|
||||||
#[error("Built wheel has an invalid filename")]
|
#[error("Built wheel has an invalid filename")]
|
||||||
WheelFilename(#[from] WheelFilenameError),
|
WheelFilename(#[from] WheelFilenameError),
|
||||||
#[error("Package metadata name `{metadata}` does not match given name `{given}`")]
|
#[error("Package metadata name `{metadata}` does not match given name `{given}`")]
|
||||||
NameMismatch {
|
WheelMetadataNameMismatch {
|
||||||
given: PackageName,
|
given: PackageName,
|
||||||
metadata: PackageName,
|
metadata: PackageName,
|
||||||
},
|
},
|
||||||
#[error("Package metadata version `{metadata}` does not match given version `{given}`")]
|
#[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")]
|
#[error("Failed to parse metadata from built wheel")]
|
||||||
Metadata(#[from] uv_pypi_types::MetadataError),
|
Metadata(#[from] uv_pypi_types::MetadataError),
|
||||||
#[error("Failed to read metadata: `{}`", _0.user_display())]
|
#[error("Failed to read metadata: `{}`", _0.user_display())]
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ use uv_cache_info::CacheInfo;
|
||||||
use uv_distribution_filename::WheelFilename;
|
use uv_distribution_filename::WheelFilename;
|
||||||
use uv_distribution_types::Hashed;
|
use uv_distribution_types::Hashed;
|
||||||
use uv_fs::files;
|
use uv_fs::files;
|
||||||
|
use uv_normalize::PackageName;
|
||||||
|
use uv_pep440::Version;
|
||||||
use uv_platform_tags::Tags;
|
use uv_platform_tags::Tags;
|
||||||
use uv_pypi_types::HashDigest;
|
use uv_pypi_types::HashDigest;
|
||||||
|
|
||||||
|
|
@ -56,6 +58,12 @@ impl BuiltWheelMetadata {
|
||||||
self.hashes = hashes;
|
self.hashes = hashes;
|
||||||
self
|
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 {
|
impl Hashed for BuiltWheelMetadata {
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,8 @@ use uv_distribution_types::{
|
||||||
use uv_extract::hash::Hasher;
|
use uv_extract::hash::Hasher;
|
||||||
use uv_fs::{rename_with_retry, write_atomic, LockedFile};
|
use uv_fs::{rename_with_retry, write_atomic, LockedFile};
|
||||||
use uv_metadata::read_archive_metadata;
|
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_platform_tags::Tags;
|
||||||
use uv_pypi_types::{HashAlgorithm, HashDigest, Metadata12, RequiresTxt, ResolutionMetadata};
|
use uv_pypi_types::{HashAlgorithm, HashDigest, Metadata12, RequiresTxt, ResolutionMetadata};
|
||||||
use uv_types::{BuildContext, SourceBuildTrait};
|
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 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()));
|
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.
|
// If the cache contains compatible metadata, return it.
|
||||||
let metadata_entry = cache_shard.entry(METADATA);
|
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}");
|
debug!("Using cached metadata for: {source}");
|
||||||
return Ok(ArchiveMetadata {
|
return Ok(ArchiveMetadata {
|
||||||
metadata: Metadata::from_metadata23(metadata),
|
metadata: Metadata::from_metadata23(metadata.into()),
|
||||||
hashes: revision.into_hashes(),
|
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 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);
|
return Ok(built_wheel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -824,10 +832,13 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
|
||||||
|
|
||||||
// If the cache contains compatible metadata, return it.
|
// If the cache contains compatible metadata, return it.
|
||||||
let metadata_entry = cache_shard.entry(METADATA);
|
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}");
|
debug!("Using cached metadata for: {source}");
|
||||||
return Ok(ArchiveMetadata {
|
return Ok(ArchiveMetadata {
|
||||||
metadata: Metadata::from_metadata23(metadata),
|
metadata: Metadata::from_metadata23(metadata.into()),
|
||||||
hashes: revision.into_hashes(),
|
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 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);
|
return Ok(built_wheel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1095,11 +1108,14 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
|
||||||
|
|
||||||
// If the cache contains compatible metadata, return it.
|
// If the cache contains compatible metadata, return it.
|
||||||
let metadata_entry = cache_shard.entry(METADATA);
|
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}");
|
debug!("Using cached metadata for: {source}");
|
||||||
return Ok(ArchiveMetadata::from(
|
return Ok(ArchiveMetadata::from(
|
||||||
Metadata::from_workspace(
|
Metadata::from_workspace(
|
||||||
metadata,
|
metadata.into(),
|
||||||
resource.install_path.as_ref(),
|
resource.install_path.as_ref(),
|
||||||
None,
|
None,
|
||||||
self.build_context.locations(),
|
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 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);
|
return Ok(built_wheel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1388,7 +1406,10 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
|
||||||
.map_err(Error::CacheRead)?
|
.map_err(Error::CacheRead)?
|
||||||
.is_fresh()
|
.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 {
|
let path = if let Some(subdirectory) = resource.subdirectory {
|
||||||
Cow::Owned(fetch.path().join(subdirectory))
|
Cow::Owned(fetch.path().join(subdirectory))
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -1398,7 +1419,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
|
||||||
debug!("Using cached metadata for: {source}");
|
debug!("Using cached metadata for: {source}");
|
||||||
return Ok(ArchiveMetadata::from(
|
return Ok(ArchiveMetadata::from(
|
||||||
Metadata::from_workspace(
|
Metadata::from_workspace(
|
||||||
metadata,
|
metadata.into(),
|
||||||
&path,
|
&path,
|
||||||
None,
|
None,
|
||||||
self.build_context.locations(),
|
self.build_context.locations(),
|
||||||
|
|
@ -1798,6 +1819,14 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
|
||||||
.await
|
.await
|
||||||
.map_err(Error::Build)?;
|
.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.
|
// Move the wheel to the cache.
|
||||||
rename_with_retry(
|
rename_with_retry(
|
||||||
temp_dir.path().join(&disk_filename),
|
temp_dir.path().join(&disk_filename),
|
||||||
|
|
@ -1806,13 +1835,6 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
|
||||||
.await
|
.await
|
||||||
.map_err(Error::CacheWrite)?;
|
.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}");
|
debug!("Finished building: {source}");
|
||||||
Ok((disk_filename, filename, metadata))
|
Ok((disk_filename, filename, metadata))
|
||||||
}
|
}
|
||||||
|
|
@ -1883,7 +1905,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
|
||||||
let metadata = ResolutionMetadata::parse_metadata(&content)?;
|
let metadata = ResolutionMetadata::parse_metadata(&content)?;
|
||||||
|
|
||||||
// Validate the metadata.
|
// Validate the metadata.
|
||||||
validate(source, &metadata)?;
|
validate_metadata(source, &metadata)?;
|
||||||
|
|
||||||
Ok(Some(metadata))
|
Ok(Some(metadata))
|
||||||
}
|
}
|
||||||
|
|
@ -1899,7 +1921,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
|
||||||
debug!("Found static `pyproject.toml` for: {source}");
|
debug!("Found static `pyproject.toml` for: {source}");
|
||||||
|
|
||||||
// Validate the metadata.
|
// Validate the metadata.
|
||||||
validate(source, &metadata)?;
|
validate_metadata(source, &metadata)?;
|
||||||
|
|
||||||
return Ok(Some(metadata));
|
return Ok(Some(metadata));
|
||||||
}
|
}
|
||||||
|
|
@ -1929,7 +1951,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
|
||||||
debug!("Found static `PKG-INFO` for: {source}");
|
debug!("Found static `PKG-INFO` for: {source}");
|
||||||
|
|
||||||
// Validate the metadata.
|
// Validate the metadata.
|
||||||
validate(source, &metadata)?;
|
validate_metadata(source, &metadata)?;
|
||||||
|
|
||||||
return Ok(Some(metadata));
|
return Ok(Some(metadata));
|
||||||
}
|
}
|
||||||
|
|
@ -1953,7 +1975,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
|
||||||
debug!("Found static `egg-info` for: {source}");
|
debug!("Found static `egg-info` for: {source}");
|
||||||
|
|
||||||
// Validate the metadata.
|
// Validate the metadata.
|
||||||
validate(source, &metadata)?;
|
validate_metadata(source, &metadata)?;
|
||||||
|
|
||||||
return Ok(Some(metadata));
|
return Ok(Some(metadata));
|
||||||
}
|
}
|
||||||
|
|
@ -2065,10 +2087,13 @@ pub fn prune(cache: &Cache) -> Result<Removal, Error> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Validate that the source distribution matches the built metadata.
|
/// 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 let Some(name) = source.name() {
|
||||||
if metadata.name != *name {
|
if metadata.name != *name {
|
||||||
return Err(Error::NameMismatch {
|
return Err(Error::WheelMetadataNameMismatch {
|
||||||
metadata: metadata.name.clone(),
|
metadata: metadata.name.clone(),
|
||||||
given: name.clone(),
|
given: name.clone(),
|
||||||
});
|
});
|
||||||
|
|
@ -2077,7 +2102,7 @@ fn validate(source: &BuildableSource<'_>, metadata: &ResolutionMetadata) -> Resu
|
||||||
|
|
||||||
if let Some(version) = source.version() {
|
if let Some(version) = source.version() {
|
||||||
if metadata.version != *version {
|
if metadata.version != *version {
|
||||||
return Err(Error::VersionMismatch {
|
return Err(Error::WheelMetadataVersionMismatch {
|
||||||
metadata: metadata.version.clone(),
|
metadata: metadata.version.clone(),
|
||||||
given: version.clone(),
|
given: version.clone(),
|
||||||
});
|
});
|
||||||
|
|
@ -2087,6 +2112,25 @@ fn validate(source: &BuildableSource<'_>, metadata: &ResolutionMetadata) -> Resu
|
||||||
Ok(())
|
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.
|
/// 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.
|
/// Encoded with `MsgPack`, and represented on disk by a `.http` file.
|
||||||
|
|
@ -2335,14 +2379,30 @@ async fn read_requires_dist(project_root: &Path) -> Result<uv_pypi_types::Requir
|
||||||
Ok(requires_dist)
|
Ok(requires_dist)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read an existing cached [`ResolutionMetadata`], if it exists.
|
/// Wheel metadata stored in the source distribution cache.
|
||||||
async fn read_cached_metadata(
|
#[derive(Debug, Clone)]
|
||||||
cache_entry: &CacheEntry,
|
struct CachedMetadata(ResolutionMetadata);
|
||||||
) -> Result<Option<ResolutionMetadata>, Error> {
|
|
||||||
match fs::read(&cache_entry.path()).await {
|
impl CachedMetadata {
|
||||||
Ok(cached) => Ok(Some(rmp_serde::from_slice(&cached)?)),
|
/// Read an existing cached [`ResolutionMetadata`], if it exists.
|
||||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
|
async fn read(cache_entry: &CacheEntry) -> Result<Option<Self>, Error> {
|
||||||
Err(err) => Err(Error::CacheRead(err)),
|
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<CachedMetadata> for ResolutionMetadata {
|
||||||
|
fn from(value: CachedMetadata) -> Self {
|
||||||
|
value.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
use anyhow::{bail, Result};
|
use anyhow::{bail, Result};
|
||||||
use tracing::debug;
|
use tracing::{debug, warn};
|
||||||
|
|
||||||
use uv_cache::{Cache, CacheBucket, WheelCache};
|
use uv_cache::{Cache, CacheBucket, WheelCache};
|
||||||
use uv_cache_info::{CacheInfo, Timestamp};
|
use uv_cache_info::{CacheInfo, Timestamp};
|
||||||
|
|
@ -135,6 +135,7 @@ impl<'a> Planner<'a> {
|
||||||
Some(&entry.dist)
|
Some(&entry.dist)
|
||||||
}) {
|
}) {
|
||||||
debug!("Requirement already cached: {distribution}");
|
debug!("Requirement already cached: {distribution}");
|
||||||
|
// STOPSHIP(charlie): If these are mismatched, skip and warn.
|
||||||
cached.push(CachedDist::Registry(distribution.clone()));
|
cached.push(CachedDist::Registry(distribution.clone()));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -176,6 +177,7 @@ impl<'a> Planner<'a> {
|
||||||
);
|
);
|
||||||
|
|
||||||
debug!("URL wheel requirement already cached: {cached_dist}");
|
debug!("URL wheel requirement already cached: {cached_dist}");
|
||||||
|
// STOPSHIP(charlie): If these are mismatched, skip and warn.
|
||||||
cached.push(CachedDist::Url(cached_dist));
|
cached.push(CachedDist::Url(cached_dist));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -236,6 +238,9 @@ impl<'a> Planner<'a> {
|
||||||
if *entry.index.url() != sdist.index {
|
if *entry.index.url() != sdist.index {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
if entry.dist.filename.name != sdist.name {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
if entry.dist.filename.version != sdist.version {
|
if entry.dist.filename.version != sdist.version {
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
|
|
@ -256,20 +261,36 @@ impl<'a> Planner<'a> {
|
||||||
// Find the most-compatible wheel from the cache, since we don't know
|
// Find the most-compatible wheel from the cache, since we don't know
|
||||||
// the filename in advance.
|
// the filename in advance.
|
||||||
if let Some(wheel) = built_index.url(sdist)? {
|
if let Some(wheel) = built_index.url(sdist)? {
|
||||||
let cached_dist = wheel.into_url_dist(sdist.url.clone());
|
if wheel.filename.name == sdist.name {
|
||||||
debug!("URL source requirement already cached: {cached_dist}");
|
let cached_dist = wheel.into_url_dist(sdist.url.clone());
|
||||||
cached.push(CachedDist::Url(cached_dist));
|
debug!("URL source requirement already cached: {cached_dist}");
|
||||||
continue;
|
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)) => {
|
Dist::Source(SourceDist::Git(sdist)) => {
|
||||||
// Find the most-compatible wheel from the cache, since we don't know
|
// Find the most-compatible wheel from the cache, since we don't know
|
||||||
// the filename in advance.
|
// the filename in advance.
|
||||||
if let Some(wheel) = built_index.git(sdist) {
|
if let Some(wheel) = built_index.git(sdist) {
|
||||||
let cached_dist = wheel.into_url_dist(sdist.url.clone());
|
if wheel.filename.name == sdist.name {
|
||||||
debug!("Git source requirement already cached: {cached_dist}");
|
let cached_dist = wheel.into_url_dist(sdist.url.clone());
|
||||||
cached.push(CachedDist::Url(cached_dist));
|
debug!("Git source requirement already cached: {cached_dist}");
|
||||||
continue;
|
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)) => {
|
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
|
// Find the most-compatible wheel from the cache, since we don't know
|
||||||
// the filename in advance.
|
// the filename in advance.
|
||||||
if let Some(wheel) = built_index.path(sdist)? {
|
if let Some(wheel) = built_index.path(sdist)? {
|
||||||
let cached_dist = wheel.into_url_dist(sdist.url.clone());
|
if wheel.filename.name == sdist.name {
|
||||||
debug!("Path source requirement already cached: {cached_dist}");
|
let cached_dist = wheel.into_url_dist(sdist.url.clone());
|
||||||
cached.push(CachedDist::Url(cached_dist));
|
debug!("Path source requirement already cached: {cached_dist}");
|
||||||
continue;
|
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)) => {
|
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
|
// Find the most-compatible wheel from the cache, since we don't know
|
||||||
// the filename in advance.
|
// the filename in advance.
|
||||||
if let Some(wheel) = built_index.directory(sdist)? {
|
if let Some(wheel) = built_index.directory(sdist)? {
|
||||||
let cached_dist = if sdist.editable {
|
if wheel.filename.name == sdist.name {
|
||||||
wheel.into_editable(sdist.url.clone())
|
let cached_dist = if sdist.editable {
|
||||||
} else {
|
wheel.into_editable(sdist.url.clone())
|
||||||
wheel.into_url_dist(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));
|
debug!("Directory source requirement already cached: {cached_dist}");
|
||||||
continue;
|
cached.push(CachedDist::Url(cached_dist));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
warn!(
|
||||||
|
"Cached wheel filename does not match requested distribution for: `{}` (found: `{}`)",
|
||||||
|
sdist,
|
||||||
|
wheel.filename
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -162,7 +162,35 @@ impl<'a, Context: BuildContext> Preparer<'a, Context> {
|
||||||
.expect("missing value for registered task");
|
.expect("missing value for registered task");
|
||||||
|
|
||||||
match result.as_ref() {
|
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())),
|
Err(err) => Err(Error::Thread(err.to_string())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -198,10 +198,10 @@ impl<'a, Context: BuildContext> ResolverProvider for DefaultResolverProvider<'a,
|
||||||
}
|
}
|
||||||
kind => Err(uv_client::Error::from(kind).into()),
|
kind => Err(uv_client::Error::from(kind).into()),
|
||||||
},
|
},
|
||||||
uv_distribution::Error::VersionMismatch { .. } => {
|
uv_distribution::Error::WheelMetadataVersionMismatch { .. } => {
|
||||||
Ok(MetadataResponse::InconsistentMetadata(Box::new(err)))
|
Ok(MetadataResponse::InconsistentMetadata(Box::new(err)))
|
||||||
}
|
}
|
||||||
uv_distribution::Error::NameMismatch { .. } => {
|
uv_distribution::Error::WheelMetadataNameMismatch { .. } => {
|
||||||
Ok(MetadataResponse::InconsistentMetadata(Box::new(err)))
|
Ok(MetadataResponse::InconsistentMetadata(Box::new(err)))
|
||||||
}
|
}
|
||||||
uv_distribution::Error::Metadata(err) => {
|
uv_distribution::Error::Metadata(err) => {
|
||||||
|
|
|
||||||
|
|
@ -18874,3 +18874,39 @@ fn lock_derivation_chain_extended() -> Result<()> {
|
||||||
|
|
||||||
Ok(())
|
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(())
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -5109,3 +5109,94 @@ fn sync_derivation_chain_group() -> Result<()> {
|
||||||
|
|
||||||
Ok(())
|
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(())
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue