Skip to content

Commit ebc6d20

Browse files
authored
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)
1 parent b7df5db commit ebc6d20

File tree

24 files changed

+428
-264
lines changed

24 files changed

+428
-264
lines changed

Cargo.lock

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/uv-build-frontend/Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ uv-types = { workspace = true }
3030
uv-virtualenv = { workspace = true }
3131

3232
anstream = { workspace = true }
33-
anyhow = { workspace = true }
3433
fs-err = { workspace = true }
3534
indoc = { workspace = true }
3635
itertools = { workspace = true }

crates/uv-build-frontend/src/error.rs

Lines changed: 58 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,17 @@ use std::path::PathBuf;
55
use std::process::ExitStatus;
66
use std::sync::LazyLock;
77

8+
use crate::PythonRunnerOutput;
89
use owo_colors::OwoColorize;
910
use regex::Regex;
1011
use thiserror::Error;
1112
use tracing::error;
1213
use uv_configuration::BuildOutput;
14+
use uv_distribution_types::IsBuildBackendError;
1315
use uv_fs::Simplified;
1416
use uv_pep440::Version;
1517
use uv_pep508::PackageName;
16-
17-
use crate::PythonRunnerOutput;
18+
use uv_types::AnyErrorBuild;
1819

1920
/// e.g. `pygraphviz/graphviz_wrap.c:3020:10: fatal error: graphviz/cgraph.h: No such file or directory`
2021
static MISSING_HEADER_RE_GCC: LazyLock<Regex> = LazyLock::new(|| {
@@ -68,19 +69,47 @@ pub enum Error {
6869
#[error("Editable installs with setup.py legacy builds are unsupported, please specify a build backend in pyproject.toml")]
6970
EditableSetupPy,
7071
#[error("Failed to resolve requirements from {0}")]
71-
RequirementsResolve(&'static str, #[source] anyhow::Error),
72+
RequirementsResolve(&'static str, #[source] AnyErrorBuild),
7273
#[error("Failed to install requirements from {0}")]
73-
RequirementsInstall(&'static str, #[source] anyhow::Error),
74+
RequirementsInstall(&'static str, #[source] AnyErrorBuild),
7475
#[error("Failed to create temporary virtualenv")]
7576
Virtualenv(#[from] uv_virtualenv::Error),
77+
// Build backend errors
7678
#[error("Failed to run `{0}`")]
7779
CommandFailed(PathBuf, #[source] io::Error),
78-
#[error(transparent)]
80+
#[error("The build backend returned an error")]
7981
BuildBackend(#[from] BuildBackendError),
80-
#[error(transparent)]
82+
#[error("The build backend returned an error")]
8183
MissingHeader(#[from] MissingHeaderError),
8284
#[error("Failed to build PATH for build script")]
8385
BuildScriptPath(#[source] env::JoinPathsError),
86+
// For the convenience of typing `setup_build` properly.
87+
#[error("Building source distributions for {0} is disabled")]
88+
NoSourceDistBuild(PackageName),
89+
#[error("Building source distributions is disabled")]
90+
NoSourceDistBuilds,
91+
}
92+
93+
impl IsBuildBackendError for Error {
94+
fn is_build_backend_error(&self) -> bool {
95+
match self {
96+
Self::Io(_)
97+
| Self::Lowering(_)
98+
| Self::InvalidSourceDist(_)
99+
| Self::InvalidPyprojectTomlSyntax(_)
100+
| Self::InvalidPyprojectTomlSchema(_)
101+
| Self::EditableSetupPy
102+
| Self::RequirementsResolve(_, _)
103+
| Self::RequirementsInstall(_, _)
104+
| Self::Virtualenv(_)
105+
| Self::NoSourceDistBuild(_)
106+
| Self::NoSourceDistBuilds => false,
107+
Self::CommandFailed(_, _)
108+
| Self::BuildBackend(_)
109+
| Self::MissingHeader(_)
110+
| Self::BuildScriptPath(_) => true,
111+
}
112+
}
84113
}
85114

86115
#[derive(Debug)]
@@ -247,6 +276,13 @@ impl Display for BuildBackendError {
247276
writeln!(f)?;
248277
}
249278

279+
write!(
280+
f,
281+
"\n{}{} This usually indicates a problem with the package or the build environment.",
282+
"hint".bold().cyan(),
283+
":".bold()
284+
)?;
285+
250286
Ok(())
251287
}
252288
}
@@ -416,7 +452,10 @@ mod test {
416452

417453
assert!(matches!(err, Error::MissingHeader { .. }));
418454
// Unix uses exit status, Windows uses exit code.
419-
let formatted = err.to_string().replace("exit status: ", "exit code: ");
455+
let formatted = std::error::Error::source(&err)
456+
.unwrap()
457+
.to_string()
458+
.replace("exit status: ", "exit code: ");
420459
let formatted = anstream::adapter::strip_str(&formatted);
421460
insta::assert_snapshot!(formatted, @r###"
422461
Failed building wheel through setup.py (exit code: 0)
@@ -471,7 +510,10 @@ mod test {
471510
);
472511
assert!(matches!(err, Error::MissingHeader { .. }));
473512
// Unix uses exit status, Windows uses exit code.
474-
let formatted = err.to_string().replace("exit status: ", "exit code: ");
513+
let formatted = std::error::Error::source(&err)
514+
.unwrap()
515+
.to_string()
516+
.replace("exit status: ", "exit code: ");
475517
let formatted = anstream::adapter::strip_str(&formatted);
476518
insta::assert_snapshot!(formatted, @r###"
477519
Failed building wheel through setup.py (exit code: 0)
@@ -516,7 +558,10 @@ mod test {
516558
);
517559
assert!(matches!(err, Error::MissingHeader { .. }));
518560
// Unix uses exit status, Windows uses exit code.
519-
let formatted = err.to_string().replace("exit status: ", "exit code: ");
561+
let formatted = std::error::Error::source(&err)
562+
.unwrap()
563+
.to_string()
564+
.replace("exit status: ", "exit code: ");
520565
let formatted = anstream::adapter::strip_str(&formatted);
521566
insta::assert_snapshot!(formatted, @r###"
522567
Failed building wheel through setup.py (exit code: 0)
@@ -559,7 +604,10 @@ mod test {
559604
);
560605
assert!(matches!(err, Error::MissingHeader { .. }));
561606
// Unix uses exit status, Windows uses exit code.
562-
let formatted = err.to_string().replace("exit status: ", "exit code: ");
607+
let formatted = std::error::Error::source(&err)
608+
.unwrap()
609+
.to_string()
610+
.replace("exit status: ", "exit code: ");
563611
let formatted = anstream::adapter::strip_str(&formatted);
564612
insta::assert_snapshot!(formatted, @r###"
565613
Failed building wheel through setup.py (exit code: 0)

crates/uv-build-frontend/src/lib.rs

Lines changed: 45 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ mod error;
77
use fs_err as fs;
88
use indoc::formatdoc;
99
use itertools::Itertools;
10-
use owo_colors::OwoColorize;
1110
use rustc_hash::FxHashMap;
1211
use serde::de::{value, IntoDeserializer, SeqAccess, Visitor};
1312
use serde::{de, Deserialize, Deserializer};
@@ -36,7 +35,7 @@ use uv_pep508::PackageName;
3635
use uv_pypi_types::{Requirement, VerbatimParsedUrl};
3736
use uv_python::{Interpreter, PythonEnvironment};
3837
use uv_static::EnvVars;
39-
use uv_types::{BuildContext, BuildIsolation, SourceBuildTrait};
38+
use uv_types::{AnyErrorBuild, BuildContext, BuildIsolation, SourceBuildTrait};
4039

4140
pub use crate::error::{Error, MissingHeaderCause};
4241

@@ -325,7 +324,7 @@ impl SourceBuild {
325324
build_context
326325
.install(&resolved_requirements, &venv)
327326
.await
328-
.map_err(|err| Error::RequirementsInstall("`build-system.requires`", err))?;
327+
.map_err(|err| Error::RequirementsInstall("`build-system.requires`", err.into()))?;
329328
} else {
330329
debug!("Proceeding without build isolation");
331330
}
@@ -423,15 +422,19 @@ impl SourceBuild {
423422
let resolved_requirements = build_context
424423
.resolve(&default_backend.requirements)
425424
.await
426-
.map_err(|err| Error::RequirementsResolve("`setup.py` build", err))?;
425+
.map_err(|err| {
426+
Error::RequirementsResolve("`setup.py` build", err.into())
427+
})?;
427428
*resolution = Some(resolved_requirements.clone());
428429
resolved_requirements
429430
}
430431
} else {
431432
build_context
432433
.resolve(&pep517_backend.requirements)
433434
.await
434-
.map_err(|err| Error::RequirementsResolve("`build-system.requires`", err))?
435+
.map_err(|err| {
436+
Error::RequirementsResolve("`build-system.requires`", err.into())
437+
})?
435438
},
436439
)
437440
}
@@ -622,8 +625,8 @@ impl SourceBuild {
622625
if !output.status.success() {
623626
return Err(Error::from_command_output(
624627
format!(
625-
"Build backend failed to determine metadata through `{}`",
626-
format!("prepare_metadata_for_build_{}", self.build_kind).green()
628+
"Call to `{}.prepare_metadata_for_build_{}` failed",
629+
self.pep517_backend.backend, self.build_kind
627630
),
628631
&output,
629632
self.level,
@@ -745,9 +748,8 @@ impl SourceBuild {
745748
if !output.status.success() {
746749
return Err(Error::from_command_output(
747750
format!(
748-
"Build backend failed to build {} through `{}`",
749-
self.build_kind,
750-
format!("build_{}", self.build_kind).green(),
751+
"Call to `{}.build_{}` failed",
752+
pep517_backend.backend, self.build_kind
751753
),
752754
&output,
753755
self.level,
@@ -761,8 +763,8 @@ impl SourceBuild {
761763
if !output_dir.join(&distribution_filename).is_file() {
762764
return Err(Error::from_command_output(
763765
format!(
764-
"Build backend failed to produce {} through `{}`: `{distribution_filename}` not found",
765-
self.build_kind, format!("build_{}", self.build_kind).green(),
766+
"Call to `{}.build_{}` failed",
767+
pep517_backend.backend, self.build_kind
766768
),
767769
&output,
768770
self.level,
@@ -776,11 +778,11 @@ impl SourceBuild {
776778
}
777779

778780
impl SourceBuildTrait for SourceBuild {
779-
async fn metadata(&mut self) -> anyhow::Result<Option<PathBuf>> {
781+
async fn metadata(&mut self) -> Result<Option<PathBuf>, AnyErrorBuild> {
780782
Ok(self.get_metadata_without_build().await?)
781783
}
782784

783-
async fn wheel<'a>(&'a self, wheel_dir: &'a Path) -> anyhow::Result<String> {
785+
async fn wheel<'a>(&'a self, wheel_dir: &'a Path) -> Result<String, AnyErrorBuild> {
784786
Ok(self.build(wheel_dir).await?)
785787
}
786788
}
@@ -858,8 +860,8 @@ async fn create_pep517_build_environment(
858860
if !output.status.success() {
859861
return Err(Error::from_command_output(
860862
format!(
861-
"Build backend failed to determine requirements with `{}`",
862-
format!("build_{build_kind}()").green()
863+
"Call to `{}.build_{}` failed",
864+
pep517_backend.backend, build_kind
863865
),
864866
&output,
865867
level,
@@ -869,37 +871,27 @@ async fn create_pep517_build_environment(
869871
));
870872
}
871873

872-
// Read the requirements from the output file.
873-
let contents = fs_err::read(&outfile).map_err(|err| {
874-
Error::from_command_output(
875-
format!(
876-
"Build backend failed to read requirements from `{}`: {err}",
877-
format!("get_requires_for_build_{build_kind}").green(),
878-
),
879-
&output,
880-
level,
881-
package_name,
882-
package_version,
883-
version_id,
884-
)
885-
})?;
886-
887-
// Deserialize the requirements from the output file.
888-
let extra_requires: Vec<uv_pep508::Requirement<VerbatimParsedUrl>> =
889-
serde_json::from_slice::<Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>(&contents)
890-
.map_err(|err| {
891-
Error::from_command_output(
892-
format!(
893-
"Build backend failed to return requirements from `{}`: {err}",
894-
format!("get_requires_for_build_{build_kind}").green(),
895-
),
896-
&output,
897-
level,
898-
package_name,
899-
package_version,
900-
version_id,
901-
)
902-
})?;
874+
// Read and deserialize the requirements from the output file.
875+
let read_requires_result = fs_err::read(&outfile)
876+
.map_err(|err| err.to_string())
877+
.and_then(|contents| serde_json::from_slice(&contents).map_err(|err| err.to_string()));
878+
let extra_requires: Vec<uv_pep508::Requirement<VerbatimParsedUrl>> = match read_requires_result
879+
{
880+
Ok(extra_requires) => extra_requires,
881+
Err(err) => {
882+
return Err(Error::from_command_output(
883+
format!(
884+
"Call to `{}.get_requires_for_build_{}` failed: {}",
885+
pep517_backend.backend, build_kind, err
886+
),
887+
&output,
888+
level,
889+
package_name,
890+
package_version,
891+
version_id,
892+
))
893+
}
894+
};
903895

904896
// If necessary, lower the requirements.
905897
let extra_requires = match source_strategy {
@@ -937,15 +929,16 @@ async fn create_pep517_build_environment(
937929
.cloned()
938930
.chain(extra_requires)
939931
.collect();
940-
let resolution = build_context
941-
.resolve(&requirements)
942-
.await
943-
.map_err(|err| Error::RequirementsResolve("`build-system.requires`", err))?;
932+
let resolution = build_context.resolve(&requirements).await.map_err(|err| {
933+
Error::RequirementsResolve("`build-system.requires`", AnyErrorBuild::from(err))
934+
})?;
944935

945936
build_context
946937
.install(&resolution, venv)
947938
.await
948-
.map_err(|err| Error::RequirementsInstall("`build-system.requires`", err))?;
939+
.map_err(|err| {
940+
Error::RequirementsInstall("`build-system.requires`", AnyErrorBuild::from(err))
941+
})?;
949942
}
950943

951944
Ok(())

crates/uv-dispatch/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ uv-distribution-types = { workspace = true }
2828
uv-git = { workspace = true }
2929
uv-install-wheel = { workspace = true }
3030
uv-installer = { workspace = true }
31+
uv-platform-tags = { workspace = true }
3132
uv-pypi-types = { workspace = true }
3233
uv-python = { workspace = true }
3334
uv-resolver = { workspace = true }
@@ -38,5 +39,6 @@ anyhow = { workspace = true }
3839
futures = { workspace = true }
3940
itertools = { workspace = true }
4041
rustc-hash = { workspace = true }
42+
thiserror = { workspace = true }
4143
tokio = { workspace = true }
4244
tracing = { workspace = true }

0 commit comments

Comments
 (0)