Add KiCad WASM build infrastructure and dependency scripts

- Add build environment setup (scripts/common/env.sh, versions.sh, functions.sh)
- Add dependency build scripts for Zstd, GLM, FreeType, HarfBuzz, Pixman, Cairo
- Add placeholder scripts for OpenCASCADE, ngspice, protobuf
- Add WASM compatibility layer (wasm/kiplatform/, wasm/libcontext/)
- Add CMake modules for KiCad WASM cross-compilation
- Add PCBnew test infrastructure (tests/kicad/)
- Add build plan documentation

Successfully tested builds: Zstd 1.5.5, GLM 0.9.9.8, FreeType 2.13.2,
HarfBuzz 8.3.0, Pixman 0.42.2, Cairo 1.18.0

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Viktor Vaczi 2025-12-04 13:15:04 +01:00
commit 3034d9d76f
36 changed files with 3708 additions and 0 deletions

View file

@ -6,6 +6,8 @@ The e2e tests are in /tests, with a README and WHATWORKS md files
The e2e tests are separated per feature
The tests depend on canvas, there's an app to find button positions, use that, don't find buttons by estimating pixels
The test have screenshots that are tracked with git, use compare-screenshots.sh to see what changed, update them when a new image is added
The tests have log files after each run where the js console and cpp logs are visible
Always check screenshots for validating tests
Our current goal is to test every wxwidgets feature kicad uses, write the wasm layer and e2e tests, documented in WHATWORKS
Never run builds manually, we have scripts that run the builds in the /scripts folder

View file

@ -0,0 +1,546 @@
# KiCad PCBnew WASM Build Plan
## Goal
Build the full KiCad PCBnew application for WebAssembly with all major features:
- **Target**: PCBnew (PCB Editor)
- **3D/STEP**: OpenCASCADE ported to WASM
- **Simulation**: ngspice ported to WASM
- **Threading**: Emscripten pthreads (true parallelism)
- **Coroutines**: Emscripten Asyncify fibers for libcontext
- **Stub only**: nanodbc (ODBC not available in browser)
## Core Principles
### 1. No Source Modifications to KiCad or wxWidgets
**CRITICAL**: All WASM-specific code must go into compatibility layers, NOT into the KiCad or wxWidgets source trees.
```
kicad-wasm/
├── kicad/ # Git submodule - DO NOT MODIFY
├── wxwidgets/ # Git submodule - DO NOT MODIFY (except WASM platform)
├── wasm/ # NEW: All WASM compatibility layers
│ ├── kiplatform/ # Platform layer implementations
│ ├── libcontext/ # Fiber implementation
│ ├── shims/ # Header shims and wrappers
│ └── stubs/ # Feature stubs
├── stubs/ # (existing) Stub headers/implementations
├── cmake/ # (existing) CMake find modules
└── patches/ # Minimal patches ONLY if absolutely necessary
```
### 2. Compatibility Layer Strategy
Instead of patching KiCad source, we:
1. **Override include paths** - Put our headers first in include path
2. **Provide stub libraries** - Link our stubs instead of real libraries
3. **CMake module overrides** - Replace find_package results with our targets
4. **Platform implementations** - Provide WASM versions of platform-specific code
### 3. Dependencies First Approach
Build all dependencies (including OpenCASCADE and ngspice) BEFORE building KiCad to ensure a clean build.
---
## KiCad Submodule Version
```
Commit: 4bfed3f1746e8cc0a7d942767770f56fa28b393c
Version: 8.99 (development)
```
---
## Exact Dependency Versions
These versions are from KiCad's `CMakeLists.txt` and `vcpkg.json`:
### Required Dependencies
| Dependency | Min Version | Pinned Version | Source |
|------------|-------------|----------------|--------|
| wxWidgets | 3.2.0 | 3.3.1 | vcpkg override |
| GLM | 0.9.8 | 0.9.9.8 | vcpkg override |
| Boost | 1.71.0 | latest | CMakeLists.txt |
| FreeType | 2.11.1 | latest | CMakeLists.txt |
| HarfBuzz | - | latest | CMakeLists.txt |
| Fontconfig | - | latest | CMakeLists.txt |
| Cairo | 1.12 | latest | CMakeLists.txt |
| Pixman | 0.30 | latest | CMakeLists.txt |
| zlib | - | latest | CMakeLists.txt |
| Zstd | - | latest | CMakeLists.txt |
| OpenCASCADE | 7.5.0+ | 7.8.0+ preferred | CMakeLists.txt |
| ngspice | - | 45.2 | vcpkg override |
| Protobuf | 3.21.12 | 3.21.12 | vcpkg override |
| libgit2 | 1.5 | latest | CMakeLists.txt |
| CURL | - | latest | CMakeLists.txt |
| Python | 3.6+ | 3.11.5 | vcpkg override |
### What We Build vs Stub
| Dependency | Action | Reason |
|------------|--------|--------|
| wxWidgets | Already ported | WASM platform in wxwidgets submodule |
| GLM | Header-only | Just include |
| Boost | Header-only subset | Only need headers for most parts |
| FreeType | Emscripten port | `-sUSE_FREETYPE=1` |
| HarfBuzz | Build for WASM | Text shaping needed |
| zlib | Emscripten port | `-sUSE_ZLIB=1` |
| Zstd | Build for WASM | Compression needed |
| OpenCASCADE | Build for WASM | 3D/STEP support |
| ngspice | Build for WASM | Simulation support |
| Cairo | Build for WASM | 2D rendering fallback |
| Pixman | Build for WASM | Cairo dependency |
| libgit2 | **STUB** | No git in browser |
| CURL | **STUB** | Use fetch API instead |
| nanodbc | **STUB** | No ODBC in browser |
| Python/SWIG | **DISABLE** | No Python scripting |
| nng | **STUB** | No IPC in browser |
| SPNAV | **STUB** | No 3D mouse in browser |
---
## Compatibility Layer Structure
### Directory Layout
```
wasm/
├── CMakeLists.txt # Master WASM compat build
├── kiplatform/ # Platform layer for WASM
│ ├── CMakeLists.txt
│ ├── app.cpp # App lifecycle
│ ├── drivers.cpp # GPU detection ("WebGL")
│ ├── environment.cpp # Env vars via localStorage
│ ├── io.cpp # Virtual filesystem
│ ├── policy.cpp # Permissions (always allow)
│ ├── secrets.cpp # Credentials (localStorage)
│ ├── sysinfo.cpp # System info
│ └── printing.cpp # Browser print()
├── libcontext/ # Coroutine implementation
│ ├── CMakeLists.txt
│ └── fcontext_wasm.cpp # Asyncify fiber impl
├── shims/ # Header overrides
│ ├── CMakeLists.txt
│ ├── kiplatform_redirect.h # Redirect to our impl
│ └── libcontext_redirect.h # Redirect to our impl
└── config/ # Build configuration
├── kicad_wasm_config.h # Version/feature config
└── setup.h # Platform setup
```
### Existing Stubs (Already Done)
```
stubs/
├── include/
│ ├── curl/curl.h, easy.h # CURL stubs
│ ├── git2.h # libgit2 stub
│ ├── git2/sys/errors.h, merge.h # libgit2 internals
│ ├── ngspice/sharedspice.h # ngspice header (for stub build)
│ └── Standard_Version.hxx # OCC version stub
└── src/
├── disabled_features_stubs.cpp # CURL/git function stubs
├── kicad_git_stubs.cpp # Git feature stubs
├── kicad_git_all_stubs.cpp # Complete git stubs
├── occ_stubs.cpp # OpenCASCADE stubs
└── panel_git_repos_stub.cpp # Git UI stubs
```
### CMake Overrides (Already Done)
```
cmake/
├── FindCURL.cmake # Returns stub target
├── FindOCC.cmake # Configurable real/stub
├── Findlibgit2.cmake # Returns stub target
├── Findngspice.cmake # Configurable real/stub
└── KicadWasmOptions.cmake # Feature flags
```
---
## Build Phases
### Phase 1: Build Infrastructure
Create common utilities:
```bash
scripts/
├── common/
│ ├── env.sh # Emscripten environment, paths
│ ├── functions.sh # Error handling, logging
│ └── versions.sh # Dependency versions (from above table)
├── build-kicad-wasm.sh # Master orchestrator
└── build-deps/ # Per-dependency scripts
```
**versions.sh** - Pin to KiCad's required versions:
```bash
#!/bin/bash
# Versions matching KiCad 8.99 requirements
export KICAD_COMMIT="4bfed3f1746e8cc0a7d942767770f56fa28b393c"
# From vcpkg.json overrides
export GLM_VERSION="0.9.9.8"
export NGSPICE_VERSION="45.2"
export PROTOBUF_VERSION="3.21.12"
# From CMakeLists.txt minimums
export WXWIDGETS_MIN="3.2.0"
export GLM_MIN="0.9.8"
export BOOST_MIN="1.71.0"
export FREETYPE_MIN="2.11.1"
export CAIRO_MIN="1.12"
export PIXMAN_MIN="0.30"
export LIBGIT2_MIN="1.5"
export OCC_MIN="7.5.0"
# Recommended versions for WASM build
export OCC_VERSION="7.8.0"
export ZSTD_VERSION="1.5.5"
export HARFBUZZ_VERSION="8.3.0"
```
### Phase 2: Dependencies (In Order)
Build these BEFORE KiCad:
| Order | Dependency | Script | Notes |
|-------|------------|--------|-------|
| 1 | zlib | Emscripten port | `-sUSE_ZLIB=1` |
| 2 | Zstd | `build-zstd-wasm.sh` | Compression |
| 3 | FreeType | Emscripten port | `-sUSE_FREETYPE=1` |
| 4 | HarfBuzz | `build-harfbuzz-wasm.sh` | Text shaping |
| 5 | Pixman | `build-pixman-wasm.sh` | Cairo dep |
| 6 | Cairo | `build-cairo-wasm.sh` | 2D rendering |
| 7 | Boost | Headers only | Copy headers |
| 8 | Protobuf | `build-protobuf-wasm.sh` | If IPC needed |
| 9 | OpenCASCADE | `build-occ-wasm.sh` | 3D/STEP (large) |
| 10 | ngspice | `build-ngspice-wasm.sh` | Simulation |
| 11 | wxWidgets | Already done | `build-wxuniversal-wasm.sh` |
### Phase 3: WASM Compatibility Layer
Create `wasm/` directory with platform implementations.
#### kiplatform WASM (wasm/kiplatform/)
These files provide WASM implementations of KiCad's platform abstraction:
```cpp
// wasm/kiplatform/app.cpp
#include <kiplatform/app.h>
namespace KIPLATFORM::APP {
bool Init() { return true; }
wxString GetUserConfigPath() { return "/home/kicad"; }
wxString GetUserDataPath() { return "/home/kicad"; }
// ... etc
}
```
```cpp
// wasm/kiplatform/environment.cpp
#include <kiplatform/environment.h>
#include <emscripten.h>
namespace KIPLATFORM::ENV {
wxString GetEnv(const wxString& var) {
// Use localStorage via JS
char* val = (char*)EM_ASM_PTR({
var key = UTF8ToString($0);
var val = localStorage.getItem('env_' + key) || '';
return stringToNewUTF8(val);
}, var.c_str());
wxString result(val);
free(val);
return result;
}
}
```
#### libcontext Asyncify Fibers (wasm/libcontext/)
```cpp
// wasm/libcontext/fcontext_wasm.cpp
#ifdef __EMSCRIPTEN__
#include <emscripten/fiber.h>
// Provide same interface as libcontext but using Emscripten fibers
struct fcontext_transfer {
void* fctx;
void* data;
};
static emscripten_fiber_t main_fiber;
static bool main_fiber_initialized = false;
extern "C" {
fcontext_transfer jump_fcontext(void* to, void* vp);
void* make_fcontext(void* sp, size_t size, void (*fn)(fcontext_transfer));
}
// Implementation using emscripten_fiber_* APIs
#endif
```
### Phase 4: KiCad Build
**Script**: `scripts/build-pcbnew-wasm.sh`
```bash
#!/bin/bash
set -e
source "$(dirname "$0")/common/env.sh"
# Key: Override include paths to use our compatibility layers FIRST
WASM_INCLUDES="-I$PROJECT_ROOT/wasm/kiplatform"
WASM_INCLUDES="$WASM_INCLUDES -I$PROJECT_ROOT/wasm/libcontext"
WASM_INCLUDES="$WASM_INCLUDES -I$PROJECT_ROOT/wasm/shims"
WASM_INCLUDES="$WASM_INCLUDES -I$PROJECT_ROOT/stubs/include"
emcmake cmake ../kicad \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_CXX_FLAGS="$WASM_INCLUDES" \
\
# Use our CMake modules for stubs
-DCMAKE_MODULE_PATH="$PROJECT_ROOT/cmake" \
\
# Feature flags
-DKICAD_USE_OCC=ON \
-DKICAD_USE_NGSPICE=ON \
-DKICAD_USE_GIT=OFF \
-DKICAD_USE_CURL=OFF \
-DKICAD_SCRIPTING_WXPYTHON=OFF \
-DKICAD_IPC_API=OFF \
-DKICAD_BUILD_QA_TESTS=OFF \
-DKICAD_BUILD_I18N=OFF \
\
# Point to our builds
-DwxWidgets_CONFIG_EXECUTABLE="$WX_BUILD/wx-config" \
-DOCC_INCLUDE_DIR="$SYSROOT/include/opencascade" \
-DNGSPICE_LIBRARY="$SYSROOT/lib/libngspice.a"
emmake make pcbnew -j$(nproc)
```
### Phase 5: Testing
Follow existing patterns in `tests/`:
```
tests/wasm-app/standalone/pcbnew/
├── pcbnew_test.cpp # Minimal PCBnew test app
├── pcbnew_test.html # Generated
├── pcbnew_test.js # Generated
└── pcbnew_test.wasm # Generated
tests/e2e/
└── pcbnew.spec.ts # Playwright E2E tests
```
---
## Link Flags
```bash
# Core flags
-sALLOW_MEMORY_GROWTH=1
-sINITIAL_MEMORY=256MB
-sSTACK_SIZE=5MB
# Async/modal support
-sASYNCIFY=1
-sASYNCIFY_STACK_SIZE=16384
# OpenGL/WebGL
-sLEGACY_GL_EMULATION
-sMAX_WEBGL_VERSION=2
# Threading
-pthread
-sPROXY_TO_PTHREAD=1
-sPTHREAD_POOL_SIZE=navigator.hardwareConcurrency
-sOFFSCREENCANVAS_SUPPORT=1
```
Server headers for pthreads:
```
Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Opener-Policy: same-origin
```
---
## File Organization Summary
### What Goes Where
| Code Type | Location | Reason |
|-----------|----------|--------|
| WASM platform impl | `wasm/kiplatform/` | Don't touch kicad/ |
| Fiber implementation | `wasm/libcontext/` | Don't touch kicad/ |
| Header overrides | `wasm/shims/` | Include path override |
| Stub headers | `stubs/include/` | Already exists |
| Stub implementations | `stubs/src/` | Already exists |
| CMake finders | `cmake/` | Already exists |
| Build scripts | `scripts/` | Reproducible |
| Test apps | `tests/wasm-app/` | Existing pattern |
| E2E tests | `tests/e2e/` | Existing pattern |
### Files to Create
```
NEW:
├── wasm/
│ ├── CMakeLists.txt
│ ├── kiplatform/*.cpp (8 files)
│ ├── libcontext/fcontext_wasm.cpp
│ ├── shims/*.h
│ └── config/*.h
├── scripts/
│ ├── common/{env,functions,versions}.sh
│ ├── build-kicad-wasm.sh
│ ├── build-pcbnew-wasm.sh
│ └── build-deps/*.sh
├── stubs/src/nanodbc_stub.cpp
└── tests/
├── wasm-app/standalone/pcbnew/*
└── e2e/pcbnew.spec.ts
```
### Files NOT to Modify
```
DO NOT MODIFY:
├── kicad/ # Git submodule - use compatibility layers instead
└── wxwidgets/ # Git submodule - WASM platform already added
```
---
## Patches (Only If Absolutely Necessary)
If patches are unavoidable, they go in `patches/` with clear documentation:
```
patches/
├── kicad/
│ ├── 0001-*.patch
│ ├── checksums.sha256
│ └── README.md # Explain WHY each patch is needed
├── opencascade/
│ └── *.patch # OCC WASM compatibility
└── ngspice/
└── *.patch # Remove fork/exec
```
**Rule**: Before creating a patch, ask "Can this be done with a compatibility layer instead?"
---
## Success Criteria
### MVP (Milestone 1)
- [ ] All dependencies built for WASM
- [ ] PCBnew window opens in browser
- [ ] Menu bar and toolbars visible
- [ ] Can load .kicad_pcb file
- [ ] Board renders in WebGL
- [ ] Pan/zoom works
### Full Features (Milestone 2)
- [ ] All editing tools work
- [ ] Interactive router works (via Asyncify fibers)
- [ ] Zone filling works (via pthreads)
- [ ] DRC runs
- [ ] Save/export works
- [ ] 3D viewer works (OCC)
- [ ] Simulation works (ngspice)
---
## Quick Start
```bash
# 1. Set up Emscripten (already available via homebrew)
# Emscripten is at /opt/homebrew/bin/emcc
# 2. Build wxWidgets (if not already done)
./scripts/build-wxuniversal-wasm.sh
# 3. Build all dependencies
./scripts/deps/build-all-deps.sh --all
# 4. Build PCBnew for WASM
./scripts/build-pcbnew-wasm.sh
# 5. Test
cd tests && npm test
# 6. Serve (with COOP/COEP headers for SharedArrayBuffer)
cd build-wasm && npx serve -p 8080
```
---
## Implementation Status
### Created Files
#### Build Infrastructure (`scripts/common/`)
- `env.sh` - Environment setup (paths, Emscripten config)
- `functions.sh` - Utility functions (logging, downloads, stamps)
- `versions.sh` - Pinned dependency versions from KiCad
#### Dependency Build Scripts (`scripts/deps/`)
- `build-all-deps.sh` - Master dependency builder
- `build-zstd.sh` - Compression library
- `build-freetype.sh` - Font rendering
- `build-harfbuzz.sh` - Text shaping
- `build-pixman.sh` - Pixel manipulation
- `build-cairo.sh` - 2D graphics
- `build-glm.sh` - Math library (header-only)
- `build-protobuf.sh` - Protocol buffers
- `build-opencascade.sh` - 3D geometry/STEP
- `build-ngspice.sh` - SPICE simulation
#### WASM Compatibility Layer (`wasm/`)
- `CMakeLists.txt` - Main CMake configuration
- `README.md` - Documentation
- `kiplatform/CMakeLists.txt`
- `kiplatform/app.cpp` - Application lifecycle
- `kiplatform/drivers.cpp` - 3D mouse (stub)
- `kiplatform/environment.cpp` - Environment/paths with localStorage
- `kiplatform/io.cpp` - File I/O for virtual filesystem
- `kiplatform/policy.cpp` - Enterprise policies (stub)
- `kiplatform/secrets.cpp` - Credential storage via localStorage
- `kiplatform/sysinfo.cpp` - System info via WebGL/navigator
- `kiplatform/ui.cpp` - UI utilities (theme detection, etc.)
- `libcontext/CMakeLists.txt`
- `libcontext/libcontext_wasm.h` - Asyncify fiber header
- `libcontext/libcontext_wasm.cpp` - Asyncify fiber implementation
- `cmake/KiCadWASMConfig.cmake` - CMake config for WASM
- `cmake/FindKiplatformWASM.cmake` - Find module for kiplatform
- `cmake/FindLibcontextWASM.cmake` - Find module for libcontext
#### PCBnew Build
- `scripts/build-pcbnew-wasm.sh` - Main PCBnew build script
#### Tests (`tests/kicad/`)
- `pcbnew.html` - Test app HTML
- `pcbnew.spec.ts` - Playwright E2E tests
### Known Issues
1. **CMake Policy**: Zstd and some older libraries need `-DCMAKE_POLICY_VERSION_MINIMUM=3.5`
to work with modern CMake
2. **Shell Environment**: When sourcing scripts, use a fresh bash (`bash -c '...'`) to avoid
conflicts with existing environment variables

