diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index cc2bd21e5d2e4..a0e7a4cda9f4b 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -2718,6 +2718,17 @@ pub struct RunArgs { #[arg(long)] pub no_editable: bool, + /// Do not remove extraneous packages present in the environment. + #[arg(long, overrides_with("exact"), alias = "no-exact", hide = true)] + pub inexact: bool, + + /// Perform an exact sync, removing extraneous packages. + /// + /// When enabled, uv will remove any extraneous packages from the environment. + /// By default, `uv run` will make the minimum necessary changes to satisfy the requirements. + #[arg(long, overrides_with("inexact"))] + pub exact: bool, + /// Load environment variables from a `.env` file. /// /// Can be provided multiple times, with subsequent files overriding values defined in diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index 31678867b8f9f..e08ecb95ff379 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -74,6 +74,7 @@ pub(crate) async fn run( extras: ExtrasSpecification, dev: DevGroupsSpecification, editable: EditableMode, + modifications: Modifications, python: Option, install_mirrors: PythonInstallMirrors, settings: ResolverInstallerSettings, @@ -705,7 +706,7 @@ pub(crate) async fn run( &dev.with_defaults(defaults), editable, install_options, - Modifications::Sufficient, + modifications, settings.as_ref().into(), if show_resolution { Box::new(DefaultInstallLogger) diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index f34ef7a03a151..f7962b13d8bc6 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -1414,6 +1414,7 @@ async fn run_project( args.extras, args.dev, args.editable, + args.modifications, args.python, args.install_mirrors, args.settings, diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index 80ac585954365..a70f39abf987a 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -262,6 +262,7 @@ pub(crate) struct RunSettings { pub(crate) extras: ExtrasSpecification, pub(crate) dev: DevGroupsSpecification, pub(crate) editable: EditableMode, + pub(crate) modifications: Modifications, pub(crate) with: Vec, pub(crate) with_editable: Vec, pub(crate) with_requirements: Vec, @@ -297,6 +298,8 @@ impl RunSettings { module: _, only_dev, no_editable, + inexact, + exact, script: _, gui_script: _, command: _, @@ -336,6 +339,11 @@ impl RunSettings { dev, no_dev, only_dev, group, no_group, only_group, all_groups, ), editable: EditableMode::from_args(no_editable), + modifications: if flag(exact, inexact).unwrap_or(false) { + Modifications::Exact + } else { + Modifications::Sufficient + }, with: with .into_iter() .flat_map(CommaSeparatedRequirements::into_iter) diff --git a/crates/uv/tests/it/run.rs b/crates/uv/tests/it/run.rs index 462d063a35516..b330ba9cac626 100644 --- a/crates/uv/tests/it/run.rs +++ b/crates/uv/tests/it/run.rs @@ -196,7 +196,8 @@ fn run_args() -> Result<()> { Ok(()) } -/// Run without specifying any argunments. +/// Run without specifying any arguments. +/// /// This should list the available scripts. #[test] fn run_no_args() -> Result<()> { @@ -807,6 +808,75 @@ fn run_managed_false() -> Result<()> { Ok(()) } +#[test] +fn run_exact() -> 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 = "1.0.0" + requires-python = ">=3.8" + dependencies = ["iniconfig"] + "# + })?; + + uv_snapshot!(context.filters(), context.run().arg("python").arg("-c").arg("import iniconfig"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + iniconfig==2.0.0 + "###); + + // Remove `iniconfig`. + pyproject_toml.write_str(indoc! { r#" + [project] + name = "foo" + version = "1.0.0" + requires-python = ">=3.8" + dependencies = ["anyio"] + "# + })?; + + // By default, `uv run` uses inexact semantics, so both `iniconfig` and `anyio` should still be available. + uv_snapshot!(context.filters(), context.run().arg("python").arg("-c").arg("import iniconfig; import anyio"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 6 packages in [TIME] + Prepared 3 packages in [TIME] + Installed 3 packages in [TIME] + + anyio==4.3.0 + + idna==3.6 + + sniffio==1.3.1 + "###); + + // But under `--exact`, `iniconfig` should not be available. + uv_snapshot!(context.filters(), context.run().arg("--exact").arg("python").arg("-c").arg("import iniconfig"), @r###" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + Resolved 6 packages in [TIME] + Uninstalled 1 package in [TIME] + - iniconfig==2.0.0 + Traceback (most recent call last): + File "", line 1, in + ModuleNotFoundError: No module named 'iniconfig' + "###); + + Ok(()) +} + #[test] fn run_with() -> Result<()> { let context = TestContext::new("3.12"); diff --git a/docs/reference/cli.md b/docs/reference/cli.md index cf8110eec7f80..3c48e558f16c6 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -148,6 +148,10 @@ uv run [OPTIONS] [COMMAND]

Can be provided multiple times, with subsequent files overriding values defined in previous files.

May also be set with the UV_ENV_FILE environment variable.

+
--exact

Perform an exact sync, removing extraneous packages.

+ +

When enabled, uv will remove any extraneous packages from the environment. By default, uv run will make the minimum necessary changes to satisfy the requirements.

+
--exclude-newer exclude-newer

Limit candidate packages to those that were uploaded prior to the given date.

Accepts both RFC 3339 timestamps (e.g., 2006-12-02T02:07:43Z) and local dates in the same format (e.g., 2006-12-02) in your system’s configured time zone.