summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorStéphane Bidoul <stephane.bidoul@gmail.com>2023-03-25 14:18:44 +0100
committerStéphane Bidoul <stephane.bidoul@gmail.com>2023-04-14 08:03:48 +0200
commit40cd79d6e54d53bccfcad0b855cd87116de896b9 (patch)
tree585aabdf24e238a91bbc295aea1f977dd00ecabf
parent8e2205d8495474df088d773b4658aa4a40aefcac (diff)
downloadpip-40cd79d6e54d53bccfcad0b855cd87116de896b9.tar.gz
Check hashes of cached built wheels agains origin source archive
-rw-r--r--news/5037.feature.rst1
-rw-r--r--src/pip/_internal/operations/prepare.py34
-rw-r--r--src/pip/_internal/resolution/resolvelib/factory.py2
-rw-r--r--tests/functional/test_install.py38
4 files changed, 73 insertions, 2 deletions
diff --git a/news/5037.feature.rst b/news/5037.feature.rst
new file mode 100644
index 000000000..b0a25aaee
--- /dev/null
+++ b/news/5037.feature.rst
@@ -0,0 +1 @@
+Support wheel cache when using --require-hashes.
diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py
index dda92d29b..6fa6cf5b4 100644
--- a/src/pip/_internal/operations/prepare.py
+++ b/src/pip/_internal/operations/prepare.py
@@ -536,9 +536,41 @@ class RequirementPreparer:
assert req.link
link = req.link
- self._ensure_link_req_src_dir(req, parallel_builds)
hashes = self._get_linked_req_hashes(req)
+ if (
+ hashes
+ and link.is_wheel
+ and link.is_file
+ and req.original_link_is_in_wheel_cache
+ ):
+ assert req.download_info is not None
+ # We need to verify hashes, and we have found the requirement in the cache
+ # of locally built wheels.
+ if (
+ isinstance(req.download_info.info, ArchiveInfo)
+ and req.download_info.info.hashes
+ and hashes.has_one_of(req.download_info.info.hashes)
+ ):
+ # At this point we know the requirement was built from a hashable source
+ # artifact, and we verified that the cache entry's hash of the original
+ # artifact matches one of the hashes we expect. We don't verify hashes
+ # against the cached wheel, because the wheel is not the original.
+ hashes = None
+ else:
+ logger.warning(
+ "The hashes of the source archive found in cache entry "
+ "don't match, ignoring cached built wheel "
+ "and re-downloading source."
+ )
+ # For some reason req.original_link is not set here, even though
+ # req.original_link_is_in_wheel_cache is True. So we get the original
+ # link from download_info.
+ req.link = Link(req.download_info.url) # TODO comes_from?
+ link = req.link
+
+ self._ensure_link_req_src_dir(req, parallel_builds)
+
if link.is_existing_dir():
local_file = None
elif link.url not in self._downloaded:
diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py
index 0ad4641b1..0331297b8 100644
--- a/src/pip/_internal/resolution/resolvelib/factory.py
+++ b/src/pip/_internal/resolution/resolvelib/factory.py
@@ -535,7 +535,7 @@ class Factory:
hash mismatches. Furthermore, cached wheels at present have
nondeterministic contents due to file modification times.
"""
- if self._wheel_cache is None or self.preparer.require_hashes:
+ if self._wheel_cache is None:
return None
return self._wheel_cache.get_cache_entry(
link=link,
diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py
index e50779688..bc974d1a8 100644
--- a/tests/functional/test_install.py
+++ b/tests/functional/test_install.py
@@ -729,6 +729,44 @@ def test_bad_link_hash_in_dep_install_failure(
assert "THESE PACKAGES DO NOT MATCH THE HASHES" in result.stderr, result.stderr
+def test_hashed_install_from_cache(
+ script: PipTestEnvironment, data: TestData, tmpdir: Path
+) -> None:
+ """
+ Test that installing from a cached built wheel works and that the hash is verified
+ against the hash of the original source archived stored in the cache entry.
+ """
+ with requirements_file(
+ "simple2==1.0 --hash=sha256:"
+ "9336af72ca661e6336eb87bc7de3e8844d853e3848c2b9bbd2e8bf01db88c2c7\n",
+ tmpdir,
+ ) as reqs_file:
+ result = script.pip_install_local(
+ "--use-pep517", "--no-build-isolation", "-r", reqs_file.resolve()
+ )
+ assert "Created wheel for simple2" in result.stdout
+ script.pip("uninstall", "simple2", "-y")
+ result = script.pip_install_local(
+ "--use-pep517", "--no-build-isolation", "-r", reqs_file.resolve()
+ )
+ assert "Using cached simple2" in result.stdout
+ # now try with an invalid hash
+ with requirements_file(
+ "simple2==1.0 --hash=sha256:invalid\n",
+ tmpdir,
+ ) as reqs_file:
+ script.pip("uninstall", "simple2", "-y")
+ result = script.pip_install_local(
+ "--use-pep517",
+ "--no-build-isolation",
+ "-r",
+ reqs_file.resolve(),
+ expect_error=True,
+ )
+ assert "Using cached simple2" in result.stdout
+ assert "ERROR: THESE PACKAGES DO NOT MATCH THE HASHES" in result.stderr
+
+
def assert_re_match(pattern: str, text: str) -> None:
assert re.search(pattern, text), f"Could not find {pattern!r} in {text!r}"