Skip to content

Commit 23494d8

Browse files
authored
Warn when trying to uv sync a package without build configuration (#7420)
This enhances `uv sync` logic in order to detect and warn if it is trying to operate on a packaged project with entrypoints. Ref: #6998 (comment) Closes: #7034
1 parent d4f4ded commit 23494d8

File tree

4 files changed

+160
-2
lines changed

4 files changed

+160
-2
lines changed

crates/uv-workspace/src/pyproject.rs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ pub struct PyProjectToml {
4444
#[serde(skip)]
4545
pub raw: String,
4646

47-
/// Used to determine whether a `build-system` is present.
47+
/// Used to determine whether a `build-system` section is present.
4848
#[serde(default, skip_serializing)]
4949
build_system: Option<serde::de::IgnoredAny>,
5050
}
@@ -82,6 +82,15 @@ impl PyProjectToml {
8282
// Otherwise, a project is assumed to be a package if `build-system` is present.
8383
self.build_system.is_some()
8484
}
85+
86+
/// Returns whether the project manifest contains any script table.
87+
pub fn has_scripts(&self) -> bool {
88+
if let Some(ref project) = self.project {
89+
project.gui_scripts.is_some() || project.scripts.is_some()
90+
} else {
91+
false
92+
}
93+
}
8594
}
8695

8796
// Ignore raw document in comparison.
@@ -102,7 +111,7 @@ impl AsRef<[u8]> for PyProjectToml {
102111
/// PEP 621 project metadata (`project`).
103112
///
104113
/// See <https://packaging.python.org/en/latest/specifications/pyproject-toml>.
105-
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
114+
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
106115
#[serde(rename_all = "kebab-case")]
107116
pub struct Project {
108117
/// The name of the project
@@ -113,6 +122,13 @@ pub struct Project {
113122
pub requires_python: Option<VersionSpecifiers>,
114123
/// The optional dependencies of the project.
115124
pub optional_dependencies: Option<BTreeMap<ExtraName, Vec<String>>>,
125+
126+
/// Used to determine whether a `gui-scripts` section is present.
127+
#[serde(default, skip_serializing)]
128+
pub(crate) gui_scripts: Option<serde::de::IgnoredAny>,
129+
/// Used to determine whether a `scripts` section is present.
130+
#[serde(default, skip_serializing)]
131+
pub(crate) scripts: Option<serde::de::IgnoredAny>,
116132
}
117133

118134
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]

crates/uv/src/commands/project/sync.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ use uv_normalize::{PackageName, DEV_DEPENDENCIES};
1616
use uv_python::{PythonDownloads, PythonEnvironment, PythonPreference, PythonRequest};
1717
use uv_resolver::{FlatIndex, Lock};
1818
use uv_types::{BuildIsolation, HashStrategy};
19+
use uv_warnings::warn_user;
1920
use uv_workspace::{DiscoveryOptions, InstallTarget, MemberDiscovery, VirtualProject, Workspace};
2021

2122
use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger, InstallLogger};
@@ -74,6 +75,14 @@ pub(crate) async fn sync(
7475
InstallTarget::from(&project)
7576
};
7677

