Validate PEP 517 backend paths (#19834)

## Summary

Prior to this change, we added `backend-path` entries to `sys.path`
without checking that they existed in the current source tree. When an
in-tree backend was omitted from an sdist, the subsequent wheel build
failed with `ModuleNotFoundError` and suggested that the backend was an
undeclared build dependency.

This validates each `backend-path` entry before resolving or invoking
the backend. Missing directories now produce a direct error explaining
that the configured path does not exist, matching the behavior of PyPA's
`build` frontend.

The regression test builds an sdist with a Flit wrapper in
`backend_dir`, intentionally omits that directory from the sdist, and
verifies the diagnostic during the wheel-from-sdist build.

Closes https://github.com/astral-sh/uv/issues/19771.
This commit is contained in:
Charlie Marsh
2026-06-13 14:54:18 -04:00
committed by GitHub
parent c462cc0b62
commit 75dccfb1bd
3 changed files with 55 additions and 0 deletions
+3
View File
@@ -69,6 +69,8 @@ pub enum 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("`backend-path` entry `{0}` does not exist or is not a directory")]
InvalidBackendPath(String),
#[error("Failed to resolve requirements from {0}")]
RequirementsResolve(&'static str, #[source] AnyErrorBuild),
#[error("Failed to install requirements from {0}")]
@@ -105,6 +107,7 @@ impl IsBuildBackendError for Error {
| Self::InvalidSourceDist(_)
| Self::InvalidPyprojectTomlSyntax(_)
| Self::InvalidPyprojectTomlSchema(_)
| Self::InvalidBackendPath(_)
| Self::RequirementsResolve(_, _)
| Self::RequirementsInstall(_, _)
| Self::Virtualenv(_)
+13
View File
@@ -610,6 +610,19 @@ impl SourceBuild {
.build_system
.as_ref()
.and_then(|build_system| build_system.build_backend.as_deref());
if let Some(backend_path) = pyproject_toml
.build_system
.as_ref()
.and_then(|build_system| build_system.backend_path.as_ref())
{
for path in backend_path.iter() {
if !source_tree.join(path).is_dir() {
return Err(Box::new(Error::InvalidBackendPath(path.to_string())));
}
}
}
// Only show the warning for first party and URL dependencies, not for registry dependencies
// (which have sources disabled).
if !no_sources.all()
+39
View File
@@ -148,6 +148,45 @@ fn build_basic() -> Result<()> {
Ok(())
}
/// A source distribution must include an in-tree build backend referenced by `backend-path`.
/// Regression test for <https://github.com/astral-sh/uv/issues/19771>.
#[test]
fn build_sdist_missing_backend_path() -> Result<()> {
let context = uv_test::test_context!("3.12");
let project = context.temp_dir.child("project");
project.child("pyproject.toml").write_str(indoc! {r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
description = "A test project"
[build-system]
requires = ["flit_core >=3.4,<4"]
build-backend = "my_backend:builder"
backend-path = ["backend_dir"]
"#})?;
project.child("src/project/__init__.py").touch()?;
project
.child("backend_dir/my_backend.py")
.write_str("from flit_core import buildapi\n\nbuilder = buildapi\n")?;
uv_snapshot!(context.filters(), context.build().arg(project.path()), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
Building source distribution...
Building wheel from source distribution...
× Failed to build `[TEMP_DIR]/project`
╰─▶ `backend-path` entry `backend_dir` does not exist or is not a directory
");
Ok(())
}
#[test]
fn build_sdist() -> Result<()> {
let context = uv_test::test_context!("3.12");