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
```
This commit is contained in:
ChrisJr404
2026-06-05 20:40:07 -04:00
committed by GitHub
parent b3e68d1fba
commit 39f79b3e56
7 changed files with 286 additions and 6 deletions
+6
View File
@@ -22,11 +22,17 @@ doc-valid-idents = [
"UV_NO_DEV", "UV_NO_DEV",
"UV_NO_EDITABLE", "UV_NO_EDITABLE",
"UV_NO_ENV_FILE", "UV_NO_ENV_FILE",
"UV_NO_INSTALL_LOCAL",
"UV_NO_INSTALL_PROJECT",
"UV_NO_INSTALL_WORKSPACE",
"UV_NO_INSTALLER_METADATA", "UV_NO_INSTALLER_METADATA",
"UV_NO_MANAGED_PYTHON", "UV_NO_MANAGED_PYTHON",
"UV_NO_PROGRESS", "UV_NO_PROGRESS",
"UV_NO_SYNC", "UV_NO_SYNC",
"UV_OFFLINE", "UV_OFFLINE",
"UV_ONLY_INSTALL_LOCAL",
"UV_ONLY_INSTALL_PROJECT",
"UV_ONLY_INSTALL_WORKSPACE",
"UV_PREVIEW", "UV_PREVIEW",
"UV_SHOW_RESOLUTION", "UV_SHOW_RESOLUTION",
"UV_VENV_CLEAR", "UV_VENV_CLEAR",
+6 -6
View File
@@ -3999,7 +3999,7 @@ pub struct SyncArgs {
#[arg(long, overrides_with = "active", hide = true)] #[arg(long, overrides_with = "active", hide = true)]
pub no_active: bool, 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 /// 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 /// 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)] #[arg(long, conflicts_with = "no_install_project", hide = true)]
pub only_install_project: bool, 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 /// By default, all workspace members and their dependencies are installed into the
/// environment. The `--no-install-workspace` option allows exclusion of all the workspace /// 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)] #[arg(long, conflicts_with = "no_install_workspace", hide = true)]
pub only_install_workspace: bool, 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) /// 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 /// 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")] #[arg(long, overrides_with = "workspace")]
pub no_workspace: bool, 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 /// 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 /// 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, 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 /// By default, all workspace members and their dependencies are installed into the
/// environment. The `--no-install-workspace` option allows exclusion of all the workspace /// environment. The `--no-install-workspace` option allows exclusion of all the workspace
@@ -4557,7 +4557,7 @@ pub struct AddArgs {
)] )]
pub only_install_workspace: bool, 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) /// 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 /// packages. Only remote/indexed dependencies are installed. Useful in Docker builds to cache
+12
View File
@@ -749,6 +749,12 @@ pub struct EnvironmentOptions {
pub no_dev: EnvFlag, pub no_dev: EnvFlag,
pub show_resolution: EnvFlag, pub show_resolution: EnvFlag,
pub no_editable: 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_env_file: EnvFlag,
pub no_group: Option<Vec<GroupName>>, pub no_group: Option<Vec<GroupName>>,
pub no_binary_package: Option<Vec<PackageName>>, pub no_binary_package: Option<Vec<PackageName>>,
@@ -859,6 +865,12 @@ impl EnvironmentOptions {
no_dev: EnvFlag::new(EnvVars::UV_NO_DEV)?, no_dev: EnvFlag::new(EnvVars::UV_NO_DEV)?,
show_resolution: EnvFlag::new(EnvVars::UV_SHOW_RESOLUTION)?, show_resolution: EnvFlag::new(EnvVars::UV_SHOW_RESOLUTION)?,
no_editable: EnvFlag::new(EnvVars::UV_NO_EDITABLE)?, 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_env_file: EnvFlag::new(EnvVars::UV_NO_ENV_FILE)?,
no_group: parse_name_list_environment_variable(EnvVars::UV_NO_GROUP)?, no_group: parse_name_list_environment_variable(EnvVars::UV_NO_GROUP)?,
no_binary_package: parse_name_list_environment_variable(EnvVars::UV_NO_BINARY_PACKAGE)?, no_binary_package: parse_name_list_environment_variable(EnvVars::UV_NO_BINARY_PACKAGE)?,
+32
View File
@@ -260,6 +260,38 @@ impl EnvVars {
#[attr_added_in("0.9.9")] #[attr_added_in("0.9.9")]
pub const UV_NO_DEFAULT_GROUPS: &'static str = "UV_NO_DEFAULT_GROUPS"; 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 /// 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 /// all packages from source. The resolver will still use pre-built wheels to
/// extract package metadata, if available. /// extract package metadata, if available.
+100
View File
@@ -1794,6 +1794,46 @@ impl SyncSettings {
Some(environment.no_editable), 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); let malware_settings = MalwareCheckSettings::from(&environment);
Self { Self {
@@ -2092,6 +2132,34 @@ impl AddSettings {
Some(environment.no_editable), 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 { let dependency_type = if let Some(extra) = optional {
DependencyType::Optional(extra) DependencyType::Optional(extra)
} else if let Some(group) = group { } else if let Some(group) = group {
@@ -2169,6 +2237,38 @@ impl AddSettings {
// Check for conflicts between no_sync and frozen. // Check for conflicts between no_sync and frozen.
check_conflicts(no_sync, 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); let malware_settings = MalwareCheckSettings::from(&environment);
Self { Self {
+30
View File
@@ -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(()) Ok(())
} }
+100
View File
@@ -5849,6 +5849,39 @@ fn no_install_project() -> Result<()> {
+ sniffio==1.3.1 + 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`. // However, we do require the `pyproject.toml`.
fs_err::remove_file(pyproject_toml)?; fs_err::remove_file(pyproject_toml)?;
@@ -5941,6 +5974,23 @@ fn no_install_workspace() -> Result<()> {
// Remove the virtual environment. // Remove the virtual environment.
fs_err::remove_dir_all(&context.venv)?; 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. // We don't require the `pyproject.toml` for non-root members, if `--frozen` is provided.
fs_err::remove_file(child.join("pyproject.toml"))?; fs_err::remove_file(child.join("pyproject.toml"))?;
@@ -6097,6 +6147,56 @@ fn no_install_local() -> Result<()> {
+ sniffio==1.3.1 + 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(()) Ok(())
} }