78+
// TODO(lucab): improve warning content
79+
// <https://github.com/astral-sh/uv/issues/7428>
80+
if project.workspace().pyproject_toml().has_scripts()
81+
&& !project.workspace().pyproject_toml().is_package()
82+
{
83+
warn_user!("Skipping installation of entry points (`project.scripts`) because this project is not packaged; to install entry points, set `tool.uv.package = true` or define a `build-system`");
84+
}
85+
7786
// Discover or create the virtual environment.
7887
let venv = project::get_or_init_environment(
7988
target.workspace(),

crates/uv/tests/run.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1950,3 +1950,45 @@ fn run_invalid_project_table() -> Result<()> {
19501950

19511951
Ok(())
19521952
}
1953+
1954+
#[test]
1955+
#[cfg(target_family = "unix")]
1956+
fn run_script_without_build_system() -> Result<()> {
1957+
let context = TestContext::new("3.12");
1958+
1959+
let pyproject_toml = context.temp_dir.child("pyproject.toml");
1960+
pyproject_toml.write_str(indoc! { r#"
1961+
[project]
1962+
name = "foo"
1963+
version = "0.1.0"
1964+
requires-python = ">=3.12"
1965+
dependencies = []
1966+
1967+
[project.scripts]
1968+
entry = "foo:custom_entry"
1969+
"#
1970+
})?;
1971+
1972+
let test_script = context.temp_dir.child("src/__init__.py");
1973+
test_script.write_str(indoc! { r#"
1974+
def custom_entry():
1975+
print!("Hello")
1976+
"#
1977+
})?;
1978+
1979+
// TODO(lucab): this should match `entry` and warn
1980+
// <https://github.com/astral-sh/uv/issues/7428>
1981+
uv_snapshot!(context.filters(), context.run().arg("entry"), @r###"
1982+
success: false
1983+
exit_code: 2
1984+
----- stdout -----
1985+
1986+
----- stderr -----
1987+
Resolved 1 package in [TIME]
1988+
Audited in [TIME]
1989+
error: Failed to spawn: `entry`
1990+
Caused by: No such file or directory (os error 2)
1991+
"###);
1992+
1993+
Ok(())
1994+
}

crates/uv/tests/sync.rs

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2328,3 +2328,94 @@ fn transitive_dev() -> Result<()> {
23282328

23292329
Ok(())
23302330
}
2331+
2332+
#[test]
2333+
/// Check warning message for <https://github.com/astral-sh/uv/issues/6998>
2334+
/// if no `build-system` section is defined.
2335+
fn sync_scripts_without_build_system() -> Result<()> {
2336+
let context = TestContext::new("3.12");
2337+
2338+
let pyproject_toml = context.temp_dir.child("pyproject.toml");
2339+
pyproject_toml.write_str(
2340+
r#"
2341+
[project]
2342+
name = "foo"
2343+
version = "0.1.0"
2344+
requires-python = ">=3.12"
2345+
dependencies = []
2346+
2347+
[project.scripts]
2348+
entry = "foo:custom_entry"
2349+
"#,
2350+
)?;
2351+
2352+
let test_script = context.temp_dir.child("src/__init__.py");
2353+
test_script.write_str(
2354+
r#"
2355+
def custom_entry():
2356+
print!("Hello")
2357+
"#,
2358+
)?;
2359+
2360+
uv_snapshot!(context.filters(), context.sync(), @r###"
2361+
success: true
2362+
exit_code: 0
2363+
----- stdout -----
2364+
2365+
----- stderr -----
2366+
warning: Skipping installation of entry points (`project.scripts`) because this project is not packaged; to install entry points, set `tool.uv.package = true` or define a `build-system`
2367+
Resolved 1 package in [TIME]
2368+
Audited in [TIME]
2369+
"###);
2370+
2371+
Ok(())
2372+
}
2373+
2374+
#[test]
2375+
/// Check warning message for <https://github.com/astral-sh/uv/issues/6998>
2376+
/// if the project is marked as `package = false`.
2377+
fn sync_scripts_project_not_packaged() -> Result<()> {
2378+
let context = TestContext::new("3.12");
2379+
2380+
let pyproject_toml = context.temp_dir.child("pyproject.toml");
2381+
pyproject_toml.write_str(
2382+
r#"
2383+
[project]
2384+
name = "foo"
2385+
version = "0.1.0"
2386+
requires-python = ">=3.12"
2387+
dependencies = []
2388+
2389+
[project.scripts]
2390+
entry = "foo:custom_entry"
2391+
2392+
[build-system]
2393+
requires = ["hatchling"]
2394+
build-backend = "hatchling.build"
2395+
2396+
[tool.uv]
2397+
package = false
2398+
"#,
2399+
)?;
2400+
2401+
let test_script = context.temp_dir.child("src/__init__.py");
2402+
test_script.write_str(
2403+
r#"
2404+
def custom_entry():
2405+
print!("Hello")
2406+
"#,
2407+
)?;
2408+
2409+
uv_snapshot!(context.filters(), context.sync(), @r###"
2410+
success: true
2411+
exit_code: 0
2412+
----- stdout -----
2413+
2414+
----- stderr -----
2415+
warning: Skipping installation of entry points (`project.scripts`) because this project is not packaged; to install entry points, set `tool.uv.package = true` or define a `build-system`
2416+
Resolved 1 package in [TIME]
2417+
Audited in [TIME]
2418+
"###);
2419+
2420+
Ok(())
2421+
}

0 commit comments

Comments
 (0)