From 75a28a2e99058fe35a7e9aebb08d847d6210190c Mon Sep 17 00:00:00 2001 From: konstin Date: Thu, 16 Jan 2025 15:42:57 +0100 Subject: [PATCH] Discard insufficient fork markers In #10669, a pyproject.toml with requires-python but no environment had a lockfile covering only a subset of the requires-python space: ```toml resolution-markers = [ "python_full_version >= '3.10' and platform_python_implementation == 'CPython'", "python_full_version == '3.9.*'", "python_full_version < '3.9'", ] ``` This marker set is invalid, we have to reject the lockfile. (We can still use the versions though, to avoid churn). --- crates/uv/src/commands/project/lock.rs | 37 ++++++++++++ crates/uv/tests/it/lock.rs | 81 ++++++++++++++++++++++++++ 2 files changed, 118 insertions(+) diff --git a/crates/uv/src/commands/project/lock.rs b/crates/uv/src/commands/project/lock.rs index 1f7ece16db630..b00bfa456e552 100644 --- a/crates/uv/src/commands/project/lock.rs +++ b/crates/uv/src/commands/project/lock.rs @@ -24,6 +24,7 @@ use uv_distribution_types::{ use uv_git::ResolvedRepositoryReference; use uv_normalize::{GroupName, PackageName}; use uv_pep440::Version; +use uv_pep508::MarkerTree; use uv_pypi_types::{Conflicts, Requirement, SupportedEnvironments}; use uv_python::{Interpreter, PythonDownloads, PythonEnvironment, PythonPreference, PythonRequest}; use uv_requirements::upgrade::{read_lock_requirements, LockedRequirements}; @@ -883,6 +884,42 @@ impl ValidatedLock { return Ok(Self::Versions(lock)); } + // Catch a lockfile where the union of fork markers doesn't cover the supported + // environments. + // + // We subset by requires-python, since the space outside of it does not matter. The fork + // markers however should contain the requires-python value, if they don't it wouldn't be + // a problem on sync, but it indicates a broken lockfile where we should recompute the + // fork-markers (the versions are preserved, so the churn is low). + let fork_markers_union = if lock.fork_markers().is_empty() { + requires_python.to_marker_tree() + } else { + let mut fork_markers_union = MarkerTree::FALSE; + for fork_marker in lock.fork_markers() { + fork_markers_union.or(fork_marker.pep508()); + } + fork_markers_union + }; + let mut environments_union = if let Some(environments) = environments { + let mut environments_union = MarkerTree::FALSE; + for fork_marker in environments.as_markers() { + environments_union.or(*fork_marker); + } + environments_union + } else { + MarkerTree::TRUE + }; + // We respect requires-python in addition to the environments the user specified. + environments_union.and(requires_python.to_marker_tree()); + if !fork_markers_union.negate().is_disjoint(environments_union) { + warn_user!( + "Ignoring 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 the conflicting group config has changed, we have to perform a clean resolution. if conflicts != lock.conflicts() { debug!( diff --git a/crates/uv/tests/it/lock.rs b/crates/uv/tests/it/lock.rs index cc9137f04efaa..0a5098a449da2 100644 --- a/crates/uv/tests/it/lock.rs +++ b/crates/uv/tests/it/lock.rs @@ -23155,3 +23155,84 @@ fn lock_pytorch_cpu() -> Result<()> { Ok(()) } + +/// The fork markers in the lockfile don't cover the supported environments (here: universal). We +/// need to discard the lockfile. +#[test] +fn lock_invalid_fork_markers() -> Result<()> { + let context = TestContext::new("3.12"); + + context.temp_dir.child("pyproject.toml").write_str( + r#" + [project] + name = "attrs" + requires-python = ">=3.8" + version = "1.0.0" + + [dependency-groups] + dev = ["idna"] + "#, + )?; + + context.temp_dir.child("uv.lock").write_str( + r#" + version = 1 + requires-python = ">=3.8" + resolution-markers = [ + "python_full_version >= '3.10' and platform_python_implementation == 'CPython'", + "python_full_version == '3.9.*'", + "python_full_version < '3.9'", + ] + + [options] + exclude-newer = "2024-03-25T00:00:00Z" + + [[package]] + name = "attrs" + version = "1.0.0" + source = { editable = "." } + + [package.dev-dependencies] + dev = [ + { name = "idna", marker = "python_full_version < '3.10' or platform_python_implementation == 'CPython'" }, + ] + + [package.metadata] + + [package.metadata.requires-dev] + dev = [{ name = "idna" }] + + [[package]] + name = "idna" + version = "3.10" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, + ] + "#, + )?; + + uv_snapshot!(context.filters(), context.lock(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + warning: Ignoring existing lockfile due to fork markers not covering the supported environments: `(python_full_version >= '3.8' and python_full_version < '3.10') or (python_full_version >= '3.8' and platform_python_implementation == 'CPython')` vs `python_full_version >= '3.8'` + Resolved 2 packages in [TIME] + Updated idna v3.10 -> v3.6 + "###); + + // Check that the lockfile got updated and we don't show the warning anymore. + uv_snapshot!(context.filters(), context.lock(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + "###); + + Ok(()) +}