Skip to content

Commit 85de6ac

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 33f37fc commit 85de6ac

File tree

2 files changed

+10
-11
lines changed

2 files changed

+10
-11
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: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -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}."
@@ -152,7 +150,7 @@ 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

158156
def list_pythons(use_cache: bool = True) -> Dict[str, str]:
@@ -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

178176
python_versions: dict[str, str] = {}
179-
for link in available_python_links:
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)