diff --git a/Cargo.lock b/Cargo.lock index 9742d37ef..dd33b7672 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4961,7 +4961,6 @@ dependencies = [ name = "uv-requirements" version = "0.1.0" dependencies = [ - "anstream", "anyhow", "cache-key", "configparser", diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index 72cba19a9..f94d3af33 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -1784,6 +1784,14 @@ pub struct RunArgs { #[derive(Args)] #[allow(clippy::struct_excessive_bools)] pub struct SyncArgs { + /// 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. #[arg(long, conflicts_with = "all_extras", value_parser = extra_name_with_clap_error)] @@ -1811,7 +1819,7 @@ pub struct SyncArgs { pub no_clean: bool, #[command(flatten)] - pub installer: InstallerArgs, + pub installer: ResolverInstallerArgs, #[command(flatten)] pub build: BuildArgs, diff --git a/crates/uv-requirements/Cargo.toml b/crates/uv-requirements/Cargo.toml index 77fe9202e..28e6da027 100644 --- a/crates/uv-requirements/Cargo.toml +++ b/crates/uv-requirements/Cargo.toml @@ -29,7 +29,6 @@ uv-resolver = { workspace = true, features = ["clap"] } uv-types = { workspace = true } uv-warnings = { workspace = true } -anstream = { workspace = true } anyhow = { workspace = true } configparser = { workspace = true } console = { workspace = true } diff --git a/crates/uv-requirements/src/upgrade.rs b/crates/uv-requirements/src/upgrade.rs index 669f566c5..5e22b854b 100644 --- a/crates/uv-requirements/src/upgrade.rs +++ b/crates/uv-requirements/src/upgrade.rs @@ -1,12 +1,10 @@ use std::path::Path; -use anstream::eprint; use anyhow::Result; use requirements_txt::RequirementsTxt; use uv_client::{BaseClientBuilder, Connectivity}; use uv_configuration::Upgrade; -use uv_distribution::Workspace; use uv_git::ResolvedRepositoryReference; use uv_resolver::{Lock, Preference, PreferenceError}; @@ -63,34 +61,6 @@ pub async fn read_requirements_txt( }) } -/// Load the lockfile from the workspace. -/// -/// Returns `Ok(None)` if the lockfile does not exist, is invalid, or is not required for the given upgrade strategy. -pub async fn read_lockfile(workspace: &Workspace, upgrade: &Upgrade) -> Result> { - // As an optimization, skip reading the lockfile is we're upgrading all packages anyway. - if upgrade.is_all() { - return Ok(None); - } - - // If an existing lockfile exists, build up a set of preferences. - let lockfile = workspace.install_path().join("uv.lock"); - let lock = match fs_err::tokio::read_to_string(&lockfile).await { - Ok(encoded) => match toml::from_str::(&encoded) { - Ok(lock) => lock, - Err(err) => { - eprint!("Failed to parse lockfile; ignoring locked requirements: {err}"); - return Ok(None); - } - }, - Err(err) if err.kind() == std::io::ErrorKind::NotFound => { - return Ok(None); - } - Err(err) => return Err(err.into()), - }; - - Ok(Some(lock)) -} - /// Load the preferred requirements from an existing lockfile, applying the upgrade strategy. pub fn read_lock_requirements(lock: &Lock, upgrade: &Upgrade) -> LockedRequirements { let mut preferences = Vec::new(); diff --git a/crates/uv/src/commands/project/add.rs b/crates/uv/src/commands/project/add.rs index ea851c7fa..2c8e1e6ac 100644 --- a/crates/uv/src/commands/project/add.rs +++ b/crates/uv/src/commands/project/add.rs @@ -10,6 +10,7 @@ use uv_distribution::pyproject_mut::PyProjectTomlMut; use uv_distribution::{DistributionDatabase, ProjectWorkspace, VirtualProject, Workspace}; use uv_normalize::PackageName; use uv_python::{PythonFetch, PythonPreference, PythonRequest}; + use uv_requirements::{NamedRequirementsResolver, RequirementsSource, RequirementsSpecification}; use uv_resolver::FlatIndex; use uv_types::{BuildIsolation, HashStrategy}; @@ -17,6 +18,7 @@ use uv_warnings::warn_user_once; use crate::commands::pip::operations::Modifications; use crate::commands::pip::resolution_environment; +use crate::commands::project::lock::commit; use crate::commands::reporters::ResolverReporter; use crate::commands::{project, ExitStatus, SharedState}; use crate::printer::Printer; @@ -204,10 +206,14 @@ pub(crate) async fn add( // Initialize any shared state. let state = SharedState::default(); - // Lock and sync the environment. + // Read the existing lockfile. + let existing = project::lock::read(project.workspace()).await?; + + // Lock and sync the environment, if necessary. let lock = project::lock::do_lock( project.workspace(), venv.interpreter(), + existing.as_ref(), settings.as_ref().into(), &state, preview, @@ -218,6 +224,9 @@ pub(crate) async fn add( printer, ) .await?; + if !existing.is_some_and(|existing| existing == lock) { + commit(&lock, project.workspace()).await?; + } // Perform a full sync, because we don't know what exactly is affected by the removal. // TODO(ibraheem): Should we accept CLI overrides for this? Should we even sync here? diff --git a/crates/uv/src/commands/project/lock.rs b/crates/uv/src/commands/project/lock.rs index 678ae7282..0a815814d 100644 --- a/crates/uv/src/commands/project/lock.rs +++ b/crates/uv/src/commands/project/lock.rs @@ -8,7 +8,7 @@ use uv_dispatch::BuildDispatch; use uv_distribution::{Workspace, DEV_DEPENDENCIES}; use uv_git::ResolvedRepositoryReference; use uv_python::{Interpreter, PythonFetch, PythonPreference, PythonRequest}; -use uv_requirements::upgrade::{read_lock_requirements, read_lockfile, LockedRequirements}; +use uv_requirements::upgrade::{read_lock_requirements, LockedRequirements}; use uv_resolver::{FlatIndex, Lock, OptionsBuilder, PythonRequirement, RequiresPython}; use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy}; use uv_warnings::{warn_user, warn_user_once}; @@ -52,10 +52,14 @@ pub(crate) async fn lock( .await? .into_interpreter(); + // Read the existing lockfile. + let existing = read(&workspace).await?; + // Perform the lock operation. match do_lock( &workspace, &interpreter, + existing.as_ref(), settings.as_ref(), &SharedState::default(), preview, @@ -67,7 +71,12 @@ pub(crate) async fn lock( ) .await { - Ok(_) => Ok(ExitStatus::Success), + Ok(lock) => { + if !existing.is_some_and(|existing| existing == lock) { + commit(&lock, &workspace).await?; + } + Ok(ExitStatus::Success) + } Err(ProjectError::Operation(pip::operations::Error::Resolve( uv_resolver::ResolveError::NoSolution(err), ))) => { @@ -84,6 +93,7 @@ pub(crate) async fn lock( pub(super) async fn do_lock( workspace: &Workspace, interpreter: &Interpreter, + existing: Option<&Lock>, settings: ResolverSettingsRef<'_>, state: &SharedState, preview: PreviewMode, @@ -175,9 +185,7 @@ pub(super) async fn do_lock( }; // If an existing lockfile exists, build up a set of preferences. - let existing_lock = read_lockfile(workspace, upgrade).await?; - let LockedRequirements { preferences, git } = existing_lock - .as_ref() + let LockedRequirements { preferences, git } = existing .map(|lock| read_lock_requirements(lock, upgrade)) .unwrap_or_default(); @@ -238,15 +246,29 @@ pub(super) async fn do_lock( // Notify the user of any resolution diagnostics. pip::operations::diagnose_resolution(resolution.diagnostics(), printer)?; - // Avoid serializing and writing to disk if the lock hasn't changed. - let lock = Lock::from_resolution_graph(&resolution)?; - if existing_lock.is_some_and(|existing_lock| existing_lock == lock) { - return Ok(lock); - } - - // Write the lockfile to disk. - let encoded = lock.to_toml()?; - fs_err::tokio::write(workspace.install_path().join("uv.lock"), encoded.as_bytes()).await?; - - Ok(lock) + Ok(Lock::from_resolution_graph(&resolution)?) +} + +/// Write the lockfile to disk. +pub(crate) async fn commit(lock: &Lock, workspace: &Workspace) -> Result<(), ProjectError> { + let encoded = lock.to_toml()?; + fs_err::tokio::write(workspace.install_path().join("uv.lock"), encoded).await?; + Ok(()) +} + +/// Read the lockfile from the workspace. +/// +/// Returns `Ok(None)` if the lockfile does not exist. +pub(crate) async fn read(workspace: &Workspace) -> Result, ProjectError> { + match fs_err::tokio::read_to_string(&workspace.install_path().join("uv.lock")).await { + Ok(encoded) => match toml::from_str::(&encoded) { + Ok(lock) => Ok(Some(lock)), + Err(err) => { + eprint!("Failed to parse lockfile; ignoring locked requirements: {err}"); + Ok(None) + } + }, + Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None), + Err(err) => Err(err.into()), + } } diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index 5c0f7b833..33cbfec8b 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -41,6 +41,14 @@ pub(crate) mod tree; #[derive(thiserror::Error, Debug)] pub(crate) enum ProjectError { + #[error("The lockfile at `uv.lock` needs to be updated, but `--locked` was provided. To update the lockfile, run `uv lock`.")] + LockMismatch, + + #[error( + "Unable to find lockfile at `uv.lock`. To create a lockfile, run `uv lock` or `uv sync`." + )] + MissingLockfile, + #[error("The current Python version ({0}) is not compatible with the locked Python requirement: `{1}`")] LockedPythonIncompatibility(Version, RequiresPython), diff --git a/crates/uv/src/commands/project/remove.rs b/crates/uv/src/commands/project/remove.rs index c9757339f..82be0cfad 100644 --- a/crates/uv/src/commands/project/remove.rs +++ b/crates/uv/src/commands/project/remove.rs @@ -11,6 +11,7 @@ use uv_python::{PythonFetch, PythonPreference, PythonRequest}; use uv_warnings::{warn_user, warn_user_once}; use crate::commands::pip::operations::Modifications; +use crate::commands::project::lock::commit; use crate::commands::{project, ExitStatus, SharedState}; use crate::printer::Printer; use crate::settings::ResolverInstallerSettings; @@ -98,10 +99,14 @@ pub(crate) async fn remove( // Initialize any shared state. let state = SharedState::default(); - // Lock and sync the environment. + // Read the existing lockfile. + let existing = project::lock::read(project.workspace()).await?; + + // Lock and sync the environment, if necessary. let lock = project::lock::do_lock( project.workspace(), venv.interpreter(), + existing.as_ref(), settings.as_ref().into(), &state, preview, @@ -112,6 +117,9 @@ pub(crate) async fn remove( printer, ) .await?; + if !existing.is_some_and(|existing| existing == lock) { + commit(&lock, project.workspace()).await?; + } // Perform a full sync, because we don't know what exactly is affected by the removal. // TODO(ibraheem): Should we accept CLI overrides for this? Should we even sync here? diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index f97eb307a..59979db1e 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -22,6 +22,7 @@ use uv_python::{ request_from_version_file, EnvironmentPreference, Interpreter, PythonEnvironment, PythonFetch, PythonInstallation, PythonPreference, PythonRequest, VersionRequest, }; + use uv_requirements::{RequirementsSource, RequirementsSpecification}; use uv_warnings::warn_user_once; @@ -179,10 +180,14 @@ 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( project.workspace(), venv.interpreter(), + existing.as_ref(), settings.as_ref().into(), &state, preview, @@ -193,6 +198,11 @@ pub(crate) async fn run( printer, ) .await?; + + if !existing.is_some_and(|existing| existing == lock) { + project::lock::commit(&lock, project.workspace()).await?; + } + project::sync::do_sync( &project, &venv, diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index 443c36ed5..8b2288a07 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -7,25 +7,30 @@ use uv_dispatch::BuildDispatch; use uv_distribution::{VirtualProject, DEV_DEPENDENCIES}; use uv_installer::SitePackages; use uv_python::{PythonEnvironment, PythonFetch, PythonPreference, PythonRequest}; + use uv_resolver::{FlatIndex, Lock}; 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::{ProjectError, SharedState}; use crate::commands::{pip, project, ExitStatus}; use crate::printer::Printer; -use crate::settings::{InstallerSettings, InstallerSettingsRef}; +use crate::settings::{InstallerSettingsRef, ResolverInstallerSettings}; /// Sync the project environment. +#[allow(clippy::fn_params_excessive_bools)] pub(crate) async fn sync( + locked: bool, + frozen: bool, extras: ExtrasSpecification, dev: bool, modifications: Modifications, python: Option, python_preference: PythonPreference, python_fetch: PythonFetch, - settings: InstallerSettings, + settings: ResolverInstallerSettings, preview: PreviewMode, connectivity: Connectivity, concurrency: Concurrency, @@ -53,12 +58,88 @@ pub(crate) async fn sync( ) .await?; - // Read the lockfile. - let lock: Lock = { - let encoded = - fs_err::tokio::read_to_string(project.workspace().install_path().join("uv.lock")) - .await?; - toml::from_str(&encoded)? + // 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("No solution found when resolving dependencies:"); + 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("No solution found when resolving dependencies:"); + anstream::eprint!("{report:?}"); + return Ok(ExitStatus::Failure); + } + Err(err) => return Err(err.into()), + } }; // Perform the sync operation. @@ -69,8 +150,8 @@ pub(crate) async fn sync( extras, dev, modifications, - settings.as_ref(), - &SharedState::default(), + settings.as_ref().into(), + &state, preview, connectivity, concurrency, diff --git a/crates/uv/src/commands/project/tree.rs b/crates/uv/src/commands/project/tree.rs index 0fa7ec2f3..aae1cdd7b 100644 --- a/crates/uv/src/commands/project/tree.rs +++ b/crates/uv/src/commands/project/tree.rs @@ -10,15 +10,15 @@ use uv_client::Connectivity; use uv_configuration::{Concurrency, PreviewMode}; use uv_distribution::Workspace; use uv_python::{PythonFetch, PythonPreference, PythonRequest}; + use uv_warnings::warn_user_once; use crate::commands::pip::tree::DisplayDependencyGraph; use crate::commands::project::FoundInterpreter; -use crate::commands::ExitStatus; +use crate::commands::{project, ExitStatus}; use crate::printer::Printer; use crate::settings::ResolverSettings; -use super::lock::do_lock; use super::SharedState; /// Run a command. @@ -60,10 +60,14 @@ pub(crate) async fn tree( .await? .into_interpreter(); - // Update the lock file. - let lock = do_lock( + // Read the existing lockfile. + let existing = project::lock::read(&workspace).await?; + + // Update the lock file, if necessary. + let lock = project::lock::do_lock( &workspace, &interpreter, + existing.as_ref(), settings.as_ref(), &SharedState::default(), preview, @@ -74,6 +78,9 @@ pub(crate) async fn tree( printer, ) .await?; + if !existing.is_some_and(|existing| existing == lock) { + project::lock::commit(&lock, &workspace).await?; + } // Read packages from the lockfile. let mut packages: IndexMap<_, Vec<_>> = IndexMap::new(); diff --git a/crates/uv/src/main.rs b/crates/uv/src/main.rs index aac5b2cbe..14b30151c 100644 --- a/crates/uv/src/main.rs +++ b/crates/uv/src/main.rs @@ -884,6 +884,8 @@ async fn run_project( let cache = cache.init()?.with_refresh(args.refresh); commands::sync( + args.locked, + args.frozen, args.extras, args.dev, args.modifications, diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index 5e028794a..fc896f108 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -9,7 +9,7 @@ use install_wheel_rs::linker::LinkMode; use pep508_rs::{ExtraName, RequirementOrigin}; use pypi_types::Requirement; use uv_cache::{CacheArgs, Refresh}; -use uv_cli::options::{flag, installer_options, resolver_installer_options, resolver_options}; +use uv_cli::options::{flag, resolver_installer_options, resolver_options}; use uv_cli::{ AddArgs, ColorChoice, Commands, ExternalCommand, GlobalArgs, ListFormat, LockArgs, Maybe, PipCheckArgs, PipCompileArgs, PipFreezeArgs, PipInstallArgs, PipListArgs, PipShowArgs, @@ -29,8 +29,7 @@ use uv_python::{Prefix, PythonFetch, PythonPreference, PythonVersion, Target}; use uv_requirements::RequirementsSource; use uv_resolver::{AnnotationStyle, DependencyMode, ExcludeNewer, PreReleaseMode, ResolutionMode}; use uv_settings::{ - Combine, FilesystemOptions, InstallerOptions, Options, PipOptions, ResolverInstallerOptions, - ResolverOptions, + Combine, FilesystemOptions, Options, PipOptions, ResolverInstallerOptions, ResolverOptions, }; use crate::commands::pip::operations::Modifications; @@ -415,12 +414,14 @@ impl PythonFindSettings { #[allow(clippy::struct_excessive_bools, dead_code)] #[derive(Debug, Clone)] pub(crate) struct SyncSettings { + pub(crate) locked: bool, + pub(crate) frozen: bool, pub(crate) extras: ExtrasSpecification, pub(crate) dev: bool, pub(crate) modifications: Modifications, pub(crate) python: Option, pub(crate) refresh: Refresh, - pub(crate) settings: InstallerSettings, + pub(crate) settings: ResolverInstallerSettings, } impl SyncSettings { @@ -428,6 +429,8 @@ impl SyncSettings { #[allow(clippy::needless_pass_by_value)] pub(crate) fn resolve(args: SyncArgs, filesystem: Option) -> Self { let SyncArgs { + locked, + frozen, extra, all_extras, no_all_extras, @@ -447,6 +450,8 @@ impl SyncSettings { }; Self { + locked, + frozen, extras: ExtrasSpecification::from_args( flag(all_extras, no_all_extras).unwrap_or_default(), extra.unwrap_or_default(), @@ -455,7 +460,10 @@ impl SyncSettings { modifications, python, refresh: Refresh::from(refresh), - settings: InstallerSettings::combine(installer_options(installer, build), filesystem), + settings: ResolverInstallerSettings::combine( + resolver_installer_options(installer, build), + filesystem, + ), } } } @@ -1272,20 +1280,6 @@ impl VenvSettings { /// /// Combines the `[tool.uv]` persistent configuration with the command-line arguments /// ([`InstallerArgs`], represented as [`InstallerOptions`]). -#[allow(clippy::struct_excessive_bools)] -#[derive(Debug, Clone, Default)] -pub(crate) struct InstallerSettings { - pub(crate) index_locations: IndexLocations, - pub(crate) index_strategy: IndexStrategy, - pub(crate) keyring_provider: KeyringProviderType, - pub(crate) config_setting: ConfigSettings, - pub(crate) exclude_newer: Option, - pub(crate) link_mode: LinkMode, - pub(crate) compile_bytecode: bool, - pub(crate) reinstall: Reinstall, - pub(crate) build_options: BuildOptions, -} - #[derive(Debug, Clone)] pub(crate) struct InstallerSettingsRef<'a> { pub(crate) index_locations: &'a IndexLocations, @@ -1299,100 +1293,6 @@ pub(crate) struct InstallerSettingsRef<'a> { pub(crate) build_options: &'a BuildOptions, } -impl InstallerSettings { - /// Resolve the [`InstallerSettings`] from the CLI and filesystem configuration. - pub(crate) fn combine(args: InstallerOptions, filesystem: Option) -> Self { - let ResolverInstallerOptions { - index_url, - extra_index_url, - no_index, - find_links, - index_strategy, - keyring_provider, - resolution: _, - prerelease: _, - config_settings, - exclude_newer, - link_mode, - compile_bytecode, - upgrade: _, - upgrade_package: _, - reinstall, - reinstall_package, - no_build, - no_build_package, - no_binary, - no_binary_package, - } = filesystem - .map(FilesystemOptions::into_options) - .map(|options| options.top_level) - .unwrap_or_default(); - - Self { - index_locations: IndexLocations::new( - args.index_url.combine(index_url), - args.extra_index_url - .combine(extra_index_url) - .unwrap_or_default(), - args.find_links.combine(find_links).unwrap_or_default(), - args.no_index.combine(no_index).unwrap_or_default(), - ), - index_strategy: args - .index_strategy - .combine(index_strategy) - .unwrap_or_default(), - keyring_provider: args - .keyring_provider - .combine(keyring_provider) - .unwrap_or_default(), - config_setting: args - .config_settings - .combine(config_settings) - .unwrap_or_default(), - exclude_newer: args.exclude_newer.combine(exclude_newer), - link_mode: args.link_mode.combine(link_mode).unwrap_or_default(), - compile_bytecode: args - .compile_bytecode - .combine(compile_bytecode) - .unwrap_or_default(), - reinstall: Reinstall::from_args( - args.reinstall.combine(reinstall), - args.reinstall_package - .combine(reinstall_package) - .unwrap_or_default(), - ), - build_options: BuildOptions::new( - NoBinary::from_args( - args.no_binary.combine(no_binary), - args.no_binary_package - .combine(no_binary_package) - .unwrap_or_default(), - ), - NoBuild::from_args( - args.no_build.combine(no_build), - args.no_build_package - .combine(no_build_package) - .unwrap_or_default(), - ), - ), - } - } - - pub(crate) fn as_ref(&self) -> InstallerSettingsRef { - InstallerSettingsRef { - index_locations: &self.index_locations, - index_strategy: self.index_strategy, - keyring_provider: self.keyring_provider, - config_setting: &self.config_setting, - exclude_newer: self.exclude_newer, - link_mode: self.link_mode, - compile_bytecode: self.compile_bytecode, - reinstall: &self.reinstall, - build_options: &self.build_options, - } - } -} - /// The resolved settings to use for an invocation of the `uv` CLI when resolving dependencies. /// /// Combines the `[tool.uv]` persistent configuration with the command-line arguments diff --git a/crates/uv/tests/edit.rs b/crates/uv/tests/edit.rs index 12ccccc14..660643dc1 100644 --- a/crates/uv/tests/edit.rs +++ b/crates/uv/tests/edit.rs @@ -112,7 +112,7 @@ fn add_registry() -> Result<()> { }); // Install from the lockfile. - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###" success: true exit_code: 0 ----- stdout ----- @@ -149,7 +149,7 @@ fn add_git() -> Result<()> { Resolved 4 packages in [TIME] "###); - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###" success: true exit_code: 0 ----- stdout ----- @@ -270,7 +270,7 @@ fn add_git() -> Result<()> { }); // Install from the lockfile. - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###" success: true exit_code: 0 ----- stdout ----- @@ -307,7 +307,7 @@ fn add_git_raw() -> Result<()> { Resolved 4 packages in [TIME] "###); - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###" success: true exit_code: 0 ----- stdout ----- @@ -416,7 +416,7 @@ fn add_git_raw() -> Result<()> { }); // Install from the lockfile. - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###" success: true exit_code: 0 ----- stdout ----- @@ -506,7 +506,7 @@ fn add_unnamed() -> Result<()> { }); // Install from the lockfile. - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###" success: true exit_code: 0 ----- stdout ----- @@ -626,7 +626,7 @@ fn add_remove_dev() -> Result<()> { }); // Install from the lockfile. - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###" success: true exit_code: 0 ----- stdout ----- @@ -705,7 +705,7 @@ fn add_remove_dev() -> Result<()> { }); // Install from the lockfile. - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###" success: true exit_code: 0 ----- stdout ----- @@ -786,7 +786,7 @@ fn add_remove_optional() -> Result<()> { }); // Install from the lockfile. - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###" success: true exit_code: 0 ----- stdout ----- @@ -862,7 +862,7 @@ fn add_remove_optional() -> Result<()> { }); // Install from the lockfile. - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###" success: true exit_code: 0 ----- stdout ----- @@ -993,7 +993,7 @@ fn add_remove_workspace() -> Result<()> { }); // Install from the lockfile. - uv_snapshot!(context.filters(), context.sync().current_dir(&child1), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--frozen").current_dir(&child1), @r###" success: true exit_code: 0 ----- stdout ----- @@ -1062,7 +1062,7 @@ fn add_remove_workspace() -> Result<()> { }); // Install from the lockfile. - uv_snapshot!(context.filters(), context.sync().current_dir(&child1), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--frozen").current_dir(&child1), @r###" success: true exit_code: 0 ----- stdout ----- @@ -1173,7 +1173,7 @@ fn add_workspace_editable() -> Result<()> { }); // Install from the lockfile. - uv_snapshot!(context.filters(), context.sync().current_dir(&child1), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--frozen").current_dir(&child1), @r###" success: true exit_code: 0 ----- stdout ----- @@ -1212,7 +1212,7 @@ fn update() -> Result<()> { Resolved 6 packages in [TIME] "###); - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###" success: true exit_code: 0 ----- stdout ----- @@ -1451,7 +1451,7 @@ fn update() -> Result<()> { }); // Install from the lockfile. - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###" success: true exit_code: 0 ----- stdout ----- @@ -1490,7 +1490,7 @@ fn add_no_clean() -> Result<()> { Resolved 4 packages in [TIME] "###); - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###" success: true exit_code: 0 ----- stdout ----- @@ -1579,7 +1579,7 @@ fn add_no_clean() -> Result<()> { }); // Install from the lockfile without cleaning the environment. - uv_snapshot!(context.filters(), context.sync().arg("--no-clean"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--frozen").arg("--no-clean"), @r###" success: true exit_code: 0 ----- stdout ----- @@ -1590,7 +1590,7 @@ fn add_no_clean() -> Result<()> { "###); // Install from the lockfile, cleaning the environment. - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###" success: true exit_code: 0 ----- stdout ----- @@ -1630,7 +1630,7 @@ fn remove_registry() -> Result<()> { Resolved 4 packages in [TIME] "###); - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###" success: true exit_code: 0 ----- stdout ----- @@ -1698,7 +1698,7 @@ fn remove_registry() -> Result<()> { }); // Install from the lockfile. - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###" success: true exit_code: 0 ----- stdout ----- diff --git a/crates/uv/tests/lock.rs b/crates/uv/tests/lock.rs index 2949a6797..14dff499f 100644 --- a/crates/uv/tests/lock.rs +++ b/crates/uv/tests/lock.rs @@ -88,7 +88,7 @@ fn lock_wheel_registry() -> Result<()> { }); // Install from the lockfile. - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###" success: true exit_code: 0 ----- stdout ----- @@ -160,7 +160,7 @@ fn lock_sdist_registry() -> Result<()> { }); // Install from the lockfile. - uv_snapshot!(context.filters(), context.sync().env_remove("UV_EXCLUDE_NEWER"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--frozen").env_remove("UV_EXCLUDE_NEWER"), @r###" success: true exit_code: 0 ----- stdout ----- @@ -229,7 +229,7 @@ fn lock_sdist_git() -> Result<()> { }); // Install from the lockfile. - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###" success: true exit_code: 0 ----- stdout ----- @@ -323,7 +323,7 @@ fn lock_wheel_url() -> Result<()> { }); // Install from the lockfile. - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###" success: true exit_code: 0 ----- stdout ----- @@ -417,7 +417,7 @@ fn lock_sdist_url() -> Result<()> { }); // Install from the lockfile. - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###" success: true exit_code: 0 ----- stdout ----- @@ -531,7 +531,7 @@ fn lock_project_extra() -> Result<()> { }); // Install the base dependencies from the lockfile. - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###" success: true exit_code: 0 ----- stdout ----- @@ -547,7 +547,7 @@ fn lock_project_extra() -> Result<()> { "###); // Install the extras from the lockfile. - uv_snapshot!(context.filters(), context.sync().arg("--extra").arg("test"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--frozen").arg("--extra").arg("test"), @r###" success: true exit_code: 0 ----- stdout ----- @@ -591,7 +591,7 @@ fn lock_project_with_overrides() -> Result<()> { "###); // Install the base dependencies from the lockfile. - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###" success: true exit_code: 0 ----- stdout ----- @@ -772,7 +772,7 @@ fn lock_dependency_extra() -> Result<()> { }); // Install from the lockfile. - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###" success: true exit_code: 0 ----- stdout ----- @@ -998,7 +998,7 @@ fn lock_conditional_dependency_extra() -> Result<()> { }); // Install from the lockfile. - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###" success: true exit_code: 0 ----- stdout ----- @@ -1022,7 +1022,7 @@ fn lock_conditional_dependency_extra() -> Result<()> { fs_err::copy(lockfile, context_38.temp_dir.join("uv.lock"))?; // Install from the lockfile. - uv_snapshot!(context_38.filters(), context_38.sync(), @r###" + uv_snapshot!(context_38.filters(), context_38.sync().arg("--frozen"), @r###" success: true exit_code: 0 ----- stdout ----- @@ -1189,7 +1189,7 @@ fn lock_dependency_non_existent_extra() -> Result<()> { }); // Install from the lockfile. - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###" success: true exit_code: 0 ----- stdout ----- @@ -1943,7 +1943,7 @@ fn lock_requires_python() -> Result<()> { // Install from the lockfile. // Note we need to disable Python fetches or we'll just download 3.12 - uv_snapshot!(filters, context38.sync().arg("--python-fetch").arg("manual"), @r###" + uv_snapshot!(filters, context38.sync().arg("--frozen").arg("--python-fetch").arg("manual"), @r###" success: false exit_code: 2 ----- stdout ----- @@ -2398,7 +2398,7 @@ fn lock_dev() -> Result<()> { }); // Install from the lockfile, excluding development dependencies. - uv_snapshot!(context.filters(), context.sync().arg("--no-dev"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--frozen").arg("--no-dev"), @r###" success: true exit_code: 0 ----- stdout ----- @@ -2412,7 +2412,7 @@ fn lock_dev() -> Result<()> { "###); // Install from the lockfile, including development dependencies (the default). - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###" success: true exit_code: 0 ----- stdout ----- @@ -2806,7 +2806,7 @@ fn lock_cycles() -> Result<()> { }); // Install from the lockfile. - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###" success: true exit_code: 0 ----- stdout ----- diff --git a/crates/uv/tests/sync.rs b/crates/uv/tests/sync.rs new file mode 100644 index 000000000..ee28786d6 --- /dev/null +++ b/crates/uv/tests/sync.rs @@ -0,0 +1,164 @@ +#![cfg(all(feature = "python", feature = "pypi"))] + +use anyhow::Result; +use assert_cmd::prelude::*; +use assert_fs::prelude::*; + +use common::{uv_snapshot, TestContext}; + +mod common; + +#[test] +fn sync() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["iniconfig"] + "#, + )?; + + // Running `uv sync` should generate a lockfile. + uv_snapshot!(context.filters(), context.sync(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + warning: `uv sync` is experimental and may change without warning. + Resolved 2 packages in [TIME] + Prepared 2 packages in [TIME] + Installed 2 packages in [TIME] + + iniconfig==2.0.0 + + project==0.1.0 (from file://[TEMP_DIR]/) + "###); + + assert!(context.temp_dir.child("uv.lock").exists()); + + Ok(()) +} + +#[test] +fn locked() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["anyio==3.7.0"] + "#, + )?; + + // Running with `--locked` should error, if no lockfile is present. + uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + warning: `uv sync` is experimental and may change without warning. + error: Unable to find lockfile at `uv.lock`. To create a lockfile, run `uv lock` or `uv sync`. + "###); + + // Lock the initial requirements. + context.lock().assert().success(); + + let existing = fs_err::read_to_string(context.temp_dir.child("uv.lock"))?; + + // Update the requirements. + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["iniconfig"] + "#, + )?; + + // Running with `--locked` should error. + uv_snapshot!(context.filters(), context.sync().arg("--locked"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + warning: `uv sync` is experimental and may change without warning. + Resolved 2 packages in [TIME] + error: The lockfile at `uv.lock` needs to be updated, but `--locked` was provided. To update the lockfile, run `uv lock`. + "###); + + let updated = fs_err::read_to_string(context.temp_dir.child("uv.lock"))?; + + // And the lockfile should be unchanged. + assert_eq!(existing, updated); + + Ok(()) +} + +#[test] +fn frozen() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["anyio==3.7.0"] + "#, + )?; + + // Running with `--frozen` should error, if no lockfile is present. + uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + warning: `uv sync` is experimental and may change without warning. + error: Unable to find lockfile at `uv.lock`. To create a lockfile, run `uv lock` or `uv sync`. + "###); + + context.lock().assert().success(); + + // Update the requirements. + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["iniconfig"] + "#, + )?; + + // Running with `--frozen` should install the stale lockfile. + uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + warning: `uv sync` is experimental and may change without warning. + Prepared 4 packages in [TIME] + Installed 4 packages in [TIME] + + anyio==3.7.0 + + idna==3.6 + + project==0.1.0 (from file://[TEMP_DIR]/) + + sniffio==1.3.1 + "###); + + Ok(()) +}