diff --git a/.github/workflows/update-dependencies.yml b/.github/workflows/update-dependencies.yml index bd7f531b1..8caca5a2e 100644 --- a/.github/workflows/update-dependencies.yml +++ b/.github/workflows/update-dependencies.yml @@ -11,6 +11,11 @@ on: - 'docs/data/projects.yml' - 'noxfile.py' workflow_dispatch: + inputs: + ignore-cooldown: + description: 'Ignore the cooldown window and pick up the very latest releases' + type: boolean + default: false schedule: - cron: '0 6 * * 1' # "At 06:00 on Monday." @@ -47,8 +52,12 @@ jobs: - name: "Run update: dependencies" run: uvx nox --force-color -s update_constraints + env: + CIBW_IGNORE_COOLDOWN: ${{ github.event.inputs.ignore-cooldown || 'false' }} - name: "Run update: python configs" run: uvx nox --force-color -s update_pins + env: + CIBW_IGNORE_COOLDOWN: ${{ github.event.inputs.ignore-cooldown || 'false' }} - name: "Run update: docs user projects" run: uvx nox --force-color -s update_proj -- --auth=${{ secrets.GITHUB_TOKEN }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 164bfac10..f241c44f2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -30,7 +30,7 @@ repos: - id: mypy name: mypy 3.11 on cibuildwheel/ args: ["--python-version=3.11"] - exclude: ^cibuildwheel/resources/android/_cross_venv.py$ # Requires Python 3.13 or later + exclude: (^cibuildwheel/resources/android/_cross_venv.py$|^bin/_cooldown\.py$) # _cross_venv.py requires Python 3.13 or later and _cooldown.py is a support module for bin/ scripts additional_dependencies: &mypy-dependencies - bracex - build @@ -54,6 +54,7 @@ repos: - id: mypy name: mypy 3.14 args: ["--python-version=3.14"] + exclude: ^bin/_cooldown\.py$ # support module, not a top-level file additional_dependencies: *mypy-dependencies - repo: https://github.com/shellcheck-py/shellcheck-py diff --git a/bin/_cooldown.py b/bin/_cooldown.py new file mode 100644 index 000000000..36b3df87c --- /dev/null +++ b/bin/_cooldown.py @@ -0,0 +1,11 @@ +import os + +# Number of days a release must age before the update scripts will pick it up. +# This is intentionally different from Dependabot because we want to have these +# updates coming faster to align with the latest releases. +# NOTE: Keep this in sync with noxfile.py's update_constraints session. +COOLDOWN_DAYS = 3 + +# Set CIBW_IGNORE_COOLDOWN to a truthy value to bypass the cooldown and always pick +# up the very latest releases regardless of age. +IGNORE_COOLDOWN = os.environ.get("CIBW_IGNORE_COOLDOWN", "").lower() in ("1", "true") diff --git a/bin/update_pythons.py b/bin/update_pythons.py index dd6bb38cc..ef7151178 100755 --- a/bin/update_pythons.py +++ b/bin/update_pythons.py @@ -20,6 +20,8 @@ import operator import re import tomllib +from collections.abc import Mapping, MutableMapping +from datetime import UTC, date, datetime, timedelta from pathlib import Path from typing import Any, Final, Literal, NotRequired, TypedDict from xml.etree import ElementTree as ET @@ -27,12 +29,13 @@ import click import requests import rich +from _cooldown import COOLDOWN_DAYS, IGNORE_COOLDOWN from packaging.specifiers import Specifier from packaging.version import Version from rich.logging import RichHandler from rich.syntax import Syntax -from cibuildwheel.extra import dump_python_configurations, get_pyodide_xbuildenv_info +from cibuildwheel.extra import dump_python_configurations from cibuildwheel.platforms.android import android_triplet TYPE_CHECKING = False @@ -418,9 +421,17 @@ def update_version_ios(self, identifier: str, version: Version) -> ConfigUrl | N class PyodideVersions: - def __init__(self) -> None: - xbuildenv_info = get_pyodide_xbuildenv_info() - self.releases = xbuildenv_info["releases"] + def __init__(self, cutoff_date: date) -> None: + response = requests.get( + "https://pyodide.github.io/pyodide/api/v2/pyodide-cross-build-environments.json" + ) + response.raise_for_status() + all_releases = response.json()["releases"] + self.releases = { + version_str: release + for version_str, release in all_releases.items() + if datetime.fromisoformat(release["published_at"]).date() <= cutoff_date + } def update_version_pyodide( self, identifier: str, version: Version, spec: Specifier, node_version: str @@ -457,6 +468,12 @@ def update_version_pyodide( class AllVersions: def __init__(self) -> None: + cutoff_date: date = ( + date.max + if IGNORE_COOLDOWN + else (datetime.now(tz=UTC) - timedelta(days=COOLDOWN_DAYS)).date() + ) + self.windows_32 = WindowsVersions("32", False) self.windows_t_32 = WindowsVersions("32", True) self.windows_64 = WindowsVersions("64", False) @@ -474,7 +491,7 @@ def __init__(self) -> None: self.graalpy = GraalPyVersions() - self.pyodide = PyodideVersions() + self.pyodide = PyodideVersions(cutoff_date) def _stream_sha256(self, url: str) -> str: """Download a file (streaming) and return its SHA256 hex digest.""" diff --git a/bin/update_virtualenv.py b/bin/update_virtualenv.py index 5dfe4a3d9..deeb9b0f2 100755 --- a/bin/update_virtualenv.py +++ b/bin/update_virtualenv.py @@ -19,12 +19,14 @@ import hashlib import logging import tomllib +from datetime import UTC, datetime, timedelta from pathlib import Path from typing import Final import click import requests import rich +from _cooldown import COOLDOWN_DAYS, IGNORE_COOLDOWN from packaging.version import Version from rich.logging import RichHandler from rich.syntax import Syntax @@ -49,6 +51,7 @@ class VersionTuple: name: str download_url: str version: Version + published_at: datetime = dataclasses.field(compare=False) def get_latest_virtualenv_release() -> VersionTuple: @@ -63,8 +66,12 @@ def get_latest_virtualenv_release() -> VersionTuple: msg = "No asset named 'virtualenv.pyz' found in the latest release of get-virtualenv." raise RuntimeError(msg) + published_at = datetime.fromisoformat(response["published_at"]) return VersionTuple( - version=Version(tag_name), name=tag_name, download_url=asset["browser_download_url"] + version=Version(tag_name), + name=tag_name, + download_url=asset["browser_download_url"], + published_at=published_at, ) @@ -92,6 +99,15 @@ def update_virtualenv(force: bool, level: str) -> None: latest_release = get_latest_virtualenv_release() + if not IGNORE_COOLDOWN and datetime.now(tz=UTC) - latest_release.published_at < timedelta( + days=COOLDOWN_DAYS + ): + rich.print( + f"[yellow]Skipping update: latest release {latest_release.name!r} was published " + f"less than {COOLDOWN_DAYS} cooldown days ago." + ) + return + if latest_release.version > Version(local_version): version = latest_release.name url = latest_release.download_url diff --git a/cibuildwheel/extra.py b/cibuildwheel/extra.py index 30f280cb7..b69f4fff0 100644 --- a/cibuildwheel/extra.py +++ b/cibuildwheel/extra.py @@ -93,6 +93,9 @@ class PyodideXBuildEnvRelease(typing.TypedDict): sha256: str python_version: str emscripten_version: str + published_at: str + url: NotRequired[str] + sha256: NotRequired[str] min_pyodide_build_version: NotRequired[str] max_pyodide_build_version: NotRequired[str] diff --git a/noxfile.py b/noxfile.py index 319f1b915..736b545c6 100755 --- a/noxfile.py +++ b/noxfile.py @@ -79,6 +79,10 @@ def update_constraints(session: nox.Session) -> None: env = os.environ.copy() env["UV_CUSTOM_COMPILE_COMMAND"] = f"nox -s {session.name}" + # NOTE: Keep this in sync with bin/_cooldown.py + ignore_cooldown = os.environ.get("CIBW_IGNORE_COOLDOWN", "").lower() in ("1", "true") + exclude_newer_args = [] if ignore_cooldown else ["--exclude-newer=3 days"] + for minor_version in range(9, 16): python_version = f"3.{minor_version}" output_file = resources / f"constraints-python{python_version.replace('.', '')}.txt" @@ -88,6 +92,7 @@ def update_constraints(session: nox.Session) -> None: "compile", f"--python-version={python_version}", "--upgrade", + *exclude_newer_args, resources / "constraints.in", f"--output-file={output_file}", env=env, @@ -120,6 +125,7 @@ def update_constraints(session: nox.Session) -> None: "compile", f"--python-version={python_version}", "--upgrade", + *exclude_newer_args, tmp_file, f"--output-file={output_file}", env=env, diff --git a/pyproject.toml b/pyproject.toml index 6b499a9cd..b1b22a937 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -112,7 +112,7 @@ files = [ "cibuildwheel/*.py", "test/**/*.py", "unit_test/**/*.py", - "bin/*.py", + "bin/[!_]*.py", "noxfile.py", ] warn_unused_configs = true