Skip to content

Commit

Permalink
Avoid reading metadata from .egg-info files (#11395)
Browse files Browse the repository at this point in the history
## Summary

We added this to help with resolving some specific packages, and for
parity with Poetry. But in some cases, this metadata is just wrong, and
at the very least it's unreliable.

Closes #8989.

Closes #10945.
  • Loading branch information
charliermarsh authored and Gankra committed Feb 12, 2025
1 parent b3e4083 commit bb191d5
Show file tree
Hide file tree
Showing 7 changed files with 41 additions and 272 deletions.
4 changes: 0 additions & 4 deletions crates/uv-distribution/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`")]
Expand Down
194 changes: 14 additions & 180 deletions crates/uv-distribution/src/source/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,30 +10,26 @@

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;
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,
Expand All @@ -45,10 +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, RequiresTxt, ResolutionMetadata};
use uv_pypi_types::{HashAlgorithm, HashDigest, 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;
Expand Down Expand Up @@ -2456,8 +2458,8 @@ impl StaticMetadata {
Err(err) => return Err(err),
}

// 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(Self::None);
}
Expand Down Expand Up @@ -2491,41 +2493,6 @@ 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(()) => {
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(Self::None)
}
}
Expand Down Expand Up @@ -2681,139 +2648,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: <https://setuptools.pypa.io/en/latest/deprecated/python_eggs.html#dependency-metadata>
async fn read_egg_info(
source_tree: &Path,
subdirectory: Option<&Path>,
name: Option<&PackageName>,
version: Option<&Version>,
) -> Result<ResolutionMetadata, Error> {
fn find_egg_info(
source_tree: &Path,
name: Option<&PackageName>,
version: Option<&Version>,
) -> std::io::Result<Option<PathBuf>> {
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.
Expand Down
20 changes: 13 additions & 7 deletions crates/uv-install-wheel/src/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,23 @@
//! 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::{
dist_info_metadata, find_dist_info, install_data, parse_scripts, parse_wheel_file,
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
///
Expand All @@ -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 {
Expand Down
6 changes: 4 additions & 2 deletions crates/uv-pypi-types/src/metadata/metadata10.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
use crate::metadata::Headers;
use crate::MetadataError;
use serde::Deserialize;

use uv_normalize::PackageName;

use crate::metadata::Headers;
use crate::MetadataError;

/// A subset of the full core metadata specification, including only the
/// fields that have been consistent across all versions of the specification.
///
Expand Down
68 changes: 0 additions & 68 deletions crates/uv-pypi-types/src/metadata/metadata12.rs

This file was deleted.

2 changes: 0 additions & 2 deletions crates/uv-pypi-types/src/metadata/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
mod build_requires;
mod metadata10;
mod metadata12;
mod metadata23;
mod metadata_resolver;
mod pyproject_toml;
Expand All @@ -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 requires_dist::RequiresDist;
Expand Down
Loading

0 comments on commit bb191d5

Please sign in to comment.