Skip to content

Commit cb65a03

Browse files
committed
fix #1652: Use GitHub API for standalone python checksums
Since the [20250708](https://github.com/astral-sh/python-build-standalone/releases/tag/20250708) release, the [python-build-standalone](https://github.com/astral-sh/python-build-standalone) project does not provide checksum files for individual artifacts, but rather a single `SHA256SUMS` manifest file containing checksums for all artifacts. However, since June 3, the GitHub API [directly provides checksums for release artifacts](https://github.blog/changelog/2025-06-03-releases-now-expose-digests-for-release-assets/). Because we always fetch the latest release and it's been over a month since the last cached index file, we can just save the checksums in out cached index of Python versions.
1 parent 849d3f4 commit cb65a03

File tree

2 files changed

+15
-16
lines changed

2 files changed

+15
-16
lines changed

changelog.d/1652.bugfix.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Set up standalone python fetching to use checksums directly from the GitHub API.

src/pipx/standalone_python.py

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import urllib.error
1111
from functools import partial
1212
from pathlib import Path
13-
from typing import Any, Dict, List
13+
from typing import Any
1414
from urllib.request import urlopen
1515

1616
from pipx import constants, paths
@@ -22,7 +22,7 @@
2222
# Much of the code in this module is adapted with extreme gratitude from
2323
# https://github.com/tusharsadhwani/yen/blob/main/src/yen/github.py
2424

25-
MACHINE_SUFFIX: Dict[str, Dict[str, Any]] = {
25+
MACHINE_SUFFIX: dict[str, dict[str, Any]] = {
2626
"Darwin": {
2727
"arm64": ["aarch64-apple-darwin-install_only.tar.gz"],
2828
"x86_64": ["x86_64-apple-darwin-install_only.tar.gz"],
@@ -71,7 +71,7 @@ def download_python_build_standalone(python_version: str, override: bool = False
7171
logger.warning(f"A previous attempt to install python {python_version} failed. Retrying.")
7272
shutil.rmtree(install_dir)
7373

74-
full_version, download_link = resolve_python_version(python_version)
74+
full_version, (download_link, digest) = resolve_python_version(python_version)
7575

7676
with tempfile.TemporaryDirectory() as tempdir:
7777
archive = Path(tempdir) / f"python-{full_version}.tar.gz"
@@ -81,7 +81,7 @@ def download_python_build_standalone(python_version: str, override: bool = False
8181
_download(full_version, download_link, archive)
8282

8383
# unpack the python build
84-
_unpack(full_version, download_link, archive, download_dir)
84+
_unpack(full_version, download_link, archive, download_dir, digest)
8585

8686
# the python installation we want is nested in the tarball
8787
# under a directory named 'python'. We move it to the install
@@ -104,15 +104,13 @@ def _download(full_version: str, download_link: str, archive: Path):
104104
raise PipxError(f"Unable to download python {full_version} build.") from e
105105

106106

107-
def _unpack(full_version, download_link, archive: Path, download_dir: Path):
107+
def _unpack(full_version, download_link, archive: Path, download_dir: Path, expected_checksum: str):
108108
with animate(f"Unpacking python {full_version} build", True):
109109
# Calculate checksum
110110
with open(archive, "rb") as python_zip:
111-
checksum = hashlib.sha256(python_zip.read()).hexdigest()
111+
checksum = "sha256:" + hashlib.sha256(python_zip.read()).hexdigest()
112112

113113
# Validate checksum
114-
checksum_link = download_link + ".sha256"
115-
expected_checksum = urlopen(checksum_link).read().decode().rstrip("\n")
116114
if checksum != expected_checksum:
117115
raise PipxError(
118116
f"Checksum mismatch for python {full_version} build. Expected {expected_checksum}, got {checksum}."
@@ -142,7 +140,7 @@ def get_or_update_index(use_cache: bool = True):
142140
return index
143141

144142

145-
def get_latest_python_releases() -> List[str]:
143+
def get_latest_python_releases() -> list[tuple[str, str | None]]:
146144
"""Returns the list of python download links from the latest github release."""
147145
try:
148146
with urlopen(GITHUB_API_URL) as response:
@@ -152,10 +150,10 @@ def get_latest_python_releases() -> List[str]:
152150
# raise
153151
raise PipxError(f"Unable to fetch python-build-standalone release data (from {GITHUB_API_URL}).") from e
154152

155-
return [asset["browser_download_url"] for asset in release_data["assets"]]
153+
return [(asset["browser_download_url"], asset.get("digest")) for asset in release_data["assets"]]
156154

157155

158-
def list_pythons(use_cache: bool = True) -> Dict[str, str]:
156+
def list_pythons(use_cache: bool = True) -> dict[str, tuple[str, str | None]]:
159157
"""Returns available python versions for your machine and their download links."""
160158
system, machine = platform.system(), platform.machine()
161159
download_link_suffixes = MACHINE_SUFFIX[system][machine]
@@ -168,23 +166,23 @@ def list_pythons(use_cache: bool = True) -> Dict[str, str]:
168166
python_releases = get_or_update_index(use_cache)["releases"]
169167

170168
available_python_links = [
171-
link
169+
(link, digest)
172170
# Suffixes are in order of preference.
173171
for download_link_suffix in download_link_suffixes
174-
for link in python_releases
172+
for link, digest in python_releases
175173
if link.endswith(download_link_suffix)
176174
]
177175

178-
python_versions: dict[str, str] = {}
179-
for link in available_python_links:
176+
python_versions: dict[str, tuple[str, str | None]] = {}
177+
for link, digest in available_python_links:
180178
match = PYTHON_VERSION_REGEX.search(link)
181179
assert match is not None
182180
python_version = match[1]
183181
# Don't override already found versions, they are in order of preference
184182
if python_version in python_versions:
185183
continue
186184

187-
python_versions[python_version] = link
185+
python_versions[python_version] = link, digest
188186

189187
return {
190188
version: python_versions[version]

0 commit comments

Comments
 (0)