Skip to content

Commit 1445669

Browse files
Use lockfile versions as resolution preferences (#3921)
## Summary Ensures that we avoid upgrading packages unless `--upgrade` or similar is passed. For now, the resolver only respects these for registry distributions. Closes #3918.
1 parent 502e042 commit 1445669

8 files changed

Lines changed: 333 additions & 13 deletions

File tree

crates/uv-resolver/src/lock.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@ impl Lock {
5252
Lock::try_from(wire)
5353
}
5454

55+
/// Returns the [`Distribution`] entries in this lock.
56+
pub fn distributions(&self) -> &[Distribution] {
57+
&self.distributions
58+
}
59+
5560
pub fn to_resolution(
5661
&self,
5762
marker_env: &MarkerEnvironment,
@@ -202,7 +207,7 @@ impl TryFrom<LockWire> for Lock {
202207
}
203208

204209
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
205-
pub(crate) struct Distribution {
210+
pub struct Distribution {
206211
#[serde(flatten)]
207212
pub(crate) id: DistributionId,
208213
#[serde(default)]

crates/uv-resolver/src/preferences.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,16 @@ impl Preference {
8686
}
8787
}
8888

89+
/// Create a [`Preference`] from a locked distribution.
90+
pub fn from_lock(dist: &crate::lock::Distribution) -> Self {
91+
Self {
92+
name: dist.id.name.clone(),
93+
version: dist.id.version.clone(),
94+
marker: None,
95+
hashes: Vec::new(),
96+
}
97+
}
98+
8999
/// Return the [`PackageName`] of the package for this [`Preference`].
90100
pub fn name(&self) -> &PackageName {
91101
&self.name

crates/uv/src/cli.rs

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -372,8 +372,6 @@ pub(crate) struct PipCompileArgs {
372372
#[arg(long, env = "UV_CUSTOM_COMPILE_COMMAND")]
373373
pub(crate) custom_compile_command: Option<String>,
374374

375-
/// Run offline, i.e., without accessing the network.
376-
377375
/// Refresh all cached data.
378376
#[arg(long, conflicts_with("offline"), overrides_with("no_refresh"))]
379377
pub(crate) refresh: bool,
@@ -1786,6 +1784,33 @@ pub(crate) struct RunArgs {
17861784
#[arg(long)]
17871785
pub(crate) with: Vec<String>,
17881786

1787+
/// Refresh all cached data.
1788+
#[arg(long, conflicts_with("offline"), overrides_with("no_refresh"))]
1789+
pub(crate) refresh: bool,
1790+
1791+
#[arg(
1792+
long,
1793+
conflicts_with("offline"),
1794+
overrides_with("refresh"),
1795+
hide = true
1796+
)]
1797+
pub(crate) no_refresh: bool,
1798+
1799+
/// Refresh cached data for a specific package.
1800+
#[arg(long)]
1801+
pub(crate) refresh_package: Vec<PackageName>,
1802+
1803+
/// Allow package upgrades, ignoring pinned versions in the existing lockfile.
1804+
#[arg(long, short = 'U', overrides_with("no_upgrade"))]
1805+
pub(crate) upgrade: bool,
1806+
1807+
#[arg(long, overrides_with("upgrade"), hide = true)]
1808+
pub(crate) no_upgrade: bool,
1809+
1810+
/// Allow upgrades for a specific package, ignoring pinned versions in the existing lockfile.
1811+
#[arg(long, short = 'P')]
1812+
pub(crate) upgrade_package: Vec<PackageName>,
1813+
17891814
/// The Python interpreter to use to build the run environment.
17901815
///
17911816
/// By default, `uv` uses the virtual environment in the current working directory or any parent
@@ -1822,6 +1847,22 @@ pub(crate) struct SyncArgs {
18221847
#[arg(long, overrides_with("all_extras"), hide = true)]
18231848
pub(crate) no_all_extras: bool,
18241849

1850+
/// Refresh all cached data.
1851+
#[arg(long, conflicts_with("offline"), overrides_with("no_refresh"))]
1852+
pub(crate) refresh: bool,
1853+
1854+
#[arg(
1855+
long,
1856+
conflicts_with("offline"),
1857+
overrides_with("refresh"),
1858+
hide = true
1859+
)]
1860+
pub(crate) no_refresh: bool,
1861+
1862+
/// Refresh cached data for a specific package.
1863+
#[arg(long)]
1864+
pub(crate) refresh_package: Vec<PackageName>,
1865+
18251866
/// The Python interpreter to use to build the run environment.
18261867
///
18271868
/// By default, `uv` uses the virtual environment in the current working directory or any parent
@@ -1840,6 +1881,33 @@ pub(crate) struct SyncArgs {
18401881
#[derive(Args)]
18411882
#[allow(clippy::struct_excessive_bools)]
18421883
pub(crate) struct LockArgs {
1884+
/// Refresh all cached data.
1885+
#[arg(long, conflicts_with("offline"), overrides_with("no_refresh"))]
1886+
pub(crate) refresh: bool,
1887+
1888+
#[arg(
1889+
long,
1890+
conflicts_with("offline"),
1891+
overrides_with("refresh"),
1892+
hide = true
1893+
)]
1894+
pub(crate) no_refresh: bool,
1895+
1896+
/// Refresh cached data for a specific package.
1897+
#[arg(long)]
1898+
pub(crate) refresh_package: Vec<PackageName>,
1899+
1900+
/// Allow package upgrades, ignoring pinned versions in the existing lockfile.
1901+
#[arg(long, short = 'U', overrides_with("no_upgrade"))]
1902+
pub(crate) upgrade: bool,
1903+
1904+
#[arg(long, overrides_with("upgrade"), hide = true)]
1905+
pub(crate) no_upgrade: bool,
1906+
1907+
/// Allow upgrades for a specific package, ignoring pinned versions in the existing lockfile.
1908+
#[arg(long, short = 'P')]
1909+
pub(crate) upgrade_package: Vec<PackageName>,
1910+
18431911
/// The Python interpreter to use to build the run environment.
18441912
///
18451913
/// By default, `uv` uses the virtual environment in the current working directory or any parent

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

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use uv_configuration::{
1212
use uv_dispatch::BuildDispatch;
1313
use uv_interpreter::PythonEnvironment;
1414
use uv_requirements::ProjectWorkspace;
15-
use uv_resolver::{ExcludeNewer, FlatIndex, InMemoryIndex, Lock, OptionsBuilder};
15+
use uv_resolver::{ExcludeNewer, FlatIndex, InMemoryIndex, Lock, OptionsBuilder, Preference};
1616
use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy, InFlight};
1717
use uv_warnings::warn_user;
1818

@@ -23,6 +23,7 @@ use crate::printer::Printer;
2323
/// Resolve the project requirements into a lockfile.
2424
#[allow(clippy::too_many_arguments)]
2525
pub(crate) async fn lock(
26+
upgrade: Upgrade,
2627
exclude_newer: Option<ExcludeNewer>,
2728
preview: PreviewMode,
2829
cache: &Cache,
@@ -39,7 +40,7 @@ pub(crate) async fn lock(
3940
let venv = project::init_environment(&project, preview, cache, printer)?;
4041

4142
// Perform the lock operation.
42-
match do_lock(&project, &venv, exclude_newer, cache, printer).await {
43+
match do_lock(&project, &venv, upgrade, exclude_newer, cache, printer).await {
4344
Ok(_) => Ok(ExitStatus::Success),
4445
Err(ProjectError::Operation(pip::operations::Error::Resolve(
4546
uv_resolver::ResolveError::NoSolution(err),
@@ -57,6 +58,7 @@ pub(crate) async fn lock(
5758
pub(super) async fn do_lock(
5859
project: &ProjectWorkspace,
5960
venv: &PythonEnvironment,
61+
upgrade: Upgrade,
6062
exclude_newer: Option<ExcludeNewer>,
6163
cache: &Cache,
6264
printer: Printer,
@@ -96,14 +98,34 @@ pub(super) async fn do_lock(
9698
let link_mode = LinkMode::default();
9799
let no_binary = NoBinary::default();
98100
let no_build = NoBuild::default();
99-
let preferences = Vec::default();
100101
let reinstall = Reinstall::default();
101102
let setup_py = SetupPyStrategy::default();
102-
let upgrade = Upgrade::default();
103103

104104
let hasher = HashStrategy::Generate;
105105
let options = OptionsBuilder::new().exclude_newer(exclude_newer).build();
106106

107+
// If an existing lockfile exists, build up a set of preferences.
108+
let lockfile = project.workspace().root().join("uv.lock");
109+
let lock = match fs_err::tokio::read_to_string(&lockfile).await {
110+
Ok(encoded) => match toml::from_str::<Lock>(&encoded) {
111+
Ok(lock) => Some(lock),
112+
Err(err) => {
113+
eprint!("Failed to parse lockfile; ignoring locked requirements: {err}");
114+
None
115+
}
116+
},
117+
Err(err) if err.kind() == std::io::ErrorKind::NotFound => None,
118+
Err(err) => return Err(err.into()),
119+
};
120+
let preferences: Vec<Preference> = lock
121+
.map(|lock| {
122+
lock.distributions()
123+
.iter()
124+
.map(Preference::from_lock)
125+
.collect()
126+
})
127+
.unwrap_or_default();
128+
107129
// Create a build dispatch.
108130
let build_dispatch = BuildDispatch::new(
109131
&client,

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use tracing::debug;
99

1010
use uv_cache::Cache;
1111
use uv_client::Connectivity;
12-
use uv_configuration::{ExtrasSpecification, PreviewMode};
12+
use uv_configuration::{ExtrasSpecification, PreviewMode, Upgrade};
1313
use uv_interpreter::{PythonEnvironment, SystemPython};
1414
use uv_requirements::{ProjectWorkspace, RequirementsSource};
1515
use uv_resolver::ExcludeNewer;
@@ -26,6 +26,7 @@ pub(crate) async fn run(
2626
mut args: Vec<OsString>,
2727
requirements: Vec<RequirementsSource>,
2828
python: Option<String>,
29+
upgrade: Upgrade,
2930
exclude_newer: Option<ExcludeNewer>,
3031
isolated: bool,
3132
preview: PreviewMode,
@@ -47,7 +48,8 @@ pub(crate) async fn run(
4748
let venv = project::init_environment(&project, preview, cache, printer)?;
4849

4950
// Lock and sync the environment.
50-
let lock = project::lock::do_lock(&project, &venv, exclude_newer, cache, printer).await?;
51+
let lock =
52+
project::lock::do_lock(&project, &venv, upgrade, exclude_newer, cache, printer).await?;
5153
project::sync::do_sync(&project, &venv, &lock, extras, cache, printer).await?;
5254

5355
Some(venv)

crates/uv/src/main.rs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -550,7 +550,7 @@ async fn run() -> Result<ExitStatus> {
550550
let args = settings::RunSettings::resolve(args, workspace);
551551

552552
// Initialize the cache.
553-
let cache = cache.init()?;
553+
let cache = cache.init()?.with_refresh(args.refresh);
554554

555555
let requirements = args
556556
.with
@@ -578,6 +578,7 @@ async fn run() -> Result<ExitStatus> {
578578
args.args,
579579
requirements,
580580
args.python,
581+
args.upgrade,
581582
args.exclude_newer,
582583
globals.isolated,
583584
globals.preview,
@@ -592,7 +593,7 @@ async fn run() -> Result<ExitStatus> {
592593
let args = settings::SyncSettings::resolve(args, workspace);
593594

594595
// Initialize the cache.
595-
let cache = cache.init()?;
596+
let cache = cache.init()?.with_refresh(args.refresh);
596597

597598
commands::sync(args.extras, globals.preview, &cache, printer).await
598599
}
@@ -601,9 +602,16 @@ async fn run() -> Result<ExitStatus> {
601602
let args = settings::LockSettings::resolve(args, workspace);
602603

603604
// Initialize the cache.
604-
let cache = cache.init()?;
605+
let cache = cache.init()?.with_refresh(args.refresh);
605606

606-
commands::lock(args.exclude_newer, globals.preview, &cache, printer).await
607+
commands::lock(
608+
args.upgrade,
609+
args.exclude_newer,
610+
globals.preview,
611+
&cache,
612+
printer,
613+
)
614+
.await
607615
}
608616
#[cfg(feature = "self-update")]
609617
Commands::Self_(SelfNamespace {

crates/uv/src/settings.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,8 @@ pub(crate) struct RunSettings {
102102
pub(crate) args: Vec<OsString>,
103103
pub(crate) with: Vec<String>,
104104
pub(crate) python: Option<String>,
105+
pub(crate) refresh: Refresh,
106+
pub(crate) upgrade: Upgrade,
105107
pub(crate) exclude_newer: Option<ExcludeNewer>,
106108
}
107109

@@ -116,11 +118,19 @@ impl RunSettings {
116118
target,
117119
args,
118120
with,
121+
refresh,
122+
no_refresh,
123+
refresh_package,
124+
upgrade,
125+
no_upgrade,
126+
upgrade_package,
119127
python,
120128
exclude_newer,
121129
} = args;
122130

123131
Self {
132+
refresh: Refresh::from_args(flag(refresh, no_refresh), refresh_package),
133+
upgrade: Upgrade::from_args(flag(upgrade, no_upgrade), upgrade_package),
124134
extras: ExtrasSpecification::from_args(
125135
flag(all_extras, no_all_extras).unwrap_or_default(),
126136
extra.unwrap_or_default(),
@@ -138,6 +148,7 @@ impl RunSettings {
138148
#[allow(clippy::struct_excessive_bools, dead_code)]
139149
#[derive(Debug, Clone)]
140150
pub(crate) struct SyncSettings {
151+
pub(crate) refresh: Refresh,
141152
pub(crate) extras: ExtrasSpecification,
142153
pub(crate) python: Option<String>,
143154
}
@@ -150,10 +161,14 @@ impl SyncSettings {
150161
extra,
151162
all_extras,
152163
no_all_extras,
164+
refresh,
165+
no_refresh,
166+
refresh_package,
153167
python,
154168
} = args;
155169

156170
Self {
171+
refresh: Refresh::from_args(flag(refresh, no_refresh), refresh_package),
157172
extras: ExtrasSpecification::from_args(
158173
flag(all_extras, no_all_extras).unwrap_or_default(),
159174
extra.unwrap_or_default(),
@@ -167,6 +182,8 @@ impl SyncSettings {
167182
#[allow(clippy::struct_excessive_bools, dead_code)]
168183
#[derive(Debug, Clone)]
169184
pub(crate) struct LockSettings {
185+
pub(crate) refresh: Refresh,
186+
pub(crate) upgrade: Upgrade,
170187
pub(crate) exclude_newer: Option<ExcludeNewer>,
171188
pub(crate) python: Option<String>,
172189
}
@@ -176,11 +193,19 @@ impl LockSettings {
176193
#[allow(clippy::needless_pass_by_value)]
177194
pub(crate) fn resolve(args: LockArgs, _workspace: Option<Workspace>) -> Self {
178195
let LockArgs {
196+
refresh,
197+
no_refresh,
198+
refresh_package,
199+
upgrade,
200+
no_upgrade,
201+
upgrade_package,
179202
exclude_newer,
180203
python,
181204
} = args;
182205

183206
Self {
207+
refresh: Refresh::from_args(flag(refresh, no_refresh), refresh_package),
208+
upgrade: Upgrade::from_args(flag(upgrade, no_upgrade), upgrade_package),
184209
exclude_newer,
185210
python,
186211
}

0 commit comments

Comments
 (0)