diff --git a/bump_packaging.py b/bump_packaging.py index ce6890e..8d1e0c6 100644 --- a/bump_packaging.py +++ b/bump_packaging.py @@ -33,11 +33,14 @@ from __future__ import annotations import argparse import hashlib +import os import re +import ssl import subprocess import sys import tarfile import tempfile +import urllib.error import urllib.request from pathlib import Path @@ -193,6 +196,30 @@ def sha256(path: Path) -> str: return digest.hexdigest() +def ca_file() -> str | None: + """The CA bundle to verify with, or None to let OpenSSL use its own default. + + `uv python install` fetches a python-build-standalone interpreter whose + OpenSSL is compiled with default trust paths that need not exist on the + host (on macOS, and on distros that keep certs only in + /etc/ssl/certs/ca-certificates.crt, they often do not). Verification then + fails with `unable to get local issuer certificate` for every site, even + though curl and browsers are happy. Fall back to certifi, which `httpx` + already puts in the environment. + """ + if os.environ.get("SSL_CERT_FILE"): + return None # ssl honours the variable itself; do not second-guess it + paths = ssl.get_default_verify_paths() + for candidate in (paths.cafile, paths.openssl_cafile): + if candidate and Path(candidate).is_file(): + return None + try: + import certifi + except ModuleNotFoundError: + return None + return certifi.where() + + def fetch_asset(version: str, override: Path | None) -> Path: if override is not None: if not override.is_file(): @@ -200,10 +227,25 @@ def fetch_asset(version: str, override: Path | None) -> Path: return override url = RELEASE_ASSET.format(version=version, name=ASSET_NAME) say(f"downloading {url}") + cafile = ca_file() + context = ssl.create_default_context(cafile=cafile) target = Path(tempfile.mkdtemp()) / f"{ASSET_NAME}-{version}.tar.gz" try: - with urllib.request.urlopen(url) as response, target.open("wb") as handle: + with urllib.request.urlopen(url, context=context) as response, target.open( + "wb" + ) as handle: handle.write(response.read()) + except urllib.error.URLError as error: + # urlopen wraps socket/SSL failures; the cause is in `.reason`. + if isinstance(error.reason, ssl.SSLCertVerificationError): + bundle = cafile or ssl.get_default_verify_paths().cafile or "no bundle found" + raise fail( + f"cannot fetch the release asset ({error.reason.verify_message or error.reason}); " + f"verified against {bundle}. If that bundle is stale, point SSL_CERT_FILE " + "at a current one (on Arch and Debian that is " + "/etc/ssl/certs/ca-certificates.crt)" + ) from error + raise fail(f"cannot fetch the release asset ({error.reason}); is the tag published?") from error except OSError as error: raise fail(f"cannot fetch the release asset ({error}); is the tag published?") from error return target