Skip to content

Commit

Permalink
Add --locked and --frozen to uv run CLI (#5196)
Browse files Browse the repository at this point in the history
## Summary

You can now use `uv run --locked` to assert that the lockfile doesn't
change, or `uv run --frozen` to run without attempting to update the
lockfile at all.

Closes #5185.
  • Loading branch information
charliermarsh authored Jul 18, 2024
1 parent 6a6e3b4 commit dfe2faa
Show file tree
Hide file tree
Showing 8 changed files with 301 additions and 101 deletions.
16 changes: 16 additions & 0 deletions crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1784,6 +1784,14 @@ impl ExternalCommand {
#[derive(Args)]
#[allow(clippy::struct_excessive_bools)]
pub struct RunArgs {
/// Assert that the `uv.lock` will remain unchanged.
#[arg(long, conflicts_with = "frozen")]
pub locked: bool,

/// Install without updating the `uv.lock` file.
#[arg(long, conflicts_with = "locked")]
pub frozen: bool,

/// Include optional dependencies from the extra group name; may be provided more than once.
///
/// Only applies to `pyproject.toml`, `setup.py`, and `setup.cfg` sources.
Expand Down Expand Up @@ -1909,6 +1917,14 @@ pub struct SyncArgs {
#[derive(Args)]
#[allow(clippy::struct_excessive_bools)]
pub struct LockArgs {
/// Assert that the `uv.lock` will remain unchanged.
#[arg(long, conflicts_with = "frozen")]
pub locked: bool,

/// Assert that a `uv.lock` exists, without updating it.
#[arg(long, conflicts_with = "locked")]
pub frozen: bool,

#[command(flatten)]
pub resolver: ResolverArgs,

Expand Down
93 changes: 82 additions & 11 deletions crates/uv/src/commands/project/lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ use crate::settings::{ResolverSettings, ResolverSettingsRef};

/// Resolve the project requirements into a lockfile.
pub(crate) async fn lock(
locked: bool,
frozen: bool,
python: Option<String>,
settings: ResolverSettings,
preview: PreviewMode,
Expand Down Expand Up @@ -62,14 +64,12 @@ pub(crate) async fn lock(
.await?
.into_interpreter();

// Read the existing lockfile.
let existing = read(&workspace).await?;

// Perform the lock operation.
match do_lock(
match do_safe_lock(
locked,
frozen,
&workspace,
&interpreter,
existing.as_ref(),
settings.as_ref(),
&SharedState::default(),
preview,
Expand All @@ -81,12 +81,7 @@ pub(crate) async fn lock(
)
.await
{
Ok(lock) => {
if !existing.is_some_and(|existing| existing == lock) {
commit(&lock, &workspace).await?;
}
Ok(ExitStatus::Success)
}
Ok(_) => Ok(ExitStatus::Success),
Err(ProjectError::Operation(pip::operations::Error::Resolve(
uv_resolver::ResolveError::NoSolution(err),
))) => {
Expand All @@ -98,6 +93,82 @@ pub(crate) async fn lock(
}
}

/// Perform a lock operation, respecting the `--locked` and `--frozen` parameters.
pub(super) async fn do_safe_lock(
locked: bool,
frozen: bool,
workspace: &Workspace,
interpreter: &Interpreter,
settings: ResolverSettingsRef<'_>,
state: &SharedState,
preview: PreviewMode,
connectivity: Connectivity,
concurrency: Concurrency,
native_tls: bool,
cache: &Cache,
printer: Printer,
) -> Result<Lock, ProjectError> {
if frozen {
// Read the existing lockfile, but don't attempt to lock the project.
read(workspace)
.await?
.ok_or_else(|| ProjectError::MissingLockfile)
} else if locked {
// Read the existing lockfile.
let existing = read(workspace)
.await?
.ok_or_else(|| ProjectError::MissingLockfile)?;

// Perform the lock operation, but don't write the lockfile to disk.
let lock = do_lock(
workspace,
interpreter,
Some(&existing),
settings,
state,
preview,
connectivity,
concurrency,
native_tls,
cache,
printer,
)
.await?;

// If the locks disagree, return an error.
if lock != existing {
return Err(ProjectError::LockMismatch);
}

Ok(lock)
} else {
// Read the existing lockfile.
let existing = read(workspace).await?;

// Perform the lock operation.
let lock = do_lock(
workspace,
interpreter,
existing.as_ref(),
settings,
state,
preview,
connectivity,
concurrency,
native_tls,
cache,
printer,
)
.await?;

if !existing.is_some_and(|existing| existing == lock) {
commit(&lock, workspace).await?;
}

Ok(lock)
}
}

/// Lock the project requirements into a lockfile.
pub(super) async fn do_lock(
workspace: &Workspace,
Expand Down
32 changes: 20 additions & 12 deletions crates/uv/src/commands/project/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,19 @@ use uv_warnings::warn_user_once;

use crate::commands::pip::operations::Modifications;
use crate::commands::project::environment::CachedEnvironment;
use crate::commands::project::ProjectError;
use crate::commands::reporters::PythonDownloadReporter;
use crate::commands::{project, ExitStatus, SharedState};
use crate::commands::{pip, project, ExitStatus, SharedState};
use crate::printer::Printer;
use crate::settings::ResolverInstallerSettings;

/// Run a command.
#[allow(clippy::fn_params_excessive_bools)]
pub(crate) async fn run(
command: ExternalCommand,
requirements: Vec<RequirementsSource>,
locked: bool,
frozen: bool,
package: Option<PackageName>,
extras: ExtrasSpecification,
dev: bool,
Expand Down Expand Up @@ -180,14 +184,11 @@ pub(crate) async fn run(
)
.await?;

// Read the existing lockfile.
let existing = project::lock::read(project.workspace()).await?;

// Lock and sync the environment.
let lock = project::lock::do_lock(
let lock = match project::lock::do_safe_lock(
locked,
frozen,
project.workspace(),
venv.interpreter(),
existing.as_ref(),
settings.as_ref().into(),
&state,
preview,
Expand All @@ -197,11 +198,18 @@ pub(crate) async fn run(
cache,
printer,
)
.await?;

if !existing.is_some_and(|existing| existing == lock) {
project::lock::commit(&lock, project.workspace()).await?;
}
.await
{
Ok(lock) => lock,
Err(ProjectError::Operation(pip::operations::Error::Resolve(
uv_resolver::ResolveError::NoSolution(err),
))) => {
let report = miette::Report::msg(format!("{err}")).context(err.header());
anstream::eprint!("{report:?}");
return Ok(ExitStatus::Failure);
}
Err(err) => return Err(err.into()),
};

project::sync::do_sync(
&project,
Expand Down
102 changes: 25 additions & 77 deletions crates/uv/src/commands/project/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use uv_types::{BuildIsolation, HashStrategy};
use uv_warnings::warn_user_once;

use crate::commands::pip::operations::Modifications;
use crate::commands::project::lock::do_lock;
use crate::commands::project::lock::do_safe_lock;
use crate::commands::project::{ProjectError, SharedState};
use crate::commands::{pip, project, ExitStatus};
use crate::printer::Printer;
Expand Down Expand Up @@ -63,83 +63,31 @@ pub(crate) async fn sync(
// Initialize any shared state.
let state = SharedState::default();

let lock = if frozen {
// Read the existing lockfile.
project::lock::read(project.workspace())
.await?
.ok_or_else(|| ProjectError::MissingLockfile)?
} else if locked {
// Read the existing lockfile.
let existing = project::lock::read(project.workspace())
.await?
.ok_or_else(|| ProjectError::MissingLockfile)?;

// Perform the lock operation, but don't write the lockfile to disk.
let lock = match do_lock(
project.workspace(),
venv.interpreter(),
Some(&existing),
settings.as_ref().into(),
&state,
preview,
connectivity,
concurrency,
native_tls,
cache,
printer,
)
.await
{
Ok(lock) => lock,
Err(ProjectError::Operation(pip::operations::Error::Resolve(
uv_resolver::ResolveError::NoSolution(err),
))) => {
let report = miette::Report::msg(format!("{err}")).context(err.header());
anstream::eprint!("{report:?}");
return Ok(ExitStatus::Failure);
}
Err(err) => return Err(err.into()),
};

// If the locks disagree, return an error.
if lock != existing {
return Err(ProjectError::LockMismatch.into());
}

lock
} else {
// Read the existing lockfile.
let existing = project::lock::read(project.workspace()).await?;

// Perform the lock operation.
match do_lock(
project.workspace(),
venv.interpreter(),
existing.as_ref(),
settings.as_ref().into(),
&state,
preview,
connectivity,
concurrency,
native_tls,
cache,
printer,
)
.await
{
Ok(lock) => {
project::lock::commit(&lock, project.workspace()).await?;
lock
}
Err(ProjectError::Operation(pip::operations::Error::Resolve(
uv_resolver::ResolveError::NoSolution(err),
))) => {
let report = miette::Report::msg(format!("{err}")).context(err.header());
anstream::eprint!("{report:?}");
return Ok(ExitStatus::Failure);
}
Err(err) => return Err(err.into()),
let lock = match do_safe_lock(
locked,
frozen,
project.workspace(),
venv.interpreter(),
settings.as_ref().into(),
&state,
preview,
connectivity,
concurrency,
native_tls,
cache,
printer,
)
.await
{
Ok(lock) => lock,
Err(ProjectError::Operation(pip::operations::Error::Resolve(
uv_resolver::ResolveError::NoSolution(err),
))) => {
let report = miette::Report::msg(format!("{err}")).context(err.header());
anstream::eprint!("{report:?}");
return Ok(ExitStatus::Failure);
}
Err(err) => return Err(err.into()),
};

// Perform the sync operation.
Expand Down
4 changes: 4 additions & 0 deletions crates/uv/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -838,6 +838,8 @@ async fn run_project(
commands::run(
args.command,
requirements,
args.locked,
args.frozen,
args.package,
args.extras,
args.dev,
Expand Down Expand Up @@ -891,6 +893,8 @@ async fn run_project(
let cache = cache.init()?.with_refresh(args.refresh);

commands::lock(
args.locked,
args.frozen,
args.python,
args.settings,
globals.preview,
Expand Down
Loading

0 comments on commit dfe2faa

Please sign in to comment.