Rename uv-build to uv-build-frontend (#7688)

uv will soon support both a build frontend (`uv build`) and a build
backend (`build-system = "uv"`). To avoid the name clash, I'm renaming
the `uv-build` crate to `uv-build-frontend`. In a follow-up PR, I will
add a `uv-build-backend` crate with the build backend implementation.
This commit is contained in:
konsti 2024-09-25 20:17:54 +02:00 committed by GitHub
parent 106633a5e5
commit 9004364de3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 8 additions and 8 deletions

2
crates/uv-build-frontend/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
downloads
wheels

View file

@ -0,0 +1,43 @@
[package]
name = "uv-build-frontend"
version = "0.0.1"
description = "Build wheels from source distributions"
edition = { workspace = true }
rust-version = { workspace = true }
homepage = { workspace = true }
documentation = { workspace = true }
repository = { workspace = true }
authors = { workspace = true }
license = { workspace = true }
[lints]
workspace = true
[dependencies]
distribution-types = { workspace = true }
pep440_rs = { workspace = true }
pep508_rs = { workspace = true }
pypi-types = { workspace = true }
uv-configuration = { workspace = true }
uv-fs = { workspace = true }
uv-python = { workspace = true }
uv-types = { workspace = true }
uv-virtualenv = { workspace = true }
anstream = { workspace = true }
anyhow = { workspace = true }
fs-err = { workspace = true }
indoc = { workspace = true }
itertools = { workspace = true }
regex = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
tempfile = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true }
toml_edit = { workspace = true }
tracing = { workspace = true }
rustc-hash = { workspace = true }
[dev-dependencies]
insta = { version = "1.40.0" }

View file

