Skip to content

CI: WASM Pyodide#1903

Merged
ax3l merged 11 commits into
openPMD:devfrom
ax3l:wasm-pyodide-ci-tests
Jul 2, 2026
Merged

CI: WASM Pyodide#1903
ax3l merged 11 commits into
openPMD:devfrom
ax3l:wasm-pyodide-ci-tests

Conversation

@ax3l

@ax3l ax3l commented Jun 28, 2026

Copy link
Copy Markdown
Member

CI test for WASM Pyodide.

Ensure co-loading with h5py, even when both use different HDF5 versions, works.

@ax3l ax3l added this to the 0.17.2 milestone Jun 28, 2026
@ax3l ax3l mentioned this pull request Jun 28, 2026
5 tasks
@ax3l ax3l force-pushed the wasm-pyodide-ci-tests branch 2 times, most recently from 5ca8125 to 5d1a7a0 Compare June 29, 2026 06:50
Add a WASM CI job that cross-compiles the openPMD-api Python wheel for Pyodide
(openPMD_USE_PYTHON=ON) and runs the full Python unittest suite (APITest, incl.
the HDF5 read/write path) in the Pyodide test runtime via cibuildwheel's own
test runner -- so the test ABI matches the freshly built wheel. Catches
wasm-specific regressions a wheel smoke test cannot (e.g. the HDF5 64-bit long
double read path, openPMD#1902).

.github/ci/wasm_deps.sh cross-builds static zlib + HDF5 into the Emscripten
sysroot (Emscripten restricts find_package to the sysroot) incl. the HDF5
FE_INVALID emscripten patch. run_python_tests.py sets cwd + sys.path so the
suite's relative sample paths and API.APITest import resolve.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ax3l ax3l force-pushed the wasm-pyodide-ci-tests branch from 5d1a7a0 to 6e756f7 Compare June 29, 2026 07:03
Comment thread .github/workflows/wasm.yml
ax3l and others added 6 commits July 1, 2026 13:48
Reproduce the ImpactX WASM teardown fault in openPMD-api's own CI: h5py bundles
a second static HDF5, which under Pyodide's single namespace interposes with
openpmd_api's HDF5. At interpreter teardown the tangled type registry makes the
openPMD HDF5 handler-destructor's H5Tclose fault ("not a datatype" -> wasm OOB),
even though the round-trip succeeds.

The probe runs after the full Python suite and is guarded with `|| echo` so a
crash is logged without failing the job while we reproduce (step 1). The guard
is dropped once the openPMD-api teardown fix lands, turning it into an enforced
regression test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Run coload_repro.py first and standalone -- openpmd_api imported and run before
h5py loads (the ImpactX order, openPMD as the primary HDF5) -- printing a marker
per phase and both bundled HDF5 versions. This localizes any fatal fault
(mid-operation vs. teardown) and distinguishes an ABI mismatch (h5py bundling a
different HDF5) from the same-version teardown fault.

Pin the import order with `# isort: skip_file` so pre-commit's isort does not
alphabetize h5py ahead of openpmd_api (which would invert primary/secondary).
Probe and suite are both guarded (`|| echo`, `;`) so both run and the job stays
green while we diagnose from the log.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pyodide links the Python extension as a side module with -sSIDE_MODULE=1, which
whole-archives and force-exports every symbol -- including the statically linked
HDF5. A second co-loaded wheel that also bundles HDF5 (ImpactX, h5py, a second
openPMD) then cross-binds its HDF5 symbols into ours, so two HDF5 instances share
one tangled registry and crash on the first file op or at teardown ("not a
datatype" -> wasm memory access out of bounds). -fvisibility=hidden cannot
prevent this because the whole-archive export overrides it.

Mirror the APPLE `-exported_symbol` / Linux `--exclude-libs` co-load fix on
Emscripten: -sSIDE_MODULE=2 exports only the symbols in EXPORTED_FUNCTIONS (just
the module init _PyInit_openpmd_api_cxx), keeping HDF5 private to this wheel.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ty deps

The co-load crash (two wheels each embedding HDF5 in Pyodide's single namespace)
needs BOTH halves, neither of which suffices alone:

  - -sSIDE_MODULE=2 on the extension: Pyodide's default -sSIDE_MODULE=1 whole-
    archives and force-exports every symbol (so -fvisibility=hidden alone cannot
    hide HDF5); =2 exports only _PyInit_openpmd_api_cxx.
  - -fvisibility=hidden on the static zlib/HDF5 (wasm_deps.sh): makes their
    definitions DSO-local so wasm-ld can relax our GOT references to HDF5 into
    direct calls into our own copy, instead of binding to a co-loaded second
    HDF5 (h5py, ImpactX) at load time.

Without the visibility half, =2 stops the re-export but our HDF5 calls still bind
to the first-loaded global HDF5 via the GOT and crash on the first file op; this
was verified in CI (the co-load probe still OOB'd with =2 alone).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Now that the co-load fix works, drop the `|| echo` guards so coload_repro.py and
the full suite are enforced regression tests: any wasm co-load regression fails
the job.

Also fix testScalarHdf5Fields, which ran for the first time on wasm (h5py is now
present in the test env): on wasm32 h5py writes the scalar with its native int
(int32) while np.array([45]) defaults to int64, so the store type-mismatched the
component ("store from Python array of type 'LONG' into Record Component of type
'INT'"). Store the component's own dtype instead of assuming the two agree.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ax3l added a commit to ax3l/openPMD-api that referenced this pull request Jul 2, 2026
…E=2)

library_builders.sh already builds the wasm zlib/HDF5 with -fvisibility=hidden
(the DSO-local half). Add the complementary link-time half as a source patch:
python-wasm-side-module.patch adds an EMSCRIPTEN branch to the co-load block
(from python-hide-symbols.patch) that links the extension with -sSIDE_MODULE=2
and -sEXPORTED_FUNCTIONS=_PyInit_openpmd_api_cxx, so Pyodide no longer whole-
archives and re-exports the bundled HDF5. Neither half fixes the co-load alone.

Ports the dev fix openPMD#1903 (validated green there: the co-load
probe + full Python suite run with a second HDF5, h5py, co-loaded). Applied in
COMMON_PATCHES right after python-hide-symbols.patch, whose block it extends.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ax3l and others added 2 commits July 1, 2026 23:59
Wrap the over-79-character comment lines flagged by the `style` job:
coload_repro.py header, run_python_tests.py header, and the testScalarHdf5Fields
dtype comment in APITest.py. No code changes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- test/python/coload_repro.py (was .github/ci/): the co-load probe now lives
  with the other Python tests.
- .github/workflows/dependencies/install_wasm.sh (was .github/ci/wasm_deps.sh):
  next to where dependency installers belong.
- Drop run_python_tests.py: it only replicated cwd + sys.path[0]. Instead run
  `cd test/python/unittest && python Test.py -v`, mirroring the native ctest
  (CMakeLists.txt runs Test.py -v from that WORKING_DIRECTORY). Not pyodide-
  specific -- the ../samples relative paths require that cwd on every platform.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread test/python/unittest/API/APITest.py Outdated
Comment thread CMakeLists.txt Outdated
@ax3l ax3l merged commit 0db0738 into openPMD:dev Jul 2, 2026
31 of 32 checks passed
@ax3l ax3l deleted the wasm-pyodide-ci-tests branch July 2, 2026 07:43
ax3l added a commit to ax3l/openPMD-api that referenced this pull request Jul 2, 2026
…E=2)

library_builders.sh already builds the wasm zlib/HDF5 with -fvisibility=hidden
(the DSO-local half). Add the complementary link-time half as a source patch:
python-wasm-side-module.patch adds an EMSCRIPTEN branch to the co-load block
(from python-hide-symbols.patch) that links the extension with -sSIDE_MODULE=2
and -sEXPORTED_FUNCTIONS=_PyInit_openpmd_api_cxx, so Pyodide no longer whole-
archives and re-exports the bundled HDF5. Neither half fixes the co-load alone.

Ports the dev fix openPMD#1903 (validated green there: the co-load
probe + full Python suite run with a second HDF5, h5py, co-loaded). Applied in
COMMON_PATCHES right after python-hide-symbols.patch, whose block it extends.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ax3l added a commit to ax3l/openPMD-api that referenced this pull request Jul 2, 2026
…d patch

- setup.py version 0.17.1.post3 -> 0.17.1.post4 (rename the setup-py-version
  patch to match; update COMMON_PATCHES).
- Regenerate python-wasm-side-module.patch so the applied CMakeLists co-load
  block matches openPMD-api#1903's current version (refined comment wording, no
  file-path reference, bare EMSCRIPTEN branch). Verified it applies cleanly
  after python-hide-symbols.patch and reproduces the openPMD#1903 block byte-for-byte.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ax3l added a commit that referenced this pull request Jul 2, 2026
