Skip to content

Commit 33b70b1

Browse files
authored
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
1 parent 797f1fb commit 33b70b1

File tree

3 files changed

+125
-0
lines changed

3 files changed

+125
-0
lines changed

crates/uv-resolver/src/lock/mod.rs

+35
Original file line numberDiff line numberDiff line change
@@ -723,8 +723,43 @@ impl Lock {
723723
self.fork_markers.as_slice()
724724
}
725725

726+
/// Checks whether the fork markers cover the entire supported marker space.
727+
///
728+
/// Returns the actually covered and the expected marker space on validation error.
729+
pub fn check_marker_coverage(&self) -> Result<(), (MarkerTree, MarkerTree)> {
730+
let fork_markers_union = if self.fork_markers().is_empty() {
731+
self.requires_python.to_marker_tree()
732+
} else {
733+
let mut fork_markers_union = MarkerTree::FALSE;
734+
for fork_marker in self.fork_markers() {
735+
fork_markers_union.or(fork_marker.pep508());
736+
}
737+
fork_markers_union
738+
};
739+
let mut environments_union = if !self.supported_environments.is_empty() {
740+
let mut environments_union = MarkerTree::FALSE;
741+
for fork_marker in &self.supported_environments {
742+
environments_union.or(*fork_marker);
743+
}
744+
environments_union
745+
} else {
746+
MarkerTree::TRUE
747+
};
748+
// When a user defines environments, they are implicitly constrained by requires-python.
749+
environments_union.and(self.requires_python.to_marker_tree());
750+
if fork_markers_union.negate().is_disjoint(environments_union) {
751+
Ok(())
752+
} else {
753+
Err((fork_markers_union, environments_union))
754+
}
755+
}
756+
726757
/// Returns the TOML representation of this lockfile.
727758
pub fn to_toml(&self) -> Result<String, toml_edit::ser::Error> {
759+
// Catch a lockfile where the union of fork markers doesn't cover the supported
760+
// environments.
761+
debug_assert!(self.check_marker_coverage().is_ok());
762+
728763
// We construct a TOML document manually instead of going through Serde to enable
729764
// the use of inline tables.
730765
let mut doc = toml_edit::DocumentMut::new();

crates/uv/src/commands/project/lock.rs

+9
Original file line numberDiff line numberDiff line change
@@ -965,6 +965,15 @@ impl ValidatedLock {
965965
return Ok(Self::Versions(lock));
966966
}
967967

968+
if let Err((fork_markers_union, environments_union)) = lock.check_marker_coverage() {
969+
warn_user!(
970+
"Ignoring existing lockfile due to fork markers not covering the supported environments: `{}` vs `{}`",
971+
fork_markers_union.try_to_string().unwrap_or("true".to_string()),
972+
environments_union.try_to_string().unwrap_or("true".to_string()),
973+
);
974+
return Ok(Self::Versions(lock));
975+
}
976+
968977
// If the set of required platforms has changed, we have to perform a clean resolution.
969978
let expected = lock.simplified_required_environments();
970979
let actual = required_environments

crates/uv/tests/it/lock.rs

+81
Original file line numberDiff line numberDiff line change
@@ -26372,3 +26372,84 @@ fn lock_empty_extra() -> Result<()> {
2637226372

2637326373
Ok(())
2637426374
}
26375+
26376+
/// The fork markers in the lockfile don't cover the supported environments (here: universal). We
26377+
/// need to discard the lockfile.
26378+
#[test]
26379+
fn lock_invalid_fork_markers() -> Result<()> {
26380+
let context = TestContext::new("3.12");
26381+
26382+
context.temp_dir.child("pyproject.toml").write_str(
26383+
r#"
26384+
[project]
26385+
name = "attrs"
26386+
requires-python = ">=3.8"
26387+
version = "1.0.0"
26388+
26389+
[dependency-groups]
26390+
dev = ["idna"]
26391+
"#,
26392+
)?;
26393+
26394+
context.temp_dir.child("uv.lock").write_str(
26395+
r#"
26396+
version = 1
26397+
requires-python = ">=3.8"
26398+
resolution-markers = [
26399+
"python_full_version >= '3.10' and platform_python_implementation == 'CPython'",
26400+
"python_full_version == '3.9.*'",
26401+
"python_full_version < '3.9'",
26402+
]
26403+
26404+
[options]
26405+
exclude-newer = "2024-03-25T00:00:00Z"
26406+
26407+
[[package]]
26408+
name = "attrs"
26409+
version = "1.0.0"
26410+
source = { editable = "." }
26411+
26412+
[package.dev-dependencies]
26413+
dev = [
26414+
{ name = "idna", marker = "python_full_version < '3.10' or platform_python_implementation == 'CPython'" },
26415+
]
26416+
26417+
[package.metadata]
26418+
26419+
[package.metadata.requires-dev]
26420+
dev = [{ name = "idna" }]
26421+
26422+
[[package]]
26423+
name = "idna"
26424+
version = "3.10"
26425+
source = { registry = "https://pypi.org/simple" }
26426+
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 }
26427+
wheels = [
26428+
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
26429+
]
26430+
"#,
26431+
)?;
26432+
26433+
uv_snapshot!(context.filters(), context.lock(), @r###"
26434+
success: true
26435+
exit_code: 0
26436+
----- stdout -----
26437+
26438+
----- stderr -----
26439+
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'`
26440+
Resolved 2 packages in [TIME]
26441+
Updated idna v3.10 -> v3.6
26442+
"###);
26443+
26444+
// Check that the lockfile got updated and we don't show the warning anymore.
26445+
uv_snapshot!(context.filters(), context.lock(), @r###"
26446+
success: true
26447+
exit_code: 0
26448+
----- stdout -----
26449+
26450+
----- stderr -----
26451+
Resolved 2 packages in [TIME]
26452+
"###);
26453+
26454+
Ok(())
26455+
}

0 commit comments

Comments
 (0)