From 19e19d579566e5a8e4da84f5d5d4e46fee91a96c Mon Sep 17 00:00:00 2001 From: konsti Date: Tue, 2 Sep 2025 15:22:42 +0200 Subject: [PATCH] Add error trace to invalid package format (#15626) In https://github.com/astral-sh/uv/issues/11636, we're getting reports for installation flakes that report an invalid package format for what appears to be a network problem. Since we're cutting the error reporting to the first error message in the chain, we're not reporting the actual network error underneath it. This PR displays the whole error chain for invalid package format errors, so we can debug and eventually catch-and-retry https://github.com/astral-sh/uv/issues/11636. --- crates/uv-resolver/src/pubgrub/report.rs | 15 ++++--- .../uv-resolver/src/resolver/availability.rs | 44 ++++++++++++++++--- crates/uv-resolver/src/resolver/mod.rs | 3 +- crates/uv/tests/it/pip_sync.rs | 17 +++---- 4 files changed, 56 insertions(+), 23 deletions(-) diff --git a/crates/uv-resolver/src/pubgrub/report.rs b/crates/uv-resolver/src/pubgrub/report.rs index 1c7bfceb2..48b7b60e7 100644 --- a/crates/uv-resolver/src/pubgrub/report.rs +++ b/crates/uv-resolver/src/pubgrub/report.rs @@ -25,7 +25,8 @@ use crate::prerelease::AllowPrerelease; use crate::pubgrub::{PubGrubPackage, PubGrubPackageInner, PubGrubPython}; use crate::python_requirement::{PythonRequirement, PythonRequirementSource}; use crate::resolver::{ - MetadataUnavailable, UnavailablePackage, UnavailableReason, UnavailableVersion, + MetadataUnavailable, UnavailableErrorChain, UnavailablePackage, UnavailableReason, + UnavailableVersion, }; use crate::{Flexibility, InMemoryIndex, Options, ResolverEnvironment, VersionsResponse}; @@ -1022,13 +1023,13 @@ pub(crate) enum PubGrubHint { InvalidPackageMetadata { package: PackageName, // excluded from `PartialEq` and `Hash` - reason: String, + reason: UnavailableErrorChain, }, /// The structure of a package was invalid (e.g., multiple `.dist-info` directories). InvalidPackageStructure { package: PackageName, // excluded from `PartialEq` and `Hash` - reason: String, + reason: UnavailableErrorChain, }, /// Metadata for a package version could not be parsed. InvalidVersionMetadata { @@ -1344,21 +1345,21 @@ impl std::fmt::Display for PubGrubHint { Self::InvalidPackageMetadata { package, reason } => { write!( f, - "{}{} Metadata for `{}` could not be parsed:\n{}", + "{}{} Metadata for `{}` could not be parsed.\n{}", "hint".bold().cyan(), ":".bold(), package.cyan(), - textwrap::indent(reason, " ") + textwrap::indent(reason.to_string().as_str(), " ") ) } Self::InvalidPackageStructure { package, reason } => { write!( f, - "{}{} The structure of `{}` was invalid:\n{}", + "{}{} The structure of `{}` was invalid\n{}", "hint".bold().cyan(), ":".bold(), package.cyan(), - textwrap::indent(reason, " ") + textwrap::indent(reason.to_string().as_str(), " ") ) } Self::InvalidVersionMetadata { diff --git a/crates/uv-resolver/src/resolver/availability.rs b/crates/uv-resolver/src/resolver/availability.rs index 0015199be..278d31550 100644 --- a/crates/uv-resolver/src/resolver/availability.rs +++ b/crates/uv-resolver/src/resolver/availability.rs @@ -1,10 +1,13 @@ use std::fmt::{Display, Formatter}; +use std::iter; +use std::sync::Arc; -use crate::resolver::{MetadataUnavailable, VersionFork}; use uv_distribution_types::IncompatibleDist; use uv_pep440::{Version, VersionSpecifiers}; use uv_platform_tags::{AbiTag, Tags}; +use crate::resolver::{MetadataUnavailable, VersionFork}; + /// The reason why a package or a version cannot be used. #[derive(Debug, Clone, Eq, PartialEq)] pub enum UnavailableReason { @@ -119,6 +122,29 @@ impl From<&MetadataUnavailable> for UnavailableVersion { } } +/// Display the error chain for unavailable packages. +#[derive(Debug, Clone)] +pub struct UnavailableErrorChain(Arc); + +impl Display for UnavailableErrorChain { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + for source in iter::successors(Some(&self.0 as &dyn std::error::Error), |&err| err.source()) + { + writeln!(f, "Caused by: {}", source.to_string().trim())?; + } + Ok(()) + } +} + +impl PartialEq for UnavailableErrorChain { + /// Whether we can collapse two reasons into one because they would be rendered the same. + fn eq(&self, other: &Self) -> bool { + self.to_string() == other.to_string() + } +} + +impl Eq for UnavailableErrorChain {} + /// The package is unavailable and cannot be used. #[derive(Debug, Clone, Eq, PartialEq)] pub enum UnavailablePackage { @@ -129,9 +155,9 @@ pub enum UnavailablePackage { /// The package was not found in the registry. NotFound, /// The package metadata was found, but could not be parsed. - InvalidMetadata(String), + InvalidMetadata(UnavailableErrorChain), /// The package has an invalid structure. - InvalidStructure(String), + InvalidStructure(UnavailableErrorChain), } impl UnavailablePackage { @@ -166,11 +192,15 @@ impl From<&MetadataUnavailable> for UnavailablePackage { fn from(reason: &MetadataUnavailable) -> Self { match reason { MetadataUnavailable::Offline => Self::Offline, - MetadataUnavailable::InvalidMetadata(err) => Self::InvalidMetadata(err.to_string()), - MetadataUnavailable::InconsistentMetadata(err) => { - Self::InvalidMetadata(err.to_string()) + MetadataUnavailable::InvalidMetadata(err) => { + Self::InvalidMetadata(UnavailableErrorChain(err.clone())) + } + MetadataUnavailable::InconsistentMetadata(err) => { + Self::InvalidMetadata(UnavailableErrorChain(err.clone())) + } + MetadataUnavailable::InvalidStructure(err) => { + Self::InvalidStructure(UnavailableErrorChain(err.clone())) } - MetadataUnavailable::InvalidStructure(err) => Self::InvalidStructure(err.to_string()), MetadataUnavailable::RequiresPython(..) => { unreachable!("`requires-python` is only known upfront for registry distributions") } diff --git a/crates/uv-resolver/src/resolver/mod.rs b/crates/uv-resolver/src/resolver/mod.rs index 0ec707a40..c0b558dc8 100644 --- a/crates/uv-resolver/src/resolver/mod.rs +++ b/crates/uv-resolver/src/resolver/mod.rs @@ -56,7 +56,8 @@ use crate::python_requirement::PythonRequirement; use crate::resolution::ResolverOutput; use crate::resolution_mode::ResolutionStrategy; pub(crate) use crate::resolver::availability::{ - ResolverVersion, UnavailablePackage, UnavailableReason, UnavailableVersion, + ResolverVersion, UnavailableErrorChain, UnavailablePackage, UnavailableReason, + UnavailableVersion, }; use crate::resolver::batch_prefetch::BatchPrefetcher; pub use crate::resolver::derivation::DerivationChainBuilder; diff --git a/crates/uv/tests/it/pip_sync.rs b/crates/uv/tests/it/pip_sync.rs index 30cc315c1..663da1a11 100644 --- a/crates/uv/tests/it/pip_sync.rs +++ b/crates/uv/tests/it/pip_sync.rs @@ -1269,7 +1269,7 @@ fn mismatched_name() -> Result<()> { uv_snapshot!(context.filters(), context.pip_sync() .arg("requirements.txt") - .arg("--strict"), @r###" + .arg("--strict"), @r" success: false exit_code: 1 ----- stdout ----- @@ -1278,9 +1278,9 @@ fn mismatched_name() -> Result<()> { × No solution found when resolving dependencies: ╰─▶ Because foo has an invalid package format and you require foo, we can conclude that your requirements are unsatisfiable. - hint: The structure of `foo` was invalid: - The .dist-info directory tomli-2.0.1 does not start with the normalized package name: foo - "### + hint: The structure of `foo` was invalid + Caused by: The .dist-info directory tomli-2.0.1 does not start with the normalized package name: foo + " ); Ok(()) @@ -2613,7 +2613,7 @@ fn incompatible_wheel() -> Result<()> { uv_snapshot!(context.filters(), context.pip_sync() .arg("requirements.txt") - .arg("--strict"), @r###" + .arg("--strict"), @r" success: false exit_code: 1 ----- stdout ----- @@ -2622,9 +2622,10 @@ fn incompatible_wheel() -> Result<()> { × No solution found when resolving dependencies: ╰─▶ Because foo has an invalid package format and you require foo, we can conclude that your requirements are unsatisfiable. - hint: The structure of `foo` was invalid: - Failed to read from zip file - "### + hint: The structure of `foo` was invalid + Caused by: Failed to read from zip file + Caused by: unable to locate the end of central directory record + " ); Ok(())