diff --git a/.gitignore b/.gitignore index a7bffed..e703372 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,9 @@ cmake-build-*/ # Emscripten cache .emscripten_cache/ +# Local emsdk installation (managed by scripts/setup-emsdk.sh) +/tools/emsdk/ + # Clean clone for reproducible builds (generated by build-wxwidgets-wasm-clean.sh) wxwidgets-clean/ diff --git a/README.md b/README.md index 871ea29..18506ea 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ npm run test:kicad # KiCad tests (2 tests) ### wxWidgets Only (No Docker) ```bash -# Requires: Emscripten SDK 4.0+, Node.js 18+ +# Requires: Node.js 18+ (Emscripten SDK auto-installed on first build) ./scripts/build-wxuniversal-wasm.sh ./scripts/build-wasm-test.sh cd tests && npm install && npm test @@ -117,15 +117,15 @@ Output: `tests/apps/standalone/` - 10+ GB disk space for build cache ### For wxWidgets Build (Local) -- Emscripten SDK 4.0+ - Node.js 18+ (for tests) +- Emscripten SDK (auto-installed on first build) ```bash -# macOS -brew install emscripten node - # Initialize submodules git submodule update --init --recursive + +# Install Emscripten SDK (auto-runs on first build, or run manually) +./scripts/setup-emsdk.sh ``` ## Testing diff --git a/docker/Dockerfile b/docker/Dockerfile index 2e2cca1..fc69fca 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,5 +1,5 @@ # Use ARM64-native image for Apple Silicon (M1/M2/M3/M4) -FROM emscripten/emsdk:4.0.22-arm64 +FROM emscripten/emsdk:4.0.2-arm64 # Install build tools required for KiCad WASM build RUN apt-get update && apt-get install -y \ diff --git a/docker/build.sh b/docker/build.sh index 7dc66ca..30f8561 100755 --- a/docker/build.sh +++ b/docker/build.sh @@ -33,6 +33,7 @@ docker compose -f docker/docker-compose.yml up -d # This avoids the timestamp mismatch cycle that caused full rebuilds every time. # Transferred files get current container time, so make detects them correctly. echo "Syncing source code to container..." +# rsync exit code 24 = "some files vanished before transfer" (harmless race condition) docker compose -f docker/docker-compose.yml exec kicad-wasm-builder \ rsync -r --delete --checksum \ --exclude="build-wasm" \ @@ -41,7 +42,8 @@ docker compose -f docker/docker-compose.yml exec kicad-wasm-builder \ --exclude="logs" \ --exclude=".idea" \ --exclude="node_modules" \ - /workspace-host/ /workspace/ + --exclude="tools/emsdk" \ + /workspace-host/ /workspace/ || [ $? -eq 24 ] # Run build command (without asyncify - handled on host due to memory requirements) docker compose -f docker/docker-compose.yml exec kicad-wasm-builder \ diff --git a/scripts/build-gal-webgl-test.sh b/scripts/build-gal-webgl-test.sh index 1e9ad24..e0aca78 100755 --- a/scripts/build-gal-webgl-test.sh +++ b/scripts/build-gal-webgl-test.sh @@ -21,7 +21,7 @@ set -e SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# Source common environment (sets up Python 3.10+ for Emscripten) +# Source common environment (sets up local emsdk) QUIET=1 source "$SCRIPT_DIR/common/env.sh" TEST_DIR="$PROJECT_ROOT/tests/gal-regression/wasm" @@ -35,7 +35,7 @@ echo " clang: $(which clang)" # Verify em++ is available if ! command -v em++ &> /dev/null; then - echo "ERROR: em++ not found. Please install via: brew install emscripten" + echo "ERROR: em++ not found. Run: ./scripts/setup-emsdk.sh" exit 1 fi diff --git a/scripts/build-wxuniversal-wasm.sh b/scripts/build-wxuniversal-wasm.sh index 836df82..6283e30 100755 --- a/scripts/build-wxuniversal-wasm.sh +++ b/scripts/build-wxuniversal-wasm.sh @@ -4,12 +4,12 @@ # Redirect all output to a log file (re-execs script with redirection) source "$(dirname "$0")/common/logging.sh" -# Source common environment (sets up EMSDK_PYTHON for Emscripten 4.0.22+) +# Source common environment (sets up local emsdk) source "$(dirname "$0")/common/env.sh" # This builds the GUI-enabled wxWidgets needed for KiCad # # Prerequisites: -# - Emscripten SDK installed and activated +# - Emscripten SDK (auto-installed by env.sh via scripts/setup-emsdk.sh) # - autoconf (for regenerating configure from configure.in) # # To regenerate Makefile.in from bakefiles (after modifying files.bkl): @@ -23,14 +23,6 @@ source "$(dirname "$0")/common/env.sh" set -e -# Ensure 'python' command is available (macOS only has python3) -# Homebrew's python libexec has the python -> python3 symlink -if [[ -d "/opt/homebrew/opt/python/libexec/bin" ]]; then - export PATH="/opt/homebrew/opt/python/libexec/bin:$PATH" -elif [[ -d "/usr/local/opt/python/libexec/bin" ]]; then - export PATH="/usr/local/opt/python/libexec/bin:$PATH" -fi - SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" BUILD_DIR="$PROJECT_ROOT/build-wasm/wxwidgets-universal" diff --git a/scripts/common/env.sh b/scripts/common/env.sh index 268b94a..7916a74 100755 --- a/scripts/common/env.sh +++ b/scripts/common/env.sh @@ -36,47 +36,18 @@ export WX_BUILD="$BUILD_ROOT/wxwidgets-universal" # Emscripten settings export EMSDK_QUIET=1 -# Homebrew Emscripten configuration -# The em++ script uses EMSDK_PYTHON (not PYTHON env var) for the Python interpreter -# and finds clang via PATH - Emscripten bundles its own LLVM with WebAssembly support -if [[ -d "/opt/homebrew/Cellar/emscripten" ]]; then - _EM_VERSION=$(ls /opt/homebrew/Cellar/emscripten/ | sort -V | tail -1) - _EM_LLVM_BIN="/opt/homebrew/Cellar/emscripten/$_EM_VERSION/libexec/llvm/bin" - if [[ -d "$_EM_LLVM_BIN" ]]; then - # Add bundled LLVM to PATH so Emscripten finds its clang (not /usr/bin/clang) - export PATH="$_EM_LLVM_BIN:$PATH" - fi -elif [[ -d "/usr/local/Cellar/emscripten" ]]; then - _EM_VERSION=$(ls /usr/local/Cellar/emscripten/ | sort -V | tail -1) - _EM_LLVM_BIN="/usr/local/Cellar/emscripten/$_EM_VERSION/libexec/llvm/bin" - if [[ -d "$_EM_LLVM_BIN" ]]; then - export PATH="$_EM_LLVM_BIN:$PATH" - fi +# Local emsdk: auto-install if missing, then source its environment +# The emsdk bundles its own Python, Node, and LLVM — no Homebrew needed +_EMSDK_DIR="$_KICAD_WASM_PROJECT_ROOT/tools/emsdk" +_EMSDK_ENV="$_EMSDK_DIR/emsdk_env.sh" + +if [ ! -f "$_EMSDK_ENV" ]; then + echo "Emscripten SDK not found. Installing..." + "$_KICAD_WASM_SCRIPTS_DIR/setup-emsdk.sh" fi -# Emscripten 4.0.22+ requires Python 3.10+ (uses match statement and type union syntax) -# The em++ shell script checks EMSDK_PYTHON first, then falls back to `which python3` -# Set EMSDK_PYTHON to Homebrew's Python to ensure correct version is used -if [[ -d "/opt/homebrew/opt/python@3.14/bin" ]]; then - export EMSDK_PYTHON="/opt/homebrew/opt/python@3.14/bin/python3.14" -elif [[ -d "/opt/homebrew/opt/python@3.13/bin" ]]; then - export EMSDK_PYTHON="/opt/homebrew/opt/python@3.13/bin/python3.13" -elif [[ -d "/opt/homebrew/opt/python@3.12/bin" ]]; then - export EMSDK_PYTHON="/opt/homebrew/opt/python@3.12/bin/python3.12" -elif [[ -d "/opt/homebrew/opt/python@3.11/bin" ]]; then - export EMSDK_PYTHON="/opt/homebrew/opt/python@3.11/bin/python3.11" -elif [[ -d "/opt/homebrew/opt/python@3.10/bin" ]]; then - export EMSDK_PYTHON="/opt/homebrew/opt/python@3.10/bin/python3.10" -elif [[ -d "/usr/local/opt/python@3.14/bin" ]]; then - export EMSDK_PYTHON="/usr/local/opt/python@3.14/bin/python3.14" -elif [[ -d "/usr/local/opt/python@3.13/bin" ]]; then - export EMSDK_PYTHON="/usr/local/opt/python@3.13/bin/python3.13" -elif [[ -d "/usr/local/opt/python@3.12/bin" ]]; then - export EMSDK_PYTHON="/usr/local/opt/python@3.12/bin/python3.12" -elif [[ -d "/usr/local/opt/python@3.11/bin" ]]; then - export EMSDK_PYTHON="/usr/local/opt/python@3.11/bin/python3.11" -elif [[ -d "/usr/local/opt/python@3.10/bin" ]]; then - export EMSDK_PYTHON="/usr/local/opt/python@3.10/bin/python3.10" +if [ -f "$_EMSDK_ENV" ]; then + source "$_EMSDK_ENV" 2>/dev/null fi # Common compiler flags diff --git a/scripts/common/functions.sh b/scripts/common/functions.sh index 36e8b76..82d2c09 100755 --- a/scripts/common/functions.sh +++ b/scripts/common/functions.sh @@ -53,7 +53,7 @@ setup_error_trap() { # Verify Emscripten is available verify_emscripten() { if ! command -v emcc &> /dev/null; then - log_error "Emscripten not found. Please run: source /path/to/emsdk/emsdk_env.sh" + log_error "Emscripten not found. Run: ./scripts/setup-emsdk.sh" exit 1 fi log_info "Using Emscripten: $(emcc --version | head -1)" diff --git a/scripts/common/get-wasm-opt.sh b/scripts/common/get-wasm-opt.sh index 647505e..a20b980 100755 --- a/scripts/common/get-wasm-opt.sh +++ b/scripts/common/get-wasm-opt.sh @@ -1,17 +1,35 @@ #!/bin/bash -# Download and cache Binaryen wasm-opt +# Get path to Binaryen wasm-opt # -# Usage: ./scripts/tools/get-wasm-opt.sh +# Usage: ./scripts/common/get-wasm-opt.sh # Output: Prints path to wasm-opt executable # -# Binaryen v121 is used because v125 has a regression causing asyncify crashes. -# The binary is cached in tools/binaryen-121/ +# Prefers the emsdk-bundled Binaryen (tools/emsdk/upstream/bin/) so that the +# wasm-opt and wasm-emscripten-finalize versions match the Emscripten that +# generated the JS glue. A version mismatch between the compiler's Binaryen +# (e.g. v121+72 dev) and a standalone release (v121) corrupts asyncify +# metadata, causing "func is not a function" errors at runtime. +# +# Falls back to downloading standalone Binaryen v121 if emsdk is not installed +# locally (e.g. CI environments that only use Docker). set -e SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" +# --- Prefer emsdk-bundled Binaryen (matches the Emscripten that compiled the WASM) --- +EMSDK_WASM_OPT="${PROJECT_ROOT}/tools/emsdk/upstream/bin/wasm-opt" +if [ -x "${EMSDK_WASM_OPT}" ]; then + EMSDK_VERSION=$("${EMSDK_WASM_OPT}" --version 2>&1 || true) + echo "Using emsdk-bundled Binaryen: ${EMSDK_VERSION}" >&2 + echo "${EMSDK_WASM_OPT}" + exit 0 +fi + +# --- Fallback: download standalone Binaryen (for CI or environments without local emsdk) --- +echo "emsdk Binaryen not found at ${EMSDK_WASM_OPT}, falling back to standalone download..." >&2 + BINARYEN_VERSION="121" BINARYEN_DIR="${PROJECT_ROOT}/build-wasm/tools/binaryen-${BINARYEN_VERSION}" WASM_OPT="${BINARYEN_DIR}/bin/wasm-opt" diff --git a/scripts/common/inject-dyncall-shims.sh b/scripts/common/inject-dyncall-shims.sh index d89ea9b..d0d2e74 100755 --- a/scripts/common/inject-dyncall-shims.sh +++ b/scripts/common/inject-dyncall-shims.sh @@ -38,6 +38,11 @@ cat > "$SHIM_FILE" << 'HEADER' // === dynCall shims for Emscripten exception handling === // Auto-generated: maps dynCall_SIG() calls to getWasmTableEntry() // This fixes "dynCall_* is not defined" errors in Emscripten 4.x +// +// These shims are asyncify-aware: they track Asyncify.exportCallStack so that +// asyncify can rewind through indirect calls (e.g., main loop callbacks, +// timer callbacks, event handlers). Without this tracking, asyncify's doRewind +// fails because it can't find the entry function to re-enter the WASM module. HEADER for sig in $SIGNATURES; do @@ -55,12 +60,31 @@ for sig in $SIGNATURES; do call_args="${call_args}a$i" done - echo "var dynCall_$sig = ($args) => getWasmTableEntry(index)($call_args);" >> "$SHIM_FILE" + cat >> "$SHIM_FILE" << SHIMEOF +function dynCall_$sig($args) { + var tableFunc = getWasmTableEntry(index); + if (typeof Asyncify !== 'undefined') { + var rewindKey = '__dyn_${sig}_' + index; + if (!wasmExports[rewindKey]) wasmExports[rewindKey] = tableFunc; + Asyncify.exportCallStack.push(rewindKey); + try { + return tableFunc($call_args); + } finally { + if (!ABORT) { + Asyncify.exportCallStack.pop(); + Asyncify.maybeStopUnwind(); + } + } + } + return tableFunc($call_args); +} +SHIMEOF done echo "" >> "$SHIM_FILE" echo "// === End dynCall shims ===" >> "$SHIM_FILE" + # Find the insertion point: after getWasmTableEntry definition # The pattern is: # var getWasmTableEntry = funcPtr => { @@ -169,6 +193,18 @@ if [ "$COUNT_BEFORE" -gt 0 ]; then TOTAL_FIXED=$((TOTAL_FIXED + FIXED)) fi +# Fix 5: _emscripten_set_main_loop empty iterFunc callback - signature v (void, no args) +# Pattern: var iterFunc = (() => {}); +# The 'func' variable is the function pointer passed to _emscripten_set_main_loop +COUNT_BEFORE=$(grep -c 'var iterFunc = (() => {});' "$JS_FILE" || true) +if [ "$COUNT_BEFORE" -gt 0 ]; then + sed -i '' 's/var iterFunc = (() => {});/var iterFunc = () => dynCall_v(func);/g' "$JS_FILE" + COUNT_AFTER=$(grep -c 'var iterFunc = (() => {});' "$JS_FILE" || true) + FIXED=$((COUNT_BEFORE - COUNT_AFTER)) + echo " Fixed $FIXED main loop callback(s) (dynCall_v)" + TOTAL_FIXED=$((TOTAL_FIXED + FIXED)) +fi + if [ "$TOTAL_FIXED" -gt 0 ]; then echo "Total: Fixed $TOTAL_FIXED empty callback(s)" else diff --git a/scripts/common/versions.sh b/scripts/common/versions.sh index 1cb9739..85d5a40 100755 --- a/scripts/common/versions.sh +++ b/scripts/common/versions.sh @@ -2,6 +2,9 @@ # Dependency versions for KiCad WASM build # These versions match KiCad 8.99 requirements from CMakeLists.txt and vcpkg.json +# Emscripten SDK version (single source of truth for Docker and local builds) +export EMSCRIPTEN_VERSION="4.0.2" + # KiCad submodule version export KICAD_COMMIT="4bfed3f1746e8cc0a7d942767770f56fa28b393c" export KICAD_VERSION="8.99" diff --git a/scripts/setup-emsdk.sh b/scripts/setup-emsdk.sh new file mode 100755 index 0000000..be93317 --- /dev/null +++ b/scripts/setup-emsdk.sh @@ -0,0 +1,60 @@ +#!/bin/bash +# Install and activate the Emscripten SDK (emsdk) locally +# +# This script is idempotent - it skips installation if the correct version +# is already installed. The emsdk is installed into tools/emsdk/ which is +# gitignored (~1.5GB). +# +# Usage: +# ./scripts/setup-emsdk.sh # Install/activate pinned version +# ./scripts/setup-emsdk.sh --force # Force reinstall + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" + +# Source version constants +source "$SCRIPT_DIR/common/versions.sh" + +EMSDK_DIR="$PROJECT_ROOT/tools/emsdk" +VERSION="$EMSCRIPTEN_VERSION" + +echo "=== Emscripten SDK Setup ===" +echo " Version: $VERSION" +echo " Location: $EMSDK_DIR" + +# Check if already installed with correct version (unless --force) +if [ "$1" != "--force" ] && [ -f "$EMSDK_DIR/emsdk" ]; then + if "$EMSDK_DIR/emsdk" list 2>/dev/null | grep -q "^[[:space:]]*${VERSION}[[:space:]]*INSTALLED"; then + echo "Emscripten $VERSION is already installed and active." + echo "Use --force to reinstall." + exit 0 + fi +fi + +# Clone emsdk if not present +if [ ! -d "$EMSDK_DIR" ]; then + echo "" + echo "Cloning emsdk..." + mkdir -p "$PROJECT_ROOT/tools" + git clone https://github.com/emscripten-core/emsdk.git "$EMSDK_DIR" +else + echo "" + echo "Updating emsdk..." + (cd "$EMSDK_DIR" && git pull) +fi + +# Install and activate the pinned version +echo "" +echo "Installing Emscripten $VERSION..." +"$EMSDK_DIR/emsdk" install "$VERSION" + +echo "" +echo "Activating Emscripten $VERSION..." +"$EMSDK_DIR/emsdk" activate "$VERSION" + +echo "" +echo "=== Setup complete ===" +echo "Emscripten $VERSION is ready." +echo "Source env.sh to use it: source scripts/common/env.sh" diff --git a/wxwidgets b/wxwidgets index dd370ac..c5aaa19 160000 --- a/wxwidgets +++ b/wxwidgets @@ -1 +1 @@ -Subproject commit dd370ac191d5f20ea847b1bce4a611464f5baa65 +Subproject commit c5aaa19ca9cffe739d9ccbc99003bc712eda031e