Skip to content

Commit

Permalink
Take intersection of constraint and requirements hashes (#7108)
Browse files Browse the repository at this point in the history
## Summary

Small follow-up to #7093.
  • Loading branch information
charliermarsh authored Sep 6, 2024
1 parent 84f25e8 commit c494f69
Show file tree
Hide file tree
Showing 2 changed files with 62 additions and 19 deletions.
35 changes: 29 additions & 6 deletions crates/uv-types/src/hash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -219,23 +219,44 @@ impl HashStrategy {
.collect::<Result<Vec<_>, _>>()?
};

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

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<VersionId, Vec<HashDigest>> = constraint_hashes
.into_iter()
.chain(requirement_hashes)
Expand Down Expand Up @@ -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),
}
46 changes: 33 additions & 13 deletions crates/uv/tests/pip_install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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");
Expand All @@ -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()
Expand Down

0 comments on commit c494f69

Please sign in to comment.