feat: pyemscripten platform tag (PEP 783), 314.0a1, pyodide-eol flag, and maintenance updates (#2812)

* Use `pyodide config get emscripten_dir`

* Update constraints

* Updates for `pyemscripten` platform tag

* Weird constraints stuff...

* Revert "Weird constraints stuff..."

This reverts commit 4bd785de872269c0b27362ee800f17288b2b4789.

* Add some special constraints for pyodide

* Wheel pyemscripten collision

* Put delocate back in constraints (not needed tho)

* Drop Pyodide 0.27.7 (cp312-pyodide_wasm32)

* Add Pyodide 314.0.0a1 (cp314-pyodide_wasm32)

* Delete no-longer-needed Pyodide 312 constraints

* Update Pyodide 313 and 314 constraints

* Add back prerelease stuff and update tests

* Fix `test_pyodide_on_windows`

* Update and fix tests some more

* Also enable pyodide-prerelease in GHA sample build

* Update tests yet again (`SINGLE_PYTHON_VERSION` changes)

* Add a Pyodide-specific maintenance guide

* Add suggestions from Hood and Gyeongjae

Co-Authored-By: Hood Chatham <roberthoodchatham@gmail.com>
Co-Authored-By: Gyeongjae Choi <def6488@gmail.com>

* Add "Last updated: May 2026" to Pyodide document

* Add a Pyodide EOL filter

* Fix typo

* Update maintenance docs about EoL Pyodide

* Add Pyodide 312 constraints back

* Update Pyodide 313 and 314 constraints

* Remove "experimental" note about Pyodide

* Remove another experimental Pyodide sentence

* Add docs about the `pyodide-eol` enable option

---------

Co-authored-by: Hood Chatham <roberthoodchatham@gmail.com>
Co-authored-by: Gyeongjae Choi <def6488@gmail.com>
Co-authored-by: Henry Schreiner <henryfs@princeton.edu>
This commit is contained in:
agriya khetarpal
2026-05-09 14:01:41 -04:00
committed by GitHub
co-authored by Hood Chatham Gyeongjae Choi Henry Schreiner
parent 542335fb4a
commit d2c0e59833
20 changed files with 343 additions and 99 deletions
+2
View File
@@ -321,11 +321,13 @@ jobs:
output-dir: wheelhouse output-dir: wheelhouse
env: env:
CIBW_PLATFORM: pyodide CIBW_PLATFORM: pyodide
CIBW_ENABLE: pyodide-prerelease
- name: Run tests with 'CIBW_PLATFORM' set to 'pyodide' - name: Run tests with 'CIBW_PLATFORM' set to 'pyodide'
run: uv run --no-sync ./bin/run_tests.py run: uv run --no-sync ./bin/run_tests.py
env: env:
CIBW_PLATFORM: pyodide CIBW_PLATFORM: pyodide
CIBW_ENABLE: pyodide-prerelease
test-uv-extras: test-uv-extras:
name: Test uv extra on ${{ matrix.os }} ${{ matrix.test_select }} name: Test uv extra on ${{ matrix.os }} ${{ matrix.test_select }}
+10 -11
View File
@@ -94,20 +94,19 @@ def ensure_node(major_version: str) -> Path:
return path return path
def install_emscripten( def install_emscripten(env: dict[str, str], version: str, xbuildenv_cache_path: Path) -> Path:
env: dict[str, str], version: str, xbuildenv_cache_path: Path, pyodide_version: str
) -> Path:
"""Install Emscripten via pyodide-build, which also applies Pyodide-specific patches.""" """Install Emscripten via pyodide-build, which also applies Pyodide-specific patches."""
emcc_path = ( emscripten_dir = Path(
xbuildenv_cache_path / pyodide_version / "emsdk" / "upstream" / "emscripten" / "emcc" call("pyodide", "config", "get", "emscripten_dir", env=env, capture_stdout=True).strip()
) )
with FileLock(CIBW_CACHE_PATH / "emscripten.lock"): with FileLock(CIBW_CACHE_PATH / "emscripten.lock"):
if emcc_path.exists(): if emscripten_dir.exists():
return emcc_path return emscripten_dir
call( call(
"pyodide", "pyodide",
"xbuildenv", "xbuildenv",
"install-emscripten", "install-emscripten",
"--force",
"--version", "--version",
version, version,
"--path", "--path",
@@ -115,8 +114,8 @@ def install_emscripten(
env=env, env=env,
cwd=CIBW_CACHE_PATH, cwd=CIBW_CACHE_PATH,
) )
assert emcc_path.exists() assert emscripten_dir.exists()
return emcc_path return emscripten_dir
def get_all_xbuildenv_version_info(env: dict[str, str]) -> list[PyodideXBuildEnvInfo]: def get_all_xbuildenv_version_info(env: dict[str, str]) -> list[PyodideXBuildEnvInfo]:
@@ -315,9 +314,9 @@ def setup_python(
log.step( log.step(
f"Installing Emscripten {emscripten_version} and applying Pyodide-specific patches ..." f"Installing Emscripten {emscripten_version} and applying Pyodide-specific patches ..."
) )
emcc_path = install_emscripten(env, emscripten_version, xbuildenv_cache_path, pyodide_version) emscripten_dir = install_emscripten(env, emscripten_version, xbuildenv_cache_path)
env["PATH"] = os.pathsep.join([str(emcc_path.parent), env["PATH"]]) env["PATH"] = os.pathsep.join([str(emscripten_dir), env["PATH"]])
return env return env
+2 -1
View File
@@ -206,7 +206,8 @@ python_configurations = [
[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" },
{ identifier = "cp313-pyodide_wasm32", version = "3.13", default_pyodide_version = "0.29.3", node_version = "v22" }, { identifier = "cp313-pyodide_wasm32", version = "3.13", default_pyodide_version = "0.29.4", node_version = "v22" },
{ identifier = "cp314-pyodide_wasm32", version = "3.14", default_pyodide_version = "314.0.0a1", node_version = "v24" },
] ]
[android] [android]
@@ -2,15 +2,15 @@
# nox -s update_constraints # nox -s update_constraints
annotated-types==0.7.0 annotated-types==0.7.0
# via pydantic # via pydantic
auditwheel-emscripten==0.2.3 auditwheel-emscripten==0.2.4
# via pyodide-build # via pyodide-build
build==1.2.2.post1 build==1.5.0
# via # via
# -r .nox/update_constraints/tmp/constraints-pyodide.in # -r .nox/update_constraints/tmp/constraints-pyodide.in
# pyodide-build # pyodide-build
certifi==2026.2.25 certifi==2026.4.22
# via requests # via requests
charset-normalizer==3.4.6 charset-normalizer==3.4.7
# via requests # via requests
click==8.1.8 click==8.1.8
# via # via
@@ -19,54 +19,54 @@ click==8.1.8
# pyodide-cli # pyodide-cli
distlib==0.4.0 distlib==0.4.0
# via virtualenv # via virtualenv
filelock==3.25.2 filelock==3.29.0
# via # via
# python-discovery # python-discovery
# virtualenv # virtualenv
idna==3.11 idna==3.13
# via requests # via requests
leb128==1.0.9 leb128==1.0.9
# via auditwheel-emscripten # via auditwheel-emscripten
markdown-it-py==4.0.0 markdown-it-py==4.2.0
# via rich # via rich
mdurl==0.1.2 mdurl==0.1.2
# via markdown-it-py # via markdown-it-py
packaging==26.0 packaging==26.2
# via # via
# auditwheel-emscripten # auditwheel-emscripten
# build # build
# pyodide-build # pyodide-build
# wheel # wheel
pip==26.0.1 pip==26.1.1
# via -r .nox/update_constraints/tmp/constraints-pyodide.in # via -r .nox/update_constraints/tmp/constraints-pyodide.in
platformdirs==4.9.4 platformdirs==4.9.6
# via # via
# pyodide-build # pyodide-build
# python-discovery # python-discovery
# virtualenv # virtualenv
pydantic==2.12.5 pydantic==2.13.4
# via # via
# pyodide-build # pyodide-build
# pyodide-lock # pyodide-lock
pydantic-core==2.41.5 pydantic-core==2.46.4
# via pydantic # via pydantic
pygments==2.19.2 pygments==2.20.0
# via rich # via rich
pyodide-build==0.33.0 pyodide-build==0.34.3
# via -r .nox/update_constraints/tmp/constraints-pyodide.in # via -r .nox/update_constraints/tmp/constraints-pyodide.in
pyodide-cli==0.5.0 pyodide-cli==0.5.0
# via # via
# auditwheel-emscripten # auditwheel-emscripten
# pyodide-build # pyodide-build
pyodide-lock==0.1.2 pyodide-lock==0.1.3
# via pyodide-build # via pyodide-build
pyproject-hooks==1.2.0 pyproject-hooks==1.2.0
# via build # via build
python-discovery==1.2.0 python-discovery==1.3.0
# via virtualenv # via virtualenv
requests==2.32.5 requests==2.33.1
# via pyodide-build # via pyodide-build
rich==14.3.3 rich==15.0.0
# via # via
# pyodide-build # pyodide-build
# pyodide-cli # pyodide-cli
@@ -81,11 +81,11 @@ typing-inspection==0.4.2
# via pydantic # via pydantic
urllib3==2.6.3 urllib3==2.6.3
# via requests # via requests
virtualenv==21.2.0 virtualenv==21.3.1
# via # via
# build # build
# pyodide-build # pyodide-build
wheel==0.46.3 wheel==0.47.0
# via # via
# auditwheel-emscripten # auditwheel-emscripten
# pyodide-build # pyodide-build
@@ -2,15 +2,15 @@
# nox -s update_constraints # nox -s update_constraints
annotated-types==0.7.0 annotated-types==0.7.0
# via pydantic # via pydantic
auditwheel-emscripten==0.2.3 auditwheel-emscripten==0.2.4
# via pyodide-build # via pyodide-build
build==1.2.2.post1 build==1.5.0
# via # via
# -r .nox/update_constraints/tmp/constraints-pyodide.in # -r .nox/update_constraints/tmp/constraints-pyodide.in
# pyodide-build # pyodide-build
certifi==2026.2.25 certifi==2026.4.22
# via requests # via requests
charset-normalizer==3.4.6 charset-normalizer==3.4.7
# via requests # via requests
click==8.1.8 click==8.1.8
# via # via
@@ -19,54 +19,54 @@ click==8.1.8
# pyodide-cli # pyodide-cli
distlib==0.4.0 distlib==0.4.0
# via virtualenv # via virtualenv
filelock==3.25.2 filelock==3.29.0
# via # via
# python-discovery # python-discovery
# virtualenv # virtualenv
idna==3.11 idna==3.13
# via requests # via requests
leb128==1.0.9 leb128==1.0.9
# via auditwheel-emscripten # via auditwheel-emscripten
markdown-it-py==4.0.0 markdown-it-py==4.2.0
# via rich # via rich
mdurl==0.1.2 mdurl==0.1.2
# via markdown-it-py # via markdown-it-py
packaging==26.0 packaging==26.2
# via # via
# auditwheel-emscripten # auditwheel-emscripten
# build # build
# pyodide-build # pyodide-build
# wheel # wheel
pip==26.0.1 pip==26.1.1
# via -r .nox/update_constraints/tmp/constraints-pyodide.in # via -r .nox/update_constraints/tmp/constraints-pyodide.in
platformdirs==4.9.4 platformdirs==4.9.6
# via # via
# pyodide-build # pyodide-build
# python-discovery # python-discovery
# virtualenv # virtualenv
pydantic==2.12.5 pydantic==2.13.4
# via # via
# pyodide-build # pyodide-build
# pyodide-lock # pyodide-lock
pydantic-core==2.41.5 pydantic-core==2.46.4
# via pydantic # via pydantic
pygments==2.19.2 pygments==2.20.0
# via rich # via rich
pyodide-build==0.33.0 pyodide-build==0.34.3
# via -r .nox/update_constraints/tmp/constraints-pyodide.in # via -r .nox/update_constraints/tmp/constraints-pyodide.in
pyodide-cli==0.5.0 pyodide-cli==0.5.0
# via # via
# auditwheel-emscripten # auditwheel-emscripten
# pyodide-build # pyodide-build
pyodide-lock==0.1.2 pyodide-lock==0.1.3
# via pyodide-build # via pyodide-build
pyproject-hooks==1.2.0 pyproject-hooks==1.2.0
# via build # via build
python-discovery==1.2.0 python-discovery==1.3.0
# via virtualenv # via virtualenv
requests==2.32.5 requests==2.33.1
# via pyodide-build # via pyodide-build
rich==14.3.3 rich==15.0.0
# via # via
# pyodide-build # pyodide-build
# pyodide-cli # pyodide-cli
@@ -81,11 +81,11 @@ typing-inspection==0.4.2
# via pydantic # via pydantic
urllib3==2.6.3 urllib3==2.6.3
# via requests # via requests
virtualenv==21.2.0 virtualenv==21.3.1
# via # via
# build # build
# pyodide-build # pyodide-build
wheel==0.46.3 wheel==0.47.0
# via # via
# auditwheel-emscripten # auditwheel-emscripten
# pyodide-build # pyodide-build
@@ -0,0 +1,91 @@
# This file was autogenerated by uv via the following command:
# nox -s update_constraints
annotated-types==0.7.0
# via pydantic
auditwheel-emscripten==0.2.4
# via pyodide-build
build==1.5.0
# via
# -r .nox/update_constraints/tmp/constraints-pyodide.in
# pyodide-build
certifi==2026.4.22
# via requests
charset-normalizer==3.4.7
# via requests
click==8.1.8
# via
# -r .nox/update_constraints/tmp/constraints-pyodide.in
# pyodide-build
# pyodide-cli
distlib==0.4.0
# via virtualenv
filelock==3.29.0
# via
# python-discovery
# virtualenv
idna==3.13
# via requests
leb128==1.0.9
# via auditwheel-emscripten
markdown-it-py==4.2.0
# via rich
mdurl==0.1.2
# via markdown-it-py
packaging==26.2
# via
# auditwheel-emscripten
# build
# pyodide-build
# wheel
pip==26.1.1
# via -r .nox/update_constraints/tmp/constraints-pyodide.in
platformdirs==4.9.6
# via
# pyodide-build
# python-discovery
# virtualenv
pydantic==2.13.4
# via
# pyodide-build
# pyodide-lock
pydantic-core==2.46.4
# via pydantic
pygments==2.20.0
# via rich
pyodide-build==0.34.3
# via -r .nox/update_constraints/tmp/constraints-pyodide.in
pyodide-cli==0.5.0
# via
# auditwheel-emscripten
# pyodide-build
pyodide-lock==0.1.3
# via pyodide-build
pyproject-hooks==1.2.0
# via build
python-discovery==1.3.0
# via virtualenv
requests==2.33.1
# via pyodide-build
rich==15.0.0
# via
# pyodide-build
# pyodide-cli
ruamel-yaml==0.19.1
# via pyodide-build
typing-extensions==4.15.0
# via
# pydantic
# pydantic-core
# typing-inspection
typing-inspection==0.4.2
# via pydantic
urllib3==2.6.3
# via requests
virtualenv==21.3.1
# via
# build
# pyodide-build
wheel==0.47.0
# via
# auditwheel-emscripten
# pyodide-build
+15 -10
View File
@@ -33,6 +33,7 @@ class EnableGroup(StrEnum):
GraalPy = "graalpy" GraalPy = "graalpy"
PyPy = "pypy" PyPy = "pypy"
PyPyEoL = "pypy-eol" PyPyEoL = "pypy-eol"
PyodideEoL = "pyodide-eol"
PyodidePrerelease = "pyodide-prerelease" PyodidePrerelease = "pyodide-prerelease"
@classmethod @classmethod
@@ -93,17 +94,21 @@ class BuildSelector:
return False return False
if EnableGroup.GraalPy not in self.enable and fnmatch(build_id, "gp*"): if EnableGroup.GraalPy not in self.enable and fnmatch(build_id, "gp*"):
return False return False
# TODO: Re-enable this when we have Pyodide prereleases again (e.g., 0.29.0a1+) if EnableGroup.PyodideEoL not in self.enable and fnmatch(build_id, "cp312-pyodide_*"):
# Python 3.13 support became stable in Pyodide 0.28.0, so it no longer needs a prerelease return False
# flag. # NOTE: Disable this when we don't have any Pyodide prereleases (e.g., 314.0.0a1+)
# Also update Pyodide tests in unit_test/build_selector_test.py accordingly. # When doing this, also:
# When re-enabling, update the pattern to match the experimental Python version in case # 1. update Pyodide tests in unit_test/build_selector_test.py and unit_test/options_test.py accordingly.
# it is bumped to Python 3.14 (likely cp314-pyodide_* but could remain as 3.13 as well). # 2. update Python versions for Pyodide identifiers in cibuildwheel/selector.py.
# 3. update constraints as necessary via bin/generate_pyodide_constraints.py and add/delete
# Pyodide constraints files in cibuildwheel/resources/constraints/ as necessary.
# When disabling, update the pattern to match the experimental Python version in case
# it is bumped to Python 3.15 (likely cp315-pyodide_* but could remain as 3.14 as well).
# This depends on the CPython version being used in the Pyodide runtime at the time. # This depends on the CPython version being used in the Pyodide runtime at the time.
# if EnableGroup.PyodidePrerelease not in self.enable and fnmatch( if EnableGroup.PyodidePrerelease not in self.enable and fnmatch(
# build_id, "cp313-pyodide_*" build_id, "cp314-pyodide_*"
# ): ):
# return False return False
should_build = selector_matches(self.build_config, build_id) should_build = selector_matches(self.build_config, build_id)
should_skip = selector_matches(self.skip_config, build_id) should_skip = selector_matches(self.skip_config, build_id)
+113
View File
@@ -0,0 +1,113 @@
# Maintaining Pyodide support
Last updated: May 2026
This page describes how to update cibuildwheel's Pyodide platform code when either:
- a new Pyodide alpha release arrives with support for a new [PyEmscripten Platform](https://pyodide.org/en/latest/development/abi.html) (which is tied to updates in Emscripten and CPython versions, compiler/linker flags, and so on), or
- when that alpha release graduates to a stable one.
## Background
Pyodide has three types of releases that matter to cibuildwheel:
- **Stable** the most recent full Pyodide release (e.g., `0.29.x` / cp313). This is enabled by default with no special `CIBW_ENABLE` flag needed.
- **Prerelease** an alpha/beta/rc Pyodide release that uses the _next_ CPython version (e.g., `314.0.0a1` / cp314). Users must opt in with `CIBW_ENABLE: pyodide-prerelease` to build against this version. This may or may not be available at any given time, depending on the Pyodide release cycle.
- **End-of-life (EoL)** older Pyodide stable releases that are no longer the current stable. These are kept available behind `CIBW_ENABLE: pyodide-eol` so that users who still need to build for older Pyodide versions can do so.
The guards in `cibuildwheel/selector.py` enforce this distinction. The constraints files under `cibuildwheel/resources/` pin the exact tool versions that go with each build.
---
## When a new Pyodide prerelease becomes available
For example, consider a scenario when Pyodide ships a new `315.0.0a1` with cp315 support.
### 1. Add the new Python configuration
In `cibuildwheel/resources/build-platforms.toml`, add an entry under `[pyodide]`:
```toml
{ identifier = "cp315-pyodide_wasm32", version = "3.15", default_pyodide_version = "315.0.0a1", node_version = "v24" },
```
`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.
### 2. Update the prerelease guards in the selector
In `cibuildwheel/selector.py`, update the patterns in the `PyodidePrerelease` guards to match the new identifier:
```python
if EnableGroup.PyodidePrerelease not in self.enable and fnmatch(
build_id, "cp315-pyodide_*"
):
return False
```
### 3. Generate and pin a constraints file
Run the `update_constraints` `nox` session, which reads `build-platforms.toml` and regenerates all Pyodide constraints files automatically:
```bash
nox -s update_constraints
```
### 4. Update tests
Update the unit tests so the new identifier is accepted by the selector with `PyodidePrerelease` enabled and rejected without it. Pyodide-specific integration tests may also need their hardcoded expected-wheel lists extended.
## When a Pyodide prerelease becomes stable
Pyodide uses a versioning scheme where the stable release for a given CPython version is named `[PythonMajorMinor].0.0`, so the first stable release shipping cp314 will be **`314.0.0`**. See [pyodide/pyodide#6084](https://github.com/pyodide/pyodide/issues/6084) for a rationale of this versioning scheme.
### 1. Update the stable entries, and remove (or replace) the prerelease entry
In `build-platforms.toml`, update the former prerelease entry's `default_pyodide_version` to the new stable release (e.g. `314.0.0`) and remove the prerelease marker from the identifier if present. Remove previous prerelease entries if they are now obsolete, or update them to the next prerelease if one is available. Move the previous stable version behind `pyodide-eol` rather than dropping it outright (see below).
### 2. Disable or update the prerelease guards
In `selector.py`:
- **If a new prerelease is available**: update the `fnmatch` pattern to the next identifier (e.g. `cp315-pyodide_*`) as described above.
- **If there is no new prerelease**: comment out the `PyodidePrerelease` logic.
### 3. Update the constraints file
Run `nox -s update_constraints` to regenerate the constraints file for the newly stable version. If the entry was already in `build-platforms.toml` as a prerelease, its constraints file already exists, and this step just refreshes it against the stable Pyodide release and updates the dependencies' versions.
### 4. Update tests
Update the unit tests so the newly stable identifier is accepted by the selector without needing `PyodidePrerelease` in the enable set. Pyodide-specific integration tests may also need their hardcoded expected-wheel lists extended.
## When an old Pyodide version is to be moved to end-of-life
When a Pyodide version is superseded by a new stable release, move it behind the `pyodide-eol` enable flag. We want to allow users who still build for older Pyodide ABIs time to upgrade.
### 1. Add the `pyodide-eol` guard in the selector
In `cibuildwheel/selector.py`, add (or update) the `PyodideEoL` guard to include the old identifier:
```python
if EnableGroup.PyodideEoL not in self.enable and fnmatch(build_id, "cp312-pyodide_*"):
return False
```
### 2. Update tests
Update the unit tests so the EoL identifier requires `PyodideEoL` to be included in the enable set. The default (no `CIBW_ENABLE`) should exclude it.
## When an old Pyodide version is to be fully retired
Retirement is not expected to happen on a routine basis. It is only warranted when the Pyodide ecosystem itself has evolved to the point where an older ABI version is considered obsolete for example, if the surrounding toolchain, packaging standards, or runtime infrastructure have moved on so substantially that building for the older version no longer makes practical sense. Any retirement is to be discussed and agreed upon by Pyodide maintainers before proceeding.
### 1. Remove the Python configuration
Delete the entry from `build-platforms.toml` and remove the `PyodideEoL` guard for that identifier in `selector.py`.
### 2. Delete the constraints file
Remove `cibuildwheel/resources/constraints-pyodideXYZ.txt`.
### 3. Update tests
Remove references to the old identifier from the unit tests, integration tests, and drop any expected-wheel entries for it from the test helper.
+6
View File
@@ -208,3 +208,9 @@ git push && git push --tags
Then head to https://github.com/pypa/cibuildwheel/releases and create a GitHub release from the new tag, pasting in the changelog entry. Once the release is created inside GitHub, a CI job will create the assets and upload them to PyPI. Then head to https://github.com/pypa/cibuildwheel/releases and create a GitHub release from the new tag, pasting in the changelog entry. Once the release is created inside GitHub, a CI job will create the assets and upload them to PyPI.
If there were any schema updates, run `pipx run ./bin/generate_schema.py --schemastore > partial-cibuildwheel.json` and contribute the changes to SchemaStore. If there were any schema updates, run `pipx run ./bin/generate_schema.py --schemastore > partial-cibuildwheel.json` and contribute the changes to SchemaStore.
### Platform-specific maintenance
This section is a stub. Please open a PR to add guidance for any platform you would like to help maintain!
- **Pyodide**: see [Maintaining Pyodide support](_internal/pyodide-maintenance.md) for instructions and maintainer-specific information on updating Pyodide-related code in cibuildwheel and updating to new Pyodide releases.
+2
View File
@@ -361,6 +361,8 @@ values are:
- `pyodide-prerelease`: Pyodide versions that haven't released yet, if one is - `pyodide-prerelease`: Pyodide versions that haven't released yet, if one is
available. Safe if you are shipping a site with an early build, not for available. Safe if you are shipping a site with an early build, not for
general distribution. general distribution.
- `pyodide-eol`: Enable Pyodide versions that are no longer the current stable
release (if still available).
- `all`: Enable all of the above. - `all`: Enable all of the above.
!!! caution !!! caution
+1 -3
View File
@@ -159,8 +159,6 @@ By default, `ARM64` is not enabled when running on non-`ARM64` runners. Use [`CI
## Pyodide/WebAssembly {: #pyodide} ## Pyodide/WebAssembly {: #pyodide}
Pyodide is offered as an experimental feature in cibuildwheel.
### System requirements ### System requirements
Pyodide builds require a Linux or macOS machine. Pyodide builds require a Linux or macOS machine.
@@ -173,7 +171,7 @@ You must target pyodide with `--platform pyodide` (or use `--only` on the identi
It is also possible to target a specific Pyodide version by setting the [`pyodide-version`](options.md#pyodide-version) option to the desired version. Users are responsible for setting an appropriate Pyodide version according to the `pyodide-build` version. A list is available in Pyodide's [cross-build environments metadata file](https://github.com/pyodide/pyodide/blob/main/pyodide-cross-build-environments.json), which can be viewed more easily by installing `pyodide-build` from PyPI and using `pyodide xbuildenv search --all` to see a compatibility table. It is also possible to target a specific Pyodide version by setting the [`pyodide-version`](options.md#pyodide-version) option to the desired version. Users are responsible for setting an appropriate Pyodide version according to the `pyodide-build` version. A list is available in Pyodide's [cross-build environments metadata file](https://github.com/pyodide/pyodide/blob/main/pyodide-cross-build-environments.json), which can be viewed more easily by installing `pyodide-build` from PyPI and using `pyodide xbuildenv search --all` to see a compatibility table.
If there are pre-releases available for a newer Pyodide version, the `pyodide-prerelease` [`enable`](options.md#enable) can be used to include pre-release versions. If there are pre-releases available for a newer Pyodide version, the `pyodide-prerelease` [`enable`](options.md#enable) can be used to include pre-release versions. To build for older Pyodide versions that are no longer the current stable, use the `pyodide-eol` [`enable`](options.md#enable).
### Running tests ### Running tests
+7 -2
View File
@@ -43,15 +43,20 @@ def test_abi3(tmp_path: Path) -> None:
project_dir, project_dir,
add_env={ add_env={
# free_threaded, GraalPy, and PyPy do not have a Py_LIMITED_API equivalent, just build one of those # free_threaded, GraalPy, and PyPy do not have a Py_LIMITED_API equivalent, just build one of those
# pyodide uses cp313 (the stable version) which supports limited API / abi3
# also limit the number of builds for test performance reasons # also limit the number of builds for test performance reasons
"CIBW_BUILD": "cp39-* cp310-* pp310-* gp312_250-* cp312-* cp314t-*", "CIBW_BUILD": (
"cp313-*"
if utils.get_platform() == "pyodide"
else "cp39-* cp310-* pp310-* gp312_250-* cp312-* cp314t-*"
),
"CIBW_ENABLE": "all", "CIBW_ENABLE": "all",
}, },
) )
# check that the expected wheels are produced # check that the expected wheels are produced
if utils.get_platform() == "pyodide": if utils.get_platform() == "pyodide":
# there's only 1 possible configuration for pyodide, cp312. It builds # there's only 1 possible configuration for pyodide, cp313. It builds
# a wheel that is tagged abi3, compatible back to 3.10 # a wheel that is tagged abi3, compatible back to 3.10
expected_wheels = utils.expected_wheels( expected_wheels = utils.expected_wheels(
"spam", "spam",
+3 -3
View File
@@ -18,10 +18,10 @@ from pathlib import Path
wheel = Path(sys.argv[1]) wheel = Path(sys.argv[1])
dest_dir = Path(sys.argv[2]) dest_dir = Path(sys.argv[2])
platform = wheel.stem.split("-")[-1] platform = wheel.stem.split("-")[-1]
if platform.startswith("pyodide"): if platform.startswith("pyemscripten"):
# for the sake of this test, munge the pyodide platforms into one, it's # for the sake of this test, munge the pyemscripten platforms into one, it's
# not valid, but it does activate the uniqueness check # not valid, but it does activate the uniqueness check
platform = "pyodide" platform = "pyemscripten"
name = f"spam-0.1.0-py2-none-{platform}.whl" name = f"spam-0.1.0-py2-none-{platform}.whl"
dest = dest_dir / name dest = dest_dir / name
+17 -8
View File
@@ -77,13 +77,13 @@ def get_versions_from_constraint_file(constraint_file: Path) -> dict[str, str]:
return dict(re.findall(VERSION_REGEX, constraint_file_text)) return dict(re.findall(VERSION_REGEX, constraint_file_text))
@pytest.mark.parametrize("python_version", ["3.8", "3.12"]) @pytest.mark.parametrize("python_version", ["3.8", "3.13"])
def test_pinned_versions( def test_pinned_versions(
tmp_path: Path, python_version: str, build_frontend_env_nouv: dict[str, str] tmp_path: Path, python_version: str, build_frontend_env_nouv: dict[str, str]
) -> None: ) -> None:
if utils.get_platform() == "linux": if utils.get_platform() == "linux":
pytest.skip("linux doesn't pin individual tool versions, it pins manylinux images instead") pytest.skip("linux doesn't pin individual tool versions, it pins manylinux images instead")
if python_version != "3.12" and utils.get_platform() == "pyodide": if python_version != "3.13" and utils.get_platform() == "pyodide":
pytest.skip(f"pyodide does not support Python {python_version}") pytest.skip(f"pyodide does not support Python {python_version}")
if ( if (
python_version == "3.8" python_version == "3.8"
@@ -124,7 +124,7 @@ def test_pinned_versions(
expected_wheels = [ expected_wheels = [
w w
for w in utils.expected_wheels("spam", "0.1.0") for w in utils.expected_wheels("spam", "0.1.0")
if f"-cp{version_no_dot}" in w or f"-pp{version_no_dot}" in w if f"-cp{version_no_dot}-cp{version_no_dot}-" in w or f"-pp{version_no_dot}-" in w
] ]
assert set(actual_wheels) == set(expected_wheels) assert set(actual_wheels) == set(expected_wheels)
@@ -140,11 +140,20 @@ def test_dependency_constraints(
project_dir = tmp_path / "project" project_dir = tmp_path / "project"
test_projects.new_c_project().generate(project_dir) test_projects.new_c_project().generate(project_dir)
tool_versions = { if utils.get_platform() == "pyodide":
"pip": "23.1.2", # pyodide-build 0.34+ requires build~=1.4.0, so we must use a
"build": "1.2.2", # compatible version here. delocate is macOS-only and not used on pyodide.
"delocate": "0.10.3", tool_versions = {
} "pip": "23.1.2",
"build": "1.4.2",
"delocate": "0.10.3",
}
else:
tool_versions = {
"pip": "23.1.2",
"build": "1.2.2",
"delocate": "0.10.3",
}
if method == "file": if method == "file":
constraints_file = tmp_path / "constraints file.txt" constraints_file = tmp_path / "constraints file.txt"
+3 -3
View File
@@ -34,7 +34,7 @@ def test_cross_compiled_build(tmp_path: Path) -> None:
add_env={"CIBW_ARCHS": "x86_64, universal2, arm64"}, add_env={"CIBW_ARCHS": "x86_64, universal2, arm64"},
single_python=True, single_python=True,
) )
python_tag = "cp{}{}".format(*utils.SINGLE_PYTHON_VERSION) python_tag = "cp{0}{1}-cp{0}{1}-".format(*utils.SINGLE_PYTHON_VERSION)
expected_wheels = [w for w in ALL_MACOS_WHEELS if python_tag in w] expected_wheels = [w for w in ALL_MACOS_WHEELS if python_tag in w]
assert set(actual_wheels) == set(expected_wheels) assert set(actual_wheels) == set(expected_wheels)
@@ -176,7 +176,7 @@ def test_universal2_testing_on_x86_64(
else: else:
assert warning_message in captured.err assert warning_message in captured.err
python_tag = "cp{}{}".format(*utils.SINGLE_PYTHON_VERSION) python_tag = "cp{0}{1}-cp{0}{1}-".format(*utils.SINGLE_PYTHON_VERSION)
expected_wheels = [w for w in ALL_MACOS_WHEELS if python_tag in w and "universal2" in w] expected_wheels = [w for w in ALL_MACOS_WHEELS if python_tag in w and "universal2" in w]
assert set(actual_wheels) == set(expected_wheels) assert set(actual_wheels) == set(expected_wheels)
@@ -210,7 +210,7 @@ def test_universal2_testing_on_arm64(
assert "running tests on arm64 with pillow" in captured.out assert "running tests on arm64 with pillow" in captured.out
assert "running tests on x86_64 with pillow" in captured.out assert "running tests on x86_64 with pillow" in captured.out
python_tag = "cp{}{}".format(*utils.SINGLE_PYTHON_VERSION) python_tag = "cp{0}{1}-cp{0}{1}-".format(*utils.SINGLE_PYTHON_VERSION)
expected_wheels = [w for w in ALL_MACOS_WHEELS if python_tag in w and "universal2" in w] expected_wheels = [w for w in ALL_MACOS_WHEELS if python_tag in w and "universal2" in w]
assert set(actual_wheels) == set(expected_wheels) assert set(actual_wheels) == set(expected_wheels)
+5 -5
View File
@@ -79,8 +79,8 @@ def test_pyodide_build(tmp_path: Path, use_pyproject_toml: bool) -> None:
# check that the expected wheels are produced # check that the expected wheels are produced
expected_wheels = [ expected_wheels = [
"spam-0.1.0-cp312-cp312-pyodide_2024_0_wasm32.whl", "spam-0.1.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl",
"spam-0.1.0-cp313-cp313-pyodide_2025_0_wasm32.whl", "spam-0.1.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl",
] ]
print("actual_wheels", actual_wheels) print("actual_wheels", actual_wheels)
@@ -144,8 +144,8 @@ def test_pyodide_build_and_test(tmp_path: Path, expect_failure: bool) -> None:
) )
# check that the expected wheels are produced # check that the expected wheels are produced
expected_wheels = [ expected_wheels = [
"spam-0.1.0-cp312-cp312-pyodide_2024_0_wasm32.whl", "spam-0.1.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl",
"spam-0.1.0-cp313-cp313-pyodide_2025_0_wasm32.whl", "spam-0.1.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl",
] ]
assert set(actual_wheels) == set(expected_wheels) assert set(actual_wheels) == set(expected_wheels)
@@ -170,6 +170,6 @@ def test_pyodide_repair_wheel(tmp_path: Path) -> None:
# check that the expected wheels are produced # check that the expected wheels are produced
expected_wheels = [ expected_wheels = [
"spam-0.1.0-cp312-cp312-pyodide_2024_0_wasm32.whl", "spam-0.1.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl",
] ]
assert set(actual_wheels) == set(expected_wheels) assert set(actual_wheels) == set(expected_wheels)
+10 -9
View File
@@ -26,7 +26,7 @@ EMULATED_ARCHS: Final[list[str]] = sorted(
PYPY_ARCHS = ["x86_64", "i686", "AMD64", "aarch64", "arm64"] PYPY_ARCHS = ["x86_64", "i686", "AMD64", "aarch64", "arm64"]
GRAALPY_ARCHS = ["x86_64", "AMD64", "aarch64", "arm64"] GRAALPY_ARCHS = ["x86_64", "AMD64", "aarch64", "arm64"]
SINGLE_PYTHON_VERSION: Final[tuple[int, int]] = (3, 12) SINGLE_PYTHON_VERSION: Final[tuple[int, int]] = (3, 13)
# temporary workaround: set by build_frontend_env fixture to skip graalpy # temporary workaround: set by build_frontend_env fixture to skip graalpy
# when uv is the build frontend (compatibility issue between graalpy and uv) # when uv is the build frontend (compatibility issue between graalpy and uv)
@@ -263,11 +263,11 @@ def _expected_wheels(
if musllinux_versions is None: if musllinux_versions is None:
musllinux_versions = ["musllinux_1_2"] musllinux_versions = ["musllinux_1_2"]
# To be kept in sync with Python versions for Pyodide identifiers in cibuildwheel/selector.py.
if platform == "pyodide" and python_abi_tags is None: if platform == "pyodide" and python_abi_tags is None:
python_abi_tags = [ python_abi_tags = ["cp313-cp313"]
"cp312-cp312", if EnableGroup.PyodidePrerelease in enable_groups:
"cp313-cp313", python_abi_tags.append("cp314-cp314")
]
elif platform == "android" and python_abi_tags is None: # noqa: SIM114 elif platform == "android" and python_abi_tags is None: # noqa: SIM114
python_abi_tags = [ python_abi_tags = [
"cp313-cp313", "cp313-cp313",
@@ -412,13 +412,14 @@ def _expected_wheels(
elif platform == "pyodide": elif platform == "pyodide":
platform_tags = { platform_tags = {
"cp312-cp312": ["pyodide_2024_0_wasm32"], "cp312-cp312": ["pyodide_2024_0_wasm32"],
"cp313-cp313": ["pyodide_2025_0_wasm32"], "cp313-cp313": ["pyemscripten_2025_0_wasm32"],
"cp314-cp314": ["pyemscripten_2026_0_wasm32"],
}.get(python_abi_tag, []) }.get(python_abi_tag, [])
if not platform_tags: if not platform_tags:
# for example if the python tag is `none` or `abi3`, all # for example if the python tag is `none` or `abi3`, the wheel
# platform tags are built with that python tag # is built by the stable cp313 Python version
platform_tags = ["pyodide_2024_0_wasm32"] platform_tags = ["pyemscripten_2025_0_wasm32"]
else: else:
msg = f"Unsupported platform {platform!r}" msg = f"Unsupported platform {platform!r}"
+13 -1
View File
@@ -97,8 +97,8 @@ def test_build_filter_pyodide_prerelease() -> None:
skip_config="", skip_config="",
enable=frozenset([EnableGroup.PyodidePrerelease]), enable=frozenset([EnableGroup.PyodidePrerelease]),
) )
assert build_selector("cp312-pyodide_wasm32")
assert build_selector("cp313-pyodide_wasm32") assert build_selector("cp313-pyodide_wasm32")
assert build_selector("cp314-pyodide_wasm32")
def test_build_filter_pyodide() -> None: def test_build_filter_pyodide() -> None:
@@ -107,8 +107,20 @@ def test_build_filter_pyodide() -> None:
skip_config="", skip_config="",
enable=frozenset(), enable=frozenset(),
) )
assert build_selector("cp313-pyodide_wasm32")
assert not build_selector("cp312-pyodide_wasm32")
assert not build_selector("cp314-pyodide_wasm32")
def test_build_filter_pyodide_eol() -> None:
build_selector = BuildSelector(
build_config="*",
skip_config="",
enable=frozenset([EnableGroup.PyodideEoL]),
)
assert build_selector("cp312-pyodide_wasm32") assert build_selector("cp312-pyodide_wasm32")
assert build_selector("cp313-pyodide_wasm32") assert build_selector("cp313-pyodide_wasm32")
assert not build_selector("cp314-pyodide_wasm32")
def test_skip() -> None: def test_skip() -> None:
+1 -1
View File
@@ -308,7 +308,7 @@ def test_pyodide_on_windows(
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
) -> None: ) -> None:
monkeypatch.setattr(sys, "platform", "win32") monkeypatch.setattr(sys, "platform", "win32")
monkeypatch.setattr(sys, "argv", [*sys.argv, "--only", "cp312-pyodide_wasm32"]) monkeypatch.setattr(sys, "argv", [*sys.argv, "--only", "cp314-pyodide_wasm32"])
with pytest.raises(SystemExit) as exit: with pytest.raises(SystemExit) as exit:
main() main()
+2 -2
View File
@@ -35,7 +35,7 @@ manylinux-x86_64-image = "manylinux_2_28"
environment-pass = ["EXAMPLE_ENV"] environment-pass = ["EXAMPLE_ENV"]
pyodide-version = "0.28.0" pyodide-version = "0.29.4"
[tool.cibuildwheel.macos] [tool.cibuildwheel.macos]
test-requires = "else" test-requires = "else"
@@ -92,7 +92,7 @@ def test_options_1(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
assert local.manylinux_images["x86_64"] == pinned_x86_64_container_image["manylinux_2_34"] assert local.manylinux_images["x86_64"] == pinned_x86_64_container_image["manylinux_2_34"]
local = options.build_options("cp312-pyodide_wasm32") local = options.build_options("cp312-pyodide_wasm32")
assert local.pyodide_version == "0.28.0" assert local.pyodide_version == "0.29.4"
def test_passthrough(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: def test_passthrough(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: