diff --git a/CLAUDE.md b/CLAUDE.md index 98ae503..3fd9a34 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 diff --git a/docs/07-KICAD-WASM-BUILD-PLAN.md b/docs/07-KICAD-WASM-BUILD-PLAN.md new file mode 100644 index 0000000..25cbe73 --- /dev/null +++ b/docs/07-KICAD-WASM-BUILD-PLAN.md @@ -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 + +namespace KIPLATFORM::APP { + bool Init() { return true; } + wxString GetUserConfigPath() { return "/home/kicad"; } + wxString GetUserDataPath() { return "/home/kicad"; } + // ... etc +} +``` + +```cpp +// wasm/kiplatform/environment.cpp +#include +#include + +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 + +// 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 diff --git a/scripts/build-kicad-wasm.sh b/scripts/build-kicad-wasm.sh new file mode 100755 index 0000000..c2b3354 --- /dev/null +++ b/scripts/build-kicad-wasm.sh @@ -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 "========================================" diff --git a/scripts/build-pcbnew-wasm.sh b/scripts/build-pcbnew-wasm.sh new file mode 100755 index 0000000..ac2f27e --- /dev/null +++ b/scripts/build-pcbnew-wasm.sh @@ -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" diff --git a/scripts/common/env.sh b/scripts/common/env.sh new file mode 100755 index 0000000..82fa03c --- /dev/null +++ b/scripts/common/env.sh @@ -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 diff --git a/scripts/common/functions.sh b/scripts/common/functions.sh new file mode 100755 index 0000000..ed66799 --- /dev/null +++ b/scripts/common/functions.sh @@ -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 "========================================" +} diff --git a/scripts/common/versions.sh b/scripts/common/versions.sh new file mode 100755 index 0000000..1cb9739 --- /dev/null +++ b/scripts/common/versions.sh @@ -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="" diff --git a/scripts/deps/build-all-deps.sh b/scripts/deps/build-all-deps.sh new file mode 100755 index 0000000..581ce10 --- /dev/null +++ b/scripts/deps/build-all-deps.sh @@ -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 "============================================" diff --git a/scripts/deps/build-cairo.sh b/scripts/deps/build-cairo.sh new file mode 100755 index 0000000..40a11e4 --- /dev/null +++ b/scripts/deps/build-cairo.sh @@ -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!" diff --git a/scripts/deps/build-freetype.sh b/scripts/deps/build-freetype.sh new file mode 100755 index 0000000..7c68fa4 --- /dev/null +++ b/scripts/deps/build-freetype.sh @@ -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!" diff --git a/scripts/deps/build-glm.sh b/scripts/deps/build-glm.sh new file mode 100755 index 0000000..3c5cb17 --- /dev/null +++ b/scripts/deps/build-glm.sh @@ -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!" diff --git a/scripts/deps/build-harfbuzz.sh b/scripts/deps/build-harfbuzz.sh new file mode 100755 index 0000000..fe26ff1 --- /dev/null +++ b/scripts/deps/build-harfbuzz.sh @@ -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!" diff --git a/scripts/deps/build-ngspice.sh b/scripts/deps/build-ngspice.sh new file mode 100755 index 0000000..436558b --- /dev/null +++ b/scripts/deps/build-ngspice.sh @@ -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!" diff --git a/scripts/deps/build-opencascade.sh b/scripts/deps/build-opencascade.sh new file mode 100755 index 0000000..45b97e4 --- /dev/null +++ b/scripts/deps/build-opencascade.sh @@ -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!" diff --git a/scripts/deps/build-pixman.sh b/scripts/deps/build-pixman.sh new file mode 100755 index 0000000..7f4c815 --- /dev/null +++ b/scripts/deps/build-pixman.sh @@ -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!" diff --git a/scripts/deps/build-protobuf.sh b/scripts/deps/build-protobuf.sh new file mode 100755 index 0000000..c19c5bd --- /dev/null +++ b/scripts/deps/build-protobuf.sh @@ -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!" diff --git a/scripts/deps/build-zstd.sh b/scripts/deps/build-zstd.sh new file mode 100755 index 0000000..828da64 --- /dev/null +++ b/scripts/deps/build-zstd.sh @@ -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!" diff --git a/tests/kicad/pcbnew.html b/tests/kicad/pcbnew.html new file mode 100644 index 0000000..88d12b9 --- /dev/null +++ b/tests/kicad/pcbnew.html @@ -0,0 +1,318 @@ + + + + + + KiCad PCBnew - WASM + + + +
+
+
Loading PCBnew...
+
+
+
+
+ +
+

KiCad PCBnew (WASM)

+ + + + + + +
+ +
+
+ +
+ +
+ +
+ X: 0.00mm Y: 0.00mm + Ready + Memory: -- +
+ + + + diff --git a/tests/kicad/pcbnew.spec.ts b/tests/kicad/pcbnew.spec.ts new file mode 100644 index 0000000..e0dc8f0 --- /dev/null +++ b/tests/kicad/pcbnew.spec.ts @@ -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'); + }); +}); diff --git a/wasm/CMakeLists.txt b/wasm/CMakeLists.txt new file mode 100644 index 0000000..13ee1bf --- /dev/null +++ b/wasm/CMakeLists.txt @@ -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) diff --git a/wasm/README.md b/wasm/README.md new file mode 100644 index 0000000..e9c3e26 --- /dev/null +++ b/wasm/README.md @@ -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 ``, 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 diff --git a/wasm/cmake/FindKiplatformWASM.cmake b/wasm/cmake/FindKiplatformWASM.cmake new file mode 100644 index 0000000..416c249 --- /dev/null +++ b/wasm/cmake/FindKiplatformWASM.cmake @@ -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) diff --git a/wasm/cmake/FindLibcontextWASM.cmake b/wasm/cmake/FindLibcontextWASM.cmake new file mode 100644 index 0000000..945463c --- /dev/null +++ b/wasm/cmake/FindLibcontextWASM.cmake @@ -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) diff --git a/wasm/cmake/KiCadWASMConfig.cmake b/wasm/cmake/KiCadWASMConfig.cmake new file mode 100644 index 0000000..bcb85b5 --- /dev/null +++ b/wasm/cmake/KiCadWASMConfig.cmake @@ -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) diff --git a/wasm/kiplatform/CMakeLists.txt b/wasm/kiplatform/CMakeLists.txt new file mode 100644 index 0000000..31b8e98 --- /dev/null +++ b/wasm/kiplatform/CMakeLists.txt @@ -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 +) diff --git a/wasm/kiplatform/app.cpp b/wasm/kiplatform/app.cpp new file mode 100644 index 0000000..c473f27 --- /dev/null +++ b/wasm/kiplatform/app.cpp @@ -0,0 +1,87 @@ +/* + * WASM implementation of kiplatform/app.h + * Provides application lifecycle functions for browser environment + */ + +#include +#include +#include + +#ifdef __EMSCRIPTEN__ +#include +#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 diff --git a/wasm/kiplatform/drivers.cpp b/wasm/kiplatform/drivers.cpp new file mode 100644 index 0000000..9fba76c --- /dev/null +++ b/wasm/kiplatform/drivers.cpp @@ -0,0 +1,20 @@ +/* + * WASM implementation of kiplatform/drivers.h + * 3D mouse drivers are not available in browser + */ + +#include + +namespace KIPLATFORM +{ +namespace DRIVERS +{ + +bool Valid3DConnexionDriverVersion() +{ + // No 3D mouse support in browser + return false; +} + +} // namespace DRIVERS +} // namespace KIPLATFORM diff --git a/wasm/kiplatform/environment.cpp b/wasm/kiplatform/environment.cpp new file mode 100644 index 0000000..13fb84f --- /dev/null +++ b/wasm/kiplatform/environment.cpp @@ -0,0 +1,120 @@ +/* + * WASM implementation of kiplatform/environment.h + * Provides environment and path functions for browser environment + */ + +#include +#include +#include + +#ifdef __EMSCRIPTEN__ +#include +#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 diff --git a/wasm/kiplatform/io.cpp b/wasm/kiplatform/io.cpp new file mode 100644 index 0000000..84b7d54 --- /dev/null +++ b/wasm/kiplatform/io.cpp @@ -0,0 +1,49 @@ +/* + * WASM implementation of kiplatform/io.h + * Provides file I/O functions for WASM virtual filesystem + */ + +#include +#include +#include +#include + +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 diff --git a/wasm/kiplatform/policy.cpp b/wasm/kiplatform/policy.cpp new file mode 100644 index 0000000..41296e1 --- /dev/null +++ b/wasm/kiplatform/policy.cpp @@ -0,0 +1,27 @@ +/* + * WASM implementation of kiplatform/policy.h + * Policies are not configured in browser environment + */ + +#include +#include + +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 diff --git a/wasm/kiplatform/secrets.cpp b/wasm/kiplatform/secrets.cpp new file mode 100644 index 0000000..46bb9df --- /dev/null +++ b/wasm/kiplatform/secrets.cpp @@ -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 +#include + +#ifdef __EMSCRIPTEN__ +#include +#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 diff --git a/wasm/kiplatform/sysinfo.cpp b/wasm/kiplatform/sysinfo.cpp new file mode 100644 index 0000000..5e56232 --- /dev/null +++ b/wasm/kiplatform/sysinfo.cpp @@ -0,0 +1,123 @@ +/* + * WASM implementation of kiplatform/sysinfo.h + * Provides limited system info available in browser environment + */ + +#include +#include +#include + +#ifdef __EMSCRIPTEN__ +#include +#endif + +namespace KIPLATFORM +{ + +class SYSINFO_WASM : public SYSINFO_BASE +{ +public: + bool GetGPUInfo( std::vector& 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& 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; +} diff --git a/wasm/kiplatform/ui.cpp b/wasm/kiplatform/ui.cpp new file mode 100644 index 0000000..c66d770 --- /dev/null +++ b/wasm/kiplatform/ui.cpp @@ -0,0 +1,198 @@ +/* + * WASM implementation of kiplatform/ui.h + * Provides UI functions for browser environment + */ + +#include +#include +#include +#include +#include +#include +#include + +#ifdef __EMSCRIPTEN__ +#include +#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( 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 diff --git a/wasm/libcontext/CMakeLists.txt b/wasm/libcontext/CMakeLists.txt new file mode 100644 index 0000000..a948121 --- /dev/null +++ b/wasm/libcontext/CMakeLists.txt @@ -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 +) diff --git a/wasm/libcontext/libcontext_wasm.cpp b/wasm/libcontext/libcontext_wasm.cpp new file mode 100644 index 0000000..acd45ef --- /dev/null +++ b/wasm/libcontext/libcontext_wasm.cpp @@ -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 +#include +#include +#include + +#ifdef __EMSCRIPTEN__ +#include +#include +#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 diff --git a/wasm/libcontext/libcontext_wasm.h b/wasm/libcontext/libcontext_wasm.h new file mode 100644 index 0000000..21b3b99 --- /dev/null +++ b/wasm/libcontext/libcontext_wasm.h @@ -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 +#include + +// 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