217
scripts/build-kicad-wasm.sh Executable file
View file

@ -0,0 +1,217 @@
#!/bin/bash
# Master build script for KiCad PCBnew WASM
# Usage: ./build-kicad-wasm.sh [OPTIONS]
#
# Options:
# --clean Full clean rebuild
# --deps-only Only build dependencies
# --skip-deps Skip dependency builds
# --with-occ Enable OpenCASCADE (3D/STEP support)
# --with-ngspice Enable ngspice (simulation)
# --with-pthread Enable pthreads (multi-threading)
# --debug Debug build with symbols
# -j N Parallel jobs (default: nproc)
# --help Show this help
set -e
# Source common environment
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/common/env.sh"
# Default options
DEPS_ONLY=0
SKIP_DEPS=0
WITH_OCC=0
WITH_NGSPICE=0
WITH_PTHREAD=0
# Show help
show_help() {
head -20 "$0" | tail -18 | sed 's/^# //' | sed 's/^#//'
exit 0
}
# Parse command line arguments
while [[ $# -gt 0 ]]; do
case $1 in
--help|-h)
show_help
;;
--clean)
CLEAN_BUILD=1
shift
;;
--deps-only)
DEPS_ONLY=1
shift
;;
--skip-deps)
SKIP_DEPS=1
shift
;;
--with-occ)
WITH_OCC=1
shift
;;
--with-ngspice)
WITH_NGSPICE=1
shift
;;
--with-pthread)
WITH_PTHREAD=1
shift
;;
--debug)
DEBUG_BUILD=1
shift
;;
-j)
PARALLEL_JOBS="$2"
shift 2
;;
-j*)
PARALLEL_JOBS="${1#-j}"
shift
;;
*)
log_error "Unknown option: $1"
show_help
;;
esac
done
# Set defaults if not set
CLEAN_BUILD=${CLEAN_BUILD:-0}
DEBUG_BUILD=${DEBUG_BUILD:-0}
PARALLEL_JOBS=${PARALLEL_JOBS:-$(get_nproc)}
# Export for sub-scripts
export CLEAN_BUILD DEBUG_BUILD PARALLEL_JOBS WITH_OCC WITH_NGSPICE WITH_PTHREAD
# Print configuration
echo "========================================"
echo "KiCad WASM Build"
echo "========================================"
echo " Clean build: $CLEAN_BUILD"
echo " Debug build: $DEBUG_BUILD"
echo " Deps only: $DEPS_ONLY"
echo " Skip deps: $SKIP_DEPS"
echo " With OCC: $WITH_OCC"
echo " With ngspice: $WITH_NGSPICE"
echo " With pthreads: $WITH_PTHREAD"
echo " Parallel jobs: $PARALLEL_JOBS"
echo "========================================"
# Verify prerequisites
setup_error_trap
verify_emscripten
verify_submodules "$PROJECT_ROOT"
# Clean if requested
if [ "$CLEAN_BUILD" = "1" ]; then
log_step "Cleaning previous builds..."
rm -rf "$STAMPS_DIR"/*
rm -rf "$DEPS_ROOT"/*
rm -rf "$BUILD_ROOT/kicad"
fi
# Build dependencies
if [ "$SKIP_DEPS" != "1" ]; then
log_step "Building dependencies..."
# Tier 1: Header-only (just verify they exist)
log_info "Tier 1: Header-only libraries (no build needed)"
# Tier 2: Simple C/C++ libraries from thirdparty
log_info "Tier 2: Simple libraries"
# These are built as part of KiCad's CMake, no separate build needed
# Tier 3: Emscripten ports
log_info "Tier 3: Emscripten ports (zlib, freetype via -sUSE_*)"
# These are linked via emscripten flags, no separate build
# Build Zstd
if [ -f "$SCRIPT_DIR/build-deps/build-zstd-wasm.sh" ]; then
build_if_needed "zstd" "$SCRIPT_DIR/build-deps/build-zstd-wasm.sh" "$CLEAN_BUILD"
else
log_warn "Zstd build script not found, skipping"
fi
# Build HarfBuzz
if [ -f "$SCRIPT_DIR/build-deps/build-harfbuzz-wasm.sh" ]; then
build_if_needed "harfbuzz" "$SCRIPT_DIR/build-deps/build-harfbuzz-wasm.sh" "$CLEAN_BUILD"
else
log_warn "HarfBuzz build script not found, skipping"
fi
# Build Pixman
if [ -f "$SCRIPT_DIR/build-deps/build-pixman-wasm.sh" ]; then
build_if_needed "pixman" "$SCRIPT_DIR/build-deps/build-pixman-wasm.sh" "$CLEAN_BUILD"
else
log_warn "Pixman build script not found, skipping"
fi
# Build Cairo
if [ -f "$SCRIPT_DIR/build-deps/build-cairo-wasm.sh" ]; then
build_if_needed "cairo" "$SCRIPT_DIR/build-deps/build-cairo-wasm.sh" "$CLEAN_BUILD"
else
log_warn "Cairo build script not found, skipping"
fi
# Tier 4: Complex dependencies
if [ "$WITH_OCC" = "1" ]; then
log_info "Tier 4: Building OpenCASCADE..."
if [ -f "$SCRIPT_DIR/build-deps/build-opencascade-wasm.sh" ]; then
build_if_needed "opencascade" "$SCRIPT_DIR/build-deps/build-opencascade-wasm.sh" "$CLEAN_BUILD"
else
log_warn "OpenCASCADE build script not found"
fi
fi
if [ "$WITH_NGSPICE" = "1" ]; then
log_info "Tier 4: Building ngspice..."
if [ -f "$SCRIPT_DIR/build-deps/build-ngspice-wasm.sh" ]; then
build_if_needed "ngspice" "$SCRIPT_DIR/build-deps/build-ngspice-wasm.sh" "$CLEAN_BUILD"
else
log_warn "ngspice build script not found"
fi
fi
# Verify wxWidgets is built
if [ ! -f "$WX_BUILD/wx-config" ]; then
log_step "Building wxWidgets..."
"$SCRIPT_DIR/build-wxuniversal-wasm.sh"
else
log_info "wxWidgets already built"
fi
log_info "Dependencies complete"
fi
if [ "$DEPS_ONLY" = "1" ]; then
log_info "Dependency build complete (--deps-only specified)"
exit 0
fi
# Build KiCad compatibility layer
log_step "Building WASM compatibility layer..."
if [ -f "$SCRIPT_DIR/build-wasm-compat.sh" ]; then
"$SCRIPT_DIR/build-wasm-compat.sh"
else
log_warn "WASM compatibility layer script not found, skipping"
fi
# Build KiCad PCBnew
log_step "Building KiCad PCBnew..."
if [ -f "$SCRIPT_DIR/build-pcbnew-wasm.sh" ]; then
"$SCRIPT_DIR/build-pcbnew-wasm.sh"
else
log_error "PCBnew build script not found: $SCRIPT_DIR/build-pcbnew-wasm.sh"
exit 1
fi
log_info "========================================"
log_info "Build complete!"
log_info "Output: $BUILD_ROOT/kicad/pcbnew/"
log_info "========================================"

110
scripts/build-pcbnew-wasm.sh Executable file
View file

@ -0,0 +1,110 @@
#!/bin/bash
# Build KiCad PCBnew for WebAssembly
# This builds the PCB editor as a standalone WASM application
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/common/env.sh"
source "${SCRIPT_DIR}/common/versions.sh"
source "${SCRIPT_DIR}/common/functions.sh"
KICAD_DIR="${PROJECT_ROOT}/kicad"
KICAD_BUILD="${BUILD_ROOT}/kicad"
KICAD_STAMP="${BUILD_ROOT}/stamps/kicad-pcbnew.stamp"
WASM_LAYER="${PROJECT_ROOT}/wasm"
# Parse arguments
CLEAN=0
SKIP_DEPS=0
DEBUG=0
for arg in "$@"; do
case $arg in
--clean)
CLEAN=1
;;
--skip-deps)
SKIP_DEPS=1
;;
--debug)
DEBUG=1
;;
esac
done
if [ $CLEAN -eq 1 ]; then
log_info "Cleaning KiCad build..."
rm -rf "${KICAD_BUILD}" "${KICAD_STAMP}"
fi
# Build dependencies first
if [ $SKIP_DEPS -eq 0 ]; then
log_info "Building dependencies..."
"${SCRIPT_DIR}/deps/build-all-deps.sh" --all
fi
# Check if already built
if check_stamp "${KICAD_STAMP}"; then
log_info "KiCad PCBnew already built, skipping..."
exit 0
fi
# Ensure wxWidgets is built
if [ ! -f "${SYSROOT}/lib/libwx_baseu-3.2.a" ]; then
log_error "wxWidgets not found. Please build wxWidgets first with:"
log_error " ./scripts/build-wxuniversal-wasm.sh"
exit 1
fi
log_info "Building KiCad PCBnew ${KICAD_VERSION} for WASM..."
# Set build type
if [ $DEBUG -eq 1 ]; then
BUILD_TYPE="Debug"
EXTRA_FLAGS="-g -O0"
else
BUILD_TYPE="Release"
EXTRA_FLAGS="-O2"
fi
mkdir -p "${KICAD_BUILD}"
cd "${KICAD_BUILD}"
# Configure KiCad with WASM-specific options
# We use CMAKE_MODULE_PATH to inject our compatibility layer
emcmake cmake "${KICAD_DIR}" \
-DCMAKE_BUILD_TYPE=${BUILD_TYPE} \
-DCMAKE_INSTALL_PREFIX="${SYSROOT}" \
-DCMAKE_MODULE_PATH="${WASM_LAYER}/cmake" \
-DCMAKE_CXX_FLAGS="${EXTRA_FLAGS} -pthread -DKICAD_USE_PLATFORM_WASM=1" \
-DCMAKE_C_FLAGS="${EXTRA_FLAGS} -pthread" \
-DCMAKE_EXE_LINKER_FLAGS="-pthread -sASYNCIFY=1 -sASYNCIFY_STACK_SIZE=65536 -sUSE_PTHREADS=1 -sPTHREAD_POOL_SIZE=4 -sALLOW_MEMORY_GROWTH=1 -sINITIAL_MEMORY=256MB -sMAXIMUM_MEMORY=4GB" \
-DCMAKE_PREFIX_PATH="${SYSROOT}" \
-DwxWidgets_CONFIG_EXECUTABLE="${SYSROOT}/bin/wx-config" \
\
-DKICAD_BUILD_QA_TESTS=OFF \
-DKICAD_SCRIPTING=OFF \
-DKICAD_SCRIPTING_PYTHON3=OFF \
-DKICAD_SCRIPTING_WXPYTHON=OFF \
-DKICAD_SPICE=ON \
-DKICAD_USE_OCC=ON \
-DKICAD_USE_EGL=OFF \
-DKICAD_USE_BUNDLED_GLEW=ON \
\
-DOCC_INCLUDE_DIR="${SYSROOT}/include/opencascade" \
-DOCC_LIBRARY_DIR="${SYSROOT}/lib" \
-DNGSPICE_INCLUDE_DIR="${SYSROOT}/include" \
-DNGSPICE_LIBRARY="${SYSROOT}/lib/libngspice.a" \
\
-DBUILD_GITHUB_PLUGIN=OFF \
-DKICAD_PCM=OFF \
\
-DKICAD_LIBRARY_DATA="${PROJECT_ROOT}/kicad-library"
# Build only pcbnew and its dependencies
# Note: We build specific targets to avoid building unnecessary components
emmake make -j${JOBS} pcbnew
create_stamp "${KICAD_STAMP}"
log_info "KiCad PCBnew build complete!"
log_info "Output: ${KICAD_BUILD}/pcbnew/pcbnew.js"

67
scripts/common/env.sh Executable file
View file

@ -0,0 +1,67 @@
#!/bin/bash
# Environment setup for KiCad WASM build scripts
# Get script and project directories using unique names to avoid conflicts
_KICAD_WASM_COMMON_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
_KICAD_WASM_SCRIPTS_DIR="$(dirname "$_KICAD_WASM_COMMON_DIR")"
_KICAD_WASM_PROJECT_ROOT="$(dirname "$_KICAD_WASM_SCRIPTS_DIR")"
# Export paths
export PROJECT_ROOT="$_KICAD_WASM_PROJECT_ROOT"
export SCRIPTS_DIR="$_KICAD_WASM_SCRIPTS_DIR"
export COMMON_DIR="$_KICAD_WASM_COMMON_DIR"
# Build output directories (inside the project)
export BUILD_ROOT="$PROJECT_ROOT/build-wasm"
export DEPS_ROOT="$BUILD_ROOT/deps"
export SYSROOT="$BUILD_ROOT/sysroot"
export STAMPS_DIR="$BUILD_ROOT/stamps"
# Source directories
export KICAD_SOURCE="$PROJECT_ROOT/kicad"
export WXWIDGETS_SOURCE="$PROJECT_ROOT/wxwidgets"
export WASM_COMPAT="$PROJECT_ROOT/wasm"
export STUBS_DIR="$PROJECT_ROOT/stubs"
export CMAKE_MODULES="$PROJECT_ROOT/cmake"
# wxWidgets build location
export WX_BUILD="$BUILD_ROOT/wxwidgets-universal"
# Emscripten settings
export EMSDK_QUIET=1
# Common compiler flags
export EMCC_CFLAGS="-fPIC -DEMSCRIPTEN"
export EMCC_CXXFLAGS="-fPIC -DEMSCRIPTEN -std=c++17"
# Common linker flags for WASM
export WASM_LDFLAGS="\
-sALLOW_MEMORY_GROWTH=1 \
-sINITIAL_MEMORY=256MB \
-sSTACK_SIZE=5MB \
-sASYNCIFY=1 \
-sASYNCIFY_STACK_SIZE=16384 \
-sLEGACY_GL_EMULATION \
-sMAX_WEBGL_VERSION=2"
# Threading flags (when enabled)
export PTHREAD_LDFLAGS="\
-pthread \
-sPROXY_TO_PTHREAD=1 \
-sPTHREAD_POOL_SIZE=navigator.hardwareConcurrency \
-sOFFSCREENCANVAS_SUPPORT=1"
# Create output directories
mkdir -p "$BUILD_ROOT" "$DEPS_ROOT" "$SYSROOT"/{lib,include,share} "$STAMPS_DIR"
# Source other common files
source "$COMMON_DIR/versions.sh"
source "$COMMON_DIR/functions.sh"
# Print environment info (only if not in quiet mode)
if [ "${QUIET:-0}" != "1" ]; then
echo "KiCad WASM Build Environment"
echo " Project root: $PROJECT_ROOT"
echo " Build root: $BUILD_ROOT"
echo " Sysroot: $SYSROOT"
fi

269
scripts/common/functions.sh Executable file
View file

@ -0,0 +1,269 @@
#!/bin/bash
# Common functions for KiCad WASM build scripts
# Exit on error by default
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Logging functions
log_info() {
echo -e "${GREEN}[INFO]${NC} $1"
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
log_step() {
echo -e "${BLUE}[STEP]${NC} $1"
}
# Error handler
on_error() {
local exit_code=$?
local line_no=$1
log_error "Build failed at line $line_no with exit code $exit_code"
# Save build log if available
if [ -n "$BUILD_LOG" ] && [ -f "$BUILD_LOG" ]; then
local log_file="$BUILD_ROOT/logs/build-$(date +%Y%m%d-%H%M%S).log"
mkdir -p "$(dirname "$log_file")"
cp "$BUILD_LOG" "$log_file"
log_error "Build log saved to: $log_file"
fi
exit $exit_code
}
# Set up error trap
setup_error_trap() {
trap 'on_error ${LINENO}' ERR
}
# 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"
exit 1
fi
log_info "Using Emscripten: $(emcc --version | head -1)"
}
# Verify submodules are initialized
verify_submodules() {
local project_root="$1"
if [ ! -f "$project_root/kicad/CMakeLists.txt" ]; then
log_error "KiCad submodule not initialized. Run: git submodule update --init --recursive"
exit 1
fi
if [ ! -f "$project_root/wxwidgets/configure" ]; then
log_error "wxWidgets submodule not initialized. Run: git submodule update --init --recursive"
exit 1
fi
log_info "Submodules verified"
}
# Download file with optional verification
download_file() {
local url="$1"
local dest="$2"
local expected_sha256="${3:-}"
if [ -f "$dest" ]; then
if [ -n "$expected_sha256" ]; then
local actual_sha256
actual_sha256=$(shasum -a 256 "$dest" 2>/dev/null | cut -d' ' -f1)
if [ "$actual_sha256" = "$expected_sha256" ]; then
log_info "$(basename "$dest") already downloaded and verified"
return 0
fi
log_warn "Checksum mismatch, re-downloading..."
else
log_info "$(basename "$dest") already exists"
return 0
fi
fi
log_info "Downloading $(basename "$dest")..."
mkdir -p "$(dirname "$dest")"
if ! curl -L -o "$dest" "$url"; then
log_error "Failed to download $url"
rm -f "$dest"
return 1
fi
if [ -n "$expected_sha256" ]; then
local actual_sha256
actual_sha256=$(shasum -a 256 "$dest" | cut -d' ' -f1)
if [ "$actual_sha256" != "$expected_sha256" ]; then
log_error "SHA256 mismatch for $dest"
log_error " Expected: $expected_sha256"
log_error " Actual: $actual_sha256"
rm -f "$dest"
return 1
fi
log_info "Checksum verified"
fi
return 0
}
# Extract archive (supports .tar.gz, .tar.xz, .zip)
extract_archive() {
local archive="$1"
local dest_dir="$2"
mkdir -p "$dest_dir"
case "$archive" in
*.tar.gz|*.tgz)
tar -xzf "$archive" -C "$dest_dir" --strip-components=1
;;
*.tar.xz)
tar -xJf "$archive" -C "$dest_dir" --strip-components=1
;;
*.zip)
unzip -q "$archive" -d "$dest_dir"
;;
*)
log_error "Unknown archive format: $archive"
return 1
;;
esac
log_info "Extracted to $dest_dir"
}
# Create build stamp file
# Can accept either a simple name like "zstd" or a full path like "/path/to/stamps/zstd.stamp"
create_stamp() {
local name="$1"
local stamp_file
if [[ "$name" == /* ]]; then
# Full path provided
stamp_file="$name"
else
# Just a name, use default stamps dir
local stamp_dir="${BUILD_ROOT:-$PROJECT_ROOT/build-wasm}/stamps"
mkdir -p "$stamp_dir"
stamp_file="$stamp_dir/$name.stamp"
fi
mkdir -p "$(dirname "$stamp_file")"
date +%s > "$stamp_file"
log_info "Created stamp: $(basename "$stamp_file" .stamp)"
}
# Check if build stamp exists
# Can accept either a simple name like "zstd" or a full path like "/path/to/stamps/zstd.stamp"
check_stamp() {
local name="$1"
local stamp_file
if [[ "$name" == /* ]]; then
# Full path provided
stamp_file="$name"
else
# Just a name, use default stamps dir
stamp_file="${BUILD_ROOT:-$PROJECT_ROOT/build-wasm}/stamps/$name.stamp"
fi
[ -f "$stamp_file" ]
}
# Remove build stamp
remove_stamp() {
local name="$1"
local stamp_file="${BUILD_ROOT:-$PROJECT_ROOT/build-wasm}/stamps/$name.stamp"
rm -f "$stamp_file"
}
# Build if stamp doesn't exist
build_if_needed() {
local name="$1"
local script="$2"
local force="${3:-0}"
if [ "$force" = "1" ]; then
remove_stamp "$name"
fi
if check_stamp "$name"; then
log_info "Skipping $name (already built)"
return 0
fi
log_step "Building $name..."
"$script"
}
# Get number of CPU cores for parallel builds
get_nproc() {
if command -v nproc &> /dev/null; then
nproc
elif command -v sysctl &> /dev/null; then
sysctl -n hw.ncpu
else
echo 4
fi
}
# Parse common command line arguments
parse_common_args() {
CLEAN_BUILD=0
DEBUG_BUILD=0
PARALLEL_JOBS=$(get_nproc)
while [[ $# -gt 0 ]]; do
case $1 in
--clean)
CLEAN_BUILD=1
shift
;;
--debug)
DEBUG_BUILD=1
shift
;;
-j)
PARALLEL_JOBS="$2"
shift 2
;;
-j*)
PARALLEL_JOBS="${1#-j}"
shift
;;
*)
# Unknown option, pass through
shift
;;
esac
done
export CLEAN_BUILD DEBUG_BUILD PARALLEL_JOBS
}
# Print build configuration
print_build_config() {
echo "========================================"
echo "Build Configuration:"
echo " Clean build: $CLEAN_BUILD"
echo " Debug build: $DEBUG_BUILD"
echo " Parallel jobs: $PARALLEL_JOBS"
echo " Build root: ${BUILD_ROOT:-not set}"
echo "========================================"
}

51
scripts/common/versions.sh Executable file
View file

@ -0,0 +1,51 @@
#!/bin/bash
# Dependency versions for KiCad WASM build
# These versions match KiCad 8.99 requirements from CMakeLists.txt and vcpkg.json
# KiCad submodule version
export KICAD_COMMIT="4bfed3f1746e8cc0a7d942767770f56fa28b393c"
export KICAD_VERSION="8.99"
# From vcpkg.json overrides (pinned versions)
export GLM_VERSION="0.9.9.8"
export NGSPICE_VERSION="45.2"
export PROTOBUF_VERSION="3.21.12"
export PYTHON_VERSION="3.11.5"
export WXWIDGETS_VERSION="3.3.1"
# From CMakeLists.txt minimum requirements
export WXWIDGETS_MIN="3.2.0"
export GLM_MIN="0.9.8"
export BOOST_MIN="1.71.0"
export FREETYPE_MIN="2.11.1"
export CAIRO_MIN="1.12"
export PIXMAN_MIN="0.30"
export LIBGIT2_MIN="1.5"
export OCC_MIN="7.5.0"
export SWIG_MIN="4.0"
# Recommended versions for WASM build
export OCC_VERSION="7.8.0"
export ZSTD_VERSION="1.5.5"
export FREETYPE_VERSION="2.13.2"
export HARFBUZZ_VERSION="8.3.0"
export CAIRO_VERSION="1.18.0"
export PIXMAN_VERSION="0.42.2"
export BOOST_VERSION="1.84.0"
# Download URLs
export ZSTD_URL="https://github.com/facebook/zstd/releases/download/v${ZSTD_VERSION}/zstd-${ZSTD_VERSION}.tar.gz"
export FREETYPE_URL="https://download.savannah.gnu.org/releases/freetype/freetype-${FREETYPE_VERSION}.tar.xz"
export HARFBUZZ_URL="https://github.com/harfbuzz/harfbuzz/releases/download/${HARFBUZZ_VERSION}/harfbuzz-${HARFBUZZ_VERSION}.tar.xz"
export CAIRO_URL="https://cairographics.org/releases/cairo-${CAIRO_VERSION}.tar.xz"
export PIXMAN_URL="https://cairographics.org/releases/pixman-${PIXMAN_VERSION}.tar.gz"
export OCC_URL="https://github.com/Open-Cascade-SAS/OCCT/archive/refs/tags/V${OCC_VERSION//./_}.tar.gz"
export NGSPICE_URL="https://sourceforge.net/projects/ngspice/files/ng-spice-rework/${NGSPICE_VERSION}/ngspice-${NGSPICE_VERSION}.tar.gz/download"
# SHA256 checksums (to be filled in after first successful download)
# export ZSTD_SHA256=""
# export HARFBUZZ_SHA256=""
# export CAIRO_SHA256=""
# export PIXMAN_SHA256=""
# export OCC_SHA256=""
# export NGSPICE_SHA256=""

71
scripts/deps/build-all-deps.sh Executable file
View file

@ -0,0 +1,71 @@
#!/bin/bash
# Build all KiCad dependencies for WebAssembly
# This script builds dependencies in the correct order
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/../common/env.sh"
source "${SCRIPT_DIR}/../common/functions.sh"
log_info "Building all KiCad dependencies for WASM..."
log_info "Using ${JOBS} parallel jobs"
# Parse arguments
CLEAN=""
WITH_OCC=0
WITH_NGSPICE=0
for arg in "$@"; do
case $arg in
--clean)
CLEAN="--clean"
;;
--with-occ)
WITH_OCC=1
;;
--with-ngspice)
WITH_NGSPICE=1
;;
--all)
WITH_OCC=1
WITH_NGSPICE=1
;;
esac
done
# Build dependencies in order
# 1. Header-only libraries (no dependencies)
log_info "=== Phase 1: Header-only libraries ==="
"${SCRIPT_DIR}/build-glm.sh" ${CLEAN}
# 2. Basic libraries (minimal dependencies)
log_info "=== Phase 2: Basic compression/serialization ==="
"${SCRIPT_DIR}/build-zstd.sh" ${CLEAN}
"${SCRIPT_DIR}/build-protobuf.sh" ${CLEAN}
# 3. Font rendering stack
log_info "=== Phase 3: Font rendering ==="
"${SCRIPT_DIR}/build-freetype.sh" ${CLEAN}
"${SCRIPT_DIR}/build-harfbuzz.sh" ${CLEAN}
# 4. Graphics stack (optional, for Cairo rendering)
log_info "=== Phase 4: Graphics libraries ==="
"${SCRIPT_DIR}/build-pixman.sh" ${CLEAN}
"${SCRIPT_DIR}/build-cairo.sh" ${CLEAN}
# 5. Optional heavy dependencies
if [ $WITH_OCC -eq 1 ]; then
log_info "=== Phase 5a: OpenCASCADE (3D/STEP support) ==="
"${SCRIPT_DIR}/build-opencascade.sh" ${CLEAN}
fi
if [ $WITH_NGSPICE -eq 1 ]; then
log_info "=== Phase 5b: ngspice (SPICE simulation) ==="
"${SCRIPT_DIR}/build-ngspice.sh" ${CLEAN}
fi
log_info "============================================"
log_info "All dependencies built successfully!"
log_info "Install prefix: ${SYSROOT}"
log_info "============================================"

111
scripts/deps/build-cairo.sh Executable file
View file

@ -0,0 +1,111 @@
#!/bin/bash
# Build Cairo for WebAssembly
# Cairo provides 2D graphics rendering
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/../common/env.sh"
source "${SCRIPT_DIR}/../common/versions.sh"
source "${SCRIPT_DIR}/../common/functions.sh"
# Cairo requires Pixman and FreeType
"${SCRIPT_DIR}/build-pixman.sh"
"${SCRIPT_DIR}/build-freetype.sh"
CAIRO_DIR="${DEPS_ROOT}/cairo-${CAIRO_VERSION}"
CAIRO_BUILD="${BUILD_ROOT}/deps/cairo"
CAIRO_STAMP="${BUILD_ROOT}/stamps/cairo.stamp"
# Parse arguments
CLEAN=0
for arg in "$@"; do
case $arg in
--clean)
CLEAN=1
shift
;;
esac
done
if [ $CLEAN -eq 1 ]; then
log_info "Cleaning Cairo build..."
rm -rf "${CAIRO_BUILD}" "${CAIRO_STAMP}"
fi
# Check if already built
if check_stamp "${CAIRO_STAMP}"; then
log_info "Cairo already built, skipping..."
exit 0
fi
# Download if needed
if [ ! -d "${CAIRO_DIR}" ]; then
log_info "Downloading Cairo ${CAIRO_VERSION}..."
mkdir -p "${DEPS_ROOT}"
cd "${DEPS_ROOT}"
CAIRO_URL="https://cairographics.org/releases/cairo-${CAIRO_VERSION}.tar.xz"
download_file "${CAIRO_URL}" "cairo-${CAIRO_VERSION}.tar.xz"
tar -xJf "cairo-${CAIRO_VERSION}.tar.xz"
rm "cairo-${CAIRO_VERSION}.tar.xz"
fi
log_info "Building Cairo ${CAIRO_VERSION} for WASM..."
mkdir -p "${CAIRO_BUILD}"
cd "${CAIRO_BUILD}"
# Cairo uses meson
cat > cross-file.txt << EOF
[binaries]
c = 'emcc'
cpp = 'em++'
ar = 'emar'
ranlib = 'emranlib'
strip = 'emstrip'
pkgconfig = 'pkg-config'
[host_machine]
system = 'emscripten'
cpu_family = 'wasm32'
cpu = 'wasm32'
endian = 'little'
[properties]
# Prevent finding system libraries when cross-compiling
sys_root = '${SYSROOT}'
pkg_config_libdir = '${SYSROOT}/lib/pkgconfig'
[built-in options]
c_args = ['-pthread', '-I${SYSROOT}/include', '-I${SYSROOT}/include/freetype2', '-I${SYSROOT}/include/pixman-1']
c_link_args = ['-pthread', '-L${SYSROOT}/lib']
pkg_config_path = '${SYSROOT}/lib/pkgconfig'
EOF
# Set PKG_CONFIG_PATH for dependency discovery
# Use LIBDIR to ONLY search our sysroot, preventing system lzo2 from being found
export PKG_CONFIG_LIBDIR="${SYSROOT}/lib/pkgconfig"
unset PKG_CONFIG_PATH
meson setup "${CAIRO_DIR}" \
--cross-file cross-file.txt \
--prefix="${SYSROOT}" \
--default-library=static \
-Dfontconfig=disabled \
-Dfreetype=enabled \
-Dglib=disabled \
-Dpng=disabled \
-Dxlib=disabled \
-Dxcb=disabled \
-Dzlib=enabled \
-Dtests=disabled \
-Dspectre=disabled \
-Dsymbol-lookup=disabled
JOBS=${JOBS:-$(sysctl -n hw.ncpu 2>/dev/null || nproc 2>/dev/null || echo 4)}
ninja -j${JOBS}
ninja install
create_stamp "${CAIRO_STAMP}"
log_info "Cairo build complete!"

70
scripts/deps/build-freetype.sh Executable file
View file

@ -0,0 +1,70 @@
#!/bin/bash
# Build FreeType for WebAssembly
# FreeType is required for font rendering
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/../common/env.sh"
source "${SCRIPT_DIR}/../common/versions.sh"
source "${SCRIPT_DIR}/../common/functions.sh"
FREETYPE_DIR="${DEPS_ROOT}/freetype-${FREETYPE_VERSION}"
FREETYPE_BUILD="${BUILD_ROOT}/deps/freetype"
FREETYPE_STAMP="${BUILD_ROOT}/stamps/freetype.stamp"
# Parse arguments
CLEAN=0
for arg in "$@"; do
case $arg in
--clean)
CLEAN=1
shift
;;
esac
done
if [ $CLEAN -eq 1 ]; then
log_info "Cleaning FreeType build..."
rm -rf "${FREETYPE_BUILD}" "${FREETYPE_STAMP}"
fi
# Check if already built
if check_stamp "${FREETYPE_STAMP}"; then
log_info "FreeType already built, skipping..."
exit 0
fi
# Download if needed
if [ ! -d "${FREETYPE_DIR}" ]; then
log_info "Downloading FreeType ${FREETYPE_VERSION}..."
mkdir -p "${DEPS_ROOT}"
cd "${DEPS_ROOT}"
FREETYPE_URL="https://download.savannah.gnu.org/releases/freetype/freetype-${FREETYPE_VERSION}.tar.xz"
download_file "${FREETYPE_URL}" "freetype-${FREETYPE_VERSION}.tar.xz"
tar -xJf "freetype-${FREETYPE_VERSION}.tar.xz"
rm "freetype-${FREETYPE_VERSION}.tar.xz"
fi
log_info "Building FreeType ${FREETYPE_VERSION} for WASM..."
mkdir -p "${FREETYPE_BUILD}"
cd "${FREETYPE_BUILD}"
emcmake cmake "${FREETYPE_DIR}" \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX="${SYSROOT}" \
-DCMAKE_POLICY_VERSION_MINIMUM=3.5 \
-DFT_DISABLE_BZIP2=ON \
-DFT_DISABLE_BROTLI=ON \
-DFT_DISABLE_HARFBUZZ=ON \
-DFT_DISABLE_PNG=ON \
-DFT_DISABLE_ZLIB=OFF \
-DBUILD_SHARED_LIBS=OFF
emmake make -j${JOBS}
emmake make install
create_stamp "${FREETYPE_STAMP}"
log_info "FreeType build complete!"

57
scripts/deps/build-glm.sh Executable file
View file

@ -0,0 +1,57 @@
#!/bin/bash
# Install GLM headers for WebAssembly
# GLM is a header-only math library
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/../common/env.sh"
source "${SCRIPT_DIR}/../common/versions.sh"
source "${SCRIPT_DIR}/../common/functions.sh"
GLM_DIR="${DEPS_ROOT}/glm-${GLM_VERSION}"
GLM_STAMP="${BUILD_ROOT}/stamps/glm.stamp"
# Parse arguments
CLEAN=0
for arg in "$@"; do
case $arg in
--clean)
CLEAN=1
shift
;;
esac
done
if [ $CLEAN -eq 1 ]; then
log_info "Cleaning GLM install..."
rm -rf "${GLM_STAMP}"
fi
# Check if already installed
if check_stamp "${GLM_STAMP}"; then
log_info "GLM already installed, skipping..."
exit 0
fi
# Download if needed
if [ ! -d "${GLM_DIR}" ]; then
log_info "Downloading GLM ${GLM_VERSION}..."
mkdir -p "${DEPS_ROOT}"
cd "${DEPS_ROOT}"
GLM_URL="https://github.com/g-truc/glm/releases/download/${GLM_VERSION}/glm-${GLM_VERSION}.zip"
download_file "${GLM_URL}" "glm-${GLM_VERSION}.zip"
unzip -q "glm-${GLM_VERSION}.zip"
mv glm "glm-${GLM_VERSION}"
rm "glm-${GLM_VERSION}.zip"
fi
log_info "Installing GLM ${GLM_VERSION} headers..."
# GLM is header-only, just copy headers
mkdir -p "${SYSROOT}/include"
cp -r "${GLM_DIR}/glm" "${SYSROOT}/include/"
create_stamp "${GLM_STAMP}"
log_info "GLM install complete!"

79
scripts/deps/build-harfbuzz.sh Executable file
View file

@ -0,0 +1,79 @@
#!/bin/bash
# Build HarfBuzz for WebAssembly
# HarfBuzz is used for text shaping in KiCad
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/../common/env.sh"
source "${SCRIPT_DIR}/../common/versions.sh"
source "${SCRIPT_DIR}/../common/functions.sh"
# HarfBuzz requires FreeType
"${SCRIPT_DIR}/build-freetype.sh"
HARFBUZZ_DIR="${DEPS_ROOT}/harfbuzz-${HARFBUZZ_VERSION}"
HARFBUZZ_BUILD="${BUILD_ROOT}/deps/harfbuzz"
HARFBUZZ_STAMP="${BUILD_ROOT}/stamps/harfbuzz.stamp"
# Parse arguments
CLEAN=0
for arg in "$@"; do
case $arg in
--clean)
CLEAN=1
shift
;;
esac
done
if [ $CLEAN -eq 1 ]; then
log_info "Cleaning HarfBuzz build..."
rm -rf "${HARFBUZZ_BUILD}" "${HARFBUZZ_STAMP}"
fi
# Check if already built
if check_stamp "${HARFBUZZ_STAMP}"; then
log_info "HarfBuzz already built, skipping..."
exit 0
fi
# Download if needed
if [ ! -d "${HARFBUZZ_DIR}" ]; then
log_info "Downloading HarfBuzz ${HARFBUZZ_VERSION}..."
mkdir -p "${DEPS_ROOT}"
cd "${DEPS_ROOT}"
HARFBUZZ_URL="https://github.com/harfbuzz/harfbuzz/releases/download/${HARFBUZZ_VERSION}/harfbuzz-${HARFBUZZ_VERSION}.tar.xz"
download_file "${HARFBUZZ_URL}" "harfbuzz-${HARFBUZZ_VERSION}.tar.xz"
tar -xJf "harfbuzz-${HARFBUZZ_VERSION}.tar.xz"
rm "harfbuzz-${HARFBUZZ_VERSION}.tar.xz"
fi
log_info "Building HarfBuzz ${HARFBUZZ_VERSION} for WASM..."
mkdir -p "${HARFBUZZ_BUILD}"
cd "${HARFBUZZ_BUILD}"
# HarfBuzz uses meson, but also has CMake support
emcmake cmake "${HARFBUZZ_DIR}" \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX="${SYSROOT}" \
-DCMAKE_POLICY_VERSION_MINIMUM=3.5 \
-DHB_HAVE_FREETYPE=ON \
-DHB_HAVE_GLIB=OFF \
-DHB_HAVE_ICU=OFF \
-DHB_HAVE_GOBJECT=OFF \
-DHB_HAVE_CAIRO=OFF \
-DHB_BUILD_UTILS=OFF \
-DHB_BUILD_SUBSET=OFF \
-DBUILD_SHARED_LIBS=OFF \
-DCMAKE_PREFIX_PATH="${SYSROOT}" \
-DFREETYPE_LIBRARY="${SYSROOT}/lib/libfreetype.a" \
-DFREETYPE_INCLUDE_DIRS="${SYSROOT}/include/freetype2"
emmake make -j${JOBS}
emmake make install
create_stamp "${HARFBUZZ_STAMP}"
log_info "HarfBuzz build complete!"

80
scripts/deps/build-ngspice.sh Executable file
View file

@ -0,0 +1,80 @@
#!/bin/bash
# Build ngspice for WebAssembly
# ngspice provides SPICE simulation for KiCad's Eeschema
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/../common/env.sh"
source "${SCRIPT_DIR}/../common/versions.sh"
source "${SCRIPT_DIR}/../common/functions.sh"
NGSPICE_DIR="${DEPS_ROOT}/ngspice-${NGSPICE_VERSION}"
NGSPICE_BUILD="${BUILD_ROOT}/deps/ngspice"
NGSPICE_STAMP="${BUILD_ROOT}/stamps/ngspice.stamp"
# Parse arguments
CLEAN=0
for arg in "$@"; do
case $arg in
--clean)
CLEAN=1
shift
;;
esac
done
if [ $CLEAN -eq 1 ]; then
log_info "Cleaning ngspice build..."
rm -rf "${NGSPICE_BUILD}" "${NGSPICE_STAMP}"
fi
# Check if already built
if check_stamp "${NGSPICE_STAMP}"; then
log_info "ngspice already built, skipping..."
exit 0
fi
# Download if needed
if [ ! -d "${NGSPICE_DIR}" ]; then
log_info "Downloading ngspice ${NGSPICE_VERSION}..."
mkdir -p "${DEPS_ROOT}"
cd "${DEPS_ROOT}"
NGSPICE_URL="https://sourceforge.net/projects/ngspice/files/ng-spice-rework/${NGSPICE_VERSION}/ngspice-${NGSPICE_VERSION}.tar.gz/download"
curl -L "${NGSPICE_URL}" -o "ngspice-${NGSPICE_VERSION}.tar.gz"
tar -xzf "ngspice-${NGSPICE_VERSION}.tar.gz"
rm "ngspice-${NGSPICE_VERSION}.tar.gz"
fi
log_info "Building ngspice ${NGSPICE_VERSION} for WASM..."
mkdir -p "${NGSPICE_BUILD}"
cd "${NGSPICE_BUILD}"
# ngspice uses autoconf
# First we need to configure for shared library mode (libngspice)
export CFLAGS="-pthread"
export CXXFLAGS="-pthread"
export LDFLAGS="-pthread"
# Configure ngspice as a shared library for KiCad integration
emconfigure "${NGSPICE_DIR}/configure" \
--prefix="${SYSROOT}" \
--host=wasm32-unknown-emscripten \
--build=$(uname -m)-linux-gnu \
--enable-shared \
--with-ngshared \
--disable-debug \
--disable-dependency-tracking \
--enable-cider \
--enable-xspice \
--without-x \
--without-readline \
--without-editline
emmake make -j${JOBS}
emmake make install
create_stamp "${NGSPICE_STAMP}"
log_info "ngspice build complete!"

View file

@ -0,0 +1,96 @@
#!/bin/bash
# Build OpenCASCADE Technology (OCCT) for WebAssembly
# OCCT provides 3D geometry kernel for STEP file import/export
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/../common/env.sh"
source "${SCRIPT_DIR}/../common/versions.sh"
source "${SCRIPT_DIR}/../common/functions.sh"
OCC_DIR="${DEPS_ROOT}/opencascade-${OCC_VERSION}"
OCC_BUILD="${BUILD_ROOT}/deps/opencascade"
OCC_STAMP="${BUILD_ROOT}/stamps/opencascade.stamp"
# Parse arguments
CLEAN=0
for arg in "$@"; do
case $arg in
--clean)
CLEAN=1
shift
;;
esac
done
if [ $CLEAN -eq 1 ]; then
log_info "Cleaning OpenCASCADE build..."
rm -rf "${OCC_BUILD}" "${OCC_STAMP}"
fi
# Check if already built
if check_stamp "${OCC_STAMP}"; then
log_info "OpenCASCADE already built, skipping..."
exit 0
fi
# Download if needed
if [ ! -d "${OCC_DIR}" ]; then
log_info "Downloading OpenCASCADE ${OCC_VERSION}..."
mkdir -p "${DEPS_ROOT}"
cd "${DEPS_ROOT}"
# OpenCASCADE releases are on GitHub
OCC_URL="https://github.com/Open-Cascade-SAS/OCCT/archive/refs/tags/V${OCC_VERSION//./_}.tar.gz"
download_file "${OCC_URL}" "opencascade-${OCC_VERSION}.tar.gz"
tar -xzf "opencascade-${OCC_VERSION}.tar.gz"
mv "OCCT-V${OCC_VERSION//./_}" "opencascade-${OCC_VERSION}"
rm "opencascade-${OCC_VERSION}.tar.gz"
fi
log_info "Building OpenCASCADE ${OCC_VERSION} for WASM..."
log_warn "This is a large library and may take a while..."
mkdir -p "${OCC_BUILD}"
cd "${OCC_BUILD}"
# OpenCASCADE build configuration for WASM
# Disable GUI, visualization that needs X11/OpenGL native
# Enable core geometry and data exchange modules only
emcmake cmake "${OCC_DIR}" \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX="${SYSROOT}" \
-DCMAKE_CXX_FLAGS="-pthread -O2" \
-DCMAKE_C_FLAGS="-pthread -O2" \
-DBUILD_LIBRARY_TYPE=Static \
-DBUILD_MODULE_ApplicationFramework=OFF \
-DBUILD_MODULE_Draw=OFF \
-DBUILD_MODULE_Visualization=OFF \
-DBUILD_MODULE_DETools=OFF \
-DBUILD_MODULE_FoundationClasses=ON \
-DBUILD_MODULE_ModelingData=ON \
-DBUILD_MODULE_ModelingAlgorithms=ON \
-DBUILD_MODULE_DataExchange=ON \
-DUSE_FREETYPE=OFF \
-DUSE_FREEIMAGE=OFF \
-DUSE_OPENVR=OFF \
-DUSE_FFMPEG=OFF \
-DUSE_TBB=OFF \
-DUSE_VTK=OFF \
-DUSE_TCL=OFF \
-DUSE_TK=OFF \
-DUSE_GLES2=OFF \
-DUSE_OPENGL=OFF \
-DUSE_D3D=OFF \
-DUSE_RAPIDJSON=OFF \
-DUSE_DRACO=OFF \
-DBUILD_DOC_Overview=OFF \
-DINSTALL_SAMPLES=OFF \
-DINSTALL_TEST_CASES=OFF
emmake make -j${JOBS}
emmake make install
create_stamp "${OCC_STAMP}"
log_info "OpenCASCADE build complete!"

88
scripts/deps/build-pixman.sh Executable file
View file

@ -0,0 +1,88 @@
#!/bin/bash
# Build Pixman for WebAssembly
# Pixman is required by Cairo for pixel manipulation
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/../common/env.sh"
source "${SCRIPT_DIR}/../common/versions.sh"
source "${SCRIPT_DIR}/../common/functions.sh"
PIXMAN_DIR="${DEPS_ROOT}/pixman-${PIXMAN_VERSION}"
PIXMAN_BUILD="${BUILD_ROOT}/deps/pixman"
PIXMAN_STAMP="${BUILD_ROOT}/stamps/pixman.stamp"
# Parse arguments
CLEAN=0
for arg in "$@"; do
case $arg in
--clean)
CLEAN=1
shift
;;
esac
done
if [ $CLEAN -eq 1 ]; then
log_info "Cleaning Pixman build..."
rm -rf "${PIXMAN_BUILD}" "${PIXMAN_STAMP}"
fi
# Check if already built
if check_stamp "${PIXMAN_STAMP}"; then
log_info "Pixman already built, skipping..."
exit 0
fi
# Download if needed
if [ ! -d "${PIXMAN_DIR}" ]; then
log_info "Downloading Pixman ${PIXMAN_VERSION}..."
mkdir -p "${DEPS_ROOT}"
cd "${DEPS_ROOT}"
PIXMAN_URL="https://cairographics.org/releases/pixman-${PIXMAN_VERSION}.tar.gz"
download_file "${PIXMAN_URL}" "pixman-${PIXMAN_VERSION}.tar.gz"
tar -xzf "pixman-${PIXMAN_VERSION}.tar.gz"
rm "pixman-${PIXMAN_VERSION}.tar.gz"
fi
log_info "Building Pixman ${PIXMAN_VERSION} for WASM..."
mkdir -p "${PIXMAN_BUILD}"
cd "${PIXMAN_BUILD}"
# Pixman uses meson
cat > cross-file.txt << EOF
[binaries]
c = 'emcc'
cpp = 'em++'
ar = 'emar'
ranlib = 'emranlib'
strip = 'emstrip'
[host_machine]
system = 'emscripten'
cpu_family = 'wasm32'
cpu = 'wasm32'
endian = 'little'
[built-in options]
c_args = ['-pthread']
c_link_args = ['-pthread']
EOF
meson setup "${PIXMAN_DIR}" \
--cross-file cross-file.txt \
--prefix="${SYSROOT}" \
--default-library=static \
-Dgtk=disabled \
-Dlibpng=disabled \
-Dtests=disabled
JOBS=${JOBS:-$(sysctl -n hw.ncpu 2>/dev/null || nproc 2>/dev/null || echo 4)}
ninja -j${JOBS}
ninja install
create_stamp "${PIXMAN_STAMP}"
log_info "Pixman build complete!"

69
scripts/deps/build-protobuf.sh Executable file
View file

@ -0,0 +1,69 @@
#!/bin/bash
# Build Protocol Buffers for WebAssembly
# Protobuf is used for IPC in KiCad
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/../common/env.sh"
source "${SCRIPT_DIR}/../common/versions.sh"
source "${SCRIPT_DIR}/../common/functions.sh"
PROTOBUF_DIR="${DEPS_ROOT}/protobuf-${PROTOBUF_VERSION}"
PROTOBUF_BUILD="${BUILD_ROOT}/deps/protobuf"
PROTOBUF_STAMP="${BUILD_ROOT}/stamps/protobuf.stamp"
# Parse arguments
CLEAN=0
for arg in "$@"; do
case $arg in
--clean)
CLEAN=1
shift
;;
esac
done
if [ $CLEAN -eq 1 ]; then
log_info "Cleaning Protobuf build..."
rm -rf "${PROTOBUF_BUILD}" "${PROTOBUF_STAMP}"
fi
# Check if already built
if check_stamp "${PROTOBUF_STAMP}"; then
log_info "Protobuf already built, skipping..."
exit 0
fi
# Download if needed
if [ ! -d "${PROTOBUF_DIR}" ]; then
log_info "Downloading Protobuf ${PROTOBUF_VERSION}..."
mkdir -p "${DEPS_ROOT}"
cd "${DEPS_ROOT}"
PROTOBUF_URL="https://github.com/protocolbuffers/protobuf/releases/download/v${PROTOBUF_VERSION}/protobuf-cpp-${PROTOBUF_VERSION}.tar.gz"
download_file "${PROTOBUF_URL}" "protobuf-${PROTOBUF_VERSION}.tar.gz"
tar -xzf "protobuf-${PROTOBUF_VERSION}.tar.gz"
rm "protobuf-${PROTOBUF_VERSION}.tar.gz"
fi
log_info "Building Protobuf ${PROTOBUF_VERSION} for WASM..."
mkdir -p "${PROTOBUF_BUILD}"
cd "${PROTOBUF_BUILD}"
emcmake cmake "${PROTOBUF_DIR}" \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX="${SYSROOT}" \
-DCMAKE_CXX_FLAGS="-pthread" \
-Dprotobuf_BUILD_TESTS=OFF \
-Dprotobuf_BUILD_EXAMPLES=OFF \
-Dprotobuf_BUILD_PROTOC_BINARIES=OFF \
-Dprotobuf_BUILD_SHARED_LIBS=OFF \
-Dprotobuf_WITH_ZLIB=OFF
emmake make -j${JOBS}
emmake make install
create_stamp "${PROTOBUF_STAMP}"
log_info "Protobuf build complete!"

72
scripts/deps/build-zstd.sh Executable file
View file

@ -0,0 +1,72 @@
#!/bin/bash
# Build Zstd for WebAssembly
# Zstd is used for compression in KiCad project files
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/../common/env.sh"
source "${SCRIPT_DIR}/../common/versions.sh"
source "${SCRIPT_DIR}/../common/functions.sh"
ZSTD_DIR="${DEPS_ROOT}/zstd-${ZSTD_VERSION}"
ZSTD_BUILD="${BUILD_ROOT}/deps/zstd"
ZSTD_STAMP="${BUILD_ROOT}/stamps/zstd.stamp"
# Parse arguments
CLEAN=0
for arg in "$@"; do
case $arg in
--clean)
CLEAN=1
shift
;;
esac
done
if [ $CLEAN -eq 1 ]; then
log_info "Cleaning Zstd build..."
rm -rf "${ZSTD_BUILD}" "${ZSTD_STAMP}"
fi
# Check if already built
if check_stamp "${ZSTD_STAMP}"; then
log_info "Zstd already built, skipping..."
exit 0
fi
# Download if needed
if [ ! -d "${ZSTD_DIR}" ]; then
log_info "Downloading Zstd ${ZSTD_VERSION}..."
mkdir -p "${DEPS_ROOT}"
cd "${DEPS_ROOT}"
ZSTD_URL="https://github.com/facebook/zstd/releases/download/v${ZSTD_VERSION}/zstd-${ZSTD_VERSION}.tar.gz"
download_file "${ZSTD_URL}" "zstd-${ZSTD_VERSION}.tar.gz"
tar -xzf "zstd-${ZSTD_VERSION}.tar.gz"
rm "zstd-${ZSTD_VERSION}.tar.gz"
fi
log_info "Building Zstd ${ZSTD_VERSION} for WASM..."
mkdir -p "${ZSTD_BUILD}"
cd "${ZSTD_BUILD}"
# Zstd uses CMake in build/cmake directory
emcmake cmake "${ZSTD_DIR}/build/cmake" \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX="${SYSROOT}" \
-DCMAKE_POLICY_VERSION_MINIMUM=3.5 \
-DZSTD_BUILD_PROGRAMS=OFF \
-DZSTD_BUILD_TESTS=OFF \
-DZSTD_BUILD_SHARED=OFF \
-DZSTD_BUILD_STATIC=ON \
-DZSTD_MULTITHREAD_SUPPORT=ON \
-DCMAKE_C_FLAGS="-pthread" \
-DCMAKE_CXX_FLAGS="-pthread"
emmake make -j${JOBS}
emmake make install
create_stamp "${ZSTD_STAMP}"
log_info "Zstd build complete!"

318
tests/kicad/pcbnew.html Normal file
View file

@ -0,0 +1,318 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>KiCad PCBnew - WASM</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #2c2c2c;
color: #fff;
height: 100vh;
display: flex;
flex-direction: column;
}
#toolbar {
background: #3c3c3c;
padding: 8px 16px;
display: flex;
align-items: center;
gap: 8px;
border-bottom: 1px solid #555;
}
#toolbar h1 {
font-size: 16px;
font-weight: 500;
flex: 1;
}
.btn {
padding: 6px 12px;
background: #5c5c5c;
border: none;
border-radius: 4px;
color: #fff;
cursor: pointer;
font-size: 13px;
}
.btn:hover {
background: #6c6c6c;
}
.btn:active {
background: #4c4c4c;
}
#main-container {
flex: 1;
display: flex;
overflow: hidden;
}
#canvas-container {
flex: 1;
position: relative;
background: #1a1a1a;
}
#canvas {
width: 100%;
height: 100%;
display: block;
}
#sidebar {
width: 300px;
background: #353535;
border-left: 1px solid #555;
display: flex;
flex-direction: column;
}
#status-bar {
background: #3c3c3c;
padding: 4px 16px;
font-size: 12px;
border-top: 1px solid #555;
display: flex;
justify-content: space-between;
}
#loading-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.9);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 1000;
}
#loading-overlay.hidden {
display: none;
}
.spinner {
width: 50px;
height: 50px;
border: 3px solid #333;
border-top-color: #007bff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
#loading-text {
margin-top: 20px;
font-size: 14px;
}
#progress-bar {
width: 300px;
height: 4px;
background: #333;
border-radius: 2px;
margin-top: 16px;
overflow: hidden;
}
#progress-fill {
height: 100%;
background: #007bff;
width: 0%;
transition: width 0.3s;
}
#log-panel {
flex: 1;
overflow: auto;
padding: 8px;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 11px;
background: #2a2a2a;
}
.log-entry {
padding: 2px 0;
border-bottom: 1px solid #3a3a3a;
}
.log-info { color: #a0a0a0; }
.log-warn { color: #ffcc00; }
.log-error { color: #ff6b6b; }
.log-success { color: #69db7c; }
</style>
</head>
<body>
<div id="loading-overlay">
<div class="spinner"></div>
<div id="loading-text">Loading PCBnew...</div>
<div id="progress-bar">
<div id="progress-fill"></div>
</div>
</div>
<div id="toolbar">
<h1>KiCad PCBnew (WASM)</h1>
<button class="btn" id="btn-new">New</button>
<button class="btn" id="btn-open">Open</button>
<button class="btn" id="btn-save">Save</button>
<button class="btn" id="btn-zoom-in">Zoom +</button>
<button class="btn" id="btn-zoom-out">Zoom -</button>
<button class="btn" id="btn-fit">Fit</button>
</div>
<div id="main-container">
<div id="canvas-container">
<canvas id="canvas"></canvas>
</div>
<div id="sidebar">
<div id="log-panel"></div>
</div>
</div>
<div id="status-bar">
<span id="status-coords">X: 0.00mm Y: 0.00mm</span>
<span id="status-info">Ready</span>
<span id="status-memory">Memory: --</span>
</div>
<script type="module">
// Logger
const logPanel = document.getElementById('log-panel');
const log = (msg, type = 'info') => {
const entry = document.createElement('div');
entry.className = `log-entry log-${type}`;
entry.textContent = `[${new Date().toLocaleTimeString()}] ${msg}`;
logPanel.appendChild(entry);
logPanel.scrollTop = logPanel.scrollHeight;
console.log(`[${type}] ${msg}`);
};
// Loading state
const loadingOverlay = document.getElementById('loading-overlay');
const loadingText = document.getElementById('loading-text');
const progressFill = document.getElementById('progress-fill');
const updateProgress = (percent, text) => {
progressFill.style.width = `${percent}%`;
if (text) loadingText.textContent = text;
};
// Memory monitoring
const updateMemory = () => {
if (performance.memory) {
const used = (performance.memory.usedJSHeapSize / 1024 / 1024).toFixed(1);
document.getElementById('status-memory').textContent = `Memory: ${used}MB`;
}
};
setInterval(updateMemory, 1000);
// Main initialization
async function init() {
try {
log('Starting PCBnew initialization...', 'info');
updateProgress(10, 'Loading WebAssembly module...');
// Import the PCBnew module
// In production, this would be the compiled KiCad WASM
const moduleUrl = './pcbnew.js';
log(`Loading module from ${moduleUrl}`, 'info');
updateProgress(30, 'Compiling WebAssembly...');
// For now, just show that we're ready to load
// The actual module loading would happen here
log('Note: PCBnew WASM module not yet built', 'warn');
log('Run: ./scripts/build-pcbnew-wasm.sh', 'warn');
updateProgress(100, 'Ready (module not loaded)');
setTimeout(() => {
loadingOverlay.classList.add('hidden');
log('UI ready', 'success');
}, 500);
} catch (error) {
log(`Failed to initialize: ${error.message}`, 'error');
loadingText.textContent = `Error: ${error.message}`;
}
}
// Button handlers
document.getElementById('btn-new').addEventListener('click', () => {
log('New project', 'info');
});
document.getElementById('btn-open').addEventListener('click', () => {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.kicad_pcb,.kicad_mod';
input.onchange = (e) => {
const file = e.target.files[0];
if (file) {
log(`Opening: ${file.name}`, 'info');
}
};
input.click();
});
document.getElementById('btn-save').addEventListener('click', () => {
log('Save project', 'info');
});
document.getElementById('btn-zoom-in').addEventListener('click', () => {
log('Zoom in', 'info');
});
document.getElementById('btn-zoom-out').addEventListener('click', () => {
log('Zoom out', 'info');
});
document.getElementById('btn-fit').addEventListener('click', () => {
log('Fit to view', 'info');
});
// Canvas mouse tracking
const canvas = document.getElementById('canvas');
const statusCoords = document.getElementById('status-coords');
canvas.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect();
const x = ((e.clientX - rect.left) * 0.1).toFixed(2);
const y = ((e.clientY - rect.top) * 0.1).toFixed(2);
statusCoords.textContent = `X: ${x}mm Y: ${y}mm`;
});
// Resize handling
function resizeCanvas() {
const container = document.getElementById('canvas-container');
canvas.width = container.clientWidth;
canvas.height = container.clientHeight;
}
window.addEventListener('resize', resizeCanvas);
resizeCanvas();
// Start initialization
init();
</script>
</body>
</html>

115
tests/kicad/pcbnew.spec.ts Normal file
View file

@ -0,0 +1,115 @@
import { test, expect } from '@playwright/test';
import path from 'path';
/**
* PCBnew WASM E2E Tests
*
* These tests verify that the KiCad PCBnew application runs correctly in the browser.
* They test the basic UI functionality and WASM module loading.
*/
test.describe('PCBnew WASM', () => {
test.beforeEach(async ({ page }) => {
// Navigate to PCBnew test page
const htmlPath = path.join(__dirname, 'pcbnew.html');
await page.goto(`file://${htmlPath}`);
// Wait for loading overlay to disappear
await page.waitForSelector('#loading-overlay.hidden', { timeout: 30000 });
});
test('should load the PCBnew interface', async ({ page }) => {
// Check that the toolbar is visible
await expect(page.locator('#toolbar h1')).toHaveText('KiCad PCBnew (WASM)');
// Check that toolbar buttons exist
await expect(page.locator('#btn-new')).toBeVisible();
await expect(page.locator('#btn-open')).toBeVisible();
await expect(page.locator('#btn-save')).toBeVisible();
await expect(page.locator('#btn-zoom-in')).toBeVisible();
await expect(page.locator('#btn-zoom-out')).toBeVisible();
await expect(page.locator('#btn-fit')).toBeVisible();
});
test('should have a working canvas', async ({ page }) => {
const canvas = page.locator('#canvas');
await expect(canvas).toBeVisible();
// Check that canvas has proper dimensions
const box = await canvas.boundingBox();
expect(box?.width).toBeGreaterThan(0);
expect(box?.height).toBeGreaterThan(0);
});
test('should track mouse coordinates', async ({ page }) => {
const canvas = page.locator('#canvas');
const statusCoords = page.locator('#status-coords');
// Move mouse over canvas
await canvas.hover({ position: { x: 100, y: 100 } });
// Check that coordinates are updated
const coordText = await statusCoords.textContent();
expect(coordText).toMatch(/X: \d+\.\d+mm Y: \d+\.\d+mm/);
});
test('should log UI interactions', async ({ page }) => {
const logPanel = page.locator('#log-panel');
// Click New button
await page.click('#btn-new');
// Check that action was logged
await expect(logPanel).toContainText('New project');
});
test('should have status bar with info', async ({ page }) => {
await expect(page.locator('#status-info')).toHaveText('Ready');
});
test('should show sidebar with log panel', async ({ page }) => {
const sidebar = page.locator('#sidebar');
const logPanel = page.locator('#log-panel');
await expect(sidebar).toBeVisible();
await expect(logPanel).toBeVisible();
});
// Screenshot test for visual regression
test('should match visual snapshot', async ({ page }) => {
await page.screenshot({
path: path.join(__dirname, 'screenshots', 'pcbnew-initial.png'),
fullPage: true
});
});
});
test.describe('PCBnew File Operations', () => {
test.beforeEach(async ({ page }) => {
const htmlPath = path.join(__dirname, 'pcbnew.html');
await page.goto(`file://${htmlPath}`);
await page.waitForSelector('#loading-overlay.hidden', { timeout: 30000 });
});
test('should have file open button that triggers file picker', async ({ page }) => {
const openBtn = page.locator('#btn-open');
await expect(openBtn).toBeVisible();
// Click should trigger file input (we can't fully test file selection in Playwright)
// but we can verify the button is clickable
await openBtn.click();
});
});
test.describe('PCBnew WASM Module', () => {
test('WASM module loading (placeholder)', async ({ page }) => {
// This test will be expanded once the WASM module is built
const htmlPath = path.join(__dirname, 'pcbnew.html');
await page.goto(`file://${htmlPath}`);
// Check for expected warning when module is not built
const logPanel = page.locator('#log-panel');
await page.waitForSelector('#loading-overlay.hidden', { timeout: 30000 });
await expect(logPanel).toContainText('Note: PCBnew WASM module not yet built');
});
});

