Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Take intersection of constraint and requirements hashes #7108

Merged
merged 1 commit into from
Sep 6, 2024
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
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
Loading