Skip to content

Commit 0c352c6

Browse files
authored
Error on lockfiles with incoherent wheel versions (#12235)
Reject lockfiles where the package version and the wheel versions are incoherent. This implicitly checks that all wheel files have the same version. It does not check for the source dist version, since a source dist may not contain a version in the filename and attempting to deserialize source dist filenames we may not need is a performance overhead for something that's already slow in `uv run`. Fixes #12164
1 parent 7ea2f65 commit 0c352c6

File tree

2 files changed

+114
-0
lines changed

2 files changed

+114
-0
lines changed

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

+20
Original file line numberDiff line numberDiff line change
@@ -2871,6 +2871,19 @@ impl PackageWire {
28712871
requires_python: &RequiresPython,
28722872
unambiguous_package_ids: &FxHashMap<PackageName, PackageId>,
28732873
) -> Result<Package, LockError> {
2874+
// Consistency check
2875+
if let Some(version) = &self.id.version {
2876+
for wheel in &self.wheels {
2877+
if version != &wheel.filename.version {
2878+
return Err(LockError::from(LockErrorKind::InconsistentVersions {
2879+
name: self.id.name,
2880+
}));
2881+
}
2882+
}
2883+
// We can't check the source dist version since it does not need to contain the version
2884+
// in the filename.
2885+
}
2886+
28742887
let unwire_deps = |deps: Vec<DependencyWire>| -> Result<Vec<Dependency>, LockError> {
28752888
deps.into_iter()
28762889
.map(|dep| dep.unwire(requires_python, unambiguous_package_ids))
@@ -5156,6 +5169,13 @@ enum LockErrorKind {
51565169
#[source]
51575170
err: uv_distribution::Error,
51585171
},
5172+
/// A package has inconsistent versions in a single entry
5173+
// Using name instead of id since the version in the id is part of the conflict.
5174+
#[error("Locked package and file versions are inconsistent for `{name}`", name = name.cyan())]
5175+
InconsistentVersions {
5176+
/// The name of the package with the inconsistent entry.
5177+
name: PackageName,
5178+
},
51595179
#[error(
51605180
"Found conflicting extras `{package1}[{extra1}]` \
51615181
and `{package2}[{extra2}]` enabled simultaneously"

crates/uv/tests/it/sync.rs

+94
Original file line numberDiff line numberDiff line change
@@ -8471,3 +8471,97 @@ fn prune_cache_url_subdirectory() -> Result<()> {
84718471

84728472
Ok(())
84738473
}
8474+
8475+
/// Test that incoherence in the versions in a package entry of the lockfile versions is caught.
8476+
///
8477+
/// See <https://github.com/astral-sh/uv/issues/12164>
8478+
#[test]
8479+
fn locked_version_coherence() -> Result<()> {
8480+
let context = TestContext::new("3.12");
8481+
8482+
let pyproject_toml = context.temp_dir.child("pyproject.toml");
8483+
pyproject_toml.write_str(
8484+
r#"
8485+
[project]
8486+
name = "project"
8487+
version = "0.1.0"
8488+
requires-python = ">=3.12"
8489+
dependencies = ["iniconfig"]
8490+
"#,
8491+
)?;
8492+
8493+
uv_snapshot!(context.filters(), context.lock(), @r"
8494+
success: true
8495+
exit_code: 0
8496+
----- stdout -----
8497+
8498+
----- stderr -----
8499+
Resolved 2 packages in [TIME]
8500+
");
8501+
8502+
let lock = context.read("uv.lock");
8503+
8504+
insta::with_settings!({
8505+
filters => context.filters(),
8506+
}, {
8507+
assert_snapshot!(
8508+
lock, @r#"
8509+
version = 1
8510+
revision = 1
8511+
requires-python = ">=3.12"
8512+
8513+
[options]
8514+
exclude-newer = "2024-03-25T00:00:00Z"
8515+
8516+
[[package]]
8517+
name = "iniconfig"
8518+
version = "2.0.0"
8519+
source = { registry = "https://pypi.org/simple" }
8520+
sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 }
8521+
wheels = [
8522+
{ url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 },
8523+
]
8524+
8525+
[[package]]
8526+
name = "project"
8527+
version = "0.1.0"
8528+
source = { virtual = "." }
8529+
dependencies = [
8530+
{ name = "iniconfig" },
8531+
]
8532+
8533+
[package.metadata]
8534+
requires-dist = [{ name = "iniconfig" }]
8535+
"#);
8536+
});
8537+
8538+
// Write an inconsistent iniconfig entry
8539+
context
8540+
.temp_dir
8541+
.child("uv.lock")
8542+
.write_str(&lock.replace(r#"version = "2.0.0""#, r#"version = "1.0.0""#))?;
8543+
8544+
// An inconsistent lockfile should fail with `--locked`
8545+
uv_snapshot!(context.filters(), context.sync().arg("--locked"), @r"
8546+
success: false
8547+
exit_code: 2
8548+
----- stdout -----
8549+
8550+
----- stderr -----
8551+
error: Failed to parse `uv.lock`
8552+
Caused by: Locked package and file versions are inconsistent for `iniconfig`
8553+
");
8554+
8555+
// Without `--locked`, we could fail or recreate the lockfile, currently, we fail.
8556+
uv_snapshot!(context.filters(), context.lock(), @r"
8557+
success: false
8558+
exit_code: 2
8559+
----- stdout -----
8560+
8561+
----- stderr -----
8562+
error: Failed to parse `uv.lock`
8563+
Caused by: Locked package and file versions are inconsistent for `iniconfig`
8564+
");
8565+
8566+
Ok(())
8567+
}

0 commit comments

Comments
 (0)