26
wasm/CMakeLists.txt Normal file
View file

@ -0,0 +1,26 @@
# WASM Compatibility Layer for KiCad
# This directory contains WASM-specific implementations that replace
# platform-specific code in KiCad without modifying KiCad's source.
cmake_minimum_required(VERSION 3.22)
project(kicad_wasm_compat)
# Only build for Emscripten
if(NOT EMSCRIPTEN)
message(FATAL_ERROR "This project is only for Emscripten builds")
endif()
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Include directories
include_directories(
${CMAKE_CURRENT_SOURCE_DIR}/config
${CMAKE_CURRENT_SOURCE_DIR}/shims
)
# kiplatform WASM implementation
add_subdirectory(kiplatform)
# libcontext Asyncify fiber implementation
add_subdirectory(libcontext)

74
wasm/README.md Normal file
View file

@ -0,0 +1,74 @@
# WASM Compatibility Layer
This directory contains WASM-specific implementations that allow KiCad to run in a web browser **without modifying KiCad's source code**.
## Principle
Instead of patching KiCad source files, we:
1. Override include paths to use our headers first
2. Provide alternative implementations for platform-specific code
3. Link our libraries instead of system libraries
## Directory Structure
```
wasm/
├── CMakeLists.txt # Master CMake for compatibility layer
├── README.md # This file
├── kiplatform/ # Platform abstraction implementations
│ ├── CMakeLists.txt
│ ├── app.cpp # App lifecycle (paths, startup)
│ ├── drivers.cpp # GPU detection (returns "WebGL")
│ ├── environment.cpp # Environment variables (localStorage)
│ ├── io.cpp # File I/O (WASM virtual filesystem)
│ ├── policy.cpp # Security policy (always permissive)
│ ├── secrets.cpp # Credential storage (localStorage)
│ ├── sysinfo.cpp # System information
│ └── printing.cpp # Print support (browser print())
├── libcontext/ # Coroutine/fiber implementation
│ ├── CMakeLists.txt
│ └── fcontext_wasm.cpp # Emscripten Asyncify fibers
├── shims/ # Header overrides
│ └── *.h # Headers that redirect to our impls
└── config/ # Build configuration
├── kicad_wasm_config.h # Version and feature config
└── setup.h # Platform setup
```
## How It Works
### Include Path Override
When building KiCad for WASM, we add our directories first in the include path:
```bash
-I$PROJECT_ROOT/wasm/shims
-I$PROJECT_ROOT/wasm/kiplatform
-I$PROJECT_ROOT/stubs/include
```
This means when KiCad includes `<kiplatform/app.h>`, it finds our version first.
### Library Override
We build `libkiplatform_wasm.a` and link it instead of the native kiplatform:
```bash
-L$BUILD_ROOT/wasm -lkiplatform_wasm
```
### CMake Integration
The main KiCad build is configured to find our implementations:
```cmake
-DCMAKE_MODULE_PATH="$PROJECT_ROOT/cmake"
-DKIPLATFORM_LIBRARY="$BUILD_ROOT/wasm/libkiplatform_wasm.a"
```
## Adding New Implementations
1. Create the implementation file in the appropriate directory
2. Add it to the CMakeLists.txt
3. Ensure the header interface matches KiCad's expected interface
4. Test with a minimal build before full integration

