Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Warn for builds in non-build and workspace root pyproject.toml #11394

Merged
merged 3 commits into from
Feb 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
70 changes: 59 additions & 11 deletions crates/uv-build-frontend/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,6 @@

mod error;

use fs_err as fs;
use indoc::formatdoc;
use itertools::Itertools;
use rustc_hash::FxHashMap;
use serde::de::{value, IntoDeserializer, SeqAccess, Visitor};
use serde::{de, Deserialize, Deserializer};
use std::ffi::OsString;
use std::fmt::Formatter;
use std::fmt::Write;
Expand All @@ -20,6 +14,13 @@ use std::rc::Rc;
use std::str::FromStr;
use std::sync::LazyLock;
use std::{env, iter};

use fs_err as fs;
use indoc::formatdoc;
use itertools::Itertools;
use rustc_hash::FxHashMap;
use serde::de::{value, IntoDeserializer, SeqAccess, Visitor};
use serde::{de, Deserialize, Deserializer};
use tempfile::TempDir;
use tokio::io::AsyncBufReadExt;
use tokio::process::Command;
Expand All @@ -36,6 +37,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 @@ -49,20 +51,22 @@ static DEFAULT_BACKEND: LazyLock<Pep517Backend> = LazyLock::new(|| Pep517Backend
});

/// A `pyproject.toml` as specified in PEP 517.
#[derive(Deserialize, Debug, Clone, PartialEq, Eq)]
#[derive(Deserialize, Debug)]
#[serde(rename_all = "kebab-case")]
struct PyProjectToml {
/// Build-related data
build_system: Option<BuildSystem>,
/// Project metadata
project: Option<Project>,
/// Tool configuration
tool: Option<Tool>,
}

/// The `[project]` section of a pyproject.toml as specified in PEP 621.
///
/// This representation only includes a subset of the fields defined in PEP 621 necessary for
/// informing wheel builds.
#[derive(Deserialize, Debug, Clone, PartialEq, Eq)]
#[derive(Deserialize, Debug)]
#[serde(rename_all = "kebab-case")]
struct Project {
/// The name of the project
Expand All @@ -75,7 +79,7 @@ struct Project {
}

/// The `[build-system]` section of a pyproject.toml as specified in PEP 517.
#[derive(Deserialize, Debug, Clone, PartialEq, Eq)]
#[derive(Deserialize, Debug)]
#[serde(rename_all = "kebab-case")]
struct BuildSystem {
/// PEP 508 dependencies required to execute the build system.
Expand All @@ -86,6 +90,18 @@ struct BuildSystem {
backend_path: Option<BackendPath>,
}

#[derive(Deserialize, Debug)]
#[serde(rename_all = "kebab-case")]
struct Tool {
uv: Option<ToolUv>,
}

#[derive(Deserialize, Debug)]
#[serde(rename_all = "kebab-case")]
struct ToolUv {
workspace: Option<de::IgnoredAny>,
}

impl BackendPath {
/// Return an iterator over the paths in the backend path.
fn iter(&self) -> impl Iterator<Item = &str> {
Expand Down Expand Up @@ -514,8 +530,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` 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))
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 fallback 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` to install the workspace, 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 the `pyproject.toml` does not include a `[build-system]` table, and neither `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
Loading