diff --git a/crates/uv-workspace/src/pyproject.rs b/crates/uv-workspace/src/pyproject.rs index dc0609caa63b..af1a5717fd04 100644 --- a/crates/uv-workspace/src/pyproject.rs +++ b/crates/uv-workspace/src/pyproject.rs @@ -44,7 +44,7 @@ pub struct PyProjectToml { #[serde(skip)] pub raw: String, - /// Used to determine whether a `build-system` is present. + /// Used to determine whether a `build-system` section is present. #[serde(default, skip_serializing)] build_system: Option, } @@ -82,6 +82,15 @@ impl PyProjectToml { // Otherwise, a project is assumed to be a package if `build-system` is present. self.build_system.is_some() } + + /// Returns whether the project manifest contains any script table. + pub fn has_scripts(&self) -> bool { + if let Some(ref project) = self.project { + project.gui_scripts.is_some() || project.scripts.is_some() + } else { + false + } + } } // Ignore raw document in comparison. @@ -102,7 +111,7 @@ impl AsRef<[u8]> for PyProjectToml { /// PEP 621 project metadata (`project`). /// /// See . -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[serde(rename_all = "kebab-case")] pub struct Project { /// The name of the project @@ -113,6 +122,13 @@ pub struct Project { pub requires_python: Option, /// The optional dependencies of the project. pub optional_dependencies: Option>>, + + /// Used to determine whether a `gui-scripts` section is present. + #[serde(default, skip_serializing)] + pub(crate) gui_scripts: Option, + /// Used to determine whether a `scripts` section is present. + #[serde(default, skip_serializing)] + pub(crate) scripts: Option, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index 31ed1c1a88e8..ce2449494d19 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -16,6 +16,7 @@ use uv_normalize::{PackageName, DEV_DEPENDENCIES}; use uv_python::{PythonDownloads, PythonEnvironment, PythonPreference, PythonRequest}; use uv_resolver::{FlatIndex, Lock}; use uv_types::{BuildIsolation, HashStrategy}; +use uv_warnings::warn_user; use uv_workspace::{DiscoveryOptions, InstallTarget, MemberDiscovery, VirtualProject, Workspace}; use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger, InstallLogger}; @@ -74,6 +75,14 @@ pub(crate) async fn sync( InstallTarget::from(&project) }; + // TODO(lucab): improve warning content + // + if project.workspace().pyproject_toml().has_scripts() + && !project.workspace().pyproject_toml().is_package() + { + 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`"); + } + // Discover or create the virtual environment. let venv = project::get_or_init_environment( target.workspace(), diff --git a/crates/uv/tests/run.rs b/crates/uv/tests/run.rs index c88749b4b539..c84d92b159db 100644 --- a/crates/uv/tests/run.rs +++ b/crates/uv/tests/run.rs @@ -1950,3 +1950,45 @@ fn run_invalid_project_table() -> Result<()> { Ok(()) } + +#[test] +#[cfg(target_family = "unix")] +fn run_script_without_build_system() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(indoc! { r#" + [project] + name = "foo" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [] + + [project.scripts] + entry = "foo:custom_entry" + "# + })?; + + let test_script = context.temp_dir.child("src/__init__.py"); + test_script.write_str(indoc! { r#" + def custom_entry(): + print!("Hello") + "# + })?; + + // TODO(lucab): this should match `entry` and warn + // + uv_snapshot!(context.filters(), context.run().arg("entry"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Audited in [TIME] + error: Failed to spawn: `entry` + Caused by: No such file or directory (os error 2) + "###); + + Ok(()) +} diff --git a/crates/uv/tests/sync.rs b/crates/uv/tests/sync.rs index bff1a3def208..57bcc1fcabb5 100644 --- a/crates/uv/tests/sync.rs +++ b/crates/uv/tests/sync.rs @@ -2328,3 +2328,94 @@ fn transitive_dev() -> Result<()> { Ok(()) } + +#[test] +/// Check warning message for +/// if no `build-system` section is defined. +fn sync_scripts_without_build_system() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "foo" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [] + + [project.scripts] + entry = "foo:custom_entry" + "#, + )?; + + let test_script = context.temp_dir.child("src/__init__.py"); + test_script.write_str( + r#" + def custom_entry(): + print!("Hello") + "#, + )?; + + uv_snapshot!(context.filters(), context.sync(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + 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` + Resolved 1 package in [TIME] + Audited in [TIME] + "###); + + Ok(()) +} + +#[test] +/// Check warning message for +/// if the project is marked as `package = false`. +fn sync_scripts_project_not_packaged() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "foo" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [] + + [project.scripts] + entry = "foo:custom_entry" + + [build-system] + requires = ["hatchling"] + build-backend = "hatchling.build" + + [tool.uv] + package = false + "#, + )?; + + let test_script = context.temp_dir.child("src/__init__.py"); + test_script.write_str( + r#" + def custom_entry(): + print!("Hello") + "#, + )?; + + uv_snapshot!(context.filters(), context.sync(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + 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` + Resolved 1 package in [TIME] + Audited in [TIME] + "###); + + Ok(()) +}