cd38ee1548
* 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>
72 lines
2.3 KiB
Python
72 lines
2.3 KiB
Python
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
|