From c494f6912c285c9f3122e3c87963c64cff0f2ca1 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 5 Sep 2024 20:22:34 -0400 Subject: [PATCH] Take intersection of constraint and requirements hashes (#7108) ## Summary Small follow-up to #7093. --- crates/uv-types/src/hash.rs | 35 +++++++++++++++++++++----- crates/uv/tests/pip_install.rs | 46 ++++++++++++++++++++++++---------- 2 files changed, 62 insertions(+), 19 deletions(-) diff --git a/crates/uv-types/src/hash.rs b/crates/uv-types/src/hash.rs index 0b512650c963..39808a611b77 100644 --- a/crates/uv-types/src/hash.rs +++ b/crates/uv-types/src/hash.rs @@ -219,15 +219,35 @@ impl HashStrategy { .collect::, _>>()? }; - // Under `--require-hashes`, every requirement must include a hash. - if digests.is_empty() { - if mode.is_require() { - if constraint_hashes.get(&id).map_or(true, Vec::is_empty) { - return Err(HashStrategyError::MissingHashes( + let digests = if let Some(constraint) = constraint_hashes.remove(&id) { + if digests.is_empty() { + // If there are _only_ hashes on the constraints, use them. + constraint + } else { + // If there are constraint and requirement hashes, take the intersection. + let intersection: Vec<_> = digests + .into_iter() + .filter(|digest| constraint.contains(digest)) + .collect(); + if intersection.is_empty() { + return Err(HashStrategyError::NoIntersection( requirement.to_string(), mode, )); } + intersection + } + } else { + digests + }; + + // Under `--require-hashes`, every requirement must include a hash. + if digests.is_empty() { + if mode.is_require() { + return Err(HashStrategyError::MissingHashes( + requirement.to_string(), + mode, + )); } continue; } @@ -235,7 +255,8 @@ impl HashStrategy { requirement_hashes.insert(id, digests); } - // Merge the hashes, preferring requirements over constraints, to match pip. + // Merge the hashes, preferring requirements over constraints, since overlapping + // requirements were already merged. let hashes: FxHashMap> = constraint_hashes .into_iter() .chain(requirement_hashes) @@ -311,4 +332,6 @@ pub enum HashStrategyError { UnpinnedRequirement(String, HashCheckingMode), #[error("In `{1}` mode, all requirements must have a hash, but none were provided for: {0}")] MissingHashes(String, HashCheckingMode), + #[error("In `{1}` mode, all requirements must have a hash, but there were no overlapping hashes between the requirements and constraints for: {0}")] + NoIntersection(String, HashCheckingMode), } diff --git a/crates/uv/tests/pip_install.rs b/crates/uv/tests/pip_install.rs index df8c966f7bf9..43a5607b14e1 100644 --- a/crates/uv/tests/pip_install.rs +++ b/crates/uv/tests/pip_install.rs @@ -5199,8 +5199,7 @@ fn require_hashes_constraint() -> Result<()> { "### ); - // Include the wrong hash in the requirements file, but the right hash in constraints. This - // should fail. + // Include an empty intersection. This should fail. let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); @@ -5224,21 +5223,42 @@ fn require_hashes_constraint() -> Result<()> { ----- stdout ----- ----- stderr ----- - Resolved 1 package in [TIME] - error: Failed to prepare distributions - Caused by: Failed to fetch wheel: anyio==4.0.0 - Caused by: Hash mismatch for `anyio==4.0.0` + error: In `--require-hashes` mode, all requirements must have a hash, but there were no overlapping hashes between the requirements and constraints for: anyio==4.0.0 + "### + ); - Expected: - sha256:afdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f + // Include the right hash in both files. + let context = TestContext::new("3.12"); - Computed: - sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt.write_str( + "anyio==4.0.0 --hash=sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f", + )?; + + let constraints_txt = context.temp_dir.child("constraints.txt"); + constraints_txt.write_str("anyio==4.0.0 --hash=sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f")?; + + // Install the editable packages. + uv_snapshot!(context.pip_install() + .arg("-r") + .arg(requirements_txt.path()) + .arg("--no-deps") + .arg("--require-hashes") + .arg("-c") + .arg(constraints_txt.path()), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + anyio==4.0.0 "### ); - // Include the right hash in the requirements file, but the wrong hash in constraints. This - // should succeed. + // Include the right hash in both files, along with an irrelevant, wrong hash. let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); @@ -5247,7 +5267,7 @@ fn require_hashes_constraint() -> Result<()> { )?; let constraints_txt = context.temp_dir.child("constraints.txt"); - constraints_txt.write_str("anyio==4.0.0 --hash=sha256:afdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f")?; + constraints_txt.write_str("anyio==4.0.0 --hash=sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f --hash=sha256:afdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f")?; // Install the editable packages. uv_snapshot!(context.pip_install()