From 4d4b968f250f680fe2e4bf1fa9d17b95bc12acbb Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Tue, 17 Mar 2026 08:18:09 +0900 Subject: [PATCH] Deprecate some non-PEP 625 source distributions (#17467) --- Cargo.lock | 1 + .../uv-distribution-filename/src/extension.rs | 5 ++- crates/uv-distribution-types/src/lib.rs | 10 ++++++ crates/uv-distribution/Cargo.toml | 1 + .../src/distribution_database.rs | 31 ++++++++++++++++++- crates/uv/tests/it/pip_sync.rs | 1 + docs/concepts/resolution.md | 11 +++++++ 7 files changed, 58 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 57dafc499..0f33c4314 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6323,6 +6323,7 @@ dependencies = [ "uv-pypi-types", "uv-redacted", "uv-types", + "uv-warnings", "uv-workspace", "walkdir", "zip", diff --git a/crates/uv-distribution-filename/src/extension.rs b/crates/uv-distribution-filename/src/extension.rs index 233dfa9a1..1849600a5 100644 --- a/crates/uv-distribution-filename/src/extension.rs +++ b/crates/uv-distribution-filename/src/extension.rs @@ -82,13 +82,16 @@ impl SourceDistExtension { }; match extension { + "gz" if is_tar(path.as_ref()) => Ok(Self::TarGz), + // TODO: Remove these other extensions in the future. + // NOTE: These get parsed from network sources like PyPI, so we don't + // necessarily want to hard-fail, just skip in the future. "zip" => Ok(Self::Zip), "tar" => Ok(Self::Tar), "tgz" => Ok(Self::Tgz), "tbz" => Ok(Self::Tbz), "txz" => Ok(Self::Txz), "tlz" => Ok(Self::Tlz), - "gz" if is_tar(path.as_ref()) => Ok(Self::TarGz), "bz2" if is_tar(path.as_ref()) => Ok(Self::TarBz2), "xz" if is_tar(path.as_ref()) => Ok(Self::TarXz), "lz" if is_tar(path.as_ref()) => Ok(Self::TarLz), diff --git a/crates/uv-distribution-types/src/lib.rs b/crates/uv-distribution-types/src/lib.rs index 6d91e9d36..568946cfd 100644 --- a/crates/uv-distribution-types/src/lib.rs +++ b/crates/uv-distribution-types/src/lib.rs @@ -632,6 +632,16 @@ impl BuiltDist { } impl SourceDist { + /// Returns the [`SourceDistExtension`] of the distribution, if it has one. + pub fn extension(&self) -> Option { + match self { + Self::Registry(source_dist) => Some(source_dist.ext), + Self::DirectUrl(source_dist) => Some(source_dist.ext), + Self::Path(source_dist) => Some(source_dist.ext), + Self::Git(_) | Self::Directory(_) => None, + } + } + /// Returns the [`IndexUrl`], if the distribution is from a registry. pub fn index(&self) -> Option<&IndexUrl> { match self { diff --git a/crates/uv-distribution/Cargo.toml b/crates/uv-distribution/Cargo.toml index 5ffbc921d..1eddaa6ff 100644 --- a/crates/uv-distribution/Cargo.toml +++ b/crates/uv-distribution/Cargo.toml @@ -36,6 +36,7 @@ uv-platform-tags = { workspace = true } uv-pypi-types = { workspace = true } uv-redacted = { workspace = true } uv-types = { workspace = true } +uv-warnings = { workspace = true } uv-workspace = { workspace = true } anyhow = { workspace = true } diff --git a/crates/uv-distribution/src/distribution_database.rs b/crates/uv-distribution/src/distribution_database.rs index 8f1dc9670..668022311 100644 --- a/crates/uv-distribution/src/distribution_database.rs +++ b/crates/uv-distribution/src/distribution_database.rs @@ -18,7 +18,7 @@ use uv_cache_info::{CacheInfo, Timestamp}; use uv_client::{ CacheControl, CachedClientError, Connectivity, DataWithCachePolicy, RegistryClient, }; -use uv_distribution_filename::WheelFilename; +use uv_distribution_filename::{SourceDistExtension, WheelFilename}; use uv_distribution_types::{ BuildInfo, BuildableSource, BuiltDist, Dist, File, HashPolicy, Hashed, IndexUrl, InstalledDist, Name, SourceDist, ToUrlError, @@ -29,6 +29,7 @@ use uv_platform_tags::Tags; use uv_pypi_types::{HashDigest, HashDigests, PyProjectToml}; use uv_redacted::DisplaySafeUrl; use uv_types::{BuildContext, BuildStack}; +use uv_warnings::warn_user_once; use crate::archive::Archive; use crate::metadata::{ArchiveMetadata, Metadata}; @@ -380,6 +381,34 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> { tags: &Tags, hashes: HashPolicy<'_>, ) -> Result { + // Warn if the source distribution isn't PEP 625 compliant. + // We do this here instead of in `SourceDistExtension::from_path` to minimize log volume: + // a non-compliant distribution isn't a huge problem if it's not actually being + // materialized into a wheel. Observe that we also allow no extension, since we expect that + // for directory and Git installs. + // NOTE: Observe that we also allow `.zip` sdists here, which are not PEP 625 compliant. + // This is because they were allowed on PyPI until relatively recently (2020). + if let Some(extension) = dist.extension() + && !matches!( + extension, + SourceDistExtension::TarGz | SourceDistExtension::Zip + ) + { + if matches!(dist, SourceDist::Registry(_)) { + // Observe that we display a slightly different warning when the sdist comes + // from a registry, since that suggests that the user has inadvertently + // (rather than explicitly) depended on a non-compliant sdist. + warn_user_once!( + "{dist} uses a legacy source distribution format ('.{extension}') that is not compliant with PEP 625. A future version of uv will reject this source distribution. Consider upgrading to a newer version of {package}", + package = dist.name(), + ); + } else { + warn_user_once!( + "{dist} is not a standards-compliant source distribution: expected '.tar.gz' but found '.{extension}'. A future version of uv will reject source distributions that do not meet the requirements specified in PEP 625", + ); + } + } + let built_wheel = self .builder .download_and_build(&BuildableSource::Dist(dist), tags, hashes, &self.client) diff --git a/crates/uv/tests/it/pip_sync.rs b/crates/uv/tests/it/pip_sync.rs index b482ed015..16f02f5e8 100644 --- a/crates/uv/tests/it/pip_sync.rs +++ b/crates/uv/tests/it/pip_sync.rs @@ -881,6 +881,7 @@ fn install_sdist_archive_type_bz2() -> Result<()> { ----- stderr ----- Resolved 1 package in [TIME] + warning: bz2 @ file://[WORKSPACE]/test/links/bz2-1.0.0.tar.bz2 is not a standards-compliant source distribution: expected '.tar.gz' but found '.tar.bz2'. A future version of uv will reject source distributions that do not meet the requirements specified in PEP 625 Prepared 1 package in [TIME] Installed 1 package in [TIME] + bz2==1.0.0 (from file://[WORKSPACE]/test/links/bz2-1.0.0.tar.bz2) diff --git a/docs/concepts/resolution.md b/docs/concepts/resolution.md index f1232a29d..d133e40bb 100644 --- a/docs/concepts/resolution.md +++ b/docs/concepts/resolution.md @@ -781,6 +781,17 @@ reading and extracting archives in the following formats: - lzma tarball (`.tar.lzma`) - zip (`.zip`) +!!! important + + Using source distribution extensions other than `.tar.gz` is strongly + discouraged, as these extensions are not widely or consistently + supported across the Python packaging ecosystem. + +!!! warning "Deprecated" + + Support for source distribution extensions other than `.tar.gz` is + deprecated and will be removed in a future release of uv. + ## Lockfile versioning The `uv.lock` file uses a versioned schema. The schema version is included in the `version` field of