The canvas (wxUniversal) mode is gone (wxwidgets submodule); remove every piece of side-by-side plumbing so there is exactly one build and one test flow: - scripts/build-wxuniversal-wasm.sh -> scripts/build-wx-wasm.sh; no --dom/--enable-universal; builds into build-wasm/wxwidgets - build-wasm-test.sh: no DOM_BUILD / apps-dom rsync mirror / PORT=dom; apps build straight into tests/apps (Makefile.wasm PORT conditionals collapsed; wx.js + wx-dom.js always pre-js) - docker/build.sh, build-kicad-target.sh, env.sh: WX_PORT / -dom / -universal suffixes removed; kicad builds to kicad-<app>, outputs to output/; wx.js/wx-dom.js copied from the real source path (/workspace/wxwidgets/build/wasm — the old build-wasm path never existed and silently failed) - setup-kicad-wasm.sh: single target dir; the perl wx-dom.js injection is gone — the 7 checked-in kicad pages now reference wx-dom.js directly - playwright configs serve apps/; fixtures drop the test-results/dom and logs/wxwidgets/dom namespacing; boot.spec asserts wxDomPort unconditionally; pcbnew.spec uses one reference image; appearance.spec assertions unconditional - compare/update-baseline-screenshots.sh: --port removed - tests/gal-regression/wasm/Makefile: links build-wasm/wxwidgets and carries wx-dom.js as a second pre-js — the gal-webgl suite (30 specs) now actually builds and runs here (it needed host-side boost+glm via scripts/deps; the bundle had been missing, timing the whole spec out) - tests: clickCanvas() dispatches via page.mouse (DOM widgets legitimately cover the canvas; locator actionability refused the click); the comprehensive spec drives wxChoice through its native <select> (browser-owned popup cannot be coordinate-clicked) - docs: README/CLAUDE.md/build.md script names and dirs; features/wx-dom-port README reframed (DOM is THE port), visual-notes bugs 26-28; FindwxWidgets.cmake config label drops 'wasmuniv' - wxwidgets submodule -> 9dbacc9448 (DOM-only port, fork diff shrunk) Gate: full wx e2e suite 292 passed / 1 skipped / 0 failed — first run ever with the gal-webgl specs green (28 scenarios + load + sequential). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
9.6 KiB
KiCad WASM Build System
This document describes how to build KiCad for WebAssembly using the Docker-based build system.
Prerequisites
Docker
- Docker Desktop with ARM64 support (for Apple Silicon) or x86_64
- 10+ GB disk space for build cache
- Recommended: 10 CPUs, 32GB RAM allocated to Docker
Host Tools
Binaryen (wasm-opt) is downloaded automatically by the build script. No manual installation needed.
Quick Start
# Build KiCad WASM (with debug symbols by default, sequential compilation)
./docker/build.sh
# Build with parallel compilation (faster, requires more RAM)
./docker/build.sh -j 4
# Build optimized release (smaller WASM, no debug symbols)
./docker/build.sh --release
# Interactive shell for debugging
./docker/shell.sh
Note: Builds run sequentially by default (-j 1) to avoid memory exhaustion in Docker. Use -j N for parallel compilation if you have sufficient RAM (at least 16GB for -j 4).
Build outputs:
build-wasm/kicad-pcbnew/pcbnew/pcbnew.js- Main WASM loaderbuild-wasm/kicad-pcbnew/pcbnew/pcbnew.wasm- WASM binarybuild-wasm/kicad-pcbnew/pcbnew/pcbnew.wasm.map- Source map (debug builds)
Two-Phase Build
The build is split into two phases due to memory requirements:
Phase 1: Docker Compilation
Compiles KiCad to WASM without asyncify transformation. This runs inside Docker with 32GB memory limit.
Phase 2: Host Asyncify
Applies wasm-opt --asyncify on the host machine using Binaryen v121 (downloaded automatically to tools/). This transformation uses ~20-30GB RAM.
Note: Binaryen v121 is used because v125 has a regression causing crashes in the asyncify liveness analysis.
Why Asyncify?
Asyncify is an Emscripten transformation that allows WASM code to pause and resume execution. This is required for:
- Modal dialogs -
wxDialog::ShowModal()blocks until user closes the dialog - Message boxes -
wxMessageBox()waits for user response - Clipboard operations - Browser clipboard API is async
- Sleep/wait operations - Any blocking call that needs to yield to the browser
Without asyncify, modal dialogs would freeze the browser because WASM cannot yield control back to JavaScript's event loop.
How It Works
docker/build.shcompiles KiCad in Docker (no asyncify flags)- Output is copied to
./output/directory wasm-opt --asyncifyruns on host, transforming the WASM binary- Final output is ready for browser execution
Technical Details
The asyncify transformation:
- Instruments every function that might be on the call stack during an async operation
- Adds stack save/restore logic to unwind and rewind the WASM stack
- Increases binary size by ~20% (141MB → 171MB for KiCad)
- Uses
asyncify-importspattern matching to identify async entry points
Import patterns used:
env.invoke_*- Exception handling trampolinesenv.__asyncjs__*- EM_ASYNC_JS functions (likestartModal())
Docker Architecture
Base image: emscripten/emsdk:4.0.2-arm64
Volumes:
- Source code bind mount: Project root →
/workspace - Build cache (named volume):
kicad-build-cache→/workspace/build-wasm - Output bind mount:
./output→/workspace/output
Entry scripts:
| Script | Purpose |
|---|---|
docker/build.sh |
Run build from host |
docker/shell.sh |
Interactive shell in container |
docker/entrypoint.sh |
Sources Emscripten environment |
Dependencies
| Dependency | Version | Build System | Purpose |
|---|---|---|---|
| GLM | 0.9.9.8 | Header-only | Math library |
| Zstd | 1.5.5 | CMake | Compression for project files |
| Protobuf | 3.21.12 | CMake | IPC serialization |
| FreeType | 2.13.2 | CMake | Font rendering |
| HarfBuzz | 8.3.0 | CMake | Text shaping |
| Pixman | 0.42.2 | Meson | Pixel manipulation |
| Cairo | 1.18.0 | Meson | 2D graphics rendering |
| Boost | 1.84.0 | B2 | Locale library |
| wxWidgets | 3.3.1 | Autoconf | GUI framework |
| OpenCASCADE | 7.8.0 | CMake | 3D geometry (optional) |
| ngspice | 45.2 | Autoconf | SPICE simulation (optional) |
Build Order
- Header-only: GLM
- Compression/serialization: Zstd, Protobuf
- Font stack: FreeType → HarfBuzz
- Graphics: Pixman → Cairo
- Optional: OpenCASCADE, ngspice
- GUI framework: wxWidgets
- Application: KiCad PCBnew
Build Flags
| Flag | Description |
|---|---|
--full |
Full clean rebuild (all deps + wxWidgets + KiCad) |
--clean-kicad |
Clean only KiCad build directory |
--build-deps |
Build dependencies (skipped by default) |
--release |
Disable debug symbols, enable optimizations |
--debug |
Enable debug symbols (default) |
-j N |
Parallel jobs (default: all cores) |
Build Modes
| Mode | Command | Description |
|---|---|---|
| Incremental (default) | ./docker/build.sh |
Fastest for development (~1.5 min) |
| Full rebuild | ./docker/build.sh --full |
Clean everything and rebuild |
| Rebuild KiCad | ./docker/build.sh --clean-kicad |
Clean and rebuild KiCad only |
| With dependencies | ./docker/build.sh --build-deps |
Also rebuild dependencies |
Full rebuild removes:
build-wasm/stamps/*- All build stampsbuild-wasm/deps/*- All dependency buildsbuild-wasm/wxwidgets- wxWidgets buildbuild-wasm/sysroot/*- Installed headers/librariesbuild-wasm/kicad-pcbnew- KiCad build
Incremental Build System
The build system is optimized for fast development iteration:
How It Works
- ccache: Caches compiled objects by hashing preprocessed source
- wxWidgets:
configureruns once,makehandles file-level dependencies - KiCad: CMake tracks dependencies, only recompiles changed files
- Asyncify: Post-processing runs every build (~1 min, irreducible minimum)
Performance
| Scenario | Time |
|---|---|
| No changes | ~1.5 min |
| Single file change (KiCad or wxWidgets) | ~1.5 min |
| Full rebuild | ~10 min |
Most time is spent on asyncify post-processing which runs on every build.
Debug vs Release
Debug (default):
- Compiler:
-g -O0(DWARF symbols, no optimization) - Linker:
-gsource-map(JavaScript source maps) - Output:
~30-50MBWASM with.wasm.mapfile - Use for: Development, debugging WASM exceptions
Release:
- Compiler:
-O2(optimized) - Output:
~15MBWASM - Use for: Production deployment
Stamp-based Caching
Dependency build progress is tracked with stamp files in build-wasm/stamps/:
build-wasm/stamps/
├── zstd.stamp
├── protobuf.stamp
├── freetype.stamp
├── harfbuzz.stamp
├── pixman.stamp
├── cairo.stamp
└── kicad-pcbnew.stamp
Note: wxWidgets and KiCad use make/CMake for incremental builds instead of stamps.
Clear specific component: rm build-wasm/stamps/zstd.stamp
Clear all stamps: rm -f build-wasm/stamps/*.stamp
After changing build flags (debug/release), use --full to force a complete rebuild.
Build Scripts
| Script | Purpose |
|---|---|
docker/build.sh |
Host entry point (starts Docker, runs build) |
scripts/kicad/build-pcbnew.sh |
KiCad PCBnew build (runs inside Docker) |
scripts/build-wx-wasm.sh |
wxWidgets build |
scripts/build-wasm-test.sh |
Build wxWidgets test apps |
scripts/deps/build-all-deps.sh |
All dependencies |
scripts/deps/build-*.sh |
Individual dependency builds |
scripts/common/env.sh |
Environment setup |
scripts/common/functions.sh |
Shared utilities |
scripts/common/versions.sh |
Dependency versions |
Build Times
| Component | Approximate Time |
|---|---|
| Dependencies (all) | 20-60 minutes |
| wxWidgets | 10-20 minutes |
| KiCad PCBnew | 5-15 minutes |
| Total fresh build | 1-2 hours |
OpenCASCADE is the longest dependency to build (~30 minutes).
Troubleshooting
Container freezes during build
- Check Docker resource allocation (increase CPU/memory)
- Reduce parallel jobs:
./docker/build.sh -j 4 - OpenCASCADE is resource-intensive; consider skipping with separate builds
Build fails with missing dependency
- Clear the specific stamp:
rm build-wasm/stamps/<dep>.stamp - Re-run build
Incremental build not picking up changes
- For KiCad: use
--clean-kicadto force rebuild - For wxWidgets: delete
build-wasm/wxwidgets/Makefileto force reconfigure
WASM exception with numeric error (e.g., 3788888)
- Build with debug symbols (default): No
--releaseflag - Check for
.wasm.mapfile - Use Chrome DevTools to debug with source maps
Clear build cache completely
docker volume rm docker_kicad-build-cache
WASM Compatibility Layer
The WASM port requires compatibility layers for browser execution:
| Directory | Purpose |
|---|---|
wasm/kiplatform/ |
Platform abstraction (app, UI, printing, etc.) |
wasm/libcontext/ |
Coroutine/fiber implementation for Asyncify |
wasm/stubs/ |
Stub implementations (libgit2, curl) |
wasm/config/ |
Build configuration headers |
Emscripten Flags
Key flags used in the build:
-pthread -sUSE_PTHREADS=1 # Threading support
-sASYNCIFY=1 # Async coroutine support
-sALLOW_MEMORY_GROWTH=1 # Dynamic memory
-sINITIAL_MEMORY=256MB # Starting memory
-sMAXIMUM_MEMORY=4GB # Maximum memory
-sLEGACY_GL_EMULATION # OpenGL compatibility
-sMAX_WEBGL_VERSION=2 # WebGL 2.0
Testing
After building, run the test suite:
# Copy WASM output to test directory
./tests/scripts/setup-kicad-wasm.sh
# Run KiCad tests
cd tests
npm install
npm run test:kicad # Run Playwright tests