@ -0,0 +1,492 @@
use crate::PythonRunnerOutput;
use itertools::Itertools;
use pep440_rs::Version;
use pep508_rs::PackageName;
use regex::Regex;
use std::env;
use std::fmt::{Display, Formatter};
use std::io;
use std::path::PathBuf;
use std::process::ExitStatus;
use std::sync::LazyLock;
use thiserror::Error;
use tracing::error;
use uv_configuration::BuildOutput;
use uv_fs::Simplified;
/// 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("{} 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("Editable installs with setup.py legacy builds are unsupported, please specify a build backend in pyproject.toml")]
EditableSetupPy,
#[error("Failed to install requirements from {0}")]
RequirementsInstall(&'static str, #[source] anyhow::Error),
#[error("Failed to create temporary virtualenv")]
Virtualenv(#[from] uv_virtualenv::Error),
#[error("Failed to run `{0}`")]
CommandFailed(PathBuf, #[source] io::Error),
#[error("{message} with {exit_code}\n--- stdout:\n{stdout}\n--- stderr:\n{stderr}\n---")]
BuildBackendOutput {
message: String,
exit_code: ExitStatus,
stdout: String,
stderr: String,
},
/// Nudge the user towards installing the missing dev library
#[error("{message} with {exit_code}\n--- stdout:\n{stdout}\n--- stderr:\n{stderr}\n---")]
MissingHeaderOutput {
message: String,
exit_code: ExitStatus,
stdout: String,
stderr: String,
#[source]
missing_header_cause: MissingHeaderCause,
},
#[error("{message} with {exit_code}")]
BuildBackend {
message: String,
exit_code: ExitStatus,
},
#[error("{message} with {exit_code}")]
MissingHeader {
message: String,
exit_code: ExitStatus,
#[source]
missing_header_cause: MissingHeaderCause,
},
#[error("Failed to build PATH for build script")]
BuildScriptPath(#[source] env::JoinPathsError),
}
#[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 \"{header}\" for {package_name}@{package_version}",
)
} else if let Some(version_id) = &self.version_id {
write!(
f,
"This error likely indicates that you need to install a library that provides \"{header}\" for {version_id}",
)
} else {
write!(
f,
"This error likely indicates that you need to install a library that provides \"{header}\"",
)
}
}
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 {library} for {package_name}@{package_version} (e.g., lib{library}-dev)",
)
} 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 {library} for {version_id} (e.g., lib{library}-dev)",
)
} else {
write!(
f,
"This error likely indicates that you need to install the library that provides a shared library for {library} (e.g., lib{library}-dev)",
)
}
}
MissingLibrary::BuildDependency(package) => {
if let (Some(package_name), Some(package_version)) =
(&self.package_name, &self.package_version)
{
write!(
f,
"This error likely indicates that {package_name}@{package_version} depends on {package}, but doesn't declare it as a build dependency. If {package_name} is a first-party package, consider adding {package} to its `build-system.requires`. Otherwise, `uv pip install {package}` into the environment and re-run with `--no-build-isolation`."
)
} else if let Some(version_id) = &self.version_id {
write!(
f,
"This error likely indicates that {version_id} depends on {package}, but doesn't declare it as a build dependency. If {version_id} is a first-party package, consider adding {package} to its `build-system.requires`. Otherwise, `uv pip install {package}` into the environment and re-run with `--no-build-isolation`.",
)
} else {
write!(
f,
"This error likely indicates that a package depends on {package}, but doesn't declare it as a build dependency. If the package is a first-party package, consider adding {package} to its `build-system.requires`. Otherwise, `uv pip install {package}` into the environment and re-run with `--no-build-isolation`.",
)
}
}
MissingLibrary::DeprecatedModule(package, version) => {
if let (Some(package_name), Some(package_version)) =
(&self.package_name, &self.package_version)
{
write!(
f,
"{package} was removed from the standard library in Python {version}. Consider adding a constraint (like `{package_name} >{package_version}`) to avoid building a version of {package_name} that depends on {package}.",
)
} else {
write!(
f,
"{package} was removed from the standard library in Python {version}. Consider adding a constraint to avoid building a package-version that depends on {package}.",
)
}
}
}
}
}
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 {
message,
exit_code: output.status,
missing_header_cause: MissingHeaderCause {
missing_library,
package_name: name.cloned(),
package_version: version.cloned(),
version_id: version_id.map(ToString::to_string),
},
},
BuildOutput::Debug => Self::MissingHeaderOutput {
message,
exit_code: output.status,
stdout: output.stdout.iter().join("\n"),
stderr: output.stderr.iter().join("\n"),
missing_header_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 {
message,
exit_code: output.status,
},
BuildOutput::Debug => Self::BuildBackendOutput {
message,
exit_code: output.status,
stdout: output.stdout.iter().join("\n"),
stderr: output.stderr.iter().join("\n"),
},
}
}
}
#[cfg(test)]
mod test {
use crate::{Error, PythonRunnerOutput};
use indoc::indoc;
use pep440_rs::Version;
use pep508_rs::PackageName;
use std::process::ExitStatus;
use std::str::FromStr;
use uv_configuration::BuildOutput;
#[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::MissingHeaderOutput { .. }));
// Unix uses exit status, Windows uses exit code.
let formatted = err.to_string().replace("exit status: ", "exit code: ");
insta::assert_snapshot!(formatted, @r###"
Failed building wheel through setup.py with 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
---
"###);
insta::assert_snapshot!(
std::error::Error::source(&err).unwrap(),
@r###"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::MissingHeaderOutput { .. }));
// Unix uses exit status, Windows uses exit code.
let formatted = err.to_string().replace("exit status: ", "exit code: ");
insta::assert_snapshot!(formatted, @r###"
Failed building wheel through setup.py with exit code: 0
--- stdout:
--- 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
---
"###);
insta::assert_snapshot!(
std::error::Error::source(&err).unwrap(),
@"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::MissingHeaderOutput { .. }));
// Unix uses exit status, Windows uses exit code.
let formatted = err.to_string().replace("exit status: ", "exit code: ");
insta::assert_snapshot!(formatted, @r###"
Failed building wheel through setup.py with exit code: 0
--- stdout:
--- 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'
---
"###);
insta::assert_snapshot!(
std::error::Error::source(&err).unwrap(),
@"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::MissingHeaderOutput { .. }));
// Unix uses exit status, Windows uses exit code.
let formatted = err.to_string().replace("exit status: ", "exit code: ");
insta::assert_snapshot!(formatted, @r###"
Failed building wheel through setup.py with exit code: 0
--- stdout:
--- stderr:
import distutils.core
ModuleNotFoundError: No module named 'distutils'
---
"###);
insta::assert_snapshot!(
std::error::Error::source(&err).unwrap(),
@"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."
);
}
}

File diff suppressed because it is too large Load diff