Skip to content

Commit

Permalink
Warn for builds in non-build and workspace root pyproject.toml
Browse files Browse the repository at this point in the history
When running `uv pip install .` in a directory with a pyproject.toml that does not configure a build, we will invoke setuptools and get a wheel we can't parse (#11344). This PR adds warnings around these setups.
  • Loading branch information
konstin committed Feb 17, 2025
1 parent 2352e74 commit c7281ea
Show file tree
Hide file tree
Showing 5 changed files with 146 additions and 2 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/uv-build-frontend/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ uv-python = { workspace = true }
uv-static = { workspace = true }
uv-types = { workspace = true }
uv-virtualenv = { workspace = true }
uv-warnings = { workspace = true }

anstream = { workspace = true }
fs-err = { workspace = true }
Expand Down
41 changes: 39 additions & 2 deletions crates/uv-build-frontend/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ use uv_pypi_types::{Requirement, VerbatimParsedUrl};
use uv_python::{Interpreter, PythonEnvironment};
use uv_static::EnvVars;
use uv_types::{AnyErrorBuild, BuildContext, BuildIsolation, BuildStack, SourceBuildTrait};
use uv_warnings::warn_user_once;

pub use crate::error::{Error, MissingHeaderCause};

Expand All @@ -56,6 +57,10 @@ struct PyProjectToml {
build_system: Option<BuildSystem>,
/// Project metadata
project: Option<Project>,
/// Tool configuration
///
/// Only read for diagnostics.
tool: Option<serde_json::Value>,
}

/// The `[project]` section of a pyproject.toml as specified in PEP 621.
Expand Down Expand Up @@ -514,8 +519,40 @@ impl SourceBuild {
requirements,
}
} else {
// If a `pyproject.toml` is present, but `[build-system]` is missing, proceed with
// a PEP 517 build using the default backend, to match `pip` and `build`.
// If a `pyproject.toml` and a `setup.py` are 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, fail the build instead of building a
// wheel with UNKNOWN package name through setuptools.
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.as_object())
.and_then(|tool| tool.get("uv"))
.and_then(|uv| uv.as_object())
.and_then(|uv| uv.get("workspace"))
.is_some();
if looks_like_workspace_root {
warn_user_once!(
"`{}` appears to be a workspace root without a Python project, \
consider using `uv sync` or installing workspace members \
individually, or add a `[build-system]` table to `pyproject.toml`.",
source_tree.to_path_buf().simplified_display(),
);
} else {
warn_user_once!(
"`{}` does not appear to be a Python project, as neither \
`pyproject.toml` with a `[build-system]` table, \
nor `setup.py`, nor `setup.cfg` are present in the directory",
source_tree.to_path_buf().simplified_display(),
);
};
}
default_backend.clone()
};
Ok((backend, pyproject_toml.project))
Expand Down
101 changes: 101 additions & 0 deletions crates/uv/tests/it/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use indoc::indoc;
use insta::assert_snapshot;
use predicates::prelude::predicate;
use std::env::current_dir;
use std::process::Command;
use zip::ZipArchive;

#[test]
Expand Down Expand Up @@ -1823,3 +1824,103 @@ fn build_with_hardlink() -> Result<()> {
"###);
Ok(())
}

/// This is bad project layout that is allowed: A project that defines PEP 621 metadata, but no
/// PEP 517 build system not a setup.py, so we fall back to setuptools implicitly.
#[test]
fn build_unconfigured_setuptools() -> Result<()> {
let context = TestContext::new("3.12");
context
.temp_dir
.child("pyproject.toml")
.write_str(indoc! {r#"
[project]
name = "greet"
version = "0.1.0"
"#})?;
context
.temp_dir
.child("src/greet/__init__.py")
.write_str("print('Greetings!')")?;

// This is not technically a `uv build` test, we use it to contrast this passing case with the
// failing cases later.
uv_snapshot!(context.filters(), context.pip_install().arg("."), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 1 package in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ greet==0.1.0 (from file://[TEMP_DIR]/)
"###);

uv_snapshot!(context.filters(), Command::new(context.interpreter()).arg("-c").arg("import greet"), @r###"
success: true
exit_code: 0
----- stdout -----
Greetings!
----- stderr -----
"###);
Ok(())
}

/// In a project layout with a virtual root, an easy mistake to make is running `uv pip install .`
/// in the root.
#[test]
fn build_workspace_virtual_root() -> Result<()> {
let context = TestContext::new("3.12");
context
.temp_dir
.child("pyproject.toml")
.write_str(indoc! {r#"
[tool.uv.workspace]
members = ["packages/*"]
"#})?;

uv_snapshot!(context.filters(), context.build().arg("--no-build-logs"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Building source distribution...
warning: `[TEMP_DIR]/` appears to be a workspace root without a Python project, consider using `uv sync` or installing workspace members individually, or add a `[build-system]` table to `pyproject.toml`.
Building wheel from source distribution...
Successfully built dist/cache-0.0.0.tar.gz
Successfully built dist/unknown-0.0.0-py3-none-any.whl
"###);
Ok(())
}

/// There is a `pyproject.toml`, but it does not define any build information nor is there a
/// `setup.{py,cfg}`.
#[test]
fn build_pyproject_toml_not_a_project() -> Result<()> {
let context = TestContext::new("3.12");
context
.temp_dir
.child("pyproject.toml")
.write_str(indoc! {"
# Some other content we don't know about
[tool.black]
line-length = 88
"})?;

uv_snapshot!(context.filters(), context.build().arg("--no-build-logs"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Building source distribution...
warning: `[TEMP_DIR]/` does not appear to be a Python project, as neither `pyproject.toml` with a `[build-system]` table, nor `setup.py`, nor `setup.cfg` are present in the directory
Building wheel from source distribution...
Successfully built dist/cache-0.0.0.tar.gz
Successfully built dist/unknown-0.0.0-py3-none-any.whl
"###);
Ok(())
}
4 changes: 4 additions & 0 deletions crates/uv/tests/it/pip_install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,10 @@ fn invalid_pyproject_toml_option_unknown_field() -> Result<()> {
pyproject_toml.write_str(indoc! {r#"
[tool.uv]
unknown = "field"
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"
"#})?;

let mut filters = context.filters();
Expand Down

0 comments on commit c7281ea

Please sign in to comment.