From 8bbf287debd493ad77aa5719b55ca1983a76d22a Mon Sep 17 00:00:00 2001 From: Matthew James Briggs Date: Fri, 7 Aug 2026 20:25:19 +0000 Subject: [PATCH 1/8] feat: add Emscripten (wasm) build to CI 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 --- .github/workflows/ci.yaml | 36 ++++++++++++++++++++++++++++++++ AGENTS.md | 2 ++ CMakeLists.txt | 11 ++++++++++ Dockerfile | 26 +++++++++++++++++++++++ Makefile | 44 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 119 insertions(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 5f5d48188..a883c6c09 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -96,6 +96,42 @@ jobs: - name: All C++ suites (core roundtrip + unit, api mxtest + examples + roundtrip) run: make test-all + # mx::api cross-compiled to WebAssembly via the pinned mx-sdk emsdk install + # (issue #386): a consumer (denigma, github.com/rpatters1/denigma) compiles mx + # client-side for browser use, and this is the one place that path is proven + # rather than discovered as a regression downstream. Builds through the + # mx-sdk toolchain like test-linux/quality/gen; runs mxread/mxwrite/mxhide + # under Node so the check exercises mx::api, not just a compile. + test-wasm: + name: "wasm: mx::api under Emscripten" + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v5 + + - uses: docker/setup-buildx-action@v3 + - uses: crazy-max/ghaction-github-runtime@v3 + + - name: Create bind-backed build volume + run: | + mkdir -p build/docker + docker volume create --driver local \ + --opt type=none --opt o=bind \ + --opt device="$GITHUB_WORKSPACE/build/docker" mx-build + + # Emscripten's compiled system-library cache (libc/libc++/compiler-rt), + # not source ccache -- see EM_CACHE in the Dockerfile. Populating it from + # scratch is the slow part of a wasm build. + - name: Cache EM_CACHE + uses: actions/cache@v4 + with: + path: build/docker/.emcache + key: emcache-${{ runner.os }}-wasm-${{ github.sha }} + restore-keys: emcache-${{ runner.os }}-wasm- + + - name: mx::api under Node (wasm) + run: make wasm-test + # The api product surface, built natively with AppleClang. macOS is a # portability check on the product tier only (the core suites run on Linux in # test-linux); this is the only C++ job off the pinned toolchain, by design, diff --git a/AGENTS.md b/AGENTS.md index fa29a1c27..89db5fa8c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -141,6 +141,8 @@ separately. `make test-all` runs every C++ suite at once (core roundtrip + unit api-roundtrip) — the deep gate CI runs on Linux (see `make help` / CI). Adding/removing a `data/` file: bump the pinned count in `CoreRoundtripTest.cpp`, run `make audit` (regenerates `corpus.xml` + `*.features.xml`), confirm round-trip via `make core-roundtrip-test`. `ApiLoadSmokeTest` proves a file imports without crashing, not that the data is correct; the read→write→read gate (`make api-roundtrip` / `roundtrip-baseline.txt`) is the correctness check — pin a fixture there to defend a feature. +`make wasm-test` builds `mx::api` for Emscripten and runs the examples under Node — CI's check that mx +still compiles and works client-side in a browser (a consumer, denigma, depends on this; issue #386). Look at what will run in CI `.github/workflows/ci.yaml` and anticipate issues there when coding locally. Code coverage is not part of the normal CI run; trigger it on demand with a `/coverage` diff --git a/CMakeLists.txt b/CMakeLists.txt index 08a3cf2e9..a62f6c268 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -12,6 +12,17 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON) set(EXECUTABLE_OUTPUT_PATH ${CMAKE_BINARY_DIR}) set(LIBRARY_OUTPUT_PATH ${CMAKE_BINARY_DIR}) +# Emscripten disables exception catching by default, which turns mx's internal +# MX_THROW (see src/private/mx/utility/Throw.h) into a fatal abort instead of +# something DocumentManager can catch and turn into ResultCode::internalError. +# Set directory-scoped (not per-target) so it also covers pugixml/mx_core/mx, +# and set it here rather than relying on downstream consumers (e.g. denigma) +# to remember it before FetchContent-ing mx. +if(EMSCRIPTEN) + add_compile_options(-fexceptions) + add_link_options(-fexceptions) +endif() + option(MX_CORE_DEV "Build the core roundtrip (corert) test binary against the regenerated mx/core." OFF) option(MX_API "Build the mx::api/mx::impl product library and its tests." ON) diff --git a/Dockerfile b/Dockerfile index a56425722..d64b8dd3e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,6 +19,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ libxml2-dev \ libxml2-utils \ pkg-config \ + git \ + ca-certificates \ && rm -rf /var/lib/apt/lists/* # Python quality tooling for `make gen-quality` / `make gen-lint`. Isolated in a @@ -35,6 +37,30 @@ RUN python3 -m venv /opt/quality-venv \ # Unversioned name so the Makefile invokes the formatter without the suffix. RUN ln -sf /usr/bin/clang-format-18 /usr/local/bin/clang-format +# emsdk: the Emscripten toolchain, used to build mx to WebAssembly (issue #386; +# see CMakeLists.txt's EMSCRIPTEN block). Pinned to a specific release, not +# "latest", for the same reproducibility reason every other tool in this image +# is version-pinned. emsdk writes its config (.emscripten) inside $EMSDK by +# default (has since 1.39.x) rather than $HOME, so it needs no help from us +# there -- but its system-library cache defaults to living under $EMSDK too, +# which the container's caller-uid:gid user (no passwd entry, root-owned +# /opt/emsdk) can't write to at runtime; EM_CACHE below redirects that. +# node's bundled bin dir has a version number baked into its path; symlink a +# stable name so PATH below doesn't have to guess it. +ARG EMSDK_VERSION=6.0.6 +RUN git clone --depth 1 --branch ${EMSDK_VERSION} https://github.com/emscripten-core/emsdk.git /opt/emsdk \ + && /opt/emsdk/emsdk install ${EMSDK_VERSION} \ + && /opt/emsdk/emsdk activate ${EMSDK_VERSION} \ + && ln -s "$(find /opt/emsdk/node -maxdepth 2 -type d -name bin)" /opt/emsdk/node/current-bin + +# EM_CACHE (the compiled libc/libc++/compiler-rt system-library cache) is +# redirected to the build volume for the same reason CCACHE_DIR is below: it +# would otherwise try to write under the root-owned /opt/emsdk at runtime. +ENV EMSDK=/opt/emsdk \ + EM_CONFIG=/opt/emsdk/.emscripten \ + EM_CACHE=/workspace/build/.emcache \ + PATH="/opt/emsdk:/opt/emsdk/upstream/emscripten:/opt/emsdk/node/current-bin:${PATH}" + # MX_RUNNING_IN_DOCKER flips the Makefile to its in-container branch. Build with # the pinned GCC; ccache state lives under the mounted build volume. ENV MX_RUNNING_IN_DOCKER=1 \ diff --git a/Makefile b/Makefile index 1b0fb222c..4312ce76e 100644 --- a/Makefile +++ b/Makefile @@ -69,6 +69,7 @@ FIND_CPP := find src \ core-build core-roundtrip-test core-unit core-coverage \ api-lib api-build api-test api-examples api-roundtrip \ api-roundtrip-discover api-roundtrip-dump api-roundtrip-classify api-coverage \ + wasm-lib wasm-build wasm-test \ gen-test audit-test gen-quality gen-lint \ gen gen-cpp gen-go gen-c gen-schema \ audit audit-force \ @@ -93,6 +94,11 @@ help: @echo ' make api-roundtrip-classify Classify dumped failures by root cause (Python).' @echo ' make api-coverage Instrumented api/impl/utility build + gcovr report.' @echo '' + @echo ' WebAssembly (mx::api via Emscripten, issue #386):' + @echo ' make wasm-lib Build the mx static library for wasm (MX_API=ON, emcmake).' + @echo ' make wasm-build wasm-lib plus the mxread/mxwrite/mxhide examples.' + @echo ' make wasm-test Run mxread/mxwrite/mxhide under Node against the wasm build.' + @echo '' @echo ' Core substrate (mx::core):' @echo ' make core-build Build mx_core and the corert + unit test binaries.' @echo " make core-roundtrip-test Run the core roundtrip suite. Filter: ARGS='[core-roundtrip] lysuite/*'" @@ -235,6 +241,35 @@ api-coverage: $(BUILD_ROOT)/cov-api | tee $(COV_DIR)/api/summary.txt @echo "=== api-coverage written to $(COV_DIR)/api/ ===" +# wasm: mx::api built for Emscripten (see CMakeLists.txt's EMSCRIPTEN block for +# the exceptions flag this relies on). Compiler launcher blanked, same as the +# coverage targets above -- ccache wrapping emcc's Python driver is untested +# and not worth the risk for a job that isn't anyone's hot inner loop. +wasm-lib: + emcmake $(CMAKE) -S . -B $(BUILD_ROOT)/wasm \ + -DCMAKE_BUILD_TYPE=$(BUILD_TYPE) \ + -DCMAKE_C_COMPILER_LAUNCHER= \ + -DCMAKE_CXX_COMPILER_LAUNCHER= \ + -DMX_API=on + emmake $(CMAKE) --build $(BUILD_ROOT)/wasm --target mx --parallel $(JOBS) + +wasm-build: wasm-lib + emmake $(CMAKE) --build $(BUILD_ROOT)/wasm --target mxread mxwrite mxhide --parallel $(JOBS) + +# Proves mx::api actually works under wasm, not just that it compiles: +# mxread/mxwrite/mxhide (src/private/mx/examples/) round-trip real ScoreData +# through DocumentManager, the same programs api-examples runs natively. +# Emscripten's CMake toolchain names the outputs *.js; node runs them and +# each one's C++ main() return code becomes node's exit code. mxwrite's +# writeToFile lands in Emscripten's default in-memory MEMFS, not on the real +# filesystem -- the output path won't actually appear under build/wasm/, only +# the write call (and its return code) is being exercised here. +wasm-test: wasm-build + node $(BUILD_ROOT)/wasm/mxread.js + node $(BUILD_ROOT)/wasm/mxwrite.js $(BUILD_ROOT)/wasm/example.musicxml + node $(BUILD_ROOT)/wasm/mxhide.js + @echo 'wasm-test: mxread/mxwrite/mxhide ran successfully under Node (wasm).' + core-build: $(CMAKE) -S . -B $(BUILD_ROOT)/core-dev -DCMAKE_BUILD_TYPE=$(BUILD_TYPE) -DMX_CORE_DEV=on $(CMAKE) --build $(BUILD_ROOT)/core-dev --parallel $(JOBS) @@ -400,6 +435,15 @@ api-coverage: $(DOCKER_STAMP) docker-volume $(DOCKER_RUN) make api-coverage BUILD_TYPE=$(BUILD_TYPE) ARGS='$(ARGS)' @echo "API coverage written to $(COV_DIR)/api/ (open $(COV_DIR)/api/index.html)" +wasm-lib: $(DOCKER_STAMP) docker-volume + $(DOCKER_RUN) make wasm-lib BUILD_TYPE=$(BUILD_TYPE) + +wasm-build: $(DOCKER_STAMP) docker-volume + $(DOCKER_RUN) make wasm-build BUILD_TYPE=$(BUILD_TYPE) + +wasm-test: $(DOCKER_STAMP) docker-volume + $(DOCKER_RUN) make wasm-test BUILD_TYPE=$(BUILD_TYPE) + core-build: $(DOCKER_STAMP) docker-volume $(DOCKER_RUN) make core-build BUILD_TYPE=$(BUILD_TYPE) From 89d2a0d76f9762fab5c408373d5277417d6fb2f2 Mon Sep 17 00:00:00 2001 From: Matthew James Briggs Date: Fri, 7 Aug 2026 20:30:20 +0000 Subject: [PATCH 2/8] fix: install xz-utils in mx-sdk for emsdk's node download 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`. --- .github/workflows/ci.yaml | 4 ++-- Dockerfile | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index a883c6c09..9f6eac463 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -102,8 +102,8 @@ jobs: # rather than discovered as a regression downstream. Builds through the # mx-sdk toolchain like test-linux/quality/gen; runs mxread/mxwrite/mxhide # under Node so the check exercises mx::api, not just a compile. - test-wasm: - name: "wasm: mx::api under Emscripten" + emscripten: + name: emscripten runs-on: ubuntu-latest steps: diff --git a/Dockerfile b/Dockerfile index d64b8dd3e..a0def4dbf 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,6 +21,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ pkg-config \ git \ ca-certificates \ + xz-utils \ && rm -rf /var/lib/apt/lists/* # Python quality tooling for `make gen-quality` / `make gen-lint`. Isolated in a From de4a6cca866728ab8199ad494ac95372f549a499 Mon Sep 17 00:00:00 2001 From: Matthew James Briggs Date: Fri, 7 Aug 2026 20:39:13 +0000 Subject: [PATCH 3/8] fix: stop emsdk's PATH entry from shadowing /usr/bin/cmake 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. --- Dockerfile | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index a0def4dbf..f0d5d043a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -52,15 +52,21 @@ ARG EMSDK_VERSION=6.0.6 RUN git clone --depth 1 --branch ${EMSDK_VERSION} https://github.com/emscripten-core/emsdk.git /opt/emsdk \ && /opt/emsdk/emsdk install ${EMSDK_VERSION} \ && /opt/emsdk/emsdk activate ${EMSDK_VERSION} \ - && ln -s "$(find /opt/emsdk/node -maxdepth 2 -type d -name bin)" /opt/emsdk/node/current-bin + && ln -s "$(find /opt/emsdk/node -maxdepth 2 -type d -name bin)" /opt/emsdk/node/current-bin \ + && rm -rf /opt/emsdk/downloads # EM_CACHE (the compiled libc/libc++/compiler-rt system-library cache) is # redirected to the build volume for the same reason CCACHE_DIR is below: it # would otherwise try to write under the root-owned /opt/emsdk at runtime. +# PATH appends emsdk's dirs rather than prepending them: upstream/emscripten +# (Emscripten's own tree) contains a subdirectory literally named `cmake` +# (its CMake toolchain-file module), and execve() on a directory returns +# EACCES ("Permission denied") -- prepended, it silently shadowed the real +# /usr/bin/cmake for every job, not just the wasm one. ENV EMSDK=/opt/emsdk \ EM_CONFIG=/opt/emsdk/.emscripten \ EM_CACHE=/workspace/build/.emcache \ - PATH="/opt/emsdk:/opt/emsdk/upstream/emscripten:/opt/emsdk/node/current-bin:${PATH}" + PATH="${PATH}:/opt/emsdk:/opt/emsdk/upstream/emscripten:/opt/emsdk/node/current-bin" # MX_RUNNING_IN_DOCKER flips the Makefile to its in-container branch. Build with # the pinned GCC; ccache state lives under the mounted build volume. From 133e3e711c5e37bce7a256c2ca8dd44bc6f45c54 Mon Sep 17 00:00:00 2001 From: Matthew James Briggs Date: Fri, 7 Aug 2026 20:54:14 +0000 Subject: [PATCH 4/8] fix: node PATH shadowing; trim overly long comments 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. --- CMakeLists.txt | 8 ++------ Dockerfile | 23 ++++------------------- Makefile | 16 ++++------------ 3 files changed, 10 insertions(+), 37 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index a62f6c268..4262dd6ee 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -12,12 +12,8 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON) set(EXECUTABLE_OUTPUT_PATH ${CMAKE_BINARY_DIR}) set(LIBRARY_OUTPUT_PATH ${CMAKE_BINARY_DIR}) -# Emscripten disables exception catching by default, which turns mx's internal -# MX_THROW (see src/private/mx/utility/Throw.h) into a fatal abort instead of -# something DocumentManager can catch and turn into ResultCode::internalError. -# Set directory-scoped (not per-target) so it also covers pugixml/mx_core/mx, -# and set it here rather than relying on downstream consumers (e.g. denigma) -# to remember it before FetchContent-ing mx. +# Emscripten disables exception catching by default; mx's internal MX_THROW +# (Throw.h) needs it, so DocumentManager can actually catch and convert it. if(EMSCRIPTEN) add_compile_options(-fexceptions) add_link_options(-fexceptions) diff --git a/Dockerfile b/Dockerfile index f0d5d043a..cb0ae8920 100644 --- a/Dockerfile +++ b/Dockerfile @@ -38,16 +38,7 @@ RUN python3 -m venv /opt/quality-venv \ # Unversioned name so the Makefile invokes the formatter without the suffix. RUN ln -sf /usr/bin/clang-format-18 /usr/local/bin/clang-format -# emsdk: the Emscripten toolchain, used to build mx to WebAssembly (issue #386; -# see CMakeLists.txt's EMSCRIPTEN block). Pinned to a specific release, not -# "latest", for the same reproducibility reason every other tool in this image -# is version-pinned. emsdk writes its config (.emscripten) inside $EMSDK by -# default (has since 1.39.x) rather than $HOME, so it needs no help from us -# there -- but its system-library cache defaults to living under $EMSDK too, -# which the container's caller-uid:gid user (no passwd entry, root-owned -# /opt/emsdk) can't write to at runtime; EM_CACHE below redirects that. -# node's bundled bin dir has a version number baked into its path; symlink a -# stable name so PATH below doesn't have to guess it. +# emsdk: pinned Emscripten toolchain for building mx to WebAssembly (#386). ARG EMSDK_VERSION=6.0.6 RUN git clone --depth 1 --branch ${EMSDK_VERSION} https://github.com/emscripten-core/emsdk.git /opt/emsdk \ && /opt/emsdk/emsdk install ${EMSDK_VERSION} \ @@ -55,18 +46,12 @@ RUN git clone --depth 1 --branch ${EMSDK_VERSION} https://github.com/emscripten- && ln -s "$(find /opt/emsdk/node -maxdepth 2 -type d -name bin)" /opt/emsdk/node/current-bin \ && rm -rf /opt/emsdk/downloads -# EM_CACHE (the compiled libc/libc++/compiler-rt system-library cache) is -# redirected to the build volume for the same reason CCACHE_DIR is below: it -# would otherwise try to write under the root-owned /opt/emsdk at runtime. -# PATH appends emsdk's dirs rather than prepending them: upstream/emscripten -# (Emscripten's own tree) contains a subdirectory literally named `cmake` -# (its CMake toolchain-file module), and execve() on a directory returns -# EACCES ("Permission denied") -- prepended, it silently shadowed the real -# /usr/bin/cmake for every job, not just the wasm one. +# EM_CACHE: like CCACHE_DIR, avoids writing under root-owned /opt/emsdk at runtime. +# PATH is appended, not prepended -- emsdk's own dirs shadow real tools by name (node, cmake). ENV EMSDK=/opt/emsdk \ EM_CONFIG=/opt/emsdk/.emscripten \ EM_CACHE=/workspace/build/.emcache \ - PATH="${PATH}:/opt/emsdk:/opt/emsdk/upstream/emscripten:/opt/emsdk/node/current-bin" + PATH="${PATH}:/opt/emsdk/node/current-bin:/opt/emsdk/upstream/emscripten" # MX_RUNNING_IN_DOCKER flips the Makefile to its in-container branch. Build with # the pinned GCC; ccache state lives under the mounted build volume. diff --git a/Makefile b/Makefile index 4312ce76e..30a8a976f 100644 --- a/Makefile +++ b/Makefile @@ -241,10 +241,8 @@ api-coverage: $(BUILD_ROOT)/cov-api | tee $(COV_DIR)/api/summary.txt @echo "=== api-coverage written to $(COV_DIR)/api/ ===" -# wasm: mx::api built for Emscripten (see CMakeLists.txt's EMSCRIPTEN block for -# the exceptions flag this relies on). Compiler launcher blanked, same as the -# coverage targets above -- ccache wrapping emcc's Python driver is untested -# and not worth the risk for a job that isn't anyone's hot inner loop. +# wasm: mx::api built for Emscripten. Compiler launcher blanked, same as the +# coverage targets above -- ccache wrapping emcc's Python driver is untested. wasm-lib: emcmake $(CMAKE) -S . -B $(BUILD_ROOT)/wasm \ -DCMAKE_BUILD_TYPE=$(BUILD_TYPE) \ @@ -256,14 +254,8 @@ wasm-lib: wasm-build: wasm-lib emmake $(CMAKE) --build $(BUILD_ROOT)/wasm --target mxread mxwrite mxhide --parallel $(JOBS) -# Proves mx::api actually works under wasm, not just that it compiles: -# mxread/mxwrite/mxhide (src/private/mx/examples/) round-trip real ScoreData -# through DocumentManager, the same programs api-examples runs natively. -# Emscripten's CMake toolchain names the outputs *.js; node runs them and -# each one's C++ main() return code becomes node's exit code. mxwrite's -# writeToFile lands in Emscripten's default in-memory MEMFS, not on the real -# filesystem -- the output path won't actually appear under build/wasm/, only -# the write call (and its return code) is being exercised here. +# Runs mxread/mxwrite/mxhide under Node so this exercises mx::api, not just a +# compile. mxwrite's output lands in Emscripten's in-memory MEMFS, not disk. wasm-test: wasm-build node $(BUILD_ROOT)/wasm/mxread.js node $(BUILD_ROOT)/wasm/mxwrite.js $(BUILD_ROOT)/wasm/example.musicxml From ed01cbc8d02d32734aa3eaadf12087e6d9ff4b57 Mon Sep 17 00:00:00 2001 From: Matthew James Briggs Date: Fri, 7 Aug 2026 21:04:26 +0000 Subject: [PATCH 5/8] fix: mxwrite.js output path must be bare under wasm 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. --- Makefile | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 30a8a976f..4a0d51bcf 100644 --- a/Makefile +++ b/Makefile @@ -255,10 +255,12 @@ wasm-build: wasm-lib emmake $(CMAKE) --build $(BUILD_ROOT)/wasm --target mxread mxwrite mxhide --parallel $(JOBS) # Runs mxread/mxwrite/mxhide under Node so this exercises mx::api, not just a -# compile. mxwrite's output lands in Emscripten's in-memory MEMFS, not disk. +# compile. mxwrite's output path is bare (no directory) and lands in +# Emscripten's in-memory MEMFS, not disk -- MEMFS starts empty, so a path +# with a build/wasm/ prefix would fail outright with no such directory. wasm-test: wasm-build node $(BUILD_ROOT)/wasm/mxread.js - node $(BUILD_ROOT)/wasm/mxwrite.js $(BUILD_ROOT)/wasm/example.musicxml + node $(BUILD_ROOT)/wasm/mxwrite.js example.musicxml node $(BUILD_ROOT)/wasm/mxhide.js @echo 'wasm-test: mxread/mxwrite/mxhide ran successfully under Node (wasm).' From daa7fbe71f5826d0d0426395db1e160aa9253ea5 Mon Sep 17 00:00:00 2001 From: Matthew James Briggs Date: Fri, 7 Aug 2026 21:18:51 +0000 Subject: [PATCH 6/8] perf: re-enable ccache for the wasm build 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. --- .github/workflows/ci.yaml | 14 +++++++++++--- Makefile | 6 ++---- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 9f6eac463..27130c228 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -119,9 +119,17 @@ jobs: --opt type=none --opt o=bind \ --opt device="$GITHUB_WORKSPACE/build/docker" mx-build - # Emscripten's compiled system-library cache (libc/libc++/compiler-rt), - # not source ccache -- see EM_CACHE in the Dockerfile. Populating it from - # scratch is the slow part of a wasm build. + # Two caches: ccache for mx's own object files (emcc via the ambient + # CMAKE_*_COMPILER_LAUNCHER), EM_CACHE for Emscripten's compiled + # system libraries (libc/libc++/compiler-rt). Without both, this job + # recompiles everything from scratch on every run. + - name: Cache ccache + uses: actions/cache@v4 + with: + path: build/docker/.ccache + key: ccache-${{ runner.os }}-wasm-${{ github.sha }} + restore-keys: ccache-${{ runner.os }}-wasm- + - name: Cache EM_CACHE uses: actions/cache@v4 with: diff --git a/Makefile b/Makefile index 4a0d51bcf..36132fcf3 100644 --- a/Makefile +++ b/Makefile @@ -241,13 +241,11 @@ api-coverage: $(BUILD_ROOT)/cov-api | tee $(COV_DIR)/api/summary.txt @echo "=== api-coverage written to $(COV_DIR)/api/ ===" -# wasm: mx::api built for Emscripten. Compiler launcher blanked, same as the -# coverage targets above -- ccache wrapping emcc's Python driver is untested. +# wasm: mx::api built for Emscripten. ccache wraps emcc via the ambient +# CMAKE_*_COMPILER_LAUNCHER env (Dockerfile), same as every other job. wasm-lib: emcmake $(CMAKE) -S . -B $(BUILD_ROOT)/wasm \ -DCMAKE_BUILD_TYPE=$(BUILD_TYPE) \ - -DCMAKE_C_COMPILER_LAUNCHER= \ - -DCMAKE_CXX_COMPILER_LAUNCHER= \ -DMX_API=on emmake $(CMAKE) --build $(BUILD_ROOT)/wasm --target mx --parallel $(JOBS) From 21c2ed27f661f5e7b7df17b1c7f7f0ddfe3df4b1 Mon Sep 17 00:00:00 2001 From: Matthew James Briggs Date: Fri, 7 Aug 2026 21:34:40 +0000 Subject: [PATCH 7/8] ci: retrigger to measure ccache warm-cache speedup on the wasm job From 24aef018a60e6f8d393d18dbb7331eddd67b60c1 Mon Sep 17 00:00:00 2001 From: Matthew James Briggs Date: Fri, 7 Aug 2026 21:47:35 +0000 Subject: [PATCH 8/8] docs: trim comments per review (2-3 lines max) --- .github/workflows/ci.yaml | 12 +++--------- Makefile | 6 ++---- 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 27130c228..368846a16 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -97,11 +97,7 @@ jobs: run: make test-all # mx::api cross-compiled to WebAssembly via the pinned mx-sdk emsdk install - # (issue #386): a consumer (denigma, github.com/rpatters1/denigma) compiles mx - # client-side for browser use, and this is the one place that path is proven - # rather than discovered as a regression downstream. Builds through the - # mx-sdk toolchain like test-linux/quality/gen; runs mxread/mxwrite/mxhide - # under Node so the check exercises mx::api, not just a compile. + # (#386) -- denigma (rpatters1/denigma) depends on this path staying green. emscripten: name: emscripten runs-on: ubuntu-latest @@ -119,10 +115,8 @@ jobs: --opt type=none --opt o=bind \ --opt device="$GITHUB_WORKSPACE/build/docker" mx-build - # Two caches: ccache for mx's own object files (emcc via the ambient - # CMAKE_*_COMPILER_LAUNCHER), EM_CACHE for Emscripten's compiled - # system libraries (libc/libc++/compiler-rt). Without both, this job - # recompiles everything from scratch on every run. + # ccache for mx's own object files; EM_CACHE (below) is Emscripten's + # separate cache for its compiled system libraries. - name: Cache ccache uses: actions/cache@v4 with: diff --git a/Makefile b/Makefile index 36132fcf3..f67175431 100644 --- a/Makefile +++ b/Makefile @@ -252,10 +252,8 @@ wasm-lib: wasm-build: wasm-lib emmake $(CMAKE) --build $(BUILD_ROOT)/wasm --target mxread mxwrite mxhide --parallel $(JOBS) -# Runs mxread/mxwrite/mxhide under Node so this exercises mx::api, not just a -# compile. mxwrite's output path is bare (no directory) and lands in -# Emscripten's in-memory MEMFS, not disk -- MEMFS starts empty, so a path -# with a build/wasm/ prefix would fail outright with no such directory. +# Runs mxread/mxwrite/mxhide under Node, exercising mx::api, not just a compile. +# mxwrite's output path is bare -- Emscripten's MEMFS starts empty, no build/wasm/ dir. wasm-test: wasm-build node $(BUILD_ROOT)/wasm/mxread.js node $(BUILD_ROOT)/wasm/mxwrite.js example.musicxml