feat: add emscripten build to ci - #392
Merged
Merged
Conversation
mx already gets cross-compiled to WebAssembly downstream (denigma, github.com/rpatters1/denigma, uses it for browser-side musx-to-MusicXML conversion), but nothing here proves it keeps working. Add emsdk to the pinned mx-sdk toolchain, teach mx's own CMakeLists.txt to enable exception catching under Emscripten (it disables catching by default, which turns mx's internal MX_THROW/catch boundary in DocumentManager into a fatal abort -- previously only denigma's build set this, not mx itself), and wire up `make wasm-test` to build mx::api and run the mxread/mxwrite/mxhide examples under Node so the check exercises the library, not just a compile. Closes #386
emsdk's node dependency ships as a .tar.xz; without xz-utils the tar extraction fails, so the mx-sdk image itself never finished building -- taking test-linux, quality, and the new wasm job down with it (they all build the same image first). Also rename the wasm CI job to `emscripten`.
Emscripten's own tree (upstream/emscripten/) contains a subdirectory literally named `cmake` (its CMake toolchain-file module). Prepending that directory onto PATH meant every `cmake` lookup resolved to it instead of /usr/bin/cmake -- execve() on a directory returns EACCES, which surfaced as "make: cmake: Permission denied" and broke every job that builds through mx-sdk (test-linux, quality), not just wasm. Append emsdk's dirs after the existing PATH instead. Also drop emsdk's downloads/ (raw .tar.xz archives) once installed -- pure dead weight after extraction.
webern
commented
Aug 7, 2026
Same bug as the cmake fix, one PATH entry earlier: bare /opt/emsdk contains a node/ subdirectory, searched before /opt/emsdk/node/current-bin where the real node binary lives -- so `node` resolved to a directory and failed with EACCES. Reorder PATH and drop the now-unused bare /opt/emsdk entry (nothing here invokes the emsdk CLI by name). Also trims several comments flagged as too long in review.
Emscripten's default MEMFS starts empty -- writeToFile("build/wasm/...")
failed because that parent directory doesn't exist inside the virtual
filesystem, not because of anything wrong with mx::api itself. A bare
filename writes to MEMFS's root, which always exists.
The emscripten job was the slowest in CI (8m32s vs ~3-4m for every other job) because wasm-lib explicitly blanked the compiler-launcher env, so it rebuilt all of mx_core + mx from scratch on every run -- unlike test-linux/quality/gen, which all get a persisted per-job ccache dir. ccache wrapping emcc's Python driver was an untested worry, not a real conflict (no coverage-instrumentation clash like the targets that legitimately blank it); wire up a ccache cache step for this job matching the existing pattern and let CI confirm it works.
webern
commented
Aug 7, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Human Summary
Add a smoke build to WASM and run something trivial to make sure we don't break denigma.
Summary
mxalready gets cross-compiled to WebAssembly downstream today:denigma(github.com/rpatters1/denigma) builds it client-side for browser use in a musx-to-MusicXML converter, but nothing here proves that keeps working, so a regression would only surface in someone else's CI. This adds an Emscripten build tomx's own CI.mx-sdk'sDockerfilegets a pinnedemsdkinstall (6.0.6), matching how every other tool in that image is version-pinned rather than "latest".EM_CACHEis redirected to the mounted build volume, same reasoning asCCACHE_DIR/GOPATHright below it in the file: the container runs as the caller's uid:gid, which can't write under the root-owned/opt/emsdkat runtime.CMakeLists.txtnow enables exception catching underEMSCRIPTEN(-fexceptions, compile and link). Emscripten disables catching by default, which turnsmx's internalMX_THROW/catch boundary inDocumentManager(src/private/mx/utility/Throw.h) into a fatal abort instead of a caughtResultCode::internalError. Previously onlydenigma's own build set this flag (globally, beforeFetchContent-ingmx) --mxdidn't configure itself correctly forEMSCRIPTENon its own. Confirmed this is the same fixdenigma-examples' top-levelCMakeLists.txtalready applies, so this reproduces a known-working setup rather than a new one.make wasm-lib/wasm-build/wasm-testtargets, driven insidemx-sdklike every other gate.wasm-testbuildsmx::apiand runs the existingmxread/mxwrite/mxhideexamples under Node -- it exercises the library throughDocumentManager, not just a compile.test-wasmCI job (mirrorstest-linux/quality/gen's Docker setup) runningmake wasm-test.Testing
make -n wasm-test(dry run, both outside- and inside-container branches) resolves to the expectedemcmake/emmake/nodecommandscmake -S . -B ... -DMX_API=onstill configures cleanly with the newEMSCRIPTENblock present (a no-op when not cross-compiling)ci.yamlstill parses as valid YAMLReferences