diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index d5aa9dcbdeb0..18862e672170 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -2282,6 +2282,16 @@ pub struct SyncArgs { #[arg(long)] pub no_install_project: bool, + /// Do not install any workspace members, including the root project. + /// + /// By default, all of the workspace members and their dependencies are installed into the + /// environment. The `--no-install-workspace` option allows exclusion of all the workspace + /// members while retaining their dependencies. This is particularly useful in situations like + /// building Docker images where installing the workspace separately from its dependencies + /// allows optimal layer caching. + #[arg(long)] + pub no_install_workspace: bool, + /// Assert that the `uv.lock` will remain unchanged. /// /// Requires that the lockfile is up-to-date. If the lockfile is missing or diff --git a/crates/uv/src/commands/project/add.rs b/crates/uv/src/commands/project/add.rs index 875b0878ee78..57115a19ec87 100644 --- a/crates/uv/src/commands/project/add.rs +++ b/crates/uv/src/commands/project/add.rs @@ -604,6 +604,7 @@ pub(crate) async fn add( &extras, dev, false, + false, Modifications::Sufficient, settings.as_ref().into(), &state, diff --git a/crates/uv/src/commands/project/remove.rs b/crates/uv/src/commands/project/remove.rs index 1cb0eab7f6a6..2c08631c50a7 100644 --- a/crates/uv/src/commands/project/remove.rs +++ b/crates/uv/src/commands/project/remove.rs @@ -191,6 +191,7 @@ pub(crate) async fn remove( let extras = ExtrasSpecification::All; let dev = true; let no_install_project = false; + let no_install_workspace = false; // Initialize any shared state. let state = SharedState::default(); @@ -202,6 +203,7 @@ pub(crate) async fn remove( &extras, dev, no_install_project, + no_install_workspace, Modifications::Exact, settings.as_ref().into(), &state, diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index c7dde93b30f5..56b226cfc0a1 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -418,6 +418,7 @@ pub(crate) async fn run( &extras, dev, false, + false, Modifications::Sufficient, settings.as_ref().into(), &state, diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index 3cb276228a27..22b56f4efdca 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -33,6 +33,7 @@ pub(crate) async fn sync( extras: ExtrasSpecification, dev: bool, no_install_project: bool, + no_install_workspace: bool, modifications: Modifications, python: Option, python_preference: PythonPreference, @@ -106,6 +107,7 @@ pub(crate) async fn sync( &extras, dev, no_install_project, + no_install_workspace, modifications, settings.as_ref().into(), &state, @@ -122,6 +124,7 @@ pub(crate) async fn sync( } /// Sync a lockfile with an environment. +#[allow(clippy::fn_params_excessive_bools)] pub(super) async fn do_sync( project: &VirtualProject, venv: &PythonEnvironment, @@ -129,6 +132,7 @@ pub(super) async fn do_sync( extras: &ExtrasSpecification, dev: bool, no_install_project: bool, + no_install_workspace: bool, modifications: Modifications, settings: InstallerSettingsRef<'_>, state: &SharedState, @@ -195,6 +199,9 @@ pub(super) async fn do_sync( // If `--no-install-project` is set, remove the project itself. let resolution = apply_no_install_project(no_install_project, resolution, project); + // If `--no-install-workspace` is set, remove the project and any workspace members. + let resolution = apply_no_install_workspace(no_install_workspace, resolution, project); + // Add all authenticated sources to the cache. for url in index_locations.urls() { store_credentials_from_url(url); @@ -299,3 +306,18 @@ fn apply_no_install_project( resolution.filter(|dist| dist.name() != project_name) } + +fn apply_no_install_workspace( + no_install_workspace: bool, + resolution: distribution_types::Resolution, + project: &VirtualProject, +) -> distribution_types::Resolution { + if !no_install_workspace { + return resolution; + } + + let workspace_packages = project.workspace().packages(); + resolution.filter(|dist| { + !workspace_packages.contains_key(dist.name()) && Some(dist.name()) != project.project_name() + }) +} diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index bd85e567a2e4..c2ac743acefb 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -1103,6 +1103,7 @@ async fn run_project( args.extras, args.dev, args.no_install_project, + args.no_install_workspace, args.modifications, args.python, globals.python_preference, diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index bc43ad1684a7..5f6e0265a54d 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -618,6 +618,7 @@ pub(crate) struct SyncSettings { pub(crate) extras: ExtrasSpecification, pub(crate) dev: bool, pub(crate) no_install_project: bool, + pub(crate) no_install_workspace: bool, pub(crate) modifications: Modifications, pub(crate) package: Option, pub(crate) python: Option, @@ -638,6 +639,7 @@ impl SyncSettings { inexact, exact, no_install_project, + no_install_workspace, locked, frozen, installer, @@ -671,6 +673,7 @@ impl SyncSettings { ), dev: flag(dev, no_dev).unwrap_or(true), no_install_project, + no_install_workspace, modifications, package, python, diff --git a/crates/uv/tests/sync.rs b/crates/uv/tests/sync.rs index 307d5c817b03..6bf555beb1a3 100644 --- a/crates/uv/tests/sync.rs +++ b/crates/uv/tests/sync.rs @@ -863,3 +863,70 @@ fn no_install_project() -> Result<()> { Ok(()) } + +/// Avoid syncing local dependencies for workspace dependencies when `--no-install-project` is provided, but +/// include the workspace dependency's dependencies. +#[test] +fn no_install_workspace() -> 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", "child"] + + [tool.uv.workspace] + members = ["child"] + + [tool.uv.sources] + child = { workspace = true } + "#, + )?; + + // Add a workspace member. + let child = context.temp_dir.child("child"); + child.child("pyproject.toml").write_str( + r#" + [project] + name = "child" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["iniconfig>1"] + + [build-system] + requires = ["hatchling"] + build-backend = "hatchling.build" + "#, + )?; + child + .child("src") + .child("child") + .child("__init__.py") + .touch()?; + + // Generate a lockfile. + context.lock().assert().success(); + + // Running with `--no-install-workspace` should install `anyio` and `iniconfig`, but not + // `project` or `child`. + uv_snapshot!(context.filters(), context.sync().arg("--no-install-workspace"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 6 packages in [TIME] + Prepared 4 packages in [TIME] + Installed 4 packages in [TIME] + + anyio==3.7.0 + + idna==3.6 + + iniconfig==2.0.0 + + sniffio==1.3.1 + "###); + + Ok(()) +} diff --git a/docs/guides/integration/docker.md b/docs/guides/integration/docker.md index 2d232b151715..26e8b4af68ff 100644 --- a/docs/guides/integration/docker.md +++ b/docs/guides/integration/docker.md @@ -191,7 +191,7 @@ If not mounting the cache, image size can be reduced with `--no-cache` flag. ### Intermediate layers If you're using uv to manage your project, you can improve build times by moving your transitive -dependency installation into its own layer via `uv sync --no-install-project`. +dependency installation into its own layer via the `--no-install` options. `uv sync --no-install-project` will install the dependencies of the project but not the project itself. Since the project changes frequently, but its dependencies are generally static, this can be @@ -216,3 +216,8 @@ WORKDIR /app # Sync the project RUN uv sync --frozen ``` + +!!! tip + + If you're using a [workspace](../../concepts/workspaces.md), then consider the + `--no-install-workspace` flag which excludes the project _and_ any workspace members. diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 2f5a1219f4cc..0200b2329a67 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -1174,6 +1174,10 @@ uv sync [OPTIONS]

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 its dependencies are still installed. This is particularly useful in situations like building Docker images where installing the project separately from its dependencies allows optimal layer caching.

+
--no-install-workspace

Do not install any workspace members, including the root project.

+ +

By default, all of the workspace members and their dependencies are installed into the environment. The --no-install-workspace option allows exclusion of all the workspace members while retaining their dependencies. This is particularly useful in situations like building Docker images where installing the workspace separately from its dependencies allows optimal layer caching.

+
--no-progress

Hide all progress outputs.

For example, spinners or progress bars.