View file

@ -0,0 +1,49 @@
# FindKiplatformWASM.cmake
# Find module for WASM implementation of kiplatform
#
# This module is used instead of the platform-specific kiplatform
# when building KiCad for WebAssembly.
#
# This module defines:
# KIPLATFORM_FOUND - System has kiplatform for WASM
# KIPLATFORM_INCLUDE_DIRS - Include directories
# KIPLATFORM_LIBRARIES - Libraries to link
# KIPLATFORM_SOURCES - Source files to compile
include(FindPackageHandleStandardArgs)
if(NOT EMSCRIPTEN)
message(FATAL_ERROR "FindKiplatformWASM is only for Emscripten builds")
endif()
# Get the directory containing this file
get_filename_component(_FIND_DIR "${CMAKE_CURRENT_LIST_FILE}" PATH)
get_filename_component(KIPLATFORM_WASM_DIR "${_FIND_DIR}/../kiplatform" ABSOLUTE)
# Define source files
set(KIPLATFORM_SOURCES
${KIPLATFORM_WASM_DIR}/app.cpp
${KIPLATFORM_WASM_DIR}/drivers.cpp
${KIPLATFORM_WASM_DIR}/environment.cpp
${KIPLATFORM_WASM_DIR}/io.cpp
${KIPLATFORM_WASM_DIR}/policy.cpp
${KIPLATFORM_WASM_DIR}/secrets.cpp
${KIPLATFORM_WASM_DIR}/sysinfo.cpp
${KIPLATFORM_WASM_DIR}/ui.cpp
)
# Include directory for platform headers
# We use KiCad's own headers, just provide our implementation
set(KIPLATFORM_INCLUDE_DIRS ${KIPLATFORM_WASM_DIR})
# No prebuilt library - sources are compiled directly into KiCad
set(KIPLATFORM_LIBRARIES "")
# Mark as found
set(KIPLATFORM_FOUND TRUE)
find_package_handle_standard_args(KiplatformWASM
REQUIRED_VARS KIPLATFORM_SOURCES KIPLATFORM_INCLUDE_DIRS
)
mark_as_advanced(KIPLATFORM_SOURCES KIPLATFORM_INCLUDE_DIRS KIPLATFORM_LIBRARIES)

