diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 5f5d48188..368846a16 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -96,6 +96,44 @@ 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 + # (#386) -- denigma (rpatters1/denigma) depends on this path staying green. + emscripten: + name: 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 + + # 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: + 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: + 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..4262dd6ee 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -12,6 +12,13 @@ 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; 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) +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..cb0ae8920 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,6 +19,9 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ libxml2-dev \ libxml2-utils \ 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 @@ -35,6 +38,21 @@ 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: 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} \ + && /opt/emsdk/emsdk activate ${EMSDK_VERSION} \ + && ln -s "$(find /opt/emsdk/node -maxdepth 2 -type d -name bin)" /opt/emsdk/node/current-bin \ + && rm -rf /opt/emsdk/downloads + +# 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/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. ENV MX_RUNNING_IN_DOCKER=1 \ diff --git a/Makefile b/Makefile index 1b0fb222c..f67175431 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,25 @@ 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. 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) \ + -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) + +# 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 + 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 +425,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)