fetch_strategy: show the effective URL on checksum validation failure (#46349)
This commit is contained in:
parent
bcde9a3afb
commit
b4e32706db
@ -24,6 +24,7 @@
|
|||||||
"""
|
"""
|
||||||
import copy
|
import copy
|
||||||
import functools
|
import functools
|
||||||
|
import http.client
|
||||||
import os
|
import os
|
||||||
import os.path
|
import os.path
|
||||||
import re
|
import re
|
||||||
@ -59,19 +60,6 @@
|
|||||||
#: List of all fetch strategies, created by FetchStrategy metaclass.
|
#: List of all fetch strategies, created by FetchStrategy metaclass.
|
||||||
all_strategies = []
|
all_strategies = []
|
||||||
|
|
||||||
CONTENT_TYPE_MISMATCH_WARNING_TEMPLATE = (
|
|
||||||
"The contents of {subject} look like {content_type}. Either the URL"
|
|
||||||
" you are trying to use does not exist or you have an internet gateway"
|
|
||||||
" issue. You can remove the bad archive using 'spack clean"
|
|
||||||
" <package>', then try again using the correct URL."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def warn_content_type_mismatch(subject, content_type="HTML"):
|
|
||||||
tty.warn(
|
|
||||||
CONTENT_TYPE_MISMATCH_WARNING_TEMPLATE.format(subject=subject, content_type=content_type)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _needs_stage(fun):
|
def _needs_stage(fun):
|
||||||
"""Many methods on fetch strategies require a stage to be set
|
"""Many methods on fetch strategies require a stage to be set
|
||||||
@ -265,6 +253,7 @@ def __init__(self, *, url: str, checksum: Optional[str] = None, **kwargs) -> Non
|
|||||||
self.extra_options: dict = kwargs.get("fetch_options", {})
|
self.extra_options: dict = kwargs.get("fetch_options", {})
|
||||||
self._curl: Optional[Executable] = None
|
self._curl: Optional[Executable] = None
|
||||||
self.extension: Optional[str] = kwargs.get("extension", None)
|
self.extension: Optional[str] = kwargs.get("extension", None)
|
||||||
|
self._effective_url: Optional[str] = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def curl(self) -> Executable:
|
def curl(self) -> Executable:
|
||||||
@ -320,7 +309,13 @@ def _check_headers(self, headers):
|
|||||||
# redirects properly.
|
# redirects properly.
|
||||||
content_types = re.findall(r"Content-Type:[^\r\n]+", headers, flags=re.IGNORECASE)
|
content_types = re.findall(r"Content-Type:[^\r\n]+", headers, flags=re.IGNORECASE)
|
||||||
if content_types and "text/html" in content_types[-1]:
|
if content_types and "text/html" in content_types[-1]:
|
||||||
warn_content_type_mismatch(self.archive_file or "the archive")
|
msg = (
|
||||||
|
f"The contents of {self.archive_file or 'the archive'} fetched from {self.url} "
|
||||||
|
" looks like HTML. This can indicate a broken URL, or an internet gateway issue."
|
||||||
|
)
|
||||||
|
if self._effective_url != self.url:
|
||||||
|
msg += f" The URL redirected to {self._effective_url}."
|
||||||
|
tty.warn(msg)
|
||||||
|
|
||||||
@_needs_stage
|
@_needs_stage
|
||||||
def _fetch_urllib(self, url):
|
def _fetch_urllib(self, url):
|
||||||
@ -346,6 +341,12 @@ def _fetch_urllib(self, url):
|
|||||||
with open(save_file, "wb") as f:
|
with open(save_file, "wb") as f:
|
||||||
shutil.copyfileobj(response, f)
|
shutil.copyfileobj(response, f)
|
||||||
|
|
||||||
|
# Save the redirected URL for error messages. Sometimes we're redirected to an arbitrary
|
||||||
|
# mirror that is broken, leading to spurious download failures. In that case it's helpful
|
||||||
|
# for users to know which URL was actually fetched.
|
||||||
|
if isinstance(response, http.client.HTTPResponse):
|
||||||
|
self._effective_url = response.geturl()
|
||||||
|
|
||||||
self._check_headers(str(response.headers))
|
self._check_headers(str(response.headers))
|
||||||
|
|
||||||
@_needs_stage
|
@_needs_stage
|
||||||
@ -465,7 +466,7 @@ def check(self):
|
|||||||
if not self.digest:
|
if not self.digest:
|
||||||
raise NoDigestError(f"Attempt to check {self.__class__.__name__} with no digest.")
|
raise NoDigestError(f"Attempt to check {self.__class__.__name__} with no digest.")
|
||||||
|
|
||||||
verify_checksum(self.archive_file, self.digest)
|
verify_checksum(self.archive_file, self.digest, self.url, self._effective_url)
|
||||||
|
|
||||||
@_needs_stage
|
@_needs_stage
|
||||||
def reset(self):
|
def reset(self):
|
||||||
@ -1433,21 +1434,26 @@ def expand(self):
|
|||||||
if len(files) != 1:
|
if len(files) != 1:
|
||||||
raise ChecksumError(self, f"Expected a single file in {src_dir}.")
|
raise ChecksumError(self, f"Expected a single file in {src_dir}.")
|
||||||
|
|
||||||
verify_checksum(os.path.join(src_dir, files[0]), self.expanded_sha256)
|
verify_checksum(
|
||||||
|
os.path.join(src_dir, files[0]), self.expanded_sha256, self.url, self._effective_url
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def verify_checksum(file, digest):
|
def verify_checksum(file: str, digest: str, url: str, effective_url: Optional[str]) -> None:
|
||||||
checker = crypto.Checker(digest)
|
checker = crypto.Checker(digest)
|
||||||
if not checker.check(file):
|
if not checker.check(file):
|
||||||
# On failure, provide some information about the file size and
|
# On failure, provide some information about the file size and
|
||||||
# contents, so that we can quickly see what the issue is (redirect
|
# contents, so that we can quickly see what the issue is (redirect
|
||||||
# was not followed, empty file, text instead of binary, ...)
|
# was not followed, empty file, text instead of binary, ...)
|
||||||
size, contents = fs.filesummary(file)
|
size, contents = fs.filesummary(file)
|
||||||
raise ChecksumError(
|
long_msg = (
|
||||||
f"{checker.hash_name} checksum failed for {file}",
|
|
||||||
f"Expected {digest} but got {checker.sum}. "
|
f"Expected {digest} but got {checker.sum}. "
|
||||||
f"File size = {size} bytes. Contents = {contents!r}",
|
f"File size = {size} bytes. Contents = {contents!r}. "
|
||||||
|
f"URL = {url}"
|
||||||
)
|
)
|
||||||
|
if effective_url and effective_url != url:
|
||||||
|
long_msg += f", redirected to = {effective_url}"
|
||||||
|
raise ChecksumError(f"{checker.hash_name} checksum failed for {file}", long_msg)
|
||||||
|
|
||||||
|
|
||||||
def stable_target(fetcher):
|
def stable_target(fetcher):
|
||||||
|
Loading…
Reference in New Issue
Block a user