From 39f79b3e56eaa8888b8bd2c0e958fe944d7217a9 Mon Sep 17 00:00:00 2001 From: ChrisJr404 Date: Fri, 5 Jun 2026 20:40:07 -0400 Subject: [PATCH] Recognize `UV_NO_INSTALL_PROJECT`, `UV_NO_INSTALL_WORKSPACE`, `UV_NO_INSTALL_LOCAL` (#19323) ## Summary Adds environment variable support for `--no-install-project`, `--no-install-workspace`, and `--no-install-local` so the flags can be set when uv is invoked through a wrapper that doesn't expose flag pass-through (Pulumi's `pulumi install` is the example in the issue). The new env vars (`UV_NO_INSTALL_PROJECT`, `UV_NO_INSTALL_WORKSPACE`, `UV_NO_INSTALL_LOCAL`) are wired into both `uv sync` and `uv add`, and apply alongside the CLI flag. If the corresponding `--only-install-*` is passed on the CLI, the env var is ignored to keep the existing CLI conflict semantics intact. Closes #19315. ## Test Plan Added integration tests for each of the three flags in `crates/uv/tests/it/sync.rs`, including a check that `--only-install-project` overrides `UV_NO_INSTALL_PROJECT=1`. Also verified manually: ``` $ UV_NO_INSTALL_PROJECT=1 uv sync Installed 1 package in 4ms + packaging==26.2 $ uv sync Installed 2 packages in 5ms + demo==0.1.0 (from file:///tmp/demo) + packaging==26.2 ``` --- clippy.toml | 6 ++ crates/uv-cli/src/lib.rs | 12 ++-- crates/uv-settings/src/lib.rs | 12 ++++ crates/uv-static/src/env_vars.rs | 32 ++++++++++ crates/uv/src/settings.rs | 100 +++++++++++++++++++++++++++++++ crates/uv/tests/it/edit.rs | 30 ++++++++++ crates/uv/tests/it/sync.rs | 100 +++++++++++++++++++++++++++++++ 7 files changed, 286 insertions(+), 6 deletions(-) diff --git a/clippy.toml b/clippy.toml index 2a018c7cd8..9bf33bc904 100644 --- a/clippy.toml +++ b/clippy.toml @@ -22,11 +22,17 @@ doc-valid-idents = [ "UV_NO_DEV", "UV_NO_EDITABLE", "UV_NO_ENV_FILE", + "UV_NO_INSTALL_LOCAL", + "UV_NO_INSTALL_PROJECT", + "UV_NO_INSTALL_WORKSPACE", "UV_NO_INSTALLER_METADATA", "UV_NO_MANAGED_PYTHON", "UV_NO_PROGRESS", "UV_NO_SYNC", "UV_OFFLINE", + "UV_ONLY_INSTALL_LOCAL", + "UV_ONLY_INSTALL_PROJECT", + "UV_ONLY_INSTALL_WORKSPACE", "UV_PREVIEW", "UV_SHOW_RESOLUTION", "UV_VENV_CLEAR", diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index a966d5df08..66b08274a7 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -3999,7 +3999,7 @@ pub struct SyncArgs { #[arg(long, overrides_with = "active", hide = true)] pub no_active: bool, - /// Do not install the current project. + /// Do not install the current project [env: UV_NO_INSTALL_PROJECT=] /// /// By default, the current project is installed into the environment with all of its /// dependencies. The `--no-install-project` option allows the project to be excluded, but all @@ -4016,7 +4016,7 @@ pub struct SyncArgs { #[arg(long, conflicts_with = "no_install_project", hide = true)] pub only_install_project: bool, - /// Do not install any workspace members, including the root project. + /// Do not install any workspace members, including the root project [env: UV_NO_INSTALL_WORKSPACE=] /// /// By default, all workspace members and their dependencies are installed into the /// environment. The `--no-install-workspace` option allows exclusion of all the workspace @@ -4033,7 +4033,7 @@ pub struct SyncArgs { #[arg(long, conflicts_with = "no_install_workspace", hide = true)] pub only_install_workspace: bool, - /// Do not install local path dependencies + /// Do not install local path dependencies [env: UV_NO_INSTALL_LOCAL=] /// /// Skips the current project, workspace members, and any other local (path or editable) /// packages. Only remote/indexed dependencies are installed. Useful in Docker builds to cache @@ -4501,7 +4501,7 @@ pub struct AddArgs { #[arg(long, overrides_with = "workspace")] pub no_workspace: bool, - /// Do not install the current project. + /// Do not install the current project [env: UV_NO_INSTALL_PROJECT=] /// /// By default, the current project is installed into the environment with all of its /// dependencies. The `--no-install-project` option allows the project to be excluded, but all of @@ -4529,7 +4529,7 @@ pub struct AddArgs { )] pub only_install_project: bool, - /// Do not install any workspace members, including the current project. + /// Do not install any workspace members, including the current project [env: UV_NO_INSTALL_WORKSPACE=] /// /// By default, all workspace members and their dependencies are installed into the /// environment. The `--no-install-workspace` option allows exclusion of all the workspace @@ -4557,7 +4557,7 @@ pub struct AddArgs { )] pub only_install_workspace: bool, - /// Do not install local path dependencies + /// Do not install local path dependencies [env: UV_NO_INSTALL_LOCAL=] /// /// Skips the current project, workspace members, and any other local (path or editable) /// packages. Only remote/indexed dependencies are installed. Useful in Docker builds to cache diff --git a/crates/uv-settings/src/lib.rs b/crates/uv-settings/src/lib.rs index c0d228f68e..84da74133e 100644 --- a/crates/uv-settings/src/lib.rs +++ b/crates/uv-settings/src/lib.rs @@ -749,6 +749,12 @@ pub struct EnvironmentOptions { pub no_dev: EnvFlag, pub show_resolution: EnvFlag, pub no_editable: EnvFlag, + pub no_install_project: EnvFlag, + pub no_install_workspace: EnvFlag, + pub no_install_local: EnvFlag, + pub only_install_project: EnvFlag, + pub only_install_workspace: EnvFlag, + pub only_install_local: EnvFlag, pub no_env_file: EnvFlag, pub no_group: Option>, pub no_binary_package: Option>, @@ -859,6 +865,12 @@ impl EnvironmentOptions { no_dev: EnvFlag::new(EnvVars::UV_NO_DEV)?, show_resolution: EnvFlag::new(EnvVars::UV_SHOW_RESOLUTION)?, no_editable: EnvFlag::new(EnvVars::UV_NO_EDITABLE)?, + no_install_project: EnvFlag::new(EnvVars::UV_NO_INSTALL_PROJECT)?, + no_install_workspace: EnvFlag::new(EnvVars::UV_NO_INSTALL_WORKSPACE)?, + no_install_local: EnvFlag::new(EnvVars::UV_NO_INSTALL_LOCAL)?, + only_install_project: EnvFlag::new(EnvVars::UV_ONLY_INSTALL_PROJECT)?, + only_install_workspace: EnvFlag::new(EnvVars::UV_ONLY_INSTALL_WORKSPACE)?, + only_install_local: EnvFlag::new(EnvVars::UV_ONLY_INSTALL_LOCAL)?, no_env_file: EnvFlag::new(EnvVars::UV_NO_ENV_FILE)?, no_group: parse_name_list_environment_variable(EnvVars::UV_NO_GROUP)?, no_binary_package: parse_name_list_environment_variable(EnvVars::UV_NO_BINARY_PACKAGE)?, diff --git a/crates/uv-static/src/env_vars.rs b/crates/uv-static/src/env_vars.rs index abcc8b98d9..f957343218 100644 --- a/crates/uv-static/src/env_vars.rs +++ b/crates/uv-static/src/env_vars.rs @@ -260,6 +260,38 @@ impl EnvVars { #[attr_added_in("0.9.9")] pub const UV_NO_DEFAULT_GROUPS: &'static str = "UV_NO_DEFAULT_GROUPS"; + /// Equivalent to the `--no-install-project` command-line argument. If set, uv will + /// install the project's dependencies but not the project itself. + #[attr_added_in("next release")] + pub const UV_NO_INSTALL_PROJECT: &'static str = "UV_NO_INSTALL_PROJECT"; + + /// Equivalent to the `--no-install-workspace` command-line argument. If set, uv will + /// install workspace dependencies but not workspace members (including the current + /// project). + #[attr_added_in("next release")] + pub const UV_NO_INSTALL_WORKSPACE: &'static str = "UV_NO_INSTALL_WORKSPACE"; + + /// Equivalent to the `--no-install-local` command-line argument. If set, uv will skip + /// the current project, workspace members, and any other local (path or editable) + /// packages, installing only remote dependencies. + #[attr_added_in("next release")] + pub const UV_NO_INSTALL_LOCAL: &'static str = "UV_NO_INSTALL_LOCAL"; + + /// Equivalent to the hidden `--only-install-project` command-line argument. + #[attr_hidden] + #[attr_added_in("next release")] + pub const UV_ONLY_INSTALL_PROJECT: &'static str = "UV_ONLY_INSTALL_PROJECT"; + + /// Equivalent to the hidden `--only-install-workspace` command-line argument. + #[attr_hidden] + #[attr_added_in("next release")] + pub const UV_ONLY_INSTALL_WORKSPACE: &'static str = "UV_ONLY_INSTALL_WORKSPACE"; + + /// Equivalent to the hidden `--only-install-local` command-line argument. + #[attr_hidden] + #[attr_added_in("next release")] + pub const UV_ONLY_INSTALL_LOCAL: &'static str = "UV_ONLY_INSTALL_LOCAL"; + /// Equivalent to the `--no-binary` command-line argument. If set, uv will install /// all packages from source. The resolver will still use pre-built wheels to /// extract package metadata, if available. diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index 00109abf44..2da6ea8ef6 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -1794,6 +1794,46 @@ impl SyncSettings { Some(environment.no_editable), ); + let (no_install_project, only_install_project) = resolve_flag_pair( + no_install_project, + only_install_project, + "no-install-project", + "only-install-project", + Some(environment.no_install_project), + Some(environment.only_install_project), + ); + let (no_install_workspace, only_install_workspace) = resolve_flag_pair( + no_install_workspace, + only_install_workspace, + "no-install-workspace", + "only-install-workspace", + Some(environment.no_install_workspace), + Some(environment.only_install_workspace), + ); + let (no_install_local, only_install_local) = resolve_flag_pair( + no_install_local, + only_install_local, + "no-install-local", + "only-install-local", + Some(environment.no_install_local), + Some(environment.only_install_local), + ); + check_conflicts(no_install_project, only_install_project); + check_conflicts(no_install_workspace, only_install_workspace); + check_conflicts(no_install_local, only_install_local); + if script.is_some() { + let script = Flag::from_cli("script"); + check_conflicts(no_install_project, script); + check_conflicts(no_install_workspace, script); + check_conflicts(no_install_local, script); + } + let no_install_project = no_install_project.is_enabled(); + let only_install_project = only_install_project.is_enabled(); + let no_install_workspace = no_install_workspace.is_enabled(); + let only_install_workspace = only_install_workspace.is_enabled(); + let no_install_local = no_install_local.is_enabled(); + let only_install_local = only_install_local.is_enabled(); + let malware_settings = MalwareCheckSettings::from(&environment); Self { @@ -2092,6 +2132,34 @@ impl AddSettings { Some(environment.no_editable), ); + let (no_install_project, only_install_project) = resolve_flag_pair( + no_install_project, + only_install_project, + "no-install-project", + "only-install-project", + Some(environment.no_install_project), + Some(environment.only_install_project), + ); + let (no_install_workspace, only_install_workspace) = resolve_flag_pair( + no_install_workspace, + only_install_workspace, + "no-install-workspace", + "only-install-workspace", + Some(environment.no_install_workspace), + Some(environment.only_install_workspace), + ); + let (no_install_local, only_install_local) = resolve_flag_pair( + no_install_local, + only_install_local, + "no-install-local", + "only-install-local", + Some(environment.no_install_local), + Some(environment.only_install_local), + ); + check_conflicts(no_install_project, only_install_project); + check_conflicts(no_install_workspace, only_install_workspace); + check_conflicts(no_install_local, only_install_local); + let dependency_type = if let Some(extra) = optional { DependencyType::Optional(extra) } else if let Some(group) = group { @@ -2169,6 +2237,38 @@ impl AddSettings { // Check for conflicts between no_sync and frozen. check_conflicts(no_sync, frozen); + let no_install_package_flag = if no_install_package.is_empty() { + Flag::disabled() + } else { + Flag::from_cli("no-install-package") + }; + let only_install_package_flag = if only_install_package.is_empty() { + Flag::disabled() + } else { + Flag::from_cli("only-install-package") + }; + + for install_flag in [ + no_install_project, + no_install_workspace, + no_install_local, + only_install_project, + only_install_workspace, + only_install_local, + no_install_package_flag, + only_install_package_flag, + ] { + check_conflicts(install_flag, frozen); + check_conflicts(install_flag, no_sync); + } + + let no_install_project = no_install_project.is_enabled(); + let only_install_project = only_install_project.is_enabled(); + let no_install_workspace = no_install_workspace.is_enabled(); + let only_install_workspace = only_install_workspace.is_enabled(); + let no_install_local = no_install_local.is_enabled(); + let only_install_local = only_install_local.is_enabled(); + let malware_settings = MalwareCheckSettings::from(&environment); Self { diff --git a/crates/uv/tests/it/edit.rs b/crates/uv/tests/it/edit.rs index 0442f6109d..e892f144da 100644 --- a/crates/uv/tests/it/edit.rs +++ b/crates/uv/tests/it/edit.rs @@ -15491,6 +15491,36 @@ fn add_no_install_project() -> Result<()> { ); }); + uv_snapshot!(context.filters(), context.add().arg("typing-extensions").env(EnvVars::UV_NO_INSTALL_PROJECT, "1"), @" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 3 packages in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + typing-extensions==4.10.0 + "); + + uv_snapshot!(context.filters(), context.add().arg("typing-extensions").arg("--frozen").env(EnvVars::UV_NO_INSTALL_PROJECT, "1"), @" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: the argument `UV_NO_INSTALL_PROJECT` (environment variable) cannot be used with `--frozen` + "); + + uv_snapshot!(context.filters(), context.add().arg("typing-extensions").arg("--frozen").env(EnvVars::UV_ONLY_INSTALL_PROJECT, "1"), @" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: the argument `UV_ONLY_INSTALL_PROJECT` (environment variable) cannot be used with `--frozen` + "); + Ok(()) } diff --git a/crates/uv/tests/it/sync.rs b/crates/uv/tests/it/sync.rs index f383ec7231..958d8a0749 100644 --- a/crates/uv/tests/it/sync.rs +++ b/crates/uv/tests/it/sync.rs @@ -5849,6 +5849,39 @@ fn no_install_project() -> Result<()> { + sniffio==1.3.1 "); + fs_err::remove_dir_all(&context.venv)?; + + uv_snapshot!(context.filters(), context.sync().env(EnvVars::UV_NO_INSTALL_PROJECT, "1"), @" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] + Creating virtual environment at: .venv + Resolved 4 packages in [TIME] + Installed 3 packages in [TIME] + + anyio==3.7.0 + + idna==3.6 + + sniffio==1.3.1 + "); + + uv_snapshot!(context.filters(), context.sync().env(EnvVars::UV_ONLY_INSTALL_PROJECT, "1"), @" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 4 packages in [TIME] + Prepared 1 package in [TIME] + Uninstalled 3 packages in [TIME] + Installed 1 package in [TIME] + - anyio==3.7.0 + - idna==3.6 + + project==0.1.0 (from file://[TEMP_DIR]/) + - sniffio==1.3.1 + "); + // However, we do require the `pyproject.toml`. fs_err::remove_file(pyproject_toml)?; @@ -5941,6 +5974,23 @@ fn no_install_workspace() -> Result<()> { // Remove the virtual environment. fs_err::remove_dir_all(&context.venv)?; + uv_snapshot!(context.filters(), context.sync().arg("--frozen").env(EnvVars::UV_NO_INSTALL_WORKSPACE, "1"), @" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] + Creating virtual environment at: .venv + Installed 4 packages in [TIME] + + anyio==3.7.0 + + idna==3.6 + + iniconfig==2.0.0 + + sniffio==1.3.1 + "); + + fs_err::remove_dir_all(&context.venv)?; + // We don't require the `pyproject.toml` for non-root members, if `--frozen` is provided. fs_err::remove_file(child.join("pyproject.toml"))?; @@ -6097,6 +6147,56 @@ fn no_install_local() -> Result<()> { + sniffio==1.3.1 "); + fs_err::remove_dir_all(&context.venv)?; + + uv_snapshot!(context.filters(), context.sync().arg("--frozen").env(EnvVars::UV_NO_INSTALL_LOCAL, "1"), @" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] + Creating virtual environment at: .venv + Installed 3 packages in [TIME] + + anyio==3.7.0 + + idna==3.6 + + sniffio==1.3.1 + "); + + Ok(()) +} + +/// `UV_NO_INSTALL_PROJECT=1` should conflict with `--script`, just like +/// `--no-install-project`. +#[test] +fn no_install_env_var_conflicts() -> Result<()> { + let context = uv_test::test_context!("3.12"); + + let script = context.temp_dir.child("script.py"); + script.write_str(indoc! {r#" + # /// script + # requires-python = ">=3.12" + # /// + "#})?; + + uv_snapshot!(context.filters(), context.sync().arg("--script").arg("script.py").env(EnvVars::UV_NO_INSTALL_PROJECT, "1"), @" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: the argument `UV_NO_INSTALL_PROJECT` (environment variable) cannot be used with `--script` + "); + + uv_snapshot!(context.filters(), context.sync().arg("--script").arg("script.py").env(EnvVars::UV_NO_INSTALL_PROJECT, "1").env(EnvVars::UV_ONLY_INSTALL_PROJECT, "1"), @" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: the argument `UV_NO_INSTALL_PROJECT` (environment variable) cannot be used with `UV_ONLY_INSTALL_PROJECT` (environment variable) + "); + Ok(()) }