Skip to content
Merged
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
30 changes: 30 additions & 0 deletions crates/uv-resolver/src/lock/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -768,6 +768,36 @@ impl Lock {
}
}

/// Checks whether the new requires-python specification is disjoint with
/// the fork markers in this lock file.
///
/// If they are disjoint, then the union of the fork markers along with the
/// given requires-python specification (converted to a marker tree) are
/// returned.
///
/// When disjoint, the fork markers in the lock file should be dropped and
/// not used.
pub fn requires_python_coverage(
&self,
new_requires_python: &RequiresPython,
) -> 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 new_requires_python = new_requires_python.to_marker_tree();
if fork_markers_union.is_disjoint(new_requires_python) {
Err((fork_markers_union, new_requires_python))
} else {
Ok(())
}
}

/// Returns the TOML representation of this lockfile.
pub fn to_toml(&self) -> Result<String, toml_edit::ser::Error> {
// Catch a lockfile where the union of fork markers doesn't cover the supported
Expand Down
62 changes: 45 additions & 17 deletions crates/uv/src/commands/project/lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -983,13 +983,54 @@ impl ValidatedLock {
return Ok(Self::Unusable(lock));
}
Upgrade::Packages(_) => {
// If the user specified `--upgrade-package`, then at best we can prefer some of
// the existing versions.
debug!("Ignoring existing lockfile due to `--upgrade-package`");
return Ok(Self::Preferable(lock));
// This is handled below, after some checks regarding fork
// markers. In particular, we'd like to return `Preferable`
// here, but we shouldn't if the fork markers cannot be
// reused.
}
}

// NOTE: It's important that this appears before any possible path that
// returns `Self::Preferable`. In particular, if our fork markers are
// bunk, then we shouldn't return a result that indicates we should try
// to re-use the existing fork markers.
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 `{}`",
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, are you sure this should be a user-facing warning? I don’t think any of the other lockfile-invalidation messages are user-facing. They just go through tracing.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I didn't do that. That's what is on main:

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()),
);

It looks like it was a user facing warning from the beginning: #10682

I in turn copied this to the disjoint requires-python check as well. Do you want me to make both of them traces instead?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah ok. Just leave it as-is then, thanks.

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));
}

// NOTE: Similarly as above, this should also appear before any
// possible code path that can return `Self::Preferable`.
if let Err((fork_markers_union, requires_python_marker)) =
lock.requires_python_coverage(requires_python)
{
warn_user!(
"Ignoring existing lockfile due to fork markers being disjoint with `requires-python`: `{}` vs `{}`",
fork_markers_union
.try_to_string()
.unwrap_or("true".to_string()),
requires_python_marker
.try_to_string()
.unwrap_or("true".to_string()),
);
return Ok(Self::Versions(lock));
}

if let Upgrade::Packages(_) = upgrade {
// If the user specified `--upgrade-package`, then at best we can prefer some of
// the existing versions.
debug!("Ignoring existing lockfile due to `--upgrade-package`");
return Ok(Self::Preferable(lock));
}