View file

@ -0,0 +1,41 @@
# FindLibcontextWASM.cmake
# Find module for WASM implementation of libcontext using Asyncify
#
# This module replaces the platform-specific libcontext assembly
# when building KiCad for WebAssembly.
#
# This module defines:
# LIBCONTEXT_FOUND - System has libcontext for WASM
# LIBCONTEXT_INCLUDE_DIRS - Include directories
# LIBCONTEXT_LIBRARIES - Libraries to link (none for WASM)
# LIBCONTEXT_SOURCES - Source files to compile
include(FindPackageHandleStandardArgs)
if(NOT EMSCRIPTEN)
message(FATAL_ERROR "FindLibcontextWASM is only for Emscripten builds")
endif()
# Get the directory containing this file
get_filename_component(_FIND_DIR "${CMAKE_CURRENT_LIST_FILE}" PATH)
get_filename_component(LIBCONTEXT_WASM_DIR "${_FIND_DIR}/../libcontext" ABSOLUTE)
# Define source files
set(LIBCONTEXT_SOURCES
${LIBCONTEXT_WASM_DIR}/libcontext_wasm.cpp
)
# Include directories
set(LIBCONTEXT_INCLUDE_DIRS ${LIBCONTEXT_WASM_DIR})
# No prebuilt library
set(LIBCONTEXT_LIBRARIES "")
# Mark as found
set(LIBCONTEXT_FOUND TRUE)
find_package_handle_standard_args(LibcontextWASM
REQUIRED_VARS LIBCONTEXT_SOURCES LIBCONTEXT_INCLUDE_DIRS
)
mark_as_advanced(LIBCONTEXT_SOURCES LIBCONTEXT_INCLUDE_DIRS LIBCONTEXT_LIBRARIES)

