From 1bb547b6d4c3fe415aaae1c70e6f981cd0f8a635 Mon Sep 17 00:00:00 2001 From: 11happy Date: Tue, 9 Sep 2025 21:02:24 +0530 Subject: [PATCH 1/5] feat: implement --force flag for lock Signed-off-by: 11happy --- crates/uv-cli/src/lib.rs | 4 ++ crates/uv/src/commands/project/add.rs | 4 +- crates/uv/src/commands/project/export.rs | 2 +- crates/uv/src/commands/project/lock.rs | 56 +++++++++++++++++++---- crates/uv/src/commands/project/remove.rs | 2 +- crates/uv/src/commands/project/run.rs | 4 +- crates/uv/src/commands/project/sync.rs | 4 +- crates/uv/src/commands/project/tree.rs | 2 +- crates/uv/src/commands/project/version.rs | 2 +- crates/uv/src/lib.rs | 1 + crates/uv/src/settings.rs | 3 ++ docs/reference/cli.md | 3 +- 12 files changed, 67 insertions(+), 20 deletions(-) diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index 2c43f7717..7537fc464 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -3728,6 +3728,10 @@ pub struct LockArgs { #[arg(long, conflicts_with = "check_exists", conflicts_with = "check")] pub dry_run: bool, + /// Force rewrite + #[arg(long, conflicts_with = "dry_run")] + pub force: bool, + /// Lock the specified Python script, rather than the current project. /// /// If provided, uv will lock the script (based on its inline metadata table, in adherence with diff --git a/crates/uv/src/commands/project/add.rs b/crates/uv/src/commands/project/add.rs index c449416e4..eb47880b8 100644 --- a/crates/uv/src/commands/project/add.rs +++ b/crates/uv/src/commands/project/add.rs @@ -980,7 +980,7 @@ async fn lock_and_sync( if locked { LockMode::Locked(target.interpreter()) } else { - LockMode::Write(target.interpreter()) + LockMode::Write(target.interpreter(), false) }, &settings.resolver, client_builder, @@ -1102,7 +1102,7 @@ async fn lock_and_sync( if locked { LockMode::Locked(target.interpreter()) } else { - LockMode::Write(target.interpreter()) + LockMode::Write(target.interpreter(), false) }, &settings.resolver, client_builder, diff --git a/crates/uv/src/commands/project/export.rs b/crates/uv/src/commands/project/export.rs index 600374218..700452405 100644 --- a/crates/uv/src/commands/project/export.rs +++ b/crates/uv/src/commands/project/export.rs @@ -180,7 +180,7 @@ pub(crate) async fn export( // If we're locking a script, avoid creating a lockfile if it doesn't already exist. LockMode::DryRun(interpreter.as_ref().unwrap()) } else { - LockMode::Write(interpreter.as_ref().unwrap()) + LockMode::Write(interpreter.as_ref().unwrap(), false) }; // Initialize any shared state. diff --git a/crates/uv/src/commands/project/lock.rs b/crates/uv/src/commands/project/lock.rs index b8a105522..aabd12147 100644 --- a/crates/uv/src/commands/project/lock.rs +++ b/crates/uv/src/commands/project/lock.rs @@ -85,6 +85,7 @@ pub(crate) async fn lock( frozen: bool, dry_run: DryRun, refresh: Refresh, + force: bool, python: Option, install_mirrors: PythonInstallMirrors, settings: ResolverSettings, @@ -182,7 +183,7 @@ pub(crate) async fn lock( } else if dry_run.enabled() { LockMode::DryRun(&interpreter) } else { - LockMode::Write(&interpreter) + LockMode::Write(&interpreter, force) } }; @@ -253,7 +254,7 @@ pub(crate) async fn lock( #[derive(Debug, Clone, Copy)] pub(super) enum LockMode<'env> { /// Write the lockfile to disk. - Write(&'env Interpreter), + Write(&'env Interpreter, bool), /// Perform a resolution, but don't write the lockfile to disk. DryRun(&'env Interpreter), /// Error if the lockfile is not up-to-date with the project requirements. @@ -359,6 +360,7 @@ impl<'env> LockOperation<'env> { self.workspace_cache, self.printer, self.preview, + false, ) .await?; @@ -372,7 +374,7 @@ impl<'env> LockOperation<'env> { Ok(result) } - LockMode::Write(interpreter) | LockMode::DryRun(interpreter) => { + LockMode::Write(interpreter, force) => { // Read the existing lockfile. let existing = match target.read().await { Ok(Some(existing)) => Some(existing), @@ -402,16 +404,49 @@ impl<'env> LockOperation<'env> { self.workspace_cache, self.printer, self.preview, + force, ) .await?; - // If the lockfile changed, write it to disk. - if !matches!(self.mode, LockMode::DryRun(_)) { - if let LockResult::Changed(_, lock) = &result { - target.commit(lock).await?; - } + if let LockResult::Changed(_, lock) = &result { + target.commit(lock).await?; } + Ok(result) + } + LockMode::DryRun(interpreter) => { + // Read the existing lockfile. + let existing = match target.read().await { + Ok(Some(existing)) => Some(existing), + Ok(None) => None, + Err(ProjectError::Lock(err)) => { + warn_user!( + "Failed to read existing lockfile; ignoring locked requirements: {err}" + ); + None + } + Err(err) => return Err(err), + }; + + // Perform the lock operation, but don't write the lockfile to disk. + let result = do_lock( + target, + interpreter, + existing, + self.constraints, + self.settings, + self.client_builder, + self.state, + self.logger, + self.concurrency, + self.cache, + self.workspace_cache, + self.printer, + self.preview, + false, + ) + .await?; + Ok(result) } } @@ -434,6 +469,7 @@ async fn do_lock( workspace_cache: &WorkspaceCache, printer: Printer, preview: Preview, + force: bool, ) -> Result { let start = std::time::Instant::now(); @@ -741,7 +777,9 @@ async fn do_lock( let database = DistributionDatabase::new(&client, &build_dispatch, concurrency.downloads); // If any of the resolution-determining settings changed, invalidate the lock. - let existing_lock = if let Some(existing_lock) = existing_lock { + let existing_lock = if force { + None + } else if let Some(existing_lock) = existing_lock { match ValidatedLock::validate( existing_lock, target.install_path(), diff --git a/crates/uv/src/commands/project/remove.rs b/crates/uv/src/commands/project/remove.rs index 03a3f04fc..7bbfa1d48 100644 --- a/crates/uv/src/commands/project/remove.rs +++ b/crates/uv/src/commands/project/remove.rs @@ -294,7 +294,7 @@ pub(crate) async fn remove( let mode = if locked { LockMode::Locked(target.interpreter()) } else { - LockMode::Write(target.interpreter()) + LockMode::Write(target.interpreter(), false) }; // Initialize any shared state. diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index a6ff4caca..0a3e07392 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -266,7 +266,7 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl } else if locked { LockMode::Locked(environment.interpreter()) } else { - LockMode::Write(environment.interpreter()) + LockMode::Write(environment.interpreter(), false) }; // Generate a lockfile. @@ -744,7 +744,7 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl } else if isolated { LockMode::DryRun(venv.interpreter()) } else { - LockMode::Write(venv.interpreter()) + LockMode::Write(venv.interpreter(), false) }; let result = match project::lock::LockOperation::new( diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index 27b3c70e6..4ecd67aa1 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -309,7 +309,7 @@ pub(crate) async fn sync( } else if dry_run.enabled() { LockMode::DryRun(environment.interpreter()) } else { - LockMode::Write(environment.interpreter()) + LockMode::Write(environment.interpreter(), false) }; let lock_target = match &target { @@ -1272,7 +1272,7 @@ impl From<(&LockTarget<'_>, &LockMode<'_>, &Outcome)> for LockReport { LockResult::Unchanged(..) => match mode { // When `--frozen` is used, we don't check the lockfile LockMode::Frozen => LockAction::Use, - LockMode::DryRun(_) | LockMode::Locked(_) | LockMode::Write(_) => { + LockMode::DryRun(_) | LockMode::Locked(_) | LockMode::Write(_, ..) => { LockAction::Check } }, diff --git a/crates/uv/src/commands/project/tree.rs b/crates/uv/src/commands/project/tree.rs index 667415fc0..990792f02 100644 --- a/crates/uv/src/commands/project/tree.rs +++ b/crates/uv/src/commands/project/tree.rs @@ -131,7 +131,7 @@ pub(crate) async fn tree( // If we're locking a script, avoid creating a lockfile if it doesn't already exist. LockMode::DryRun(interpreter.as_ref().unwrap()) } else { - LockMode::Write(interpreter.as_ref().unwrap()) + LockMode::Write(interpreter.as_ref().unwrap(), false) }; // Initialize any shared state. diff --git a/crates/uv/src/commands/project/version.rs b/crates/uv/src/commands/project/version.rs index 09a6810b7..6bad35263 100644 --- a/crates/uv/src/commands/project/version.rs +++ b/crates/uv/src/commands/project/version.rs @@ -582,7 +582,7 @@ async fn lock_and_sync( let mode = if locked { LockMode::Locked(target.interpreter()) } else { - LockMode::Write(target.interpreter()) + LockMode::Write(target.interpreter(), false) }; // Initialize any shared state. diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index ddb03c4bc..e8a36d51a 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -1934,6 +1934,7 @@ async fn run_project( args.frozen, args.dry_run, args.refresh, + args.force, args.python, args.install_mirrors, args.settings, diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index 3da6397a7..ec89691aa 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -1381,6 +1381,7 @@ pub(crate) struct LockSettings { pub(crate) locked: bool, pub(crate) frozen: bool, pub(crate) dry_run: DryRun, + pub(crate) force: bool, pub(crate) script: Option, pub(crate) python: Option, pub(crate) install_mirrors: PythonInstallMirrors, @@ -1400,6 +1401,7 @@ impl LockSettings { check, check_exists, dry_run, + force, script, resolver, build, @@ -1416,6 +1418,7 @@ impl LockSettings { locked: check, frozen: check_exists, dry_run: DryRun::from_args(dry_run), + force, script, python: python.and_then(Maybe::into_option), refresh: Refresh::from(refresh), diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 98da105b0..3b0a27558 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -1669,7 +1669,8 @@ uv lock [OPTIONS]

May also be set with the UV_EXTRA_INDEX_URL environment variable.

Locations to search for candidate distributions, in addition to those found in the registry indexes.

If a path, the target must be a directory that contains packages as wheel files (.whl) or source distributions (e.g., .tar.gz or .zip) at the top level.

If a URL, the page must contain a flat list of links to package files adhering to the formats described above.

-

May also be set with the UV_FIND_LINKS environment variable.

--fork-strategy fork-strategy

The strategy to use when selecting multiple versions of a given package across Python versions and platforms.

+

May also be set with the UV_FIND_LINKS environment variable.

--force

Force rewrite

+
--fork-strategy fork-strategy

The strategy to use when selecting multiple versions of a given package across Python versions and platforms.

By default, uv will optimize for selecting the latest version of each package for each supported Python version (requires-python), while minimizing the number of selected versions across platforms.

Under fewest, uv will minimize the number of selected versions for each package, preferring older versions that are compatible with a wider range of supported Python versions or platforms.

May also be set with the UV_FORK_STRATEGY environment variable.

Possible values:

From 41d76e95d8584fdc36598d84e923876fb4c18abc Mon Sep 17 00:00:00 2001 From: 11happy Date: Tue, 9 Sep 2025 21:30:46 +0530 Subject: [PATCH 2/5] chore: fix clippy error Signed-off-by: 11happy --- crates/uv/src/commands/project/sync.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index 4ecd67aa1..23c90ff75 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -1272,7 +1272,7 @@ impl From<(&LockTarget<'_>, &LockMode<'_>, &Outcome)> for LockReport { LockResult::Unchanged(..) => match mode { // When `--frozen` is used, we don't check the lockfile LockMode::Frozen => LockAction::Use, - LockMode::DryRun(_) | LockMode::Locked(_) | LockMode::Write(_, ..) => { + LockMode::DryRun(_) | LockMode::Locked(_) | LockMode::Write(..) => { LockAction::Check } }, From 3941ca648f03253b3b1ced6d0ed7bf89dccb9a5c Mon Sep 17 00:00:00 2001 From: 11happy Date: Mon, 22 Sep 2025 21:56:29 +0530 Subject: [PATCH 3/5] refactor: correct force implementation Signed-off-by: 11happy --- crates/uv/src/commands/project/lock.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/crates/uv/src/commands/project/lock.rs b/crates/uv/src/commands/project/lock.rs index aabd12147..f7bc25d4e 100644 --- a/crates/uv/src/commands/project/lock.rs +++ b/crates/uv/src/commands/project/lock.rs @@ -777,9 +777,7 @@ async fn do_lock( let database = DistributionDatabase::new(&client, &build_dispatch, concurrency.downloads); // If any of the resolution-determining settings changed, invalidate the lock. - let existing_lock = if force { - None - } else if let Some(existing_lock) = existing_lock { + let existing_lock = if let Some(existing_lock) = existing_lock { match ValidatedLock::validate( existing_lock, target.install_path(), @@ -805,6 +803,7 @@ async fn do_lock( state.index(), &database, printer, + force, ) .await { @@ -1024,11 +1023,16 @@ impl ValidatedLock { index: &InMemoryIndex, database: &DistributionDatabase<'_, Context>, printer: Printer, + force: bool, ) -> Result { // Perform checks in a deliberate order, such that the most extreme conditions are tested // first (i.e., every check that returns `Self::Unusable`, followed by every check that // returns `Self::Versions`, followed by every check that returns `Self::Preferable`, and // finally `Self::Satisfies`). + if force { + return Ok(Self::Preferable(lock)); + } + // Start with the most severe condition: a fundamental option changed between resolutions. if lock.resolution_mode() != options.resolution_mode { let _ = writeln!( printer.stderr(), From 6d4acaafe7e862c189f35d083b975b8b13c3a5e2 Mon Sep 17 00:00:00 2001 From: 11happy Date: Mon, 22 Sep 2025 22:17:25 +0530 Subject: [PATCH 4/5] test: update snapshots Signed-off-by: 11happy --- crates/uv/tests/it/show_settings.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/uv/tests/it/show_settings.rs b/crates/uv/tests/it/show_settings.rs index b37952fa9..726ec7837 100644 --- a/crates/uv/tests/it/show_settings.rs +++ b/crates/uv/tests/it/show_settings.rs @@ -9501,6 +9501,7 @@ fn upgrade_project_cli_config_interaction() -> anyhow::Result<()> { locked: false, frozen: false, dry_run: Disabled, + force: false, script: None, python: None, install_mirrors: PythonInstallMirrors { @@ -9618,6 +9619,7 @@ fn upgrade_project_cli_config_interaction() -> anyhow::Result<()> { locked: false, frozen: false, dry_run: Disabled, + force: false, script: None, python: None, install_mirrors: PythonInstallMirrors { @@ -9758,6 +9760,7 @@ fn upgrade_project_cli_config_interaction() -> anyhow::Result<()> { locked: false, frozen: false, dry_run: Disabled, + force: false, script: None, python: None, install_mirrors: PythonInstallMirrors { @@ -9873,6 +9876,7 @@ fn upgrade_project_cli_config_interaction() -> anyhow::Result<()> { locked: false, frozen: false, dry_run: Disabled, + force: false, script: None, python: None, install_mirrors: PythonInstallMirrors { @@ -9978,6 +9982,7 @@ fn upgrade_project_cli_config_interaction() -> anyhow::Result<()> { locked: false, frozen: false, dry_run: Disabled, + force: false, script: None, python: None, install_mirrors: PythonInstallMirrors { @@ -10084,6 +10089,7 @@ fn upgrade_project_cli_config_interaction() -> anyhow::Result<()> { locked: false, frozen: false, dry_run: Disabled, + force: false, script: None, python: None, install_mirrors: PythonInstallMirrors { From 4c16cf9a98b52664c80783ce19a383d990405997 Mon Sep 17 00:00:00 2001 From: 11happy Date: Tue, 30 Sep 2025 22:16:30 +0530 Subject: [PATCH 5/5] fix: add missing argument Signed-off-by: 11happy --- crates/uv/src/commands/project/lock.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/uv/src/commands/project/lock.rs b/crates/uv/src/commands/project/lock.rs index f7bc25d4e..6d4d49dc6 100644 --- a/crates/uv/src/commands/project/lock.rs +++ b/crates/uv/src/commands/project/lock.rs @@ -434,6 +434,7 @@ impl<'env> LockOperation<'env> { interpreter, existing, self.constraints, + self.refresh, self.settings, self.client_builder, self.state,