// If the Requires-Python bound has changed, we have to perform a clean resolution, since
// the set of `resolution-markers` may no longer cover the entire supported Python range.
if lock.requires_python().range() != requires_python.range() {
Expand Down Expand Up @@ -1022,19 +1063,6 @@ 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
Expand Down
169 changes: 167 additions & 2 deletions crates/uv/tests/it/lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4731,15 +4731,16 @@ fn lock_requires_python_wheels() -> Result<()> {
"#,
)?;

uv_snapshot!(context.filters(), context.lock(), @r###"
uv_snapshot!(context.filters(), context.lock(), @r"
success: true
exit_code: 0
----- stdout -----

----- stderr -----
Using CPython 3.11.[X] interpreter at: [PYTHON-3.11]
warning: Ignoring existing lockfile due to fork markers being disjoint with `requires-python`: `python_full_version == '3.12.*'` vs `python_full_version == '3.11.*'`
Resolved 2 packages in [TIME]
"###);
");

let lock = fs_err::read_to_string(&lockfile).unwrap();

Expand Down Expand Up @@ -28020,6 +28021,170 @@ fn lock_conflict_for_disjoint_python_version() -> Result<()> {
Ok(())
}

/// Check that we hint if the resolution failed for a different platform.
#[cfg(feature = "python-patch")]
#[test]
fn lock_requires_python_empty_lock_file() -> Result<()> {
// N.B. These versions were selected based on what was
// in `.python-versions` at the time of writing (2025-06-16).
let (v1, v2) = ("3.13.0", "3.13.2");
let context = TestContext::new_with_versions(&[v1, v2]);

let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(&format!(
r#"
[project]
name = "renovate-bug-repro"
version = "0.1.0"
requires-python = "=={v1}"
dependencies = ["opencv-python-headless>=4.8"]
"#,
))?;

uv_snapshot!(context.filters(), context.lock(), @r"
success: true
exit_code: 0
----- stdout -----

----- stderr -----
Using CPython 3.13.0 interpreter at: [PYTHON-3.13.0]
Resolved 3 packages in [TIME]
");

let lock = context.read("uv.lock");
insta::with_settings!({
filters => context.filters(),
}, {
assert_snapshot!(
lock, @r#"
version = 1
revision = 2
requires-python = "==3.13.0"
resolution-markers = [
"sys_platform == 'darwin'",
"platform_machine == 'aarch64' and sys_platform == 'linux'",
"(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')",
]

[options]
exclude-newer = "2024-03-25T00:00:00Z"

[[package]]
name = "numpy"
version = "1.26.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/65/6e/09db70a523a96d25e115e71cc56a6f9031e7b8cd166c1ac8438307c14058/numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010", size = 15786129, upload-time = "2024-02-06T00:26:44.495Z" }

[[package]]
name = "opencv-python-headless"
version = "4.9.0.80"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "numpy" },
]
sdist = { url = "https://files.pythonhosted.org/packages/3d/b2/c308bc696bf5d75304175c62222ec8af9a6d5cfe36c14f19f15ea9d1a132/opencv-python-headless-4.9.0.80.tar.gz", hash = "sha256:71a4cd8cf7c37122901d8e81295db7fb188730e33a0e40039a4e59c1030b0958", size = 92910044, upload-time = "2023-12-31T13:34:50.518Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ac/42/da433fca5733a3ce7e88dd0d4018f70dcffaf48770b5142250815f4faddb/opencv_python_headless-4.9.0.80-cp37-abi3-macosx_10_16_x86_64.whl", hash = "sha256:2ea8a2edc4db87841991b2fbab55fc07b97ecb602e0f47d5d485bd75cee17c1a", size = 55689478, upload-time = "2023-12-31T14:31:30.476Z" },
{ url = "https://files.pythonhosted.org/packages/32/0c/a59f2a40d6058ee8126668dc5dff6977c913f6ecd21dbd15b41563409a18/opencv_python_headless-4.9.0.80-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:e0ee54e27be493e8f7850847edae3128e18b540dac1d7b2e4001b8944e11e1c6", size = 35354670, upload-time = "2023-12-31T16:38:31.588Z" },
{ url = "https://files.pythonhosted.org/packages/36/37/225a1f8be42610ffecf677558311ab0f9dfdc63537c250a2bce76762a380/opencv_python_headless-4.9.0.80-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57ce2865e8fec431c6f97a81e9faaf23fa5be61011d0a75ccf47a3c0d65fa73d", size = 28954368, upload-time = "2023-12-31T16:40:00.838Z" },
{ url = "https://files.pythonhosted.org/packages/71/19/3c65483a80a1d062d46ae20faf5404712d25cb1dfdcaf371efbd67c38544/opencv_python_headless-4.9.0.80-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:976656362d68d9f40a5c66f83901430538002465f7db59142784f3893918f3df", size = 49591873, upload-time = "2023-12-31T13:34:44.316Z" },
{ url = "https://files.pythonhosted.org/packages/10/98/300382ff6ddff3a487e808c8a76362e430f5016002fcbefb3b3117aad32b/opencv_python_headless-4.9.0.80-cp37-abi3-win32.whl", hash = "sha256:11e3849d83e6651d4e7699aadda9ec7ed7c38957cbbcb99db074f2a2d2de9670", size = 28488841, upload-time = "2023-12-31T13:34:31.974Z" },
{ url = "https://files.pythonhosted.org/packages/20/44/458a0a135866f5e08266566b32ad9a182a7a059a894effe6c41a9c841ff1/opencv_python_headless-4.9.0.80-cp37-abi3-win_amd64.whl", hash = "sha256:a8056c2cb37cd65dfcdf4153ca16f7362afcf3a50d600d6bb69c660fc61ee29c", size = 38536073, upload-time = "2023-12-31T13:34:39.675Z" },
]

[[package]]
name = "renovate-bug-repro"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "opencv-python-headless" },
]

[package.metadata]
requires-dist = [{ name = "opencv-python-headless", specifier = ">=4.8" }]
"#
);
});