View file

@ -0,0 +1,50 @@
# KiCad WASM Configuration
# This file configures KiCad to use WASM-specific implementations
if(NOT EMSCRIPTEN)
message(FATAL_ERROR "KiCadWASMConfig.cmake is only for Emscripten builds")
endif()
# Set the path to WASM compatibility layer
get_filename_component(KICAD_WASM_DIR "${CMAKE_CURRENT_LIST_DIR}/.." ABSOLUTE)
message(STATUS "KiCad WASM compatibility layer: ${KICAD_WASM_DIR}")
# Add include directories for WASM implementations
# These will be searched BEFORE KiCad's own includes, allowing us to
# provide our own implementations without modifying KiCad source
set(KICAD_WASM_INCLUDE_DIRS
${KICAD_WASM_DIR}/kiplatform
${KICAD_WASM_DIR}/libcontext
${KICAD_WASM_DIR}/config
${KICAD_WASM_DIR}/shims
)
# Define macro to indicate we're using WASM platform
add_definitions(-DKICAD_PLATFORM_WASM=1)
add_definitions(-D__WXUNIVERSAL__=1)
# Emscripten-specific compile options
add_compile_options(
-pthread
-sUSE_PTHREADS=1
)
# Emscripten-specific link options
add_link_options(
-pthread
-sUSE_PTHREADS=1
-sPTHREAD_POOL_SIZE=4
-sASYNCIFY=1
-sASYNCIFY_STACK_SIZE=65536
-sALLOW_MEMORY_GROWTH=1
-sINITIAL_MEMORY=256MB
-sMAXIMUM_MEMORY=4GB
-sMODULARIZE=1
-sEXPORT_ES6=1
-sENVIRONMENT=web,worker
)
# Export variables for use by parent CMakeLists
set(KICAD_WASM_INCLUDE_DIRS ${KICAD_WASM_INCLUDE_DIRS} PARENT_SCOPE)
set(KICAD_WASM_DIR ${KICAD_WASM_DIR} PARENT_SCOPE)