* wheels: Windows std::mutex ABI fix (_DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR)

The win64 openpmd_api wheel access-violates at `import openpmd_api` (NULL read in
MSVCP140!Mtx_destroy) -- the VS 2022 17.10 std::mutex constexpr-constructor ABI
break. The wheel --excludes msvcp*.dll, so the SYSTEM msvcp140.dll runs and can
predate the build toolset's STL, which then NULL-derefs in Mtx_destroy.

Surfaced co-loaded under the ImpactX wheel's win64 test: with the same fix added
to the impactx build, amrex and impactx now import cleanly and the crash moved to
the openpmd_api dependency -- which needs the identical fix.

Add /D_DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR to the Windows CXXFLAGS to revert to
the non-constexpr constructor, ABI-compatible with any msvcp140.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* win: set /D_DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR in library_builders.bat too

The std::mutex ABI define is already in CIBW_ENVIRONMENT_WINDOWS (the central
wheel build); set it in the Windows dependency builder as well so ADIOS2 (and any
other C++ dependency) is built with the same non-constexpr std::mutex ABI as
openPMD-api. Since we --exclude msvcp*.dll from the wheel, this keeps the whole
toolchain consistent and portable against any system msvcp140.dll (VS 2022 17.10
made the ctor constexpr -> otherwise a NULL deref in Mtx_destroy).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* WASM: complete the co-load fix on the wheels branch (add -sSIDE_MODULE=2)

