#![expect(clippy::single_match_else)]
use std::collections::{BTreeMap, BTreeSet};
use std::fmt::Write;
use std::path::Path;
use std::sync::Arc;
use owo_colors::OwoColorize;
use rustc_hash::{FxBuildHasher, FxHashMap};
use tracing::debug;
use uv_cache::{Cache, Refresh};
use uv_client::{BaseClientBuilder, FlatIndexClient, RegistryClientBuilder};
use uv_configuration::{
Concurrency, Constraints, DependencyGroupsWithDefaults, DryRun, ExtrasSpecification, Reinstall,
Upgrade,
};
use uv_dispatch::BuildDispatch;
use uv_distribution::{DistributionDatabase, LoweredExtraBuildDependencies};
use uv_distribution_types::{
DependencyMetadata, HashGeneration, Index, IndexLocations, NameRequirementSpecification,
Requirement, RequiresPython, UnresolvedRequirementSpecification,
};
use uv_git::ResolvedRepositoryReference;
use uv_git_types::GitOid;
use uv_normalize::{GroupName, PackageName};
use uv_pep440::Version;
use uv_preview::{Preview, PreviewFeature};
use uv_pypi_types::{ConflictKind, Conflicts, SupportedEnvironments};
use uv_python::{Interpreter, PythonDownloads, PythonEnvironment, PythonPreference, PythonRequest};
use uv_requirements::ExtrasResolver;
use uv_requirements::upgrade::{LockedRequirements, read_lock_requirements};
use uv_resolver::{
FlatIndex, InMemoryIndex, Lock, Options, OptionsBuilder, Package, PythonRequirement,
ResolverEnvironment, ResolverManifest, SatisfiesResult, UniversalMarker,
};
use uv_scripts::Pep723Script;
use uv_settings::PythonInstallMirrors;
use uv_types::{
BuildContext, BuildIsolation, EmptyInstalledPackages, HashStrategy, SourceTreeEditablePolicy,
};
use uv_warnings::{warn_user, warn_user_once};
use uv_workspace::{
DiscoveryOptions, Editability, VirtualProject, WorkspaceCache, WorkspaceMember,
};
use crate::commands::pip::loggers::{DefaultResolveLogger, ResolveLogger, SummaryResolveLogger};
use crate::commands::project::lock_target::LockTarget;
use crate::commands::project::{
MissingLockfileSource, ProjectError, ProjectInterpreter, ScriptInterpreter, UniversalState,
WorkspacePython, init_script_python_requirement, script_extra_build_requires,
};
use crate::commands::reporters::{PythonDownloadReporter, ResolverReporter};
use crate::commands::{ExitStatus, ScriptPath, diagnostics, pip};
use crate::printer::Printer;
use crate::settings::{FrozenSource, LockCheck, LockCheckSource, ResolverSettings};
#[derive(Debug, Clone)]
#[expect(clippy::large_enum_variant)]
pub(crate) enum LockResult {
Unchanged(Lock),
Changed(Option<Lock>, Lock),
}
impl LockResult {
pub(crate) fn lock(&self) -> &Lock {
match self {
Self::Unchanged(lock) => lock,
Self::Changed(_, lock) => lock,
}
}
pub(crate) fn into_lock(self) -> Lock {
match self {
Self::Unchanged(lock) => lock,
Self::Changed(_, lock) => lock,
}
}
}
pub(crate) async fn lock(
project_dir: &Path,
lock_check: LockCheck,
frozen: Option<FrozenSource>,
dry_run: DryRun,
refresh: Refresh,
python: Option<String>,
install_mirrors: PythonInstallMirrors,
settings: ResolverSettings,
client_builder: BaseClientBuilder<'_>,
script: Option<ScriptPath>,
python_preference: PythonPreference,
python_downloads: PythonDownloads,
concurrency: Concurrency,
no_config: bool,
cache: &Cache,
workspace_cache: &WorkspaceCache,
printer: Printer,
preview: Preview,
) -> anyhow::Result<ExitStatus> {
let script = match script {
Some(ScriptPath::Path(path)) => {
let reporter = PythonDownloadReporter::single(printer);
let requires_python = init_script_python_requirement(
python.as_deref(),
&install_mirrors,
project_dir,
false,
python_preference,
python_downloads,
no_config,
&client_builder,
cache,
&reporter,
preview,
)
.await?;
Some(Pep723Script::init(&path, requires_python.specifiers()).await?)
}
Some(ScriptPath::Script(script)) => Some(script),
None => None,
};
let workspace;
let target = if let Some(script) = script.as_ref() {
LockTarget::Script(script)
} else {
workspace =
VirtualProject::discover(project_dir, &DiscoveryOptions::default(), workspace_cache)
.await?;
LockTarget::Workspace(workspace.workspace())
};
let interpreter;
let mode = if let Some(frozen_source) = frozen {
LockMode::Frozen(frozen_source.into())
} else {
interpreter = match target {
LockTarget::Workspace(workspace) => {
let groups = DependencyGroupsWithDefaults::none();
let workspace_python = WorkspacePython::from_request(
python.as_deref().map(PythonRequest::parse),
Some(workspace),
&groups,
project_dir,
no_config,
)
.await?;
ProjectInterpreter::discover(
workspace,
&groups,
workspace_python,
&client_builder,
python_preference,
python_downloads,
&install_mirrors,
false,
Some(false),
cache,
printer,
preview,
)
.await?
.into_interpreter()
}
LockTarget::Script(script) => ScriptInterpreter::discover(
script.into(),
python.as_deref().map(PythonRequest::parse),
&client_builder,
python_preference,
python_downloads,
&install_mirrors,
false,
no_config,
Some(false),
cache,
printer,
preview,
)
.await?
.into_interpreter(),
};
if let LockCheck::Enabled(lock_check) = lock_check {
LockMode::Locked(&interpreter, lock_check)
} else if dry_run.enabled() {
LockMode::DryRun(&interpreter)
} else {
LockMode::Write(&interpreter)
}
};
let state = UniversalState::default();
match Box::pin(
LockOperation::new(
mode,
&settings,
&client_builder,
&state,
Box::new(DefaultResolveLogger),
&concurrency,
cache,
workspace_cache,
printer,
preview,
)
.with_refresh(&refresh)
.execute(target),
)
.await
{
Ok(lock) => {
if let Some(frozen_source) = frozen {
match frozen_source {
FrozenSource::Cli => {
warn_user!(
"The lockfile at `uv.lock` was only checked for validity, not whether it is up-to-date, because `--frozen` was provided; use `--check` instead"
);
}
FrozenSource::Env | FrozenSource::Configuration => {
warn_user!(
"The lockfile at `uv.lock` was only checked for validity, not whether it is up-to-date, because {} was provided; use `--check` instead",
MissingLockfileSource::from(frozen_source)
);
}
}
}
if dry_run.enabled() {
if let LockResult::Changed(previous, lock) = &lock {
let mut changed = false;
for event in LockEvent::detect_changes(previous.as_ref(), lock, dry_run) {
changed = true;
writeln!(printer.stderr(), "{event}")?;
}
if !changed {
writeln!(printer.stderr(), "{}", "Lockfile changes detected".bold())?;
}
} else {
writeln!(
printer.stderr(),
"{}",
"No lockfile changes detected".bold()
)?;
}
} else {
if let LockResult::Changed(Some(previous), lock) = &lock {
for event in LockEvent::detect_changes(Some(previous), lock, dry_run) {
writeln!(printer.stderr(), "{event}")?;
}
}
}
Ok(ExitStatus::Success)
}
Err(err @ ProjectError::LockMismatch(..)) => {
writeln!(printer.stderr(), "{}", err.to_string().bold())?;
Ok(ExitStatus::Failure)
}
Err(ProjectError::Operation(err)) => {
diagnostics::OperationDiagnostic::with_system_certs(client_builder.system_certs())
.report(err)
.map_or(Ok(ExitStatus::Failure), |err| Err(err.into()))
}
Err(err) => Err(err.into()),
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) enum LockMode<'env> {
Write(&'env Interpreter),
DryRun(&'env Interpreter),
Locked(&'env Interpreter, LockCheckSource),
Frozen(MissingLockfileSource),
}
pub(crate) struct LockOperation<'env> {
mode: LockMode<'env>,
constraints: Vec<NameRequirementSpecification>,
refresh: Option<&'env Refresh>,
settings: &'env ResolverSettings,
client_builder: &'env BaseClientBuilder<'env>,
state: &'env UniversalState,
logger: Box<dyn ResolveLogger>,
concurrency: &'env Concurrency,
cache: &'env Cache,
workspace_cache: &'env WorkspaceCache,
printer: Printer,
preview: Preview,
}
impl<'env> LockOperation<'env> {
pub(crate) fn new(
mode: LockMode<'env>,
settings: &'env ResolverSettings,
client_builder: &'env BaseClientBuilder<'env>,
state: &'env UniversalState,
logger: Box<dyn ResolveLogger>,
concurrency: &'env Concurrency,
cache: &'env Cache,
workspace_cache: &'env WorkspaceCache,
printer: Printer,
preview: Preview,
) -> Self {
Self {
mode,
constraints: vec![],
refresh: None,
settings,
client_builder,
state,
logger,
concurrency,
cache,
workspace_cache,
printer,
preview,
}
}
#[must_use]
pub(crate) fn with_constraints(
mut self,
constraints: Vec<NameRequirementSpecification>,
) -> Self {
self.constraints = constraints;
self
}
#[must_use]
pub(crate) fn with_refresh(mut self, refresh: &'env Refresh) -> Self {
self.refresh = Some(refresh);
self
}
pub(crate) async fn execute(self, target: LockTarget<'_>) -> Result<LockResult, ProjectError> {
match self.mode {
LockMode::Frozen(source) => {
let lock_filename = target.lock_filename();
let existing = target
.read()
.await?
.ok_or(ProjectError::MissingLockfile(source, lock_filename))?;
if let LockTarget::Workspace(workspace) = target {
for package_name in workspace.packages().keys() {
existing
.find_by_name(package_name)
.map_err(|_| ProjectError::LockWorkspaceMismatch(package_name.clone()))?
.ok_or_else(|| {
ProjectError::LockWorkspaceMismatch(package_name.clone())
})?;
}
}
Ok(LockResult::Unchanged(existing))
}
LockMode::Locked(interpreter, lock_source) => {
let lock_filename = target.lock_filename();
let existing = target.read().await?.ok_or(ProjectError::MissingLockfile(
lock_source.into(),
lock_filename,
))?;
let result = Box::pin(do_lock(
target,
interpreter,
Some(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,
))
.await?;
if let LockResult::Changed(prev, cur) = result {
return Err(ProjectError::LockMismatch(
prev.map(Box::new),
Box::new(cur),
lock_source,
));
}
Ok(result)
}
LockMode::Write(interpreter) | LockMode::DryRun(interpreter) => {
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),
};
let result = Box::pin(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,
))
.await?;
if !matches!(self.mode, LockMode::DryRun(_)) {
if let LockResult::Changed(_, lock) = &result {
target.commit(lock).await?;
}
}
Ok(result)
}
}
}
}
async fn do_lock(
target: LockTarget<'_>,
interpreter: &Interpreter,
existing_lock: Option<Lock>,
external: Vec<NameRequirementSpecification>,
refresh: Option<&Refresh>,
settings: &ResolverSettings,
client_builder: &BaseClientBuilder<'_>,
state: &UniversalState,
logger: Box<dyn ResolveLogger>,
concurrency: &Concurrency,
cache: &Cache,
workspace_cache: &WorkspaceCache,
printer: Printer,
preview: Preview,
) -> Result<LockResult, ProjectError> {
let start = std::time::Instant::now();
let ResolverSettings {
index_locations,
index_strategy,
keyring_provider,
resolution,
prerelease,
fork_strategy,
dependency_metadata,
config_setting,
config_settings_package,
build_isolation,
extra_build_dependencies,
extra_build_variables,
exclude_newer,
link_mode,
upgrade,
build_options,
sources,
torch_backend: _,
} = settings;
let members = target.members();
let packages = target.packages();
let required_members = target.required_members();
let requirements = target.requirements();
let overrides = target.overrides();
let excludes = target.exclude_dependencies();
let constraints = target.constraints();
let build_constraints = target.build_constraints();
let dependency_groups = target.dependency_groups()?;
let source_trees = vec![];
let requirements = target.lower(
requirements,
index_locations,
sources,
client_builder.credentials_cache(),
)?;
let overrides = target.lower(
overrides,
index_locations,
sources,
client_builder.credentials_cache(),
)?;
let constraints = target.lower(
constraints,
index_locations,
sources,
client_builder.credentials_cache(),
)?;
let build_constraints = target.lower(
build_constraints,
index_locations,
sources,
client_builder.credentials_cache(),
)?;
let dependency_groups = dependency_groups
.into_iter()
.map(|(name, group)| {
let requirements = target.lower(
group.requirements,
index_locations,
sources,
client_builder.credentials_cache(),
)?;
Ok((name, requirements))
})
.collect::<Result<BTreeMap<_, _>, ProjectError>>()?;
let mut conflicts = target.conflicts()?;
if let LockTarget::Workspace(workspace) = target {
if let Some(groups) = &workspace.pyproject_toml().dependency_groups {
if let Some(project) = &workspace.pyproject_toml().project {
conflicts.expand_transitive_group_includes(&project.name, groups);
}
}
}
if !preview.is_enabled(PreviewFeature::PackageConflicts)
&& conflicts.iter().any(|set| {
set.iter()
.any(|item| matches!(item.kind(), ConflictKind::Project))
})
{
warn_user_once!(
"Declaring conflicts for packages (`package = ...`) is experimental and may change without warning. Pass `--preview-features {}` to disable this warning.",
PreviewFeature::PackageConflicts
);
}
let environments = {
let environments = target.environments();
if let Some(environments) = &environments {
for (lhs, rhs) in environments
.as_markers()
.iter()
.zip(environments.as_markers().iter().skip(1))
{
if !lhs.is_disjoint(*rhs) {
let mut hint = lhs.negate();
hint.and(*rhs);
let lhs = lhs
.contents()
.map(|contents| contents.to_string())
.unwrap_or_else(|| "true".to_string());
let rhs = rhs
.contents()
.map(|contents| contents.to_string())
.unwrap_or_else(|| "true".to_string());
let hint = hint
.contents()
.map(|contents| contents.to_string())
.unwrap_or_else(|| "true".to_string());
return Err(ProjectError::OverlappingMarkers(lhs, rhs, hint));
}
}
}
environments
};
let required_environments = if let Some(required_environments) = target.required_environments()
{
for (lhs, rhs) in required_environments
.as_markers()
.iter()
.zip(required_environments.as_markers().iter().skip(1))
{
if !lhs.is_disjoint(*rhs) {
let mut hint = lhs.negate();
hint.and(*rhs);
let lhs = lhs
.contents()
.map(|contents| contents.to_string())
.unwrap_or_else(|| "true".to_string());
let rhs = rhs
.contents()
.map(|contents| contents.to_string())
.unwrap_or_else(|| "true".to_string());
let hint = hint
.contents()
.map(|contents| contents.to_string())
.unwrap_or_else(|| "true".to_string());
return Err(ProjectError::OverlappingMarkers(lhs, rhs, hint));
}
}
Some(required_environments)
} else {
None
};
let requires_python = target.requires_python()?;
let requires_python = if let Some(requires_python) = requires_python {
if requires_python.is_unbounded() {
let default =
RequiresPython::greater_than_equal_version(&interpreter.python_minor_version());
warn_user_once!(
"The workspace `requires-python` value (`{requires_python}`) does not contain a lower bound. Add a lower bound to indicate the minimum compatible Python version (e.g., `{default}`)."
);
} else if requires_python.is_exact_without_patch() {
warn_user_once!(
"The workspace `requires-python` value (`{requires_python}`) contains an exact match without a patch version. When omitted, the patch version is implicitly `0` (e.g., `{requires_python}.0`). Did you mean `{requires_python}.*`?"
);
}
requires_python
} else {
let default =
RequiresPython::greater_than_equal_version(&interpreter.python_minor_version());
warn_user_once!(
"No `requires-python` value found in the workspace. Defaulting to `{default}`."
);
default
};
for environment in environments
.map(SupportedEnvironments::as_markers)
.into_iter()
.flatten()
.copied()
{
if requires_python.to_marker_tree().is_disjoint(environment) {
return if let Some(contents) = environment.contents() {
Err(ProjectError::DisjointEnvironment(
contents,
requires_python.specifiers().clone(),
))
} else {
Err(ProjectError::EmptyEnvironment)
};
}
}
let python_requirement =
PythonRequirement::from_requires_python(interpreter, requires_python.clone());
let client_builder = client_builder.clone().keyring(*keyring_provider);
for index in target.indexes() {
if let Some(credentials) = index.credentials() {
if let Some(root_url) = index.root_url() {
client_builder.store_credentials(&root_url, credentials.clone());
}
client_builder.store_credentials(index.raw_url(), credentials);
}
}
let client = RegistryClientBuilder::new(client_builder, cache.clone())
.index_locations(index_locations.clone())
.index_strategy(*index_strategy)
.markers(interpreter.markers())
.platform(interpreter.platform())
.build()?;
let environment;
let build_isolation = match build_isolation {
uv_configuration::BuildIsolation::Isolate => BuildIsolation::Isolated,
uv_configuration::BuildIsolation::Shared => {
environment = PythonEnvironment::from_interpreter(interpreter.clone());
BuildIsolation::Shared(&environment)
}
uv_configuration::BuildIsolation::SharedPackage(packages) => {
environment = PythonEnvironment::from_interpreter(interpreter.clone());
BuildIsolation::SharedPackage(&environment, packages)
}
};
let lock_supported_environments = environments.cloned().unwrap_or_default();
let lock_required_environments = required_environments.cloned().unwrap_or_default();
let artifact_environments = SupportedEnvironments::from_markers(
lock_supported_environments
.iter()
.copied()
.chain(lock_required_environments.iter().copied())
.collect(),
);
let options = OptionsBuilder::new()
.resolution_mode(*resolution)
.prerelease_mode(*prerelease)
.fork_strategy(*fork_strategy)
.exclude_newer(exclude_newer.clone())
.index_strategy(*index_strategy)
.build_options(build_options.clone())
.artifact_environments(artifact_environments.clone())
.build();
let hasher = HashStrategy::Generate(HashGeneration::Url);
let build_hasher = HashStrategy::default();
let extras = ExtrasSpecification::default();
let groups = BTreeMap::new();
let flat_index = {
let client = FlatIndexClient::new(client.cached_client(), client.connectivity(), cache);
let entries = client
.fetch_all(index_locations.flat_indexes().map(Index::url))
.await?;
FlatIndex::from_entries(entries, None, &hasher, build_options)
};
let extra_build_requires = match &target {
LockTarget::Workspace(workspace) => LoweredExtraBuildDependencies::from_workspace(
extra_build_dependencies.clone(),
workspace,
index_locations,
sources,
client.credentials_cache(),
)?,
LockTarget::Script(script) => {
script_extra_build_requires((*script).into(), settings, client.credentials_cache())?
}
}
.into_inner();
let dispatch_constraints = Constraints::from_requirements(build_constraints.iter().cloned());
let build_dispatch = BuildDispatch::new(
&client,
cache,
&dispatch_constraints,
interpreter,
index_locations,
&flat_index,
dependency_metadata,
state.fork().into_inner(),
*index_strategy,
config_setting,
config_settings_package,
build_isolation,
&extra_build_requires,
extra_build_variables,
*link_mode,
build_options,
&build_hasher,
exclude_newer.clone(),
sources.clone(),
SourceTreeEditablePolicy::Project,
workspace_cache.clone(),
concurrency.clone(),
preview,
);
let database = DistributionDatabase::new(
&client,
&build_dispatch,
concurrency.downloads_semaphore.clone(),
);
let existing_lock = if let Some(existing_lock) = existing_lock {
match ValidatedLock::validate(
existing_lock,
target.install_path(),
packages,
&members,
required_members,
&requirements,
&dependency_groups,
&constraints,
&overrides,
&excludes,
&build_constraints,
&conflicts,
environments,
required_environments,
dependency_metadata,
interpreter,
&requires_python,
index_locations,
upgrade,
refresh,
&options,
&hasher,
state.index(),
&database,
printer,
)
.await
{
Ok(result) => Some(result),
Err(ProjectError::Lock(err)) if err.is_resolution() => {
return Err(ProjectError::Lock(err));
}
Err(err) => {
warn_user!("Failed to validate existing lockfile: {err}");
None
}
}
} else {
None
};
match existing_lock {
Some(ValidatedLock::Satisfies(lock)) => {
logger.on_complete(lock.len(), start, printer)?;
Ok(LockResult::Unchanged(lock))
}
_ => {
let versions_lock = existing_lock.as_ref().and_then(|lock| match &lock {
ValidatedLock::Satisfies(lock) => Some(lock),
ValidatedLock::Preferable(lock) => Some(lock),
ValidatedLock::Versions(lock) => Some(lock),
ValidatedLock::Unusable(_) => None,
});
let LockedRequirements { preferences, git } = versions_lock
.map(|lock| read_lock_requirements(lock, target.install_path(), upgrade))
.transpose()?
.unwrap_or_default();
for ResolvedRepositoryReference { reference, sha } in git {
debug!("Inserting Git reference into resolver: `{reference:?}` at `{sha}`");
state.git().insert(reference, sha);
}
let forks_lock = existing_lock.as_ref().and_then(|lock| match &lock {
ValidatedLock::Satisfies(lock) => Some(lock),
ValidatedLock::Preferable(lock) => Some(lock),
ValidatedLock::Versions(_) => None,
ValidatedLock::Unusable(_) => None,
});
let resolver_env = ResolverEnvironment::universal(
forks_lock
.map(|lock| {
lock.fork_markers()
.iter()
.copied()
.map(UniversalMarker::combined)
.collect()
})
.unwrap_or_else(|| {
environments
.cloned()
.map(SupportedEnvironments::into_markers)
.unwrap_or_default()
}),
);
let (resolution, _) = pip::operations::resolve(
ExtrasResolver::new(&hasher, state.index(), database)
.with_reporter(Arc::new(ResolverReporter::from(printer)))
.resolve(target.members_requirements())
.await
.map_err(|err| ProjectError::Operation(err.into()))?
.into_iter()
.chain(target.group_requirements())
.chain(requirements.iter().cloned())
.chain(
dependency_groups
.values()
.flat_map(|requirements| requirements.iter().cloned()),
)
.map(UnresolvedRequirementSpecification::from)
.collect(),
constraints
.iter()
.cloned()
.map(NameRequirementSpecification::from)
.chain(external)
.collect(),
overrides
.iter()
.cloned()
.map(UnresolvedRequirementSpecification::from)
.collect(),
excludes.clone(),
source_trees,
None,
packages.keys().cloned().collect(),
&extras,
&groups,
preferences,
EmptyInstalledPackages,
&hasher,
&Reinstall::default(),
upgrade,
None,
resolver_env,
python_requirement,
interpreter.markers(),
conflicts.clone(),
&client,
&flat_index,
state.index(),
&build_dispatch,
concurrency,
options,
Box::new(SummaryResolveLogger),
printer,
)
.await?;
logger.on_complete(resolution.len(), start, printer)?;
pip::operations::diagnose_resolution(resolution.diagnostics(), printer)?;
let manifest = ResolverManifest::new(
members,
requirements,
constraints,
overrides,
excludes.clone(),
build_constraints,
dependency_groups,
dependency_metadata.values().cloned(),
)
.relative_to(target.install_path())?;
let previous = existing_lock.map(ValidatedLock::into_lock);
let lock = Lock::from_resolution(
&resolution,
target.install_path(),
lock_supported_environments.clone().into_markers(),
)?
.with_manifest(manifest)
.with_conflicts(conflicts)
.with_required_environments(lock_required_environments.into_markers());
if previous.as_ref().is_some_and(|previous| *previous == lock) {
Ok(LockResult::Unchanged(lock))
} else {
Ok(LockResult::Changed(previous, lock))
}
}
}
}
#[derive(Debug)]
enum ValidatedLock {
Unusable(Lock),
Versions(Lock),
Preferable(Lock),
Satisfies(Lock),
}
impl ValidatedLock {
async fn validate<Context: BuildContext>(
lock: Lock,
install_path: &Path,
packages: &BTreeMap<PackageName, WorkspaceMember>,
members: &[PackageName],
required_members: &BTreeMap<PackageName, Editability>,
requirements: &[Requirement],
dependency_groups: &BTreeMap<GroupName, Vec<Requirement>>,
constraints: &[Requirement],
overrides: &[Requirement],
excludes: &[PackageName],
build_constraints: &[Requirement],
conflicts: &Conflicts,
environments: Option<&SupportedEnvironments>,
required_environments: Option<&SupportedEnvironments>,
dependency_metadata: &DependencyMetadata,
interpreter: &Interpreter,
requires_python: &RequiresPython,
index_locations: &IndexLocations,
upgrade: &Upgrade,
refresh: Option<&Refresh>,
options: &Options,
hasher: &HashStrategy,
index: &InMemoryIndex,
database: &DistributionDatabase<'_, Context>,
printer: Printer,
) -> Result<Self, ProjectError> {
if lock.resolution_mode() != options.resolution_mode {
let _ = writeln!(
printer.stderr(),
"Ignoring existing lockfile due to change in resolution mode: `{}` vs. `{}`",
lock.resolution_mode().cyan(),
options.resolution_mode.cyan()
);
return Ok(Self::Unusable(lock));
}
if lock.fork_strategy() != options.fork_strategy {
let _ = writeln!(
printer.stderr(),
"Ignoring existing lockfile due to change in fork strategy: `{}` vs. `{}`",
lock.fork_strategy().cyan(),
options.fork_strategy.cyan()
);
return Ok(Self::Unusable(lock));
}
if let Some(change) = lock.exclude_newer().compare(&options.exclude_newer) {
if !change.is_relative_timestamp_change() {
let _ = writeln!(
printer.stderr(),
"Resolving despite existing lockfile due to {change}",
);
return Ok(Self::Preferable(lock));
}
}
if upgrade.is_all() {
debug!("Ignoring existing lockfile due to `--upgrade`");
return Ok(Self::Unusable(lock));
}
if let Err((fork_markers_union, environments_union)) = lock.check_marker_coverage() {
warn_user!(
"Resolving despite existing lockfile due to fork markers not covering the supported environments: `{}` vs `{}`",
fork_markers_union
.try_to_string()
.unwrap_or("true".to_string()),
environments_union
.try_to_string()
.unwrap_or("true".to_string()),
);
return Ok(Self::Versions(lock));
}
if let Err((fork_markers_union, requires_python_marker)) =
lock.requires_python_coverage(requires_python)
{
warn_user!(
"Resolving despite existing lockfile due to fork markers being disjoint with `requires-python`: `{}` vs `{}`",
fork_markers_union
.try_to_string()
.unwrap_or("true".to_string()),
requires_python_marker
.try_to_string()
.unwrap_or("true".to_string()),
);
return Ok(Self::Versions(lock));
}
let expected = lock.simplified_supported_environments();
let actual = environments
.map(SupportedEnvironments::as_markers)
.unwrap_or_default()
.iter()
.copied()
.map(|marker| lock.simplify_environment(marker))
.collect::<Vec<_>>();
if expected != actual {
debug!(
"Resolving despite existing lockfile due to change in supported environments: `{:?}` vs. `{:?}`",
expected, actual
);
return Ok(Self::Versions(lock));
}
let expected = lock.simplified_required_environments();
let actual = required_environments
.map(SupportedEnvironments::as_markers)
.unwrap_or_default()
.iter()
.copied()
.map(|marker| lock.simplify_environment(marker))
.collect::<Vec<_>>();
if expected != actual {
debug!(
"Resolving despite existing lockfile due to change in supported environments: `{:?}` vs. `{:?}`",
expected, actual
);
return Ok(Self::Versions(lock));
}
if conflicts != lock.conflicts() {
debug!(
"Resolving despite existing lockfile due to change in conflicting groups: `{:?}` vs. `{:?}`",
conflicts,
lock.conflicts(),
);
return Ok(Self::Versions(lock));
}
if lock.requires_python().range() != requires_python.range() {
debug!(
"Resolving despite existing lockfile due to change in Python requirement: `{}` vs. `{}`",
lock.requires_python(),
requires_python,
);
return if lock.fork_markers().is_empty() {
Ok(Self::Preferable(lock))
} else {
Ok(Self::Versions(lock))
};
}
if lock.prerelease_mode() != options.prerelease_mode {
let _ = writeln!(
printer.stderr(),
"Resolving despite existing lockfile due to change in pre-release mode: `{}` vs. `{}`",
lock.prerelease_mode().cyan(),
options.prerelease_mode.cyan()
);
return Ok(Self::Preferable(lock));
}
if !(upgrade.is_none() || upgrade.is_all()) {
debug!(
"Resolving despite existing lockfile due to `--upgrade-package` or `--upgrade-group`"
);
return Ok(Self::Preferable(lock));
}
if matches!(refresh, Some(Refresh::All(..) | Refresh::Packages(..))) {
debug!("Resolving despite existing lockfile due to `--refresh`");
return Ok(Self::Preferable(lock));
}
let indexes = if index_locations.is_none() {
None
} else {
Some(index_locations)
};
match lock
.satisfies(
install_path,
packages,
members,
required_members,
requirements,
constraints,
overrides,
excludes,
build_constraints,
dependency_groups,
dependency_metadata,
indexes,
interpreter.tags()?,
interpreter.markers(),
hasher,
index,
database,
)
.await?
{
SatisfiesResult::Satisfied => {
debug!("Existing `uv.lock` satisfies workspace requirements");
Ok(Self::Satisfies(lock))
}
SatisfiesResult::MismatchedMembers(expected, actual) => {
debug!(
"Resolving despite existing lockfile due to mismatched members:\n Requested: {:?}\n Existing: {:?}",
expected, actual
);
Ok(Self::Preferable(lock))
}
SatisfiesResult::MismatchedEditable(name, expected) => {
if expected {
debug!(
"Resolving despite existing lockfile due to mismatched source: `{name}` (expected: `editable`)"
);
} else {
debug!(
"Resolving despite existing lockfile due to mismatched source: `{name}` (unexpected: `editable`)"
);
}
Ok(Self::Preferable(lock))
}
SatisfiesResult::MismatchedVirtual(name, expected) => {
if expected {
debug!(
"Resolving despite existing lockfile due to mismatched source: `{name}` (expected: `virtual`)"
);
} else {
debug!(
"Resolving despite existing lockfile due to mismatched source: `{name}` (unexpected: `virtual`)"
);
}
Ok(Self::Preferable(lock))
}
SatisfiesResult::MismatchedDynamic(name, expected) => {
if expected {
debug!(
"Resolving despite existing lockfile due to static version: `{name}` (expected a dynamic version)"
);
} else {
debug!(
"Resolving despite existing lockfile due to dynamic version: `{name}` (expected a static version)"
);
}
Ok(Self::Preferable(lock))
}
SatisfiesResult::MismatchedVersion(name, expected, actual) => {
if let Some(actual) = actual {
debug!(
"Resolving despite existing lockfile due to mismatched version: `{name}` (expected: `{expected}`, found: `{actual}`)"
);
} else {
debug!(
"Resolving despite existing lockfile due to mismatched version: `{name}` (expected: `{expected}`)"
);
}
Ok(Self::Preferable(lock))
}
SatisfiesResult::MismatchedRequirements(expected, actual) => {
debug!(
"Resolving despite existing lockfile due to mismatched requirements:\n Requested: {:?}\n Existing: {:?}",
expected, actual
);
Ok(Self::Preferable(lock))
}
SatisfiesResult::MismatchedConstraints(expected, actual) => {
debug!(
"Resolving despite existing lockfile due to mismatched constraints:\n Requested: {:?}\n Existing: {:?}",
expected, actual
);
Ok(Self::Preferable(lock))
}
SatisfiesResult::MismatchedOverrides(expected, actual) => {
debug!(
"Resolving despite existing lockfile due to mismatched overrides:\n Requested: {:?}\n Existing: {:?}",
expected, actual
);
Ok(Self::Preferable(lock))
}
SatisfiesResult::MismatchedExcludes(expected, actual) => {
debug!(
"Resolving despite existing lockfile due to mismatched excludes:\n Requested: {:?}\n Existing: {:?}",
expected, actual
);
Ok(Self::Preferable(lock))
}
SatisfiesResult::MismatchedBuildConstraints(expected, actual) => {
debug!(
"Resolving despite existing lockfile due to mismatched build constraints:\n Requested: {:?}\n Existing: {:?}",
expected, actual
);
Ok(Self::Preferable(lock))
}
SatisfiesResult::MismatchedDependencyGroups(expected, actual) => {
debug!(
"Resolving despite existing lockfile due to mismatched dependency groups:\n Requested: {:?}\n Existing: {:?}",
expected, actual
);
Ok(Self::Preferable(lock))
}
SatisfiesResult::MismatchedStaticMetadata(expected, actual) => {
debug!(
"Resolving despite existing lockfile due to mismatched static metadata:\n Requested: {:?}\n Existing: {:?}",
expected, actual
);
Ok(Self::Preferable(lock))
}
SatisfiesResult::MissingRoot(name) => {
debug!("Resolving despite existing lockfile due to missing root package: `{name}`");
Ok(Self::Preferable(lock))
}
SatisfiesResult::MissingRemoteIndex(name, version, index) => {
debug!(
"Resolving despite existing lockfile due to missing remote index: `{name}` `{version}` from `{index}`"
);
Ok(Self::Preferable(lock))
}
SatisfiesResult::MissingLocalIndex(name, version, index) => {
debug!(
"Resolving despite existing lockfile due to missing local index: `{name}` `{version}` from `{}`",
index.display()
);
Ok(Self::Preferable(lock))
}
SatisfiesResult::MismatchedPackageRequirements(name, version, expected, actual) => {
if let Some(version) = version {
debug!(
"Resolving despite existing lockfile due to mismatched requirements for: `{name}=={version}`\n Requested: {:?}\n Existing: {:?}",
expected, actual
);
} else {
debug!(
"Resolving despite existing lockfile due to mismatched requirements for: `{name}`\n Requested: {:?}\n Existing: {:?}",
expected, actual
);
}
Ok(Self::Preferable(lock))
}
SatisfiesResult::MismatchedPackageDependencyGroups(name, version, expected, actual) => {
if let Some(version) = version {
debug!(
"Resolving despite existing lockfile due to mismatched dependency groups for: `{name}=={version}`\n Requested: {:?}\n Existing: {:?}",
expected, actual
);
} else {
debug!(
"Resolving despite existing lockfile due to mismatched dependency groups for: `{name}`\n Requested: {:?}\n Existing: {:?}",
expected, actual
);
}
Ok(Self::Preferable(lock))
}
SatisfiesResult::MismatchedPackageProvidesExtra(name, version, expected, actual) => {
if let Some(version) = version {
debug!(
"Resolving despite existing lockfile due to mismatched extras for: `{name}=={version}`\n Requested: {:?}\n Existing: {:?}",
expected, actual
);
} else {
debug!(
"Resolving despite existing lockfile due to mismatched extras for: `{name}`\n Requested: {:?}\n Existing: {:?}",
expected, actual
);
}
Ok(Self::Preferable(lock))
}
SatisfiesResult::MissingVersion(name) => {
debug!("Resolving despite existing lockfile due to missing version: `{name}`");
Ok(Self::Preferable(lock))
}
}
}
#[must_use]
fn into_lock(self) -> Lock {
match self {
Self::Unusable(lock) => lock,
Self::Satisfies(lock) => lock,
Self::Preferable(lock) => lock,
Self::Versions(lock) => lock,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
struct LockEventVersion<'lock> {
version: Option<&'lock Version>,
sha: Option<&'lock str>,
}
impl<'lock> From<&'lock Package> for LockEventVersion<'lock> {
fn from(value: &'lock Package) -> Self {
Self {
version: value.version(),
sha: value.git_sha().map(GitOid::as_tiny_str),
}
}
}
impl std::fmt::Display for LockEventVersion<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match (self.version, self.sha) {
(Some(version), Some(sha)) => write!(f, "v{version} ({sha})"),
(Some(version), None) => write!(f, "v{version}"),
(None, Some(sha)) => write!(f, "(dynamic) ({sha})"),
(None, None) => write!(f, "(dynamic)"),
}
}
}
#[derive(Debug, Clone)]
enum LockEvent<'lock> {
Update(
DryRun,
PackageName,
BTreeSet<LockEventVersion<'lock>>,
BTreeSet<LockEventVersion<'lock>>,
),
Add(DryRun, PackageName, BTreeSet<LockEventVersion<'lock>>),
Remove(DryRun, PackageName, BTreeSet<LockEventVersion<'lock>>),
}
impl<'lock> LockEvent<'lock> {
fn detect_changes(
existing_lock: Option<&'lock Lock>,
new_lock: &'lock Lock,
dry_run: DryRun,
) -> impl Iterator<Item = Self> {
let mut existing_packages: FxHashMap<&PackageName, BTreeSet<LockEventVersion>> =
if let Some(existing_lock) = existing_lock {
existing_lock.packages().iter().fold(
FxHashMap::with_capacity_and_hasher(
existing_lock.packages().len(),
FxBuildHasher,
),
|mut acc, package| {
acc.entry(package.name())
.or_default()
.insert(LockEventVersion::from(package));
acc
},
)
} else {
FxHashMap::default()
};
let mut new_packages: FxHashMap<&PackageName, BTreeSet<LockEventVersion>> =
new_lock.packages().iter().fold(
FxHashMap::with_capacity_and_hasher(new_lock.packages().len(), FxBuildHasher),
|mut acc, package| {
acc.entry(package.name())
.or_default()
.insert(LockEventVersion::from(package));
acc
},
);
let names = existing_packages
.keys()
.chain(new_packages.keys())
.map(|name| (*name).clone())
.collect::<BTreeSet<_>>();
names.into_iter().filter_map(move |name| {
match (existing_packages.remove(&name), new_packages.remove(&name)) {
(Some(existing_versions), Some(new_versions)) => {
if existing_versions != new_versions {
Some(Self::Update(dry_run, name, existing_versions, new_versions))
} else {
None
}
}
(Some(existing_versions), None) => {
Some(Self::Remove(dry_run, name, existing_versions))
}
(None, Some(new_versions)) => Some(Self::Add(dry_run, name, new_versions)),
(None, None) => {
unreachable!("The key `{name}` should exist in at least one of the maps");
}
}
})
}
}
impl std::fmt::Display for LockEvent<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Update(dry_run, name, existing_versions, new_versions) => {
let existing_versions = existing_versions
.iter()
.map(std::string::ToString::to_string)
.collect::<Vec<_>>()
.join(", ");
let new_versions = new_versions
.iter()
.map(std::string::ToString::to_string)
.collect::<Vec<_>>()
.join(", ");
write!(
f,
"{} {name} {existing_versions} -> {new_versions}",
if dry_run.enabled() {
"Update"
} else {
"Updated"
}
.green()
.bold()
)
}
Self::Add(dry_run, name, new_versions) => {
let new_versions = new_versions
.iter()
.map(std::string::ToString::to_string)
.collect::<Vec<_>>()
.join(", ");
write!(
f,
"{} {name} {new_versions}",
if dry_run.enabled() { "Add" } else { "Added" }
.green()
.bold()
)
}
Self::Remove(dry_run, name, existing_versions) => {
let existing_versions = existing_versions
.iter()
.map(std::string::ToString::to_string)
.collect::<Vec<_>>()
.join(", ");
write!(
f,
"{} {name} {existing_versions}",
if dry_run.enabled() {
"Remove"
} else {
"Removed"
}
.red()
.bold()
)
}
}
}
}