View file

@ -0,0 +1,25 @@
# WASM implementation of kiplatform
# Provides WASM-specific implementations of KiCad's platform abstraction layer
set(KIPLATFORM_WASM_SRCS
app.cpp
drivers.cpp
environment.cpp
io.cpp
policy.cpp
secrets.cpp
sysinfo.cpp
ui.cpp
)
add_library(kiplatform_wasm STATIC ${KIPLATFORM_WASM_SRCS})
target_include_directories(kiplatform_wasm PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/../config
${PROJECT_ROOT}/kicad/libs/kiplatform/include
)
# Link with wxWidgets
target_link_libraries(kiplatform_wasm
# wxWidgets will be added during main build
)

87
wasm/kiplatform/app.cpp Normal file
View file

@ -0,0 +1,87 @@
/*
* WASM implementation of kiplatform/app.h
* Provides application lifecycle functions for browser environment
*/
#include <kiplatform/app.h>
#include <wx/string.h>
#include <wx/window.h>
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#endif
namespace KIPLATFORM
{
namespace APP
{
bool Init()
{
// WASM initialization - nothing special needed
return true;
}
bool AttachConsole( bool aTryAlloc )
{
// Console is always available via browser dev tools
return true;
}
bool IsOperatingSystemUnsupported()
{
// WASM/browser is supported
return false;
}
bool RegisterApplicationRestart( const wxString& aCommandLine )
{
// No restart registration in browser
return false;
}
bool UnregisterApplicationRestart()
{
// No restart registration in browser
return true;
}
bool SupportsShutdownBlockReason()
{
// Browser handles page unload via beforeunload event
return false;
}
void SetShutdownBlockReason( wxWindow* aWindow, const wxString& aReason )
{
// Could implement via beforeunload event if needed
#ifdef __EMSCRIPTEN__
EM_ASM({
window.onbeforeunload = function() {
return UTF8ToString($0);
};
}, aReason.utf8_str().data());
#endif
}
void RemoveShutdownBlockReason( wxWindow* aWindow )
{
#ifdef __EMSCRIPTEN__
EM_ASM({
window.onbeforeunload = null;
});
#endif
}
void ForceTimerMessagesToBeCreatedIfNecessary()
{
// Not needed in browser - timers work differently
}
void AddDynamicLibrarySearchPath( const wxString& aPath )
{
// No dynamic library loading in WASM
}
} // namespace APP
} // namespace KIPLATFORM

View file

@ -0,0 +1,20 @@
/*
* WASM implementation of kiplatform/drivers.h
* 3D mouse drivers are not available in browser
*/
#include <kiplatform/drivers.h>
namespace KIPLATFORM
{
namespace DRIVERS
{
bool Valid3DConnexionDriverVersion()
{
// No 3D mouse support in browser
return false;
}
} // namespace DRIVERS
} // namespace KIPLATFORM

View file

@ -0,0 +1,120 @@
/*
* WASM implementation of kiplatform/environment.h
* Provides environment and path functions for browser environment
*/
#include <kiplatform/environment.h>
#include <wx/string.h>
#include <wx/window.h>
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#endif
namespace KIPLATFORM
{
namespace ENV
{
void Init()
{
// No special initialization needed for WASM
}
bool MoveToTrash( const wxString& aPath, wxString& aError )
{
// No trash/recycle bin in browser - just delete
aError = wxT( "Trash not available in browser environment" );
return false;
}
bool IsNetworkPath( const wxString& aPath )
{
// All paths in WASM virtual filesystem are local
return false;
}
wxString GetDocumentsPath()
{
// Use virtual filesystem path
return wxT( "/home/kicad/documents" );
}
wxString GetUserConfigPath()
{
// Use virtual filesystem path for config
return wxT( "/home/kicad/.config/kicad" );
}
wxString GetUserDataPath()
{
// Use virtual filesystem path for data
return wxT( "/home/kicad/.local/share/kicad" );
}
wxString GetUserLocalDataPath()
{
// Same as data path in WASM
return wxT( "/home/kicad/.local/share/kicad" );
}
wxString GetUserCachePath()
{
// Use virtual filesystem path for cache
return wxT( "/home/kicad/.cache/kicad" );
}
bool GetSystemProxyConfig( const wxString& aURL, PROXY_CONFIG& aCfg )
{
// No proxy configuration in browser - browser handles networking
return false;
}
bool VerifyFileSignature( const wxString& aPath )
{
// No code signing verification in WASM
return true;
}
wxString GetAppUserModelId()
{
// Windows-specific, return empty
return wxEmptyString;
}
void SetAppDetailsForWindow( wxWindow* aWindow, const wxString& aRelaunchCommand,
const wxString& aRelaunchDisplayName )
{
// Windows-specific, no-op
}
wxString GetCommandLineStr()
{
// No command line in browser
return wxEmptyString;
}
void AddToRecentDocs( const wxString& aPath )
{
// Could implement via localStorage if needed
#ifdef __EMSCRIPTEN__
EM_ASM({
try {
var recent = JSON.parse(localStorage.getItem('kicad_recent_docs') || '[]');
var path = UTF8ToString($0);
// Remove if already exists
recent = recent.filter(function(p) { return p !== path; });
// Add to front
recent.unshift(path);
// Keep only last 10
recent = recent.slice(0, 10);
localStorage.setItem('kicad_recent_docs', JSON.stringify(recent));
} catch(e) {
console.warn('Failed to save recent docs:', e);
}
}, aPath.utf8_str().data());
#endif
}
} // namespace ENV
} // namespace KIPLATFORM

49
wasm/kiplatform/io.cpp Normal file
View file

@ -0,0 +1,49 @@
/*
* WASM implementation of kiplatform/io.h
* Provides file I/O functions for WASM virtual filesystem
*/
#include <kiplatform/io.h>
#include <wx/string.h>
#include <wx/filename.h>
#include <stdio.h>
namespace KIPLATFORM
{
namespace IO
{
FILE* SeqFOpen( const wxString& aPath, const wxString& mode )
{
// WASM doesn't have special sequential read hints
// Just use standard fopen
return fopen( aPath.utf8_str(), mode.utf8_str() );
}
bool DuplicatePermissions( const wxString& aSrc, const wxString& aDest )
{
// WASM virtual filesystem doesn't have detailed permissions
return true;
}
bool MakeWriteable( const wxString& aFilePath )
{
// All files in WASM virtual filesystem are writeable
return true;
}
bool IsFileHidden( const wxString& aFileName )
{
// Check for Unix-style hidden files (starting with .)
wxFileName fn( aFileName );
wxString name = fn.GetFullName();
return !name.IsEmpty() && name[0] == '.';
}
void LongPathAdjustment( wxFileName& aFilename )
{
// No-op on non-Windows platforms
}
} // namespace IO
} // namespace KIPLATFORM

View file

@ -0,0 +1,27 @@
/*
* WASM implementation of kiplatform/policy.h
* Policies are not configured in browser environment
*/
#include <kiplatform/policy.h>
#include <wx/string.h>
namespace KIPLATFORM
{
namespace POLICY
{
PBOOL GetPolicyBool( const wxString& aKey )
{
// No enterprise policies in browser environment
return PBOOL::NOT_CONFIGURED;
}
std::uint32_t GetPolicyEnumUInt( const wxString& aKey )
{
// No enterprise policies in browser environment
return 0;
}
} // namespace POLICY
} // namespace KIPLATFORM

View file

@ -0,0 +1,76 @@
/*
* WASM implementation of kiplatform/secrets.h
* Uses browser localStorage for basic secret storage
* Note: localStorage is NOT secure for sensitive secrets, but provides
* the same API for KiCad functionality that expects secret storage
*/
#include <kiplatform/secrets.h>
#include <wx/string.h>
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#endif
namespace KIPLATFORM
{
namespace SECRETS
{
bool StoreSecret( const wxString& aService, const wxString& aKey, const wxString& aSecret )
{
#ifdef __EMSCRIPTEN__
int result = EM_ASM_INT({
try {
var service = UTF8ToString($0);
var key = UTF8ToString($1);
var secret = UTF8ToString($2);
var storageKey = 'kicad_secret_' + service + '_' + key;
localStorage.setItem(storageKey, secret);
return 1;
} catch(e) {
console.warn('Failed to store secret:', e);
return 0;
}
}, aService.utf8_str().data(), aKey.utf8_str().data(), aSecret.utf8_str().data());
return result == 1;
#else
return false;
#endif
}
bool GetSecret( const wxString& aService, const wxString& aKey, wxString& aSecret )
{
#ifdef __EMSCRIPTEN__
char* result = (char*)EM_ASM_PTR({
try {
var service = UTF8ToString($0);
var key = UTF8ToString($1);
var storageKey = 'kicad_secret_' + service + '_' + key;
var secret = localStorage.getItem(storageKey);
if (secret === null) {
return 0;
}
var len = lengthBytesUTF8(secret) + 1;
var buf = _malloc(len);
stringToUTF8(secret, buf, len);
return buf;
} catch(e) {
console.warn('Failed to get secret:', e);
return 0;
}
}, aService.utf8_str().data(), aKey.utf8_str().data());
if (result) {
aSecret = wxString::FromUTF8(result);
free(result);
return true;
}
return false;
#else
return false;
#endif
}
} // namespace SECRETS
} // namespace KIPLATFORM

