mirror of
https://github.com/astral-sh/uv.git
synced 2025-10-15 04:49:41 +00:00

Rustfmt introduces a lot of formatting changes in the 2024 edition. To not break everything all at once, we split out the set of formatting changes compatible with both the 2021 and 2024 edition by first formatting with the 2024 style, and then again with the currently used 2021 style. Notable changes are the formatting of derive macro attributes and lines with overly long strings and adding trailing semicolons after statements consistently.
624 lines
25 KiB
Rust
624 lines
25 KiB
Rust
use std::env;
|
|
use std::fmt::{Display, Formatter};
|
|
use std::io;
|
|
use std::path::PathBuf;
|
|
use std::process::ExitStatus;
|
|
use std::sync::LazyLock;
|
|
|
|
use crate::PythonRunnerOutput;
|
|
use owo_colors::OwoColorize;
|
|
use regex::Regex;
|
|
use thiserror::Error;
|
|
use tracing::error;
|
|
use uv_configuration::BuildOutput;
|
|
use uv_distribution_types::IsBuildBackendError;
|
|
use uv_fs::Simplified;
|
|
use uv_pep440::Version;
|
|
use uv_pep508::PackageName;
|
|
use uv_types::AnyErrorBuild;
|
|
|
|
/// e.g. `pygraphviz/graphviz_wrap.c:3020:10: fatal error: graphviz/cgraph.h: No such file or directory`
|
|
static MISSING_HEADER_RE_GCC: LazyLock<Regex> = LazyLock::new(|| {
|
|
Regex::new(
|
|
r".*\.(?:c|c..|h|h..):\d+:\d+: fatal error: (.*\.(?:h|h..)): No such file or directory",
|
|
)
|
|
.unwrap()
|
|
});
|
|
|
|
/// e.g. `pygraphviz/graphviz_wrap.c:3023:10: fatal error: 'graphviz/cgraph.h' file not found`
|
|
static MISSING_HEADER_RE_CLANG: LazyLock<Regex> = LazyLock::new(|| {
|
|
Regex::new(r".*\.(?:c|c..|h|h..):\d+:\d+: fatal error: '(.*\.(?:h|h..))' file not found")
|
|
.unwrap()
|
|
});
|
|
|
|
/// e.g. `pygraphviz/graphviz_wrap.c(3023): fatal error C1083: Cannot open include file: 'graphviz/cgraph.h': No such file or directory`
|
|
static MISSING_HEADER_RE_MSVC: LazyLock<Regex> = LazyLock::new(|| {
|
|
Regex::new(r".*\.(?:c|c..|h|h..)\(\d+\): fatal error C1083: Cannot open include file: '(.*\.(?:h|h..))': No such file or directory")
|
|
.unwrap()
|
|
});
|
|
|
|
/// e.g. `/usr/bin/ld: cannot find -lncurses: No such file or directory`
|
|
static LD_NOT_FOUND_RE: LazyLock<Regex> = LazyLock::new(|| {
|
|
Regex::new(r"/usr/bin/ld: cannot find -l([a-zA-Z10-9]+): No such file or directory").unwrap()
|
|
});
|
|
|
|
/// e.g. `error: invalid command 'bdist_wheel'`
|
|
static WHEEL_NOT_FOUND_RE: LazyLock<Regex> =
|
|
LazyLock::new(|| Regex::new(r"error: invalid command 'bdist_wheel'").unwrap());
|
|
|
|
/// e.g. `ModuleNotFoundError: No module named 'torch'`
|
|
static TORCH_NOT_FOUND_RE: LazyLock<Regex> =
|
|
LazyLock::new(|| Regex::new(r"ModuleNotFoundError: No module named 'torch'").unwrap());
|
|
|
|
/// e.g. `ModuleNotFoundError: No module named 'distutils'`
|
|
static DISTUTILS_NOT_FOUND_RE: LazyLock<Regex> =
|
|
LazyLock::new(|| Regex::new(r"ModuleNotFoundError: No module named 'distutils'").unwrap());
|
|
|
|
#[derive(Error, Debug)]
|
|
pub enum Error {
|
|
#[error(transparent)]
|
|
Io(#[from] io::Error),
|
|
#[error(transparent)]
|
|
Lowering(#[from] uv_distribution::MetadataError),
|
|
#[error("{} does not appear to be a Python project, as neither `pyproject.toml` nor `setup.py` are present in the directory", _0.simplified_display())]
|
|
InvalidSourceDist(PathBuf),
|
|
#[error("Invalid `pyproject.toml`")]
|
|
InvalidPyprojectTomlSyntax(#[from] toml_edit::TomlError),
|
|
#[error(
|
|
"`pyproject.toml` does not match the required schema. When the `[project]` table is present, `project.name` must be present and non-empty."
|
|
)]
|
|
InvalidPyprojectTomlSchema(#[from] toml_edit::de::Error),
|
|
#[error("Failed to resolve requirements from {0}")]
|
|
RequirementsResolve(&'static str, #[source] AnyErrorBuild),
|
|
#[error("Failed to install requirements from {0}")]
|
|
RequirementsInstall(&'static str, #[source] AnyErrorBuild),
|
|
#[error("Failed to create temporary virtualenv")]
|
|
Virtualenv(#[from] uv_virtualenv::Error),
|
|
// Build backend errors
|
|
#[error("Failed to run `{0}`")]
|
|
CommandFailed(PathBuf, #[source] io::Error),
|
|
#[error("The build backend returned an error")]
|
|
BuildBackend(#[from] BuildBackendError),
|
|
#[error("The build backend returned an error")]
|
|
MissingHeader(#[from] MissingHeaderError),
|
|
#[error("Failed to build PATH for build script")]
|
|
BuildScriptPath(#[source] env::JoinPathsError),
|
|
// For the convenience of typing `setup_build` properly.
|
|
#[error("Building source distributions for `{0}` is disabled")]
|
|
NoSourceDistBuild(PackageName),
|
|
#[error("Building source distributions is disabled")]
|
|
NoSourceDistBuilds,
|
|
#[error("Cyclic build dependency detected for `{0}`")]
|
|
CyclicBuildDependency(PackageName),
|
|
}
|
|
|
|
impl IsBuildBackendError for Error {
|
|
fn is_build_backend_error(&self) -> bool {
|
|
match self {
|
|
Self::Io(_)
|
|
| Self::Lowering(_)
|
|
| Self::InvalidSourceDist(_)
|
|
| Self::InvalidPyprojectTomlSyntax(_)
|
|
| Self::InvalidPyprojectTomlSchema(_)
|
|
| Self::RequirementsResolve(_, _)
|
|
| Self::RequirementsInstall(_, _)
|
|
| Self::Virtualenv(_)
|
|
| Self::NoSourceDistBuild(_)
|
|
| Self::NoSourceDistBuilds
|
|
| Self::CyclicBuildDependency(_) => false,
|
|
Self::CommandFailed(_, _)
|
|
| Self::BuildBackend(_)
|
|
| Self::MissingHeader(_)
|
|
| Self::BuildScriptPath(_) => true,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
enum MissingLibrary {
|
|
Header(String),
|
|
Linker(String),
|
|
BuildDependency(String),
|
|
DeprecatedModule(String, Version),
|
|
}
|
|
|
|
#[derive(Debug, Error)]
|
|
pub struct MissingHeaderCause {
|
|
missing_library: MissingLibrary,
|
|
package_name: Option<PackageName>,
|
|
package_version: Option<Version>,
|
|
version_id: Option<String>,
|
|
}
|
|
|
|
impl Display for MissingHeaderCause {
|
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
match &self.missing_library {
|
|
MissingLibrary::Header(header) => {
|
|
if let (Some(package_name), Some(package_version)) =
|
|
(&self.package_name, &self.package_version)
|
|
{
|
|
write!(
|
|
f,
|
|
"This error likely indicates that you need to install a library that provides \"{}\" for `{}`",
|
|
header.cyan(),
|
|
format!("{package_name}@{package_version}").cyan(),
|
|
)
|
|
} else if let Some(version_id) = &self.version_id {
|
|
write!(
|
|
f,
|
|
"This error likely indicates that you need to install a library that provides \"{}\" for `{}`",
|
|
header.cyan(),
|
|
version_id.cyan(),
|
|
)
|
|
} else {
|
|
write!(
|
|
f,
|
|
"This error likely indicates that you need to install a library that provides \"{}\"",
|
|
header.cyan(),
|
|
)
|
|
}
|
|
}
|
|
MissingLibrary::Linker(library) => {
|
|
if let (Some(package_name), Some(package_version)) =
|
|
(&self.package_name, &self.package_version)
|
|
{
|
|
write!(
|
|
f,
|
|
"This error likely indicates that you need to install the library that provides a shared library for `{}` for `{}` (e.g., `{}`)",
|
|
library.cyan(),
|
|
format!("{package_name}@{package_version}").cyan(),
|
|
format!("lib{library}-dev").cyan(),
|
|
)
|
|
} else if let Some(version_id) = &self.version_id {
|
|
write!(
|
|
f,
|
|
"This error likely indicates that you need to install the library that provides a shared library for `{}` for `{}` (e.g., `{}`)",
|
|
library.cyan(),
|
|
version_id.cyan(),
|
|
format!("lib{library}-dev").cyan(),
|
|
)
|
|
} else {
|
|
write!(
|
|
f,
|
|
"This error likely indicates that you need to install the library that provides a shared library for `{}` (e.g., `{}`)",
|
|
library.cyan(),
|
|
format!("lib{library}-dev").cyan(),
|
|
)
|
|
}
|
|
}
|
|
MissingLibrary::BuildDependency(package) => {
|
|
if let (Some(package_name), Some(package_version)) =
|
|
(&self.package_name, &self.package_version)
|
|
{
|
|
write!(
|
|
f,
|
|
"This error likely indicates that `{}` depends on `{}`, but doesn't declare it as a build dependency. If `{}` is a first-party package, consider adding `{}` to its `{}`. Otherwise, `{}` into the environment and re-run with `{}`.",
|
|
format!("{package_name}@{package_version}").cyan(),
|
|
package.cyan(),
|
|
package_name.cyan(),
|
|
package.cyan(),
|
|
"build-system.requires".green(),
|
|
format!("uv pip install {package}").green(),
|
|
"--no-build-isolation".green(),
|
|
)
|
|
} else if let Some(version_id) = &self.version_id {
|
|
write!(
|
|
f,
|
|
"This error likely indicates that `{}` depends on `{}`, but doesn't declare it as a build dependency. If `{}` is a first-party package, consider adding `{}` to its `{}`. Otherwise, `{}` into the environment and re-run with `{}`.",
|
|
version_id.cyan(),
|
|
package.cyan(),
|
|
version_id.cyan(),
|
|
package.cyan(),
|
|
"build-system.requires".green(),
|
|
format!("uv pip install {package}").green(),
|
|
"--no-build-isolation".green(),
|
|
)
|
|
} else {
|
|
write!(
|
|
f,
|
|
"This error likely indicates that a package depends on `{}`, but doesn't declare it as a build dependency. If the package is a first-party package, consider adding `{}` to its `{}`. Otherwise, `{}` into the environment and re-run with `{}`.",
|
|
package.cyan(),
|
|
package.cyan(),
|
|
"build-system.requires".green(),
|
|
format!("uv pip install {package}").green(),
|
|
"--no-build-isolation".green(),
|
|
)
|
|
}
|
|
}
|
|
MissingLibrary::DeprecatedModule(package, version) => {
|
|
if let (Some(package_name), Some(package_version)) =
|
|
(&self.package_name, &self.package_version)
|
|
{
|
|
write!(
|
|
f,
|
|
"`{}` was removed from the standard library in Python {version}. Consider adding a constraint (like `{}`) to avoid building a version of `{}` that depends on `{}`.",
|
|
package.cyan(),
|
|
format!("{package_name} >{package_version}").green(),
|
|
package_name.cyan(),
|
|
package.cyan(),
|
|
)
|
|
} else {
|
|
write!(
|
|
f,
|
|
"`{}` was removed from the standard library in Python {version}. Consider adding a constraint to avoid building a package that depends on `{}`.",
|
|
package.cyan(),
|
|
package.cyan(),
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Error)]
|
|
pub struct BuildBackendError {
|
|
message: String,
|
|
exit_code: ExitStatus,
|
|
stdout: Vec<String>,
|
|
stderr: Vec<String>,
|
|
}
|
|
|
|
impl Display for BuildBackendError {
|
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
write!(f, "{} ({})", self.message, self.exit_code)?;
|
|
|
|
let mut non_empty = false;
|
|
|
|
if self.stdout.iter().any(|line| !line.trim().is_empty()) {
|
|
write!(f, "\n\n{}\n{}", "[stdout]".red(), self.stdout.join("\n"))?;
|
|
non_empty = true;
|
|
}
|
|
|
|
if self.stderr.iter().any(|line| !line.trim().is_empty()) {
|
|
write!(f, "\n\n{}\n{}", "[stderr]".red(), self.stderr.join("\n"))?;
|
|
non_empty = true;
|
|
}
|
|
|
|
if non_empty {
|
|
writeln!(f)?;
|
|
}
|
|
|
|
write!(
|
|
f,
|
|
"\n{}{} This usually indicates a problem with the package or the build environment.",
|
|
"hint".bold().cyan(),
|
|
":".bold()
|
|
)?;
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Error)]
|
|
pub struct MissingHeaderError {
|
|
message: String,
|
|
exit_code: ExitStatus,
|
|
stdout: Vec<String>,
|
|
stderr: Vec<String>,
|
|
cause: MissingHeaderCause,
|
|
}
|
|
|
|
impl Display for MissingHeaderError {
|
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
write!(f, "{} ({})", self.message, self.exit_code)?;
|
|
|
|
if self.stdout.iter().any(|line| !line.trim().is_empty()) {
|
|
write!(f, "\n\n{}\n{}", "[stdout]".red(), self.stdout.join("\n"))?;
|
|
}
|
|
|
|
if self.stderr.iter().any(|line| !line.trim().is_empty()) {
|
|
write!(f, "\n\n{}\n{}", "[stderr]".red(), self.stderr.join("\n"))?;
|
|
}
|
|
|
|
write!(
|
|
f,
|
|
"\n\n{}{} {}",
|
|
"hint".bold().cyan(),
|
|
":".bold(),
|
|
self.cause
|
|
)?;
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl Error {
|
|
/// Construct an [`Error`] from the output of a failed command.
|
|
pub(crate) fn from_command_output(
|
|
message: String,
|
|
output: &PythonRunnerOutput,
|
|
level: BuildOutput,
|
|
name: Option<&PackageName>,
|
|
version: Option<&Version>,
|
|
version_id: Option<&str>,
|
|
) -> Self {
|
|
// In the cases I've seen it was the 5th and 3rd last line (see test case), 10 seems like a reasonable cutoff.
|
|
let missing_library = output.stderr.iter().rev().take(10).find_map(|line| {
|
|
if let Some((_, [header])) = MISSING_HEADER_RE_GCC
|
|
.captures(line.trim())
|
|
.or(MISSING_HEADER_RE_CLANG.captures(line.trim()))
|
|
.or(MISSING_HEADER_RE_MSVC.captures(line.trim()))
|
|
.map(|c| c.extract())
|
|
{
|
|
Some(MissingLibrary::Header(header.to_string()))
|
|
} else if let Some((_, [library])) =
|
|
LD_NOT_FOUND_RE.captures(line.trim()).map(|c| c.extract())
|
|
{
|
|
Some(MissingLibrary::Linker(library.to_string()))
|
|
} else if WHEEL_NOT_FOUND_RE.is_match(line.trim()) {
|
|
Some(MissingLibrary::BuildDependency("wheel".to_string()))
|
|
} else if TORCH_NOT_FOUND_RE.is_match(line.trim()) {
|
|
Some(MissingLibrary::BuildDependency("torch".to_string()))
|
|
} else if DISTUTILS_NOT_FOUND_RE.is_match(line.trim()) {
|
|
Some(MissingLibrary::DeprecatedModule(
|
|
"distutils".to_string(),
|
|
Version::new([3, 12]),
|
|
))
|
|
} else {
|
|
None
|
|
}
|
|
});
|
|
|
|
if let Some(missing_library) = missing_library {
|
|
return match level {
|
|
BuildOutput::Stderr | BuildOutput::Quiet => {
|
|
Self::MissingHeader(MissingHeaderError {
|
|
message,
|
|
exit_code: output.status,
|
|
stdout: vec![],
|
|
stderr: vec![],
|
|
cause: MissingHeaderCause {
|
|
missing_library,
|
|
package_name: name.cloned(),
|
|
package_version: version.cloned(),
|
|
version_id: version_id.map(ToString::to_string),
|
|
},
|
|
})
|
|
}
|
|
BuildOutput::Debug => Self::MissingHeader(MissingHeaderError {
|
|
message,
|
|
exit_code: output.status,
|
|
stdout: output.stdout.clone(),
|
|
stderr: output.stderr.clone(),
|
|
cause: MissingHeaderCause {
|
|
missing_library,
|
|
package_name: name.cloned(),
|
|
package_version: version.cloned(),
|
|
version_id: version_id.map(ToString::to_string),
|
|
},
|
|
}),
|
|
};
|
|
}
|
|
|
|
match level {
|
|
BuildOutput::Stderr | BuildOutput::Quiet => Self::BuildBackend(BuildBackendError {
|
|
message,
|
|
exit_code: output.status,
|
|
stdout: vec![],
|
|
stderr: vec![],
|
|
}),
|
|
BuildOutput::Debug => Self::BuildBackend(BuildBackendError {
|
|
message,
|
|
exit_code: output.status,
|
|
stdout: output.stdout.clone(),
|
|
stderr: output.stderr.clone(),
|
|
}),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use crate::{Error, PythonRunnerOutput};
|
|
use indoc::indoc;
|
|
use std::process::ExitStatus;
|
|
use std::str::FromStr;
|
|
use uv_configuration::BuildOutput;
|
|
use uv_pep440::Version;
|
|
use uv_pep508::PackageName;
|
|
|
|
#[test]
|
|
fn missing_header() {
|
|
let output = PythonRunnerOutput {
|
|
status: ExitStatus::default(), // This is wrong but `from_raw` is platform-gated.
|
|
stdout: indoc!(r"
|
|
running bdist_wheel
|
|
running build
|
|
[...]
|
|
creating build/temp.linux-x86_64-cpython-39/pygraphviz
|
|
gcc -Wno-unused-result -Wsign-compare -DNDEBUG -g -fwrapv -O3 -Wall -DOPENSSL_NO_SSL3 -fPIC -DSWIG_PYTHON_STRICT_BYTE_CHAR -I/tmp/.tmpy6vVes/.venv/include -I/home/konsti/.pyenv/versions/3.9.18/include/python3.9 -c pygraphviz/graphviz_wrap.c -o build/temp.linux-x86_64-cpython-39/pygraphviz/graphviz_wrap.o
|
|
"
|
|
).lines().map(ToString::to_string).collect(),
|
|
stderr: indoc!(r#"
|
|
warning: no files found matching '*.png' under directory 'doc'
|
|
warning: no files found matching '*.txt' under directory 'doc'
|
|
[...]
|
|
no previously-included directories found matching 'doc/build'
|
|
pygraphviz/graphviz_wrap.c:3020:10: fatal error: graphviz/cgraph.h: No such file or directory
|
|
3020 | #include "graphviz/cgraph.h"
|
|
| ^~~~~~~~~~~~~~~~~~~
|
|
compilation terminated.
|
|
error: command '/usr/bin/gcc' failed with exit code 1
|
|
"#
|
|
).lines().map(ToString::to_string).collect(),
|
|
};
|
|
|
|
let err = Error::from_command_output(
|
|
"Failed building wheel through setup.py".to_string(),
|
|
&output,
|
|
BuildOutput::Debug,
|
|
None,
|
|
None,
|
|
Some("pygraphviz-1.11"),
|
|
);
|
|
|
|
assert!(matches!(err, Error::MissingHeader { .. }));
|
|
// Unix uses exit status, Windows uses exit code.
|
|
let formatted = std::error::Error::source(&err)
|
|
.unwrap()
|
|
.to_string()
|
|
.replace("exit status: ", "exit code: ");
|
|
let formatted = anstream::adapter::strip_str(&formatted);
|
|
insta::assert_snapshot!(formatted, @r###"
|
|
Failed building wheel through setup.py (exit code: 0)
|
|
|
|
[stdout]
|
|
running bdist_wheel
|
|
running build
|
|
[...]
|
|
creating build/temp.linux-x86_64-cpython-39/pygraphviz
|
|
gcc -Wno-unused-result -Wsign-compare -DNDEBUG -g -fwrapv -O3 -Wall -DOPENSSL_NO_SSL3 -fPIC -DSWIG_PYTHON_STRICT_BYTE_CHAR -I/tmp/.tmpy6vVes/.venv/include -I/home/konsti/.pyenv/versions/3.9.18/include/python3.9 -c pygraphviz/graphviz_wrap.c -o build/temp.linux-x86_64-cpython-39/pygraphviz/graphviz_wrap.o
|
|
|
|
[stderr]
|
|
warning: no files found matching '*.png' under directory 'doc'
|
|
warning: no files found matching '*.txt' under directory 'doc'
|
|
[...]
|
|
no previously-included directories found matching 'doc/build'
|
|
pygraphviz/graphviz_wrap.c:3020:10: fatal error: graphviz/cgraph.h: No such file or directory
|
|
3020 | #include "graphviz/cgraph.h"
|
|
| ^~~~~~~~~~~~~~~~~~~
|
|
compilation terminated.
|
|
error: command '/usr/bin/gcc' failed with exit code 1
|
|
|
|
hint: This error likely indicates that you need to install a library that provides "graphviz/cgraph.h" for `pygraphviz-1.11`
|
|
"###);
|
|
}
|
|
|
|
#[test]
|
|
fn missing_linker_library() {
|
|
let output = PythonRunnerOutput {
|
|
status: ExitStatus::default(), // This is wrong but `from_raw` is platform-gated.
|
|
stdout: Vec::new(),
|
|
stderr: indoc!(
|
|
r"
|
|
1099 | n = strlen(p);
|
|
| ^~~~~~~~~
|
|
/usr/bin/ld: cannot find -lncurses: No such file or directory
|
|
collect2: error: ld returned 1 exit status
|
|
error: command '/usr/bin/x86_64-linux-gnu-gcc' failed with exit code 1"
|
|
)
|
|
.lines()
|
|
.map(ToString::to_string)
|
|
.collect(),
|
|
};
|
|
|
|
let err = Error::from_command_output(
|
|
"Failed building wheel through setup.py".to_string(),
|
|
&output,
|
|
BuildOutput::Debug,
|
|
None,
|
|
None,
|
|
Some("pygraphviz-1.11"),
|
|
);
|
|
assert!(matches!(err, Error::MissingHeader { .. }));
|
|
// Unix uses exit status, Windows uses exit code.
|
|
let formatted = std::error::Error::source(&err)
|
|
.unwrap()
|
|
.to_string()
|
|
.replace("exit status: ", "exit code: ");
|
|
let formatted = anstream::adapter::strip_str(&formatted);
|
|
insta::assert_snapshot!(formatted, @r###"
|
|
Failed building wheel through setup.py (exit code: 0)
|
|
|
|
[stderr]
|
|
1099 | n = strlen(p);
|
|
| ^~~~~~~~~
|
|
/usr/bin/ld: cannot find -lncurses: No such file or directory
|
|
collect2: error: ld returned 1 exit status
|
|
error: command '/usr/bin/x86_64-linux-gnu-gcc' failed with exit code 1
|
|
|
|
hint: This error likely indicates that you need to install the library that provides a shared library for `ncurses` for `pygraphviz-1.11` (e.g., `libncurses-dev`)
|
|
"###);
|
|
}
|
|
|
|
#[test]
|
|
fn missing_wheel_package() {
|
|
let output = PythonRunnerOutput {
|
|
status: ExitStatus::default(), // This is wrong but `from_raw` is platform-gated.
|
|
stdout: Vec::new(),
|
|
stderr: indoc!(
|
|
r"
|
|
usage: setup.py [global_opts] cmd1 [cmd1_opts] [cmd2 [cmd2_opts] ...]
|
|
or: setup.py --help [cmd1 cmd2 ...]
|
|
or: setup.py --help-commands
|
|
or: setup.py cmd --help
|
|
|
|
error: invalid command 'bdist_wheel'"
|
|
)
|
|
.lines()
|
|
.map(ToString::to_string)
|
|
.collect(),
|
|
};
|
|
|
|
let err = Error::from_command_output(
|
|
"Failed building wheel through setup.py".to_string(),
|
|
&output,
|
|
BuildOutput::Debug,
|
|
None,
|
|
None,
|
|
Some("pygraphviz-1.11"),
|
|
);
|
|
assert!(matches!(err, Error::MissingHeader { .. }));
|
|
// Unix uses exit status, Windows uses exit code.
|
|
let formatted = std::error::Error::source(&err)
|
|
.unwrap()
|
|
.to_string()
|
|
.replace("exit status: ", "exit code: ");
|
|
let formatted = anstream::adapter::strip_str(&formatted);
|
|
insta::assert_snapshot!(formatted, @r###"
|
|
Failed building wheel through setup.py (exit code: 0)
|
|
|
|
[stderr]
|
|
usage: setup.py [global_opts] cmd1 [cmd1_opts] [cmd2 [cmd2_opts] ...]
|
|
or: setup.py --help [cmd1 cmd2 ...]
|
|
or: setup.py --help-commands
|
|
or: setup.py cmd --help
|
|
|
|
error: invalid command 'bdist_wheel'
|
|
|
|
hint: This error likely indicates that `pygraphviz-1.11` depends on `wheel`, but doesn't declare it as a build dependency. If `pygraphviz-1.11` is a first-party package, consider adding `wheel` to its `build-system.requires`. Otherwise, `uv pip install wheel` into the environment and re-run with `--no-build-isolation`.
|
|
"###);
|
|
}
|
|
|
|
#[test]
|
|
fn missing_distutils() {
|
|
let output = PythonRunnerOutput {
|
|
status: ExitStatus::default(), // This is wrong but `from_raw` is platform-gated.
|
|
stdout: Vec::new(),
|
|
stderr: indoc!(
|
|
r"
|
|
import distutils.core
|
|
ModuleNotFoundError: No module named 'distutils'
|
|
"
|
|
)
|
|
.lines()
|
|
.map(ToString::to_string)
|
|
.collect(),
|
|
};
|
|
|
|
let err = Error::from_command_output(
|
|
"Failed building wheel through setup.py".to_string(),
|
|
&output,
|
|
BuildOutput::Debug,
|
|
Some(&PackageName::from_str("pygraphviz").unwrap()),
|
|
Some(&Version::new([1, 11])),
|
|
Some("pygraphviz-1.11"),
|
|
);
|
|
assert!(matches!(err, Error::MissingHeader { .. }));
|
|
// Unix uses exit status, Windows uses exit code.
|
|
let formatted = std::error::Error::source(&err)
|
|
.unwrap()
|
|
.to_string()
|
|
.replace("exit status: ", "exit code: ");
|
|
let formatted = anstream::adapter::strip_str(&formatted);
|
|
insta::assert_snapshot!(formatted, @r###"
|
|
Failed building wheel through setup.py (exit code: 0)
|
|
|
|
[stderr]
|
|
import distutils.core
|
|
ModuleNotFoundError: No module named 'distutils'
|
|
|
|
hint: `distutils` was removed from the standard library in Python 3.12. Consider adding a constraint (like `pygraphviz >1.11`) to avoid building a version of `pygraphviz` that depends on `distutils`.
|
|
"###);
|
|
}
|
|
}
|