From 1f829e6003b8686d65e694aeaf1a24d8f9110cb8 Mon Sep 17 00:00:00 2001 From: Daniel Lemire Date: Sat, 14 Nov 2026 09:39:27 -0500 Subject: [PATCH] Build Pyodide (WebAssembly) wheels - The release workflow builds Pyodide wheels with cibuildwheel (CIBW_PLATFORM: pyodide), one per Pyodide ABI (cp313, cp314, cp315), uploaded to PyPI with the others; the PR tests build and test them too. - ffibuild.py: no limited API (abi3) under Emscripten. A Pyodide wheel is tied to one Pyodide version anyway, and Pyodide 315 names abi3 extensions _xorfilter.abi3-"wasm32-emscripten".so (quotes in SOABI_PLATFORM), which Python cannot import. Native builds are unchanged. - tests: draw the keys with randrange instead of sample(range(1 << 62)), whose len() overflows a 32-bit ssize_t (WebAssembly). --- .github/workflows/pytests.yml | 17 +++++++++++++++++ .github/workflows/wheels.yml | 22 +++++++++++++++++++++- pyfusefilter/ffibuild.py | 6 ++++++ pyproject-cibuildwheel.toml | 6 ++++++ tests/test_fuse16.py | 7 +++++-- tests/test_fuse8.py | 7 +++++-- 6 files changed, 60 insertions(+), 5 deletions(-) diff --git a/.github/workflows/pytests.yml b/.github/workflows/pytests.yml index 162a53e..7c44c15 100644 --- a/.github/workflows/pytests.yml +++ b/.github/workflows/pytests.yml @@ -97,3 +97,20 @@ jobs: - name: Build wheel run: | python -m build --wheel + + pyodide: + # WebAssembly (32-bit): cibuildwheel builds the wheels and runs the tests + # in Node. + name: Pyodide (WebAssembly) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Build and test + uses: pypa/cibuildwheel@v4.3.5 + env: + CIBW_PLATFORM: pyodide + with: + config-file: "{package}/pyproject-cibuildwheel.toml" diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index fe279d9..9cf5833 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -60,8 +60,28 @@ jobs: path: ./wheelhouse/*.whl if-no-files-found: error + build_pyodide: + name: Build Pyodide (WebAssembly) wheels + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + with: + submodules: true + - name: Build wheels + uses: pypa/cibuildwheel@v4.3.5 + env: + CIBW_PLATFORM: pyodide + with: + config-file: "{package}/pyproject-cibuildwheel.toml" + + - uses: actions/upload-artifact@v4 + with: + name: wheel-artifact-pyodide + path: ./wheelhouse/*.whl + if-no-files-found: error + upload_all: - needs: [build_wheels, sdist] + needs: [build_wheels, build_pyodide, sdist] name: Uploading built packages to pypi for release. runs-on: ubuntu-latest # Only upload to PyPI when triggered by a tag (not manual workflow_dispatch). diff --git a/pyfusefilter/ffibuild.py b/pyfusefilter/ffibuild.py index 88905c2..65eba25 100644 --- a/pyfusefilter/ffibuild.py +++ b/pyfusefilter/ffibuild.py @@ -14,8 +14,14 @@ all_src += src.read() +# Under Pyodide (WebAssembly), a wheel is tied to one Pyodide version anyway, +# so the limited API (abi3) gains nothing; and Pyodide 315 names abi3 +# extensions with quotes (_xorfilter.abi3-"wasm32-emscripten".so), which +# Python then cannot import. +import sysconfig ffi.set_source( "_xorfilter", all_src, + py_limited_api=not sysconfig.get_platform().startswith("emscripten"), ) ffi.cdef( diff --git a/pyproject-cibuildwheel.toml b/pyproject-cibuildwheel.toml index 9ec23f9..d60b007 100644 --- a/pyproject-cibuildwheel.toml +++ b/pyproject-cibuildwheel.toml @@ -13,5 +13,11 @@ archs = ["x86_64", "universal2", "arm64"] # The workflow runs on both x86_64 and aarch64 runners; build natively on each. archs = ["native"] +[tool.cibuildwheel.pyodide] +# WebAssembly wheels, one per Pyodide ABI (cp313: Pyodide 0.28 and 0.29; +# cp314: Pyodide 314; cp315). In a Pyodide venv, the pytest script starts +# with a shebang to a shell script, which macOS cannot run: python -m pytest. +test-command = "python -m pytest {project}/tests -q --import-mode=importlib" + [tool.cibuildwheel.windows] archs = ["AMD64"] diff --git a/tests/test_fuse16.py b/tests/test_fuse16.py index 1621fe2..21aba9e 100644 --- a/tests/test_fuse16.py +++ b/tests/test_fuse16.py @@ -1,5 +1,5 @@ from pyfusefilter import Fuse16 -from random import sample +from random import randrange import tempfile, os @@ -66,7 +66,10 @@ def test_fuse16_few_segments(): # https://github.com/FastFilter/xor_singleheader/issues/77 for size in (3551, 11504, 11521, 12372, 13224, 37454): for _ in range(5): - keys = sample(range(1 << 62), size) + keys = set() + while len(keys) < size: + keys.add(randrange(1 << 62)) + keys = list(keys) filter = Fuse16(keys) for key in keys: assert key in filter diff --git a/tests/test_fuse8.py b/tests/test_fuse8.py index c728262..3c75bd2 100644 --- a/tests/test_fuse8.py +++ b/tests/test_fuse8.py @@ -1,5 +1,5 @@ from pyfusefilter import Fuse8 -from random import sample +from random import randrange import tempfile, os def test_readme(): @@ -65,7 +65,10 @@ def test_fuse8_few_segments(): # https://github.com/FastFilter/xor_singleheader/issues/77 for size in (3551, 11504, 11521, 12372, 13224, 37454): for _ in range(5): - keys = sample(range(1 << 62), size) + keys = set() + while len(keys) < size: + keys.add(randrange(1 << 62)) + keys = list(keys) filter = Fuse8(keys) for key in keys: assert key in filter