pyproject_toml.write_str(&format!(
r#"
[project]
name = "renovate-bug-repro"
version = "0.1.0"
requires-python = "=={v2}"
dependencies = ["opencv-python-headless>=4.8"]
"#,
))?;

uv_snapshot!(context.filters(), context.lock().arg("--upgrade-package=python"), @r"
success: true
exit_code: 0
----- stdout -----

----- stderr -----
Using CPython 3.13.2 interpreter at: [PYTHON-3.13.2]
warning: Ignoring existing lockfile due to fork markers being disjoint with `requires-python`: `python_full_version == '3.13.0'` vs `python_full_version == '3.13.2'`
Resolved 3 packages in [TIME]
");

let lock = context.read("uv.lock");
insta::with_settings!({
filters => context.filters(),
}, {
assert_snapshot!(
lock, @r#"
version = 1
revision = 2
requires-python = "==3.13.2"
resolution-markers = [
"sys_platform == 'darwin'",
"platform_machine == 'aarch64' and sys_platform == 'linux'",
"(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')",
]

[options]
exclude-newer = "2024-03-25T00:00:00Z"

[[package]]
name = "numpy"
version = "1.26.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/65/6e/09db70a523a96d25e115e71cc56a6f9031e7b8cd166c1ac8438307c14058/numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010", size = 15786129, upload-time = "2024-02-06T00:26:44.495Z" }

[[package]]
name = "opencv-python-headless"
version = "4.9.0.80"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "numpy" },
]
sdist = { url = "https://files.pythonhosted.org/packages/3d/b2/c308bc696bf5d75304175c62222ec8af9a6d5cfe36c14f19f15ea9d1a132/opencv-python-headless-4.9.0.80.tar.gz", hash = "sha256:71a4cd8cf7c37122901d8e81295db7fb188730e33a0e40039a4e59c1030b0958", size = 92910044, upload-time = "2023-12-31T13:34:50.518Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ac/42/da433fca5733a3ce7e88dd0d4018f70dcffaf48770b5142250815f4faddb/opencv_python_headless-4.9.0.80-cp37-abi3-macosx_10_16_x86_64.whl", hash = "sha256:2ea8a2edc4db87841991b2fbab55fc07b97ecb602e0f47d5d485bd75cee17c1a", size = 55689478, upload-time = "2023-12-31T14:31:30.476Z" },
{ url = "https://files.pythonhosted.org/packages/32/0c/a59f2a40d6058ee8126668dc5dff6977c913f6ecd21dbd15b41563409a18/opencv_python_headless-4.9.0.80-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:e0ee54e27be493e8f7850847edae3128e18b540dac1d7b2e4001b8944e11e1c6", size = 35354670, upload-time = "2023-12-31T16:38:31.588Z" },
{ url = "https://files.pythonhosted.org/packages/36/37/225a1f8be42610ffecf677558311ab0f9dfdc63537c250a2bce76762a380/opencv_python_headless-4.9.0.80-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57ce2865e8fec431c6f97a81e9faaf23fa5be61011d0a75ccf47a3c0d65fa73d", size = 28954368, upload-time = "2023-12-31T16:40:00.838Z" },
{ url = "https://files.pythonhosted.org/packages/71/19/3c65483a80a1d062d46ae20faf5404712d25cb1dfdcaf371efbd67c38544/opencv_python_headless-4.9.0.80-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:976656362d68d9f40a5c66f83901430538002465f7db59142784f3893918f3df", size = 49591873, upload-time = "2023-12-31T13:34:44.316Z" },
{ url = "https://files.pythonhosted.org/packages/10/98/300382ff6ddff3a487e808c8a76362e430f5016002fcbefb3b3117aad32b/opencv_python_headless-4.9.0.80-cp37-abi3-win32.whl", hash = "sha256:11e3849d83e6651d4e7699aadda9ec7ed7c38957cbbcb99db074f2a2d2de9670", size = 28488841, upload-time = "2023-12-31T13:34:31.974Z" },
{ url = "https://files.pythonhosted.org/packages/20/44/458a0a135866f5e08266566b32ad9a182a7a059a894effe6c41a9c841ff1/opencv_python_headless-4.9.0.80-cp37-abi3-win_amd64.whl", hash = "sha256:a8056c2cb37cd65dfcdf4153ca16f7362afcf3a50d600d6bb69c660fc61ee29c", size = 38536073, upload-time = "2023-12-31T13:34:39.675Z" },
]

