Better build error messages (#9660)

Build failures are one of the most common user facing failures that
aren't "obivous" errors (such as typos) or resolver errors. Currently,
they show more technical details than being focussed on this being an
error in a subprocess that is either on the side of the package or -
more likely - in the build environment, e.g. the user needs to install a
dev package or their python version is incompatible.

The new error message clearly delineates the part that's important (this
is a build backend problem) from the internals (we called this hook) and
is consistent about which part of the dist building stage failed. We
have to calibrate the exact wording of the error message some more. Most
of the implementation is working around the orphan rule, (this)error
rules and trait rules, so it came out more of a refactoring than
intended.

Example:


![image](https://github.com/user-attachments/assets/2bc12992-db79-4362-a444-fd0d94594b77)
This commit is contained in:
konsti 2024-12-17 16:44:32 +01:00 committed by GitHub
parent b7df5dbaf3
commit ebc6d20d9d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 426 additions and 262 deletions

View file

@ -1,4 +1,6 @@
use std::fmt::{Debug, Display, Formatter};
use std::future::Future;
use std::ops::Deref;
use std::path::{Path, PathBuf};
use uv_distribution_filename::DistFilename;
@ -9,8 +11,8 @@ use uv_configuration::{
BuildKind, BuildOptions, BuildOutput, ConfigSettings, LowerBound, SourceStrategy,
};
use uv_distribution_types::{
CachedDist, DependencyMetadata, IndexCapabilities, IndexLocations, InstalledDist, Resolution,
SourceDist,
CachedDist, DependencyMetadata, IndexCapabilities, IndexLocations, InstalledDist,
IsBuildBackendError, Resolution, SourceDist,
};
use uv_git::GitResolver;
use uv_pep508::PackageName;
@ -47,7 +49,7 @@ use uv_python::{Interpreter, PythonEnvironment};
/// │ │ │
/// └─────────────┐ │ ┌──────────────┘
/// ┌──┴────┴────┴───┐
/// │ uv-types
/// │ uv-types
/// └────────────────┘
/// ```
///
@ -94,7 +96,7 @@ pub trait BuildContext {
fn resolve<'a>(
&'a self,
requirements: &'a [Requirement],
) -> impl Future<Output = Result<Resolution>> + 'a;
) -> impl Future<Output = Result<Resolution, impl IsBuildBackendError>> + 'a;
/// Install the given set of package versions into the virtual environment. The environment must
/// use the same base Python as [`BuildContext::interpreter`]
@ -102,7 +104,7 @@ pub trait BuildContext {
&'a self,
resolution: &'a Resolution,
venv: &'a PythonEnvironment,
) -> impl Future<Output = Result<Vec<CachedDist>>> + 'a;
) -> impl Future<Output = Result<Vec<CachedDist>, impl IsBuildBackendError>> + 'a;
/// Set up a source distribution build by installing the required dependencies. A wrapper for
/// `uv_build::SourceBuild::setup`.
@ -121,7 +123,7 @@ pub trait BuildContext {
sources: SourceStrategy,
build_kind: BuildKind,
build_output: BuildOutput,
) -> impl Future<Output = Result<Self::SourceDistBuilder>> + 'a;
) -> impl Future<Output = Result<Self::SourceDistBuilder, impl IsBuildBackendError>> + 'a;
/// Build by calling directly into the uv build backend without PEP 517, if possible.
///
@ -136,7 +138,7 @@ pub trait BuildContext {
output_dir: &'a Path,
build_kind: BuildKind,
version_id: Option<&'a str>,
) -> impl Future<Output = Result<Option<DistFilename>>> + 'a;
) -> impl Future<Output = Result<Option<DistFilename>, impl IsBuildBackendError>> + 'a;
}
/// A wrapper for `uv_build::SourceBuild` to avoid cyclical crate dependencies.
@ -150,7 +152,7 @@ pub trait SourceBuildTrait {
///
/// Returns the metadata directory if we're having a PEP 517 build and the
/// `prepare_metadata_for_build_wheel` hook exists
fn metadata(&mut self) -> impl Future<Output = Result<Option<PathBuf>>>;
fn metadata(&mut self) -> impl Future<Output = Result<Option<PathBuf>, AnyErrorBuild>>;
/// A wrapper for `uv_build::SourceBuild::build`.
///
@ -159,7 +161,10 @@ pub trait SourceBuildTrait {
/// Returns the filename of the built wheel inside the given `wheel_dir`. The filename is a
/// string and not a `WheelFilename` because the on disk filename might not be normalized in the
/// same way as uv would.
fn wheel<'a>(&'a self, wheel_dir: &'a Path) -> impl Future<Output = Result<String>> + 'a;
fn wheel<'a>(
&'a self,
wheel_dir: &'a Path,
) -> impl Future<Output = Result<String, AnyErrorBuild>> + 'a;
}
/// A wrapper for [`uv_installer::SitePackages`]
@ -181,3 +186,61 @@ impl InstalledPackagesProvider for EmptyInstalledPackages {
std::iter::empty()
}
}
/// `anyhow::Error`-like wrapper type for [`BuildDispatch`] method return values, that also makes
/// `IsBuildBackendError` work as `thiserror` `#[source]`.
///
/// The errors types have the same problem as [`BuildDispatch`] generally: The `uv-resolver`,
/// `uv-installer` and `uv-build-frontend` error types all reference each other:
/// Resolution and installation may need to build packages, while the build frontend needs to
/// resolve and install for the PEP 517 build environment.
///
/// Usually, `anyhow::Error` is opaque error type of choice. In this case though, we error type
/// that we can inspect on whether it's a build backend error with [`IsBuildBackendError`], and
/// `anyhow::Error` does not allow attaching more traits. The next choice would be
/// `Box<dyn std::error::Error + IsBuildFrontendError + Send + Sync + 'static>`, but `thiserror`
/// complains about the internal `AsDynError` not being implemented when being used as `#[source]`.
/// This struct is an otherwise transparent error wrapper that thiserror recognizes.
pub struct AnyErrorBuild(Box<dyn IsBuildBackendError>);
impl Debug for AnyErrorBuild {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
Debug::fmt(&self.0, f)
}
}
impl Display for AnyErrorBuild {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
Display::fmt(&self.0, f)
}
}
impl std::error::Error for AnyErrorBuild {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.0.source()
}
#[allow(deprecated)]
fn description(&self) -> &str {
self.0.description()
}
#[allow(deprecated)]
fn cause(&self) -> Option<&dyn std::error::Error> {
self.0.cause()
}
}
impl<T: IsBuildBackendError> From<T> for AnyErrorBuild {
fn from(err: T) -> Self {
Self(Box::new(err))
}
}
impl Deref for AnyErrorBuild {
type Target = dyn IsBuildBackendError;
fn deref(&self) -> &Self::Target {
&*self.0
}
}