From 33b70b17abd6192a3a00649b805ca0782c99548e Mon Sep 17 00:00:00 2001 From: konsti Date: Thu, 13 Mar 2025 15:49:37 +0100 Subject: [PATCH] Discard insufficient fork markers (#10682) 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). Part 1/2 of #10669 --- crates/uv-resolver/src/lock/mod.rs | 35 +++++++++++ crates/uv/src/commands/project/lock.rs | 9 +++ crates/uv/tests/it/lock.rs | 81 ++++++++++++++++++++++++++ 3 files changed, 125 insertions(+) diff --git a/crates/uv-resolver/src/lock/mod.rs b/crates/uv-resolver/src/lock/mod.rs index 3e32ce727..c5733cee6 100644 --- a/crates/uv-resolver/src/lock/mod.rs +++ b/crates/uv-resolver/src/lock/mod.rs @@ -723,8 +723,43 @@ impl Lock { self.fork_markers.as_slice() } + /// Checks whether the fork markers cover the entire supported marker space. + /// + /// Returns the actually covered and the expected marker space on validation error. + pub fn check_marker_coverage(&self) -> Result<(), (MarkerTree, MarkerTree)> { + let fork_markers_union = if self.fork_markers().is_empty() { + self.requires_python.to_marker_tree() + } else { + let mut fork_markers_union = MarkerTree::FALSE; + for fork_marker in self.fork_markers() { + fork_markers_union.or(fork_marker.pep508()); + } + fork_markers_union + }; + let mut environments_union = if !self.supported_environments.is_empty() { + let mut environments_union = MarkerTree::FALSE; + for fork_marker in &self.supported_environments { + environments_union.or(*fork_marker); + } + environments_union + } else { + MarkerTree::TRUE + }; + // When a user defines environments, they are implicitly constrained by requires-python. + environments_union.and(self.requires_python.to_marker_tree()); + if fork_markers_union.negate().is_disjoint(environments_union) { + Ok(()) + } else { + Err((fork_markers_union, environments_union)) + } + } + /// Returns the TOML representation of this lockfile. pub fn to_toml(&self) -> Result { + // Catch a lockfile where the union of fork markers doesn't cover the supported + // environments. + debug_assert!(self.check_marker_coverage().is_ok()); + // We construct a TOML document manually instead of going through Serde to enable // the use of inline tables. let mut doc = toml_edit::DocumentMut::new(); diff --git a/crates/uv/src/commands/project/lock.rs b/crates/uv/src/commands/project/lock.rs index 6b3576d82..8853a27a8 100644 --- a/crates/uv/src/commands/project/lock.rs +++ b/crates/uv/src/commands/project/lock.rs @@ -965,6 +965,15 @@ impl ValidatedLock { return Ok(Self::Versions(lock)); } + if let Err((fork_markers_union, environments_union)) = lock.check_marker_coverage() { + 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 set of required platforms has changed, we have to perform a clean resolution. let expected = lock.simplified_required_environments(); let actual = required_environments diff --git a/crates/uv/tests/it/lock.rs b/crates/uv/tests/it/lock.rs index 3574d000a..61cc1e2ee 100644 --- a/crates/uv/tests/it/lock.rs +++ b/crates/uv/tests/it/lock.rs @@ -26372,3 +26372,84 @@ fn lock_empty_extra() -> 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(()) +}