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..6d4d49dc6 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,50 @@ 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.refresh, + 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 +470,7 @@ async fn do_lock( workspace_cache: &WorkspaceCache, printer: Printer, preview: Preview, + force: bool, ) -> Result { let start = std::time::Instant::now(); @@ -767,6 +804,7 @@ async fn do_lock( state.index(), &database, printer, + force, ) .await { @@ -986,11 +1024,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(), 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..23c90ff75 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 dcd1d1b11..42bb8a0ed 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -1940,6 +1940,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/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 { 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: