diff --git a/crates/uv-distribution/src/error.rs b/crates/uv-distribution/src/error.rs index 0727b8896..6101f7dd3 100644 --- a/crates/uv-distribution/src/error.rs +++ b/crates/uv-distribution/src/error.rs @@ -92,10 +92,6 @@ pub enum Error { Extract(#[from] uv_extract::Error), #[error("The source distribution is missing a `PKG-INFO` file")] MissingPkgInfo, - #[error("The source distribution is missing an `egg-info` directory")] - MissingEggInfo, - #[error("The source distribution is missing a `requires.txt` file")] - MissingRequiresTxt, #[error("The source distribution `{}` has no subdirectory `{}`", _0, _1.display())] MissingSubdirectory(Url, PathBuf), #[error("Failed to extract static metadata from `PKG-INFO`")] diff --git a/crates/uv-distribution/src/source/mod.rs b/crates/uv-distribution/src/source/mod.rs index 1d865f798..c3935272f 100644 --- a/crates/uv-distribution/src/source/mod.rs +++ b/crates/uv-distribution/src/source/mod.rs @@ -10,22 +10,18 @@ use std::borrow::Cow; use std::ops::Bound; -use std::path::{Path, PathBuf}; +use std::path::Path; use std::str::FromStr; use std::sync::Arc; -use crate::distribution_database::ManagedClient; -use crate::error::Error; -use crate::metadata::{ArchiveMetadata, GitWorkspaceMember, Metadata}; -use crate::source::built_wheel_metadata::BuiltWheelMetadata; -use crate::source::revision::Revision; -use crate::{Reporter, RequiresDist}; use fs_err::tokio as fs; use futures::{FutureExt, TryStreamExt}; use reqwest::{Response, StatusCode}; use tokio_util::compat::FuturesAsyncReadCompatExt; use tracing::{debug, info_span, instrument, warn, Instrument}; use url::Url; +use zip::ZipArchive; + use uv_cache::{Cache, CacheBucket, CacheEntry, CacheShard, Removal, WheelCache}; use uv_cache_info::CacheInfo; use uv_cache_key::cache_digest; @@ -33,7 +29,7 @@ use uv_client::{ CacheControl, CachedClientError, Connectivity, DataWithCachePolicy, RegistryClient, }; use uv_configuration::{BuildKind, BuildOutput, SourceStrategy}; -use uv_distribution_filename::{EggInfoFilename, SourceDistExtension, WheelFilename}; +use uv_distribution_filename::{SourceDistExtension, WheelFilename}; use uv_distribution_types::{ BuildableSource, DirectorySourceUrl, FileLocation, GitSourceUrl, HashPolicy, Hashed, PathSourceUrl, SourceDist, SourceUrl, @@ -45,12 +41,16 @@ use uv_metadata::read_archive_metadata; use uv_normalize::PackageName; use uv_pep440::{release_specifiers_to_ranges, Version}; use uv_platform_tags::Tags; -use uv_pypi_types::{ - HashAlgorithm, HashDigest, Metadata12, PyProjectToml, RequiresTxt, ResolutionMetadata, -}; +use uv_pypi_types::{HashAlgorithm, HashDigest, PyProjectToml, ResolutionMetadata}; use uv_types::{BuildContext, BuildStack, SourceBuildTrait}; use uv_workspace::pyproject::ToolUvSources; -use zip::ZipArchive; + +use crate::distribution_database::ManagedClient; +use crate::error::Error; +use crate::metadata::{ArchiveMetadata, GitWorkspaceMember, Metadata}; +use crate::source::built_wheel_metadata::BuiltWheelMetadata; +use crate::source::revision::Revision; +use crate::{Reporter, RequiresDist}; mod built_wheel_metadata; mod revision; @@ -2481,8 +2481,8 @@ impl StaticMetadata { } } - // If the source distribution is a source tree, avoid reading `PKG-INFO` or `egg-info`, - // since they could be out-of-date. + // If the source distribution is a source tree, avoid reading `PKG-INFO`, since it could be + // out-of-date. if source.is_source_tree() { return Ok(if dynamic { Self::Dynamic } else { Self::None }); } @@ -2525,51 +2525,7 @@ impl StaticMetadata { Err(err) => return Err(err), } - // Attempt to read static metadata from the `egg-info` directory. - match read_egg_info(source_root, subdirectory, source.name(), source.version()).await { - Ok(metadata) => { - debug!("Found static `egg-info` for: {source}"); - - // Validate the metadata, but ignore it if the metadata doesn't match. - match validate_metadata(source, &metadata) { - Ok(()) => { - // If necessary, mark the metadata as dynamic. - let metadata = if dynamic { - ResolutionMetadata { - dynamic: true, - ..metadata - } - } else { - metadata - }; - return Ok(Self::Some(metadata)); - } - Err(err) => { - debug!("Ignoring `egg-info` for {source}: {err}"); - } - } - } - Err( - err @ (Error::MissingEggInfo - | Error::MissingRequiresTxt - | Error::MissingPkgInfo - | Error::RequiresTxt( - uv_pypi_types::MetadataError::Pep508Error(_) - | uv_pypi_types::MetadataError::RequiresTxtContents(_), - ) - | Error::PkgInfo( - uv_pypi_types::MetadataError::Pep508Error(_) - | uv_pypi_types::MetadataError::DynamicField(_) - | uv_pypi_types::MetadataError::FieldNotFound(_) - | uv_pypi_types::MetadataError::UnsupportedMetadataVersion(_), - )), - ) => { - debug!("No static `egg-info` available for: {source} ({err:?})"); - } - Err(err) => return Err(err), - } - - Ok(if dynamic { Self::Dynamic } else { Self::None }) + Ok(Self::None) } } @@ -2724,139 +2680,6 @@ impl LocalRevisionPointer { } } -/// Read the [`ResolutionMetadata`] by combining a source distribution's `PKG-INFO` file with a -/// `requires.txt`. -/// -/// `requires.txt` is a legacy concept from setuptools. For example, here's -/// `Flask.egg-info/requires.txt` from Flask's 1.0 release: -/// -/// ```txt -/// Werkzeug>=0.14 -/// Jinja2>=2.10 -/// itsdangerous>=0.24 -/// click>=5.1 -/// -/// [dev] -/// pytest>=3 -/// coverage -/// tox -/// sphinx -/// pallets-sphinx-themes -/// sphinxcontrib-log-cabinet -/// -/// [docs] -/// sphinx -/// pallets-sphinx-themes -/// sphinxcontrib-log-cabinet -/// -/// [dotenv] -/// python-dotenv -/// ``` -/// -/// See: -async fn read_egg_info( - source_tree: &Path, - subdirectory: Option<&Path>, - name: Option<&PackageName>, - version: Option<&Version>, -) -> Result { - fn find_egg_info( - source_tree: &Path, - name: Option<&PackageName>, - version: Option<&Version>, - ) -> std::io::Result> { - for entry in fs_err::read_dir(source_tree)? { - let entry = entry?; - let ty = entry.file_type()?; - if ty.is_dir() { - let path = entry.path(); - if path - .extension() - .is_some_and(|ext| ext.eq_ignore_ascii_case("egg-info")) - { - let Some(file_stem) = path.file_stem() else { - continue; - }; - let Some(file_stem) = file_stem.to_str() else { - continue; - }; - let Ok(file_name) = EggInfoFilename::parse(file_stem) else { - continue; - }; - if let Some(name) = name { - if file_name.name != *name { - debug!("Skipping `{file_stem}.egg-info` due to name mismatch (expected: `{name}`)"); - continue; - } - } - if let Some(version) = version { - if file_name.version.as_ref().is_some_and(|v| v != version) { - debug!("Skipping `{file_stem}.egg-info` due to version mismatch (expected: `{version}`)"); - continue; - } - } - return Ok(Some(path)); - } - } - } - Ok(None) - } - - let directory = match subdirectory { - Some(subdirectory) => Cow::Owned(source_tree.join(subdirectory)), - None => Cow::Borrowed(source_tree), - }; - - // Locate the `egg-info` directory. - let egg_info = match find_egg_info(directory.as_ref(), name, version) { - Ok(Some(path)) => path, - Ok(None) => return Err(Error::MissingEggInfo), - Err(err) if err.kind() == std::io::ErrorKind::NotFound => { - return Err(Error::MissingEggInfo) - } - Err(err) => return Err(Error::CacheRead(err)), - }; - - // Read the `requires.txt`. - let requires_txt = egg_info.join("requires.txt"); - let content = match fs::read(requires_txt).await { - Ok(content) => content, - Err(err) if err.kind() == std::io::ErrorKind::NotFound => { - return Err(Error::MissingRequiresTxt); - } - Err(err) => return Err(Error::CacheRead(err)), - }; - - // Parse the `requires.txt. - let requires_txt = RequiresTxt::parse(&content).map_err(Error::RequiresTxt)?; - - // Read the `PKG-INFO` file. - let pkg_info = egg_info.join("PKG-INFO"); - let content = match fs::read(pkg_info).await { - Ok(content) => content, - Err(err) if err.kind() == std::io::ErrorKind::NotFound => { - return Err(Error::MissingPkgInfo); - } - Err(err) => return Err(Error::CacheRead(err)), - }; - - // Parse the metadata. - let metadata = Metadata12::parse_metadata(&content).map_err(Error::PkgInfo)?; - - // Determine whether the version is dynamic. - let dynamic = metadata.dynamic.iter().any(|field| field == "version"); - - // Combine the sources. - Ok(ResolutionMetadata { - name: metadata.name, - version: metadata.version, - requires_python: metadata.requires_python, - requires_dist: requires_txt.requires_dist, - provides_extras: requires_txt.provides_extras, - dynamic, - }) -} - /// Read the [`ResolutionMetadata`] from a source distribution's `PKG-INFO` file, if it uses Metadata 2.2 /// or later _and_ none of the required fields (`Requires-Python`, `Requires-Dist`, and /// `Provides-Extra`) are marked as dynamic. diff --git a/crates/uv-install-wheel/src/install.rs b/crates/uv-install-wheel/src/install.rs index b1b8387e6..9d1bb8ea1 100644 --- a/crates/uv-install-wheel/src/install.rs +++ b/crates/uv-install-wheel/src/install.rs @@ -2,6 +2,16 @@ //! reading from a zip file. use std::path::Path; +use std::str::FromStr; + +use fs_err as fs; +use fs_err::File; +use tracing::{instrument, trace}; + +use uv_cache_info::CacheInfo; +use uv_distribution_filename::WheelFilename; +use uv_pep440::Version; +use uv_pypi_types::{DirectUrl, Metadata10}; use crate::linker::{LinkMode, Locks}; use crate::wheel::{ @@ -9,12 +19,6 @@ use crate::wheel::{ read_record_file, write_installer_metadata, write_script_entrypoints, LibKind, }; use crate::{Error, Layout}; -use fs_err as fs; -use fs_err::File; -use tracing::{instrument, trace}; -use uv_cache_info::CacheInfo; -use uv_distribution_filename::WheelFilename; -use uv_pypi_types::{DirectUrl, Metadata12}; /// Install the given wheel to the given venv /// @@ -38,9 +42,11 @@ pub fn install_wheel( ) -> Result<(), Error> { let dist_info_prefix = find_dist_info(&wheel)?; let metadata = dist_info_metadata(&dist_info_prefix, &wheel)?; - let Metadata12 { name, version, .. } = Metadata12::parse_metadata(&metadata) + let Metadata10 { name, version } = Metadata10::parse_pkg_info(&metadata) .map_err(|err| Error::InvalidWheel(err.to_string()))?; + let version = Version::from_str(&version)?; + // Validate the wheel name and version. { if name != filename.name { diff --git a/crates/uv-pypi-types/src/metadata/metadata10.rs b/crates/uv-pypi-types/src/metadata/metadata10.rs index 2e41f3ee8..98efc300f 100644 --- a/crates/uv-pypi-types/src/metadata/metadata10.rs +++ b/crates/uv-pypi-types/src/metadata/metadata10.rs @@ -1,7 +1,9 @@ +use serde::Deserialize; + +use uv_normalize::PackageName; + use crate::metadata::Headers; use crate::MetadataError; -use serde::Deserialize; -use uv_normalize::PackageName; /// A subset of the full core metadata specification, including only the /// fields that have been consistent across all versions of the specification. diff --git a/crates/uv-pypi-types/src/metadata/metadata12.rs b/crates/uv-pypi-types/src/metadata/metadata12.rs deleted file mode 100644 index ef2b2038d..000000000 --- a/crates/uv-pypi-types/src/metadata/metadata12.rs +++ /dev/null @@ -1,68 +0,0 @@ -use crate::metadata::{parse_version, Headers}; -use crate::{LenientVersionSpecifiers, MetadataError}; -use serde::Deserialize; -use std::str::FromStr; -use uv_normalize::PackageName; -use uv_pep440::{Version, VersionSpecifiers}; - -/// A subset of the full cure metadata specification, only including the -/// fields that have been consistent across all versions of the specification later than 1.2, with -/// the exception of `Dynamic`, which is optional (but introduced in Metadata 2.2). -/// -/// Python Package Metadata 1.2 is specified in . -#[derive(Deserialize, Debug, Clone)] -#[serde(rename_all = "kebab-case")] -pub struct Metadata12 { - pub name: PackageName, - pub version: Version, - pub requires_python: Option, - pub dynamic: Vec, -} - -impl Metadata12 { - /// Parse the [`Metadata12`] from a `.dist-info/METADATA` file, as included in a built - /// distribution. - pub fn parse_metadata(content: &[u8]) -> Result { - let headers = Headers::parse(content)?; - - // To rely on a source distribution's `PKG-INFO` file, the `Metadata-Version` field must be - // present and set to a value of at least `2.2`. - let metadata_version = headers - .get_first_value("Metadata-Version") - .ok_or(MetadataError::FieldNotFound("Metadata-Version"))?; - - // Parse the version into (major, minor). - let (major, minor) = parse_version(&metadata_version)?; - - // At time of writing: - // > Version of the file format; legal values are “1.0”, “1.1”, “1.2”, “2.1”, “2.2”, and “2.3”. - if (major, minor) < (1, 0) || (major, minor) >= (3, 0) { - return Err(MetadataError::InvalidMetadataVersion(metadata_version)); - } - - let name = PackageName::new( - headers - .get_first_value("Name") - .ok_or(MetadataError::FieldNotFound("Name"))?, - )?; - let version = Version::from_str( - &headers - .get_first_value("Version") - .ok_or(MetadataError::FieldNotFound("Version"))?, - ) - .map_err(MetadataError::Pep440VersionError)?; - let requires_python = headers - .get_first_value("Requires-Python") - .map(|requires_python| LenientVersionSpecifiers::from_str(&requires_python)) - .transpose()? - .map(VersionSpecifiers::from); - let dynamic = headers.get_all_values("Dynamic").collect::>(); - - Ok(Self { - name, - version, - requires_python, - dynamic, - }) - } -} diff --git a/crates/uv-pypi-types/src/metadata/mod.rs b/crates/uv-pypi-types/src/metadata/mod.rs index f6886793c..8f2141d4c 100644 --- a/crates/uv-pypi-types/src/metadata/mod.rs +++ b/crates/uv-pypi-types/src/metadata/mod.rs @@ -1,6 +1,5 @@ mod build_requires; mod metadata10; -mod metadata12; mod metadata23; mod metadata_resolver; mod pyproject_toml; @@ -20,7 +19,6 @@ use crate::VerbatimParsedUrl; pub use build_requires::BuildRequires; pub use metadata10::Metadata10; -pub use metadata12::Metadata12; pub use metadata23::Metadata23; pub use metadata_resolver::ResolutionMetadata; pub use pyproject_toml::PyProjectToml; diff --git a/crates/uv/tests/it/pip_compile.rs b/crates/uv/tests/it/pip_compile.rs index f7d62c222..53836facf 100644 --- a/crates/uv/tests/it/pip_compile.rs +++ b/crates/uv/tests/it/pip_compile.rs @@ -13824,8 +13824,8 @@ fn lowest_fork_min_python() -> Result<()> { let context = TestContext::new("3.10"); let requirements_in = context.temp_dir.child("requirements.in"); requirements_in.write_str(indoc::indoc! {r" - pycountry >= 22.1.10 - setuptools >= 50.0.0 ; python_version >= '3.12' + anyio >= 3.0.0 + idna >= 3.0.0 ; python_version >= '3.12' "})?; uv_snapshot!(context.filters(), windows_filters=false, context.pip_compile() @@ -13838,20 +13838,21 @@ fn lowest_fork_min_python() -> Result<()> { ----- stdout ----- # This file was autogenerated by uv via the following command: # uv pip compile --cache-dir [CACHE_DIR] requirements.in --universal --resolution lowest - pycountry==22.1.10 + anyio==3.0.0 # via -r requirements.in - setuptools==0.7.2 ; python_full_version < '3.12' + idna==2.8 ; python_full_version < '3.12' # via # -r requirements.in - # pycountry - setuptools==50.0.0 ; python_full_version >= '3.12' + # anyio + idna==3.0 ; python_full_version >= '3.12' # via # -r requirements.in - # pycountry + # anyio + sniffio==1.1.0 + # via anyio ----- stderr ----- - Resolved 3 packages in [TIME] - warning: The transitive dependency `setuptools` is unpinned. Consider setting a lower bound with a constraint when using `--resolution lowest` to avoid using outdated versions. + Resolved 4 packages in [TIME] "### );