Skip to content

Commit 9abf6b0

Browse files
committed
Check hashes of cached built wheels agains origin source archive
1 parent b1b885e commit 9abf6b0

File tree

4 files changed

+72
-2
lines changed

4 files changed

+72
-2
lines changed

news/5037.feature.rst

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Support wheel cache when using --require-hashes.

src/pip/_internal/operations/prepare.py

+31-1
Original file line numberDiff line numberDiff line change
@@ -536,9 +536,39 @@ def _prepare_linked_requirement(
536536
assert req.link
537537
link = req.link
538538

539-
self._ensure_link_req_src_dir(req, parallel_builds)
540539
hashes = self._get_linked_req_hashes(req)
541540

541+
if (
542+
hashes
543+
and link.is_wheel
544+
and link.is_file
545+
and req.original_link_is_in_wheel_cache
546+
and isinstance(req.download_info.info, ArchiveInfo)
547+
):
548+
# We need to verify hashes, and we have found the requirement in the cache
549+
# of locally built wheels, and that requirement was built from a hashable
550+
# source artifact.
551+
if req.download_info.info.hashes and hashes.has_one_of(
552+
req.download_info.info.hashes
553+
):
554+
# At this point we verified that the cache entry's hash of the original
555+
# artifact matches one of the hashes we expect. We don't verify hashes
556+
# against the cached wheel, because the wheel is not the original.
557+
hashes = None
558+
else:
559+
logger.warning(
560+
"The hashes of the source archive found in cache entry "
561+
"don't match, ignoring cached built wheel "
562+
"and re-downloading source."
563+
)
564+
# For some reason req.original_link is not set here, even though
565+
# req.original_link_is_in_wheel_cache is True. So we get the original
566+
# link from download_info.
567+
req.link = Link(req.download_info.url) # TODO comes_from?
568+
link = req.link
569+
570+
self._ensure_link_req_src_dir(req, parallel_builds)
571+
542572
if link.is_existing_dir():
543573
local_file = None
544574
elif link.url not in self._downloaded:

src/pip/_internal/resolution/resolvelib/factory.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -535,7 +535,7 @@ def get_wheel_cache_entry(
535535
hash mismatches. Furthermore, cached wheels at present have
536536
nondeterministic contents due to file modification times.
537537
"""
538-
if self._wheel_cache is None or self.preparer.require_hashes:
538+
if self._wheel_cache is None:
539539
return None
540540
return self._wheel_cache.get_cache_entry(
541541
link=link,

tests/functional/test_install.py

+39
Original file line numberDiff line numberDiff line change
@@ -622,6 +622,45 @@ def test_hashed_install_failure(script: PipTestEnvironment, tmpdir: Path) -> Non
622622
assert len(result.files_created) == 0
623623

624624

625+
@pytest.mark.usefixtures("with_wheel")
626+
def test_hashed_install_from_cache(
627+
script: PipTestEnvironment, data: TestData, tmpdir: Path
628+
) -> None:
629+
"""
630+
Test that installing from a cached built wheel works and that the hash is verified
631+
against the hash of the original source archived stored in the cache entry.
632+
"""
633+
with requirements_file(
634+
"simple2==1.0 --hash=sha256:"
635+
"9336af72ca661e6336eb87bc7de3e8844d853e3848c2b9bbd2e8bf01db88c2c7\n",
636+
tmpdir,
637+
) as reqs_file:
638+
result = script.pip_install_local(
639+
"--use-pep517", "--no-build-isolation", "-r", reqs_file.resolve()
640+
)
641+
assert "Created wheel for simple2" in result.stdout
642+
script.pip("uninstall", "simple2", "-y")
643+
result = script.pip_install_local(
644+
"--use-pep517", "--no-build-isolation", "-r", reqs_file.resolve()
645+
)
646+
assert "Using cached simple2" in result.stdout
647+
# now try with an invalid hash
648+
with requirements_file(
649+
"simple2==1.0 --hash=sha256:invalid\n",
650+
tmpdir,
651+
) as reqs_file:
652+
script.pip("uninstall", "simple2", "-y")
653+
result = script.pip_install_local(
654+
"--use-pep517",
655+
"--no-build-isolation",
656+
"-r",
657+
reqs_file.resolve(),
658+
expect_error=True,
659+
)
660+
assert "Using cached simple2" in result.stdout
661+
assert "ERROR: THESE PACKAGES DO NOT MATCH THE HASHES" in result.stderr
662+
663+
625664
def assert_re_match(pattern: str, text: str) -> None:
626665
assert re.search(pattern, text), f"Could not find {pattern!r} in {text!r}"
627666

0 commit comments

Comments
 (0)