library_builders.sh already builds the wasm zlib/HDF5 with -fvisibility=hidden
(the DSO-local half). Add the complementary link-time half as a source patch:
python-wasm-side-module.patch adds an EMSCRIPTEN branch to the co-load block
(from python-hide-symbols.patch) that links the extension with -sSIDE_MODULE=2
and -sEXPORTED_FUNCTIONS=_PyInit_openpmd_api_cxx, so Pyodide no longer whole-
archives and re-exports the bundled HDF5. Neither half fixes the co-load alone.

Ports the dev fix #1903 (validated green there: the co-load
probe + full Python suite run with a second HDF5, h5py, co-loaded). Applied in
COMMON_PATCHES right after python-hide-symbols.patch, whose block it extends.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Wheels post4: bump version to 0.17.1.post4 + sync #1903 co-load patch

- setup.py version 0.17.1.post3 -> 0.17.1.post4 (rename the setup-py-version
  patch to match; update COMMON_PATCHES).
- Regenerate python-wasm-side-module.patch so the applied CMakeLists co-load
  block matches openPMD-api#1903's current version (refined comment wording, no
  file-path reference, bare EMSCRIPTEN branch). Verified it applies cleanly
  after python-hide-symbols.patch and reproduces the #1903 block byte-for-byte.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* simplify inline comment

Co-authored-by: Axel Huebl <axel.huebl@plasma.ninja>

* Wheels/RTD: use a supported python so the RTD skip actually cancels

The .readthedocs.yml exit-183 skip never ran: build.tools.python "3.14" is not a
supported Read the Docs build tool, so config validation failed and the per-PR
RTD build errored (red check) instead of cancelling. post_checkout (exit 183)
runs before the Python environment is created, so any valid version works -- use
3.12 so the config validates and the build cancels cleanly on the wheels branch.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Wheels/RTD: make the exit-183 skip actually parse (add a build definition)

The previous .readthedocs.yml (build.jobs.post_checkout: exit 183, with no
sphinx/mkdocs/build.commands) was rejected at config parse time: the build
failed right after `cat .readthedocs.yml`, before post_checkout ran, so exit 183
never fired and RTD posted a red check on every wheels-branch PR. The python
version was a red herring -- 3.14 and 3.12 failed identically.

Base the skip on dev's working config (add sphinx.configuration, use dev's
os/python) so it validates; post_checkout's exit 183 then cancels the build
before sphinx is read, and RTD reports the PR check as success (a cancelled
build is a green check, per RTD docs).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant