Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions crates/uv-resolver/src/lock/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -604,6 +604,39 @@ impl Lock {
}
}
}

// Normalize fork markers by round-tripping through simplify/complexify.
//
// During lockfile deserialization, `SimplifiedMarkerTree::into_marker`
// folds `requires-python` constraints back into the marker tree. For
// example, with `requires-python = ">=3.12"`, the resolver produces
// `sys_platform != 'win32'` but deserialization yields
// `python_full_version >= '3.12' and sys_platform != 'win32'`.
//
// Without this normalization, the `PartialEq` comparison between a
// freshly-resolved lock and one read from disk can report false
// differences (e.g., `--dry-run` showing changes when there are none).
let fork_markers = fork_markers
.into_iter()
.map(|marker| {
let complexified = SimplifiedMarkerTree::new(&requires_python, marker.combined())
.into_marker(&requires_python);
UniversalMarker::from_combined(complexified)
})
.collect();
for package in &mut packages {
package.fork_markers = package
.fork_markers
.iter()
.map(|marker| {
let complexified =
SimplifiedMarkerTree::new(&requires_python, marker.combined())
.into_marker(&requires_python);
UniversalMarker::from_combined(complexified)
})
.collect();
}

let lock = Self {
version,
revision,
Expand Down
56 changes: 56 additions & 0 deletions crates/uv/tests/it/lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23670,6 +23670,62 @@ fn lock_dry_run_noop() -> Result<()> {
Ok(())
}

/// Regression test for <https://github.com/astral-sh/uv/issues/16839>.
///
/// When a lockfile is deserialized, fork markers go through
/// `SimplifiedMarkerTree::into_marker(&requires_python)`, which folds
/// `requires-python` constraints back into the marker tree. For example, with
/// `requires-python = ">=3.12"`, the resolver produces fork markers like
/// `sys_platform != 'win32'`, but after the simplify/complexify round-trip
/// during deserialization the same marker becomes
/// `python_full_version >= '3.12' and sys_platform != 'win32'`.
///
/// Both are semantically equivalent given the project's `requires-python`
/// bound, but `PartialEq` on the `Lock` struct sees them as different,
/// causing `--dry-run --upgrade` to falsely report "Lockfile changes detected"
/// when the lockfile was actually up-to-date.
#[test]
fn lock_dry_run_upgrade_multi_version() -> Result<()> {
let context = uv_test::test_context!("3.12");

let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
"markupsafe<2 ; sys_platform != 'win32'",
"markupsafe==2.0.0 ; sys_platform == 'win32'",
]
"#,
)?;

// Create the initial lockfile.
uv_snapshot!(context.filters(), context.lock(), @"
success: true
exit_code: 0
----- stdout -----

----- stderr -----
Resolved 3 packages in [TIME]
");

// Dry-run with --upgrade should report no changes.
uv_snapshot!(context.filters(), context.lock().arg("--dry-run").arg("-U"), @"
success: true
exit_code: 0
----- stdout -----

----- stderr -----
Resolved 3 packages in [TIME]
No lockfile changes detected
");

Ok(())
}

#[test]
fn lock_group_include() -> Result<()> {
let context = uv_test::test_context!("3.12");
Expand Down
Loading