diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index c6e9d3e429c2..fc52da503819 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -2427,6 +2427,13 @@ pub struct RunArgs { #[arg(long)] pub isolated: bool, + /// Avoid syncing the virtual environment. + /// + /// Implies `--frozen`, as the project dependencies will be ignored (i.e., the lockfile will not + /// be updated, since the environment will not be synced regardless). + #[arg(long, conflicts_with = "frozen")] + pub no_sync: bool, + /// Assert that the `uv.lock` will remain unchanged. /// /// Requires that the lockfile is up-to-date. If the lockfile is missing or @@ -2728,7 +2735,7 @@ pub struct AddArgs { #[arg(long)] pub extra: Option>, - /// Avoid syncing the virtual environment after re-locking the project. + /// Avoid syncing the virtual environment. #[arg(long, conflicts_with = "frozen")] pub no_sync: bool, diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index a2094f3cc06d..535de1006fa4 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -49,6 +49,7 @@ pub(crate) async fn run( show_resolution: bool, locked: bool, frozen: bool, + no_sync: bool, isolated: bool, package: Option, no_project: bool, @@ -258,6 +259,11 @@ pub(crate) async fn run( "`--frozen` is a no-op for Python scripts with inline metadata, which always run in isolation" ); } + if no_sync { + warn_user!( + "`--no-sync` is a no-op for Python scripts with inline metadata, which always run in isolation" + ); + } if isolated { warn_user!( "`--isolated` is a no-op for Python scripts with inline metadata, which always run in isolation" @@ -318,6 +324,9 @@ pub(crate) async fn run( if frozen { warn_user!("`--frozen` has no effect when used alongside `--no-project`"); } + if no_sync { + warn_user!("`--no-sync` has no effect when used alongside `--no-project`"); + } } else if project.is_none() { // If we can't find a project and the user provided a project-only setting, warn. if !extras.is_empty() { @@ -329,8 +338,8 @@ pub(crate) async fn run( if locked { warn_user!("`--locked` has no effect when used outside of a project"); } - if frozen { - warn_user!("`--frozen` has no effect when used outside of a project"); + if no_sync { + warn_user!("`--no-sync` has no effect when used outside of a project"); } } @@ -408,60 +417,64 @@ pub(crate) async fn run( .await? }; - let result = match project::lock::do_safe_lock( - locked, - frozen, - project.workspace(), - venv.interpreter(), - settings.as_ref().into(), - if show_resolution { - Box::new(DefaultResolveLogger) - } else { - Box::new(SummaryResolveLogger) - }, - connectivity, - concurrency, - native_tls, - cache, - printer, - ) - .await - { - Ok(result) => result, - Err(ProjectError::Operation(operations::Error::Resolve( - uv_resolver::ResolveError::NoSolution(err), - ))) => { - let report = miette::Report::msg(format!("{err}")).context(err.header()); - eprint!("{report:?}"); - return Ok(ExitStatus::Failure); - } - Err(err) => return Err(err.into()), - }; + if no_sync { + debug!("Skipping environment synchronization due to `--no-sync`"); + } else { + let result = match project::lock::do_safe_lock( + locked, + frozen, + project.workspace(), + venv.interpreter(), + settings.as_ref().into(), + if show_resolution { + Box::new(DefaultResolveLogger) + } else { + Box::new(SummaryResolveLogger) + }, + connectivity, + concurrency, + native_tls, + cache, + printer, + ) + .await + { + Ok(result) => result, + Err(ProjectError::Operation(operations::Error::Resolve( + uv_resolver::ResolveError::NoSolution(err), + ))) => { + let report = miette::Report::msg(format!("{err}")).context(err.header()); + eprint!("{report:?}"); + return Ok(ExitStatus::Failure); + } + Err(err) => return Err(err.into()), + }; - let install_options = InstallOptions::default(); - - project::sync::do_sync( - InstallTarget::from(&project), - &venv, - result.lock(), - &extras, - dev, - install_options, - Modifications::Sufficient, - settings.as_ref().into(), - &state, - if show_resolution { - Box::new(DefaultInstallLogger) - } else { - Box::new(SummaryInstallLogger) - }, - connectivity, - concurrency, - native_tls, - cache, - printer, - ) - .await?; + let install_options = InstallOptions::default(); + + project::sync::do_sync( + InstallTarget::from(&project), + &venv, + result.lock(), + &extras, + dev, + install_options, + Modifications::Sufficient, + settings.as_ref().into(), + &state, + if show_resolution { + Box::new(DefaultInstallLogger) + } else { + Box::new(SummaryInstallLogger) + }, + connectivity, + concurrency, + native_tls, + cache, + printer, + ) + .await?; + } venv.into_interpreter() } else { diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index df4780c0a9ba..7e2ef8fcee15 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -1123,6 +1123,7 @@ async fn run_project( args.show_resolution || globals.verbose > 0, args.locked, args.frozen, + args.no_sync, args.isolated, args.package, args.no_project, diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index 1aae167b540c..1bf7a398d050 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -217,6 +217,7 @@ pub(crate) struct RunSettings { pub(crate) show_resolution: bool, pub(crate) package: Option, pub(crate) no_project: bool, + pub(crate) no_sync: bool, pub(crate) python: Option, pub(crate) refresh: Refresh, pub(crate) settings: ResolverInstallerSettings, @@ -237,6 +238,7 @@ impl RunSettings { with_editable, with_requirements, isolated, + no_sync, locked, frozen, installer, @@ -266,6 +268,7 @@ impl RunSettings { show_resolution, package, no_project, + no_sync, python, refresh: Refresh::from(refresh), settings: ResolverInstallerSettings::combine( diff --git a/crates/uv/tests/run.rs b/crates/uv/tests/run.rs index c12fd5cd6442..dae69c97e266 100644 --- a/crates/uv/tests/run.rs +++ b/crates/uv/tests/run.rs @@ -976,6 +976,62 @@ fn run_frozen() -> Result<()> { Ok(()) } +#[test] +fn run_no_sync() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["anyio==3.7.0"] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "#, + )?; + + // Running with `--no-sync` should succeed error, even if the lockfile isn't present. + uv_snapshot!(context.filters(), context.run().arg("--no-sync").arg("--").arg("python").arg("--version"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + Python 3.12.[X] + + ----- stderr ----- + "###); + + context.lock().assert().success(); + + // Running with `--no-sync` should not install any requirements. + uv_snapshot!(context.filters(), context.run().arg("--no-sync").arg("--").arg("python").arg("--version"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + Python 3.12.[X] + + ----- stderr ----- + "###); + + context.sync().assert().success(); + + // But it should have access to the installed packages. + uv_snapshot!(context.filters(), context.run().arg("--no-sync").arg("--").arg("python").arg("-c").arg("import anyio; print(anyio.__name__)"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + anyio + + ----- stderr ----- + "###); + + Ok(()) +} + #[test] fn run_empty_requirements_txt() -> Result<()> { let context = TestContext::new("3.12"); diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 3aeef064e7ae..4a8b8ba80056 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -265,6 +265,10 @@ uv run [OPTIONS]
--no-sources

Ignore the tool.uv.sources table when resolving dependencies. Used to lock against the standards-compliant, publishable package metadata, as opposed to using any local or Git sources

+
--no-sync

Avoid syncing the virtual environment.

+ +

Implies --frozen, as the project dependencies will be ignored (i.e., the lockfile will not be updated, since the environment will not be synced regardless).

+
--offline

Disable network access.

When disabled, uv will only use locally cached data and locally available files.

@@ -721,7 +725,7 @@ uv add [OPTIONS] >
--no-sources

Ignore the tool.uv.sources table when resolving dependencies. Used to lock against the standards-compliant, publishable package metadata, as opposed to using any local or Git sources

-
--no-sync

Avoid syncing the virtual environment after re-locking the project

+
--no-sync

Avoid syncing the virtual environment

--offline

Disable network access.