Warn when using uv_build settings without uv_build (#15750)

To help with cases such as https://github.com/astral-sh/uv/issues/15655.

A question is when to show this warning. I've used sources as a proxy as
URL dependencies with enabled sources are likely those controlled by the
user, and they include workspace and `git clone`d path dependencies.

The first commit is a refactoring, the second commit the implementation.

Fixes https://github.com/astral-sh/uv/issues/15740
This commit is contained in:
konsti
2026-03-05 13:56:35 +01:00
committed by GitHub
parent 8efb5421bc
commit 87b2539e80
2 changed files with 244 additions and 92 deletions
+127 -92
View File
@@ -107,6 +107,8 @@ struct Tool {
#[serde(rename_all = "kebab-case")]
struct ToolUv {
workspace: Option<de::IgnoredAny>,
/// To warn users about ignored build backend settings.
build_backend: Option<de::IgnoredAny>,
}
impl BackendPath {
@@ -572,99 +574,12 @@ impl SourceBuild {
workspace_cache: &WorkspaceCache,
credentials_cache: &CredentialsCache,
) -> Result<(Pep517Backend, Option<Project>), Box<Error>> {
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 = if let Some(name) = pyproject_toml
.project
.as_ref()
.map(|project| &project.name)
.or(package_name)
// If sources are disabled, there's nothing to do here
.filter(|_| !no_sources.all())
{
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,
no_sources,
workspace_cache,
credentials_cache,
)
.await
.map_err(Error::Lowering)?;
build_requires.requires_dist
} else {
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.
@@ -678,10 +593,130 @@ 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) => return Err(Box::new(err.into())),
};
let build_backend = pyproject_toml
.build_system
.as_ref()
.and_then(|build_system| build_system.build_backend.as_deref());
// Only show the warning for first party and URL dependencies, not for registry dependencies
// (which have sources disabled).
if !no_sources.all()
&& pyproject_toml
.tool
.as_ref()
.and_then(|tool| tool.uv.as_ref())
.map(|uv| uv.build_backend.is_some())
.unwrap_or(false)
&& build_backend != Some("uv_build")
&& let Some(package_name) =
package_name.or(pyproject_toml.project.as_ref().map(|project| &project.name))
{
// Show only the name, but not the path to `pyproject.toml`, to avoid showing a
// (duplicate) warning that contains the temporary path of an unpacked source
// distribution in a source tree -> source dist -> wheel build.
if let Some(build_backend) = build_backend {
warn_user_once!(
"`{package_name}` defines settings for `uv_build` in `tool.uv.build-backend`, \
but uses `{build_backend}` as build backend instead",
);
} else {
warn_user_once!(
"`{package_name}` defines settings for `uv_build` in `tool.uv.build-backend`, \
but the `build-system` table is missing",
);
}
Err(err) => Err(Box::new(err.into())),
}
let backend = if let Some(build_system) = pyproject_toml.build_system {
// If necessary, lower the requirements.
let requirements = if let Some(name) = pyproject_toml
.project
.as_ref()
.map(|project| &project.name)
.or(package_name)
// If sources are disabled, there's nothing to do here
.filter(|_| !no_sources.all())
{
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,
no_sources,
workspace_cache,
credentials_cache,
)
.await
.map_err(Error::Lowering)?;
build_requires.requires_dist
} else {
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
+117
View File
@@ -1445,3 +1445,120 @@ fn build_with_all_metadata() -> Result<()> {
Ok(())
}
/// 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 = uv_test::test_context!("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: `project` defines settings for `uv_build` in `tool.uv.build-backend`, but the `build-system` table is missing
Building wheel from source distribution...
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: `project` defines settings for `uv_build` in `tool.uv.build-backend`, but the `build-system` table is missing
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ project==0.1.0 (from file://[TEMP_DIR]/)
");
// Ensure that the warning isn't shown for registry dependencies.
uv_snapshot!(context.filters(), context.pip_install().arg("--find-links").arg("dist").arg("--reinstall").arg("project"), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 1 package in [TIME]
Prepared 1 package in [TIME]
Uninstalled 1 package in [TIME]
Installed 1 package in [TIME]
- project==0.1.0 (from file://[TEMP_DIR]/)
+ project==0.1.0
");
Ok(())
}
/// Warn for cases where `tool.uv.build-backend` is used without the corresponding build backend
/// entry.
#[test]
fn tool_uv_build_backend_wrong_build_backend() -> Result<()> {
let context = uv_test::test_context!("3.12");
let project = context.temp_dir.child("project");
let pyproject_toml = project.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"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
"#})?;
project.child("src/project/__init__.py").touch()?;
uv_snapshot!(context.filters(), context.build().arg("--no-build-logs").arg(project.path()), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Building source distribution...
warning: `project` defines settings for `uv_build` in `tool.uv.build-backend`, but uses `hatchling.build` as build backend instead
Building wheel from source distribution...
Successfully built project/dist/project-0.1.0.tar.gz
Successfully built project/dist/project-0.1.0-py2.py3-none-any.whl
");
uv_snapshot!(context.filters(), context.pip_install().arg(project.path()), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 1 package in [TIME]
warning: `project` defines settings for `uv_build` in `tool.uv.build-backend`, but uses `hatchling.build` as build backend instead
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ project==0.1.0 (from file://[TEMP_DIR]/project)
");
Ok(())
}