Better warning for no direct build (#15898)

**Setup**

```
$ git clone https://github.com/wheelnext/variant_aarch64
$ cd variant_aarch64
$ git checkout 1d047e667dbce4c74878a68c653a6b41bc3d3684
```

**Before**

```
$ uv build -v
[...]
DEBUG Not using uv build backend direct build of , no pyproject.toml: TOML parse error at line 5, column 1
  |
5 | [project]
  | ^^^^^^^^^
missing field `version`
[...]
```

**After**

```
$ uv build -v
[...]
DEBUG Not using uv build backend direct build of ``, pyproject.toml does not match: The value for `build_system.build-backend` should be `"uv_build"`, not `"flit_core.buildapi"`
[...]
```

The empty string gets fixed in
https://github.com/astral-sh/uv/pull/15897

---------

Co-authored-by: Zanie Blue <contact@zanie.dev>
This commit is contained in:
konsti 2025-09-17 13:18:41 +02:00 committed by GitHub
parent d805d4a370
commit eb5ec95396
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 107 additions and 91 deletions

View File

@ -64,6 +64,12 @@ pub enum ValidationError {
/// Check if the build backend is matching the currently running uv version. /// Check if the build backend is matching the currently running uv version.
pub fn check_direct_build(source_tree: &Path, name: impl Display) -> bool { pub fn check_direct_build(source_tree: &Path, name: impl Display) -> bool {
#[derive(Deserialize)]
#[serde(rename_all = "kebab-case")]
struct PyProjectToml {
build_system: BuildSystem,
}
let pyproject_toml: PyProjectToml = let pyproject_toml: PyProjectToml =
match fs_err::read_to_string(source_tree.join("pyproject.toml")) match fs_err::read_to_string(source_tree.join("pyproject.toml"))
.map_err(|err| err.to_string()) .map_err(|err| err.to_string())
@ -73,12 +79,14 @@ pub fn check_direct_build(source_tree: &Path, name: impl Display) -> bool {
Ok(pyproject_toml) => pyproject_toml, Ok(pyproject_toml) => pyproject_toml,
Err(err) => { Err(err) => {
debug!( debug!(
"Not using uv build backend direct build of {name}, no pyproject.toml: {err}" "Not using uv build backend direct build for source tree `{name}`, \
failed to parse pyproject.toml: {err}"
); );
return false; return false;
} }
}; };
match pyproject_toml match pyproject_toml
.build_system
.check_build_system(uv_version::version()) .check_build_system(uv_version::version())
.as_slice() .as_slice()
{ {
@ -87,10 +95,10 @@ pub fn check_direct_build(source_tree: &Path, name: impl Display) -> bool {
// Any warning -> no match // Any warning -> no match
[first, others @ ..] => { [first, others @ ..] => {
debug!( debug!(
"Not using uv build backend direct build of {name}, pyproject.toml does not match: {first}" "Not using uv build backend direct build of `{name}`, pyproject.toml does not match: {first}"
); );
for other in others { for other in others {
trace!("Further uv build backend direct build of {name} mismatch: {other}"); trace!("Further uv build backend direct build of `{name}` mismatch: {other}");
} }
false false
} }
@ -161,83 +169,9 @@ impl PyProjectToml {
self.tool.as_ref()?.uv.as_ref()?.build_backend.as_ref() self.tool.as_ref()?.uv.as_ref()?.build_backend.as_ref()
} }
/// Returns user-facing warnings if the `[build-system]` table looks suspicious. /// See [`BuildSystem::check_build_system`].
///
/// Example of a valid table:
///
/// ```toml
/// [build-system]
/// requires = ["uv_build>=0.4.15,<0.5.0"]
/// build-backend = "uv_build"
/// ```
pub fn check_build_system(&self, uv_version: &str) -> Vec<String> { pub fn check_build_system(&self, uv_version: &str) -> Vec<String> {
let mut warnings = Vec::new(); self.build_system.check_build_system(uv_version)
if self.build_system.build_backend.as_deref() != Some("uv_build") {
warnings.push(format!(
r#"The value for `build_system.build-backend` should be `"uv_build"`, not `"{}"`"#,
self.build_system.build_backend.clone().unwrap_or_default()
));
}
let uv_version =
Version::from_str(uv_version).expect("uv's own version is not PEP 440 compliant");
let next_minor = uv_version.release().get(1).copied().unwrap_or_default() + 1;
let next_breaking = Version::new([0, next_minor]);
let expected = || {
format!(
"Expected a single uv requirement in `build-system.requires`, found `{}`",
toml::to_string(&self.build_system.requires).unwrap_or_default()
)
};
let [uv_requirement] = &self.build_system.requires.as_slice() else {
warnings.push(expected());
return warnings;
};
if uv_requirement.name.as_str() != "uv-build" {
warnings.push(expected());
return warnings;
}
let bounded = match &uv_requirement.version_or_url {
None => false,
Some(VersionOrUrl::Url(_)) => {
// We can't validate the url
true
}
Some(VersionOrUrl::VersionSpecifier(specifier)) => {
// We don't check how wide the range is (that's up to the user), we just
// check that the current version is compliant, to avoid accidentally using a
// too new or too old uv, and we check that an upper bound exists. The latter
// is very important to allow making breaking changes in uv without breaking
// the existing immutable source distributions on pypi.
if !specifier.contains(&uv_version) {
// This is allowed to happen when testing prereleases, but we should still warn.
warnings.push(format!(
r#"`build_system.requires = ["{uv_requirement}"]` does not contain the
current uv version {uv_version}"#,
));
}
Ranges::from(specifier.clone())
.bounding_range()
.map(|bounding_range| bounding_range.1 != Bound::Unbounded)
.unwrap_or(false)
}
};
if !bounded {
warnings.push(format!(
"`build_system.requires = [\"{}\"]` is missing an \
upper bound on the `uv_build` version such as `<{next_breaking}`. \
Without bounding the `uv_build` version, the source distribution will break \
when a future, breaking version of `uv_build` is released.",
// Use an underscore consistently, to avoid confusing users between a package name with dash and a
// module name with underscore
uv_requirement.verbatim()
));
}
warnings
} }
/// Validate and convert a `pyproject.toml` to core metadata. /// Validate and convert a `pyproject.toml` to core metadata.
@ -782,18 +716,6 @@ pub(crate) enum Contact {
Email { email: String }, Email { email: String },
} }
/// The `[build-system]` section of a pyproject.toml as specified in PEP 517.
#[derive(Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
struct BuildSystem {
/// PEP 508 dependencies required to execute the build system.
requires: Vec<SerdeVerbatim<Requirement<VerbatimParsedUrl>>>,
/// A string naming a Python object that will be used to perform the build.
build_backend: Option<String>,
/// <https://peps.python.org/pep-0517/#in-tree-build-backends>
backend_path: Option<Vec<String>>,
}
/// The `tool` section as specified in PEP 517. /// The `tool` section as specified in PEP 517.
#[derive(Deserialize, Debug, Clone)] #[derive(Deserialize, Debug, Clone)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
@ -810,6 +732,100 @@ pub(crate) struct ToolUv {
build_backend: Option<BuildBackendSettings>, build_backend: Option<BuildBackendSettings>,
} }
/// The `[build-system]` section of a pyproject.toml as specified in PEP 517.
#[derive(Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
struct BuildSystem {
/// PEP 508 dependencies required to execute the build system.
requires: Vec<SerdeVerbatim<Requirement<VerbatimParsedUrl>>>,
/// A string naming a Python object that will be used to perform the build.
build_backend: Option<String>,
/// <https://peps.python.org/pep-0517/#in-tree-build-backends>
backend_path: Option<Vec<String>>,
}
impl BuildSystem {
/// Check if the `[build-system]` table matches the uv build backend expectations and return
/// a list of warnings if it looks suspicious.
///
/// Example of a valid table:
///
/// ```toml
/// [build-system]
/// requires = ["uv_build>=0.4.15,<0.5.0"]
/// build-backend = "uv_build"
/// ```
pub(crate) fn check_build_system(&self, uv_version: &str) -> Vec<String> {
let mut warnings = Vec::new();
if self.build_backend.as_deref() != Some("uv_build") {
warnings.push(format!(
r#"The value for `build_system.build-backend` should be `"uv_build"`, not `"{}"`"#,
self.build_backend.clone().unwrap_or_default()
));
}
let uv_version =
Version::from_str(uv_version).expect("uv's own version is not PEP 440 compliant");
let next_minor = uv_version.release().get(1).copied().unwrap_or_default() + 1;
let next_breaking = Version::new([0, next_minor]);
let expected = || {
format!(
"Expected a single uv requirement in `build-system.requires`, found `{}`",
toml::to_string(&self.requires).unwrap_or_default()
)
};
let [uv_requirement] = &self.requires.as_slice() else {
warnings.push(expected());
return warnings;
};
if uv_requirement.name.as_str() != "uv-build" {
warnings.push(expected());
return warnings;
}
let bounded = match &uv_requirement.version_or_url {
None => false,
Some(VersionOrUrl::Url(_)) => {
// We can't validate the url
true
}
Some(VersionOrUrl::VersionSpecifier(specifier)) => {
// We don't check how wide the range is (that's up to the user), we just
// check that the current version is compliant, to avoid accidentally using a
// too new or too old uv, and we check that an upper bound exists. The latter
// is very important to allow making breaking changes in uv without breaking
// the existing immutable source distributions on pypi.
if !specifier.contains(&uv_version) {
// This is allowed to happen when testing prereleases, but we should still warn.
warnings.push(format!(
r#"`build_system.requires = ["{uv_requirement}"]` does not contain the
current uv version {uv_version}"#,
));
}
Ranges::from(specifier.clone())
.bounding_range()
.map(|bounding_range| bounding_range.1 != Bound::Unbounded)
.unwrap_or(false)
}
};
if !bounded {
warnings.push(format!(
"`build_system.requires = [\"{}\"]` is missing an \
upper bound on the `uv_build` version such as `<{next_breaking}`. \
Without bounding the `uv_build` version, the source distribution will break \
when a future, breaking version of `uv_build` is released.",
// Use an underscore consistently, to avoid confusing users between a package name with dash and a
// module name with underscore
uv_requirement.verbatim()
));
}
warnings
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;