123
wasm/kiplatform/sysinfo.cpp Normal file
View file

@ -0,0 +1,123 @@
/*
* WASM implementation of kiplatform/sysinfo.h
* Provides limited system info available in browser environment
*/
#include <kiplatform/sysinfo.h>
#include <string>
#include <vector>
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#endif
namespace KIPLATFORM
{
class SYSINFO_WASM : public SYSINFO_BASE
{
public:
bool GetGPUInfo( std::vector<GPU_INFO>& aGpuInfos ) override
{
#ifdef __EMSCRIPTEN__
GPU_INFO info;
// Try to get WebGL renderer info
char* renderer = (char*)EM_ASM_PTR({
try {
var canvas = document.createElement('canvas');
var gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
if (gl) {
var debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
if (debugInfo) {
var renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);
var len = lengthBytesUTF8(renderer) + 1;
var buf = _malloc(len);
stringToUTF8(renderer, buf, len);
return buf;
}
}
return 0;
} catch(e) {
return 0;
}
});
if (renderer) {
info.Name = renderer;
free(renderer);
} else {
info.Name = "WebGL Renderer";
}
info.MemorySize = 0; // Not available in browser
info.DriverVersion = "WebGL";
info.Manufacturer = "Browser";
aGpuInfos.push_back(info);
return true;
#else
return false;
#endif
}
bool GetCPUInfo( std::vector<CPU_INFO>& aCpuInfos ) override
{
#ifdef __EMSCRIPTEN__
CPU_INFO info;
// Get hardware concurrency (number of logical processors)
int cores = EM_ASM_INT({
return navigator.hardwareConcurrency || 1;
});
info.Name = "WebAssembly CPU";
info.Manufacturer = "Browser";
info.NumberCores = cores;
info.NumberLogical = cores;
aCpuInfos.push_back(info);
return true;
#else
return false;
#endif
}
bool GetMemoryInfo( MEMORY_INFO& aMemoryInfo ) override
{
#ifdef __EMSCRIPTEN__
// Try to get memory info from performance.memory (Chrome only)
// or estimate from WASM heap
long long heapSize = EM_ASM_INT({
if (performance && performance.memory) {
return performance.memory.jsHeapSizeLimit || 0;
}
// Return WASM memory size as fallback
return HEAPU8.length;
});
aMemoryInfo.Usage = 0;
aMemoryInfo.TotalPhysical = heapSize;
aMemoryInfo.FreePhysical = heapSize / 2; // Estimate
aMemoryInfo.TotalPaging = 0;
aMemoryInfo.FreePaging = 0;
aMemoryInfo.TotalVirtual = heapSize;
aMemoryInfo.FreeVirtual = heapSize / 2;
return true;
#else
return false;
#endif
}
};
} // namespace KIPLATFORM
// Global instance for the WASM sysinfo implementation
static KIPLATFORM::SYSINFO_WASM s_sysInfoWasm;
// Provide access to the sysinfo implementation
KIPLATFORM::SYSINFO_BASE* GetSysInfo()
{
return &s_sysInfoWasm;
}

198
wasm/kiplatform/ui.cpp Normal file
View file

@ -0,0 +1,198 @@
/*
* WASM implementation of kiplatform/ui.h
* Provides UI functions for browser environment
*/
#include <kiplatform/ui.h>
#include <wx/window.h>
#include <wx/choice.h>
#include <wx/toplevel.h>
#include <wx/nonownedwnd.h>
#include <wx/colour.h>
#include <wx/settings.h>
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#endif
namespace KIPLATFORM
{
namespace UI
{
bool IsDarkTheme()
{
#ifdef __EMSCRIPTEN__
// Check if browser prefers dark color scheme
int isDark = EM_ASM_INT({
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
return 1;
}
return 0;
});
return isDark == 1;
#else
return false;
#endif
}
wxColour GetDialogBGColour()
{
return wxSystemSettings::GetColour( wxSYS_COLOUR_BTNFACE );
}
void ForceFocus( wxWindow* aWindow )
{
if( aWindow )
aWindow->SetFocus();
}
bool IsWindowActive( wxWindow* aWindow )
{
if( !aWindow )
return false;
wxTopLevelWindow* tlw = dynamic_cast<wxTopLevelWindow*>( aWindow );
if( tlw )
return tlw->IsActive();
// For non-TLW, check if it has focus
return aWindow->HasFocus();
}
void ReparentModal( wxNonOwnedWindow* aWindow )
{
// No-op in browser - modal handling is different
}
void ReparentWindow( wxNonOwnedWindow* aWindow, wxTopLevelWindow* aParent )
{
// Reparenting not typically needed in browser
}
void FixupCancelButtonCmdKeyCollision( wxWindow* aWindow )
{
// Not needed in browser - no Cmd key
}
bool IsStockCursorOk( wxStockCursor aCursor )
{
// All stock cursors should work in browser via CSS
return true;
}
void LargeChoiceBoxHack( wxChoice* aChoice )
{
// Not needed in browser
}
void EllipsizeChoiceBox( wxChoice* aChoice )
{
// Browser handles text overflow via CSS
}
double GetPixelScaleFactor( const wxWindow* aWindow )
{
#ifdef __EMSCRIPTEN__
double scale = EM_ASM_DOUBLE({
return window.devicePixelRatio || 1.0;
});
return scale;
#else
if( aWindow )
return aWindow->GetContentScaleFactor();
return 1.0;
#endif
}
double GetContentScaleFactor( const wxWindow* aWindow )
{
return GetPixelScaleFactor( aWindow );
}
void GetInfoBarColours( wxColour& aFGColour, wxColour& aBGColour )
{
// Use standard info bar colors
if( IsDarkTheme() )
{
aFGColour = wxColour( 255, 255, 255 );
aBGColour = wxColour( 50, 50, 120 ); // Dark blue
}
else
{
aFGColour = wxColour( 0, 0, 0 );
aBGColour = wxColour( 200, 220, 255 ); // Light blue
}
}
wxSize GetUnobscuredSize( const wxWindow* aWindow )
{
if( aWindow )
return aWindow->GetClientSize();
return wxSize( 0, 0 );
}
void SetOverlayScrolling( const wxWindow* aWindow, bool overlay )
{
// Browser handles scrollbar styling via CSS
}
bool AllowIconsInMenus()
{
// Icons in menus are fine in browser
return true;
}
wxPoint GetMousePosition()
{
return wxGetMousePosition();
}
bool WarpPointer( wxWindow* aWindow, int aX, int aY )
{
// Pointer warping is restricted in browsers for security
// We can still call WarpPointer but it may not work
if( aWindow )
{
aWindow->WarpPointer( aX, aY );
return true;
}
return false;
}
void ImmControl( wxWindow* aWindow, bool aEnable )
{
// IME control not needed in browser - handled natively
}
void ImeNotifyCancelComposition( wxWindow* aWindow )
{
// IME control not needed in browser
}
bool InfiniteDragPrepareWindow( wxWindow* aWindow )
{
// Pointer lock API could be used for infinite drag
// but requires user gesture and permission
return false;
}
void InfiniteDragReleaseWindow()
{
// No-op
}
void EnsureVisible( wxWindow* aWindow )
{
// In browser, window is always visible (single page)
if( aWindow )
aWindow->Raise();
}
void SetFloatLevel( wxWindow* aWindow )
{
// No floating window levels in browser
}
} // namespace UI
} // namespace KIPLATFORM

View file

@ -0,0 +1,23 @@
# WASM implementation of libcontext using Emscripten Asyncify
# Provides fiber/coroutine support for KiCad's router
set(LIBCONTEXT_WASM_SRCS
libcontext_wasm.cpp
)
add_library(libcontext_wasm STATIC ${LIBCONTEXT_WASM_SRCS})
target_include_directories(libcontext_wasm PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}
)
# Asyncify is required for fiber support
# These flags must also be set on the final executable
target_compile_options(libcontext_wasm PRIVATE
-pthread
)
target_link_options(libcontext_wasm INTERFACE
-sASYNCIFY=1
-sASYNCIFY_STACK_SIZE=65536
)

View file

@ -0,0 +1,180 @@
/*
* WASM implementation of libcontext using Emscripten Asyncify
*
* Emscripten's Asyncify allows us to implement fiber/coroutine semantics
* by saving and restoring the WebAssembly call stack.
*
* Implementation strategy:
* - Each fiber context stores an Asyncify data buffer
* - jump_fcontext suspends current execution and resumes target
* - make_fcontext creates a new context with a function entry point
*
* Note: This requires the WASM module to be compiled with:
* -sASYNCIFY=1
* -sASYNCIFY_STACK_SIZE=65536 (or larger if needed)
*
* For more information on Asyncify:
* https://emscripten.org/docs/porting/asyncify.html
*/
#include <cstdlib>
#include <cstring>
#include <cstdint>
#include <cstdio>
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#include <emscripten/fiber.h>
#endif
// Match the API from libcontext.h
#define LIBCONTEXT_CALL_CONVENTION
#ifdef __cplusplus
extern "C" {
#endif
namespace libcontext
{
// Context structure that stores fiber state
struct fiber_context {
emscripten_fiber_t fiber;
void (*entry_func)(intptr_t);
intptr_t entry_arg;
bool initialized;
bool running;
// Stack for asyncify data
char asyncify_stack[65536];
// C stack
char* c_stack;
size_t c_stack_size;
};
// Current running context
static fiber_context* g_current_context = nullptr;
static fiber_context g_main_context;
static bool g_main_initialized = false;
// Fiber entry wrapper
static void fiber_entry_wrapper(void* arg)
{
fiber_context* ctx = (fiber_context*)arg;
if (ctx && ctx->entry_func) {
ctx->entry_func(ctx->entry_arg);
}
// If entry function returns, we need to handle it
// In original libcontext, this would call _exit
// For WASM, we'll just return to main context
}
typedef void* fcontext_t;
void LIBCONTEXT_CALL_CONVENTION release_fcontext( fcontext_t ctx )
{
#ifdef __EMSCRIPTEN__
if (ctx) {
fiber_context* fctx = (fiber_context*)ctx;
if (fctx->c_stack) {
free(fctx->c_stack);
}
free(fctx);
}
#endif
}
intptr_t LIBCONTEXT_CALL_CONVENTION jump_fcontext( fcontext_t* ofc, fcontext_t nfc,
intptr_t vp, bool preserve_fpu )
{
#ifdef __EMSCRIPTEN__
// Initialize main context if needed
if (!g_main_initialized) {
memset(&g_main_context, 0, sizeof(g_main_context));
emscripten_fiber_init_from_current_context(
&g_main_context.fiber,
g_main_context.asyncify_stack,
sizeof(g_main_context.asyncify_stack)
);
g_main_context.initialized = true;
g_main_context.running = true;
g_current_context = &g_main_context;
g_main_initialized = true;
}
fiber_context* old_ctx = g_current_context;
fiber_context* new_ctx = (fiber_context*)nfc;
if (!new_ctx || !new_ctx->initialized) {
fprintf(stderr, "jump_fcontext: invalid target context\n");
return 0;
}
// Store the argument in the new context
new_ctx->entry_arg = vp;
// Save the old context pointer
if (ofc) {
*ofc = (fcontext_t)old_ctx;
}
// Switch contexts
g_current_context = new_ctx;
old_ctx->running = false;
new_ctx->running = true;
// Perform the fiber switch
emscripten_fiber_swap(&old_ctx->fiber, &new_ctx->fiber);
// When we return here, we've been switched back to
// Return the value passed to us
return g_current_context->entry_arg;
#else
return 0;
#endif
}
fcontext_t LIBCONTEXT_CALL_CONVENTION make_fcontext( void* sp, size_t size,
void (* fn)( intptr_t ) )
{
#ifdef __EMSCRIPTEN__
// Allocate context structure
fiber_context* ctx = (fiber_context*)malloc(sizeof(fiber_context));
if (!ctx) {
return nullptr;
}
memset(ctx, 0, sizeof(fiber_context));
ctx->entry_func = fn;
ctx->entry_arg = 0;
ctx->c_stack = (char*)sp - size; // sp points to top of stack
ctx->c_stack_size = size;
// Initialize the fiber
// Note: sp is the TOP of the stack (highest address)
// The stack grows downward, so we need to pass the bottom
void* stack_bottom = (char*)sp - size;
emscripten_fiber_init(
&ctx->fiber,
fiber_entry_wrapper,
ctx, // User data for entry function
stack_bottom, // C stack (bottom)
size, // C stack size
ctx->asyncify_stack, // Asyncify stack
sizeof(ctx->asyncify_stack) // Asyncify stack size
);
ctx->initialized = true;
ctx->running = false;
return (fcontext_t)ctx;
#else
return nullptr;
#endif
}
}; // namespace libcontext
#ifdef __cplusplus
};
#endif

View file

@ -0,0 +1,52 @@
/*
* WASM implementation of libcontext using Emscripten Asyncify
*
* This header provides WASM-specific definitions for the libcontext API.
* The implementation uses Emscripten's Asyncify feature to implement
* fiber-like context switching in WebAssembly.
*
* Asyncify works by:
* 1. Instrumenting the WASM code to save/restore the call stack
* 2. Allowing execution to suspend at any point
* 3. Resuming execution from where it was suspended
*
* This is used by KiCad's router for coroutine-based routing.
*/
#ifndef LIBCONTEXT_WASM_H
#define LIBCONTEXT_WASM_H
#include <cstddef>
#include <cstdint>
// Define WASM platform
#define LIBCONTEXT_PLATFORM_wasm
#define LIBCONTEXT_CALL_CONVENTION
#ifdef __cplusplus
namespace libcontext {
#endif
typedef void* fcontext_t;
#ifdef __cplusplus
extern "C" {
#endif
void LIBCONTEXT_CALL_CONVENTION release_fcontext( fcontext_t ctx );
intptr_t LIBCONTEXT_CALL_CONVENTION jump_fcontext( fcontext_t* ofc, fcontext_t nfc,
intptr_t vp, bool preserve_fpu = true );
fcontext_t LIBCONTEXT_CALL_CONVENTION make_fcontext( void* sp, size_t size,
void (* fn)( intptr_t ) );
#ifdef __cplusplus
} // extern "C"
#endif
#ifdef __cplusplus
} // namespace libcontext
#endif
#endif // LIBCONTEXT_WASM_H