diff --git a/crates/uv-build-frontend/src/lib.rs b/crates/uv-build-frontend/src/lib.rs index 1ef439788..1a9adcafc 100644 --- a/crates/uv-build-frontend/src/lib.rs +++ b/crates/uv-build-frontend/src/lib.rs @@ -109,6 +109,8 @@ struct Tool { #[serde(rename_all = "kebab-case")] struct ToolUv { workspace: Option, + /// To warn users about ignored build backend settings. + build_backend: Option, } impl BackendPath { @@ -562,105 +564,12 @@ impl SourceBuild { workspace_cache: &WorkspaceCache, default_backend: &Pep517Backend, ) -> Result<(Pep517Backend, Option), Box> { - match fs::read_to_string(source_tree.join("pyproject.toml")) { + let pyproject_toml = match fs::read_to_string(source_tree.join("pyproject.toml")) { Ok(toml) => { let pyproject_toml = toml_edit::Document::from_str(&toml) .map_err(Error::InvalidPyprojectTomlSyntax)?; - let pyproject_toml = PyProjectToml::deserialize(pyproject_toml.into_deserializer()) - .map_err(Error::InvalidPyprojectTomlSchema)?; - - let backend = if let Some(build_system) = pyproject_toml.build_system { - // If necessary, lower the requirements. - let requirements = match source_strategy { - SourceStrategy::Enabled => { - if let Some(name) = pyproject_toml - .project - .as_ref() - .map(|project| &project.name) - .or(package_name) - { - let build_requires = uv_pypi_types::BuildRequires { - name: Some(name.clone()), - requires_dist: build_system.requires, - }; - let build_requires = BuildRequires::from_project_maybe_workspace( - build_requires, - install_path, - locations, - source_strategy, - workspace_cache, - ) - .await - .map_err(Error::Lowering)?; - build_requires.requires_dist - } else { - build_system - .requires - .into_iter() - .map(Requirement::from) - .collect() - } - } - SourceStrategy::Disabled => build_system - .requires - .into_iter() - .map(Requirement::from) - .collect(), - }; - - Pep517Backend { - // If `build-backend` is missing, inject the legacy setuptools backend, but - // retain the `requires`, to match `pip` and `build`. Note that while PEP 517 - // says that in this case we "should revert to the legacy behaviour of running - // `setup.py` (either directly, or by implicitly invoking the - // `setuptools.build_meta:__legacy__` backend)", we found that in practice, only - // the legacy setuptools backend is allowed. See also: - // https://github.com/pypa/build/blob/de5b44b0c28c598524832dff685a98d5a5148c44/src/build/__init__.py#L114-L118 - backend: build_system - .build_backend - .unwrap_or_else(|| "setuptools.build_meta:__legacy__".to_string()), - backend_path: build_system.backend_path, - requirements, - } - } else { - // If a `pyproject.toml` is present, but `[build-system]` is missing, proceed - // with a PEP 517 build using the default backend (`setuptools`), to match `pip` - // and `build`. - // - // If there is no build system defined and there is no metadata source for - // `setuptools`, warn. The build will succeed, but the metadata will be - // incomplete (for example, the package name will be `UNKNOWN`). - if pyproject_toml.project.is_none() - && !source_tree.join("setup.py").is_file() - && !source_tree.join("setup.cfg").is_file() - { - // Give a specific hint for `uv pip install .` in a workspace root. - let looks_like_workspace_root = pyproject_toml - .tool - .as_ref() - .and_then(|tool| tool.uv.as_ref()) - .and_then(|tool| tool.workspace.as_ref()) - .is_some(); - if looks_like_workspace_root { - warn_user_once!( - "`{}` appears to be a workspace root without a Python project; \ - consider using `uv sync` to install the workspace, or add a \ - `[build-system]` table to `pyproject.toml`", - source_tree.simplified_display().cyan(), - ); - } else { - warn_user_once!( - "`{}` does not appear to be a Python project, as the `pyproject.toml` \ - does not include a `[build-system]` table, and neither `setup.py` \ - nor `setup.cfg` are present in the directory", - source_tree.simplified_display().cyan(), - ); - } - } - - default_backend.clone() - }; - Ok((backend, pyproject_toml.project)) + PyProjectToml::deserialize(pyproject_toml.into_deserializer()) + .map_err(Error::InvalidPyprojectTomlSchema)? } Err(err) if err.kind() == io::ErrorKind::NotFound => { // We require either a `pyproject.toml` or a `setup.py` file at the top level. @@ -674,10 +583,123 @@ impl SourceBuild { // the default backend, to match `build`. `pip` uses `setup.py` directly in this // case, but plans to make PEP 517 builds the default in the future. // See: https://github.com/pypa/pip/issues/9175. - Ok((default_backend.clone(), None)) + return Ok((default_backend.clone(), None)); } - Err(err) => Err(Box::new(err.into())), + Err(err) => return Err(Box::new(err.into())), + }; + + if source_strategy == SourceStrategy::Enabled + && pyproject_toml + .tool + .as_ref() + .and_then(|tool| tool.uv.as_ref()) + .map(|uv| uv.build_backend.is_some()) + .unwrap_or(false) + && pyproject_toml + .build_system + .as_ref() + .and_then(|build_backend| build_backend.build_backend.as_deref()) + != Some("uv_build") + { + warn_user_once!( + "There are settings for `uv_build` defined in \ + `tool.uv.build-backend`, but `uv_build` is not used by the project at: {}", + source_tree.join("pyproject.toml").simplified_display() + ); } + + let backend = if let Some(build_system) = pyproject_toml.build_system { + // If necessary, lower the requirements. + let requirements = match source_strategy { + SourceStrategy::Enabled => { + if let Some(name) = pyproject_toml + .project + .as_ref() + .map(|project| &project.name) + .or(package_name) + { + let build_requires = uv_pypi_types::BuildRequires { + name: Some(name.clone()), + requires_dist: build_system.requires, + }; + let build_requires = BuildRequires::from_project_maybe_workspace( + build_requires, + install_path, + locations, + source_strategy, + workspace_cache, + ) + .await + .map_err(Error::Lowering)?; + build_requires.requires_dist + } else { + build_system + .requires + .into_iter() + .map(Requirement::from) + .collect() + } + } + SourceStrategy::Disabled => build_system + .requires + .into_iter() + .map(Requirement::from) + .collect(), + }; + + Pep517Backend { + // If `build-backend` is missing, inject the legacy setuptools backend, but + // retain the `requires`, to match `pip` and `build`. Note that while PEP 517 + // says that in this case we "should revert to the legacy behaviour of running + // `setup.py` (either directly, or by implicitly invoking the + // `setuptools.build_meta:__legacy__` backend)", we found that in practice, only + // the legacy setuptools backend is allowed. See also: + // https://github.com/pypa/build/blob/de5b44b0c28c598524832dff685a98d5a5148c44/src/build/__init__.py#L114-L118 + backend: build_system + .build_backend + .unwrap_or_else(|| "setuptools.build_meta:__legacy__".to_string()), + backend_path: build_system.backend_path, + requirements, + } + } else { + // If a `pyproject.toml` is present, but `[build-system]` is missing, proceed + // with a PEP 517 build using the default backend (`setuptools`), to match `pip` + // and `build`. + // + // If there is no build system defined and there is no metadata source for + // `setuptools`, warn. The build will succeed, but the metadata will be + // incomplete (for example, the package name will be `UNKNOWN`). + if pyproject_toml.project.is_none() + && !source_tree.join("setup.py").is_file() + && !source_tree.join("setup.cfg").is_file() + { + // Give a specific hint for `uv pip install .` in a workspace root. + let looks_like_workspace_root = pyproject_toml + .tool + .as_ref() + .and_then(|tool| tool.uv.as_ref()) + .and_then(|tool| tool.workspace.as_ref()) + .is_some(); + if looks_like_workspace_root { + warn_user_once!( + "`{}` appears to be a workspace root without a Python project; \ + consider using `uv sync` to install the workspace, or add a \ + `[build-system]` table to `pyproject.toml`", + source_tree.simplified_display().cyan(), + ); + } else { + warn_user_once!( + "`{}` does not appear to be a Python project, as the `pyproject.toml` \ + does not include a `[build-system]` table, and neither `setup.py` \ + nor `setup.cfg` are present in the directory", + source_tree.simplified_display().cyan(), + ); + } + } + + default_backend.clone() + }; + Ok((backend, pyproject_toml.project)) } /// Try calling `prepare_metadata_for_build_wheel` to get the metadata without executing the diff --git a/crates/uv/tests/it/build_backend.rs b/crates/uv/tests/it/build_backend.rs index 73d3a6882..2c37ff6cc 100644 --- a/crates/uv/tests/it/build_backend.rs +++ b/crates/uv/tests/it/build_backend.rs @@ -1029,3 +1029,52 @@ fn venv_in_source_tree() { ╰─▶ Virtual environments must not be added to source distributions or wheels, remove the directory or exclude it from the build: src/foo/.venv "); } + +/// Warn for cases where `tool.uv.build-backend` is used without the corresponding build backend +/// entry. +#[test] +fn tool_uv_build_backend_without_build_backend() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(indoc! {r#" + [project] + name = "project" + version = "0.1.0" + + [tool.uv] + package = true + + [tool.uv.build-backend.data] + data = "assets" + "#})?; + + uv_snapshot!(context.filters(), context.build().arg("--no-build-logs"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Building source distribution... + warning: There are settings for `uv_build` defined in `tool.uv.build-backend`, but `uv_build` is not used by the project at: [TEMP_DIR]/pyproject.toml + Building wheel from source distribution... + warning: There are settings for `uv_build` defined in `tool.uv.build-backend`, but `uv_build` is not used by the project at: [CACHE_DIR]/sdists-v9/[TMP]/pyproject.toml + Successfully built dist/project-0.1.0.tar.gz + Successfully built dist/project-0.1.0-py3-none-any.whl + "); + + uv_snapshot!(context.filters(), context.pip_install().arg("."), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + warning: There are settings for `uv_build` defined in `tool.uv.build-backend`, but `uv_build` is not used by the project at: [TEMP_DIR]/pyproject.toml + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + project==0.1.0 (from file://[TEMP_DIR]/) + "); + + Ok(()) +}