feat: add SHA256 verification for direct downloads (#2873)
* feat: add SHA256 verification for direct downloads
Store SHA256 hashes when running update scripts and verify them
when downloading files at build time. This improves security by
detecting unexpected changes to downloaded artifacts.
Platforms covered: macOS (CPython, PyPy, GraalPy), iOS, Android,
virtualenv, and python-build-standalone. Windows (nuget) and
Linux (Docker) are excluded.
SHA256 sources per platform:
- macOS/iOS/Android CPython (python.org): sha256_sum from API
- GraalPy: .sha256 sidecar assets from GitHub releases
- python-build-standalone: SHA256SUMS file in release
- PyPy, BeeWare iOS, Maven (Chaquopy): stream-download and compute
Changes:
- cibuildwheel/util/file.py: add sha256 param to download()
- cibuildwheel/platforms/{macos,ios,android}.py: add sha256 to
PythonConfiguration and pass to download()
- cibuildwheel/venv.py: read sha256 from toml and pass to download()
- cibuildwheel/util/python_build_standalone.py: add sha256 to
PythonBuildStandaloneAsset and pass to download()
- cibuildwheel/resources/build-platforms.toml: add sha256 fields
- cibuildwheel/resources/virtualenv.toml: add sha256 field
- cibuildwheel/resources/python-build-standalone-releases.json: add sha256
- bin/update_pythons.py: compute/store sha256 per source strategy
- bin/update_virtualenv.py: compute sha256 by streaming download
- bin/update_python_build_standalone.py: parse SHA256SUMS file
Closes #908
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Assisted-by: copilot-cli:claude-sonnet-4.6
* fix: populate sha256 in resource files and fix Windows PythonConfiguration
- Add sha256 field to Windows PythonConfiguration (PyPy/GraalPy have
direct download URLs on Windows too)
- Pass sha256 to install_pypy() and install_graalpy() in windows.py
- Fix update_pythons.py: handle empty sha256 from CPython API (older
versions) by streaming download to compute it; fix condition to
check 'not sha256' rather than 'not in dict'
- Fix update_virtualenv.py: compute sha256 even when version unchanged
but sha256 is empty (first-time population)
- Fix update_python_build_standalone.py: resolve file path relative to
the script itself (not the installed package) so writes go to source
checkout, not the uv cache
- Populate actual sha256 values by running all three update scripts
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Assisted-by: copilot-cli:claude-sonnet-4.6
* fix: also include pyodide
Assisted-by: CopilotCLI:gpt-5.3-codex
Signed-off-by: Henry Schreiner <henryfs@princeton.edu>
* fix: PR review comments for cache verification and docs wording
Co-authored-by: henryiii <4616906+henryiii@users.noreply.github.com>
Signed-off-by: Henry Schreiner <henryfs@princeton.edu>
* fix: require sha256 for download configs
Require sha256 for URL-backed Python and virtualenv download configs. Update the GraalPy updater to refresh macOS x86_64 entries by selecting the latest release that still has a matching asset, and fill the two missing GraalPy checksums in build-platforms.toml.
Assisted-by: CopilotCLI:gpt-5.4
* ci: remove unit test for bin item
Signed-off-by: Henry Schreiner <henryfs@princeton.edu>
* refactor: combine sha256 unit tests into test_sha256.py
Merge pyodide_test.py and python_build_standalone_test.py into a
single unit_test/test_sha256.py since both test sha256-related
behaviour.
Assisted-by: opencode:glm-5
---------
Signed-off-by: Henry Schreiner <henryfs@princeton.edu>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: henryiii <4616906+henryiii@users.noreply.github.com>
This commit is contained in:
co-authored by
Copilot
copilot-swe-agent[bot]
henryiii
parent
322b876891
commit
cd38ee1548
@@ -3,6 +3,7 @@
|
|||||||
# /// script
|
# /// script
|
||||||
# dependencies = [
|
# dependencies = [
|
||||||
# "cibuildwheel",
|
# "cibuildwheel",
|
||||||
|
# "requests",
|
||||||
# ]
|
# ]
|
||||||
#
|
#
|
||||||
# [tool.uv.sources]
|
# [tool.uv.sources]
|
||||||
@@ -10,13 +11,23 @@
|
|||||||
# ///
|
# ///
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Final
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
from cibuildwheel.extra import github_api_request
|
from cibuildwheel.extra import github_api_request
|
||||||
from cibuildwheel.util.python_build_standalone import (
|
from cibuildwheel.util.python_build_standalone import (
|
||||||
PythonBuildStandaloneAsset,
|
PythonBuildStandaloneAsset,
|
||||||
PythonBuildStandaloneReleaseData,
|
PythonBuildStandaloneReleaseData,
|
||||||
)
|
)
|
||||||
from cibuildwheel.util.resources import PYTHON_BUILD_STANDALONE_RELEASES
|
|
||||||
|
# Resolve path relative to this script so writes go to the source checkout,
|
||||||
|
# not the uv-installed copy of the package.
|
||||||
|
DIR: Final[Path] = Path(__file__).parent.parent.resolve()
|
||||||
|
PYTHON_BUILD_STANDALONE_RELEASES: Final[Path] = (
|
||||||
|
DIR / "cibuildwheel/resources/python-build-standalone-releases.json"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
@@ -34,8 +45,27 @@ def main() -> None:
|
|||||||
f"repos/astral-sh/python-build-standalone/releases/tags/{latest_tag}"
|
f"repos/astral-sh/python-build-standalone/releases/tags/{latest_tag}"
|
||||||
)["assets"]
|
)["assets"]
|
||||||
|
|
||||||
|
# Build a sha256 map from the SHA256SUMS file in the release
|
||||||
|
sha256_sums_urls = [
|
||||||
|
ga["browser_download_url"] for ga in github_assets if ga["name"] == "SHA256SUMS"
|
||||||
|
]
|
||||||
|
name_to_sha256: dict[str, str] = {}
|
||||||
|
if sha256_sums_urls:
|
||||||
|
response = requests.get(sha256_sums_urls[0])
|
||||||
|
response.raise_for_status()
|
||||||
|
for line in response.text.splitlines():
|
||||||
|
parts = line.split()
|
||||||
|
if len(parts) == 2:
|
||||||
|
sha256_hex, filename = parts
|
||||||
|
# The filename may have a leading "./" or spaces - strip it
|
||||||
|
name_to_sha256[filename.lstrip("./")] = sha256_hex
|
||||||
|
|
||||||
assets = [
|
assets = [
|
||||||
PythonBuildStandaloneAsset(name=ga["name"], url=ga["browser_download_url"])
|
PythonBuildStandaloneAsset(
|
||||||
|
name=ga["name"],
|
||||||
|
url=ga["browser_download_url"],
|
||||||
|
sha256=name_to_sha256.get(ga["name"], ""),
|
||||||
|
)
|
||||||
for ga in github_assets
|
for ga in github_assets
|
||||||
if ga["name"].endswith("install_only.tar.gz")
|
if ga["name"].endswith("install_only.tar.gz")
|
||||||
]
|
]
|
||||||
|
|||||||
+62
-17
@@ -15,12 +15,13 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import difflib
|
import difflib
|
||||||
|
import hashlib
|
||||||
import logging
|
import logging
|
||||||
import operator
|
import operator
|
||||||
import re
|
import re
|
||||||
import tomllib
|
import tomllib
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Final, Literal, TypedDict
|
from typing import Any, Final, Literal, NotRequired, TypedDict
|
||||||
from xml.etree import ElementTree as ET
|
from xml.etree import ElementTree as ET
|
||||||
|
|
||||||
import click
|
import click
|
||||||
@@ -56,11 +57,13 @@ class Config(TypedDict):
|
|||||||
|
|
||||||
class ConfigUrl(Config):
|
class ConfigUrl(Config):
|
||||||
url: str
|
url: str
|
||||||
|
sha256: NotRequired[str]
|
||||||
|
|
||||||
|
|
||||||
class ConfigPyodide(Config):
|
class ConfigPyodide(Config):
|
||||||
default_pyodide_version: str
|
default_pyodide_version: str
|
||||||
node_version: str
|
node_version: str
|
||||||
|
sha256: str
|
||||||
|
|
||||||
|
|
||||||
# The following set of "Versions" classes allow the initial call to the APIs to
|
# The following set of "Versions" classes allow the initial call to the APIs to
|
||||||
@@ -155,10 +158,6 @@ class GraalPyVersions:
|
|||||||
msg = f"GraalPy {arch} not found for {spec}!"
|
msg = f"GraalPy {arch} not found for {spec}!"
|
||||||
raise RuntimeError(msg)
|
raise RuntimeError(msg)
|
||||||
|
|
||||||
release = releases[-1]
|
|
||||||
version = release["python_version"]
|
|
||||||
gpversion = release["graalpy_version"]
|
|
||||||
|
|
||||||
if "macosx" in identifier:
|
if "macosx" in identifier:
|
||||||
arch = "x86_64" if "x86_64" in identifier else "arm64"
|
arch = "x86_64" if "x86_64" in identifier else "arm64"
|
||||||
platform = "macos"
|
platform = "macos"
|
||||||
@@ -171,19 +170,38 @@ class GraalPyVersions:
|
|||||||
|
|
||||||
arch = "amd64" if arch == "x86_64" else "aarch64"
|
arch = "amd64" if arch == "x86_64" else "aarch64"
|
||||||
ext = "zip" if "win" in identifier else "tar.gz"
|
ext = "zip" if "win" in identifier else "tar.gz"
|
||||||
urls = [
|
for release in reversed(releases):
|
||||||
rf["browser_download_url"]
|
version = release["python_version"]
|
||||||
for rf in release["assets"]
|
gpversion = release["graalpy_version"]
|
||||||
if rf["name"].endswith(f"{platform}-{arch}.{ext}")
|
urls = [
|
||||||
and rf["name"].startswith(f"graalpy-{gpversion.major}")
|
rf["browser_download_url"]
|
||||||
]
|
for rf in release["assets"]
|
||||||
if urls:
|
if rf["name"].endswith(f"{platform}-{arch}.{ext}")
|
||||||
|
and rf["name"].startswith(f"graalpy-{gpversion.major}")
|
||||||
|
]
|
||||||
|
if not urls:
|
||||||
|
continue
|
||||||
|
|
||||||
(url,) = urls
|
(url,) = urls
|
||||||
|
# Fetch sha256 from the ".sha256" sidecar asset in the same release.
|
||||||
|
sha256 = ""
|
||||||
|
sha256_asset_name = url.rsplit("/", 1)[-1] + ".sha256"
|
||||||
|
sha256_urls = [
|
||||||
|
rf["browser_download_url"]
|
||||||
|
for rf in release["assets"]
|
||||||
|
if rf["name"] == sha256_asset_name
|
||||||
|
]
|
||||||
|
if sha256_urls:
|
||||||
|
sha256_response = requests.get(sha256_urls[0])
|
||||||
|
sha256_response.raise_for_status()
|
||||||
|
sha256 = sha256_response.text.strip().split()[0]
|
||||||
return ConfigUrl(
|
return ConfigUrl(
|
||||||
identifier=identifier,
|
identifier=identifier,
|
||||||
version=f"{version.major}.{version.minor}",
|
version=f"{version.major}.{version.minor}",
|
||||||
url=url,
|
url=url,
|
||||||
|
sha256=sha256,
|
||||||
)
|
)
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -298,12 +316,14 @@ class CPythonVersions:
|
|||||||
uri = self.versions_dict[new_version]
|
uri = self.versions_dict[new_version]
|
||||||
files = [rf for rf in self.files_info if rf["release"] == uri]
|
files = [rf for rf in self.files_info if rf["release"] == uri]
|
||||||
|
|
||||||
urls = [rf["url"] for rf in files if file_ident in rf["url"]]
|
matching = [rf for rf in files if file_ident in rf["url"]]
|
||||||
if urls:
|
if matching:
|
||||||
|
rf = matching[0]
|
||||||
return ConfigUrl(
|
return ConfigUrl(
|
||||||
identifier=identifier,
|
identifier=identifier,
|
||||||
version=f"{new_version.major}.{new_version.minor}",
|
version=f"{new_version.major}.{new_version.minor}",
|
||||||
url=urls[0],
|
url=rf["url"],
|
||||||
|
sha256=rf.get("sha256_sum", ""),
|
||||||
)
|
)
|
||||||
|
|
||||||
return None
|
return None
|
||||||
@@ -420,6 +440,7 @@ class PyodideVersions:
|
|||||||
version=str(version),
|
version=str(version),
|
||||||
default_pyodide_version=release["version"],
|
default_pyodide_version=release["version"],
|
||||||
node_version=node_version,
|
node_version=node_version,
|
||||||
|
sha256=release["sha256"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -448,6 +469,16 @@ class AllVersions:
|
|||||||
|
|
||||||
self.pyodide = PyodideVersions()
|
self.pyodide = PyodideVersions()
|
||||||
|
|
||||||
|
def _stream_sha256(self, url: str) -> str:
|
||||||
|
"""Download a file (streaming) and return its SHA256 hex digest."""
|
||||||
|
log.debug("Computing sha256 for %s by streaming download...", url)
|
||||||
|
response = requests.get(url, stream=True)
|
||||||
|
response.raise_for_status()
|
||||||
|
hasher = hashlib.sha256()
|
||||||
|
for chunk in response.iter_content(65536):
|
||||||
|
hasher.update(chunk)
|
||||||
|
return hasher.hexdigest()
|
||||||
|
|
||||||
def update_config(self, config: MutableMapping[str, str]) -> None:
|
def update_config(self, config: MutableMapping[str, str]) -> None:
|
||||||
identifier = config["identifier"]
|
identifier = config["identifier"]
|
||||||
version = Version(config["version"])
|
version = Version(config["version"])
|
||||||
@@ -465,8 +496,6 @@ class AllVersions:
|
|||||||
elif "macosx_arm64" in identifier:
|
elif "macosx_arm64" in identifier:
|
||||||
config_update = self.macos_pypy_arm64.update_version_macos(spec)
|
config_update = self.macos_pypy_arm64.update_version_macos(spec)
|
||||||
elif identifier.startswith("gp"):
|
elif identifier.startswith("gp"):
|
||||||
if "macosx_x86_64" in identifier:
|
|
||||||
return
|
|
||||||
config_update = self.graalpy.update_version(identifier, spec)
|
config_update = self.graalpy.update_version(identifier, spec)
|
||||||
elif "t-win32" in identifier and identifier.startswith("cp"):
|
elif "t-win32" in identifier and identifier.startswith("cp"):
|
||||||
config_update = self.windows_t_32.update_version_windows(spec)
|
config_update = self.windows_t_32.update_version_windows(spec)
|
||||||
@@ -505,6 +534,22 @@ class AllVersions:
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert config_update is not None, f"{identifier} not found!"
|
assert config_update is not None, f"{identifier} not found!"
|
||||||
|
|
||||||
|
# Fill in sha256 for URL-based configs if not already provided by the
|
||||||
|
# update_version_* method (e.g. PyPy, BeeWare iOS, Maven have no sidecar).
|
||||||
|
# Also fills in sha256 when the CPython API doesn't return a sha256_sum
|
||||||
|
# (e.g. for older releases).
|
||||||
|
# Widen the type to allow arbitrary key access on the underlying dict.
|
||||||
|
config_update_dict: dict[str, str] = config_update # type: ignore[assignment]
|
||||||
|
if "url" in config_update_dict and not config_update_dict.get("sha256"):
|
||||||
|
url = config_update_dict["url"]
|
||||||
|
existing_sha256 = config.get("sha256", "")
|
||||||
|
if url == config.get("url") and existing_sha256:
|
||||||
|
# URL unchanged — preserve the existing sha256
|
||||||
|
config_update_dict["sha256"] = existing_sha256
|
||||||
|
else:
|
||||||
|
config_update_dict["sha256"] = self._stream_sha256(url)
|
||||||
|
|
||||||
if config_update != config:
|
if config_update != config:
|
||||||
log.info(" Updated %s to %s", config, config_update)
|
log.info(" Updated %s to %s", config, config_update)
|
||||||
config.clear()
|
config.clear()
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
# dependencies = [
|
# dependencies = [
|
||||||
# "click",
|
# "click",
|
||||||
# "packaging",
|
# "packaging",
|
||||||
|
# "requests",
|
||||||
# "rich",
|
# "rich",
|
||||||
# "cibuildwheel",
|
# "cibuildwheel",
|
||||||
# ]
|
# ]
|
||||||
@@ -15,12 +16,14 @@
|
|||||||
|
|
||||||
import dataclasses
|
import dataclasses
|
||||||
import difflib
|
import difflib
|
||||||
|
import hashlib
|
||||||
import logging
|
import logging
|
||||||
import tomllib
|
import tomllib
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Final
|
from typing import Final
|
||||||
|
|
||||||
import click
|
import click
|
||||||
|
import requests
|
||||||
import rich
|
import rich
|
||||||
from packaging.version import Version
|
from packaging.version import Version
|
||||||
from rich.logging import RichHandler
|
from rich.logging import RichHandler
|
||||||
@@ -92,16 +95,29 @@ def update_virtualenv(force: bool, level: str) -> None:
|
|||||||
if latest_release.version > Version(local_version):
|
if latest_release.version > Version(local_version):
|
||||||
version = latest_release.name
|
version = latest_release.name
|
||||||
url = latest_release.download_url
|
url = latest_release.download_url
|
||||||
|
sha256 = "" # recomputed below
|
||||||
else:
|
else:
|
||||||
version = local_version
|
version = local_version
|
||||||
url = default["url"]
|
url = default["url"]
|
||||||
|
sha256 = default["sha256"]
|
||||||
|
|
||||||
|
# Compute sha256 if not already stored (new version or first-time population)
|
||||||
|
if not sha256:
|
||||||
|
log.info("Computing sha256 for %s...", url)
|
||||||
|
response = requests.get(url, stream=True)
|
||||||
|
response.raise_for_status()
|
||||||
|
hasher = hashlib.sha256()
|
||||||
|
for chunk in response.iter_content(65536):
|
||||||
|
hasher.update(chunk)
|
||||||
|
sha256 = hasher.hexdigest()
|
||||||
|
|
||||||
configurations["default"] = {
|
configurations["default"] = {
|
||||||
"version": version,
|
"version": version,
|
||||||
"url": url,
|
"url": url,
|
||||||
|
"sha256": sha256,
|
||||||
}
|
}
|
||||||
result_toml = "".join(
|
result_toml = "".join(
|
||||||
f'{key} = {{ version = "{value["version"]}", url = "{value["url"]}" }}\n'
|
f'{key} = {{ version = "{value["version"]}", url = "{value["url"]}", sha256 = "{value["sha256"]}" }}\n'
|
||||||
for key, value in configurations.items()
|
for key, value in configurations.items()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ def github_api_request(path: str, *, max_retries: int = 3) -> dict[str, Any]:
|
|||||||
|
|
||||||
class PyodideXBuildEnvRelease(typing.TypedDict):
|
class PyodideXBuildEnvRelease(typing.TypedDict):
|
||||||
version: str
|
version: str
|
||||||
|
sha256: str
|
||||||
python_version: str
|
python_version: str
|
||||||
emscripten_version: str
|
emscripten_version: str
|
||||||
min_pyodide_build_version: NotRequired[str]
|
min_pyodide_build_version: NotRequired[str]
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ class PythonConfiguration:
|
|||||||
version: str
|
version: str
|
||||||
identifier: str
|
identifier: str
|
||||||
url: str
|
url: str
|
||||||
|
sha256: str
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def arch(self) -> str:
|
def arch(self) -> str:
|
||||||
@@ -181,7 +182,7 @@ def setup_target_python(config: PythonConfiguration, build_path: Path) -> Path:
|
|||||||
python_tgz = CIBW_CACHE_PATH / config.url.rpartition("/")[-1]
|
python_tgz = CIBW_CACHE_PATH / config.url.rpartition("/")[-1]
|
||||||
with FileLock(f"{python_tgz}.lock"):
|
with FileLock(f"{python_tgz}.lock"):
|
||||||
if not python_tgz.exists():
|
if not python_tgz.exists():
|
||||||
download(config.url, python_tgz)
|
download(config.url, python_tgz, sha256=config.sha256)
|
||||||
|
|
||||||
python_dir = build_path / "python"
|
python_dir = build_path / "python"
|
||||||
python_dir.mkdir()
|
python_dir.mkdir()
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ class PythonConfiguration:
|
|||||||
identifier: str
|
identifier: str
|
||||||
url: str
|
url: str
|
||||||
build_url: str
|
build_url: str
|
||||||
|
build_sha256: str
|
||||||
|
sha256: str
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def sdk(self) -> str:
|
def sdk(self) -> str:
|
||||||
@@ -78,7 +80,7 @@ def all_python_configurations() -> list[PythonConfiguration]:
|
|||||||
# configuration.
|
# configuration.
|
||||||
macos_python_configs = resources.read_python_configs("macos")
|
macos_python_configs = resources.read_python_configs("macos")
|
||||||
|
|
||||||
def build_url(config_dict: dict[str, str]) -> str:
|
def build_python_config(config_dict: dict[str, str]) -> dict[str, str]:
|
||||||
# The iOS identifier will be something like cp313-ios_arm64_iphoneos.
|
# The iOS identifier will be something like cp313-ios_arm64_iphoneos.
|
||||||
# Drop the iphoneos suffix, then replace ios with macosx to yield
|
# Drop the iphoneos suffix, then replace ios with macosx to yield
|
||||||
# cp313-macosx_arm64, which will be a macOS build identifier.
|
# cp313-macosx_arm64, which will be a macOS build identifier.
|
||||||
@@ -87,18 +89,22 @@ def all_python_configurations() -> list[PythonConfiguration]:
|
|||||||
matching = [
|
matching = [
|
||||||
config for config in macos_python_configs if config["identifier"] == macos_identifier
|
config for config in macos_python_configs if config["identifier"] == macos_identifier
|
||||||
]
|
]
|
||||||
return matching[0]["url"]
|
return matching[0]
|
||||||
|
|
||||||
# Load the platform configuration
|
# Load the platform configuration
|
||||||
full_python_configs = resources.read_python_configs("ios")
|
full_python_configs = resources.read_python_configs("ios")
|
||||||
# Build the configurations, annotating with macOS URL details.
|
# Build the configurations, annotating with macOS URL details.
|
||||||
return [
|
python_configurations = []
|
||||||
PythonConfiguration(
|
for item in full_python_configs:
|
||||||
**item,
|
build_config = build_python_config(item)
|
||||||
build_url=build_url(item),
|
python_configurations.append(
|
||||||
|
PythonConfiguration(
|
||||||
|
**item,
|
||||||
|
build_url=build_config["url"],
|
||||||
|
build_sha256=build_config["sha256"],
|
||||||
|
)
|
||||||
)
|
)
|
||||||
for item in full_python_configs
|
return python_configurations
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def get_python_configurations(
|
def get_python_configurations(
|
||||||
@@ -133,7 +139,7 @@ def install_target_cpython(tmp: Path, config: PythonConfiguration, free_threadin
|
|||||||
with FileLock(str(installation_path) + ".lock"):
|
with FileLock(str(installation_path) + ".lock"):
|
||||||
if not installation_path.exists():
|
if not installation_path.exists():
|
||||||
downloaded_tar_gz = tmp / ios_python_tar_gz
|
downloaded_tar_gz = tmp / ios_python_tar_gz
|
||||||
download(config.url, downloaded_tar_gz)
|
download(config.url, downloaded_tar_gz, sha256=config.sha256)
|
||||||
installation_path.mkdir(parents=True, exist_ok=True)
|
installation_path.mkdir(parents=True, exist_ok=True)
|
||||||
call("tar", "-C", installation_path, "-xf", downloaded_tar_gz)
|
call("tar", "-C", installation_path, "-xf", downloaded_tar_gz)
|
||||||
downloaded_tar_gz.unlink()
|
downloaded_tar_gz.unlink()
|
||||||
@@ -319,6 +325,7 @@ def setup_python(
|
|||||||
python_configuration.version,
|
python_configuration.version,
|
||||||
python_configuration.build_url,
|
python_configuration.build_url,
|
||||||
free_threading,
|
free_threading,
|
||||||
|
python_configuration.build_sha256,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
msg = f"Unknown Python implementation: {implementation_id}"
|
msg = f"Unknown Python implementation: {implementation_id}"
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ class PythonConfiguration:
|
|||||||
version: str
|
version: str
|
||||||
identifier: str
|
identifier: str
|
||||||
url: str
|
url: str
|
||||||
|
sha256: str
|
||||||
|
|
||||||
|
|
||||||
def all_python_configurations() -> list[PythonConfiguration]:
|
def all_python_configurations() -> list[PythonConfiguration]:
|
||||||
@@ -135,7 +136,7 @@ def get_python_configurations(
|
|||||||
return python_configurations
|
return python_configurations
|
||||||
|
|
||||||
|
|
||||||
def install_cpython(_tmp: Path, version: str, url: str, free_threading: bool) -> Path:
|
def install_cpython(_tmp: Path, version: str, url: str, free_threading: bool, sha256: str) -> Path:
|
||||||
ft = "T" if free_threading else ""
|
ft = "T" if free_threading else ""
|
||||||
installation_path = Path(f"/Library/Frameworks/Python{ft}.framework/Versions/{version}")
|
installation_path = Path(f"/Library/Frameworks/Python{ft}.framework/Versions/{version}")
|
||||||
with FileLock(CIBW_CACHE_PATH / f"cpython{version}.lock"):
|
with FileLock(CIBW_CACHE_PATH / f"cpython{version}.lock"):
|
||||||
@@ -161,7 +162,7 @@ def install_cpython(_tmp: Path, version: str, url: str, free_threading: bool) ->
|
|||||||
python_filename = url.rsplit("/", maxsplit=1)[-1]
|
python_filename = url.rsplit("/", maxsplit=1)[-1]
|
||||||
pkg_path = CIBW_CACHE_PATH / "cpython-installer" / python_filename
|
pkg_path = CIBW_CACHE_PATH / "cpython-installer" / python_filename
|
||||||
if not pkg_path.exists():
|
if not pkg_path.exists():
|
||||||
download(url, pkg_path)
|
download(url, pkg_path, sha256=sha256)
|
||||||
args = []
|
args = []
|
||||||
if version.startswith("3.14"):
|
if version.startswith("3.14"):
|
||||||
args += ["-applyChoiceChangesXML", str(resources.FREE_THREAD_ENABLE_314.resolve())]
|
args += ["-applyChoiceChangesXML", str(resources.FREE_THREAD_ENABLE_314.resolve())]
|
||||||
@@ -185,7 +186,7 @@ def install_cpython(_tmp: Path, version: str, url: str, free_threading: bool) ->
|
|||||||
return installation_path / "bin" / (f"python{version}t" if free_threading else "python3")
|
return installation_path / "bin" / (f"python{version}t" if free_threading else "python3")
|
||||||
|
|
||||||
|
|
||||||
def install_pypy(tmp: Path, url: str) -> Path:
|
def install_pypy(tmp: Path, url: str, sha256: str) -> Path:
|
||||||
pypy_tar_bz2 = url.rsplit("/", 1)[-1]
|
pypy_tar_bz2 = url.rsplit("/", 1)[-1]
|
||||||
extension = ".tar.bz2"
|
extension = ".tar.bz2"
|
||||||
assert pypy_tar_bz2.endswith(extension)
|
assert pypy_tar_bz2.endswith(extension)
|
||||||
@@ -193,14 +194,14 @@ def install_pypy(tmp: Path, url: str) -> Path:
|
|||||||
with FileLock(str(installation_path) + ".lock"):
|
with FileLock(str(installation_path) + ".lock"):
|
||||||
if not installation_path.exists():
|
if not installation_path.exists():
|
||||||
downloaded_tar_bz2 = tmp / pypy_tar_bz2
|
downloaded_tar_bz2 = tmp / pypy_tar_bz2
|
||||||
download(url, downloaded_tar_bz2)
|
download(url, downloaded_tar_bz2, sha256=sha256)
|
||||||
installation_path.parent.mkdir(parents=True, exist_ok=True)
|
installation_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
call("tar", "-C", installation_path.parent, "-xf", downloaded_tar_bz2)
|
call("tar", "-C", installation_path.parent, "-xf", downloaded_tar_bz2)
|
||||||
downloaded_tar_bz2.unlink()
|
downloaded_tar_bz2.unlink()
|
||||||
return installation_path / "bin" / "pypy3"
|
return installation_path / "bin" / "pypy3"
|
||||||
|
|
||||||
|
|
||||||
def install_graalpy(tmp: Path, url: str) -> Path:
|
def install_graalpy(tmp: Path, url: str, sha256: str) -> Path:
|
||||||
graalpy_archive = url.rsplit("/", 1)[-1]
|
graalpy_archive = url.rsplit("/", 1)[-1]
|
||||||
extension = ".tar.gz"
|
extension = ".tar.gz"
|
||||||
assert graalpy_archive.endswith(extension)
|
assert graalpy_archive.endswith(extension)
|
||||||
@@ -208,7 +209,7 @@ def install_graalpy(tmp: Path, url: str) -> Path:
|
|||||||
with FileLock(str(installation_path) + ".lock"):
|
with FileLock(str(installation_path) + ".lock"):
|
||||||
if not installation_path.exists():
|
if not installation_path.exists():
|
||||||
downloaded_archive = tmp / graalpy_archive
|
downloaded_archive = tmp / graalpy_archive
|
||||||
download(url, downloaded_archive)
|
download(url, downloaded_archive, sha256=sha256)
|
||||||
installation_path.mkdir(parents=True)
|
installation_path.mkdir(parents=True)
|
||||||
# GraalPy top-folder name is inconsistent with archive name
|
# GraalPy top-folder name is inconsistent with archive name
|
||||||
call("tar", "-C", installation_path, "--strip-components=1", "-xzf", downloaded_archive)
|
call("tar", "-C", installation_path, "--strip-components=1", "-xzf", downloaded_archive)
|
||||||
@@ -232,13 +233,17 @@ def setup_python(
|
|||||||
if implementation_id.startswith("cp"):
|
if implementation_id.startswith("cp"):
|
||||||
free_threading = "t-macos" in python_configuration.identifier
|
free_threading = "t-macos" in python_configuration.identifier
|
||||||
base_python = install_cpython(
|
base_python = install_cpython(
|
||||||
tmp, python_configuration.version, python_configuration.url, free_threading
|
tmp,
|
||||||
|
python_configuration.version,
|
||||||
|
python_configuration.url,
|
||||||
|
free_threading,
|
||||||
|
python_configuration.sha256,
|
||||||
)
|
)
|
||||||
|
|
||||||
elif implementation_id.startswith("pp"):
|
elif implementation_id.startswith("pp"):
|
||||||
base_python = install_pypy(tmp, python_configuration.url)
|
base_python = install_pypy(tmp, python_configuration.url, python_configuration.sha256)
|
||||||
elif implementation_id.startswith("gp"):
|
elif implementation_id.startswith("gp"):
|
||||||
base_python = install_graalpy(tmp, python_configuration.url)
|
base_python = install_graalpy(tmp, python_configuration.url, python_configuration.sha256)
|
||||||
else:
|
else:
|
||||||
msg = "Unknown Python implementation"
|
msg = "Unknown Python implementation"
|
||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ class PythonConfiguration:
|
|||||||
identifier: str
|
identifier: str
|
||||||
default_pyodide_version: str
|
default_pyodide_version: str
|
||||||
node_version: str
|
node_version: str
|
||||||
|
sha256: str = ""
|
||||||
|
|
||||||
|
|
||||||
class PyodideXBuildEnvInfoVersionRange(TypedDict):
|
class PyodideXBuildEnvInfoVersionRange(TypedDict):
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ class PythonConfiguration:
|
|||||||
version: str
|
version: str
|
||||||
identifier: str
|
identifier: str
|
||||||
url: str | None = None
|
url: str | None = None
|
||||||
|
sha256: str = ""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def arch(self) -> str:
|
def arch(self) -> str:
|
||||||
@@ -131,7 +132,7 @@ def install_cpython(configuration: PythonConfiguration, arch: str | None = None)
|
|||||||
return installation_path / "python.exe"
|
return installation_path / "python.exe"
|
||||||
|
|
||||||
|
|
||||||
def install_pypy(tmp: Path, arch: str, url: str) -> Path:
|
def install_pypy(tmp: Path, arch: str, url: str, sha256: str) -> Path:
|
||||||
assert arch == "64"
|
assert arch == "64"
|
||||||
assert "win64" in url
|
assert "win64" in url
|
||||||
# Inside the PyPy zip file is a directory with the same name
|
# Inside the PyPy zip file is a directory with the same name
|
||||||
@@ -142,13 +143,13 @@ def install_pypy(tmp: Path, arch: str, url: str) -> Path:
|
|||||||
with FileLock(str(installation_path) + ".lock"):
|
with FileLock(str(installation_path) + ".lock"):
|
||||||
if not installation_path.exists():
|
if not installation_path.exists():
|
||||||
pypy_zip = tmp / zip_filename
|
pypy_zip = tmp / zip_filename
|
||||||
download(url, pypy_zip)
|
download(url, pypy_zip, sha256=sha256)
|
||||||
# Extract to the parent directory because the zip file still contains a directory
|
# Extract to the parent directory because the zip file still contains a directory
|
||||||
extract_zip(pypy_zip, installation_path.parent)
|
extract_zip(pypy_zip, installation_path.parent)
|
||||||
return installation_path / "python.exe"
|
return installation_path / "python.exe"
|
||||||
|
|
||||||
|
|
||||||
def install_graalpy(tmp: Path, url: str) -> Path:
|
def install_graalpy(tmp: Path, url: str, sha256: str) -> Path:
|
||||||
zip_filename = url.rsplit("/", 1)[-1]
|
zip_filename = url.rsplit("/", 1)[-1]
|
||||||
extension = ".zip"
|
extension = ".zip"
|
||||||
assert zip_filename.endswith(extension)
|
assert zip_filename.endswith(extension)
|
||||||
@@ -156,7 +157,7 @@ def install_graalpy(tmp: Path, url: str) -> Path:
|
|||||||
with FileLock(str(installation_path) + ".lock"):
|
with FileLock(str(installation_path) + ".lock"):
|
||||||
if not installation_path.exists():
|
if not installation_path.exists():
|
||||||
graalpy_zip = tmp / zip_filename
|
graalpy_zip = tmp / zip_filename
|
||||||
download(url, graalpy_zip)
|
download(url, graalpy_zip, sha256=sha256)
|
||||||
# Extract to the parent directory because the zip file still contains a directory
|
# Extract to the parent directory because the zip file still contains a directory
|
||||||
extract_zip(graalpy_zip, installation_path.parent)
|
extract_zip(graalpy_zip, installation_path.parent)
|
||||||
return installation_path / "bin" / "graalpy.exe"
|
return installation_path / "bin" / "graalpy.exe"
|
||||||
@@ -272,9 +273,14 @@ def setup_python(
|
|||||||
base_python = install_cpython(python_configuration, arch=native_arch)
|
base_python = install_cpython(python_configuration, arch=native_arch)
|
||||||
elif implementation_id.startswith("pp"):
|
elif implementation_id.startswith("pp"):
|
||||||
assert python_configuration.url is not None
|
assert python_configuration.url is not None
|
||||||
base_python = install_pypy(tmp, python_configuration.arch, python_configuration.url)
|
assert python_configuration.sha256
|
||||||
|
base_python = install_pypy(
|
||||||
|
tmp, python_configuration.arch, python_configuration.url, python_configuration.sha256
|
||||||
|
)
|
||||||
elif implementation_id.startswith("gp"):
|
elif implementation_id.startswith("gp"):
|
||||||
base_python = install_graalpy(tmp, python_configuration.url or "")
|
assert python_configuration.url is not None
|
||||||
|
assert python_configuration.sha256
|
||||||
|
base_python = install_graalpy(tmp, python_configuration.url, python_configuration.sha256)
|
||||||
else:
|
else:
|
||||||
msg = "Unknown Python implementation"
|
msg = "Unknown Python implementation"
|
||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
|
|||||||
@@ -143,43 +143,43 @@ python_configurations = [
|
|||||||
|
|
||||||
[macos]
|
[macos]
|
||||||
python_configurations = [
|
python_configurations = [
|
||||||
{ identifier = "cp39-macosx_x86_64", version = "3.9", url = "https://www.python.org/ftp/python/3.9.13/python-3.9.13-macos11.pkg" },
|
{ identifier = "cp39-macosx_x86_64", version = "3.9", url = "https://www.python.org/ftp/python/3.9.13/python-3.9.13-macos11.pkg", sha256 = "351fe18f4fb03be7afac5e4012fc0a51345f43202af43ef620cf1eee5ee36578" },
|
||||||
{ identifier = "cp39-macosx_arm64", version = "3.9", url = "https://www.python.org/ftp/python/3.9.13/python-3.9.13-macos11.pkg" },
|
{ identifier = "cp39-macosx_arm64", version = "3.9", url = "https://www.python.org/ftp/python/3.9.13/python-3.9.13-macos11.pkg", sha256 = "351fe18f4fb03be7afac5e4012fc0a51345f43202af43ef620cf1eee5ee36578" },
|
||||||
{ identifier = "cp39-macosx_universal2", version = "3.9", url = "https://www.python.org/ftp/python/3.9.13/python-3.9.13-macos11.pkg" },
|
{ identifier = "cp39-macosx_universal2", version = "3.9", url = "https://www.python.org/ftp/python/3.9.13/python-3.9.13-macos11.pkg", sha256 = "351fe18f4fb03be7afac5e4012fc0a51345f43202af43ef620cf1eee5ee36578" },
|
||||||
{ identifier = "cp310-macosx_x86_64", version = "3.10", url = "https://www.python.org/ftp/python/3.10.11/python-3.10.11-macos11.pkg" },
|
{ identifier = "cp310-macosx_x86_64", version = "3.10", url = "https://www.python.org/ftp/python/3.10.11/python-3.10.11-macos11.pkg", sha256 = "767ed35ad688d28ea4494081ae96408a0318d0d5bb9ca0139d74d6247b231cfc" },
|
||||||
{ identifier = "cp310-macosx_arm64", version = "3.10", url = "https://www.python.org/ftp/python/3.10.11/python-3.10.11-macos11.pkg" },
|
{ identifier = "cp310-macosx_arm64", version = "3.10", url = "https://www.python.org/ftp/python/3.10.11/python-3.10.11-macos11.pkg", sha256 = "767ed35ad688d28ea4494081ae96408a0318d0d5bb9ca0139d74d6247b231cfc" },
|
||||||
{ identifier = "cp310-macosx_universal2", version = "3.10", url = "https://www.python.org/ftp/python/3.10.11/python-3.10.11-macos11.pkg" },
|
{ identifier = "cp310-macosx_universal2", version = "3.10", url = "https://www.python.org/ftp/python/3.10.11/python-3.10.11-macos11.pkg", sha256 = "767ed35ad688d28ea4494081ae96408a0318d0d5bb9ca0139d74d6247b231cfc" },
|
||||||
{ identifier = "cp311-macosx_x86_64", version = "3.11", url = "https://www.python.org/ftp/python/3.11.9/python-3.11.9-macos11.pkg" },
|
{ identifier = "cp311-macosx_x86_64", version = "3.11", url = "https://www.python.org/ftp/python/3.11.9/python-3.11.9-macos11.pkg", sha256 = "b6cfdee2571ca56ee895043ca1e7110fb78a878cee3eb0c21accb2de34d24b55" },
|
||||||
{ identifier = "cp311-macosx_arm64", version = "3.11", url = "https://www.python.org/ftp/python/3.11.9/python-3.11.9-macos11.pkg" },
|
{ identifier = "cp311-macosx_arm64", version = "3.11", url = "https://www.python.org/ftp/python/3.11.9/python-3.11.9-macos11.pkg", sha256 = "b6cfdee2571ca56ee895043ca1e7110fb78a878cee3eb0c21accb2de34d24b55" },
|
||||||
{ identifier = "cp311-macosx_universal2", version = "3.11", url = "https://www.python.org/ftp/python/3.11.9/python-3.11.9-macos11.pkg" },
|
{ identifier = "cp311-macosx_universal2", version = "3.11", url = "https://www.python.org/ftp/python/3.11.9/python-3.11.9-macos11.pkg", sha256 = "b6cfdee2571ca56ee895043ca1e7110fb78a878cee3eb0c21accb2de34d24b55" },
|
||||||
{ identifier = "cp312-macosx_x86_64", version = "3.12", url = "https://www.python.org/ftp/python/3.12.10/python-3.12.10-macos11.pkg" },
|
{ identifier = "cp312-macosx_x86_64", version = "3.12", url = "https://www.python.org/ftp/python/3.12.10/python-3.12.10-macos11.pkg", sha256 = "8373e58da4ea146b3eb1c1f9834f19a319440b6b679b06050b1f9ee3237aa8e4" },
|
||||||
{ identifier = "cp312-macosx_arm64", version = "3.12", url = "https://www.python.org/ftp/python/3.12.10/python-3.12.10-macos11.pkg" },
|
{ identifier = "cp312-macosx_arm64", version = "3.12", url = "https://www.python.org/ftp/python/3.12.10/python-3.12.10-macos11.pkg", sha256 = "8373e58da4ea146b3eb1c1f9834f19a319440b6b679b06050b1f9ee3237aa8e4" },
|
||||||
{ identifier = "cp312-macosx_universal2", version = "3.12", url = "https://www.python.org/ftp/python/3.12.10/python-3.12.10-macos11.pkg" },
|
{ identifier = "cp312-macosx_universal2", version = "3.12", url = "https://www.python.org/ftp/python/3.12.10/python-3.12.10-macos11.pkg", sha256 = "8373e58da4ea146b3eb1c1f9834f19a319440b6b679b06050b1f9ee3237aa8e4" },
|
||||||
{ identifier = "cp313-macosx_x86_64", version = "3.13", url = "https://www.python.org/ftp/python/3.13.13/python-3.13.13-macos11.pkg" },
|
{ identifier = "cp313-macosx_x86_64", version = "3.13", url = "https://www.python.org/ftp/python/3.13.13/python-3.13.13-macos11.pkg", sha256 = "a909cb655af5db67d5a90b3603437a1d58bec3446d624e4034e278ac62023cc9" },
|
||||||
{ identifier = "cp313-macosx_arm64", version = "3.13", url = "https://www.python.org/ftp/python/3.13.13/python-3.13.13-macos11.pkg" },
|
{ identifier = "cp313-macosx_arm64", version = "3.13", url = "https://www.python.org/ftp/python/3.13.13/python-3.13.13-macos11.pkg", sha256 = "a909cb655af5db67d5a90b3603437a1d58bec3446d624e4034e278ac62023cc9" },
|
||||||
{ identifier = "cp313-macosx_universal2", version = "3.13", url = "https://www.python.org/ftp/python/3.13.13/python-3.13.13-macos11.pkg" },
|
{ identifier = "cp313-macosx_universal2", version = "3.13", url = "https://www.python.org/ftp/python/3.13.13/python-3.13.13-macos11.pkg", sha256 = "a909cb655af5db67d5a90b3603437a1d58bec3446d624e4034e278ac62023cc9" },
|
||||||
{ identifier = "cp314-macosx_x86_64", version = "3.14", url = "https://www.python.org/ftp/python/3.14.5/python-3.14.5-macos11.pkg" },
|
{ identifier = "cp314-macosx_x86_64", version = "3.14", url = "https://www.python.org/ftp/python/3.14.5/python-3.14.5-macos11.pkg", sha256 = "b28a8dc33c456dd06c97024697d63ca916cfb494594c06fa3e4ef4d41fa82335" },
|
||||||
{ identifier = "cp314-macosx_arm64", version = "3.14", url = "https://www.python.org/ftp/python/3.14.5/python-3.14.5-macos11.pkg" },
|
{ identifier = "cp314-macosx_arm64", version = "3.14", url = "https://www.python.org/ftp/python/3.14.5/python-3.14.5-macos11.pkg", sha256 = "b28a8dc33c456dd06c97024697d63ca916cfb494594c06fa3e4ef4d41fa82335" },
|
||||||
{ identifier = "cp314-macosx_universal2", version = "3.14", url = "https://www.python.org/ftp/python/3.14.5/python-3.14.5-macos11.pkg" },
|
{ identifier = "cp314-macosx_universal2", version = "3.14", url = "https://www.python.org/ftp/python/3.14.5/python-3.14.5-macos11.pkg", sha256 = "b28a8dc33c456dd06c97024697d63ca916cfb494594c06fa3e4ef4d41fa82335" },
|
||||||
{ identifier = "cp314t-macosx_x86_64", version = "3.14", url = "https://www.python.org/ftp/python/3.14.5/python-3.14.5-macos11.pkg" },
|
{ identifier = "cp314t-macosx_x86_64", version = "3.14", url = "https://www.python.org/ftp/python/3.14.5/python-3.14.5-macos11.pkg", sha256 = "b28a8dc33c456dd06c97024697d63ca916cfb494594c06fa3e4ef4d41fa82335" },
|
||||||
{ identifier = "cp314t-macosx_arm64", version = "3.14", url = "https://www.python.org/ftp/python/3.14.5/python-3.14.5-macos11.pkg" },
|
{ identifier = "cp314t-macosx_arm64", version = "3.14", url = "https://www.python.org/ftp/python/3.14.5/python-3.14.5-macos11.pkg", sha256 = "b28a8dc33c456dd06c97024697d63ca916cfb494594c06fa3e4ef4d41fa82335" },
|
||||||
{ identifier = "cp314t-macosx_universal2", version = "3.14", url = "https://www.python.org/ftp/python/3.14.5/python-3.14.5-macos11.pkg" },
|
{ identifier = "cp314t-macosx_universal2", version = "3.14", url = "https://www.python.org/ftp/python/3.14.5/python-3.14.5-macos11.pkg", sha256 = "b28a8dc33c456dd06c97024697d63ca916cfb494594c06fa3e4ef4d41fa82335" },
|
||||||
{ identifier = "cp315-macosx_x86_64", version = "3.15", url = "https://www.python.org/ftp/python/3.15.0/python-3.15.0b1-macos11.pkg" },
|
{ identifier = "cp315-macosx_x86_64", version = "3.15", url = "https://www.python.org/ftp/python/3.15.0/python-3.15.0b1-macos11.pkg", sha256 = "d9e6de70d8bb02c9300b9d8e8b56aadb8dd426073d97e6225ac5319b3eb6d84e" },
|
||||||
{ identifier = "cp315-macosx_arm64", version = "3.15", url = "https://www.python.org/ftp/python/3.15.0/python-3.15.0b1-macos11.pkg" },
|
{ identifier = "cp315-macosx_arm64", version = "3.15", url = "https://www.python.org/ftp/python/3.15.0/python-3.15.0b1-macos11.pkg", sha256 = "d9e6de70d8bb02c9300b9d8e8b56aadb8dd426073d97e6225ac5319b3eb6d84e" },
|
||||||
{ identifier = "cp315-macosx_universal2", version = "3.15", url = "https://www.python.org/ftp/python/3.15.0/python-3.15.0b1-macos11.pkg" },
|
{ identifier = "cp315-macosx_universal2", version = "3.15", url = "https://www.python.org/ftp/python/3.15.0/python-3.15.0b1-macos11.pkg", sha256 = "d9e6de70d8bb02c9300b9d8e8b56aadb8dd426073d97e6225ac5319b3eb6d84e" },
|
||||||
{ identifier = "cp315t-macosx_x86_64", version = "3.15", url = "https://www.python.org/ftp/python/3.15.0/python-3.15.0b1-macos11.pkg" },
|
{ identifier = "cp315t-macosx_x86_64", version = "3.15", url = "https://www.python.org/ftp/python/3.15.0/python-3.15.0b1-macos11.pkg", sha256 = "d9e6de70d8bb02c9300b9d8e8b56aadb8dd426073d97e6225ac5319b3eb6d84e" },
|
||||||
{ identifier = "cp315t-macosx_arm64", version = "3.15", url = "https://www.python.org/ftp/python/3.15.0/python-3.15.0b1-macos11.pkg" },
|
{ identifier = "cp315t-macosx_arm64", version = "3.15", url = "https://www.python.org/ftp/python/3.15.0/python-3.15.0b1-macos11.pkg", sha256 = "d9e6de70d8bb02c9300b9d8e8b56aadb8dd426073d97e6225ac5319b3eb6d84e" },
|
||||||
{ identifier = "cp315t-macosx_universal2", version = "3.15", url = "https://www.python.org/ftp/python/3.15.0/python-3.15.0b1-macos11.pkg" },
|
{ identifier = "cp315t-macosx_universal2", version = "3.15", url = "https://www.python.org/ftp/python/3.15.0/python-3.15.0b1-macos11.pkg", sha256 = "d9e6de70d8bb02c9300b9d8e8b56aadb8dd426073d97e6225ac5319b3eb6d84e" },
|
||||||
{ identifier = "pp39-macosx_x86_64", version = "3.9", url = "https://downloads.python.org/pypy/pypy3.9-v7.3.16-macos_x86_64.tar.bz2" },
|
{ identifier = "pp39-macosx_x86_64", version = "3.9", url = "https://downloads.python.org/pypy/pypy3.9-v7.3.16-macos_x86_64.tar.bz2", sha256 = "fda015431621e7e5aa16359d114f2c45a77ed936992c1efff86302e768a6b21c" },
|
||||||
{ identifier = "pp39-macosx_arm64", version = "3.9", url = "https://downloads.python.org/pypy/pypy3.9-v7.3.16-macos_arm64.tar.bz2" },
|
{ identifier = "pp39-macosx_arm64", version = "3.9", url = "https://downloads.python.org/pypy/pypy3.9-v7.3.16-macos_arm64.tar.bz2", sha256 = "88f824e7a2d676440d09bc90fc959ae0fd3557d7e2f14bfbbe53d41d159a47fe" },
|
||||||
{ identifier = "pp310-macosx_x86_64", version = "3.10", url = "https://downloads.python.org/pypy/pypy3.10-v7.3.19-macos_x86_64.tar.bz2" },
|
{ identifier = "pp310-macosx_x86_64", version = "3.10", url = "https://downloads.python.org/pypy/pypy3.10-v7.3.19-macos_x86_64.tar.bz2", sha256 = "2c5e5c2a33ac882551d7f28b98d19d486b8995aa50824a73b4edcc6aeec35c63" },
|
||||||
{ identifier = "pp310-macosx_arm64", version = "3.10", url = "https://downloads.python.org/pypy/pypy3.10-v7.3.19-macos_arm64.tar.bz2" },
|
{ identifier = "pp310-macosx_arm64", version = "3.10", url = "https://downloads.python.org/pypy/pypy3.10-v7.3.19-macos_arm64.tar.bz2", sha256 = "3db8a03fc496164801646844d7f3b12baa0adb3ad9a0b7cb124521bc2e168a36" },
|
||||||
{ identifier = "pp311-macosx_x86_64", version = "3.11", url = "https://downloads.python.org/pypy/pypy3.11-v7.3.23-macos_x86_64.tar.bz2" },
|
{ identifier = "pp311-macosx_x86_64", version = "3.11", url = "https://downloads.python.org/pypy/pypy3.11-v7.3.23-macos_x86_64.tar.bz2", sha256 = "c95363c4e87235d11a6cec8128239c291b1eb67a752778fbcfe029a71da82b5e" },
|
||||||
{ identifier = "pp311-macosx_arm64", version = "3.11", url = "https://downloads.python.org/pypy/pypy3.11-v7.3.23-macos_arm64.tar.bz2" },
|
{ identifier = "pp311-macosx_arm64", version = "3.11", url = "https://downloads.python.org/pypy/pypy3.11-v7.3.23-macos_arm64.tar.bz2", sha256 = "4747b3aceba4c1c6104cddc0fe5ea302101d32955f0957347b9ecc4fbd7aed05" },
|
||||||
{ identifier = "gp311_242-macosx_x86_64", version = "3.11", url = "https://github.com/oracle/graalpython/releases/download/graal-24.2.2/graalpy-24.2.2-macos-amd64.tar.gz" },
|
{ identifier = "gp311_242-macosx_x86_64", version = "3.11", url = "https://github.com/oracle/graalpython/releases/download/graal-24.2.2/graalpy-24.2.2-macos-amd64.tar.gz", sha256 = "2f4d5e7dbdf90e38778dfcb8ca3e1ec7eee257ef726b1937d5bc91b54cdddf9b" },
|
||||||
{ identifier = "gp311_242-macosx_arm64", version = "3.11", url = "https://github.com/oracle/graalpython/releases/download/graal-24.2.2/graalpy-24.2.2-macos-aarch64.tar.gz" },
|
{ identifier = "gp311_242-macosx_arm64", version = "3.11", url = "https://github.com/oracle/graalpython/releases/download/graal-24.2.2/graalpy-24.2.2-macos-aarch64.tar.gz", sha256 = "f4a2ae01bae0fa53ec0d19f86d73c6dcc2a162d245552030183b84bfdd8f7635" },
|
||||||
{ identifier = "gp312_250-macosx_x86_64", version = "3.12", url = "https://github.com/oracle/graalpython/releases/download/graal-25.0.1/graalpy-25.0.1-macos-amd64.tar.gz" },
|
{ identifier = "gp312_250-macosx_x86_64", version = "3.12", url = "https://github.com/oracle/graalpython/releases/download/graal-25.0.1/graalpy-25.0.1-macos-amd64.tar.gz", sha256 = "10b0721d52397f0cc85f038900318da2203711cbcfae7899e3faed49d3dc6221" },
|
||||||
{ identifier = "gp312_250-macosx_arm64", version = "3.12", url = "https://github.com/oracle/graalpython/releases/download/graal-25.0.3/graalpy-25.0.3-macos-aarch64.tar.gz" },
|
{ identifier = "gp312_250-macosx_arm64", version = "3.12", url = "https://github.com/oracle/graalpython/releases/download/graal-25.0.3/graalpy-25.0.3-macos-aarch64.tar.gz", sha256 = "387d9f5b376860842bea4d55aae5820974f3f3b68fc36c77ae863a061a888857" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[windows]
|
[windows]
|
||||||
@@ -211,39 +211,39 @@ python_configurations = [
|
|||||||
{ identifier = "cp314t-win_arm64", version = "3.14.5" },
|
{ identifier = "cp314t-win_arm64", version = "3.14.5" },
|
||||||
{ identifier = "cp315-win_arm64", version = "3.15.0-b1" },
|
{ identifier = "cp315-win_arm64", version = "3.15.0-b1" },
|
||||||
{ identifier = "cp315t-win_arm64", version = "3.15.0-b1" },
|
{ identifier = "cp315t-win_arm64", version = "3.15.0-b1" },
|
||||||
{ identifier = "pp39-win_amd64", version = "3.9", url = "https://downloads.python.org/pypy/pypy3.9-v7.3.16-win64.zip" },
|
{ identifier = "pp39-win_amd64", version = "3.9", url = "https://downloads.python.org/pypy/pypy3.9-v7.3.16-win64.zip", sha256 = "06ec12a5e964dc0ad33e6f380185a4d295178dce6d6df512f508e7aee00a1323" },
|
||||||
{ identifier = "pp310-win_amd64", version = "3.10", url = "https://downloads.python.org/pypy/pypy3.10-v7.3.19-win64.zip" },
|
{ identifier = "pp310-win_amd64", version = "3.10", url = "https://downloads.python.org/pypy/pypy3.10-v7.3.19-win64.zip", sha256 = "c0d07bba6c8fb4e5804f4a8b3f8ef07cc3d89f6ad1db42a45ffb9be60bbb7cc2" },
|
||||||
{ identifier = "pp311-win_amd64", version = "3.11", url = "https://downloads.python.org/pypy/pypy3.11-v7.3.23-win64.zip" },
|
{ identifier = "pp311-win_amd64", version = "3.11", url = "https://downloads.python.org/pypy/pypy3.11-v7.3.23-win64.zip", sha256 = "948b8ea58dea5b9917210fe4afd242c788fbfaba1c3f1a25e696a404f703389a" },
|
||||||
{ identifier = "gp311_242-win_amd64", version = "3.11", url = "https://github.com/oracle/graalpython/releases/download/graal-24.2.2/graalpy-24.2.2-windows-amd64.zip" },
|
{ identifier = "gp311_242-win_amd64", version = "3.11", url = "https://github.com/oracle/graalpython/releases/download/graal-24.2.2/graalpy-24.2.2-windows-amd64.zip", sha256 = "9606134284d4d95b2f9d69c3087cd3e9e488f46355b419f5e66588a3281df6a3" },
|
||||||
{ identifier = "gp312_250-win_amd64", version = "3.12", url = "https://github.com/oracle/graalpython/releases/download/graal-25.0.3/graalpy-25.0.3-windows-amd64.zip" },
|
{ identifier = "gp312_250-win_amd64", version = "3.12", url = "https://github.com/oracle/graalpython/releases/download/graal-25.0.3/graalpy-25.0.3-windows-amd64.zip", sha256 = "2ae5c42b93e08d9e106a017634a3442b272ccae6d08ace157244de0449e937d4" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[pyodide]
|
[pyodide]
|
||||||
python_configurations = [
|
python_configurations = [
|
||||||
{ identifier = "cp312-pyodide_wasm32", version = "3.12", default_pyodide_version = "0.27.7", node_version = "v22" },
|
{ identifier = "cp312-pyodide_wasm32", version = "3.12", default_pyodide_version = "0.27.7", node_version = "v22", sha256 = "0c2e7db42efa5d1dac38b50f8b3d659a1e3885d0a233494831b8206281307d52" },
|
||||||
{ identifier = "cp313-pyodide_wasm32", version = "3.13", default_pyodide_version = "0.29.4", node_version = "v22" },
|
{ identifier = "cp313-pyodide_wasm32", version = "3.13", default_pyodide_version = "0.29.4", node_version = "v22", sha256 = "a29fc4a076408a18fc29eb4b280f80c6dddc95c19514c874c29e590d0931c02a" },
|
||||||
{ identifier = "cp314-pyodide_wasm32", version = "3.14", default_pyodide_version = "314.0.0a2", node_version = "v24" },
|
{ identifier = "cp314-pyodide_wasm32", version = "3.14", default_pyodide_version = "314.0.0a2", node_version = "v24", sha256 = "ac7bbcdf289ab3ae25621c436b1d013099ef6bec644ad0538dae5d65552c5c95" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[android]
|
[android]
|
||||||
python_configurations = [
|
python_configurations = [
|
||||||
{ identifier = "cp313-android_arm64_v8a", version = "3.13", url = "https://repo.maven.apache.org/maven2/com/chaquo/python/python/3.13.13/python-3.13.13-aarch64-linux-android.tar.gz" },
|
{ identifier = "cp313-android_arm64_v8a", version = "3.13", url = "https://repo.maven.apache.org/maven2/com/chaquo/python/python/3.13.13/python-3.13.13-aarch64-linux-android.tar.gz", sha256 = "a21250c246b36eb704be096be51e40ab24cafa3ae1d7ca4c396e6bf780cf87cf" },
|
||||||
{ identifier = "cp313-android_x86_64", version = "3.13", url = "https://repo.maven.apache.org/maven2/com/chaquo/python/python/3.13.13/python-3.13.13-x86_64-linux-android.tar.gz" },
|
{ identifier = "cp313-android_x86_64", version = "3.13", url = "https://repo.maven.apache.org/maven2/com/chaquo/python/python/3.13.13/python-3.13.13-x86_64-linux-android.tar.gz", sha256 = "b021b76df2c8d64e41c76d18a2d91f27652356bd932c06f9076bac1b664c0e37" },
|
||||||
{ identifier = "cp314-android_arm64_v8a", version = "3.14", url = "https://www.python.org/ftp/python/3.14.5/python-3.14.5-aarch64-linux-android.tar.gz" },
|
{ identifier = "cp314-android_arm64_v8a", version = "3.14", url = "https://www.python.org/ftp/python/3.14.5/python-3.14.5-aarch64-linux-android.tar.gz", sha256 = "f008321abf837fcaec569df143283ece0e764b18d8c75763200160553f906af1" },
|
||||||
{ identifier = "cp314-android_x86_64", version = "3.14", url = "https://www.python.org/ftp/python/3.14.5/python-3.14.5-x86_64-linux-android.tar.gz" },
|
{ identifier = "cp314-android_x86_64", version = "3.14", url = "https://www.python.org/ftp/python/3.14.5/python-3.14.5-x86_64-linux-android.tar.gz", sha256 = "36184e31464b2b959d92c81ded8d025834342cc080623bced282a2a9a57bd47c" },
|
||||||
{ identifier = "cp315-android_arm64_v8a", version = "3.15", url = "https://www.python.org/ftp/python/3.15.0/python-3.15.0b1-aarch64-linux-android.tar.gz" },
|
{ identifier = "cp315-android_arm64_v8a", version = "3.15", url = "https://www.python.org/ftp/python/3.15.0/python-3.15.0b1-aarch64-linux-android.tar.gz", sha256 = "9a25e5499d639d4f59bc766ad36c9eddd892232a9f9224c2fa4ef55dc1d49ff3" },
|
||||||
{ identifier = "cp315-android_x86_64", version = "3.15", url = "https://www.python.org/ftp/python/3.15.0/python-3.15.0b1-x86_64-linux-android.tar.gz" },
|
{ identifier = "cp315-android_x86_64", version = "3.15", url = "https://www.python.org/ftp/python/3.15.0/python-3.15.0b1-x86_64-linux-android.tar.gz", sha256 = "86f492e92340028c8c7efedde6760b40f5e645062d0ef17af0d0bf4d18b127f2" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[ios]
|
[ios]
|
||||||
python_configurations = [
|
python_configurations = [
|
||||||
{ identifier = "cp313-ios_arm64_iphoneos", version = "3.13", url = "https://github.com/beeware/Python-Apple-support/releases/download/3.13-b13/Python-3.13-iOS-support.b13.tar.gz" },
|
{ identifier = "cp313-ios_arm64_iphoneos", version = "3.13", url = "https://github.com/beeware/Python-Apple-support/releases/download/3.13-b13/Python-3.13-iOS-support.b13.tar.gz", sha256 = "d1f95f95137a4b91dc0cbe9b99ddfc0a78918dde883c7fbe7147074a5e715274" },
|
||||||
{ identifier = "cp313-ios_x86_64_iphonesimulator", version = "3.13", url = "https://github.com/beeware/Python-Apple-support/releases/download/3.13-b13/Python-3.13-iOS-support.b13.tar.gz" },
|
{ identifier = "cp313-ios_x86_64_iphonesimulator", version = "3.13", url = "https://github.com/beeware/Python-Apple-support/releases/download/3.13-b13/Python-3.13-iOS-support.b13.tar.gz", sha256 = "d1f95f95137a4b91dc0cbe9b99ddfc0a78918dde883c7fbe7147074a5e715274" },
|
||||||
{ identifier = "cp313-ios_arm64_iphonesimulator", version = "3.13", url = "https://github.com/beeware/Python-Apple-support/releases/download/3.13-b13/Python-3.13-iOS-support.b13.tar.gz" },
|
{ identifier = "cp313-ios_arm64_iphonesimulator", version = "3.13", url = "https://github.com/beeware/Python-Apple-support/releases/download/3.13-b13/Python-3.13-iOS-support.b13.tar.gz", sha256 = "d1f95f95137a4b91dc0cbe9b99ddfc0a78918dde883c7fbe7147074a5e715274" },
|
||||||
{ identifier = "cp314-ios_arm64_iphoneos", version = "3.14", url = "https://github.com/beeware/Python-Apple-support/releases/download/3.14-b9/Python-3.14-iOS-support.b9.tar.gz" },
|
{ identifier = "cp314-ios_arm64_iphoneos", version = "3.14", url = "https://github.com/beeware/Python-Apple-support/releases/download/3.14-b9/Python-3.14-iOS-support.b9.tar.gz", sha256 = "8e4123b543bf17fdae2e2c6c033434487752438431014eb12e6d833aa35927a8" },
|
||||||
{ identifier = "cp314-ios_x86_64_iphonesimulator", version = "3.14", url = "https://github.com/beeware/Python-Apple-support/releases/download/3.14-b9/Python-3.14-iOS-support.b9.tar.gz" },
|
{ identifier = "cp314-ios_x86_64_iphonesimulator", version = "3.14", url = "https://github.com/beeware/Python-Apple-support/releases/download/3.14-b9/Python-3.14-iOS-support.b9.tar.gz", sha256 = "8e4123b543bf17fdae2e2c6c033434487752438431014eb12e6d833aa35927a8" },
|
||||||
{ identifier = "cp314-ios_arm64_iphonesimulator", version = "3.14", url = "https://github.com/beeware/Python-Apple-support/releases/download/3.14-b9/Python-3.14-iOS-support.b9.tar.gz" },
|
{ identifier = "cp314-ios_arm64_iphonesimulator", version = "3.14", url = "https://github.com/beeware/Python-Apple-support/releases/download/3.14-b9/Python-3.14-iOS-support.b9.tar.gz", sha256 = "8e4123b543bf17fdae2e2c6c033434487752438431014eb12e6d833aa35927a8" },
|
||||||
{ identifier = "cp315-ios_arm64_iphoneos", version = "3.15", url = "https://www.python.org/ftp/python/3.15.0/python-3.15.0b1-iOS-XCframework.tar.gz" },
|
{ identifier = "cp315-ios_arm64_iphoneos", version = "3.15", url = "https://www.python.org/ftp/python/3.15.0/python-3.15.0b1-iOS-XCframework.tar.gz", sha256 = "abd43fc362bb6e40a5233a499aa3fa96bf49306ca10b4cbcb27418778e429eb8" },
|
||||||
{ identifier = "cp315-ios_x86_64_iphonesimulator", version = "3.15", url = "https://www.python.org/ftp/python/3.15.0/python-3.15.0b1-iOS-XCframework.tar.gz" },
|
{ identifier = "cp315-ios_x86_64_iphonesimulator", version = "3.15", url = "https://www.python.org/ftp/python/3.15.0/python-3.15.0b1-iOS-XCframework.tar.gz", sha256 = "abd43fc362bb6e40a5233a499aa3fa96bf49306ca10b4cbcb27418778e429eb8" },
|
||||||
{ identifier = "cp315-ios_arm64_iphonesimulator", version = "3.15", url = "https://www.python.org/ftp/python/3.15.0/python-3.15.0b1-iOS-XCframework.tar.gz" },
|
{ identifier = "cp315-ios_arm64_iphonesimulator", version = "3.15", url = "https://www.python.org/ftp/python/3.15.0/python-3.15.0b1-iOS-XCframework.tar.gz", sha256 = "abd43fc362bb6e40a5233a499aa3fa96bf49306ca10b4cbcb27418778e429eb8" },
|
||||||
]
|
]
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1 +1 @@
|
|||||||
default = { version = "21.3.3", url = "https://github.com/pypa/get-virtualenv/releases/download/21.3.3/virtualenv.pyz" }
|
default = { version = "21.3.3", url = "https://github.com/pypa/get-virtualenv/releases/download/21.3.3/virtualenv.pyz", sha256 = "d3a7f4ee4c820c4247fe14ec1a478e19ef8f63f716c0f15bc10fab4d70ad9b69" }
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import ssl
|
import ssl
|
||||||
@@ -45,7 +46,7 @@ def ensure_cache_sentinel(cache_path: Path) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def download(url: str, dest: Path) -> None:
|
def download(url: str, dest: Path, *, sha256: str | None = None) -> None:
|
||||||
print(f"+ Download {url} to {dest}")
|
print(f"+ Download {url} to {dest}")
|
||||||
dest_dir = dest.parent
|
dest_dir = dest.parent
|
||||||
dest_dir.mkdir(parents=True, exist_ok=True)
|
dest_dir.mkdir(parents=True, exist_ok=True)
|
||||||
@@ -59,13 +60,20 @@ def download(url: str, dest: Path) -> None:
|
|||||||
try:
|
try:
|
||||||
with urllib.request.urlopen(url, context=context) as response:
|
with urllib.request.urlopen(url, context=context) as response:
|
||||||
dest.write_bytes(response.read())
|
dest.write_bytes(response.read())
|
||||||
return
|
break
|
||||||
|
|
||||||
except OSError:
|
except OSError:
|
||||||
if i == repeat_num - 1:
|
if i == repeat_num - 1:
|
||||||
raise
|
raise
|
||||||
time.sleep(3)
|
time.sleep(3)
|
||||||
|
|
||||||
|
if sha256:
|
||||||
|
computed = hashlib.sha256(dest.read_bytes()).hexdigest()
|
||||||
|
if computed != sha256:
|
||||||
|
dest.unlink(missing_ok=True)
|
||||||
|
msg = f"SHA256 mismatch for {url}: expected {sha256!r}, got {computed!r}"
|
||||||
|
raise FatalError(msg)
|
||||||
|
|
||||||
|
|
||||||
def extract_zip(zip_src: Path, dest: Path) -> None:
|
def extract_zip(zip_src: Path, dest: Path) -> None:
|
||||||
"""Extracts a zip and correctly sets permissions on extracted files.
|
"""Extracts a zip and correctly sets permissions on extracted files.
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import fnmatch
|
import fnmatch
|
||||||
import functools
|
import functools
|
||||||
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import platform
|
import platform
|
||||||
import typing
|
import typing
|
||||||
@@ -19,6 +20,7 @@ if TYPE_CHECKING:
|
|||||||
class PythonBuildStandaloneAsset(typing.TypedDict):
|
class PythonBuildStandaloneAsset(typing.TypedDict):
|
||||||
name: str
|
name: str
|
||||||
url: str
|
url: str
|
||||||
|
sha256: str
|
||||||
|
|
||||||
|
|
||||||
class PythonBuildStandaloneRelease(typing.TypedDict):
|
class PythonBuildStandaloneRelease(typing.TypedDict):
|
||||||
@@ -86,8 +88,8 @@ def _get_pbs_asset(
|
|||||||
arch_identifier: str,
|
arch_identifier: str,
|
||||||
platform_identifier: str,
|
platform_identifier: str,
|
||||||
libc_identifier: str | None,
|
libc_identifier: str | None,
|
||||||
) -> tuple[str, str, str]:
|
) -> tuple[str, str, str, str]:
|
||||||
"""Finds the asset, returning (tag, filename, url)."""
|
"""Finds the asset, returning (tag, url, filename, sha256)."""
|
||||||
release_data = get_python_build_standalone_release_data()
|
release_data = get_python_build_standalone_release_data()
|
||||||
|
|
||||||
expected_suffix = f"{arch_identifier}-{platform_identifier}"
|
expected_suffix = f"{arch_identifier}-{platform_identifier}"
|
||||||
@@ -105,22 +107,36 @@ def _get_pbs_asset(
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
asset_url = asset["url"]
|
asset_url = asset["url"]
|
||||||
return release["tag"], asset_url, asset_name
|
asset_sha256 = asset.get("sha256", "")
|
||||||
|
return release["tag"], asset_url, asset_name, asset_sha256
|
||||||
|
|
||||||
# If loop completes without finding a match
|
# If loop completes without finding a match
|
||||||
msg = f"Could not find python-build-standalone release asset matching {asset_pattern!r}."
|
msg = f"Could not find python-build-standalone release asset matching {asset_pattern!r}."
|
||||||
raise PythonBuildStandaloneError(msg)
|
raise PythonBuildStandaloneError(msg)
|
||||||
|
|
||||||
|
|
||||||
def _download_or_get_from_cache(asset_url: str, asset_filename: str, cache_dir: Path) -> Path:
|
def _download_or_get_from_cache(
|
||||||
|
asset_url: str, asset_filename: str, cache_dir: Path, sha256: str = ""
|
||||||
|
) -> Path:
|
||||||
with FileLock(cache_dir / (asset_filename + ".lock")):
|
with FileLock(cache_dir / (asset_filename + ".lock")):
|
||||||
asset_cache_path = cache_dir / asset_filename
|
asset_cache_path = cache_dir / asset_filename
|
||||||
if asset_cache_path.is_file():
|
if asset_cache_path.is_file():
|
||||||
print(f"Using cached python_build_standalone: {asset_cache_path}")
|
if sha256:
|
||||||
return asset_cache_path
|
computed = hashlib.sha256(asset_cache_path.read_bytes()).hexdigest()
|
||||||
|
if computed != sha256:
|
||||||
|
print(
|
||||||
|
f"Cached python_build_standalone SHA256 mismatch for {asset_cache_path}; redownloading."
|
||||||
|
)
|
||||||
|
asset_cache_path.unlink(missing_ok=True)
|
||||||
|
else:
|
||||||
|
print(f"Using cached python_build_standalone: {asset_cache_path}")
|
||||||
|
return asset_cache_path
|
||||||
|
else:
|
||||||
|
print(f"Using cached python_build_standalone: {asset_cache_path}")
|
||||||
|
return asset_cache_path
|
||||||
|
|
||||||
print(f"Downloading python_build_standalone: {asset_url} to {asset_cache_path}")
|
print(f"Downloading python_build_standalone: {asset_url} to {asset_cache_path}")
|
||||||
download(asset_url, asset_cache_path)
|
download(asset_url, asset_cache_path, sha256=sha256 or None)
|
||||||
return asset_cache_path
|
return asset_cache_path
|
||||||
|
|
||||||
|
|
||||||
@@ -166,7 +182,7 @@ def create_python_build_standalone_environment(
|
|||||||
|
|
||||||
arch_id, platform_id, libc_id = _get_platform_identifiers()
|
arch_id, platform_id, libc_id = _get_platform_identifiers()
|
||||||
|
|
||||||
pbs_tag, asset_url, asset_filename = _get_pbs_asset(
|
pbs_tag, asset_url, asset_filename, asset_sha256 = _get_pbs_asset(
|
||||||
python_version=python_version,
|
python_version=python_version,
|
||||||
arch_identifier=arch_id,
|
arch_identifier=arch_id,
|
||||||
platform_identifier=platform_id,
|
platform_identifier=platform_id,
|
||||||
@@ -176,7 +192,7 @@ def create_python_build_standalone_environment(
|
|||||||
print(f"Using python-build-standalone release: {pbs_tag}")
|
print(f"Using python-build-standalone release: {pbs_tag}")
|
||||||
|
|
||||||
archive_path = _download_or_get_from_cache(
|
archive_path = _download_or_get_from_cache(
|
||||||
asset_url=asset_url, asset_filename=asset_filename, cache_dir=cache_dir
|
asset_url=asset_url, asset_filename=asset_filename, cache_dir=cache_dir, sha256=asset_sha256
|
||||||
)
|
)
|
||||||
|
|
||||||
python_base_dir = temp_dir / "pbs"
|
python_base_dir = temp_dir / "pbs"
|
||||||
|
|||||||
@@ -31,7 +31,14 @@ IOS_SUPPORT_FILES: Final[Path] = PATH / "ios-support"
|
|||||||
def read_all_configs() -> dict[str, list[dict[str, str]]]:
|
def read_all_configs() -> dict[str, list[dict[str, str]]]:
|
||||||
with BUILD_PLATFORMS.open("rb") as f:
|
with BUILD_PLATFORMS.open("rb") as f:
|
||||||
loaded_file = tomllib.load(f)
|
loaded_file = tomllib.load(f)
|
||||||
return {k: list[dict[str, str]](v["python_configurations"]) for k, v in loaded_file.items()}
|
configs = {k: list[dict[str, str]](v["python_configurations"]) for k, v in loaded_file.items()}
|
||||||
|
for platform, python_configs in configs.items():
|
||||||
|
for config in python_configs:
|
||||||
|
if "url" in config and not config.get("sha256"):
|
||||||
|
identifier = config["identifier"]
|
||||||
|
msg = f"{platform} Python configuration {identifier!r} is missing a sha256"
|
||||||
|
raise ValueError(msg)
|
||||||
|
return configs
|
||||||
|
|
||||||
|
|
||||||
def read_python_configs(config: PlatformName) -> list[dict[str, str]]:
|
def read_python_configs(config: PlatformName) -> list[dict[str, str]]:
|
||||||
|
|||||||
@@ -53,10 +53,11 @@ def _ensure_virtualenv(version: str) -> tuple[Path, Version]:
|
|||||||
configuration = loaded_file.get(key, loaded_file["default"])
|
configuration = loaded_file.get(key, loaded_file["default"])
|
||||||
version = str(configuration["version"])
|
version = str(configuration["version"])
|
||||||
url = str(configuration["url"])
|
url = str(configuration["url"])
|
||||||
|
sha256 = str(configuration["sha256"])
|
||||||
path = CIBW_CACHE_PATH / f"virtualenv-{version}.pyz"
|
path = CIBW_CACHE_PATH / f"virtualenv-{version}.pyz"
|
||||||
with FileLock(str(path) + ".lock"):
|
with FileLock(str(path) + ".lock"):
|
||||||
if not path.exists():
|
if not path.exists():
|
||||||
download(url, path)
|
download(url, path, sha256=sha256)
|
||||||
return (path, Version(version))
|
return (path, Version(version))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -28,10 +28,10 @@ For example, consider a scenario when Pyodide ships a new `315.0.0a1` with cp315
|
|||||||
In `cibuildwheel/resources/build-platforms.toml`, add an entry under `[pyodide]`:
|
In `cibuildwheel/resources/build-platforms.toml`, add an entry under `[pyodide]`:
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
{ identifier = "cp315-pyodide_wasm32", version = "3.15", default_pyodide_version = "315.0.0a1", node_version = "v24" },
|
{ identifier = "cp315-pyodide_wasm32", version = "3.15", default_pyodide_version = "315.0.0a1", node_version = "v24", sha256 = "SHA256" },
|
||||||
```
|
```
|
||||||
|
|
||||||
`version` is the CPython version string, `default_pyodide_version` is the Pyodide release to use when the user does not pin one explicitly (use the latest available alpha/beta for a prerelease entry), and `node_version` is the minimum Node.js major required by that Pyodide release — check the [pyodide-build FAQ](https://pyodide-build.readthedocs.io/en/latest/faq.html#what-node-js-version-do-i-need) for a rudimentary idea of what the correct value is.
|
`version` is the CPython version string, `default_pyodide_version` is the Pyodide release to use when the user does not pin one explicitly (use the latest available alpha/beta for a prerelease entry), and `node_version` is the minimum Node.js major required by that Pyodide release — check the [pyodide-build FAQ](https://pyodide-build.readthedocs.io/en/latest/faq.html#what-node-js-version-do-i-need) for a rudimentary idea of what the correct value is. `sha256` is the checksum of the Pyodide xbuildenv tarball.
|
||||||
|
|
||||||
### 2. Update the prerelease guards in the selector
|
### 2. Update the prerelease guards in the selector
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
from cibuildwheel.platforms import pyodide
|
||||||
|
from cibuildwheel.util import python_build_standalone
|
||||||
|
|
||||||
|
TYPE_CHECKING = False
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
def test_pyodide_all_python_configurations_accept_sha256() -> None:
|
||||||
|
python_configurations = pyodide.all_python_configurations()
|
||||||
|
|
||||||
|
assert python_configurations
|
||||||
|
assert all(config.sha256 for config in python_configurations)
|
||||||
|
|
||||||
|
|
||||||
|
def test_download_or_get_from_cache_uses_valid_cached_file(
|
||||||
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||||
|
) -> None:
|
||||||
|
cached_file = tmp_path / "python-build-standalone.tar.gz"
|
||||||
|
cached_bytes = b"cached archive"
|
||||||
|
cached_file.write_bytes(cached_bytes)
|
||||||
|
cached_sha256 = hashlib.sha256(cached_bytes).hexdigest()
|
||||||
|
|
||||||
|
was_downloaded = False
|
||||||
|
|
||||||
|
def fake_download(url: str, dest: Path, *, sha256: str | None = None) -> None: # noqa: ARG001
|
||||||
|
nonlocal was_downloaded
|
||||||
|
was_downloaded = True
|
||||||
|
|
||||||
|
monkeypatch.setattr(python_build_standalone, "download", fake_download)
|
||||||
|
|
||||||
|
archive_path = python_build_standalone._download_or_get_from_cache(
|
||||||
|
asset_url="https://example.com/python-build-standalone.tar.gz",
|
||||||
|
asset_filename=cached_file.name,
|
||||||
|
cache_dir=tmp_path,
|
||||||
|
sha256=cached_sha256,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert archive_path == cached_file
|
||||||
|
assert not was_downloaded
|
||||||
|
|
||||||
|
|
||||||
|
def test_download_or_get_from_cache_redownloads_invalid_cached_file(
|
||||||
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||||
|
) -> None:
|
||||||
|
cached_file = tmp_path / "python-build-standalone.tar.gz"
|
||||||
|
cached_file.write_bytes(b"bad cache")
|
||||||
|
expected_bytes = b"good archive"
|
||||||
|
expected_sha256 = hashlib.sha256(expected_bytes).hexdigest()
|
||||||
|
|
||||||
|
def fake_download(url: str, dest: Path, *, sha256: str | None = None) -> None: # noqa: ARG001
|
||||||
|
assert sha256 == expected_sha256
|
||||||
|
dest.write_bytes(expected_bytes)
|
||||||
|
|
||||||
|
monkeypatch.setattr(python_build_standalone, "download", fake_download)
|
||||||
|
|
||||||
|
archive_path = python_build_standalone._download_or_get_from_cache(
|
||||||
|
asset_url="https://example.com/python-build-standalone.tar.gz",
|
||||||
|
asset_filename=cached_file.name,
|
||||||
|
cache_dir=tmp_path,
|
||||||
|
sha256=expected_sha256,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert archive_path == cached_file
|
||||||
|
assert cached_file.read_bytes() == expected_bytes
|
||||||
Reference in New Issue
Block a user