mirror of
https://github.com/astral-sh/uv
synced 2026-06-21 13:47:25 +00:00
Remove xz support entirely (#19143)
Signed-off-by: William Woodruff <william@astral.sh>
This commit is contained in:
committed by
William Woodruff
parent
6abcbf3bb9
commit
73c9b21128
Generated
-24
@@ -370,7 +370,6 @@ dependencies = [
|
||||
"memchr",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
"xz2",
|
||||
"zstd",
|
||||
"zstd-safe",
|
||||
]
|
||||
@@ -2622,17 +2621,6 @@ version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
||||
|
||||
[[package]]
|
||||
name = "lzma-sys"
|
||||
version = "0.1.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mailparse"
|
||||
version = "0.16.1"
|
||||
@@ -6364,7 +6352,6 @@ dependencies = [
|
||||
"uv-python",
|
||||
"uv-redacted",
|
||||
"uv-types",
|
||||
"uv-warnings",
|
||||
"uv-workspace",
|
||||
"walkdir",
|
||||
]
|
||||
@@ -6467,8 +6454,6 @@ dependencies = [
|
||||
"uv-distribution-filename",
|
||||
"uv-pypi-types",
|
||||
"uv-static",
|
||||
"uv-warnings",
|
||||
"xz2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8118,15 +8103,6 @@ version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9"
|
||||
|
||||
[[package]]
|
||||
name = "xz2"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2"
|
||||
dependencies = [
|
||||
"lzma-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yansi"
|
||||
version = "1.0.1"
|
||||
|
||||
@@ -104,7 +104,6 @@ async-channel = { version = "2.3.1" }
|
||||
async-compression = { version = "0.4.12", features = [
|
||||
"bzip2",
|
||||
"gzip",
|
||||
"xz",
|
||||
"zstd",
|
||||
] }
|
||||
async-trait = { version = "0.1.82" }
|
||||
@@ -312,7 +311,6 @@ windows = { version = "0.61.0", features = [
|
||||
windows-registry = { version = "0.5.0" }
|
||||
windows-version = { version = "0.1.6" }
|
||||
wiremock = { version = "0.6.4" }
|
||||
xz2 = { version = "0.1.7", features = ["static"] }
|
||||
zeroize = { version = "1.8.1" }
|
||||
|
||||
# dev-dependencies
|
||||
|
||||
@@ -56,5 +56,5 @@ tempfile = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
tokio-util = { workspace = true }
|
||||
|
||||
[features]
|
||||
static = ["uv-extract/static"]
|
||||
[package.metadata.cargo-shear]
|
||||
ignored = ["uv-extract"]
|
||||
|
||||
@@ -21,6 +21,7 @@ use tokio::io::{AsyncRead, ReadBuf};
|
||||
use tokio_util::compat::FuturesAsyncReadCompatExt;
|
||||
use url::Url;
|
||||
use uv_client::retryable_on_request_failure;
|
||||
use uv_distribution_filename::LegacySourceDistExtension;
|
||||
use uv_distribution_filename::SourceDistExtension;
|
||||
use uv_static::{astral_mirror_base_url, astral_mirror_url_from_env, custom_astral_mirror_url};
|
||||
|
||||
@@ -230,7 +231,7 @@ impl ArchiveFormat {
|
||||
impl From<ArchiveFormat> for SourceDistExtension {
|
||||
fn from(val: ArchiveFormat) -> Self {
|
||||
match val {
|
||||
ArchiveFormat::Zip => Self::Zip,
|
||||
ArchiveFormat::Zip => Self::Legacy(LegacySourceDistExtension::Zip),
|
||||
ArchiveFormat::TarGz => Self::TarGz,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ name = "uv-dev"
|
||||
required-features = ["dev"]
|
||||
|
||||
[features]
|
||||
default = ["performance", "uv-extract/static"]
|
||||
default = ["performance"]
|
||||
# Actually build the dev CLI.
|
||||
dev = []
|
||||
performance = ["performance-memory-allocator"]
|
||||
|
||||
@@ -27,9 +27,29 @@ pub enum DistExtension {
|
||||
)]
|
||||
#[rkyv(derive(Debug))]
|
||||
pub enum SourceDistExtension {
|
||||
TarGz,
|
||||
Legacy(LegacySourceDistExtension),
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone,
|
||||
Copy,
|
||||
Debug,
|
||||
PartialEq,
|
||||
Eq,
|
||||
PartialOrd,
|
||||
Ord,
|
||||
Hash,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
rkyv::Archive,
|
||||
rkyv::Deserialize,
|
||||
rkyv::Serialize,
|
||||
)]
|
||||
#[rkyv(derive(Debug))]
|
||||
pub enum LegacySourceDistExtension {
|
||||
Tar,
|
||||
TarBz2,
|
||||
TarGz,
|
||||
TarLz,
|
||||
TarLzma,
|
||||
TarXz,
|
||||
@@ -83,20 +103,17 @@ 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),
|
||||
"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),
|
||||
"lzma" if is_tar(path.as_ref()) => Ok(Self::TarLzma),
|
||||
"zst" if is_tar(path.as_ref()) => Ok(Self::TarZst),
|
||||
"zip" => Ok(Self::Legacy(LegacySourceDistExtension::Zip)),
|
||||
"tar" => Ok(Self::Legacy(LegacySourceDistExtension::Tar)),
|
||||
"tgz" => Ok(Self::Legacy(LegacySourceDistExtension::Tgz)),
|
||||
"tbz" => Ok(Self::Legacy(LegacySourceDistExtension::Tbz)),
|
||||
"txz" => Ok(Self::Legacy(LegacySourceDistExtension::Txz)),
|
||||
"tlz" => Ok(Self::Legacy(LegacySourceDistExtension::Tlz)),
|
||||
"bz2" if is_tar(path.as_ref()) => Ok(Self::Legacy(LegacySourceDistExtension::TarBz2)),
|
||||
"xz" if is_tar(path.as_ref()) => Ok(Self::Legacy(LegacySourceDistExtension::TarXz)),
|
||||
"lz" if is_tar(path.as_ref()) => Ok(Self::Legacy(LegacySourceDistExtension::TarLz)),
|
||||
"lzma" if is_tar(path.as_ref()) => Ok(Self::Legacy(LegacySourceDistExtension::TarLzma)),
|
||||
"zst" if is_tar(path.as_ref()) => Ok(Self::Legacy(LegacySourceDistExtension::TarZst)),
|
||||
_ => Err(ExtensionError::SourceDist),
|
||||
}
|
||||
}
|
||||
@@ -104,20 +121,31 @@ impl SourceDistExtension {
|
||||
/// Return the name for the extension.
|
||||
pub(crate) fn name(self) -> &'static str {
|
||||
match self {
|
||||
Self::Tar => "tar",
|
||||
Self::TarBz2 => "tar.bz2",
|
||||
Self::TarGz => "tar.gz",
|
||||
Self::TarLz => "tar.lz",
|
||||
Self::TarLzma => "tar.lzma",
|
||||
Self::TarXz => "tar.xz",
|
||||
Self::TarZst => "tar.zst",
|
||||
Self::Tbz => "tbz",
|
||||
Self::Tgz => "tgz",
|
||||
Self::Tlz => "tlz",
|
||||
Self::Txz => "txz",
|
||||
Self::Zip => "zip",
|
||||
Self::Legacy(LegacySourceDistExtension::Tar) => "tar",
|
||||
Self::Legacy(LegacySourceDistExtension::TarBz2) => "tar.bz2",
|
||||
Self::Legacy(LegacySourceDistExtension::TarLz) => "tar.lz",
|
||||
Self::Legacy(LegacySourceDistExtension::TarLzma) => "tar.lzma",
|
||||
Self::Legacy(LegacySourceDistExtension::TarXz) => "tar.xz",
|
||||
Self::Legacy(LegacySourceDistExtension::TarZst) => "tar.zst",
|
||||
Self::Legacy(LegacySourceDistExtension::Tbz) => "tbz",
|
||||
Self::Legacy(LegacySourceDistExtension::Tgz) => "tgz",
|
||||
Self::Legacy(LegacySourceDistExtension::Tlz) => "tlz",
|
||||
Self::Legacy(LegacySourceDistExtension::Txz) => "txz",
|
||||
Self::Legacy(LegacySourceDistExtension::Zip) => "zip",
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if the extension conforms to [PEP 625](https://peps.python.org/pep-0625/)'s
|
||||
/// naming requirements.
|
||||
///
|
||||
/// PEP 625 mandates `.tar.gz`; `.zip` is also accepted for backwards compatibility.
|
||||
pub fn is_pep625_compliant(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Self::TarGz | Self::Legacy(LegacySourceDistExtension::Zip)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for SourceDistExtension {
|
||||
|
||||
@@ -7,7 +7,9 @@ use uv_pep440::Version;
|
||||
pub use build_tag::{BuildTag, BuildTagError};
|
||||
pub use egg::{EggInfoFilename, EggInfoFilenameError};
|
||||
pub use expanded_tags::{ExpandedTagError, ExpandedTags};
|
||||
pub use extension::{DistExtension, ExtensionError, SourceDistExtension};
|
||||
pub use extension::{
|
||||
DistExtension, ExtensionError, LegacySourceDistExtension, SourceDistExtension,
|
||||
};
|
||||
pub use source_dist::{SourceDistFilename, SourceDistFilenameError};
|
||||
pub use wheel::{WheelFilename, WheelFilenameError};
|
||||
|
||||
|
||||
@@ -199,7 +199,7 @@ mod tests {
|
||||
|
||||
use uv_normalize::PackageName;
|
||||
|
||||
use crate::{SourceDistExtension, SourceDistFilename};
|
||||
use crate::{SourceDistExtension, SourceDistFilename, extension::LegacySourceDistExtension};
|
||||
|
||||
/// Only test already normalized names since the parsing is lossy
|
||||
///
|
||||
@@ -281,7 +281,7 @@ mod tests {
|
||||
assert!(
|
||||
SourceDistFilename::parse(
|
||||
"foo.zip",
|
||||
SourceDistExtension::Zip,
|
||||
SourceDistExtension::Legacy(LegacySourceDistExtension::Zip),
|
||||
&PackageName::from_str("foo-lib").unwrap()
|
||||
)
|
||||
.is_err()
|
||||
|
||||
@@ -24,4 +24,9 @@ pub enum Error {
|
||||
|
||||
#[error("Requested package name `{0}` does not match `{1}` in the distribution filename: {2}")]
|
||||
PackageNameMismatch(PackageName, PackageName, String),
|
||||
|
||||
#[error(
|
||||
"Source distribution `{0}` has a non-PEP 625-compliant filename; only `.tar.gz` and `.zip` archives are accepted"
|
||||
)]
|
||||
NotPep625Filename(String),
|
||||
}
|
||||
|
||||
@@ -409,6 +409,9 @@ impl Dist {
|
||||
})))
|
||||
}
|
||||
DistExtension::Source(ext) => {
|
||||
if !ext.is_pep625_compliant() {
|
||||
return Err(Error::NotPep625Filename(url.verbatim().to_string()));
|
||||
}
|
||||
Ok(Self::Source(SourceDist::DirectUrl(DirectUrlSourceDist {
|
||||
name,
|
||||
location: Box::new(location),
|
||||
@@ -461,6 +464,10 @@ impl Dist {
|
||||
})))
|
||||
}
|
||||
DistExtension::Source(ext) => {
|
||||
if !ext.is_pep625_compliant() {
|
||||
return Err(Error::NotPep625Filename(url.verbatim().to_string()));
|
||||
}
|
||||
|
||||
// If there is a version in the filename, record it.
|
||||
let version = url
|
||||
.filename()
|
||||
|
||||
@@ -144,6 +144,7 @@ impl IncompatibleDist {
|
||||
IncompatibleSource::RequiresPython(..) => {
|
||||
format!("requires {self}")
|
||||
}
|
||||
IncompatibleSource::NotPep625Filename => format!("has {self}"),
|
||||
},
|
||||
Self::Unavailable => format!("has {self}"),
|
||||
}
|
||||
@@ -172,6 +173,7 @@ impl IncompatibleDist {
|
||||
IncompatibleSource::RequiresPython(..) => {
|
||||
format!("require {self}")
|
||||
}
|
||||
IncompatibleSource::NotPep625Filename => format!("have {self}"),
|
||||
},
|
||||
Self::Unavailable => format!("have {self}"),
|
||||
}
|
||||
@@ -277,6 +279,9 @@ impl Display for IncompatibleDist {
|
||||
IncompatibleSource::RequiresPython(python, _) => {
|
||||
write!(f, "Python {python}")
|
||||
}
|
||||
IncompatibleSource::NotPep625Filename => {
|
||||
f.write_str("a non-PEP 625-compliant source distribution filename")
|
||||
}
|
||||
},
|
||||
Self::Unavailable => f.write_str("no available distributions"),
|
||||
}
|
||||
@@ -327,6 +332,8 @@ pub enum IncompatibleSource {
|
||||
RequiresPython(VersionSpecifiers, PythonRequirementKind),
|
||||
Yanked(Yanked),
|
||||
NoBuild,
|
||||
/// The source distribution's filename does not confirm to PEP 625.
|
||||
NotPep625Filename,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
@@ -737,21 +744,26 @@ impl IncompatibleSource {
|
||||
Self::ExcludeNewer(timestamp_self) => match other {
|
||||
// Smaller timestamps are closer to the cut-off time
|
||||
Self::ExcludeNewer(timestamp_other) => timestamp_other < timestamp_self,
|
||||
Self::NoBuild | Self::RequiresPython(_, _) | Self::Yanked(_) => true,
|
||||
Self::NoBuild
|
||||
| Self::RequiresPython(_, _)
|
||||
| Self::Yanked(_)
|
||||
| Self::NotPep625Filename => true,
|
||||
},
|
||||
Self::RequiresPython(_, _) => match other {
|
||||
Self::ExcludeNewer(_) => false,
|
||||
// Version specifiers cannot be reasonably compared
|
||||
Self::RequiresPython(_, _) => false,
|
||||
Self::NoBuild | Self::Yanked(_) => true,
|
||||
Self::NoBuild | Self::Yanked(_) | Self::NotPep625Filename => true,
|
||||
},
|
||||
Self::Yanked(_) => match other {
|
||||
Self::ExcludeNewer(_) | Self::RequiresPython(_, _) => false,
|
||||
Self::ExcludeNewer(_) | Self::RequiresPython(_, _) | Self::NotPep625Filename => {
|
||||
false
|
||||
}
|
||||
// Yanks with a reason are more helpful for errors
|
||||
Self::Yanked(yanked_other) => matches!(yanked_other, Yanked::Reason(_)),
|
||||
Self::NoBuild => true,
|
||||
},
|
||||
Self::NoBuild => false,
|
||||
Self::NoBuild | Self::NotPep625Filename => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,6 @@ uv-pypi-types = { workspace = true }
|
||||
uv-python = { workspace = true }
|
||||
uv-redacted = { workspace = true }
|
||||
uv-types = { workspace = true }
|
||||
uv-warnings = { workspace = true }
|
||||
uv-workspace = { workspace = true }
|
||||
|
||||
anyhow = { workspace = true }
|
||||
@@ -68,4 +67,3 @@ insta = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
static = ["uv-extract/static"]
|
||||
|
||||
@@ -17,7 +17,7 @@ use uv_cache_info::{CacheInfo, Timestamp};
|
||||
use uv_client::{
|
||||
CacheControl, CachedClientError, Connectivity, DataWithCachePolicy, RegistryClient,
|
||||
};
|
||||
use uv_distribution_filename::{SourceDistExtension, WheelFilename};
|
||||
use uv_distribution_filename::WheelFilename;
|
||||
use uv_distribution_types::{
|
||||
BuildInfo, BuildableSource, BuiltDist, Dist, DistRef, File, HashPolicy, Hashed, IndexUrl,
|
||||
InstalledDist, Name, SourceDist, ToUrlError,
|
||||
@@ -31,7 +31,6 @@ use uv_pypi_types::{HashDigest, HashDigests, PyProjectToml};
|
||||
use uv_python::PythonVariant;
|
||||
use uv_redacted::DisplaySafeUrl;
|
||||
use uv_types::{BuildContext, BuildStack};
|
||||
use uv_warnings::warn_user_once;
|
||||
|
||||
use crate::archive::Archive;
|
||||
use crate::error::PythonVersion;
|
||||
@@ -437,34 +436,6 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> {
|
||||
tags: &Tags,
|
||||
hashes: HashPolicy<'_>,
|
||||
) -> Result<LocalWheel, Error> {
|
||||
// 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)
|
||||
|
||||
@@ -22,7 +22,7 @@ uv-pypi-types = { workspace = true }
|
||||
uv-static = { workspace = true }
|
||||
|
||||
astral-tokio-tar = { workspace = true }
|
||||
async-compression = { workspace = true, features = ["bzip2", "gzip", "zstd", "xz"] }
|
||||
async-compression = { workspace = true, features = ["bzip2", "gzip", "zstd"] }
|
||||
async_zip = { workspace = true }
|
||||
blake2 = { workspace = true }
|
||||
flate2 = { workspace = true }
|
||||
@@ -38,15 +38,12 @@ thiserror = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
tokio-util = { workspace = true, features = ["compat"] }
|
||||
tracing = { workspace = true }
|
||||
xz2 = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
# Avoid a liblzma.so dependency
|
||||
static = ["xz2/static"]
|
||||
|
||||
[package.metadata.cargo-shear]
|
||||
# NOTE: `async-compression` owns the actual Deflate codec, but we need to
|
||||
# explicitly declare `flate2` here so the workspace's `zlib-rs` backend stays
|
||||
# selected after removing the synchronous `zip` dependency.
|
||||
ignored = ["flate2", "xz2"]
|
||||
ignored = ["flate2"]
|
||||
|
||||
@@ -8,7 +8,7 @@ use rustc_hash::{FxHashMap, FxHashSet};
|
||||
use tokio_util::compat::{FuturesAsyncReadCompatExt, TokioAsyncReadCompatExt};
|
||||
use tracing::{debug, warn};
|
||||
|
||||
use uv_distribution_filename::SourceDistExtension;
|
||||
use uv_distribution_filename::{LegacySourceDistExtension, SourceDistExtension};
|
||||
|
||||
use crate::{Error, insecure_no_validate, validate_archive_member_name};
|
||||
|
||||
@@ -713,30 +713,6 @@ pub async fn untar_zst<R: tokio::io::AsyncRead + Unpin>(
|
||||
.map_err(Error::io_or_compression)
|
||||
}
|
||||
|
||||
/// Unpack a `.tar.xz` archive into the target directory, without requiring `Seek`.
|
||||
///
|
||||
/// This is useful for unpacking files as they're being downloaded.
|
||||
///
|
||||
/// Returns the list of unpacked files and their sizes.
|
||||
async fn untar_xz<R: tokio::io::AsyncRead + Unpin>(
|
||||
reader: R,
|
||||
target: impl AsRef<Path>,
|
||||
) -> Result<Vec<(PathBuf, u64)>, Error> {
|
||||
let reader = tokio::io::BufReader::with_capacity(DEFAULT_BUF_SIZE, reader);
|
||||
let mut decompressed_bytes = async_compression::tokio::bufread::XzDecoder::new(reader);
|
||||
|
||||
let archive = tokio_tar::ArchiveBuilder::new(
|
||||
&mut decompressed_bytes as &mut (dyn tokio::io::AsyncRead + Unpin),
|
||||
)
|
||||
.set_preserve_mtime(false)
|
||||
.set_preserve_permissions(false)
|
||||
.set_allow_external_symlinks(false)
|
||||
.build();
|
||||
untar_in(archive, target.as_ref())
|
||||
.await
|
||||
.map_err(Error::io_or_compression)
|
||||
}
|
||||
|
||||
/// Unpack a `.tar` archive into the target directory, without requiring `Seek`.
|
||||
///
|
||||
/// This is useful for unpacking files as they're being downloaded.
|
||||
@@ -759,7 +735,7 @@ async fn untar<R: tokio::io::AsyncRead + Unpin>(
|
||||
.map_err(Error::io_or_compression)
|
||||
}
|
||||
|
||||
/// Unpack a `.zip`, `.tar.gz`, `.tar.bz2`, `.tar.zst`, or `.tar.xz` archive into the target directory,
|
||||
/// Unpack a `.zip`, `.tar.gz`, `.tar.bz2`, or `.tar.zst` archive into the target directory,
|
||||
/// without requiring `Seek`.
|
||||
///
|
||||
/// Returns the list of unpacked files and their sizes.
|
||||
@@ -769,15 +745,16 @@ pub async fn archive<R: tokio::io::AsyncRead + Unpin>(
|
||||
target: impl AsRef<Path>,
|
||||
) -> Result<Vec<(PathBuf, u64)>, Error> {
|
||||
match ext {
|
||||
SourceDistExtension::Zip => unzip(reader, target).await,
|
||||
SourceDistExtension::Tar => untar(reader, target).await,
|
||||
SourceDistExtension::Tgz | SourceDistExtension::TarGz => untar_gz(reader, target).await,
|
||||
SourceDistExtension::Tbz | SourceDistExtension::TarBz2 => untar_bz2(reader, target).await,
|
||||
SourceDistExtension::Txz
|
||||
| SourceDistExtension::TarXz
|
||||
| SourceDistExtension::Tlz
|
||||
| SourceDistExtension::TarLz
|
||||
| SourceDistExtension::TarLzma => untar_xz(reader, target).await,
|
||||
SourceDistExtension::TarZst => untar_zst(reader, target).await,
|
||||
SourceDistExtension::Legacy(LegacySourceDistExtension::Zip) => unzip(reader, target).await,
|
||||
SourceDistExtension::Legacy(LegacySourceDistExtension::Tar) => untar(reader, target).await,
|
||||
SourceDistExtension::Legacy(LegacySourceDistExtension::Tgz)
|
||||
| SourceDistExtension::TarGz => untar_gz(reader, target).await,
|
||||
SourceDistExtension::Legacy(
|
||||
LegacySourceDistExtension::Tbz | LegacySourceDistExtension::TarBz2,
|
||||
) => untar_bz2(reader, target).await,
|
||||
SourceDistExtension::Legacy(LegacySourceDistExtension::TarZst) => {
|
||||
untar_zst(reader, target).await
|
||||
}
|
||||
SourceDistExtension::Legacy(_) => Err(Error::UnsupportedCompression),
|
||||
}
|
||||
}
|
||||
|
||||
+56
-41
@@ -150,56 +150,69 @@ impl<T: Pep508Url> Requirement<T> {
|
||||
|
||||
/// Returns a [`Display`] implementation that doesn't mask credentials.
|
||||
pub fn displayable_with_credentials(&self) -> impl Display {
|
||||
std::fmt::from_fn(|f| fmt_requirement(self, f, true))
|
||||
RequirementDisplay {
|
||||
requirement: self,
|
||||
display_credentials: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Pep508Url + Display> Display for Requirement<T> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
fmt_requirement(self, f, false)
|
||||
RequirementDisplay {
|
||||
requirement: self,
|
||||
display_credentials: false,
|
||||
}
|
||||
.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
fn fmt_requirement<T: Pep508Url + Display>(
|
||||
requirement: &Requirement<T>,
|
||||
f: &mut Formatter<'_>,
|
||||
struct RequirementDisplay<'a, T>
|
||||
where
|
||||
T: Pep508Url + Display,
|
||||
{
|
||||
requirement: &'a Requirement<T>,
|
||||
display_credentials: bool,
|
||||
) -> std::fmt::Result {
|
||||
write!(f, "{}", requirement.name)?;
|
||||
if !requirement.extras.is_empty() {
|
||||
write!(
|
||||
f,
|
||||
"[{}]",
|
||||
requirement
|
||||
.extras
|
||||
.iter()
|
||||
.map(ToString::to_string)
|
||||
.collect::<Vec<_>>()
|
||||
.join(",")
|
||||
)?;
|
||||
}
|
||||
if let Some(version_or_url) = &requirement.version_or_url {
|
||||
match version_or_url {
|
||||
VersionOrUrl::VersionSpecifier(version_specifier) => {
|
||||
let version_specifier: Vec<String> =
|
||||
version_specifier.iter().map(ToString::to_string).collect();
|
||||
write!(f, "{}", version_specifier.join(","))?;
|
||||
}
|
||||
VersionOrUrl::Url(url) => {
|
||||
let url_string = if display_credentials {
|
||||
url.displayable_with_credentials().to_string()
|
||||
} else {
|
||||
url.to_string()
|
||||
};
|
||||
// We add the space for markers later if necessary
|
||||
write!(f, " @ {url_string}")?;
|
||||
}
|
||||
|
||||
impl<T: Pep508Url + Display> Display for RequirementDisplay<'_, T> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.requirement.name)?;
|
||||
if !self.requirement.extras.is_empty() {
|
||||
write!(
|
||||
f,
|
||||
"[{}]",
|
||||
self.requirement
|
||||
.extras
|
||||
.iter()
|
||||
.map(ToString::to_string)
|
||||
.collect::<Vec<_>>()
|
||||
.join(",")
|
||||
)?;
|
||||
}
|
||||
if let Some(version_or_url) = &self.requirement.version_or_url {
|
||||
match version_or_url {
|
||||
VersionOrUrl::VersionSpecifier(version_specifier) => {
|
||||
let version_specifier: Vec<String> =
|
||||
version_specifier.iter().map(ToString::to_string).collect();
|
||||
write!(f, "{}", version_specifier.join(","))?;
|
||||
}
|
||||
VersionOrUrl::Url(url) => {
|
||||
let url_string = if self.display_credentials {
|
||||
url.displayable_with_credentials().to_string()
|
||||
} else {
|
||||
url.to_string()
|
||||
};
|
||||
// We add the space for markers later if necessary
|
||||
write!(f, " @ {url_string}")?;
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(marker) = self.requirement.marker.contents() {
|
||||
write!(f, " ; {marker}")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
if let Some(marker) = requirement.marker.contents() {
|
||||
write!(f, " ; {marker}")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// <https://github.com/serde-rs/serde/issues/908#issuecomment-298027413>
|
||||
@@ -554,7 +567,10 @@ fn looks_like_unnamed_requirement(cursor: &mut Cursor) -> bool {
|
||||
/// Returns `true` if a file looks like an archive.
|
||||
///
|
||||
/// See <https://github.com/pypa/pip/blob/111eed14b6e9fba7c78a5ec2b7594812d17b5d2b/src/pip/_internal/utils/filetypes.py#L8>
|
||||
/// for the list of supported archive extensions.
|
||||
/// for the original list of supported archive extensions.
|
||||
///
|
||||
/// NOTE: Unlike pip, we don't allow xz or lzma archives here.
|
||||
/// Longer term, we will also probably disallow bzip2 archives.
|
||||
fn looks_like_archive(file: impl AsRef<Path>) -> bool {
|
||||
let file = file.as_ref();
|
||||
|
||||
@@ -570,8 +586,7 @@ fn looks_like_archive(file: impl AsRef<Path>) -> bool {
|
||||
|
||||
matches!(
|
||||
(pre_extension, extension),
|
||||
(_, "whl" | "tbz" | "txz" | "tlz" | "zip" | "tgz" | "tar")
|
||||
| (Some("tar"), "bz2" | "xz" | "lz" | "lzma" | "gz")
|
||||
(_, "whl" | "tbz" | "tlz" | "zip" | "tgz" | "tar") | (Some("tar"), "bz2" | "gz")
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -164,6 +164,14 @@ impl FlatDistributions {
|
||||
return SourceDistCompatibility::Incompatible(IncompatibleSource::NoBuild);
|
||||
}
|
||||
|
||||
// Check if the filename is PEP 625-compliant.
|
||||
// TODO: Strengthen this check more; right now we allow `.zip`
|
||||
// (which is not compliant) and we don't strictly
|
||||
// enforce the formatting rules for the name or version.
|
||||
if !filename.extension.is_pep625_compliant() {
|
||||
return SourceDistCompatibility::Incompatible(IncompatibleSource::NotPep625Filename);
|
||||
}
|
||||
|
||||
// Check if hashes line up
|
||||
let hash_policy = hasher.get_package(&filename.name, &filename.version);
|
||||
let hash = if hash_policy.requires_validation() {
|
||||
|
||||
@@ -3161,6 +3161,12 @@ impl Package {
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
if !ext.is_pep625_compliant() {
|
||||
return Err(LockErrorKind::NotPep625Filename {
|
||||
id: self.id.clone(),
|
||||
}
|
||||
.into());
|
||||
}
|
||||
let install_path = absolute_path(workspace_root, path)?;
|
||||
let given = path.to_str().expect("lock file paths must be UTF-8");
|
||||
let path_dist = PathSourceDist {
|
||||
@@ -3276,6 +3282,12 @@ impl Package {
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
if !ext.is_pep625_compliant() {
|
||||
return Err(LockErrorKind::NotPep625Filename {
|
||||
id: self.id.clone(),
|
||||
}
|
||||
.into());
|
||||
}
|
||||
let location = url.to_url().map_err(LockErrorKind::InvalidUrl)?;
|
||||
let url = DisplaySafeUrl::from(ParsedArchiveUrl {
|
||||
url: location.clone(),
|
||||
@@ -5957,6 +5969,12 @@ impl LockError {
|
||||
LockErrorKind::NoBuild { .. } | LockErrorKind::NoBinaryNoBuild { .. }
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns true if the [`LockError`] indicates that the lockfile references a
|
||||
/// non-PEP 625-compliant source distribution.
|
||||
pub fn is_not_pep625(&self) -> bool {
|
||||
matches!(&*self.kind, LockErrorKind::NotPep625Filename { .. })
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> From<E> for LockError
|
||||
@@ -6375,6 +6393,16 @@ enum LockErrorKind {
|
||||
/// The list of valid extensions that were expected.
|
||||
err: ExtensionError,
|
||||
},
|
||||
/// An error that occurs when a locked source distribution has a
|
||||
/// non-PEP 625-compliant filename (e.g., `.tar.bz2`).
|
||||
#[error(
|
||||
"Source distribution for `{id}` has a non-PEP 625-compliant filename; only `.tar.gz` and `.zip` archives are accepted",
|
||||
id = id.cyan()
|
||||
)]
|
||||
NotPep625Filename {
|
||||
/// The ID of the package whose source distribution has a non-PEP 625-compliant filename.
|
||||
id: PackageId,
|
||||
},
|
||||
/// Failed to parse a Git source URL.
|
||||
#[error("Failed to parse Git URL")]
|
||||
InvalidGitSourceUrl(
|
||||
|
||||
@@ -10,7 +10,7 @@ use tracing::{instrument, trace};
|
||||
|
||||
use uv_client::{FlatIndexEntry, OwnedArchive, SimpleDetailMetadata, VersionFiles};
|
||||
use uv_configuration::BuildOptions;
|
||||
use uv_distribution_filename::{DistFilename, WheelFilename};
|
||||
use uv_distribution_filename::{DistFilename, SourceDistFilename, WheelFilename};
|
||||
use uv_distribution_types::{
|
||||
HashComparison, IncompatibleSource, IncompatibleWheel, IndexUrl, PrioritizedDist,
|
||||
RegistryBuiltWheel, RegistrySourceDist, RequiresPython, SourceDistCompatibility,
|
||||
@@ -521,8 +521,7 @@ impl VersionMapLazy {
|
||||
}
|
||||
DistFilename::SourceDistFilename(filename) => {
|
||||
let compatibility = self.source_dist_compatibility(
|
||||
&filename.name,
|
||||
&filename.version,
|
||||
&filename,
|
||||
hashes.as_slice(),
|
||||
yanked,
|
||||
excluded,
|
||||
@@ -551,8 +550,7 @@ impl VersionMapLazy {
|
||||
|
||||
fn source_dist_compatibility(
|
||||
&self,
|
||||
name: &PackageName,
|
||||
version: &Version,
|
||||
filename: &SourceDistFilename,
|
||||
hashes: &[HashDigest],
|
||||
yanked: Option<&Yanked>,
|
||||
excluded: bool,
|
||||
@@ -572,15 +570,27 @@ impl VersionMapLazy {
|
||||
|
||||
// Check if yanked
|
||||
if let Some(yanked) = yanked {
|
||||
if yanked.is_yanked() && !self.allowed_yanks.contains(name, version) {
|
||||
if yanked.is_yanked()
|
||||
&& !self
|
||||
.allowed_yanks
|
||||
.contains(&filename.name, &filename.version)
|
||||
{
|
||||
return SourceDistCompatibility::Incompatible(IncompatibleSource::Yanked(
|
||||
yanked.clone(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the filename is PEP 625-compliant.
|
||||
// TODO: Strengthen this check more; right now we allow `.zip`
|
||||
// (which is not compliant) and we don't strictly
|
||||
// enforce the formatting rules for the name or version.
|
||||
if !filename.extension.is_pep625_compliant() {
|
||||
return SourceDistCompatibility::Incompatible(IncompatibleSource::NotPep625Filename);
|
||||
}
|
||||
|
||||
// Check if hashes line up. If hashes aren't required, they're considered matching.
|
||||
let hash_policy = self.hasher.get_package(name, version);
|
||||
let hash_policy = self.hasher.get_package(&filename.name, &filename.version);
|
||||
let required_hashes = hash_policy.digests();
|
||||
let hash = if required_hashes.is_empty() {
|
||||
HashComparison::Matched
|
||||
|
||||
@@ -157,7 +157,7 @@ nix = { workspace = true }
|
||||
uv-unix = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = ["performance", "uv-distribution/static", "test-defaults"]
|
||||
default = ["performance", "test-defaults"]
|
||||
native-auth = []
|
||||
# Use better memory allocators, etc.
|
||||
performance = ["performance-memory-allocator"]
|
||||
|
||||
@@ -856,6 +856,11 @@ async fn do_lock(
|
||||
// metadata that cannot be obtained under `--no-build`.
|
||||
return Err(ProjectError::Lock(err));
|
||||
}
|
||||
Err(ProjectError::Lock(err)) if err.is_not_pep625() => {
|
||||
// A non-PEP 625-compliant sdist in the lockfile will also be rejected by a fresh
|
||||
// resolve, so short-circuit rather than doing the extra work.
|
||||
return Err(ProjectError::Lock(err));
|
||||
}
|
||||
Err(err) => {
|
||||
warn_user!("Failed to validate existing lockfile: {err}");
|
||||
None
|
||||
|
||||
@@ -860,9 +860,10 @@ fn install_sdist_url() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Install a package with source archive format `.tar.bz2`.
|
||||
/// Attempt to install a direct URL source distribution with a non-PEP 625-compliant
|
||||
/// archive format (e.g., `.tar.bz2`). This should hard-error.
|
||||
#[test]
|
||||
fn install_sdist_archive_type_bz2() -> Result<()> {
|
||||
fn reject_sdist_archive_type_bz2() -> Result<()> {
|
||||
let context = uv_test::test_context!("3.9");
|
||||
|
||||
let requirements_txt = context.temp_dir.child("requirements.txt");
|
||||
@@ -876,17 +877,13 @@ fn install_sdist_archive_type_bz2() -> Result<()> {
|
||||
|
||||
uv_snapshot!(context.filters(), context.pip_sync()
|
||||
.arg("requirements.txt")
|
||||
.arg("--strict"), @"
|
||||
success: true
|
||||
exit_code: 0
|
||||
.arg("--strict"), @r"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- 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)
|
||||
error: Source distribution `[WORKSPACE]/test/links/bz2-1.0.0.tar.bz2` has a non-PEP 625-compliant filename; only `.tar.gz` and `.zip` archives are accepted
|
||||
"
|
||||
);
|
||||
|
||||
|
||||
@@ -16229,6 +16229,247 @@ async fn sync_zstd_wheel() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sync with an index whose only source distribution has a non-PEP 625-compliant
|
||||
/// extension (e.g., `.tar.bz2`). The resolver should reject it as incompatible.
|
||||
#[tokio::test]
|
||||
async fn sync_non_pep625_sdist() -> Result<()> {
|
||||
use serde_json::json;
|
||||
use wiremock::{
|
||||
Mock, MockServer, ResponseTemplate,
|
||||
matchers::{method, path},
|
||||
};
|
||||
|
||||
let context = uv_test::test_context!("3.13");
|
||||
let server = MockServer::start().await;
|
||||
|
||||
let sdist_url = format!("{}/files/basic_package-0.1.0.tar.bz2", server.uri());
|
||||
|
||||
let simple_index = json!({
|
||||
"meta": {
|
||||
"api-version": "1.1"
|
||||
},
|
||||
"name": "basic-package",
|
||||
"files": [{
|
||||
"filename": "basic_package-0.1.0.tar.bz2",
|
||||
"url": sdist_url,
|
||||
"hashes": {
|
||||
"sha256": "0000000000000000000000000000000000000000000000000000000000000000"
|
||||
}
|
||||
}]
|
||||
});
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/simple/basic-package/"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_raw(
|
||||
simple_index.to_string().into_bytes(),
|
||||
"application/vnd.pyx.simple.v1+json",
|
||||
))
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
pyproject_toml.write_str(&formatdoc! { r#"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = ["basic-package"]
|
||||
|
||||
[tool.uv.sources]
|
||||
basic-package = {{ index = "test-registry" }}
|
||||
|
||||
[[tool.uv.index]]
|
||||
name = "test-registry"
|
||||
url = "{}/simple"
|
||||
"#,
|
||||
server.uri()
|
||||
})?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.sync().env_remove(EnvVars::UV_EXCLUDE_NEWER), @"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
× No solution found when resolving dependencies:
|
||||
╰─▶ Because only basic-package==0.1.0 is available and basic-package==0.1.0 has a non-PEP 625-compliant source distribution filename, we can conclude that all versions of basic-package cannot be used.
|
||||
And because your project depends on basic-package, we can conclude that your project's requirements are unsatisfiable.
|
||||
|
||||
hint: `basic-package` was found on http://[LOCALHOST]/simple, but not at the requested version (all of:
|
||||
basic-package<0.1.0
|
||||
basic-package>0.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.
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sync with an index that serves both a non-PEP 625-compliant sdist and a
|
||||
/// compatible wheel. The resolver should use the wheel and skip the sdist
|
||||
/// without surfacing an incompatibility error.
|
||||
#[tokio::test]
|
||||
async fn sync_non_pep625_sdist_with_compatible_wheel() -> Result<()> {
|
||||
use serde_json::json;
|
||||
use wiremock::{
|
||||
Mock, MockServer, ResponseTemplate,
|
||||
matchers::{method, path},
|
||||
};
|
||||
|
||||
let context = uv_test::test_context!("3.13");
|
||||
let server = MockServer::start().await;
|
||||
|
||||
let wheel_path = context
|
||||
.temp_dir
|
||||
.child("basic_package-0.1.0-py3-none-any.whl");
|
||||
fs_err::copy(
|
||||
context
|
||||
.workspace_root
|
||||
.join("test/links/basic_package-0.1.0-py3-none-any.whl"),
|
||||
&wheel_path,
|
||||
)?;
|
||||
|
||||
let wheel_url = format!(
|
||||
"{}/files/basic_package-0.1.0-py3-none-any.whl",
|
||||
server.uri()
|
||||
);
|
||||
let sdist_url = format!("{}/files/basic_package-0.1.0.tar.bz2", server.uri());
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/files/basic_package-0.1.0-py3-none-any.whl"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_bytes(fs_err::read(&wheel_path)?))
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let simple_index = json!({
|
||||
"meta": {
|
||||
"api-version": "1.1"
|
||||
},
|
||||
"name": "basic-package",
|
||||
"files": [
|
||||
{
|
||||
"filename": "basic_package-0.1.0.tar.bz2",
|
||||
"url": sdist_url,
|
||||
"hashes": {
|
||||
"sha256": "0000000000000000000000000000000000000000000000000000000000000000"
|
||||
}
|
||||
},
|
||||
{
|
||||
"filename": "basic_package-0.1.0-py3-none-any.whl",
|
||||
"url": wheel_url,
|
||||
"hashes": {
|
||||
"sha256": "7b6229db79b5800e4e98a351b5628c1c8a944533a2d428aeeaa7275a30d4ea82"
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/simple/basic-package/"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_raw(
|
||||
simple_index.to_string().into_bytes(),
|
||||
"application/vnd.pyx.simple.v1+json",
|
||||
))
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
pyproject_toml.write_str(&formatdoc! { r#"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = ["basic-package"]
|
||||
|
||||
[tool.uv.sources]
|
||||
basic-package = {{ index = "test-registry" }}
|
||||
|
||||
[[tool.uv.index]]
|
||||
name = "test-registry"
|
||||
url = "{}/simple"
|
||||
"#,
|
||||
server.uri()
|
||||
})?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.sync().env_remove(EnvVars::UV_EXCLUDE_NEWER), @"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
WARN Range requests not supported for basic_package-0.1.0-py3-none-any.whl; streaming wheel
|
||||
Resolved 2 packages in [TIME]
|
||||
Prepared 1 package in [TIME]
|
||||
Installed 1 package in [TIME]
|
||||
+ basic-package==0.1.0
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A pre-existing lockfile may refer to a non-PEP 625-compliant direct URL sdist (e.g.
|
||||
/// one that was locked by an older uv). Refreshing it via `uv sync` should hard-error
|
||||
/// rather than silently install.
|
||||
#[test]
|
||||
fn sync_non_pep625_sdist_from_lockfile() -> Result<()> {
|
||||
let context = uv_test::test_context!("3.12");
|
||||
|
||||
// Stage the `.tar.bz2` sdist alongside the project.
|
||||
let archive = context.temp_dir.child("bz2-1.0.0.tar.bz2");
|
||||
fs_err::copy(
|
||||
context.workspace_root.join("test/links/bz2-1.0.0.tar.bz2"),
|
||||
&archive,
|
||||
)?;
|
||||
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
pyproject_toml.write_str(indoc! {r#"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = ["bz2"]
|
||||
|
||||
[tool.uv.sources]
|
||||
bz2 = { path = "bz2-1.0.0.tar.bz2" }
|
||||
"#})?;
|
||||
|
||||
// Pre-write a lockfile as if a previous uv had resolved the `.tar.bz2` direct URL.
|
||||
context.temp_dir.child("uv.lock").write_str(indoc! {r#"
|
||||
version = 1
|
||||
requires-python = ">=3.12"
|
||||
|
||||
[options]
|
||||
exclude-newer = "2024-03-25T00:00:00Z"
|
||||
|
||||
[[package]]
|
||||
name = "bz2"
|
||||
version = "1.0.0"
|
||||
source = { path = "bz2-1.0.0.tar.bz2" }
|
||||
sdist = { hash = "sha256:f792d237bf8d8f1fc0b0ea16848371cbbb06a6b8a43b0da2f50d30bfba834f7b" }
|
||||
|
||||
[[package]]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "bz2" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [{ name = "bz2", path = "bz2-1.0.0.tar.bz2" }]
|
||||
"#})?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.sync(), @r"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: Source distribution for `bz2==1.0.0 @ path+bz2-1.0.0.tar.bz2` has a non-PEP 625-compliant filename; only `.tar.gz` and `.zip` archives are accepted
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(windows))]
|
||||
fn toggle_workspace_editable() -> Result<()> {
|
||||
|
||||
@@ -806,27 +806,13 @@ exclude-newer-package = { setuptools = "30 days" }
|
||||
|
||||
[PEP 625](https://peps.python.org/pep-0625/) specifies that packages must distribute source
|
||||
distributions as gzip tarball (`.tar.gz`) archives. Prior to this specification, other archive
|
||||
formats, which need to be supported for backward compatibility, were also allowed. uv supports
|
||||
reading and extracting archives in the following formats:
|
||||
|
||||
- gzip tarball (`.tar.gz`, `.tgz`)
|
||||
- bzip2 tarball (`.tar.bz2`, `.tbz`)
|
||||
- xz tarball (`.tar.xz`, `.txz`)
|
||||
- zstd tarball (`.tar.zst`)
|
||||
- lzip tarball (`.tar.lz`)
|
||||
- lzma tarball (`.tar.lzma`)
|
||||
- zip (`.zip`)
|
||||
formats, which need to be supported for backward compatibility, were also allowed.
|
||||
|
||||
!!! 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.
|
||||
As of 0.12, uv rejects source distributions that do not confirm to
|
||||
[PEP 625]'s extension requirements with the exception of `.zip` archives,
|
||||
which are still accepted for backward compatibility.
|
||||
|
||||
## Lockfile versioning
|
||||
|
||||
|
||||
Reference in New Issue
Block a user