Skip to content

Commit

Permalink
feat: add Pyodide support (#1456)
Browse files Browse the repository at this point in the history
* feat: add Pyodide support

Signed-off-by: Henry Schreiner <[email protected]>
Co-authored-by: Hood Chatham <[email protected]>
Co-authored-by: Matthieu Darbois <[email protected]>

tests: fix two merge issues

Signed-off-by: Henry Schreiner <[email protected]>

fix: include schema

Signed-off-by: Henry Schreiner <[email protected]>

* Try to fix xbuildenv path

[pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

Try to install pyodide-build from main branch

Try again

Try again

Update constraints file

Try again

Remove unused variable

Drop constraints

Remove --download option

Fix xbuildenv install

Try again

Try again

[pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* fix: remove pinning on pyodide

Signed-off-by: Henry Schreiner <[email protected]>

* Update for Pyodide 0.26.0a5

* Install pyodide-build from pypi

* Update docs/options.md

Co-authored-by: Henry Schreiner <[email protected]>

* Unxfail things that look like they were just a version mismatch

* refactor: add constraints for pyodide

Signed-off-by: Henry Schreiner <[email protected]>

* chore: minor cleanup

Signed-off-by: Henry Schreiner <[email protected]>

* Apply suggestions from code review

* Apply suggestion from code review

* refactor: minor touchup

Signed-off-by: Henry Schreiner <[email protected]>

* ci: xfail the pyodide test

Signed-off-by: Henry Schreiner <[email protected]>

* review: use a pinned version of node

* fix tests

* review: error out on Windows

* test: check node & test on macos arm64

* chore: minor cleanup

* Add reference to emscripten libc issue

* Apply suggestion from code review

* review: use a pinned pip in test virtual environment

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* chore: rework test virtual environment seed packages

* chore: workaround direct invocation of pytest

This allows to still test direct invocation of `pytest` on most platforms (including pyodide on Linux) but falls back to `python -m pytest` when running pyodide on macOS.

* Use release version of pyodide

* fix: tests for 0.26.0 & parallel initialization of xbuildenv

* Debug CI

* fix: test/test_build_frontend_args.py

* Revert "Debug CI"

This reverts commit 917646c.

---------

Signed-off-by: Henry Schreiner <[email protected]>
Co-authored-by: Henry Schreiner <[email protected]>
Co-authored-by: Matthieu Darbois <[email protected]>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
  • Loading branch information
4 people authored May 28, 2024
1 parent 687406f commit 9f2a3cb
Show file tree
Hide file tree
Showing 32 changed files with 1,154 additions and 87 deletions.
34 changes: 34 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -169,3 +169,37 @@ jobs:

- name: Run the emulation tests
run: pytest --run-emulation ${{ matrix.arch }} test/test_emulation.py

test-pyodide:
name: Test cibuildwheel building pyodide wheels
needs: lint
runs-on: ubuntu-24.04
timeout-minutes: 180
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
name: Install Python 3.12
with:
python-version: '3.12'

- name: Install dependencies
run: |
python -m pip install ".[test]"
- name: Generate a sample project
run: |
python -m test.test_projects test.test_0_basic.basic_project sample_proj
- name: Run a sample build (GitHub Action)
uses: ./
with:
package-dir: sample_proj
output-dir: wheelhouse
env:
CIBW_PLATFORM: pyodide

- name: Run tests with 'CIBW_PLATFORM' set to 'pyodide'
run: |
python ./bin/run_tests.py
env:
CIBW_PLATFORM: pyodide
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ repos:
rev: v2.3.0
hooks:
- id: codespell
args: ["-L", "sur", "-w"]
args: ["-L", "sur,assertin", "-w"]
exclude: ^docs/working-examples\.md$ # Autogenerated


Expand Down
31 changes: 16 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,24 +22,25 @@ Python wheels are great. Building them across **Mac, Linux, Windows**, on **mult
What does it do?
----------------

| | macOS Intel | macOS Apple Silicon | Windows 64bit | Windows 32bit | Windows Arm64 | manylinux<br/>musllinux x86_64 | manylinux<br/>musllinux i686 | manylinux<br/>musllinux aarch64 | manylinux<br/>musllinux ppc64le | manylinux<br/>musllinux s390x |
|----------------|----|-----|-----|-----|-----|----|-----|----|-----|-----|
| CPython 3.6 || N/A ||| N/A ||||||
| CPython 3.7 || N/A ||| N/A ||||||
| CPython 3.8 ||||| N/A ||||||
| CPython 3.9 ||||| ✅² ||||||
| CPython 3.10 ||||| ✅² ||||||
| CPython 3.11 ||||| ✅² ||||||
| CPython 3.12 ||||| ✅² ||||||
| CPython 3.13³ ||||| ✅² ||||||
| PyPy 3.7 v7.3 || N/A || N/A | N/A | ✅¹ | ✅¹ | ✅¹ | N/A | N/A |
| PyPy 3.8 v7.3 |||| N/A | N/A | ✅¹ | ✅¹ | ✅¹ | N/A | N/A |
| PyPy 3.9 v7.3 |||| N/A | N/A | ✅¹ | ✅¹ | ✅¹ | N/A | N/A |
| PyPy 3.10 v7.3 |||| N/A | N/A | ✅¹ | ✅¹ | ✅¹ | N/A | N/A |
| | macOS Intel | macOS Apple Silicon | Windows 64bit | Windows 32bit | Windows Arm64 | manylinux<br/>musllinux x86_64 | manylinux<br/>musllinux i686 | manylinux<br/>musllinux aarch64 | manylinux<br/>musllinux ppc64le | manylinux<br/>musllinux s390x | Pyodide |
|----------------|----|-----|-----|-----|-----|----|-----|----|-----|-----|-----|
| CPython 3.6 || N/A ||| N/A |||||| N/A |
| CPython 3.7 || N/A ||| N/A |||||| N/A |
| CPython 3.8 ||||| N/A |||||| N/A |
| CPython 3.9 ||||| ✅² |||||| N/A |
| CPython 3.10 ||||| ✅² |||||| N/A |
| CPython 3.11 ||||| ✅² |||||| N/A |
| CPython 3.12 ||||| ✅² |||||| ✅⁴ |
| CPython 3.13³ ||||| ✅² |||||| N/A |
| PyPy 3.7 v7.3 || N/A || N/A | N/A | ✅¹ | ✅¹ | ✅¹ | N/A | N/A | N/A |
| PyPy 3.8 v7.3 |||| N/A | N/A | ✅¹ | ✅¹ | ✅¹ | N/A | N/A | N/A |
| PyPy 3.9 v7.3 |||| N/A | N/A | ✅¹ | ✅¹ | ✅¹ | N/A | N/A | N/A |
| PyPy 3.10 v7.3 |||| N/A | N/A | ✅¹ | ✅¹ | ✅¹ | N/A | N/A | N/A |

<sup>¹ PyPy is only supported for manylinux wheels.</sup><br>
<sup>² Windows arm64 support is experimental.</sup><br>
<sup>³ CPython 3.13 is available using the [CIBW_PRERELEASE_PYTHONS](https://cibuildwheel.pypa.io/en/stable/options/#prerelease-pythons) option.</sup><br>
<sup>³ CPython 3.13 is available using the [`CIBW_PRERELEASE_PYTHONS`](https://cibuildwheel.pypa.io/en/stable/options/#prerelease-pythons) option. Free-threaded mode requires opt-in, not yet available on macOS.</sup><br>
<sup>⁴ Experimental, not yet supported on PyPI, but can be used directly in web deployment. Use `--platform pyodide` to build.</sup><br>

- Builds manylinux, musllinux, macOS 10.9+, and Windows wheels for CPython and PyPy
- Works on GitHub Actions, Azure Pipelines, Travis CI, AppVeyor, CircleCI, GitLab CI, and Cirrus CI
Expand Down
1 change: 1 addition & 0 deletions bin/generate_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,7 @@ def as_object(d: dict[str, Any]) -> dict[str, Any]:
"linux": as_object(non_global_options),
"windows": as_object(not_linux),
"macos": as_object(not_linux),
"pyodide": as_object(not_linux),
}

oses["linux"]["properties"]["repair-wheel-command"] = {
Expand Down
148 changes: 148 additions & 0 deletions bin/update_nodejs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
#!/usr/bin/env python3

from __future__ import annotations

import difflib
import logging
from dataclasses import dataclass
from pathlib import Path
from typing import Final

import click
import packaging.specifiers
import requests
import rich
from packaging.version import InvalidVersion, Version
from rich.logging import RichHandler
from rich.syntax import Syntax

from cibuildwheel._compat import tomllib

log = logging.getLogger("cibw")

# Looking up the dir instead of using utils.resources_dir
# since we want to write to it.
DIR: Final[Path] = Path(__file__).parent.parent.resolve()
RESOURCES_DIR: Final[Path] = DIR / "cibuildwheel/resources"

NODEJS_DIST: Final[str] = "https://nodejs.org/dist/"
NODEJS_INDEX: Final[str] = f"{NODEJS_DIST}index.json"


@dataclass(frozen=True, order=True)
class VersionTuple:
version: Version
version_string: str


def parse_nodejs_index() -> list[VersionTuple]:
versions: list[VersionTuple] = []
response = requests.get(NODEJS_INDEX)
response.raise_for_status()
versions_info = response.json()
for version_info in versions_info:
version_string = version_info.get("version", "???")
if not version_info.get("lts", False):
log.debug("Ignoring non LTS release %r", version_string)
continue
if "linux-x64" not in version_info.get("files", []):
log.warning(
"Ignoring release %r which does not include a linux-x64 binary", version_string
)
continue
try:
version = Version(version_string)
if version.is_devrelease:
log.info("Ignoring development release %r", str(version))
continue
if version.is_prerelease:
log.info("Ignoring pre-release %r", str(version))
continue
versions.append(VersionTuple(version, version_string))
except InvalidVersion:
log.warning("Ignoring release %r", version_string)
versions.sort(reverse=True)
return versions


@click.command()
@click.option("--force", is_flag=True)
@click.option(
"--level", default="INFO", type=click.Choice(["WARNING", "INFO", "DEBUG"], case_sensitive=False)
)
def update_nodejs(force: bool, level: str) -> None:
logging.basicConfig(
level="INFO",
format="%(message)s",
datefmt="[%X]",
handlers=[RichHandler(rich_tracebacks=True, markup=True)],
)
log.setLevel(level)

toml_file_path = RESOURCES_DIR / "nodejs.toml"

original_toml = toml_file_path.read_text()
with toml_file_path.open("rb") as f:
nodejs_data = tomllib.load(f)

nodejs_data.pop("url")

major_versions = [VersionTuple(Version(key), key) for key in nodejs_data]
major_versions.sort(reverse=True)

versions = parse_nodejs_index()

# update existing versions, 1 per LTS
for major_version in major_versions:
current = Version(nodejs_data[major_version.version_string])
specifier = packaging.specifiers.SpecifierSet(
specifiers=f"=={major_version.version.major}.*"
)
for version in versions:
if specifier.contains(version.version) and version.version > current:
nodejs_data[major_version.version_string] = version.version_string
break

# check for a new major LTS to insert
if versions and versions[0].version.major > major_versions[0].version.major:
major_versions.insert(
0,
VersionTuple(Version(str(versions[0].version.major)), f"v{versions[0].version.major}"),
)
nodejs_data[major_versions[0].version_string] = versions[0].version_string

versions_toml = "\n".join(
f'{major_version.version_string} = "{nodejs_data[major_version.version_string]}"'
for major_version in major_versions
)
result_toml = f'url = "{NODEJS_DIST}"\n{versions_toml}\n'

rich.print() # spacer

if original_toml == result_toml:
rich.print("[green]Check complete, nodejs version unchanged.")
return

rich.print("nodejs version updated.")
rich.print("Changes:")
rich.print()

toml_relpath = toml_file_path.relative_to(DIR).as_posix()
diff_lines = difflib.unified_diff(
original_toml.splitlines(keepends=True),
result_toml.splitlines(keepends=True),
fromfile=toml_relpath,
tofile=toml_relpath,
)
rich.print(Syntax("".join(diff_lines), "diff", theme="ansi_light"))
rich.print()

if force:
toml_file_path.write_text(result_toml)
rich.print("[green]TOML file updated.")
else:
rich.print("[yellow]File left unchanged. Use --force flag to update.")


if __name__ == "__main__":
update_nodejs()
12 changes: 11 additions & 1 deletion cibuildwheel/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import cibuildwheel
import cibuildwheel.linux
import cibuildwheel.macos
import cibuildwheel.pyodide
import cibuildwheel.util
import cibuildwheel.windows
from cibuildwheel._compat.typing import assert_never
Expand Down Expand Up @@ -45,7 +46,7 @@ def main() -> None:

parser.add_argument(
"--platform",
choices=["auto", "linux", "macos", "windows"],
choices=["auto", "linux", "macos", "windows", "pyodide"],
default=None,
help="""
Platform to build for. Use this option to override the
Expand Down Expand Up @@ -176,6 +177,8 @@ def _compute_platform_only(only: str) -> PlatformName:
return "macos"
if "win_" in only or "win32" in only:
return "windows"
if "pyodide_" in only:
return "pyodide"
print(
f"Invalid --only='{only}', must be a build selector with a known platform",
file=sys.stderr,
Expand Down Expand Up @@ -246,11 +249,18 @@ def get_platform_module(platform: PlatformName) -> PlatformModule:
return cibuildwheel.windows
if platform == "macos":
return cibuildwheel.macos
if platform == "pyodide":
return cibuildwheel.pyodide
assert_never(platform)


def build_in_directory(args: CommandLineArguments) -> None:
platform: PlatformName = _compute_platform(args)
if platform == "pyodide" and sys.platform == "win32":
msg = "cibuildwheel: Building for pyodide is not supported on Windows"
print(msg, file=sys.stderr)
sys.exit(2)

options = compute_options(platform=platform, command_line_arguments=args, env=os.environ)

package_dir = options.globals.package_dir
Expand Down
22 changes: 18 additions & 4 deletions cibuildwheel/architecture.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"linux": "Linux",
"macos": "macOS",
"windows": "Windows",
"pyodide": "Pyodide",
}

ARCH_SYNONYMS: Final[list[dict[PlatformName, str | None]]] = [
Expand Down Expand Up @@ -46,6 +47,9 @@ class Architecture(Enum):
AMD64 = "AMD64"
ARM64 = "ARM64"

# WebAssembly
wasm32 = "wasm32"

# Allow this to be sorted
def __lt__(self, other: Architecture) -> bool:
return self.value < other.value
Expand All @@ -72,8 +76,9 @@ def parse_config(config: str, platform: PlatformName) -> set[Architecture]:
return result

@staticmethod
def auto_archs(platform: PlatformName) -> set[Architecture]:
native_machine = platform_module.machine()
def native_arch(platform: PlatformName) -> Architecture | None:
if platform == "pyodide":
return Architecture.wasm32

# Cross-platform support. Used for --print-build-identifiers or docker builds.
host_platform: PlatformName = (
Expand All @@ -82,6 +87,7 @@ def auto_archs(platform: PlatformName) -> set[Architecture]:
else ("macos" if sys.platform.startswith("darwin") else "linux")
)

native_machine = platform_module.machine()
native_architecture = Architecture(native_machine)

# we might need to rename the native arch to the machine we're running
Expand All @@ -93,11 +99,18 @@ def auto_archs(platform: PlatformName) -> set[Architecture]:

if synonym is None:
# can't build anything on this platform
return set()
return None

native_architecture = Architecture(synonym)

result = {native_architecture}
return native_architecture

@staticmethod
def auto_archs(platform: PlatformName) -> set[Architecture]:
native_arch = Architecture.native_arch(platform)
if native_arch is None:
return set() # can't build anything on this platform
result = {native_arch}

if platform == "linux" and Architecture.x86_64 in result:
# x86_64 machines can run i686 containers
Expand All @@ -120,6 +133,7 @@ def all_archs(platform: PlatformName) -> set[Architecture]:
},
"macos": {Architecture.x86_64, Architecture.arm64, Architecture.universal2},
"windows": {Architecture.x86, Architecture.AMD64, Architecture.ARM64},
"pyodide": {Architecture.wasm32},
}
return all_archs_map[platform]

Expand Down
1 change: 1 addition & 0 deletions cibuildwheel/logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"macosx_x86_64": "macOS x86_64",
"macosx_universal2": "macOS Universal 2 - x86_64 and arm64",
"macosx_arm64": "macOS arm64 - Apple Silicon",
"pyodide_wasm32": "Pyodide",
}


Expand Down
Loading

0 comments on commit 9f2a3cb

Please sign in to comment.