Skip to content

Commit

Permalink
Add uv sync --no-install-project to skip installation of the project
Browse files Browse the repository at this point in the history
  • Loading branch information
charliermarsh authored and zanieb committed Aug 23, 2024
1 parent 681d605 commit af2cbc8
Show file tree
Hide file tree
Showing 11 changed files with 140 additions and 4 deletions.
23 changes: 23 additions & 0 deletions crates/distribution-types/src/resolution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,29 @@ impl Resolution {
pub fn diagnostics(&self) -> &[ResolutionDiagnostic] {
&self.diagnostics
}

/// Filter the resolution to only include packages that match the given predicate.
#[must_use]
pub fn filter(self, predicate: impl Fn(&ResolvedDist) -> bool) -> Self {
let packages = self
.packages
.iter()
.filter(|(_, dist)| predicate(dist))
.map(|(name, dist)| (name.clone(), dist.clone()))
.collect::<BTreeMap<_, _>>();
let hashes = self
.hashes
.iter()
.filter(|(name, _)| packages.contains_key(name))
.map(|(name, hashes)| (name.clone(), hashes.clone()))
.collect();
let diagnostics = self.diagnostics.clone();
Self {
packages,
hashes,
diagnostics,
}
}
}

#[derive(Debug, Clone, Hash)]
Expand Down
10 changes: 10 additions & 0 deletions crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2273,6 +2273,16 @@ pub struct SyncArgs {
#[arg(long, overrides_with("inexact"), hide = true)]
pub exact: bool,

/// Do not install the current 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
/// 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.
#[arg(long)]
pub no_install_project: bool,

/// Assert that the `uv.lock` will remain unchanged.
///
/// Requires that the lockfile is up-to-date. If the lockfile is missing or
Expand Down
1 change: 1 addition & 0 deletions crates/uv/src/commands/project/add.rs
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,7 @@ pub(crate) async fn add(
&lock,
&extras,
dev,
false,
Modifications::Sufficient,
settings.as_ref().into(),
&state,
Expand Down
2 changes: 2 additions & 0 deletions crates/uv/src/commands/project/remove.rs
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ pub(crate) async fn remove(
// TODO(ibraheem): Should we accept CLI overrides for this? Should we even sync here?
let extras = ExtrasSpecification::All;
let dev = true;
let no_install_project = false;

// Initialize any shared state.
let state = SharedState::default();
Expand All @@ -200,6 +201,7 @@ pub(crate) async fn remove(
&lock,
&extras,
dev,
no_install_project,
Modifications::Exact,
settings.as_ref().into(),
&state,
Expand Down
1 change: 1 addition & 0 deletions crates/uv/src/commands/project/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,7 @@ pub(crate) async fn run(
result.lock(),
&extras,
dev,
false,
Modifications::Sufficient,
settings.as_ref().into(),
&state,
Expand Down
25 changes: 25 additions & 0 deletions crates/uv/src/commands/project/sync.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use anyhow::{Context, Result};
use distribution_types::Name;
use itertools::Itertools;
use pep508_rs::MarkerTree;
use tracing::debug;
use uv_auth::store_credentials_from_url;
use uv_cache::Cache;
use uv_client::{Connectivity, FlatIndexClient, RegistryClientBuilder};
Expand Down Expand Up @@ -30,6 +32,7 @@ pub(crate) async fn sync(
package: Option<PackageName>,
extras: ExtrasSpecification,
dev: bool,
no_install_project: bool,
modifications: Modifications,
python: Option<String>,
python_preference: PythonPreference,
Expand Down Expand Up @@ -102,6 +105,7 @@ pub(crate) async fn sync(
&lock,
&extras,
dev,
no_install_project,
modifications,
settings.as_ref().into(),
&state,
Expand All @@ -124,6 +128,7 @@ pub(super) async fn do_sync(
lock: &Lock,
extras: &ExtrasSpecification,
dev: bool,
no_install_project: bool,
modifications: Modifications,
settings: InstallerSettingsRef<'_>,
state: &SharedState,
Expand Down Expand Up @@ -187,6 +192,9 @@ pub(super) async fn do_sync(
// Read the lockfile.
let resolution = lock.to_resolution(project, markers, tags, extras, &dev)?;

// If `--no-install-project` is set, remove the project itself.
let resolution = apply_no_install_project(no_install_project, resolution, project);

// Add all authenticated sources to the cache.
for url in index_locations.urls() {
store_credentials_from_url(url);
Expand Down Expand Up @@ -274,3 +282,20 @@ pub(super) async fn do_sync(

Ok(())
}

fn apply_no_install_project(
no_install_project: bool,
resolution: distribution_types::Resolution,
project: &VirtualProject,
) -> distribution_types::Resolution {
if !no_install_project {
return resolution;
}

let Some(project_name) = project.project_name() else {
debug!("Ignoring `--no-install-project` for virtual workspace");
return resolution;
};

resolution.filter(|dist| dist.name() != project_name)
}
1 change: 1 addition & 0 deletions crates/uv/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1081,6 +1081,7 @@ async fn run_project(
args.package,
args.extras,
args.dev,
args.no_install_project,
args.modifications,
args.python,
globals.python_preference,
Expand Down
7 changes: 5 additions & 2 deletions crates/uv/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -618,6 +618,7 @@ pub(crate) struct SyncSettings {
pub(crate) frozen: bool,
pub(crate) extras: ExtrasSpecification,
pub(crate) dev: bool,
pub(crate) no_install_project: bool,
pub(crate) modifications: Modifications,
pub(crate) package: Option<PackageName>,
pub(crate) python: Option<String>,
Expand All @@ -630,15 +631,16 @@ impl SyncSettings {
#[allow(clippy::needless_pass_by_value)]
pub(crate) fn resolve(args: SyncArgs, filesystem: Option<FilesystemOptions>) -> Self {
let SyncArgs {
locked,
frozen,
extra,
all_extras,
no_all_extras,
dev,
no_dev,
inexact,
exact,
no_install_project,
locked,
frozen,
installer,
build,
refresh,
Expand Down Expand Up @@ -669,6 +671,7 @@ impl SyncSettings {
extra.unwrap_or_default(),
),
dev: flag(dev, no_dev).unwrap_or(true),
no_install_project,
modifications,
package,
python,
Expand Down
37 changes: 37 additions & 0 deletions crates/uv/tests/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -826,3 +826,40 @@ fn read_metadata_statically_over_the_cache() -> Result<()> {

Ok(())
}

/// Avoid syncing the project package when `--no-install-project` is provided.
#[test]
fn no_install_project() -> 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"]
"#,
)?;

// Generate a lockfile.
context.lock().assert().success();

// Running with `--no-install-project` should install `anyio`, but not `project`.
uv_snapshot!(context.filters(), context.sync().arg("--no-install-project"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 4 packages in [TIME]
Prepared 3 packages in [TIME]
Installed 3 packages in [TIME]
+ anyio==3.7.0
+ idna==3.6
+ sniffio==1.3.1
"###);

Ok(())
}
33 changes: 31 additions & 2 deletions docs/guides/integration/docker.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ If you're using uv to manage your project, you can copy it into the image and in
ADD . /app
WORKDIR /app

# Sync the project into a new environment
RUN uv sync
# Sync the project into a new environment, using the frozen lockfile
RUN uv sync --frozen
```

Once the project is installed, you can either _activate_ the virtual environment:
Expand Down Expand Up @@ -169,3 +169,32 @@ ENV UV_CACHE_DIR=/opt/uv-cache/
```

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`.

`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
a big time saver.

```dockerfile title="Dockerfile"
# Install uv
FROM python:3.12-slim
COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv

# Copy the lockfile into the image
ADD uv.lock /app/uv.lock

# Install dependencies
WORKDIR /app
RUN uv sync --frozen --no-install-project

# Copy the project into the image
ADD . /app
WORKDIR /app

# Sync the project
RUN uv sync --frozen
```
4 changes: 4 additions & 0 deletions docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -1170,6 +1170,10 @@ uv sync [OPTIONS]

</dd><dt><code>--no-index</code></dt><dd><p>Ignore the registry index (e.g., PyPI), instead relying on direct URL dependencies and those provided via <code>--find-links</code></p>

</dd><dt><code>--no-install-project</code></dt><dd><p>Do not install the current project.</p>

<p>By default, the current project is installed into the environment with all of its dependencies. The <code>--no-install-project</code> 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.</p>

</dd><dt><code>--no-progress</code></dt><dd><p>Hide all progress outputs.</p>

<p>For example, spinners or progress bars.</p>
Expand Down

0 comments on commit af2cbc8

Please sign in to comment.