[[package]]
name = "renovate-bug-repro"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "opencv-python-headless" },
]

[package.metadata]
requires-dist = [{ name = "opencv-python-headless", specifier = ">=4.8" }]
"#
);
});

Ok(())
}

/// Check that we hint if the resolution failed for a different platform.
#[test]
fn lock_conflict_for_disjoint_platform() -> Result<()> {
Expand Down
12 changes: 8 additions & 4 deletions crates/uv/tests/it/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8140,22 +8140,23 @@ fn sync_dry_run() -> Result<()> {
"#,
)?;

uv_snapshot!(context.filters(), context.sync().arg("--dry-run"), @r###"
uv_snapshot!(context.filters(), context.sync().arg("--dry-run"), @r"
success: true
exit_code: 0
----- stdout -----

----- stderr -----
Using CPython 3.9.[X] interpreter at: [PYTHON-3.9]
Would replace existing virtual environment at: .venv
warning: Ignoring existing lockfile due to fork markers being disjoint with `requires-python`: `python_full_version >= '3.12'` vs `python_full_version == '3.9.*'`
Resolved 2 packages in [TIME]
Would update lockfile at: uv.lock
Would install 1 package
+ iniconfig==2.0.0
"###);
");

// Perform a full sync.
uv_snapshot!(context.filters(), context.sync(), @r###"
uv_snapshot!(context.filters(), context.sync(), @r"
success: true
exit_code: 0
----- stdout -----
Expand All @@ -8164,10 +8165,11 @@ fn sync_dry_run() -> Result<()> {
Using CPython 3.9.[X] interpreter at: [PYTHON-3.9]
Removed virtual environment at: .venv
Creating virtual environment at: .venv
warning: Ignoring existing lockfile due to fork markers being disjoint with `requires-python`: `python_full_version >= '3.12'` vs `python_full_version == '3.9.*'`
Resolved 2 packages in [TIME]
Installed 1 package in [TIME]
+ iniconfig==2.0.0
"###);
");

let output = context.sync().arg("--dry-run").arg("-vv").output()?;
let stderr = String::from_utf8_lossy(&output.stderr);
Expand Down Expand Up @@ -8658,6 +8660,7 @@ fn sync_locked_script() -> Result<()> {

----- stderr -----
Recreating script environment at: [CACHE_DIR]/environments-v2/script-[HASH]
warning: Ignoring existing lockfile due to fork markers being disjoint with `requires-python`: `python_full_version >= '3.11'` vs `python_full_version >= '3.8' and python_full_version < '3.11'`
Resolved 6 packages in [TIME]
error: The lockfile at `uv.lock` needs to be updated, but `--locked` was provided. To update the lockfile, run `uv lock`.
");
Expand All @@ -8669,6 +8672,7 @@ fn sync_locked_script() -> Result<()> {

----- stderr -----
Using script environment at: [CACHE_DIR]/environments-v2/script-[HASH]
warning: Ignoring existing lockfile due to fork markers being disjoint with `requires-python`: `python_full_version >= '3.11'` vs `python_full_version >= '3.8' and python_full_version < '3.11'`
Resolved 6 packages in [TIME]
Prepared 2 packages in [TIME]
Installed 6 packages in [TIME]
Expand Down
Loading