diff --git a/crates/uv-build-frontend/src/error.rs b/crates/uv-build-frontend/src/error.rs index 93b916c99c..f8231455d3 100644 --- a/crates/uv-build-frontend/src/error.rs +++ b/crates/uv-build-frontend/src/error.rs @@ -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(_) diff --git a/crates/uv-build-frontend/src/lib.rs b/crates/uv-build-frontend/src/lib.rs index 443b661a21..bf6f50bf4f 100644 --- a/crates/uv-build-frontend/src/lib.rs +++ b/crates/uv-build-frontend/src/lib.rs @@ -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() diff --git a/crates/uv/tests/build/build.rs b/crates/uv/tests/build/build.rs index bb957a8432..d0bfc99eb0 100644 --- a/crates/uv/tests/build/build.rs +++ b/crates/uv/tests/build/build.rs @@ -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 . +#[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");