Skip to content

Commit

Permalink
Discard insufficient fork markers
Browse files Browse the repository at this point in the history
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).
  • Loading branch information
konstin committed Jan 20, 2025
1 parent 6bf84b4 commit 75a28a2
Show file tree
Hide file tree
Showing 2 changed files with 118 additions and 0 deletions.
37 changes: 37 additions & 0 deletions crates/uv/src/commands/project/lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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!(
Expand Down
81 changes: 81 additions & 0 deletions crates/uv/tests/it/lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(())
}

0 comments on commit 75a28a2

Please sign in to comment.