Skip to content

Commit 6e2fbbc

Browse files
authored
Add --no-install-local option to uv sync, uv add and uv export (#15328)
<!-- Thank you for contributing to uv! To help us out with reviewing, please consider the following: - Does this pull request include a summary of the change? (See below.) - Does this pull request include a descriptive title? - Does this pull request include references to any relevant issues? --> ## Summary <!-- What's the purpose of the change? What does it do, and why? --> Closes #14866. Adds a `no-install-local` flag to the sync and export commands that excludes locally defined packages from being installed. This helps with if you're caching your virtual environment. You can exclude local packages since they're more likely to change between builds. ## Test Plan snapshot test: `sync::no_install_local` CI ## Notes I made an `InstallOptions` struct to avoid a crate isolation issue I was running into while implementing. Thanks for maintaining this project!
1 parent f231d0f commit 6e2fbbc

File tree

10 files changed

+185
-14
lines changed

10 files changed

+185
-14
lines changed

crates/uv-cli/src/lib.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3435,6 +3435,14 @@ pub struct SyncArgs {
34353435
#[arg(long)]
34363436
pub no_install_workspace: bool,
34373437

3438+
/// Do not install local path dependencies
3439+
///
3440+
/// Skips the current project, workspace members, and any other local (path or editable)
3441+
/// packages. Only remote/indexed dependencies are installed. Useful in Docker builds to cache
3442+
/// heavy third-party dependencies first and layer local packages separately.
3443+
#[arg(long)]
3444+
pub no_install_local: bool,
3445+
34383446
/// Do not install the given package(s).
34393447
///
34403448
/// By default, all of the project's dependencies are installed into the environment. The
@@ -3503,6 +3511,7 @@ pub struct SyncArgs {
35033511
conflicts_with = "package",
35043512
conflicts_with = "no_install_project",
35053513
conflicts_with = "no_install_workspace",
3514+
conflicts_with = "no_install_local",
35063515
conflicts_with = "extra",
35073516
conflicts_with = "all_extras",
35083517
conflicts_with = "no_extra",
@@ -3847,6 +3856,14 @@ pub struct AddArgs {
38473856
/// allows optimal layer caching.
38483857
#[arg(long, conflicts_with = "frozen", conflicts_with = "no_sync")]
38493858
pub no_install_workspace: bool,
3859+
3860+
/// Do not install local path dependencies
3861+
///
3862+
/// Skips the current project, workspace members, and any other local (path or editable)
3863+
/// packages. Only remote/indexed dependencies are installed. Useful in Docker builds to cache
3864+
/// heavy third-party dependencies first and layer local packages separately.
3865+
#[arg(long, conflicts_with = "frozen", conflicts_with = "no_sync")]
3866+
pub no_install_local: bool,
38503867
}
38513868

38523869
#[derive(Args)]
@@ -4239,6 +4256,14 @@ pub struct ExportArgs {
42394256
#[arg(long, alias = "no-install-workspace")]
42404257
pub no_emit_workspace: bool,
42414258

4259+
/// Do not include local path dependencies in the exported requirements.
4260+
///
4261+
/// Omits the current project, workspace members, and any other local (path or editable)
4262+
/// packages from the export. Only remote/indexed dependencies are written. Useful for Docker
4263+
/// and CI flows that want to export and cache third-party dependencies first.
4264+
#[arg(long, alias = "no-install-local")]
4265+
pub no_emit_local: bool,
4266+
42424267
/// Do not emit the given package(s).
42434268
///
42444269
/// By default, all of the project's dependencies are included in the exported requirements

crates/uv-configuration/src/install_options.rs

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,23 @@ use tracing::debug;
44

55
use uv_normalize::PackageName;
66

7+
/// Minimal view of a package used to apply install filters.
8+
#[derive(Debug, Clone, Copy)]
9+
pub struct InstallTarget<'a> {
10+
/// The package name.
11+
pub name: &'a PackageName,
12+
/// Whether the package refers to a local source (path, directory, editable, etc.).
13+
pub is_local: bool,
14+
}
15+
716
#[derive(Debug, Clone, Default)]
817
pub struct InstallOptions {
918
/// Omit the project itself from the resolution.
1019
pub no_install_project: bool,
1120
/// Omit all workspace members (including the project itself) from the resolution.
1221
pub no_install_workspace: bool,
22+
/// Omit all local packages from the resolution.
23+
pub no_install_local: bool,
1324
/// Omit the specified packages from the resolution.
1425
pub no_install_package: Vec<PackageName>,
1526
}
@@ -18,27 +29,32 @@ impl InstallOptions {
1829
pub fn new(
1930
no_install_project: bool,
2031
no_install_workspace: bool,
32+
no_install_local: bool,
2133
no_install_package: Vec<PackageName>,
2234
) -> Self {
2335
Self {
2436
no_install_project,
2537
no_install_workspace,
38+
no_install_local,
2639
no_install_package,
2740
}
2841
}
2942

3043
/// Returns `true` if a package passes the install filters.
3144
pub fn include_package(
3245
&self,
33-
package: &PackageName,
46+
target: InstallTarget<'_>,
3447
project_name: Option<&PackageName>,
3548
members: &BTreeSet<PackageName>,
3649
) -> bool {
50+
let package_name = target.name;
3751
// If `--no-install-project` is set, remove the project itself.
3852
if self.no_install_project {
3953
if let Some(project_name) = project_name {
40-
if package == project_name {
41-
debug!("Omitting `{package}` from resolution due to `--no-install-project`");
54+
if package_name == project_name {
55+
debug!(
56+
"Omitting `{package_name}` from resolution due to `--no-install-project`"
57+
);
4258
return false;
4359
}
4460
}
@@ -51,24 +67,32 @@ impl InstallOptions {
5167
// is set.)
5268
if !self.no_install_project {
5369
if let Some(project_name) = project_name {
54-
if package == project_name {
70+
if package_name == project_name {
5571
debug!(
56-
"Omitting `{package}` from resolution due to `--no-install-workspace`"
72+
"Omitting `{package_name}` from resolution due to `--no-install-workspace`"
5773
);
5874
return false;
5975
}
6076
}
6177
}
6278

63-
if members.contains(package) {
64-
debug!("Omitting `{package}` from resolution due to `--no-install-workspace`");
79+
if members.contains(package_name) {
80+
debug!("Omitting `{package_name}` from resolution due to `--no-install-workspace`");
81+
return false;
82+
}
83+
}
84+
85+
// If `--no-install-local` is set, remove local packages.
86+
if self.no_install_local {
87+
if target.is_local {
88+
debug!("Omitting `{package_name}` from resolution due to `--no-install-local`");
6589
return false;
6690
}
6791
}
6892

6993
// If `--no-install-package` is provided, remove the requested packages.
70-
if self.no_install_package.contains(package) {
71-
debug!("Omitting `{package}` from resolution due to `--no-install-package`");
94+
if self.no_install_package.contains(package_name) {
95+
debug!("Omitting `{package_name}` from resolution due to `--no-install-package`");
7296
return false;
7397
}
7498

crates/uv-resolver/src/lock/export/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,7 @@ impl<'lock> ExportableRequirements<'lock> {
303303
})
304304
.filter(|(_index, package)| {
305305
install_options.include_package(
306-
&package.id.name,
306+
package.as_install_target(),
307307
target.project_name(),
308308
target.lock().members(),
309309
)

crates/uv-resolver/src/lock/installable.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -563,7 +563,7 @@ pub trait Installable<'lock> {
563563
install_options: &InstallOptions,
564564
) -> Result<Node, LockError> {
565565
if install_options.include_package(
566-
package.name(),
566+
package.as_install_target(),
567567
self.project_name(),
568568
self.lock().members(),
569569
) {

crates/uv-resolver/src/lock/mod.rs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ use tracing::debug;
1919
use url::Url;
2020

2121
use uv_cache_key::RepositoryUrl;
22-
use uv_configuration::{BuildOptions, Constraints};
22+
use uv_configuration::{BuildOptions, Constraints, InstallTarget};
2323
use uv_distribution::{DistributionDatabase, FlatRequiresDist};
2424
use uv_distribution_filename::{
2525
BuildTag, DistExtension, ExtensionError, SourceDistExtension, WheelFilename,
@@ -3123,6 +3123,14 @@ impl Package {
31233123
pub fn resolved_dependency_groups(&self) -> &BTreeMap<GroupName, Vec<Dependency>> {
31243124
&self.dependency_groups
31253125
}
3126+
3127+
/// Returns an [`InstallTarget`] view for filtering decisions.
3128+
pub fn as_install_target(&self) -> InstallTarget<'_> {
3129+
InstallTarget {
3130+
name: self.name(),
3131+
is_local: self.id.source.is_local(),
3132+
}
3133+
}
31263134
}
31273135

31283136
/// Attempts to construct a `VerbatimUrl` from the given normalized `Path`.
@@ -3688,6 +3696,14 @@ impl Source {
36883696
}
36893697
}
36903698
}
3699+
3700+
/// Check if a package is local by examining its source.
3701+
pub(crate) fn is_local(&self) -> bool {
3702+
matches!(
3703+
self,
3704+
Self::Path(_) | Self::Directory(_) | Self::Editable(_) | Self::Virtual(_)
3705+
)
3706+
}
36913707
}
36923708

36933709
#[derive(Clone, Debug, serde::Deserialize)]

crates/uv/src/commands/project/add.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ pub(crate) async fn add(
7171
no_sync: bool,
7272
no_install_project: bool,
7373
no_install_workspace: bool,
74+
no_install_local: bool,
7475
requirements: Vec<RequirementsSource>,
7576
constraints: Vec<RequirementsSource>,
7677
marker: Option<MarkerTree>,
@@ -738,6 +739,7 @@ pub(crate) async fn add(
738739
locked,
739740
no_install_project,
740741
no_install_workspace,
742+
no_install_local,
741743
&defaulted_extras,
742744
&defaulted_groups,
743745
raw,
@@ -968,6 +970,7 @@ async fn lock_and_sync(
968970
locked: bool,
969971
no_install_project: bool,
970972
no_install_workspace: bool,
973+
no_install_local: bool,
971974
extras: &ExtrasSpecificationWithDefaults,
972975
groups: &DependencyGroupsWithDefaults,
973976
raw: bool,
@@ -1154,7 +1157,12 @@ async fn lock_and_sync(
11541157
extras,
11551158
groups,
11561159
EditableMode::Editable,
1157-
InstallOptions::new(no_install_project, no_install_workspace, vec![]),
1160+
InstallOptions::new(
1161+
no_install_project,
1162+
no_install_workspace,
1163+
no_install_local,
1164+
vec![],
1165+
),
11581166
Modifications::Sufficient,
11591167
None,
11601168
settings.into(),

crates/uv/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1978,6 +1978,7 @@ async fn run_project(
19781978
args.no_sync,
19791979
args.no_install_project,
19801980
args.no_install_workspace,
1981+
args.no_install_local,
19811982
requirements,
19821983
constraints,
19831984
args.marker,

crates/uv/src/settings.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1221,6 +1221,7 @@ impl SyncSettings {
12211221
exact,
12221222
no_install_project,
12231223
no_install_workspace,
1224+
no_install_local,
12241225
no_install_package,
12251226
locked,
12261227
frozen,
@@ -1286,6 +1287,7 @@ impl SyncSettings {
12861287
install_options: InstallOptions::new(
12871288
no_install_project,
12881289
no_install_workspace,
1290+
no_install_local,
12891291
no_install_package,
12901292
),
12911293
modifications: if flag(exact, inexact, "inexact").unwrap_or(true) {
@@ -1377,6 +1379,7 @@ pub(crate) struct AddSettings {
13771379
pub(crate) workspace: Option<bool>,
13781380
pub(crate) no_install_project: bool,
13791381
pub(crate) no_install_workspace: bool,
1382+
pub(crate) no_install_local: bool,
13801383
pub(crate) install_mirrors: PythonInstallMirrors,
13811384
pub(crate) refresh: Refresh,
13821385
pub(crate) indexes: Vec<Index>,
@@ -1418,6 +1421,7 @@ impl AddSettings {
14181421
no_workspace,
14191422
no_install_project,
14201423
no_install_workspace,
1424+
no_install_local,
14211425
} = args;
14221426

14231427
let dependency_type = if let Some(extra) = optional {
@@ -1521,6 +1525,7 @@ impl AddSettings {
15211525
workspace: flag(workspace, no_workspace, "workspace"),
15221526
no_install_project,
15231527
no_install_workspace,
1528+
no_install_local,
15241529
editable: flag(editable, no_editable, "editable"),
15251530
extras: extra.unwrap_or_default(),
15261531
refresh: Refresh::from(refresh),
@@ -1820,6 +1825,7 @@ impl ExportSettings {
18201825
no_emit_project,
18211826
no_emit_workspace,
18221827
no_emit_package,
1828+
no_emit_local,
18231829
locked,
18241830
frozen,
18251831
resolver,
@@ -1862,6 +1868,7 @@ impl ExportSettings {
18621868
install_options: InstallOptions::new(
18631869
no_emit_project,
18641870
no_emit_workspace,
1871+
no_emit_local,
18651872
no_emit_package,
18661873
),
18671874
output_file,

0 commit comments

Comments
 (0)