Don't read metadata from stale .egg-info files (#9760)
## Summary We were reading an `.egg-info` file from the root directory that didn't apply to the root member -- it was for another workspace member. I think this is driven from some idiosyncracies in the `setuptools` setup for that workspace member, but it's still wrong to fail. This PR adds a few measures to fix this: 1. We validate the `egg-info` filename against the package metadata. 2. We skip, rather than fail, if we see incorrect metadata in an `egg-info` file or similar. This is an optimization anyway; worst case, we try to build the package, then fail there. Closes https://github.com/astral-sh/uv/issues/9743.
This commit is contained in:
@@ -466,8 +466,6 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> {
|
||||
.boxed_local()
|
||||
.await?;
|
||||
|
||||
// Validate that the metadata is consistent with the distribution.
|
||||
|
||||
Ok(metadata)
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ use uv_client::{
|
||||
CacheControl, CachedClientError, Connectivity, DataWithCachePolicy, RegistryClient,
|
||||
};
|
||||
use uv_configuration::{BuildKind, BuildOutput, SourceStrategy};
|
||||
use uv_distribution_filename::{SourceDistExtension, WheelFilename};
|
||||
use uv_distribution_filename::{EggInfoFilename, SourceDistExtension, WheelFilename};
|
||||
use uv_distribution_types::{
|
||||
BuildableSource, DirectorySourceUrl, FileLocation, GitSourceUrl, HashPolicy, Hashed,
|
||||
PathSourceUrl, SourceDist, SourceUrl,
|
||||
@@ -1973,10 +1973,18 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
|
||||
Ok(metadata) => {
|
||||
debug!("Found static `pyproject.toml` for: {source}");
|
||||
|
||||
// Validate the metadata.
|
||||
validate_metadata(source, &metadata)?;
|
||||
|
||||
return Ok(Some(metadata));
|
||||
// Validate the metadata, but ignore it if the metadata doesn't match.
|
||||
match validate_metadata(source, &metadata) {
|
||||
Ok(()) => {
|
||||
return Ok(Some(metadata));
|
||||
}
|
||||
Err(Error::WheelMetadataNameMismatch { metadata, given }) => {
|
||||
debug!(
|
||||
"Ignoring `pyproject.toml` for: {source} (metadata: {metadata}, given: {given})"
|
||||
);
|
||||
}
|
||||
Err(err) => return Err(err),
|
||||
}
|
||||
}
|
||||
Err(
|
||||
err @ (Error::MissingPyprojectToml
|
||||
@@ -2003,10 +2011,18 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
|
||||
Ok(metadata) => {
|
||||
debug!("Found static `PKG-INFO` for: {source}");
|
||||
|
||||
// Validate the metadata.
|
||||
validate_metadata(source, &metadata)?;
|
||||
|
||||
return Ok(Some(metadata));
|
||||
// Validate the metadata, but ignore it if the metadata doesn't match.
|
||||
match validate_metadata(source, &metadata) {
|
||||
Ok(()) => {
|
||||
return Ok(Some(metadata));
|
||||
}
|
||||
Err(Error::WheelMetadataNameMismatch { metadata, given }) => {
|
||||
debug!(
|
||||
"Ignoring `PKG-INFO` for: {source} (metadata: {metadata}, given: {given})"
|
||||
);
|
||||
}
|
||||
Err(err) => return Err(err),
|
||||
}
|
||||
}
|
||||
Err(
|
||||
err @ (Error::MissingPkgInfo
|
||||
@@ -2023,14 +2039,22 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
|
||||
}
|
||||
|
||||
// Attempt to read static metadata from the `egg-info` directory.
|
||||
match read_egg_info(source_root, subdirectory).await {
|
||||
match read_egg_info(source_root, subdirectory, source.name(), source.version()).await {
|
||||
Ok(metadata) => {
|
||||
debug!("Found static `egg-info` for: {source}");
|
||||
|
||||
// Validate the metadata.
|
||||
validate_metadata(source, &metadata)?;
|
||||
|
||||
return Ok(Some(metadata));
|
||||
// Validate the metadata, but ignore it if the metadata doesn't match.
|
||||
match validate_metadata(source, &metadata) {
|
||||
Ok(()) => {
|
||||
return Ok(Some(metadata));
|
||||
}
|
||||
Err(Error::WheelMetadataNameMismatch { metadata, given }) => {
|
||||
debug!(
|
||||
"Ignoring `egg-info` for: {source} (metadata: {metadata}, given: {given})"
|
||||
);
|
||||
}
|
||||
Err(err) => return Err(err),
|
||||
}
|
||||
}
|
||||
Err(
|
||||
err @ (Error::MissingEggInfo
|
||||
@@ -2292,8 +2316,14 @@ impl LocalRevisionPointer {
|
||||
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) -> std::io::Result<Option<PathBuf>> {
|
||||
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()?;
|
||||
@@ -2303,6 +2333,27 @@ async fn read_egg_info(
|
||||
.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 {
|
||||
debug!("Skipping `{file_stem}.egg-info` due to name mismatch (expected: `{name}`)");
|
||||
if file_name.name != *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));
|
||||
}
|
||||
}
|
||||
@@ -2316,7 +2367,7 @@ async fn read_egg_info(
|
||||
};
|
||||
|
||||
// Locate the `egg-info` directory.
|
||||
let egg_info = match find_egg_info(directory.as_ref()) {
|
||||
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 => {
|
||||
|
||||
@@ -5110,6 +5110,110 @@ fn sync_derivation_chain_group() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// See: <https://github.com/astral-sh/uv/issues/9743>
|
||||
#[test]
|
||||
fn sync_stale_egg_info() -> Result<()> {
|
||||
let context = TestContext::new("3.13");
|
||||
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
pyproject_toml.write_str(
|
||||
r#"
|
||||
[project]
|
||||
name = "foo"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = [
|
||||
"member @ git+https://github.com/astral-sh/uv-stale-egg-info-test.git#subdirectory=member",
|
||||
"root @ git+https://github.com/astral-sh/uv-stale-egg-info-test.git",
|
||||
]
|
||||
"#,
|
||||
)?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.lock(), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 4 packages in [TIME]
|
||||
"###);
|
||||
|
||||
let lock = context.read("uv.lock");
|
||||
|
||||
insta::with_settings!(
|
||||
{
|
||||
filters => context.filters(),
|
||||
},
|
||||
{
|
||||
assert_snapshot!(
|
||||
lock, @r###"
|
||||
version = 1
|
||||
requires-python = ">=3.13"
|
||||
|
||||
[options]
|
||||
exclude-newer = "2024-03-25T00:00:00Z"
|
||||
|
||||
[[package]]
|
||||
name = "foo"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "member" },
|
||||
{ name = "root" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "member", git = "https://github.com/astral-sh/uv-stale-egg-info-test.git?subdirectory=member" },
|
||||
{ name = "root", git = "https://github.com/astral-sh/uv-stale-egg-info-test.git" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "member"
|
||||
version = "0.1.dev5+gfea1041"
|
||||
source = { git = "https://github.com/astral-sh/uv-stale-egg-info-test.git?subdirectory=member#fea10416b9c479ac88fb217e14e40249b63bfbee" }
|
||||
dependencies = [
|
||||
{ name = "setuptools" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "root"
|
||||
version = "0.1.dev5+gfea1041"
|
||||
source = { git = "https://github.com/astral-sh/uv-stale-egg-info-test.git#fea10416b9c479ac88fb217e14e40249b63bfbee" }
|
||||
dependencies = [
|
||||
{ name = "member" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "setuptools"
|
||||
version = "69.2.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4d/5b/dc575711b6b8f2f866131a40d053e30e962e633b332acf7cd2c24843d83d/setuptools-69.2.0.tar.gz", hash = "sha256:0ff4183f8f42cd8fa3acea16c45205521a4ef28f73c6391d8a25e92893134f2e", size = 2222950 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/92/e1/1c8bb3420105e70bdf357d57dd5567202b4ef8d27f810e98bb962d950834/setuptools-69.2.0-py3-none-any.whl", hash = "sha256:c21c49fb1042386df081cb5d86759792ab89efca84cf114889191cd09aacc80c", size = 821485 },
|
||||
]
|
||||
"###
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 4 packages in [TIME]
|
||||
Prepared 3 packages in [TIME]
|
||||
Installed 3 packages in [TIME]
|
||||
+ member==0.1.dev5+gfea1041 (from git+https://github.com/astral-sh/uv-stale-egg-info-test.git@fea10416b9c479ac88fb217e14e40249b63bfbee#subdirectory=member)
|
||||
+ root==0.1.dev5+gfea1041 (from git+https://github.com/astral-sh/uv-stale-egg-info-test.git@fea10416b9c479ac88fb217e14e40249b63bfbee)
|
||||
+ setuptools==69.2.0
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// See: <https://github.com/astral-sh/uv/issues/8887>
|
||||
#[test]
|
||||
fn sync_git_repeated_member_static_metadata() -> Result<()> {
|
||||
|
||||
Reference in New Issue
Block a user