mirror of https://github.com/astral-sh/uv
Avoid reading metadata from `.egg-info` files (#11395)
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 https://github.com/astral-sh/uv/issues/8989. Closes #10945.
This commit is contained in:
parent
4d5041dc00
commit
35564e189d
|
|
@ -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`")]
|
||||
|
|
|
|||
|
|
@ -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: <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.
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 <https://peps.python.org/pep-0345/>.
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct Metadata12 {
|
||||
pub name: PackageName,
|
||||
pub version: Version,
|
||||
pub requires_python: Option<VersionSpecifiers>,
|
||||
pub dynamic: Vec<String>,
|
||||
}
|
||||
|
||||
impl Metadata12 {
|
||||
/// Parse the [`Metadata12`] from a `.dist-info/METADATA` file, as included in a built
|
||||
/// distribution.
|
||||
pub fn parse_metadata(content: &[u8]) -> Result<Self, MetadataError> {
|
||||
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::<Vec<_>>();
|
||||
|
||||
Ok(Self {
|
||||
name,
|
||||
version,
|
||||
requires_python,
|
||||
dynamic,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
"###
|
||||
);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue